Skip to content

Commit ac2e5fd

Browse files
committed
refactor: enhance form validation and data handling in submission form
- Improved the FormSchema by adding detailed validation messages for various fields. - Implemented preprocessing for GitHub repository and demo link inputs to ensure valid formats and uniqueness. - Updated the useEffect for reactive validation to ensure fields are validated as the user types. - Added a refinement to require an explanation when the idea is pre-existing.
1 parent 14c4f3c commit ac2e5fd

File tree

1 file changed

+158
-32
lines changed

1 file changed

+158
-32
lines changed

components/hackathons/project-submission/hooks/useSubmissionFormSecure.ts

Lines changed: 158 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,155 @@ import { useSession } from 'next-auth/react';
77
import { useToast } from '@/hooks/use-toast';
88
import { useProjectSubmission } from '../context/ProjectSubmissionContext';
99
import { useRouter } from 'next/navigation';
10-
export const FormSchema = z.object({
11-
project_name: z.string().min(2).max(60),
12-
short_description: z.string().min(30).max(280),
13-
full_description: z.string().min(30),
14-
tech_stack: z.string().min(30),
15-
github_repository: z.array(z.string()).min(1),
16-
explanation: z.string().optional(),
17-
demo_link: z.array(z.string()).min(1),
18-
is_preexisting_idea: z.boolean(),
19-
demo_video_link: z.string().optional(),
20-
tracks: z.array(z.string()).min(1),
21-
logoFile: z.any().optional(),
22-
coverFile: z.any().optional(),
23-
screenshots: z.array(z.any()).optional(),
24-
logo_url: z.string().optional(),
25-
cover_url: z.string().optional(),
26-
hackaton_id: z.string().optional(),
27-
user_id: z.string().optional(),
28-
is_winner: z.boolean().optional(),
29-
isDraft: z.boolean().optional(),
30-
});
10+
export const FormSchema = z
11+
.object({
12+
project_name: z
13+
.string()
14+
.min(2, { message: 'Project Name must be at least 2 characters' })
15+
.max(60, { message: 'Max 60 characters allowed' }),
16+
short_description: z
17+
.string()
18+
.min(30, { message: 'Short description must be at least 30 characters' })
19+
.max(280, { message: 'Max 280 characters allowed' }),
20+
full_description: z
21+
.string()
22+
.min(30, { message: 'Full description must be at least 30 characters' }),
23+
tech_stack: z
24+
.string()
25+
.min(30, { message: 'Tech stack must be at least 30 characters' }),
26+
github_repository: z.preprocess(
27+
(val) => {
28+
if (!val) return [];
29+
if (typeof val === 'string') return [];
30+
return val;
31+
},
32+
z.array(
33+
z.string()
34+
.min(1, { message: 'GitHub repository is required' })
35+
)
36+
.min(1, { message: 'At least one GitHub repository is required' })
37+
.refine(
38+
(links) => {
39+
const uniqueLinks = new Set(links);
40+
return uniqueLinks.size === links.length;
41+
},
42+
{ message: 'Duplicate GitHub repositories are not allowed' }
43+
)
44+
.transform((val) => {
45+
const invalidRepos = val.filter(repo => {
46+
if (repo.startsWith('http')) {
47+
try {
48+
const url = new URL(repo);
49+
return !(
50+
url.hostname === 'github.com' &&
51+
url.pathname.split('/').length >= 2 &&
52+
url.pathname.split('/')[1].length > 0
53+
);
54+
} catch {
55+
return true;
56+
}
57+
}
58+
59+
const parts = repo.split('/');
60+
return !(
61+
parts.length === 2 &&
62+
parts[0].length > 0 &&
63+
!parts[0].includes(' ') &&
64+
parts[1].length > 0 &&
65+
!parts[1].includes(' ')
66+
);
67+
});
68+
69+
if (invalidRepos.length > 0) {
70+
throw new z.ZodError([
71+
{
72+
code: 'custom',
73+
message: 'Please enter a valid GitHub URL (e.g., https://github.com/username/repo) or username/repo format',
74+
path: ['github_repository']
75+
}
76+
]);
77+
}
78+
return val;
79+
})
80+
),
81+
explanation: z.string().optional(),
82+
demo_link: z.preprocess(
83+
(val) => {
84+
if (!val) return [];
85+
if (typeof val === 'string') return [];
86+
return val;
87+
},
88+
z.array(
89+
z.string()
90+
.min(1, { message: 'Demo link cannot be empty' })
91+
)
92+
.min(1, { message: 'At least one demo link is required' })
93+
.refine(
94+
(links) => {
95+
const uniqueLinks = new Set(links);
96+
return uniqueLinks.size === links.length;
97+
},
98+
{ message: 'Duplicate demo links are not allowed' }
99+
)
100+
.refine(
101+
(links) => {
102+
return links.every(url => {
103+
try {
104+
new URL(url);
105+
return true;
106+
} catch {
107+
return false;
108+
}
109+
});
110+
},
111+
{ message: 'Please enter a valid URL' }
112+
)
113+
),
114+
is_preexisting_idea: z.boolean(),
115+
logoFile: z.any().optional(),
116+
coverFile: z.any().optional(),
117+
screenshots: z.any().optional(),
118+
demo_video_link: z
119+
.string()
120+
.url({ message: 'Please enter a valid URL' })
121+
.optional()
122+
.or(z.literal(''))
123+
.refine(
124+
(val) => {
125+
if (!val) return true;
126+
try {
127+
const url = new URL(val);
128+
return (
129+
url.hostname.includes('youtube.com') ||
130+
url.hostname.includes('youtu.be') ||
131+
url.hostname.includes('loom.com')
132+
);
133+
} catch {
134+
return false;
135+
}
136+
},
137+
{ message: 'Please enter a valid YouTube or Loom URL' }
138+
),
139+
tracks: z.array(z.string()).min(1, 'track are required'),
140+
logo_url: z.string().optional(),
141+
cover_url: z.string().optional(),
142+
hackaton_id: z.string().optional(),
143+
user_id: z.string().optional(),
144+
is_winner: z.boolean().optional(),
145+
isDraft: z.boolean().optional(),
146+
})
147+
.refine(
148+
(data) => {
149+
if (data.is_preexisting_idea) {
150+
return data.explanation && data.explanation.length >= 2;
151+
}
152+
return true;
153+
},
154+
{
155+
message: 'explanation is required when the idea is pre-existing',
156+
path: ['explanation'],
157+
}
158+
);
31159

32160
export type SubmissionForm = z.infer<typeof FormSchema>;
33161

@@ -57,18 +185,16 @@ export const useSubmissionFormSecure = () => {
57185
});
58186

59187
const canSubmit = state.isEditing && state.hackathonId;
60-
61-
// ✅ REACTIVE VALIDATION: Auto-validate fields as user types
62-
// useEffect(() => {
63-
// const subscription = form.watch((value, { name, type }) => {
64-
// if (type === 'change' && name) {
65-
// // ✅ Validate specific field that changed
66-
// form.trigger(name as keyof SubmissionForm);
67-
// }
68-
// });
188+
189+
useEffect(() => {
190+
const subscription = form.watch((value, { name, type }) => {
191+
if (type === 'change' && name) {
192+
form.trigger(name as keyof SubmissionForm);
193+
}
194+
});
69195

70-
// return () => subscription.unsubscribe();
71-
// }, [form]);
196+
return () => subscription.unsubscribe();
197+
}, [form]);
72198

73199
const uploadFile = useCallback(async (file: File): Promise<string> => {
74200
if (!state.hackathonId) {

0 commit comments

Comments
 (0)