@@ -7,27 +7,155 @@ import { useSession } from 'next-auth/react';
77import { useToast } from '@/hooks/use-toast' ;
88import { useProjectSubmission } from '../context/ProjectSubmissionContext' ;
99import { 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
32160export 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