diff --git a/app/api/dsoc/mentee/me/route.ts b/app/api/dsoc/mentee/me/route.ts
new file mode 100644
index 0000000..315e08c
--- /dev/null
+++ b/app/api/dsoc/mentee/me/route.ts
@@ -0,0 +1,45 @@
+import { NextRequest, NextResponse } from 'next/server';
+import connectDB from '@/lib/db';
+import { DSOCMentee } from '@/models/DSOCMentee';
+import jwt from 'jsonwebtoken';
+
+export async function GET(request: NextRequest) {
+ try {
+ await connectDB();
+
+ const token = request.cookies.get('dsoc-mentee-token')?.value;
+ if (!token) {
+ return NextResponse.json({ success: false }, { status: 200 });
+ }
+
+ const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as {
+ id: string;
+ role: string;
+ };
+
+ if (decoded.role !== 'dsoc-mentee') {
+ return NextResponse.json({ success: false }, { status: 200 });
+ }
+
+ const mentee = await DSOCMentee.findById(decoded.id)
+ .select('_id name email username isActive')
+ .lean();
+
+ if (!mentee || !mentee.isActive) {
+ return NextResponse.json({ success: false }, { status: 200 });
+ }
+
+ return NextResponse.json({
+ success: true,
+ data: {
+ id: mentee._id,
+ name: mentee.name,
+ email: mentee.email,
+ username: mentee.username,
+ },
+ });
+ } catch (error) {
+ console.error('Error checking DSOC mentee session:', error);
+ return NextResponse.json({ success: false }, { status: 200 });
+ }
+}
diff --git a/app/api/dsoc/projects/[id]/route.ts b/app/api/dsoc/projects/[id]/route.ts
index 6b04aec..255cd63 100644
--- a/app/api/dsoc/projects/[id]/route.ts
+++ b/app/api/dsoc/projects/[id]/route.ts
@@ -1,5 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
import connectDB from '@/lib/db';
+import '@/models/DSOCMentor';
+import '@/models/DSOCMentee';
import { DSOCProject } from '@/models/DSOCProject';
// GET single project by ID
diff --git a/app/dsoc/apply/[id]/page.tsx b/app/dsoc/apply/[id]/page.tsx
index 5d55463..e8b76e7 100644
--- a/app/dsoc/apply/[id]/page.tsx
+++ b/app/dsoc/apply/[id]/page.tsx
@@ -2,7 +2,7 @@
import Link from "next/link";
import { useState, useEffect, use } from "react";
-import { useRouter } from "next/navigation";
+import { useForm } from "react-hook-form";
import {
ArrowLeft,
ArrowRight,
@@ -33,6 +33,24 @@ interface Project {
mentors?: { name: string; company?: string }[];
}
+type ApplicationFormValues = {
+ whyThisProject: string;
+ motivation: string;
+ relevantExperience: string;
+ technicalSkills: string;
+ portfolioLinks: string;
+ githubProfile: string;
+ previousContributions: string;
+ proposal: string;
+ timeline: string;
+ expectedLearnings: string;
+ challenges: string;
+ availability: string;
+ timezone: string;
+ startDate: string;
+ coverLetter: string;
+};
+
const STEPS = [
{ id: 1, title: 'About You', icon: User, description: 'Your background' },
{ id: 2, title: 'Experience', icon: Briefcase, description: 'Skills & projects' },
@@ -42,36 +60,88 @@ const STEPS = [
export default function ApplyPage({ params }: { params: Promise<{ id: string }> }) {
const resolvedParams = use(params);
- const router = useRouter();
const [project, setProject] = useState(null);
const [loading, setLoading] = useState(true);
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState('');
const [success, setSuccess] = useState(false);
const [currentStep, setCurrentStep] = useState(1);
-
- const [formData, setFormData] = useState({
- whyThisProject: '',
- motivation: '',
- relevantExperience: '',
- technicalSkills: '',
- portfolioLinks: '',
- githubProfile: '',
- previousContributions: '',
- proposal: '',
- timeline: '',
- expectedLearnings: '',
- challenges: '',
- availability: '',
- timezone: '',
- startDate: '',
- coverLetter: ''
+ const [isMentee, setIsMentee] = useState(null);
+ const {
+ register,
+ handleSubmit,
+ formState: { errors },
+ trigger,
+ watch,
+ } = useForm({
+ defaultValues: {
+ whyThisProject: '',
+ motivation: '',
+ relevantExperience: '',
+ technicalSkills: '',
+ portfolioLinks: '',
+ githubProfile: '',
+ previousContributions: '',
+ proposal: '',
+ timeline: '',
+ expectedLearnings: '',
+ challenges: '',
+ availability: '',
+ timezone: '',
+ startDate: '',
+ coverLetter: '',
+ },
+ mode: 'onBlur',
});
+ const availabilityValue = watch('availability');
+
+ const stepFields: Record = {
+ 1: ['whyThisProject', 'motivation'],
+ 2: ['relevantExperience', 'technicalSkills', 'githubProfile', 'portfolioLinks'],
+ 3: ['proposal', 'timeline', 'expectedLearnings'],
+ 4: ['availability', 'timezone'],
+ };
+
useEffect(() => {
fetchProject();
}, [resolvedParams.id]);
+ useEffect(() => {
+ checkMenteeSession();
+ }, []);
+
+ const checkMenteeSession = async () => {
+ try {
+ const res = await fetch('/api/dsoc/mentee/me', { credentials: 'include' });
+ const data = await res.json();
+ setIsMentee(Boolean(data?.success));
+ } catch (err) {
+ console.error('Error checking mentee session:', err);
+ setIsMentee(false);
+ }
+ };
+
+ const validateUrl = (value: string) => {
+ if (!value) return true;
+ try {
+ new URL(value);
+ return true;
+ } catch {
+ return false;
+ }
+ };
+
+ const validateUrlList = (value: string) => {
+ if (!value) return true;
+ const entries = value
+ .split(/[\n,]+/)
+ .map((entry) => entry.trim())
+ .filter(Boolean);
+ if (entries.length === 0) return true;
+ return entries.every(validateUrl) || 'Enter a valid URL.';
+ };
+
const fetchProject = async () => {
try {
const res = await fetch(`/api/dsoc/projects/${resolvedParams.id}`);
@@ -90,12 +160,15 @@ export default function ApplyPage({ params }: { params: Promise<{ id: string }>
}
};
- const handleChange = (e: React.ChangeEvent) => {
- setFormData({ ...formData, [e.target.name]: e.target.value });
- };
-
- const nextStep = () => {
+ const nextStep = async () => {
if (currentStep < STEPS.length) {
+ const fields = stepFields[currentStep] || [];
+ const isValid = await trigger(fields);
+ if (!isValid) {
+ setError('Please fix the highlighted fields.');
+ return;
+ }
+ setError('');
setCurrentStep(currentStep + 1);
window.scrollTo({ top: 0, behavior: 'smooth' });
}
@@ -108,19 +181,28 @@ export default function ApplyPage({ params }: { params: Promise<{ id: string }>
}
};
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault();
+ const onSubmit = async (values: ApplicationFormValues) => {
setError('');
setSubmitting(true);
+ if (!isMentee) {
+ setError('Please login as a mentee to apply.');
+ setSubmitting(false);
+ return;
+ }
+
+ const portfolioLinks = values.portfolioLinks
+ ? values.portfolioLinks.split(/[\n,]+/).map((link) => link.trim()).filter(Boolean)
+ : [];
+
try {
const res = await fetch('/api/dsoc/applications', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
projectId: resolvedParams.id,
- ...formData,
- portfolioLinks: formData.portfolioLinks.split('\n').map(s => s.trim()).filter(Boolean)
+ ...values,
+ portfolioLinks,
})
});
@@ -139,6 +221,10 @@ export default function ApplyPage({ params }: { params: Promise<{ id: string }>
}
};
+ const onInvalid = () => {
+ setError('Please fix the highlighted fields.');
+ };
+
const formatDeadline = (date: string) => {
return new Date(date).toLocaleDateString('en-US', {
year: 'numeric',
@@ -359,8 +445,21 @@ export default function ApplyPage({ params }: { params: Promise<{ id: string }>
{/* Form */}
-
+ {errors.relevantExperience?.message && (
+
+ {errors.relevantExperience.message}
+
+ )}
@@ -457,30 +562,38 @@ export default function ApplyPage({ params }: { params: Promise<{ id: string }>
List your programming languages, frameworks, and tools (comma-separated)
+ {errors.technicalSkills?.message && (
+
+ {errors.technicalSkills.message}
+
+ )}
- GitHub Profile
+ GitHub Profile *
validateUrl(value) || 'Enter a valid URL.',
+ })}
className="neo-brutal-input"
placeholder="https://github.com/username"
/>
+ {errors.githubProfile?.message && (
+
+ {errors.githubProfile.message}
+
+ )}
@@ -489,12 +602,15 @@ export default function ApplyPage({ params }: { params: Promise<{ id: string }>
+ {errors.portfolioLinks?.message && (
+
+ {errors.portfolioLinks.message}
+
+ )}
@@ -506,9 +622,7 @@ export default function ApplyPage({ params }: { params: Promise<{ id: string }>
Have you contributed to open source before? Share links or describe your contributions.
How would you approach this project? Describe your understanding and planned implementation.
+ {errors.proposal?.message && (
+
+ {errors.proposal.message}
+
+ )}
@@ -556,14 +672,16 @@ export default function ApplyPage({ params }: { params: Promise<{ id: string }>
Break down the project into phases/milestones. Be realistic about what you can achieve.
+ {errors.timeline?.message && (
+
+ {errors.timeline.message}
+
+ )}
@@ -574,14 +692,16 @@ export default function ApplyPage({ params }: { params: Promise<{ id: string }>
What technical skills or knowledge do you hope to gain?
+ {errors.expectedLearnings?.message && (
+
+ {errors.expectedLearnings.message}
+
+ )}
@@ -592,9 +712,7 @@ export default function ApplyPage({ params }: { params: Promise<{ id: string }>
What challenges do you anticipate? How would you overcome them?
Hours per week you can dedicate
Select availability
@@ -638,6 +753,11 @@ export default function ApplyPage({ params }: { params: Promise<{ id: string }>
25-30 hours/week
30+ hours/week (Full-time)
+ {errors.availability?.message && (
+
+ {errors.availability.message}
+
+ )}
@@ -648,10 +768,7 @@ export default function ApplyPage({ params }: { params: Promise<{ id: string }>
For scheduling calls with mentors
Select timezone
@@ -663,6 +780,11 @@ export default function ApplyPage({ params }: { params: Promise<{ id: string }>
UTC+08:00 (Singapore/China)
UTC+09:00 (Japan/Korea)
+ {errors.timezone?.message && (
+
+ {errors.timezone.message}
+
+ )}
@@ -675,9 +797,7 @@ export default function ApplyPage({ params }: { params: Promise<{ id: string }>
@@ -690,9 +810,7 @@ export default function ApplyPage({ params }: { params: Promise<{ id: string }>
Anything else you'd like the mentors to know about you?
By submitting this application, you confirm that all information provided is accurate and you commit to actively participating in DSOC 2026 if selected.
- ✓ You understand this is a commitment of {formData.availability || 'X hours/week'}
+ ✓ You understand this is a commitment of {availabilityValue || 'X hours/week'}
✓ You will communicate regularly with your mentor
✓ You agree to the DSOC Code of Conduct
@@ -744,7 +862,7 @@ export default function ApplyPage({ params }: { params: Promise<{ id: string }>
) : (
{submitting ? (
@@ -752,7 +870,7 @@ export default function ApplyPage({ params }: { params: Promise<{ id: string }>
) : (
<>
- Submit Application
+ {isMentee === false ? 'Login to Apply' : 'Submit Application'}
>
)}
diff --git a/app/dsoc/projects/[id]/page.tsx b/app/dsoc/projects/[id]/page.tsx
index 2fde66e..29861bd 100644
--- a/app/dsoc/projects/[id]/page.tsx
+++ b/app/dsoc/projects/[id]/page.tsx
@@ -293,11 +293,24 @@ export default function ProjectDetailPage({ params }: { params: Promise<{ id: st
const [project, setProject] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
+ const [isMentee, setIsMentee] = useState(null);
useEffect(() => {
fetchProject();
+ checkMenteeSession();
}, [projectId]);
+ const checkMenteeSession = async () => {
+ try {
+ const res = await fetch('/api/dsoc/mentee/me', { credentials: 'include' });
+ const data = await res.json();
+ setIsMentee(Boolean(data?.success));
+ } catch (err) {
+ console.error('Error checking mentee session:', err);
+ setIsMentee(false);
+ }
+ };
+
const fetchProject = async () => {
if (!projectId) {
setLoading(true);
@@ -356,6 +369,7 @@ export default function ProjectDetailPage({ params }: { params: Promise<{ id: st
const isDeadlinePassed = project ? new Date() > new Date(project.applicationDeadline) : false;
const spotsRemaining = project ? project.maxMentees - (project.selectedMentees?.length || 0) : 0;
+ const canApply = project ? project.status === 'open' && !isDeadlinePassed && spotsRemaining > 0 : false;
if (loading) {
return (
@@ -657,13 +671,29 @@ export default function ProjectDetailPage({ params }: { params: Promise<{ id: st
- {project.status === 'open' && !isDeadlinePassed && spotsRemaining > 0 ? (
-
- Apply Now
-
+ {canApply ? (
+ isMentee === null ? (
+
+ Checking eligibility...
+
+ ) : isMentee ? (
+
+ Apply Now
+
+ ) : (
+
+ Apply as Mentee First
+
+ )
) : (
=12"
}
@@ -5186,6 +5192,7 @@
"integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -5359,6 +5366,7 @@
"integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.8",
@@ -6986,6 +6994,7 @@
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"license": "MIT",
+ "peer": true,
"bin": {
"jiti": "bin/jiti.js"
}
@@ -8452,6 +8461,7 @@
"resolved": "https://registry.npmjs.org/next/-/next-15.5.15.tgz",
"integrity": "sha512-VSqCrJwtLVGwAVE0Sb/yikrQfkwkZW9p+lL/J4+xe+G3ZA+QnWPqgcfH1tDUEuk9y+pthzzVFp4L/U8JerMfMQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@next/env": "15.5.15",
"@swc/helpers": "0.5.15",
@@ -8919,6 +8929,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"nanoid": "^3.3.8",
"picocolors": "^1.1.1",
@@ -9160,6 +9171,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
"integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -9169,6 +9181,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz",
"integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"scheduler": "^0.25.0"
},
@@ -9193,6 +9206,22 @@
"react": "*"
}
},
+ "node_modules/react-hook-form": {
+ "version": "7.76.0",
+ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.76.0.tgz",
+ "integrity": "sha512-eKtLGgFeSgkHqQD8J59AMZ9a4uD1D83iSIzt4YlTGD7liDen5rrjcUO1rVIGd9yC1gofryjtHbv+4ny4hkLWlw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-hook-form"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17 || ^18 || ^19"
+ }
+ },
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@@ -10301,6 +10330,7 @@
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
"integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
@@ -10419,6 +10449,7 @@
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=12"
},
@@ -10605,6 +10636,7 @@
"integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
"dev": true,
"license": "Apache-2.0",
+ "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
diff --git a/package.json b/package.json
index 6a63eda..595547c 100644
--- a/package.json
+++ b/package.json
@@ -45,6 +45,7 @@
"pptxgenjs": "4.0.1",
"react": "19.0.0",
"react-dom": "19.0.0",
+ "react-hook-form": "^7.53.0",
"react-force-graph-2d": "1.27.1",
"react-markdown": "^10.1.0",
"remark-gfm": "^4.0.1",