Remove dynamic form builder and complete RoundProject→roundId migration

Major cleanup and schema migration:
- Remove unused dynamic form builder system (ApplicationForm, ApplicationFormField, etc.)
- Complete migration from RoundProject junction table to direct Project.roundId
- Add sortOrder and entryNotificationType fields to Round model
- Add country field to User model for mentor matching
- Enhance onboarding with profile photo and country selection steps
- Fix all TypeScript errors related to roundProjects references
- Remove unused libraries (@radix-ui/react-toast, embla-carousel-react, vaul)

Files removed:
- admin/forms/* pages and related components
- admin/onboarding/* pages
- applicationForm.ts and onboarding.ts routers
- Dynamic form builder Prisma models and enums

Schema changes:
- Removed ApplicationForm, ApplicationFormField, OnboardingStep, ApplicationFormSubmission, SubmissionFile models
- Removed FormFieldType and SpecialFieldType enums
- Added Round.sortOrder, Round.entryNotificationType
- Added User.country

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-04 14:15:06 +01:00
parent 7bcd2ce6ca
commit 29827268b2
71 changed files with 2139 additions and 6609 deletions

View File

@@ -3,28 +3,28 @@ import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
async function check() {
const projects = await prisma.project.count()
console.log('Total projects:', projects)
const projectCount = await prisma.project.count()
console.log('Total projects:', projectCount)
const rounds = await prisma.round.findMany({
include: {
_count: { select: { roundProjects: true } }
_count: { select: { projects: true } }
}
})
for (const r of rounds) {
console.log(`Round: ${r.name} (id: ${r.id})`)
console.log(` Projects: ${r._count.roundProjects}`)
console.log(` Projects: ${r._count.projects}`)
}
// Check if projects have programId set
// Check sample projects with their round
const sampleProjects = await prisma.project.findMany({
select: { id: true, title: true, programId: true },
select: { id: true, title: true, roundId: true },
take: 5
})
console.log('\nSample projects:')
for (const p of sampleProjects) {
console.log(` ${p.title}: programId=${p.programId}`)
console.log(` ${p.title}: roundId=${p.roundId}`)
}
}

View File

@@ -10,18 +10,18 @@ async function cleanup() {
id: true,
name: true,
slug: true,
roundProjects: { select: { id: true, projectId: true, project: { select: { id: true, title: true } } } },
_count: { select: { roundProjects: true } }
projects: { select: { id: true, title: true } },
_count: { select: { projects: true } }
}
})
console.log(`Found ${rounds.length} rounds:`)
for (const round of rounds) {
console.log(`- ${round.name} (slug: ${round.slug}): ${round._count.roundProjects} projects`)
console.log(`- ${round.name} (slug: ${round.slug}): ${round._count.projects} projects`)
}
// Find rounds with 9 or fewer projects (dummy data)
const dummyRounds = rounds.filter(r => r._count.roundProjects <= 9)
const dummyRounds = rounds.filter(r => r._count.projects <= 9)
if (dummyRounds.length > 0) {
console.log(`\nDeleting ${dummyRounds.length} dummy round(s)...`)
@@ -29,15 +29,9 @@ async function cleanup() {
for (const round of dummyRounds) {
console.log(`\nProcessing: ${round.name}`)
const projectIds = round.roundProjects.map(rp => rp.projectId)
const projectIds = round.projects.map(p => p.id)
if (projectIds.length > 0) {
// Delete round-project associations first
const rpDeleted = await prisma.roundProject.deleteMany({
where: { roundId: round.id }
})
console.log(` Deleted ${rpDeleted.count} round-project associations`)
// Delete team members
const teamDeleted = await prisma.teamMember.deleteMany({
where: { projectId: { in: projectIds } }

View File

@@ -8,15 +8,15 @@ async function cleanup() {
// Find and delete the dummy round
const dummyRound = await prisma.round.findFirst({
where: { slug: 'round-1-2026' },
include: { roundProjects: { include: { project: true } } }
include: { projects: true }
})
if (dummyRound) {
console.log(`Found dummy round: ${dummyRound.name}`)
console.log(`Projects in round: ${dummyRound.roundProjects.length}`)
console.log(`Projects in round: ${dummyRound.projects.length}`)
// Get project IDs to delete
const projectIds = dummyRound.roundProjects.map(rp => rp.projectId)
const projectIds = dummyRound.projects.map(p => p.id)
// Delete team members for these projects
if (projectIds.length > 0) {
@@ -25,12 +25,6 @@ async function cleanup() {
})
console.log(`Deleted ${teamDeleted.count} team members`)
// Delete round-project associations
await prisma.roundProject.deleteMany({
where: { roundId: dummyRound.id }
})
console.log(`Deleted round-project associations`)
// Delete the projects
const projDeleted = await prisma.project.deleteMany({
where: { id: { in: projectIds } }

View File

@@ -147,35 +147,6 @@ enum PartnerType {
OTHER
}
enum FormFieldType {
TEXT
TEXTAREA
NUMBER
EMAIL
PHONE
URL
DATE
DATETIME
SELECT
MULTI_SELECT
RADIO
CHECKBOX
CHECKBOX_GROUP
FILE
FILE_MULTIPLE
SECTION
INSTRUCTIONS
}
enum SpecialFieldType {
TEAM_MEMBERS // Team member repeater
COMPETITION_CATEGORY // Business Concept vs Startup
OCEAN_ISSUE // Ocean issue dropdown
FILE_UPLOAD // File upload
GDPR_CONSENT // GDPR consent checkbox
COUNTRY_SELECT // Country dropdown
}
// =============================================================================
// APPLICANT SYSTEM ENUMS
// =============================================================================
@@ -225,6 +196,7 @@ model User {
status UserStatus @default(INVITED)
expertiseTags String[] @default([])
maxAssignments Int? // Per-round limit
country String? // User's home country (for mentor matching)
metadataJson Json? @db.JsonB
// Profile image
@@ -348,10 +320,8 @@ model Program {
// Relations
rounds Round[]
projects Project[]
learningResources LearningResource[]
partners Partner[]
applicationForms ApplicationForm[]
specialAwards SpecialAward[]
@@unique([name, year])
@@ -365,7 +335,10 @@ model Round {
slug String? @unique // URL-friendly identifier for public submissions
status RoundStatus @default(DRAFT)
roundType RoundType @default(EVALUATION)
sortOrder Int @default(0) // Progression order within program
sortOrder Int @default(0) // Display order within program
// Entry notification settings
entryNotificationType String? // Type of notification to send when project enters round
// Submission window (for applicant portal)
submissionDeadline DateTime? // Deadline for project submissions
@@ -385,15 +358,12 @@ model Round {
requiredReviews Int @default(3) // Min evaluations per project
settingsJson Json? @db.JsonB // Grace periods, visibility rules, etc.
// Notification sent to project team when they enter this round
entryNotificationType String? // e.g., "ADVANCED_SEMIFINAL", "ADVANCED_FINAL"
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
program Program @relation(fields: [programId], references: [id], onDelete: Cascade)
roundProjects RoundProject[]
projects Project[]
assignments Assignment[]
evaluationForms EvaluationForm[]
gracePeriods GracePeriod[]
@@ -401,7 +371,6 @@ model Round {
filteringRules FilteringRule[]
filteringResults FilteringResult[]
filteringJobs FilteringJob[]
applicationForm ApplicationForm?
@@index([programId])
@@index([status])
@@ -439,7 +408,8 @@ model EvaluationForm {
model Project {
id String @id @default(cuid())
programId String
roundId String
status ProjectStatus @default(SUBMITTED)
// Core fields
title String
@@ -493,8 +463,7 @@ model Project {
updatedAt DateTime @updatedAt
// Relations
program Program @relation(fields: [programId], references: [id], onDelete: Cascade)
roundProjects RoundProject[]
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
files ProjectFile[]
assignments Assignment[]
submittedBy User? @relation("ProjectSubmittedBy", fields: [submittedByUserId], references: [id], onDelete: SetNull)
@@ -504,8 +473,10 @@ model Project {
awardEligibilities AwardEligibility[]
awardVotes AwardVote[]
wonAwards SpecialAward[] @relation("AwardWinner")
projectTags ProjectTag[]
@@index([programId])
@@index([roundId])
@@index([status])
@@index([tags])
@@index([submissionSource])
@@index([submittedByUserId])
@@ -514,23 +485,6 @@ model Project {
@@index([country])
}
model RoundProject {
id String @id @default(cuid())
roundId String
projectId String
status ProjectStatus @default(SUBMITTED)
addedAt DateTime @default(now())
// Relations
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
@@unique([roundId, projectId])
@@index([roundId])
@@index([projectId])
@@index([status])
}
model ProjectFile {
id String @id @default(cuid())
projectId String
@@ -906,149 +860,6 @@ model Partner {
@@index([sortOrder])
}
// =============================================================================
// APPLICATION FORMS (Phase 2)
// =============================================================================
model ApplicationForm {
id String @id @default(cuid())
programId String? // null = global form
name String
description String? @db.Text
status String @default("DRAFT") // DRAFT, PUBLISHED, CLOSED
isPublic Boolean @default(false)
publicSlug String? @unique // /apply/ocean-challenge-2026
submissionLimit Int?
opensAt DateTime?
closesAt DateTime?
confirmationMessage String? @db.Text
// Round linking (for onboarding forms that create projects)
roundId String? @unique
// Email settings
sendConfirmationEmail Boolean @default(true)
sendTeamInviteEmails Boolean @default(true)
confirmationEmailSubject String?
confirmationEmailBody String? @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
program Program? @relation(fields: [programId], references: [id], onDelete: SetNull)
round Round? @relation(fields: [roundId], references: [id], onDelete: SetNull)
fields ApplicationFormField[]
steps OnboardingStep[]
submissions ApplicationFormSubmission[]
@@index([programId])
@@index([status])
@@index([isPublic])
@@index([roundId])
}
model ApplicationFormField {
id String @id @default(cuid())
formId String
stepId String? // Which step this field belongs to (for onboarding)
fieldType FormFieldType
name String // Internal name (e.g., "project_title")
label String // Display label (e.g., "Project Title")
description String? @db.Text
placeholder String?
required Boolean @default(false)
minLength Int?
maxLength Int?
minValue Float? // For NUMBER type
maxValue Float? // For NUMBER type
optionsJson Json? @db.JsonB // For select/radio: [{ value, label }]
conditionJson Json? @db.JsonB // Conditional logic: { fieldId, operator, value }
// Onboarding-specific fields
projectMapping String? // Maps to Project column: "title", "description", etc.
specialType SpecialFieldType? // Special handling for complex fields
sortOrder Int @default(0)
width String @default("full") // full, half
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
form ApplicationForm @relation(fields: [formId], references: [id], onDelete: Cascade)
step OnboardingStep? @relation(fields: [stepId], references: [id], onDelete: SetNull)
@@index([formId])
@@index([stepId])
@@index([sortOrder])
}
model OnboardingStep {
id String @id @default(cuid())
formId String
name String // Internal identifier (e.g., "category", "contact")
title String // Display title (e.g., "Category", "Contact Information")
description String? @db.Text
sortOrder Int @default(0)
isOptional Boolean @default(false)
conditionJson Json? @db.JsonB // Conditional visibility: { fieldId, operator, value }
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
form ApplicationForm @relation(fields: [formId], references: [id], onDelete: Cascade)
fields ApplicationFormField[]
@@index([formId])
@@index([sortOrder])
}
model ApplicationFormSubmission {
id String @id @default(cuid())
formId String
email String?
name String?
dataJson Json @db.JsonB // Field values: { fieldName: value, ... }
status String @default("SUBMITTED") // SUBMITTED, REVIEWED, APPROVED, REJECTED
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
form ApplicationForm @relation(fields: [formId], references: [id], onDelete: Cascade)
files SubmissionFile[]
@@index([formId])
@@index([status])
@@index([email])
@@index([createdAt])
}
model SubmissionFile {
id String @id @default(cuid())
submissionId String
fieldName String
fileName String
mimeType String?
size Int?
bucket String
objectKey String
createdAt DateTime @default(now())
// Relations
submission ApplicationFormSubmission @relation(fields: [submissionId], references: [id], onDelete: Cascade)
@@index([submissionId])
@@unique([bucket, objectKey])
}
// =============================================================================
// EXPERTISE TAGS (Phase 2B)
// =============================================================================
@@ -1065,11 +876,32 @@ model ExpertiseTag {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
projectTags ProjectTag[]
@@index([category])
@@index([isActive])
@@index([sortOrder])
}
// Project-Tag relationship for AI tagging
model ProjectTag {
id String @id @default(cuid())
projectId String
tagId String
confidence Float @default(1.0) // AI confidence score 0-1
source String @default("AI") // "AI" or "MANUAL"
createdAt DateTime @default(now())
// Relations
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
tag ExpertiseTag @relation(fields: [tagId], references: [id], onDelete: Cascade)
@@unique([projectId, tagId])
@@index([projectId])
@@index([tagId])
}
// =============================================================================
// LIVE VOTING (Phase 2B)
// =============================================================================

View File

@@ -321,7 +321,7 @@ async function main() {
// Check if project already exists
const existingProject = await prisma.project.findFirst({
where: {
programId: program.id,
roundId: round.id,
OR: [
{ title: projectName },
{ submittedByEmail: email },
@@ -365,7 +365,7 @@ async function main() {
// Create project
const project = await prisma.project.create({
data: {
programId: program.id,
roundId: round.id,
title: projectName,
description: row['Comment ']?.trim() || null,
competitionCategory: mapCategory(row['Category']),
@@ -391,15 +391,6 @@ async function main() {
},
})
// Create round-project association
await prisma.roundProject.create({
data: {
roundId: round.id,
projectId: project.id,
status: 'SUBMITTED',
},
})
// Create team lead membership
await prisma.teamMember.create({
data: {
@@ -474,7 +465,7 @@ async function main() {
console.log('\nBackfilling missing country codes...\n')
let backfilled = 0
const nullCountryProjects = await prisma.project.findMany({
where: { programId: program.id, country: null },
where: { roundId: round.id, country: null },
select: { id: true, submittedByEmail: true, title: true },
})

View File

@@ -64,14 +64,13 @@ async function main() {
console.log(`Voting window: ${votingStart.toISOString()}${votingEnd.toISOString()}\n`)
// Get some projects to assign (via RoundProject)
const roundProjects = await prisma.roundProject.findMany({
// Get some projects to assign
const projects = await prisma.project.findMany({
where: { roundId: round.id },
take: 8,
orderBy: { addedAt: 'desc' },
select: { project: { select: { id: true, title: true } } },
orderBy: { createdAt: 'desc' },
select: { id: true, title: true },
})
const projects = roundProjects.map(rp => rp.project)
if (projects.length === 0) {
console.error('No projects found! Run seed-candidatures first.')

View File

@@ -1,270 +0,0 @@
/**
* Seed script for MOPC Onboarding Form (ESM version for production)
* Run with: node prisma/seed-mopc-onboarding.mjs
*/
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
const MOPC_FORM_CONFIG = {
name: 'MOPC Application 2026',
description: 'Monaco Ocean Protection Challenge application form',
publicSlug: 'mopc-2026',
status: 'PUBLISHED',
isPublic: true,
sendConfirmationEmail: true,
sendTeamInviteEmails: true,
confirmationEmailSubject: 'Application Received - Monaco Ocean Protection Challenge',
confirmationEmailBody: `Thank you for applying to the Monaco Ocean Protection Challenge 2026!
We have received your application and our team will review it carefully.
If you have any questions, please don't hesitate to reach out.
Good luck!
The MOPC Team`,
confirmationMessage: 'Thank you for your application! We have sent a confirmation email to the address you provided. Our team will review your submission and get back to you soon.',
}
const STEPS = [
{
name: 'category',
title: 'Competition Category',
description: 'Select your competition track',
sortOrder: 0,
isOptional: false,
fields: [
{
name: 'competitionCategory',
label: 'Which category best describes your project?',
fieldType: 'RADIO',
specialType: 'COMPETITION_CATEGORY',
required: true,
sortOrder: 0,
width: 'full',
projectMapping: 'competitionCategory',
description: 'Choose the category that best fits your stage of development',
optionsJson: [
{ value: 'STARTUP', label: 'Startup', description: 'You have an existing company or registered business entity' },
{ value: 'BUSINESS_CONCEPT', label: 'Business Concept', description: 'You are a student, graduate, or have an idea not yet incorporated' },
],
},
],
},
{
name: 'contact',
title: 'Contact Information',
description: 'Tell us how to reach you',
sortOrder: 1,
isOptional: false,
fields: [
{ name: 'contactName', label: 'Full Name', fieldType: 'TEXT', required: true, sortOrder: 0, width: 'half', placeholder: 'Enter your full name' },
{ name: 'contactEmail', label: 'Email Address', fieldType: 'EMAIL', required: true, sortOrder: 1, width: 'half', placeholder: 'your.email@example.com', description: 'We will use this email for all communications' },
{ name: 'contactPhone', label: 'Phone Number', fieldType: 'PHONE', required: true, sortOrder: 2, width: 'half', placeholder: '+1 (555) 123-4567' },
{ name: 'country', label: 'Country', fieldType: 'SELECT', specialType: 'COUNTRY_SELECT', required: true, sortOrder: 3, width: 'half', projectMapping: 'country' },
{ name: 'city', label: 'City', fieldType: 'TEXT', required: false, sortOrder: 4, width: 'half', placeholder: 'City name' },
],
},
{
name: 'project',
title: 'Project Details',
description: 'Tell us about your ocean protection project',
sortOrder: 2,
isOptional: false,
fields: [
{ name: 'projectName', label: 'Project Name', fieldType: 'TEXT', required: true, sortOrder: 0, width: 'full', projectMapping: 'title', maxLength: 200, placeholder: 'Give your project a memorable name' },
{ name: 'teamName', label: 'Team / Company Name', fieldType: 'TEXT', required: false, sortOrder: 1, width: 'half', projectMapping: 'teamName', placeholder: 'Your team or company name' },
{ name: 'oceanIssue', label: 'Primary Ocean Issue', fieldType: 'SELECT', specialType: 'OCEAN_ISSUE', required: true, sortOrder: 2, width: 'half', projectMapping: 'oceanIssue', description: 'Select the primary ocean issue your project addresses' },
{ name: 'description', label: 'Project Description', fieldType: 'TEXTAREA', required: true, sortOrder: 3, width: 'full', projectMapping: 'description', minLength: 50, maxLength: 2000, placeholder: 'Describe your project, its goals, and how it will help protect the ocean...', description: 'Provide a clear description of your project (50-2000 characters)' },
{ name: 'websiteUrl', label: 'Website URL', fieldType: 'URL', required: false, sortOrder: 4, width: 'half', projectMapping: 'websiteUrl', placeholder: 'https://yourproject.com' },
],
},
{
name: 'team',
title: 'Team Members',
description: 'Add your team members (they will receive email invitations)',
sortOrder: 3,
isOptional: true,
fields: [
{ name: 'teamMembers', label: 'Team Members', fieldType: 'TEXT', specialType: 'TEAM_MEMBERS', required: false, sortOrder: 0, width: 'full', description: 'Add up to 5 team members. They will receive an invitation email to join your application.' },
],
},
{
name: 'additional',
title: 'Additional Details',
description: 'A few more questions about your project',
sortOrder: 4,
isOptional: false,
fields: [
{ name: 'institution', label: 'University / School', fieldType: 'TEXT', required: false, sortOrder: 0, width: 'half', projectMapping: 'institution', placeholder: 'Name of your institution', conditionJson: { field: 'competitionCategory', operator: 'equals', value: 'BUSINESS_CONCEPT' } },
{ name: 'startupCreatedDate', label: 'Startup Founded Date', fieldType: 'DATE', required: false, sortOrder: 1, width: 'half', description: 'When was your company founded?', conditionJson: { field: 'competitionCategory', operator: 'equals', value: 'STARTUP' } },
{ name: 'wantsMentorship', label: 'I am interested in receiving mentorship', fieldType: 'CHECKBOX', required: false, sortOrder: 2, width: 'full', projectMapping: 'wantsMentorship', description: 'Check this box if you would like to be paired with an expert mentor' },
{ name: 'referralSource', label: 'How did you hear about MOPC?', fieldType: 'SELECT', required: false, sortOrder: 3, width: 'half', optionsJson: [
{ value: 'social_media', label: 'Social Media' },
{ value: 'search_engine', label: 'Search Engine' },
{ value: 'word_of_mouth', label: 'Word of Mouth' },
{ value: 'university', label: 'University / School' },
{ value: 'partner', label: 'Partner Organization' },
{ value: 'media', label: 'News / Media' },
{ value: 'event', label: 'Event / Conference' },
{ value: 'other', label: 'Other' },
]},
],
},
{
name: 'review',
title: 'Review & Submit',
description: 'Review your application and accept the terms',
sortOrder: 5,
isOptional: false,
fields: [
{ name: 'instructions', label: 'Review Instructions', fieldType: 'INSTRUCTIONS', required: false, sortOrder: 0, width: 'full', description: 'Please review all the information you have provided. Once submitted, you will not be able to make changes.' },
{ name: 'gdprConsent', label: 'I consent to the processing of my personal data in accordance with the GDPR and the MOPC Privacy Policy', fieldType: 'CHECKBOX', specialType: 'GDPR_CONSENT', required: true, sortOrder: 1, width: 'full' },
{ name: 'termsAccepted', label: 'I have read and accept the Terms and Conditions of the Monaco Ocean Protection Challenge', fieldType: 'CHECKBOX', required: true, sortOrder: 2, width: 'full' },
],
},
]
async function main() {
console.log('Seeding MOPC onboarding form...')
// Check if form already exists
const existingForm = await prisma.applicationForm.findUnique({
where: { publicSlug: MOPC_FORM_CONFIG.publicSlug },
})
if (existingForm) {
console.log('Form with slug "mopc-2026" already exists. Updating...')
// Delete existing steps and fields to recreate them
await prisma.applicationFormField.deleteMany({ where: { formId: existingForm.id } })
await prisma.onboardingStep.deleteMany({ where: { formId: existingForm.id } })
// Update the form
await prisma.applicationForm.update({
where: { id: existingForm.id },
data: {
name: MOPC_FORM_CONFIG.name,
description: MOPC_FORM_CONFIG.description,
status: MOPC_FORM_CONFIG.status,
isPublic: MOPC_FORM_CONFIG.isPublic,
sendConfirmationEmail: MOPC_FORM_CONFIG.sendConfirmationEmail,
sendTeamInviteEmails: MOPC_FORM_CONFIG.sendTeamInviteEmails,
confirmationEmailSubject: MOPC_FORM_CONFIG.confirmationEmailSubject,
confirmationEmailBody: MOPC_FORM_CONFIG.confirmationEmailBody,
confirmationMessage: MOPC_FORM_CONFIG.confirmationMessage,
},
})
// Create steps and fields
for (const stepData of STEPS) {
const step = await prisma.onboardingStep.create({
data: {
formId: existingForm.id,
name: stepData.name,
title: stepData.title,
description: stepData.description,
sortOrder: stepData.sortOrder,
isOptional: stepData.isOptional,
},
})
for (const field of stepData.fields) {
await prisma.applicationFormField.create({
data: {
formId: existingForm.id,
stepId: step.id,
name: field.name,
label: field.label,
fieldType: field.fieldType,
specialType: field.specialType || null,
required: field.required,
sortOrder: field.sortOrder,
width: field.width,
description: field.description || null,
placeholder: field.placeholder || null,
projectMapping: field.projectMapping || null,
minLength: field.minLength || null,
maxLength: field.maxLength || null,
optionsJson: field.optionsJson || undefined,
conditionJson: field.conditionJson || undefined,
},
})
}
console.log(` - Created step: ${stepData.title} (${stepData.fields.length} fields)`)
}
console.log(`\nForm updated: ${existingForm.id}`)
return
}
// Create new form
const form = await prisma.applicationForm.create({
data: {
name: MOPC_FORM_CONFIG.name,
description: MOPC_FORM_CONFIG.description,
publicSlug: MOPC_FORM_CONFIG.publicSlug,
status: MOPC_FORM_CONFIG.status,
isPublic: MOPC_FORM_CONFIG.isPublic,
sendConfirmationEmail: MOPC_FORM_CONFIG.sendConfirmationEmail,
sendTeamInviteEmails: MOPC_FORM_CONFIG.sendTeamInviteEmails,
confirmationEmailSubject: MOPC_FORM_CONFIG.confirmationEmailSubject,
confirmationEmailBody: MOPC_FORM_CONFIG.confirmationEmailBody,
confirmationMessage: MOPC_FORM_CONFIG.confirmationMessage,
},
})
console.log(`Created form: ${form.id}`)
// Create steps and fields
for (const stepData of STEPS) {
const step = await prisma.onboardingStep.create({
data: {
formId: form.id,
name: stepData.name,
title: stepData.title,
description: stepData.description,
sortOrder: stepData.sortOrder,
isOptional: stepData.isOptional,
},
})
for (const field of stepData.fields) {
await prisma.applicationFormField.create({
data: {
formId: form.id,
stepId: step.id,
name: field.name,
label: field.label,
fieldType: field.fieldType,
specialType: field.specialType || null,
required: field.required,
sortOrder: field.sortOrder,
width: field.width,
description: field.description || null,
placeholder: field.placeholder || null,
projectMapping: field.projectMapping || null,
minLength: field.minLength || null,
maxLength: field.maxLength || null,
optionsJson: field.optionsJson || undefined,
conditionJson: field.conditionJson || undefined,
},
})
}
console.log(` - Created step: ${stepData.title} (${stepData.fields.length} fields)`)
}
console.log(`\nMOPC form seeded successfully!`)
console.log(`Form ID: ${form.id}`)
console.log(`Public URL: /apply/${form.publicSlug}`)
}
main()
.catch((e) => {
console.error(e)
process.exit(1)
})
.finally(async () => {
await prisma.$disconnect()
})

View File

@@ -1,456 +0,0 @@
/**
* Seed script for MOPC Onboarding Form
*
* This creates the application form configuration for the Monaco Ocean Protection Challenge.
* The form is accessible at /apply/mopc-2026
*
* Run with: npx tsx prisma/seed-mopc-onboarding.ts
*/
import { PrismaClient, FormFieldType, SpecialFieldType } from '@prisma/client'
const prisma = new PrismaClient()
const MOPC_FORM_CONFIG = {
name: 'MOPC Application 2026',
description: 'Monaco Ocean Protection Challenge application form',
publicSlug: 'mopc-2026',
status: 'PUBLISHED',
isPublic: true,
sendConfirmationEmail: true,
sendTeamInviteEmails: true,
confirmationEmailSubject: 'Application Received - Monaco Ocean Protection Challenge',
confirmationEmailBody: `Thank you for applying to the Monaco Ocean Protection Challenge 2026!
We have received your application and our team will review it carefully.
If you have any questions, please don't hesitate to reach out.
Good luck!
The MOPC Team`,
confirmationMessage: 'Thank you for your application! We have sent a confirmation email to the address you provided. Our team will review your submission and get back to you soon.',
}
const STEPS = [
{
name: 'category',
title: 'Competition Category',
description: 'Select your competition track',
sortOrder: 0,
isOptional: false,
fields: [
{
name: 'competitionCategory',
label: 'Which category best describes your project?',
fieldType: FormFieldType.RADIO,
specialType: SpecialFieldType.COMPETITION_CATEGORY,
required: true,
sortOrder: 0,
width: 'full',
projectMapping: 'competitionCategory',
description: 'Choose the category that best fits your stage of development',
optionsJson: [
{
value: 'STARTUP',
label: 'Startup',
description: 'You have an existing company or registered business entity',
},
{
value: 'BUSINESS_CONCEPT',
label: 'Business Concept',
description: 'You are a student, graduate, or have an idea not yet incorporated',
},
],
},
],
},
{
name: 'contact',
title: 'Contact Information',
description: 'Tell us how to reach you',
sortOrder: 1,
isOptional: false,
fields: [
{
name: 'contactName',
label: 'Full Name',
fieldType: FormFieldType.TEXT,
required: true,
sortOrder: 0,
width: 'half',
placeholder: 'Enter your full name',
},
{
name: 'contactEmail',
label: 'Email Address',
fieldType: FormFieldType.EMAIL,
required: true,
sortOrder: 1,
width: 'half',
placeholder: 'your.email@example.com',
description: 'We will use this email for all communications',
},
{
name: 'contactPhone',
label: 'Phone Number',
fieldType: FormFieldType.PHONE,
required: true,
sortOrder: 2,
width: 'half',
placeholder: '+1 (555) 123-4567',
},
{
name: 'country',
label: 'Country',
fieldType: FormFieldType.SELECT,
specialType: SpecialFieldType.COUNTRY_SELECT,
required: true,
sortOrder: 3,
width: 'half',
projectMapping: 'country',
},
{
name: 'city',
label: 'City',
fieldType: FormFieldType.TEXT,
required: false,
sortOrder: 4,
width: 'half',
placeholder: 'City name',
},
],
},
{
name: 'project',
title: 'Project Details',
description: 'Tell us about your ocean protection project',
sortOrder: 2,
isOptional: false,
fields: [
{
name: 'projectName',
label: 'Project Name',
fieldType: FormFieldType.TEXT,
required: true,
sortOrder: 0,
width: 'full',
projectMapping: 'title',
maxLength: 200,
placeholder: 'Give your project a memorable name',
},
{
name: 'teamName',
label: 'Team / Company Name',
fieldType: FormFieldType.TEXT,
required: false,
sortOrder: 1,
width: 'half',
projectMapping: 'teamName',
placeholder: 'Your team or company name',
},
{
name: 'oceanIssue',
label: 'Primary Ocean Issue',
fieldType: FormFieldType.SELECT,
specialType: SpecialFieldType.OCEAN_ISSUE,
required: true,
sortOrder: 2,
width: 'half',
projectMapping: 'oceanIssue',
description: 'Select the primary ocean issue your project addresses',
},
{
name: 'description',
label: 'Project Description',
fieldType: FormFieldType.TEXTAREA,
required: true,
sortOrder: 3,
width: 'full',
projectMapping: 'description',
minLength: 50,
maxLength: 2000,
placeholder: 'Describe your project, its goals, and how it will help protect the ocean...',
description: 'Provide a clear description of your project (50-2000 characters)',
},
{
name: 'websiteUrl',
label: 'Website URL',
fieldType: FormFieldType.URL,
required: false,
sortOrder: 4,
width: 'half',
projectMapping: 'websiteUrl',
placeholder: 'https://yourproject.com',
},
],
},
{
name: 'team',
title: 'Team Members',
description: 'Add your team members (they will receive email invitations)',
sortOrder: 3,
isOptional: true,
fields: [
{
name: 'teamMembers',
label: 'Team Members',
fieldType: FormFieldType.TEXT, // Will use specialType for rendering
specialType: SpecialFieldType.TEAM_MEMBERS,
required: false,
sortOrder: 0,
width: 'full',
description: 'Add up to 5 team members. They will receive an invitation email to join your application.',
},
],
},
{
name: 'additional',
title: 'Additional Details',
description: 'A few more questions about your project',
sortOrder: 4,
isOptional: false,
fields: [
{
name: 'institution',
label: 'University / School',
fieldType: FormFieldType.TEXT,
required: false,
sortOrder: 0,
width: 'half',
projectMapping: 'institution',
placeholder: 'Name of your institution',
conditionJson: {
field: 'competitionCategory',
operator: 'equals',
value: 'BUSINESS_CONCEPT',
},
},
{
name: 'startupCreatedDate',
label: 'Startup Founded Date',
fieldType: FormFieldType.DATE,
required: false,
sortOrder: 1,
width: 'half',
description: 'When was your company founded?',
conditionJson: {
field: 'competitionCategory',
operator: 'equals',
value: 'STARTUP',
},
},
{
name: 'wantsMentorship',
label: 'I am interested in receiving mentorship',
fieldType: FormFieldType.CHECKBOX,
required: false,
sortOrder: 2,
width: 'full',
projectMapping: 'wantsMentorship',
description: 'Check this box if you would like to be paired with an expert mentor',
},
{
name: 'referralSource',
label: 'How did you hear about MOPC?',
fieldType: FormFieldType.SELECT,
required: false,
sortOrder: 3,
width: 'half',
optionsJson: [
{ value: 'social_media', label: 'Social Media' },
{ value: 'search_engine', label: 'Search Engine' },
{ value: 'word_of_mouth', label: 'Word of Mouth' },
{ value: 'university', label: 'University / School' },
{ value: 'partner', label: 'Partner Organization' },
{ value: 'media', label: 'News / Media' },
{ value: 'event', label: 'Event / Conference' },
{ value: 'other', label: 'Other' },
],
},
],
},
{
name: 'review',
title: 'Review & Submit',
description: 'Review your application and accept the terms',
sortOrder: 5,
isOptional: false,
fields: [
{
name: 'instructions',
label: 'Review Instructions',
fieldType: FormFieldType.INSTRUCTIONS,
required: false,
sortOrder: 0,
width: 'full',
description: 'Please review all the information you have provided. Once submitted, you will not be able to make changes.',
},
{
name: 'gdprConsent',
label: 'I consent to the processing of my personal data in accordance with the GDPR and the MOPC Privacy Policy',
fieldType: FormFieldType.CHECKBOX,
specialType: SpecialFieldType.GDPR_CONSENT,
required: true,
sortOrder: 1,
width: 'full',
},
{
name: 'termsAccepted',
label: 'I have read and accept the Terms and Conditions of the Monaco Ocean Protection Challenge',
fieldType: FormFieldType.CHECKBOX,
required: true,
sortOrder: 2,
width: 'full',
},
],
},
]
async function main() {
console.log('Seeding MOPC onboarding form...')
// Check if form already exists
const existingForm = await prisma.applicationForm.findUnique({
where: { publicSlug: MOPC_FORM_CONFIG.publicSlug },
})
if (existingForm) {
console.log('Form with slug "mopc-2026" already exists. Updating...')
// Delete existing steps and fields to recreate them
await prisma.applicationFormField.deleteMany({
where: { formId: existingForm.id },
})
await prisma.onboardingStep.deleteMany({
where: { formId: existingForm.id },
})
// Update the form
await prisma.applicationForm.update({
where: { id: existingForm.id },
data: {
name: MOPC_FORM_CONFIG.name,
description: MOPC_FORM_CONFIG.description,
status: MOPC_FORM_CONFIG.status,
isPublic: MOPC_FORM_CONFIG.isPublic,
sendConfirmationEmail: MOPC_FORM_CONFIG.sendConfirmationEmail,
sendTeamInviteEmails: MOPC_FORM_CONFIG.sendTeamInviteEmails,
confirmationEmailSubject: MOPC_FORM_CONFIG.confirmationEmailSubject,
confirmationEmailBody: MOPC_FORM_CONFIG.confirmationEmailBody,
confirmationMessage: MOPC_FORM_CONFIG.confirmationMessage,
},
})
// Create steps and fields
for (const stepData of STEPS) {
const step = await prisma.onboardingStep.create({
data: {
formId: existingForm.id,
name: stepData.name,
title: stepData.title,
description: stepData.description,
sortOrder: stepData.sortOrder,
isOptional: stepData.isOptional,
},
})
for (const fieldData of stepData.fields) {
const field = fieldData as Record<string, unknown>
await prisma.applicationFormField.create({
data: {
formId: existingForm.id,
stepId: step.id,
name: field.name as string,
label: field.label as string,
fieldType: field.fieldType as FormFieldType,
specialType: (field.specialType as SpecialFieldType) || null,
required: field.required as boolean,
sortOrder: field.sortOrder as number,
width: field.width as string,
description: (field.description as string) || null,
placeholder: (field.placeholder as string) || null,
projectMapping: (field.projectMapping as string) || null,
minLength: (field.minLength as number) || null,
maxLength: (field.maxLength as number) || null,
optionsJson: field.optionsJson as object | undefined,
conditionJson: field.conditionJson as object | undefined,
},
})
}
console.log(` - Created step: ${stepData.title} (${stepData.fields.length} fields)`)
}
console.log(`\nForm updated: ${existingForm.id}`)
return
}
// Create new form
const form = await prisma.applicationForm.create({
data: {
name: MOPC_FORM_CONFIG.name,
description: MOPC_FORM_CONFIG.description,
publicSlug: MOPC_FORM_CONFIG.publicSlug,
status: MOPC_FORM_CONFIG.status,
isPublic: MOPC_FORM_CONFIG.isPublic,
sendConfirmationEmail: MOPC_FORM_CONFIG.sendConfirmationEmail,
sendTeamInviteEmails: MOPC_FORM_CONFIG.sendTeamInviteEmails,
confirmationEmailSubject: MOPC_FORM_CONFIG.confirmationEmailSubject,
confirmationEmailBody: MOPC_FORM_CONFIG.confirmationEmailBody,
confirmationMessage: MOPC_FORM_CONFIG.confirmationMessage,
},
})
console.log(`Created form: ${form.id}`)
// Create steps and fields
for (const stepData of STEPS) {
const step = await prisma.onboardingStep.create({
data: {
formId: form.id,
name: stepData.name,
title: stepData.title,
description: stepData.description,
sortOrder: stepData.sortOrder,
isOptional: stepData.isOptional,
},
})
for (const fieldData of stepData.fields) {
const field = fieldData as Record<string, unknown>
await prisma.applicationFormField.create({
data: {
formId: form.id,
stepId: step.id,
name: field.name as string,
label: field.label as string,
fieldType: field.fieldType as FormFieldType,
specialType: (field.specialType as SpecialFieldType) || null,
required: field.required as boolean,
sortOrder: field.sortOrder as number,
width: field.width as string,
description: (field.description as string) || null,
placeholder: (field.placeholder as string) || null,
projectMapping: (field.projectMapping as string) || null,
minLength: (field.minLength as number) || null,
maxLength: (field.maxLength as number) || null,
optionsJson: field.optionsJson as object | undefined,
conditionJson: field.conditionJson as object | undefined,
},
})
}
console.log(` - Created step: ${stepData.title} (${stepData.fields.length} fields)`)
}
console.log(`\nMOPC form seeded successfully!`)
console.log(`Form ID: ${form.id}`)
console.log(`Public URL: /apply/${form.publicSlug}`)
}
main()
.catch((e) => {
console.error(e)
process.exit(1)
})
.finally(async () => {
await prisma.$disconnect()
})