457 lines
14 KiB
TypeScript
457 lines
14 KiB
TypeScript
|
|
/**
|
||
|
|
* 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()
|
||
|
|
})
|