From e869164a272a9c99c0c72150281722805b1a4e21 Mon Sep 17 00:00:00 2001 From: Talha Amjad Date: Sat, 9 May 2026 15:54:22 +0500 Subject: [PATCH 1/2] Assign mentor to project functionality --- app/admin/dsoc/projects/[id]/edit/page.tsx | 137 +++++++++++++- app/admin/dsoc/projects/new/page.tsx | 202 ++++++++++++++++++--- app/api/dsoc/mentors/route.ts | 29 +++ 3 files changed, 340 insertions(+), 28 deletions(-) create mode 100644 app/api/dsoc/mentors/route.ts diff --git a/app/admin/dsoc/projects/[id]/edit/page.tsx b/app/admin/dsoc/projects/[id]/edit/page.tsx index 0293b7e..0c77fb8 100644 --- a/app/admin/dsoc/projects/[id]/edit/page.tsx +++ b/app/admin/dsoc/projects/[id]/edit/page.tsx @@ -8,17 +8,31 @@ import { Save, Plus, Trash2, - AlertCircle + AlertCircle, + CheckCircle2, + Users } from "lucide-react"; import "../../../../../dsoc/styles.css"; +interface MentorOption { + _id: string; + name: string; + company?: string; + jobTitle?: string; + picture?: string; + expertise?: string[]; +} + export default function EditProjectPage({ params }: { params: Promise<{ id: string }> }) { const resolvedParams = use(params); const router = useRouter(); const [loading, setLoading] = useState(true); + const [mentorLoading, setMentorLoading] = useState(true); + const [mentorError, setMentorError] = useState(''); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(''); const [success, setSuccess] = useState(false); + const [availableMentors, setAvailableMentors] = useState([]); const [formData, setFormData] = useState({ title: '', @@ -35,6 +49,7 @@ export default function EditProjectPage({ params }: { params: Promise<{ id: stri applicationDeadline: '', startDate: '', endDate: '', + mentors: [] as string[], requirements: [''], learningOutcomes: [''], season: '2025', @@ -43,8 +58,27 @@ export default function EditProjectPage({ params }: { params: Promise<{ id: stri useEffect(() => { fetchProject(); + fetchMentors(); }, [resolvedParams.id]); + const fetchMentors = async () => { + try { + const res = await fetch('/api/dsoc/mentors'); + const data = await res.json(); + + if (data.success) { + setAvailableMentors(data.data || []); + } else { + setMentorError(data.error || 'Failed to load mentors'); + } + } catch (err) { + console.error('Error fetching mentors:', err); + setMentorError('Failed to load mentors'); + } finally { + setMentorLoading(false); + } + }; + const fetchProject = async () => { try { const res = await fetch(`/api/dsoc/projects/${resolvedParams.id}`); @@ -64,6 +98,9 @@ export default function EditProjectPage({ params }: { params: Promise<{ id: stri technologies: Array.isArray(project.technologies) ? project.technologies.join(', ') : '', tags: Array.isArray(project.tags) ? project.tags.join(', ') : '', maxMentees: project.maxMentees || 3, + mentors: Array.isArray(project.mentors) + ? project.mentors.map((mentor: any) => (typeof mentor === 'string' ? mentor : mentor?._id)).filter(Boolean) + : [], applicationDeadline: project.applicationDeadline ? new Date(project.applicationDeadline).toISOString().split('T')[0] : '', startDate: project.startDate ? new Date(project.startDate).toISOString().split('T')[0] : '', endDate: project.endDate ? new Date(project.endDate).toISOString().split('T')[0] : '', @@ -93,6 +130,19 @@ export default function EditProjectPage({ params }: { params: Promise<{ id: stri setFormData({ ...formData, [field]: updated }); }; + const toggleMentor = (mentorId: string) => { + setFormData((current) => { + const isSelected = current.mentors.includes(mentorId); + + return { + ...current, + mentors: isSelected + ? current.mentors.filter((id) => id !== mentorId) + : [...current.mentors, mentorId] + }; + }); + }; + const addArrayItem = (field: 'requirements' | 'learningOutcomes') => { setFormData({ ...formData, [field]: [...formData[field], ''] }); }; @@ -124,6 +174,7 @@ export default function EditProjectPage({ params }: { params: Promise<{ id: stri technologies: formData.technologies.split(',').map(s => s.trim()).filter(Boolean), tags: formData.tags.split(',').map(s => s.trim()).filter(Boolean), maxMentees: parseInt(formData.maxMentees as unknown as string), + mentors: formData.mentors, applicationDeadline: formData.applicationDeadline, startDate: formData.startDate, endDate: formData.endDate, @@ -368,6 +419,90 @@ export default function EditProjectPage({ params }: { params: Promise<{ id: stri + {/* Mentors */} +
+

+ + Select Mentors +

+ + {mentorLoading ? ( +
+ Loading mentors... +
+ ) : mentorError ? ( +
+ {mentorError} +
+ ) : availableMentors.length === 0 ? ( +
+ No mentors found. Create and verify a mentor first. +
+ ) : ( +
+ {availableMentors.map((mentor) => { + const isSelected = formData.mentors.includes(mentor._id); + + return ( + + ); + })} +
+ )} + + {formData.mentors.length > 0 && ( +

+ {formData.mentors.length} mentor{formData.mentors.length > 1 ? 's' : ''} selected +

+ )} +
+ {/* Timeline */}

Timeline

diff --git a/app/admin/dsoc/projects/new/page.tsx b/app/admin/dsoc/projects/new/page.tsx index 5d826a3..104eeb3 100644 --- a/app/admin/dsoc/projects/new/page.tsx +++ b/app/admin/dsoc/projects/new/page.tsx @@ -1,21 +1,35 @@ 'use client'; import Link from "next/link"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; -import { +import { ArrowLeft, - Save, + CheckCircle2, Plus, - Trash2 + Save, + Trash2, + Users, } from "lucide-react"; import "../../../../dsoc/styles.css"; +interface MentorOption { + _id: string; + name: string; + company?: string; + jobTitle?: string; + picture?: string; + expertise?: string[]; +} + export default function NewProjectPage() { const router = useRouter(); const [loading, setLoading] = useState(false); + const [mentorLoading, setMentorLoading] = useState(true); + const [mentorError, setMentorError] = useState(''); const [error, setError] = useState(''); - + const [availableMentors, setAvailableMentors] = useState([]); + const [formData, setFormData] = useState({ title: '', description: '', @@ -31,16 +45,50 @@ export default function NewProjectPage() { applicationDeadline: '', startDate: '', endDate: '', + mentors: [] as string[], requirements: [''], learningOutcomes: [''], - season: '2025' + season: '2025', }); - const handleChange = (e: React.ChangeEvent) => { - setFormData({ ...formData, [e.target.name]: e.target.value }); + useEffect(() => { + fetchMentors(); + }, []); + + const fetchMentors = async () => { + try { + const res = await fetch('/api/dsoc/mentors'); + const data = await res.json(); + + if (data.success) { + setAvailableMentors(data.data || []); + } else { + setMentorError(data.error || 'Failed to load mentors'); + } + } catch (err) { + console.error('Error fetching mentors:', err); + setMentorError('Failed to load mentors'); + } finally { + setMentorLoading(false); + } + }; + + const handleChange = ( + e: React.ChangeEvent, + ) => { + const { name, value } = e.target; + + setFormData((current) => ({ + ...current, + [name]: name === 'maxMentees' ? Number(value) : value, + })); }; - const handleArrayChange = (field: 'requirements' | 'learningOutcomes', index: number, value: string) => { + const handleArrayChange = ( + field: 'requirements' | 'learningOutcomes', + index: number, + value: string, + ) => { const updated = [...formData[field]]; updated[index] = value; setFormData({ ...formData, [field]: updated }); @@ -55,6 +103,19 @@ export default function NewProjectPage() { setFormData({ ...formData, [field]: updated }); }; + const toggleMentor = (mentorId: string) => { + setFormData((current) => { + const isSelected = current.mentors.includes(mentorId); + + return { + ...current, + mentors: isSelected + ? current.mentors.filter((id) => id !== mentorId) + : [...current.mentors, mentorId], + }; + }); + }; + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(''); @@ -66,12 +127,13 @@ export default function NewProjectPage() { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...formData, - technologies: formData.technologies.split(',').map(s => s.trim()).filter(Boolean), - tags: formData.tags.split(',').map(s => s.trim()).filter(Boolean), + technologies: formData.technologies.split(',').map((s) => s.trim()).filter(Boolean), + tags: formData.tags.split(',').map((s) => s.trim()).filter(Boolean), + mentors: formData.mentors, requirements: formData.requirements.filter(Boolean), learningOutcomes: formData.learningOutcomes.filter(Boolean), - status: 'draft' - }) + status: 'draft', + }), }); const data = await res.json(); @@ -93,7 +155,7 @@ export default function NewProjectPage() {
- @@ -114,7 +176,7 @@ export default function NewProjectPage() { {/* Basic Info */}

Basic Information

- +
+ {/* Mentors */} +
+

+ + Select Mentors +

+ + {mentorLoading ? ( +
+ Loading mentors... +
+ ) : mentorError ? ( +
+ {mentorError} +
+ ) : availableMentors.length === 0 ? ( +
+ No mentors found. Create and verify a mentor first. +
+ ) : ( +
+ {availableMentors.map((mentor) => { + const isSelected = formData.mentors.includes(mentor._id); + + return ( + + ); + })} +
+ )} + + {formData.mentors.length > 0 && ( +

+ {formData.mentors.length} mentor{formData.mentors.length > 1 ? 's' : ''} selected +

+ )} +
+ {/* Links */}

Links

- +
@@ -201,7 +347,7 @@ export default function NewProjectPage() { {/* Project Details */}

Project Details

- +
@@ -274,7 +420,7 @@ export default function NewProjectPage() { {/* Timeline */}

Timeline

- +
@@ -328,7 +474,7 @@ export default function NewProjectPage() { {/* Requirements */}

Requirements

- + {formData.requirements.map((req, index) => (
))} +
@@ -362,7 +509,7 @@ export default function NewProjectPage() { {/* Learning Outcomes */}

Learning Outcomes

- + {formData.learningOutcomes.map((outcome, index) => (
))} +
@@ -396,9 +544,9 @@ export default function NewProjectPage() { @@ -407,4 +555,4 @@ export default function NewProjectPage() {
); -} +} \ No newline at end of file diff --git a/app/api/dsoc/mentors/route.ts b/app/api/dsoc/mentors/route.ts new file mode 100644 index 0000000..82430d6 --- /dev/null +++ b/app/api/dsoc/mentors/route.ts @@ -0,0 +1,29 @@ +import { NextResponse } from 'next/server'; +import connectDB from '@/lib/db'; +import { DSOCMentor } from '@/models/DSOCMentor'; + +// GET active, verified mentors for project assignment +export async function GET() { + try { + await connectDB(); + + const mentors = await DSOCMentor.find({ + isActive: true, + isVerified: true, + }) + .select('_id name company jobTitle picture expertise') + .sort({ name: 1 }) + .lean(); + + return NextResponse.json({ + success: true, + data: mentors, + }); + } catch (error) { + console.error('Error fetching DSOC mentors:', error); + return NextResponse.json( + { success: false, error: 'Failed to fetch mentors' }, + { status: 500 } + ); + } +} \ No newline at end of file From 989c9e3b5643d6bff047ea4c3c5bec2f1b93cebc Mon Sep 17 00:00:00 2001 From: Talha Amjad Date: Mon, 11 May 2026 16:37:35 +0500 Subject: [PATCH 2/2] Add upload image functionality for projects --- .DS_Store | Bin 8196 -> 10244 bytes app/admin/dsoc/projects/[id]/edit/page.tsx | 80 +++++++++++++++++++-- app/admin/dsoc/projects/new/page.tsx | 78 +++++++++++++++++++- app/api/dsoc/projects/[id]/route.ts | 8 ++- app/api/dsoc/projects/route.ts | 11 ++- app/dsoc/apply/[id]/page.tsx | 2 +- app/dsoc/page.tsx | 12 ++-- app/dsoc/projects/[id]/page.tsx | 55 +++++++++++++- app/dsoc/projects/page.tsx | 47 +++++++++--- 9 files changed, 263 insertions(+), 30 deletions(-) diff --git a/.DS_Store b/.DS_Store index ab0c8f18878049336f4fe81b751eaee3775a18a4..45c1a6e8210e06f01f632c6c739e9ae0c10ed79c 100644 GIT binary patch literal 10244 zcmeHMU1%It6h3!1$!7bLY|^C07GY^#q`zq>)KW^fX^IAsmTrh?({{T*lg*gfnPp~@ z^vCMH3ECG$s0#K;i@s@nP!Obo#=eLmRDBU!5T67^TB=y6-@P-td++Q{?L(11%)rdO zJNG;1p7Y&%&b@b*h}hUdX**Gxh`Mp}GPMeiHWzQ_cgq#?-%O!okSAK85@|+xd$wq` zpraQN0}%re0}%re0}%rc0|QvI`PIGBE2C$`K*T`AK!X9EAJVvaneoe-SBlhuhd2VD z?7(Yrp^kHakV*Sx#xHALDMX>uJ3TO8l=(*t=82;{&EY6Bep&NMPn?)1PR#F_`6m=6 zdWT$Ez=>tNGI~Y~L=3btz|Zap+Db(-C@-JiD^(9X%L6Nbs959U1;Z+!rF;JTTi?X5 zeYLM0s&pTy>Ky-CQ17h=!?T%fy!9(^4IMH!>}YlE$3F;er1mB& z?b>BN*iMnnIQzmrn!^Yc7!%|X&`s4DKA^M=g!8D-tp#vFDG-~+A3Q`L)W1coEO1! z1(snxi|BC4QEe14aZ!qiwla#vu?r`{bj$YN*tn@~e_NfhH~)oIe2ag}ed}%K+dZRK z_A())dCc>i;VS-7*~?3uy9Pq`?6X}B&jNa6pTbd!H&o-^bl@9k9@gYlOBg?8HQHnw z`ezz8NbSv;+pb;hS7eDUWUvZ)E`tY0>3iE>Ci+&|<^Gp1U94yD0H$d}7)G&UcMaVj zes~TpsB=@~QhC&Tlwv2%TxrV77T*5H>fBt9AE8|yoz_;xD*Aw1wHn;$7?~2R(`X4i zA!lKZwL}r)nM<~`3z8Q9t_{gR#EIM$idB7ko@Wq6m+axpo9{NqKfM+(*Y1IDM#lZ%Jz)0q#;dwgC7JXbY@>~{DZRpk1`8scE)7qL5X(4RWa6~1dyN1D$PMMoEO zU?&2bbtW`%++EICzw7xzh_YUqwTGV(o(#XCB?5{YDDW#V{t&RWX4d3V7X z%iA;j*Q8|`g?c?-n9{YSTxwvyWx9FY)NCQ3moz|*J*%4~YiZ0{FiVb*&3X;VWOp)` z8ajJ+bmWQ6=^djx&uvbh-8DLb`mWJ)=aSun!`pTr&dnPY(|VZ?5B&8HnusO!dIdif zpY-CQ?-TpF?$ghyldG3t9yD=ef?|t2KojE2iHC_^ys?v>vE|zg?w?oY;$Y( zTZUzi84KD>+15+hYQCtO;<%nMaN};CjN9PGkgai*!+Zbm_#DnWU9UXuRZ=wURdB2r zsMq|G4PVi(i~6K#nObr`}8SYrJv|``kN(Kifv$< z*bcUb?PX7~33ikf*&NduPsq4p^?`oO`(1xB2Q_7&<&;97-?@Z zPXbbHkAEVO%95RsJPjtWZBls?sWSwra7OeCypZ(Be7*5fgD(Zqskc zSKL#T<~!2^;s5_XxDF#t5d#qe4;=%nV>~;K&AeDm+F6G!B&Bhm#LX|h>eReaES`99 z2!Ag=$Kwb8YABXZ2 f|0}2J==|@9nH5xdE;Vrr8=i=|C|DE?}5 zv*Z@$$@^s;Cr^-DF|jOMkQrz$5J+$X30IJP8w([]); + const [imageFile, setImageFile] = useState(null); + const [imagePreview, setImagePreview] = useState(''); const [formData, setFormData] = useState({ title: '', @@ -52,8 +55,9 @@ export default function EditProjectPage({ params }: { params: Promise<{ id: stri mentors: [] as string[], requirements: [''], learningOutcomes: [''], - season: '2025', - status: 'draft' + season: '2026', + status: 'draft', + featuredImage: '' }); useEffect(() => { @@ -107,7 +111,8 @@ export default function EditProjectPage({ params }: { params: Promise<{ id: stri requirements: project.requirements && project.requirements.length > 0 ? project.requirements : [''], learningOutcomes: project.learningOutcomes && project.learningOutcomes.length > 0 ? project.learningOutcomes : [''], season: project.season || '2025', - status: project.status || 'draft' + status: project.status || 'draft', + featuredImage: project.featuredImage || project.imageUrl || '' }); } else { setError(data.error || 'Failed to load project'); @@ -152,6 +157,39 @@ export default function EditProjectPage({ params }: { params: Promise<{ id: stri setFormData({ ...formData, [field]: updated }); }; + const handleImageChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0] || null; + setImageFile(file); + + if (!file) { + setImagePreview(''); + return; + } + + const reader = new FileReader(); + reader.onloadend = () => { + setImagePreview(reader.result as string); + }; + reader.readAsDataURL(file); + }; + + const uploadImageToCloudinary = async (file: File) => { + const uploadFormData = new FormData(); + uploadFormData.append('file', file); + + const uploadRes = await fetch('/api/upload', { + method: 'POST', + body: uploadFormData, + }); + + if (!uploadRes.ok) { + throw new Error('Image upload failed'); + } + + const uploadData = await uploadRes.json(); + return uploadData.url as string; + }; + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(''); @@ -159,6 +197,13 @@ export default function EditProjectPage({ params }: { params: Promise<{ id: stri setSubmitting(true); try { + let featuredImage = formData.featuredImage; + + if (imageFile) { + setImageUploading(true); + featuredImage = await uploadImageToCloudinary(imageFile); + } + const res = await fetch(`/api/dsoc/projects/${resolvedParams.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, @@ -181,7 +226,9 @@ export default function EditProjectPage({ params }: { params: Promise<{ id: stri requirements: formData.requirements.filter(Boolean), learningOutcomes: formData.learningOutcomes.filter(Boolean), season: formData.season, - status: formData.status + status: formData.status, + featuredImage, + imageUrl: featuredImage }) }); @@ -197,8 +244,9 @@ export default function EditProjectPage({ params }: { params: Promise<{ id: stri } } catch (err) { console.error('Error updating project:', err); - setError('Something went wrong. Please try again.'); + setError(err instanceof Error ? err.message : 'Something went wrong. Please try again.'); } finally { + setImageUploading(false); setSubmitting(false); } }; @@ -297,6 +345,26 @@ export default function EditProjectPage({ params }: { params: Promise<{ id: stri placeholder="Detailed description (shown on project page)" />
+ +
+ + + {(imagePreview || formData.featuredImage) && ( +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + Project preview +
+ )} +
{/* Links */} @@ -635,7 +703,7 @@ export default function EditProjectPage({ params }: { params: Promise<{ id: stri 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" > - {submitting ? 'Saving...' : 'Save Changes'} + {imageUploading ? 'Uploading Image...' : submitting ? 'Saving...' : 'Save Changes'} ([]); + const [imageFile, setImageFile] = useState(null); + const [imagePreview, setImagePreview] = useState(''); const [formData, setFormData] = useState({ title: '', @@ -48,7 +51,8 @@ export default function NewProjectPage() { mentors: [] as string[], requirements: [''], learningOutcomes: [''], - season: '2025', + season: '2026', + featuredImage: '', }); useEffect(() => { @@ -116,12 +120,56 @@ export default function NewProjectPage() { }); }; + const handleImageChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0] || null; + setImageFile(file); + + if (!file) { + setImagePreview(''); + return; + } + + const reader = new FileReader(); + reader.onloadend = () => { + setImagePreview(reader.result as string); + }; + reader.readAsDataURL(file); + }; + + const uploadImageToCloudinary = async (file: File) => { + const uploadFormData = new FormData(); + uploadFormData.append('file', file); + + const uploadRes = await fetch('/api/upload', { + method: 'POST', + body: uploadFormData, + }); + + if (!uploadRes.ok) { + throw new Error('Image upload failed'); + } + + const uploadData = await uploadRes.json(); + return uploadData.url as string; + }; + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(''); setLoading(true); try { + let featuredImage = formData.featuredImage; + + if (imageFile) { + setImageUploading(true); + featuredImage = await uploadImageToCloudinary(imageFile); + } + + if (!featuredImage) { + throw new Error('Project image is required'); + } + const res = await fetch('/api/dsoc/projects', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -132,6 +180,8 @@ export default function NewProjectPage() { mentors: formData.mentors, requirements: formData.requirements.filter(Boolean), learningOutcomes: formData.learningOutcomes.filter(Boolean), + featuredImage, + imageUrl: featuredImage, status: 'draft', }), }); @@ -145,8 +195,9 @@ export default function NewProjectPage() { } } catch (err) { console.error('Error creating project:', err); - setError('Something went wrong. Please try again.'); + setError(err instanceof Error ? err.message : 'Something went wrong. Please try again.'); } finally { + setImageUploading(false); setLoading(false); } }; @@ -227,6 +278,27 @@ export default function NewProjectPage() { placeholder="Detailed description (shown on project page)" />
+ +
+ + + {(imagePreview || formData.featuredImage) && ( +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + Project preview +
+ )} +
{/* Mentors */} @@ -547,7 +619,7 @@ export default function NewProjectPage() { className="inline-flex items-center justify-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 w-full" > - {loading ? 'Creating Project...' : 'Create Project'} + {imageUploading ? 'Uploading Image...' : loading ? 'Creating Project...' : 'Create Project'}
diff --git a/app/api/dsoc/projects/[id]/route.ts b/app/api/dsoc/projects/[id]/route.ts index eb84877..6b04aec 100644 --- a/app/api/dsoc/projects/[id]/route.ts +++ b/app/api/dsoc/projects/[id]/route.ts @@ -47,10 +47,16 @@ export async function PUT( // TODO: Add admin/mentor authentication check const body = await request.json(); + + // Support both legacy imageUrl and schema-native featuredImage. + const normalizedBody = { + ...body, + featuredImage: body.featuredImage || body.imageUrl, + }; const project = await DSOCProject.findByIdAndUpdate( id, - { $set: body }, + { $set: normalizedBody }, { new: true, runValidators: true } ); diff --git a/app/api/dsoc/projects/route.ts b/app/api/dsoc/projects/route.ts index 1d1a27d..646900a 100644 --- a/app/api/dsoc/projects/route.ts +++ b/app/api/dsoc/projects/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; import connectDB from '@/lib/db'; +import '@/models/DSOCMentor'; import { DSOCProject } from '@/models/DSOCProject'; // GET all projects with filtering @@ -70,8 +71,14 @@ export async function POST(request: NextRequest) { // TODO: Add admin authentication check const body = await request.json(); - - const project = new DSOCProject(body); + + // Support both legacy imageUrl and schema-native featuredImage. + const normalizedBody = { + ...body, + featuredImage: body.featuredImage || body.imageUrl, + }; + + const project = new DSOCProject(normalizedBody); await project.save(); return NextResponse.json({ diff --git a/app/dsoc/apply/[id]/page.tsx b/app/dsoc/apply/[id]/page.tsx index 56ae6f3..5d55463 100644 --- a/app/dsoc/apply/[id]/page.tsx +++ b/app/dsoc/apply/[id]/page.tsx @@ -246,7 +246,7 @@ export default function ApplyPage({ params }: { params: Promise<{ id: string }>
{/* Project Info Card */}
-
+
diff --git a/app/dsoc/page.tsx b/app/dsoc/page.tsx index b4f126e..baf66f0 100644 --- a/app/dsoc/page.tsx +++ b/app/dsoc/page.tsx @@ -52,7 +52,7 @@ const SAMPLE_PROJECTS: Project[] = [ title: 'VoiceyBill', description: 'VoiceyBill is an open source AI-powered finance tracker that lets people log income and expenses by voice, receipt scan, or manual entry.\nIt uses Gemini and UpliftAI to classify transactions, supports CSV imports and recurring entries, and keeps the experience fast for multilingual users.\nThe product is designed around a responsive, multi-theme UI with analytics and monthly reports.', organization: 'Dev Weekends', - difficulty: 'advanced', + difficulty: 'intermediate', technologies: ['MongoDB', 'Express.js', 'React', 'Node.js', 'TypeScript', 'Google Gemini AI', 'UpliftAI', 'Cloudinary', 'Redux Toolkit', 'RTK Query'], status: 'open', duration: '10-12 weeks', @@ -64,7 +64,7 @@ const SAMPLE_PROJECTS: Project[] = [ title: 'Pathment', description: 'Pathment is a SaaS mentorship platform for organizations that want structured, mentor-guided employee training.\nIt connects mentees with real mentors, uses task-based workflows and progress tracking, and adds gamification to boost engagement and measurable outcomes.', organization: 'Dev Weekends', - difficulty: 'advanced', + difficulty: 'intermediate', technologies: ['Next.js', 'TypeScript', 'Node.js', 'Express', 'PostgreSQL', 'Sequelize', 'Socket.io', 'Tailwind CSS', 'Tiptap', 'Cloudinary'], status: 'open', duration: '10-12 weeks', @@ -279,14 +279,14 @@ export default function DSOCPage() { Apply as Mentee - + {/* Become a Mentor - + */}
{/* Quick Links */} -
+ {/* +
*/}
diff --git a/app/dsoc/projects/[id]/page.tsx b/app/dsoc/projects/[id]/page.tsx index aa0162e..4c89ed9 100644 --- a/app/dsoc/projects/[id]/page.tsx +++ b/app/dsoc/projects/[id]/page.tsx @@ -72,7 +72,7 @@ The goal is to help investors in Pakistan make better decisions and keep portfol organization: 'Dev Weekends', repositoryUrl: 'https://github.com/Wajahat43/psxworth', websiteUrl: 'https://psxworth.com', - difficulty: 'advanced', + difficulty: 'intermediate', duration: '10-12 weeks', technologies: ['Next.js', 'React', 'TypeScript', 'Tailwind CSS', 'shadcn/ui', 'Radix UI', 'Clerk', 'Drizzle ORM', 'PostgreSQL', 'TanStack Query', 'TanStack Table', 'PostHog', 'Upstash Redis', 'AI SDK', 'ApexCharts', 'Recharts', 'React Hook Form', 'Zod'], tags: ['fintech', 'portfolio-tracking', 'dashboard', 'open-source', 'psx'], @@ -128,7 +128,7 @@ The project is built on the MERN stack with TypeScript and integrates Google Gem organization: 'Dev Weekends', repositoryUrl: 'https://github.com/voiceyBill/voiceyBill-web', websiteUrl: 'https://www.voiceybill.com/', - difficulty: 'advanced', + difficulty: 'intermediate', duration: '10-12 weeks', technologies: ['MongoDB', 'Express.js', 'React', 'Node.js', 'TypeScript', 'Google Gemini AI', 'UpliftAI', 'Cloudinary', 'Redux Toolkit', 'RTK Query'], tags: ['full-stack', 'ai', 'finance', 'voice-input', 'open-source'], @@ -180,7 +180,7 @@ The project is built on the MERN stack with TypeScript and integrates Google Gem organization: 'Dev Weekends', repositoryUrl: 'https://github.com/Sheryar-Ahmed/pathment.git', websiteUrl: 'https://pathment.me/', - difficulty: 'advanced', + difficulty: 'intermediate', duration: '10-12 weeks', technologies: ['Next.js', 'TypeScript', 'Node.js', 'Express', 'PostgreSQL', 'Sequelize', 'Socket.io', 'Tailwind CSS', 'Tiptap', 'Cloudinary'], tags: ['full-stack', 'real-time', 'developer-tools', 'collaboration'], @@ -235,6 +235,55 @@ The project is built on the MERN stack with TypeScript and integrates Google Gem ], season: 'DSOC 2026' }, + 'goalslot': { + _id: 'goalslot', + title: 'GoalSlot', + description: "GoalSlot connects your goals to your calendar and tracks every hour you work on them. Finally know if you're actually making progress.", + longDescription: `GoalSlot helps users tie their goals directly to calendar events and measure the time spent working on them. Contributors will work on syncing calendar events, building time-tracking UX, improving analytics, and making the onboarding experience delightful. Project images and demo videos are available in the provided Drive links. + +Contributions range from frontend Next.js improvements to backend NestJS API work, integrations, and analytics features. This is a real-user product with an existing live site, so contributors should be cautious when touching production integrations and focus on well-scoped, testable changes.`, + organization: 'Dev Weekends', + repositoryUrl: 'https://github.com/ZeeshanAdilButt/goal-slot-web', + websiteUrl: 'https://www.goalslot.io/', + difficulty: 'intermediate', + duration: '8-12 weeks', + technologies: ['Next.js', 'NestJS', 'TypeScript'], + tags: ['productivity', 'calendar', 'time-tracking', 'open-source'], + mentors: [ + { + _id: 'mentor-psx-1', + name: 'Wajahat Islam Gul', + email: 'amina@devweekends.org', + company: 'Nector Social', + jobTitle: 'Software Engineer', + linkedin: 'https://www.linkedin.com/in/wajahatx1/', + github: 'https://github.com/Wajahat43' + } + ], + selectedMentees: [], + maxMentees: 3, + status: 'open', + applicationDeadline: '2026-06-30', + startDate: '2026-07-01', + endDate: '2026-09-30', + requirements: [ + 'Familiarity with React and Next.js', + 'Comfort with TypeScript', + 'Willingness to work with calendar integrations and APIs', + 'Ability to commit 8-12 hours per week' + ], + learningOutcomes: [ + 'Implement reliable calendar syncing and time-tracking features', + 'Build end-to-end features across Next.js frontend and NestJS backend', + 'Design analytics and reporting for goal progress' + ], + milestones: [ + { title: 'Onboarding & Setup', description: 'Get the monorepo and local dev environment working', dueDate: '2026-07-07', completed: false }, + { title: 'Calendar Sync', description: 'Implement calendar integration and event mapping', dueDate: '2026-08-01', completed: false }, + { title: 'Time Tracking UI', description: 'Add time tracking flows and analytics dashboards', dueDate: '2026-08-31', completed: false } + ], + season: 'DSOC 2026' + }, }; export default function ProjectDetailPage({ params }: { params: Promise<{ id: string }> }) { diff --git a/app/dsoc/projects/page.tsx b/app/dsoc/projects/page.tsx index 780da67..96ec7b9 100644 --- a/app/dsoc/projects/page.tsx +++ b/app/dsoc/projects/page.tsx @@ -40,6 +40,7 @@ interface Project { jobTitle?: string; }[]; applicationDeadline: string; + featuredImage?: string; imageUrl?: string; wikiUrl?: string; } @@ -53,7 +54,7 @@ const SAMPLE_PROJECTS: Project[] = [ organization: 'Dev Weekends', repositoryUrl: 'https://github.com/Wajahat43/psxworth', websiteUrl: 'https://psxworth.com', - difficulty: 'advanced', + difficulty: 'intermediate', duration: '10-12 weeks', technologies: ['Next.js', 'React', 'TypeScript', 'Tailwind CSS', 'shadcn/ui', 'Radix UI', 'Clerk', 'Drizzle ORM', 'PostgreSQL', 'TanStack Query', 'TanStack Table', 'PostHog', 'Upstash Redis', 'AI SDK', 'ApexCharts', 'Recharts', 'React Hook Form', 'Zod', 'NestJS'], tags: ['full-stack', 'fintech', 'portfolio-tracking', 'dashboard', 'open-source'], @@ -79,7 +80,7 @@ const SAMPLE_PROJECTS: Project[] = [ organization: 'Dev Weekends', repositoryUrl: 'https://github.com/voiceyBill/voiceyBill-web', websiteUrl: 'https://www.voiceybill.com/', - difficulty: 'advanced', + difficulty: 'intermediate', duration: '10-12 weeks', technologies: ['MongoDB', 'Express.js', 'React', 'Node.js', 'TypeScript', 'Google Gemini AI', 'UpliftAI', 'Cloudinary', 'Redux Toolkit', 'RTK Query'], tags: ['full-stack', 'ai', 'finance', 'voice-input', 'open-source'], @@ -104,7 +105,7 @@ const SAMPLE_PROJECTS: Project[] = [ organization: 'Dev Weekends', repositoryUrl: 'https://github.com/Sheryar-Ahmed/pathment.git', websiteUrl: 'https://pathment.me/', - difficulty: 'advanced', + difficulty: 'intermediate', duration: '10-12 weeks', technologies: ['Next.js', 'TypeScript', 'Node.js', 'Express', 'PostgreSQL', 'Sequelize', 'Socket.io', 'Tailwind CSS', 'Tiptap', 'Cloudinary'], tags: ['full-stack', 'real-time', 'developer-tools', 'collaboration'], @@ -122,6 +123,32 @@ const SAMPLE_PROJECTS: Project[] = [ applicationDeadline: '2026-05-25', imageUrl: '/images/dsoc/pathment.png', }, + { + _id: 'goalslot', + title: 'GoalSlot', + description: "GoalSlot connects your goals to your calendar and tracks every hour you work on them. Finally know if you're actually making progress.", + organization: 'Dev Weekends', + repositoryUrl: 'https://github.com/ZeeshanAdilButt/goal-slot-web', + websiteUrl: 'https://www.goalslot.io/', + difficulty: 'intermediate', + duration: '8-12 weeks', + technologies: ['Next.js', 'NestJS', 'TypeScript'], + tags: ['productivity', 'calendar', 'time-tracking', 'open-source'], + status: 'open', + maxMentees: 3, + selectedMentees: [], + mentors: [ + { + _id: 'mentor-psx-1', + name: 'Wajahat Ali', + picture: undefined, + company: 'Nector Social', + jobTitle: 'Software Engineer' + } + ], + applicationDeadline: '2026-06-30', + imageUrl: '/images/dsoc/goalslot.png', + }, ]; export default function ProjectsPage() { @@ -355,14 +382,17 @@ export default function ProjectsPage() {
- {projects.map((project) => ( + {projects.map((project) => { + const projectImage = project.featuredImage || project.imageUrl; + + return (
{/* Project Image */} - {project.imageUrl && ( + {projectImage && (
{project.title} @@ -379,7 +409,7 @@ export default function ProjectsPage() { {project.organization} - {!project.imageUrl && ( + {!projectImage && (
{project.status} @@ -482,7 +512,8 @@ export default function ProjectsPage() {
- ))} + ); + })}
)}