Skip to content

Commit 22eedf6

Browse files
Merge pull request #28 from devweekends/assign-mentor-page
refactor: assign mentor to project and upload image functionality
2 parents 516c9e9 + 989c9e3 commit 22eedf6

10 files changed

Lines changed: 602 additions & 57 deletions

File tree

.DS_Store

2 KB
Binary file not shown.

app/admin/dsoc/projects/[id]/edit/page.tsx

Lines changed: 210 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,34 @@ import {
88
Save,
99
Plus,
1010
Trash2,
11-
AlertCircle
11+
AlertCircle,
12+
CheckCircle2,
13+
Users
1214
} from "lucide-react";
1315
import "../../../../../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+
1526
export 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

Comments
 (0)