From 19f15b00b57871bcc1b589bf2ade166487fc817a Mon Sep 17 00:00:00 2001 From: Heryan Djaruma Date: Sat, 20 Jun 2026 22:35:08 +0800 Subject: [PATCH] add issue tab for missing --- app/applications/page.tsx | 161 ++++++++- config.ts | 2 + data/question.ts | 674 ++++++++++++++++++++++++++++++++++++++ types/application.ts | 136 ++++++++ 4 files changed, 966 insertions(+), 7 deletions(-) create mode 100644 data/question.ts create mode 100644 types/application.ts diff --git a/app/applications/page.tsx b/app/applications/page.tsx index 1ad3ca9..8d0d9a5 100644 --- a/app/applications/page.tsx +++ b/app/applications/page.tsx @@ -41,7 +41,7 @@ export default function Applications() { const [isSortDescending, setIsSortDescending] = useState(false); const [activeTab, setActiveTab] = useState<"evaluate" | "issues">("evaluate"); const [activeIssueType, setActiveIssueType] = useState< - "duplicates" | "oversize-team" + "duplicates" | "oversize-team" | "missing-fields" >("duplicates"); const [expandedGroups, setExpandedGroups] = useState>(new Set()); @@ -586,6 +586,62 @@ export default function Applications() { 0 ); + // Issue detection: missing required fields + const REQUIRED_FIELDS: { key: keyof CombinedApplicationData; label: string }[] = [ + { key: "firstName", label: "First Name" }, + { key: "lastName", label: "Last Name" }, + { key: "genderIdentity", label: "Gender Identity" }, + { key: "dateOfBirth", label: "Date of Birth" }, + { key: "nationality", label: "Nationality" }, + { key: "countryOfResidence", label: "Country of Residence" }, + { key: "preferredLanguage", label: "Preferred Language" }, + { key: "currentOccupation", label: "Current Occupation" }, + { key: "occupationPlace", label: "School / Company" }, + { key: "occupationDetail", label: "Major / Position" }, + { key: "email", label: "Email" }, + { key: "phone", label: "Phone" }, + { key: "teamFormation", label: "Team Formation" }, + { key: "teamName", label: "Team Name" }, + { key: "interestedTrack", label: "Interested Track" }, + { key: "primaryRole", label: "Primary Role" }, + { key: "roleProficiency", label: "Role Proficiency" }, + { key: "toolsUsed", label: "Tools Used" }, + { key: "resume", label: "Resume" }, + { key: "qDreamCreation", label: "Dream Creation Essay" }, + { key: "qProudestMoment", label: "Proudest Moment Essay" }, + { key: "qWhyGarudaHacks", label: "Why Garuda Hacks Essay" }, + { key: "overnightPlan", label: "Overnight Plan" }, + { key: "leaveLetter", label: "Leave Letter" }, + { key: "phoneEmergency", label: "Emergency Phone" }, + { key: "emergencyWays", label: "Emergency Contact Methods" }, + { key: "emergencyRelation", label: "Emergency Relation" }, + { key: "signedConsent", label: "Signed Consent" }, + { key: "hackathonCount", label: "Hackathon Count" }, + { key: "ghCount", label: "GH Iterations" }, + { key: "joinSource", label: "Join Source" }, + { key: "referralSource", label: "Referral Source" }, + { key: "joinReason", label: "Join Reason" }, + ]; + + const missingFieldsApps = applicationsOriginal + .map((app) => { + const missing = REQUIRED_FIELDS.filter((field) => { + const val = app[field.key]; + if (val === undefined || val === null) return true; + if (typeof val === "string" && val.trim() === "") return true; + if (Array.isArray(val) && val.length === 0) return true; + return false; + }); + if (missing.length > 0) { + return { application: app, missingFields: missing.map((f) => f.label) }; + } + return null; + }) + .filter(Boolean) as { + application: CombinedApplicationData; + missingFields: string[]; + }[]; + return (
Potential Issues - {(duplicateGroups.length > 0 || oversizeTeams.length > 0) && ( + {(duplicateGroups.length > 0 || + oversizeTeams.length > 0 || + missingFieldsApps.length > 0) && ( - {duplicateGroups.length + oversizeTeams.length} + {duplicateGroups.length + + oversizeTeams.length + + missingFieldsApps.length} )} @@ -781,13 +841,13 @@ export default function Applications() { {activeTab === "issues" && ( <>
-
+
{duplicateGroups.length}
- Duplicate Groups ({totalDuplicateApps} apps) + Duplicates ({totalDuplicateApps} apps)
@@ -795,7 +855,15 @@ export default function Applications() { {oversizeTeams.length}
- Oversize Teams (>{MAX_TEAM_SIZE}) + Oversize (>{MAX_TEAM_SIZE}) +
+
+
+
+ {missingFieldsApps.length} +
+
+ Missing Fields
@@ -827,7 +895,20 @@ export default function Applications() { : "text-white/50 hover:text-white/80 hover:bg-white/5 border border-transparent" }`} > - Oversize Teams ({oversizeTeams.length}) + Oversize ({oversizeTeams.length}) + +
@@ -961,6 +1042,46 @@ export default function Applications() { )} )} + + {activeIssueType === "missing-fields" && ( + <> + {missingFieldsApps.length === 0 ? ( +
+ No applications with missing required fields +
+ ) : ( + missingFieldsApps.map((entry) => ( +
+ handleApplicationSelect(entry.application) + } + className={`p-4 border-b border-white/10 cursor-pointer transition-colors hover:bg-white/5 ${ + selectedApplication?.id === entry.application.id + ? "bg-primary/10 border-primary/30" + : "" + }`} + > +
+

+ {entry.application.firstName || + entry.application.email || + "Unknown"}{" "} + {entry.application.lastName || ""} +

+ + {entry.missingFields.length} missing + +
+

+ {entry.missingFields.slice(0, 3).join(", ")} + {entry.missingFields.length > 3 && "..."} +

+
+ )) + )} + + )}
@@ -1027,6 +1148,32 @@ export default function Applications() { ); })()} + {activeTab === "issues" && + activeIssueType === "missing-fields" && + (() => { + const entry = missingFieldsApps.find( + (e) => e.application.id === selectedApplication.id + ); + if (!entry) return null; + return ( +
+

+ {entry.missingFields.length} Required Field + {entry.missingFields.length > 1 ? "s" : ""} Missing +

+
    + {entry.missingFields.map((field) => ( +
  • + {field} +
  • + ))} +
+
+ ); + })()} {/* PROFILE */}
diff --git a/config.ts b/config.ts index acc7954..ad7bcbc 100644 --- a/config.ts +++ b/config.ts @@ -1,3 +1,5 @@ +export const eventName = "Garuda Hacks 7.0" + export const EPOCH_START_MENTORING = 1753340400; export const EPOCH_ENDS_MENTORING = 1753448400; diff --git a/data/question.ts b/data/question.ts new file mode 100644 index 0000000..02204c7 --- /dev/null +++ b/data/question.ts @@ -0,0 +1,674 @@ +import { eventName } from "@/config"; +import { + ApplicationQuestion, + APPLICATION_STATES, + QUESTION_TYPE, +} from "@/types/application"; + +export const allQuestionsData: ApplicationQuestion[] = [ + { + id: "firstName", + text: "First Name", + state: APPLICATION_STATES.PROFILE, + type: QUESTION_TYPE.STRING, + placeholder: "Please enter your first name", + required: true, + validation: { + maxLength: 50, + }, + order: 1, + }, + { + id: "lastName", + text: "Last Name", + state: APPLICATION_STATES.PROFILE, + type: QUESTION_TYPE.STRING, + placeholder: "Please enter your last name", + required: true, + validation: { + maxLength: 50, + }, + order: 2, + }, + { + id: "genderIdentity", + text: "Gender Identity", + state: APPLICATION_STATES.PROFILE, + type: QUESTION_TYPE.DROPDOWN, + required: true, + options: ["Male", "Female", "Would rather not say"], + order: 3, + }, + { + id: "dateOfBirth", + text: "Date of Birth", + state: APPLICATION_STATES.PROFILE, + type: QUESTION_TYPE.DATE, + required: true, + validation: {}, + order: 4, + }, + { + id: "nationality", + text: "Nationality", + state: APPLICATION_STATES.PROFILE, + type: QUESTION_TYPE.DROPDOWN, + required: true, + placeholder: "Select one", + options: ["Indonesian citizen (Warga Negara Indonesia)", "Non-Indonesian Citizen (Warga Negara Asing)"], + order: 5, + }, + { + id: "countryOfresidence", + text: "What is your country of residence? \nNote: If you are currently studying or working abroad, please fill in the country where you are currently studying or working at.", + state: APPLICATION_STATES.PROFILE, + type: QUESTION_TYPE.DROPDOWN, + required: true, + options: [ + "Afghanistan", + "Albania", + "Algeria", + "Andorra", + "Angola", + "Antigua & Deps", + "Argentina", + "Armenia", + "Australia", + "Austria", + "Azerbaijan", + "Bahamas", + "Bahrain", + "Bangladesh", + "Barbados", + "Belarus", + "Belgium", + "Belize", + "Benin", + "Bhutan", + "Bolivia", + "Bosnia Herzegovina", + "Botswana", + "Brazil", + "Brunei", + "Bulgaria", + "Burkina", + "Burundi", + "Cambodia", + "Cameroon", + "Canada", + "Cape Verde", + "Central African Rep", + "Chad", + "Chile", + "China", + "Colombia", + "Comoros", + "Congo", + "Congo {Democratic Rep}", + "Costa Rica", + "Croatia", + "Cuba", + "Cyprus", + "Czech Republic", + "Denmark", + "Djibouti", + "Dominica", + "Dominican Republic", + "East Timor", + "Ecuador", + "Egypt", + "El Salvador", + "Equatorial Guinea", + "Eritrea", + "Estonia", + "Ethiopia", + "Fiji", + "Finland", + "France", + "Gabon", + "Gambia", + "Georgia", + "Germany", + "Ghana", + "Greece", + "Grenada", + "Guatemala", + "Guinea", + "Guinea-Bissau", + "Guyana", + "Haiti", + "Honduras", + "Hungary", + "Iceland", + "India", + "Indonesia", + "Iran", + "Iraq", + "Ireland {Republic}", + "Israel", + "Italy", + "Ivory Coast", + "Jamaica", + "Japan", + "Jordan", + "Kazakhstan", + "Kenya", + "Kiribati", + "Korea North", + "Korea South", + "Kosovo", + "Kuwait", + "Kyrgyzstan", + "Laos", + "Latvia", + "Lebanon", + "Lesotho", + "Liberia", + "Libya", + "Liechtenstein", + "Lithuania", + "Luxembourg", + "Macedonia", + "Madagascar", + "Malawi", + "Malaysia", + "Maldives", + "Mali", + "Malta", + "Marshall Islands", + "Mauritania", + "Mauritius", + "Mexico", + "Micronesia", + "Moldova", + "Monaco", + "Mongolia", + "Montenegro", + "Morocco", + "Mozambique", + "Myanmar, {Burma}", + "Namibia", + "Nauru", + "Nepal", + "Netherlands", + "New Zealand", + "Nicaragua", + "Niger", + "Nigeria", + "Norway", + "Oman", + "Pakistan", + "Palau", + "Panama", + "Papua New Guinea", + "Paraguay", + "Peru", + "Philippines", + "Poland", + "Portugal", + "Qatar", + "Romania", + "Russian Federation", + "Rwanda", + "St Kitts & Nevis", + "St Lucia", + "Saint Vincent & the Grenadines", + "Samoa", + "San Marino", + "Sao Tome & Principe", + "Saudi Arabia", + "Senegal", + "Serbia", + "Seychelles", + "Sierra Leone", + "Singapore", + "Slovakia", + "Slovenia", + "Solomon Islands", + "Somalia", + "South Africa", + "South Sudan", + "Spain", + "Sri Lanka", + "Sudan", + "Suriname", + "Swaziland", + "Sweden", + "Switzerland", + "Syria", + "Taiwan", + "Tajikistan", + "Tanzania", + "Thailand", + "Togo", + "Tonga", + "Trinidad & Tobago", + "Tunisia", + "Turkey", + "Turkmenistan", + "Tuvalu", + "Uganda", + "Ukraine", + "United Arab Emirates", + "United Kingdom", + "United States", + "Uruguay", + "Uzbekistan", + "Vanuatu", + "Vatican City", + "Venezuela", + "Vietnam", + "Yemen", + "Zambia", + "Zimbabwe", + ], + order: 6, + }, + { + id: "preferredLanguage", + text: "What is your preferred language?", + state: APPLICATION_STATES.PROFILE, + type: QUESTION_TYPE.DROPDOWN, + required: true, + options: ["English", "Bahasa Indonesia", "Both"], + order: 7, + }, + { + id: "currentOccupation", + text: "What is your current level of study/employment?", + state: APPLICATION_STATES.PROFILE, + type: QUESTION_TYPE.DROPDOWN, + required: true, + options: ["Currently in high school (SMA/SMK) or lower", "Currently studying for Bachelor's Degree (Sarjana)", "Currently studying for Master's Degree (Magister)", "Currently studying for Doctorate Degree (Doktor)", "Currently employed"], + order: 8, + }, + { + id: "occupationPlace", + text: "What school are you currently studying at/What company are you currently working for?", + state: APPLICATION_STATES.PROFILE, + type: QUESTION_TYPE.STRING, + required: true, + validation: { + maxLength: 50, + }, + order: 9, + }, + { + id: "occupationDetail", + text: "What major are you studying? If you are a high school student, what major are you thinking of studying? If employed, what is your current job position?", + state: APPLICATION_STATES.PROFILE, + type: QUESTION_TYPE.STRING, + required: true, + validation: { + maxLength: 50, + }, + order: 10, + }, + { + id: "universityYear", + text: "If you are a university student, what year are you currently in?", + state: APPLICATION_STATES.PROFILE, + type: QUESTION_TYPE.DROPDOWN, + required: false, + options: ["Year 1", "Year 2", "Year 3", "Year 4", "Year 5+"], + order: 11, + }, + { + id: "email", + text: "What is your email address? Please do not enter your school email address", + state: APPLICATION_STATES.PROFILE, + type: QUESTION_TYPE.STRING, + required: true, + validation: { + pattern: "^[a-zA-Z0-9._%+\\-]+@[a-zA-Z0-9.\\-]+\\.[a-zA-Z]{2,}$", + maxLength: 50, + }, + order: 12, + }, + { + id: "phone", + text: "What is your phone number? Please include your country code (e.g. +62)", + state: APPLICATION_STATES.PROFILE, + type: QUESTION_TYPE.STRING, + required: true, + validation: { + maxLength: 50, + }, + order: 13, + }, + + + // TEAM + { + id: "teamFormation", + text: "Will you be applying with a team?", + state: APPLICATION_STATES.TEAM, + type: QUESTION_TYPE.DROPDOWN, + required: true, + options: ["Yes, I already have a team", "No, I will be joining Garuda Hacks solo", "No, I do not have a complete team, but I would like to look for a team through Speed Dating"], + order: 1, + }, + { + id: "teamName", + text: "Please enter your team name. Please make sure that you and all your team members enter your team name accurately and identically, this will help us identify your team. Even if your team is still incomplete, please enter your team name. If you are joining solo or have no team, please enter “N/A”", + state: APPLICATION_STATES.TEAM, + type: QUESTION_TYPE.STRING, + required: true, + validation: { + maxLength: 50, + }, + order: 2, + }, + { + id: "teamMembers", + text: "Please list the full names of your team members, separated by commas (e.g., Budi Pratama, Siti Rahayu). Only fill this in if you are participating as a team. **Note that the maximum number of members is 4, including you.**", + state: APPLICATION_STATES.TEAM, + type: QUESTION_TYPE.STRING, + required: false, + validation: { + maxLength: 300, + }, + order: 3, + }, + { + id: "interestedTrack", + text: "Which track is your team most interested in? This is not final, and you can still change your mind during the hackathon. This will only help us predict participant interest.", + state: APPLICATION_STATES.TEAM, + type: QUESTION_TYPE.DROPDOWN, + required: true, + options: ["Health", "Safety", "Agriculture & Food Systems"], + order: 4, + }, + + + // SPEED DATING + { + id: "primaryRole", + text: "What is your primary role in a team?", + state: APPLICATION_STATES.SPEED_DATING, + type: QUESTION_TYPE.DROPDOWN, + required: true, + options: ["Developers (Frontend, Backend, Full-stack)", "Designers (UI/UX, Graphic, Product)", "Product / Business Thinkers", "Innovators / Idea Creators / First-timers"], + order: 1, + }, + { + id: "roleProficiency", + text: "Rate your proficiency in your primary role", + state: APPLICATION_STATES.SPEED_DATING, + type: QUESTION_TYPE.DROPDOWN, + required: true, + options: ["Beginner", "Intermediate", "Advanced"], + order: 2, + }, + { + id: "toolsUsed", + text: "List the programming languages, frameworks, or design tools you are most comfortable with", + state: APPLICATION_STATES.SPEED_DATING, + type: QUESTION_TYPE.STRING, + required: true, + validation: { + maxLength: 300, + }, + order: 3, + }, + { + id: "pastProjects", + text: "Link to your most prominent past project or tech stack summary", + state: APPLICATION_STATES.SPEED_DATING, + type: QUESTION_TYPE.STRING, + required: false, + validation: { + maxLength: 500, + }, + order: 4, + }, + + + // APPLICATION + { + id: "resume", + text: "Please attach your resume/CV", + state: APPLICATION_STATES.APPLICATION, + type: QUESTION_TYPE.FILE, + required: true, + validation: { + allowedTypes: "application/pdf", + maxSize: 5, + }, + order: 1, + }, + { + id: "github", + text: "Please enter your GitHub profile URL (https://github.com/your-handle)", + state: APPLICATION_STATES.APPLICATION, + type: QUESTION_TYPE.STRING, + placeholder: "https://github.com/your-handle", + required: false, + validation: { + pattern: "^https?:\\/\\/(www\\.)?github\\.com\\/.*$", + maxLength: 200, + }, + order: 2, + }, + { + id: "linkedin", + text: "Please enter your LinkedIn profile URL (https://www.linkedin.com/in/your-handle)", + state: APPLICATION_STATES.APPLICATION, + type: QUESTION_TYPE.STRING, + placeholder: "https://www.linkedin.com/in/your-handle", + required: false, + validation: { + pattern: "^https?:\\/\\/(www\\.)?linkedin\\.com\\/.*$", + maxLength: 200, + }, + order: 3, + }, + { + id: "devpost", + text: "Please enter your DevPost profile URL (https://devpost.com/your-handle)", + state: APPLICATION_STATES.APPLICATION, + type: QUESTION_TYPE.STRING, + placeholder: "https://devpost.com/your-handle", + required: false, + validation: { + pattern: "^https?:\\/\\/(www\\.)?devpost\\.com\\/.*$", + maxLength: 200, + }, + order: 4, + }, + { + id: "qDreamCreation", + text: "**Your Dream Creation**: Imagine you had all the necessary resources and skills. What would you want to create? Your answer does not have to be a website or an app- it can be anything. Please tell us what you would create, and why you want to create it.", + state: APPLICATION_STATES.APPLICATION, + type: QUESTION_TYPE.TEXTAREA, + placeholder: "Answer in 150 words or less", + required: true, + validation: { + minLength: 100, + maxLength: 400, + }, + order: 5, + }, + { + id: "qProudestMoment", + text: "**Your Proudest Moment**: Tell us about an experience that made you feel very proud of yourself. This may be a time you created something, overcame a difficulty, learned something new, or something else. Elaborate on why you felt so proud.", + state: APPLICATION_STATES.APPLICATION, + type: QUESTION_TYPE.TEXTAREA, + placeholder: "Answer in 150 words or less", + required: true, + validation: { + minLength: 100, + maxLength: 400, + }, + order: 6, + }, + { + id: "qWhyGarudaHacks", + text: "**Why Garuda Hacks**: Please tell us why you decided to join a hackathon. What do you hope to learn or take away from the experience?", + state: APPLICATION_STATES.APPLICATION, + type: QUESTION_TYPE.TEXTAREA, + placeholder: "Answer in 150 words or less", + required: true, + validation: { + minLength: 100, + maxLength: 400, + }, + order: 7, + }, + + + // LOGISITICAL DETAIL + { + id: "overnightPlan", + text: "Do you plan to stay overnight at our venue (Universitas Multimedia Nusantara)?", + state: APPLICATION_STATES.LOGISTICAL_DETAIL, + type: QUESTION_TYPE.DROPDOWN, + required: true, + options: ["Yes, I will stay overnight at UMN", "No, I have my own accommodation and will not stay overnight at UMN"], + order: 1, + }, + { + id: "leaveLetter", + text: "If the Garuda Hacks 7.0 event conflicts with your school's schedule, will you require us to issue you a notice verifying that you will be attending Garuda Hacks 7.0?", + state: APPLICATION_STATES.LOGISTICAL_DETAIL, + type: QUESTION_TYPE.DROPDOWN, + required: true, + options: [`Yes, I would like to request a leave letter to attend ${eventName}`, "No, I do not need a leave letter"], + order: 2, + }, + + + // EMERGENCY + { + id: "phoneEmergency", + text: "Please enter the phone number of your emergency contact", + state: APPLICATION_STATES.EMERGENCY_AND_CONSENT, + type: QUESTION_TYPE.STRING, + required: true, + validation: { + maxLength: 50, + }, + order: 1, + }, + { + id: "emergencyWays", + text: "Please enter any other methods of contact to reach your emergency contact, if any", + state: APPLICATION_STATES.EMERGENCY_AND_CONSENT, + type: QUESTION_TYPE.STRING, + required: true, + validation: { + maxLength: 100, + }, + order: 2, + }, + { + id: "emergencyRelation", + text: "What is your emergency contact's relationship to you?", + state: APPLICATION_STATES.EMERGENCY_AND_CONSENT, + type: QUESTION_TYPE.STRING, + required: true, + validation: { + maxLength: 50, + }, + order: 3, + }, + { + id: "signedConsent", + text: "Please read and sign the attached [Consent Form](https://drive.google.com/file/d/1fH5ll1-AgSyCgPXssMSRXWrFG9DYFN7D/view). If you are below 18, please ask your parent/guardian to sign the form. Please attach the signed form below.", + state: APPLICATION_STATES.EMERGENCY_AND_CONSENT, + type: QUESTION_TYPE.FILE, + required: true, + validation: { + allowedTypes: "application/pdf", + maxSize: 5, + }, + order: 4, + }, + { + id: "referralCode", + text: "Were you referred to join Garuda Hacks 7.0? If you were, please carefully enter your referral code below", + state: APPLICATION_STATES.EMERGENCY_AND_CONSENT, + type: QUESTION_TYPE.STRING, + required: false, + validation: { + maxLength: 50, + }, + order: 5, + }, + + + // ADDITIONAL INFO + { + id: "hackathonCount", + text: "How many hackathons have you joined before Garuda Hacks 7.0?", + state: APPLICATION_STATES.ADDITIONAL_QUESTION, + type: QUESTION_TYPE.DROPDOWN, + required: true, + options: ["0 (Garuda Hacks 7.0 is my first!)", "1-2", "3-5", "6+"], + order: 1, + }, + { + id: "ghCount", + text: "Have you joined a Garuda Hacks hackathon before? If so, which iterations have you joined?", + state: APPLICATION_STATES.ADDITIONAL_QUESTION, + type: QUESTION_TYPE.MULTI, // TODO: implement multi type + required: true, + options: [ + "I have not joined a Garuda Hacks hackathon before", + "Garuda Hacks 1.0", + "Garuda Hacks 2.0", + "Garuda Hacks 3.0", + "Garuda Hacks 4.0", + "Garuda Hacks 5.0", + "Garuda Hacks 6.0", + ], + order: 2, + }, + { + id: "joinSource", + text: "How did you hear about Garuda Hacks 7.0?", + state: APPLICATION_STATES.ADDITIONAL_QUESTION, + type: QUESTION_TYPE.MULTI, + required: true, + options: [ + "I follow your Instagram", + "Your posts showed up on my Instagram feed", + "Your posts showed up on my TikTok feed", + "Friends/Family", + "My school/university told me about Garuda Hacks 7.0", + "Instagram advertisements", + "I saw promotions on Generation Girl's Instagram", + "Other" // TODO: implement how other is implemented here + ], + order: 3, + }, + { + id: "referralSource", + text: "Where did you hear about Garuda Hacks?", + state: APPLICATION_STATES.ADDITIONAL_QUESTION, + type: QUESTION_TYPE.DROPDOWN, + required: true, + options: [ + "Instagram", + "Facebook", + "Twitter/X", + "LinkedIn", + "University Club", + "Friend", + "Other", + ], + order: 4, + }, + { + id: "joinReason", + text: "What is your main reason for joining Garuda Hacks 7.0? What attracted you to join? This question will not be graded, this is only to help us improve future events :)", + state: APPLICATION_STATES.ADDITIONAL_QUESTION, + type: QUESTION_TYPE.TEXTAREA, + placeholder: "Answer in 100 words or less", + required: true, + validation: { + maxLength: 100, + }, + order: 5, + }, +]; diff --git a/types/application.ts b/types/application.ts new file mode 100644 index 0000000..92c8522 --- /dev/null +++ b/types/application.ts @@ -0,0 +1,136 @@ +export enum QUESTION_TYPE { + NUMBER = "number", + STRING = "string", + TEXTAREA = "textarea", + DATE = "datetime", // Matches backend QUESTION_TYPE.DATE + DROPDOWN = "dropdown", + FILE = "file", + MULTI = "multi" +} + +// Validation rule interfaces +export interface StringValidation { + required?: boolean; + minLength?: number; + maxLength?: number; + pattern?: string; +} + +export interface NumberValidation { + required?: boolean; + minValue?: number; + maxValue?: number; +} + +export interface DatetimeValidation { + required?: boolean; + earliest?: string; + latest?: string; +} + +export interface DropdownValidation { + required?: boolean; + multiple?: boolean; + minSelections?: number; + maxSelections?: number; +} + +export interface MultiValidation { + required?: boolean; + minSelections?: number; + maxSelections?: number; +} + +export interface FileValidation { + required?: boolean; + allowedTypes: string; // Comma separated MIME types e.g. "image/jpeg,application/pdf" + maxSize: number; // Max size in MB +} + +export type ValidationTypeMap = { + [QUESTION_TYPE.STRING]: StringValidation; + [QUESTION_TYPE.TEXTAREA]: StringValidation; + [QUESTION_TYPE.NUMBER]: NumberValidation; + [QUESTION_TYPE.DATE]: DatetimeValidation; + [QUESTION_TYPE.DROPDOWN]: DropdownValidation; + [QUESTION_TYPE.FILE]: FileValidation; + [QUESTION_TYPE.MULTI]: MultiValidation; +}; + +// Base interface for all question types +interface BaseApplicationQuestion { + id: string; + order: number; + text: string; + placeholder?: string; + state: APPLICATION_STATES; + required?: boolean; +} + +// Specific question type interfaces for creating a discriminated union +export interface StringApplicationQuestion extends BaseApplicationQuestion { + type: QUESTION_TYPE.STRING; + validation?: StringValidation; +} + +export interface TextareaApplicationQuestion extends BaseApplicationQuestion { + type: QUESTION_TYPE.TEXTAREA; + validation?: StringValidation; +} + +export interface NumberApplicationQuestion extends BaseApplicationQuestion { + type: QUESTION_TYPE.NUMBER; + validation?: NumberValidation; +} + +export interface DateApplicationQuestion extends BaseApplicationQuestion { + type: QUESTION_TYPE.DATE; + validation?: DatetimeValidation; +} + +export interface DropdownApplicationQuestion extends BaseApplicationQuestion { + type: QUESTION_TYPE.DROPDOWN; + options: string[]; + multiple?: boolean; + validation?: DropdownValidation; +} + +export interface FileApplicationQuestion extends BaseApplicationQuestion { + type: QUESTION_TYPE.FILE; + validation: FileValidation; +} + +export interface MultiApplicationQuestion extends BaseApplicationQuestion { + type: QUESTION_TYPE.MULTI; + options: string[]; + validation?: MultiValidation; +} + +export type ApplicationQuestion = + | StringApplicationQuestion + | TextareaApplicationQuestion + | NumberApplicationQuestion + | DateApplicationQuestion + | DropdownApplicationQuestion + | FileApplicationQuestion + | MultiApplicationQuestion; + +export enum APPLICATION_STATUS { + NOT_APPLICABLE = "not applicable", + DRAFT = "draft", + SUBMITTED = "submitted", + WAITLISTED = "waitlisted", + REJECTED = "rejected", + ACCEPTED = "accepted", + CONFIRMED_RSVP = "confirmed rsvp", +} + +export enum APPLICATION_STATES { + PROFILE = "PROFILE", + TEAM = "TEAM", + SPEED_DATING = "SPEED_DATING", + APPLICATION = "APPLICATION", + LOGISTICAL_DETAIL = "LOGISTICAL_DETAIL", + EMERGENCY_AND_CONSENT = "EMERGENCY_AND_CONSENT", + ADDITIONAL_QUESTION = "ADDITIONAL_QUESTION", +}