@@ -8,17 +8,34 @@ import {
88 Save ,
99 Plus ,
1010 Trash2 ,
11- AlertCircle
11+ AlertCircle ,
12+ CheckCircle2 ,
13+ Users
1214} from "lucide-react" ;
1315import "../../../../../dsoc/styles.css" ;
1416
17+ interface MentorOption {
18+ _id : string ;
19+ name : string ;
20+ company ?: string ;
21+ jobTitle ?: string ;
22+ picture ?: string ;
23+ expertise ?: string [ ] ;
24+ }
25+
1526export default function EditProjectPage ( { params } : { params : Promise < { id : string } > } ) {
1627 const resolvedParams = use ( params ) ;
1728 const router = useRouter ( ) ;
1829 const [ loading , setLoading ] = useState ( true ) ;
30+ const [ mentorLoading , setMentorLoading ] = useState ( true ) ;
31+ const [ mentorError , setMentorError ] = useState ( '' ) ;
1932 const [ submitting , setSubmitting ] = useState ( false ) ;
33+ const [ imageUploading , setImageUploading ] = useState ( false ) ;
2034 const [ error , setError ] = useState ( '' ) ;
2135 const [ success , setSuccess ] = useState ( false ) ;
36+ const [ availableMentors , setAvailableMentors ] = useState < MentorOption [ ] > ( [ ] ) ;
37+ const [ imageFile , setImageFile ] = useState < File | null > ( null ) ;
38+ const [ imagePreview , setImagePreview ] = useState < string > ( '' ) ;
2239
2340 const [ formData , setFormData ] = useState ( {
2441 title : '' ,
@@ -35,16 +52,37 @@ export default function EditProjectPage({ params }: { params: Promise<{ id: stri
3552 applicationDeadline : '' ,
3653 startDate : '' ,
3754 endDate : '' ,
55+ mentors : [ ] as string [ ] ,
3856 requirements : [ '' ] ,
3957 learningOutcomes : [ '' ] ,
40- season : '2025' ,
41- status : 'draft'
58+ season : '2026' ,
59+ status : 'draft' ,
60+ featuredImage : ''
4261 } ) ;
4362
4463 useEffect ( ( ) => {
4564 fetchProject ( ) ;
65+ fetchMentors ( ) ;
4666 } , [ resolvedParams . id ] ) ;
4767
68+ const fetchMentors = async ( ) => {
69+ try {
70+ const res = await fetch ( '/api/dsoc/mentors' ) ;
71+ const data = await res . json ( ) ;
72+
73+ if ( data . success ) {
74+ setAvailableMentors ( data . data || [ ] ) ;
75+ } else {
76+ setMentorError ( data . error || 'Failed to load mentors' ) ;
77+ }
78+ } catch ( err ) {
79+ console . error ( 'Error fetching mentors:' , err ) ;
80+ setMentorError ( 'Failed to load mentors' ) ;
81+ } finally {
82+ setMentorLoading ( false ) ;
83+ }
84+ } ;
85+
4886 const fetchProject = async ( ) => {
4987 try {
5088 const res = await fetch ( `/api/dsoc/projects/${ resolvedParams . id } ` ) ;
@@ -64,13 +102,17 @@ export default function EditProjectPage({ params }: { params: Promise<{ id: stri
64102 technologies : Array . isArray ( project . technologies ) ? project . technologies . join ( ', ' ) : '' ,
65103 tags : Array . isArray ( project . tags ) ? project . tags . join ( ', ' ) : '' ,
66104 maxMentees : project . maxMentees || 3 ,
105+ mentors : Array . isArray ( project . mentors )
106+ ? project . mentors . map ( ( mentor : any ) => ( typeof mentor === 'string' ? mentor : mentor ?. _id ) ) . filter ( Boolean )
107+ : [ ] ,
67108 applicationDeadline : project . applicationDeadline ? new Date ( project . applicationDeadline ) . toISOString ( ) . split ( 'T' ) [ 0 ] : '' ,
68109 startDate : project . startDate ? new Date ( project . startDate ) . toISOString ( ) . split ( 'T' ) [ 0 ] : '' ,
69110 endDate : project . endDate ? new Date ( project . endDate ) . toISOString ( ) . split ( 'T' ) [ 0 ] : '' ,
70111 requirements : project . requirements && project . requirements . length > 0 ? project . requirements : [ '' ] ,
71112 learningOutcomes : project . learningOutcomes && project . learningOutcomes . length > 0 ? project . learningOutcomes : [ '' ] ,
72113 season : project . season || '2025' ,
73- status : project . status || 'draft'
114+ status : project . status || 'draft' ,
115+ featuredImage : project . featuredImage || project . imageUrl || ''
74116 } ) ;
75117 } else {
76118 setError ( data . error || 'Failed to load project' ) ;
@@ -93,6 +135,19 @@ export default function EditProjectPage({ params }: { params: Promise<{ id: stri
93135 setFormData ( { ...formData , [ field ] : updated } ) ;
94136 } ;
95137
138+ const toggleMentor = ( mentorId : string ) => {
139+ setFormData ( ( current ) => {
140+ const isSelected = current . mentors . includes ( mentorId ) ;
141+
142+ return {
143+ ...current ,
144+ mentors : isSelected
145+ ? current . mentors . filter ( ( id ) => id !== mentorId )
146+ : [ ...current . mentors , mentorId ]
147+ } ;
148+ } ) ;
149+ } ;
150+
96151 const addArrayItem = ( field : 'requirements' | 'learningOutcomes' ) => {
97152 setFormData ( { ...formData , [ field ] : [ ...formData [ field ] , '' ] } ) ;
98153 } ;
@@ -102,13 +157,53 @@ export default function EditProjectPage({ params }: { params: Promise<{ id: stri
102157 setFormData ( { ...formData , [ field ] : updated } ) ;
103158 } ;
104159
160+ const handleImageChange = ( e : React . ChangeEvent < HTMLInputElement > ) => {
161+ const file = e . target . files ?. [ 0 ] || null ;
162+ setImageFile ( file ) ;
163+
164+ if ( ! file ) {
165+ setImagePreview ( '' ) ;
166+ return ;
167+ }
168+
169+ const reader = new FileReader ( ) ;
170+ reader . onloadend = ( ) => {
171+ setImagePreview ( reader . result as string ) ;
172+ } ;
173+ reader . readAsDataURL ( file ) ;
174+ } ;
175+
176+ const uploadImageToCloudinary = async ( file : File ) => {
177+ const uploadFormData = new FormData ( ) ;
178+ uploadFormData . append ( 'file' , file ) ;
179+
180+ const uploadRes = await fetch ( '/api/upload' , {
181+ method : 'POST' ,
182+ body : uploadFormData ,
183+ } ) ;
184+
185+ if ( ! uploadRes . ok ) {
186+ throw new Error ( 'Image upload failed' ) ;
187+ }
188+
189+ const uploadData = await uploadRes . json ( ) ;
190+ return uploadData . url as string ;
191+ } ;
192+
105193 const handleSubmit = async ( e : React . FormEvent ) => {
106194 e . preventDefault ( ) ;
107195 setError ( '' ) ;
108196 setSuccess ( false ) ;
109197 setSubmitting ( true ) ;
110198
111199 try {
200+ let featuredImage = formData . featuredImage ;
201+
202+ if ( imageFile ) {
203+ setImageUploading ( true ) ;
204+ featuredImage = await uploadImageToCloudinary ( imageFile ) ;
205+ }
206+
112207 const res = await fetch ( `/api/dsoc/projects/${ resolvedParams . id } ` , {
113208 method : 'PUT' ,
114209 headers : { 'Content-Type' : 'application/json' } ,
@@ -124,13 +219,16 @@ export default function EditProjectPage({ params }: { params: Promise<{ id: stri
124219 technologies : formData . technologies . split ( ',' ) . map ( s => s . trim ( ) ) . filter ( Boolean ) ,
125220 tags : formData . tags . split ( ',' ) . map ( s => s . trim ( ) ) . filter ( Boolean ) ,
126221 maxMentees : parseInt ( formData . maxMentees as unknown as string ) ,
222+ mentors : formData . mentors ,
127223 applicationDeadline : formData . applicationDeadline ,
128224 startDate : formData . startDate ,
129225 endDate : formData . endDate ,
130226 requirements : formData . requirements . filter ( Boolean ) ,
131227 learningOutcomes : formData . learningOutcomes . filter ( Boolean ) ,
132228 season : formData . season ,
133- status : formData . status
229+ status : formData . status ,
230+ featuredImage,
231+ imageUrl : featuredImage
134232 } )
135233 } ) ;
136234
@@ -146,8 +244,9 @@ export default function EditProjectPage({ params }: { params: Promise<{ id: stri
146244 }
147245 } catch ( err ) {
148246 console . error ( 'Error updating project:' , err ) ;
149- setError ( 'Something went wrong. Please try again.' ) ;
247+ setError ( err instanceof Error ? err . message : 'Something went wrong. Please try again.' ) ;
150248 } finally {
249+ setImageUploading ( false ) ;
151250 setSubmitting ( false ) ;
152251 }
153252 } ;
@@ -246,6 +345,26 @@ export default function EditProjectPage({ params }: { params: Promise<{ id: stri
246345 placeholder = "Detailed description (shown on project page)"
247346 />
248347 </ div >
348+
349+ < div >
350+ < label className = "block font-bold text-sm mb-2" > Project Image</ label >
351+ < input
352+ type = "file"
353+ accept = "image/*"
354+ onChange = { handleImageChange }
355+ className = "neo-brutal-input"
356+ />
357+ { ( imagePreview || formData . featuredImage ) && (
358+ < div className = "mt-3" >
359+ { /* eslint-disable-next-line @next/next/no-img-element */ }
360+ < img
361+ src = { imagePreview || formData . featuredImage }
362+ alt = "Project preview"
363+ className = "w-full max-w-md h-52 object-cover border-4 border-[var(--dsoc-dark)]"
364+ />
365+ </ div >
366+ ) }
367+ </ div >
249368 </ div >
250369
251370 { /* Links */ }
@@ -368,6 +487,90 @@ export default function EditProjectPage({ params }: { params: Promise<{ id: stri
368487 </ div >
369488 </ div >
370489
490+ { /* Mentors */ }
491+ < div className = "space-y-4" >
492+ < h2 className = "font-bold text-lg border-b-2 border-[var(--dsoc-dark)] pb-2 flex items-center gap-2" >
493+ < Users className = "w-5 h-5" />
494+ Select Mentors
495+ </ h2 >
496+
497+ { mentorLoading ? (
498+ < div className = "p-4 border-2 border-dashed border-[var(--dsoc-dark)] text-sm text-muted-foreground" >
499+ Loading mentors...
500+ </ div >
501+ ) : mentorError ? (
502+ < div className = "p-4 bg-[var(--dsoc-pink)]/10 border-4 border-[var(--dsoc-pink)] text-[var(--dsoc-pink)]" >
503+ { mentorError }
504+ </ div >
505+ ) : availableMentors . length === 0 ? (
506+ < div className = "p-4 border-2 border-dashed border-[var(--dsoc-dark)] text-sm text-muted-foreground" >
507+ No mentors found. Create and verify a mentor first.
508+ </ div >
509+ ) : (
510+ < div className = "grid gap-4 md:grid-cols-2" >
511+ { availableMentors . map ( ( mentor ) => {
512+ const isSelected = formData . mentors . includes ( mentor . _id ) ;
513+
514+ return (
515+ < button
516+ key = { mentor . _id }
517+ type = "button"
518+ onClick = { ( ) => toggleMentor ( mentor . _id ) }
519+ className = { `text-left p-4 border-4 transition-all ${
520+ isSelected
521+ ? 'border-[var(--dsoc-success)] bg-[var(--dsoc-success)]/10'
522+ : 'border-[var(--dsoc-dark)] bg-background hover:-translate-y-1'
523+ } `}
524+ >
525+ < div className = "flex items-start gap-3" >
526+ < div className = "w-12 h-12 rounded-full bg-[var(--dsoc-dark)] text-white flex items-center justify-center font-bold overflow-hidden shrink-0" >
527+ { mentor . picture ? (
528+ // eslint-disable-next-line @next/next/no-img-element
529+ < img src = { mentor . picture } alt = { mentor . name } className = "w-full h-full object-cover" />
530+ ) : (
531+ mentor . name
532+ . split ( ' ' )
533+ . map ( ( part ) => part [ 0 ] )
534+ . join ( '' )
535+ . slice ( 0 , 2 )
536+ ) }
537+ </ div >
538+
539+ < div className = "flex-1 min-w-0" >
540+ < div className = "flex items-start justify-between gap-2" >
541+ < div >
542+ < h3 className = "font-bold text-lg leading-tight" > { mentor . name } </ h3 >
543+ < p className = "text-sm text-muted-foreground" >
544+ { mentor . jobTitle || 'Mentor' } { mentor . company ? ` · ${ mentor . company } ` : '' }
545+ </ p >
546+ </ div >
547+ { isSelected && < CheckCircle2 className = "w-5 h-5 text-[var(--dsoc-success)] shrink-0" /> }
548+ </ div >
549+
550+ { mentor . expertise && mentor . expertise . length > 0 && (
551+ < div className = "flex flex-wrap gap-2 mt-3" >
552+ { mentor . expertise . slice ( 0 , 3 ) . map ( ( skill ) => (
553+ < span key = { skill } className = "px-2 py-1 text-xs font-bold border-2 border-[var(--dsoc-dark)] bg-background" >
554+ { skill }
555+ </ span >
556+ ) ) }
557+ </ div >
558+ ) }
559+ </ div >
560+ </ div >
561+ </ button >
562+ ) ;
563+ } ) }
564+ </ div >
565+ ) }
566+
567+ { formData . mentors . length > 0 && (
568+ < p className = "text-sm font-medium text-[var(--dsoc-success)]" >
569+ { formData . mentors . length } mentor{ formData . mentors . length > 1 ? 's' : '' } selected
570+ </ p >
571+ ) }
572+ </ div >
573+
371574 { /* Timeline */ }
372575 < div className = "space-y-4" >
373576 < h2 className = "font-bold text-lg border-b-2 border-[var(--dsoc-dark)] pb-2" > Timeline</ h2 >
@@ -500,7 +703,7 @@ export default function EditProjectPage({ params }: { params: Promise<{ id: stri
500703 className = "flex items-center gap-2 px-6 py-3 bg-[var(--dsoc-dark)] text-white font-bold border-4 border-[var(--dsoc-dark)] hover:translate-y-1 transition-transform disabled:opacity-50"
501704 >
502705 < Save className = "w-5 h-5" />
503- { submitting ? 'Saving...' : 'Save Changes' }
706+ { imageUploading ? 'Uploading Image...' : submitting ? 'Saving...' : 'Save Changes' }
504707 </ button >
505708
506709 < Link
0 commit comments