From f2d3f5ccd9262ff4b694f35bfd88ff409917eec5 Mon Sep 17 00:00:00 2001 From: Nnaji Benjamin <60315147+Benjtalkshow@users.noreply.github.com> Date: Thu, 14 May 2026 17:14:24 +0100 Subject: [PATCH 1/4] fix(submissions): improve link validation UX, banner display, and error toasts (#547) - Reconcile frontend link types with backend enum (github, demo, video, document, presentation, other) - Allow up to 5 "other" links per submission with smart type-picking on Add Link and duplicate guards on type change - Add 500-character cap and live counter on the optional Introduction field - Pass banner through initialData on submission edit so the saved banner displays - Add banner field to ParticipantSubmission so the type compiles - Set mock-fill participation type from myTeam to avoid INDIVIDUAL submissions while on a team - Switch submission error toasts to title + description with 8s duration so backend messages are readable - Update SubmissionLinksTab icon mapping for the new link types --- .../hackathons/[slug]/submit/page.tsx | 1 + .../hackathons/submissions/SubmissionForm.tsx | 87 ++++++++++++++++--- .../SubmissionLinksTab.tsx | 20 ++++- hooks/hackathon/use-submission.ts | 15 +++- lib/api/hackathons.ts | 1 + 5 files changed, 109 insertions(+), 15 deletions(-) diff --git a/app/(landing)/hackathons/[slug]/submit/page.tsx b/app/(landing)/hackathons/[slug]/submit/page.tsx index af22d975..05e12538 100644 --- a/app/(landing)/hackathons/[slug]/submit/page.tsx +++ b/app/(landing)/hackathons/[slug]/submit/page.tsx @@ -156,6 +156,7 @@ export default function SubmitProjectPage({ category: mySubmission.category, description: mySubmission.description, logo: mySubmission.logo, + banner: mySubmission.banner, videoUrl: mySubmission.videoUrl, introduction: mySubmission.introduction, links: mySubmission.links, diff --git a/components/hackathons/submissions/SubmissionForm.tsx b/components/hackathons/submissions/SubmissionForm.tsx index 6f1eeed8..001f8def 100644 --- a/components/hackathons/submissions/SubmissionForm.tsx +++ b/components/hackathons/submissions/SubmissionForm.tsx @@ -92,7 +92,10 @@ const baseSubmissionSchema = z.object({ videoUrl: z .union([z.string().url('Please enter a valid URL'), z.literal('')]) .optional(), - introduction: z.string().optional(), + introduction: z + .string() + .max(500, 'Introduction cannot exceed 500 characters') + .optional(), links: z.array( z.object({ type: z.string(), @@ -155,12 +158,18 @@ const INITIAL_STEPS: Step[] = [ const LINK_TYPES = [ { value: 'github', label: 'GitHub' }, { value: 'demo', label: 'Demo' }, - { value: 'website', label: 'Website' }, - { value: 'documentation', label: 'Documentation' }, + { value: 'video', label: 'Video' }, + { value: 'document', label: 'Document' }, + { value: 'presentation', label: 'Presentation' }, { value: 'other', label: 'Other' }, ]; -const OTHER_LINK_TYPES = ['demo', 'website', 'documentation', 'other']; +const OTHER_LINK_TYPES = ['demo', 'video', 'document', 'presentation', 'other']; + +const MAX_OTHER_LINKS = 5; +const FIXED_LINK_TYPES = LINK_TYPES.map(t => t.value).filter( + v => v !== 'other' +); const isValidUrl = (url: string | undefined): boolean => { if (!url || String(url).trim() === '') return false; @@ -517,6 +526,9 @@ const SubmissionFormContent: React.FC = ({ }; const handleFillMockData = () => { + const participationType: 'INDIVIDUAL' | 'TEAM' = myTeam + ? 'TEAM' + : 'INDIVIDUAL'; const mockData = { projectName: 'AI-Powered Task Manager', category: categoryOptions[0], @@ -529,7 +541,7 @@ const SubmissionFormContent: React.FC = ({ { type: 'github', url: 'https://github.com/example/ai-task-manager' }, { type: 'demo', url: 'https://demo.example.com/ai-task-manager' }, ], - participationType: 'INDIVIDUAL' as const, + participationType, }; form.reset(mockData); @@ -539,7 +551,27 @@ const SubmissionFormContent: React.FC = ({ const handleAddLink = () => { const currentLinks = form.getValues('links') || []; - form.setValue('links', [...currentLinks, { type: 'github', url: '' }], { + const usedFixedTypes = new Set( + currentLinks.map(l => l.type).filter(t => t !== 'other') + ); + const otherCount = currentLinks.filter(l => l.type === 'other').length; + + const firstUnusedFixed = FIXED_LINK_TYPES.find(t => !usedFixedTypes.has(t)); + + let nextType: string; + if (firstUnusedFixed) { + nextType = firstUnusedFixed; + } else if (otherCount < MAX_OTHER_LINKS) { + nextType = 'other'; + } else { + toast.error('Cannot add another link', { + description: `All fixed link types are used and you have reached the limit of ${MAX_OTHER_LINKS} "Other" links.`, + duration: 6000, + }); + return; + } + + form.setValue('links', [...currentLinks, { type: nextType, url: '' }], { shouldValidate: false, }); }; @@ -582,6 +614,33 @@ const SubmissionFormContent: React.FC = ({ value: string ) => { const currentLinks = form.getValues('links') || []; + + if (field === 'type') { + if (value !== 'other') { + const isDuplicate = currentLinks.some( + (l, i) => i !== index && l.type === value + ); + if (isDuplicate) { + toast.error('Duplicate link type', { + description: `"${value}" is already used. Each link type can be used at most once. Choose "Other" for additional links.`, + duration: 6000, + }); + return; + } + } else { + const otherCount = currentLinks.filter( + (l, i) => i !== index && l.type === 'other' + ).length; + if (otherCount >= MAX_OTHER_LINKS) { + toast.error('Too many "Other" links', { + description: `You can include at most ${MAX_OTHER_LINKS} "Other" links per submission.`, + duration: 6000, + }); + return; + } + } + } + const updatedLinks = [...currentLinks]; updatedLinks[index] = { ...updatedLinks[index], [field]: value }; form.setValue('links', updatedLinks, { shouldValidate: true }); @@ -643,7 +702,7 @@ const SubmissionFormContent: React.FC = ({ if (requireOtherLinks && !hasValidOtherLink) { form.setError('links', { message: - 'At least one additional link (Demo, Website, Documentation, or Other) is required for this hackathon.', + 'At least one additional link (Demo, Video, Document, Presentation, or Other) is required for this hackathon.', }); return; } @@ -792,7 +851,7 @@ const SubmissionFormContent: React.FC = ({ if (requireOtherLinks && !hasValidOtherLink) { form.setError('links', { message: - 'At least one additional link (Demo, Website, Documentation, or Other) is required for this hackathon.', + 'At least one additional link (Demo, Video, Document, Presentation, or Other) is required for this hackathon.', }); setCurrentStep(2); updateStepState(2, 'active'); @@ -1346,12 +1405,13 @@ const SubmissionFormContent: React.FC = ({