Skip to content

Commit 9d26bd3

Browse files
Submission page (#430)
* feat: implement the participating page * fix: fix participating page and ui refinements * fix: fix sidebar active links and rstored earnings page * fix: fix coderabbit corrections * fix: fix coderabbit corrections * feat: Implement unified hackathon submissions dashbaord * fix: fix coderabbit corrections * fix: fix coderabbit corrections * fix: normalize the status by collapsing spaces,underscores,dashes before comparing * fix: normalize the status by collapsing spaces,underscores,dashes before comparing * fix: fix coderabbit corrections
1 parent 6fc1293 commit 9d26bd3

6 files changed

Lines changed: 1055 additions & 117 deletions

File tree

Lines changed: 368 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,370 @@
1-
import { redirect } from 'next/navigation';
1+
'use client';
22

3-
const page = () => {
4-
redirect('/coming-soon');
5-
};
3+
import { useState, useMemo } from 'react';
4+
import { motion, AnimatePresence } from 'framer-motion';
5+
import { useAuthStatus } from '@/hooks/use-auth';
6+
import BoundlessSheet from '@/components/sheet/boundless-sheet';
7+
import EmptyState from '@/components/EmptyState';
8+
import { useRouter } from 'next/navigation';
9+
import { Trophy } from 'lucide-react';
10+
import {
11+
Table,
12+
TableBody,
13+
TableHead,
14+
TableHeader,
15+
TableRow as ShadcnTableRow,
16+
} from '@/components/ui/table';
17+
import {
18+
SortField,
19+
SortDir,
20+
SubmissionRow,
21+
SortIcon,
22+
SubmissionsSheetContent,
23+
TableRow,
24+
} from './submission-components';
625

7-
export default page;
26+
export default function SubmissionsPage() {
27+
const router = useRouter();
28+
const { user, isLoading } = useAuthStatus();
29+
30+
const [sortField, setSortField] = useState<SortField>('submittedAt');
31+
const [sortDir, setSortDir] = useState<SortDir>('desc');
32+
const [selectedSubmission, setSelectedSubmission] =
33+
useState<SubmissionRow | null>(null);
34+
const [sheetOpen, setSheetOpen] = useState(false);
35+
36+
// Pull submissions data from auth state — no extra API calls
37+
const rawSubmissions: SubmissionRow[] = useMemo(() => {
38+
const profile = (user as any)?.profile;
39+
if (!profile) return [];
40+
41+
// Primary path: profile.user.hackathonSubmissionsAsParticipant
42+
const fromUser: any[] =
43+
profile?.user?.hackathonSubmissionsAsParticipant || [];
44+
// Secondary alias (some API shapes expose it at profile level)
45+
const fromProfile: any[] = profile?.hackathonSubmissionsAsParticipant || [];
46+
47+
// Merge & deduplicate by id
48+
const merged = [...fromUser, ...fromProfile];
49+
const seen = new Set<string>();
50+
const deduped = merged.filter(s => {
51+
const id = s?.id || s?._id;
52+
if (!id || seen.has(id)) return false;
53+
seen.add(id);
54+
return true;
55+
});
56+
57+
return deduped.map((s: any) => ({
58+
id: s.id || s._id || '',
59+
projectName: s.projectName || s.title || s.name || 'Untitled Submission',
60+
description: s.description,
61+
introduction: s.introduction,
62+
logo: s.logo,
63+
videoUrl: s.videoUrl,
64+
category: s.category,
65+
links: s.links,
66+
status: s.status || 'draft',
67+
rank: s.rank ?? null,
68+
submittedAt: s.submittedAt || s.submissionDate || s.createdAt || '',
69+
votes: s.votes,
70+
comments: s.comments,
71+
hackathon: s.hackathon,
72+
disqualificationReason: s.disqualificationReason,
73+
}));
74+
}, [user]);
75+
76+
const sorted = useMemo(() => {
77+
return [...rawSubmissions].sort((a, b) => {
78+
let aVal: any;
79+
let bVal: any;
80+
81+
switch (sortField) {
82+
case 'projectName':
83+
aVal = (a.projectName || '').toLowerCase();
84+
bVal = (b.projectName || '').toLowerCase();
85+
break;
86+
case 'hackathon':
87+
aVal = (a.hackathon?.title || a.hackathon?.name || '').toLowerCase();
88+
bVal = (b.hackathon?.title || b.hackathon?.name || '').toLowerCase();
89+
break;
90+
case 'status':
91+
aVal = (a.status || '').toLowerCase();
92+
bVal = (b.status || '').toLowerCase();
93+
break;
94+
case 'submittedAt':
95+
aVal = a.submittedAt ? new Date(a.submittedAt).getTime() : 0;
96+
bVal = b.submittedAt ? new Date(b.submittedAt).getTime() : 0;
97+
break;
98+
case 'rank':
99+
aVal = a.rank ?? Infinity;
100+
bVal = b.rank ?? Infinity;
101+
break;
102+
default:
103+
return 0;
104+
}
105+
106+
if (aVal < bVal) return sortDir === 'asc' ? -1 : 1;
107+
if (aVal > bVal) return sortDir === 'asc' ? 1 : -1;
108+
return 0;
109+
});
110+
}, [rawSubmissions, sortField, sortDir]);
111+
112+
const handleSort = (field: SortField) => {
113+
if (sortField === field) {
114+
setSortDir(d => (d === 'asc' ? 'desc' : 'asc'));
115+
} else {
116+
setSortField(field);
117+
setSortDir('asc');
118+
}
119+
};
120+
121+
const handleRowClick = (submission: SubmissionRow) => {
122+
setSelectedSubmission(submission);
123+
setSheetOpen(true);
124+
};
125+
126+
const getAriaSort = (field: SortField) => {
127+
if (sortField !== field) return 'none';
128+
return sortDir === 'asc' ? 'ascending' : 'descending';
129+
};
130+
131+
const thClass =
132+
'h-12 px-2 text-left align-middle font-medium text-zinc-400 [&:has([role=checkbox])]:pr-0';
133+
134+
if (isLoading) {
135+
return (
136+
<div className='flex h-[400px] items-center justify-center'>
137+
<div className='border-primary h-8 w-8 animate-spin rounded-full border-2 border-t-transparent' />
138+
</div>
139+
);
140+
}
141+
142+
return (
143+
<div className='container mx-auto max-w-7xl px-4 py-8 md:px-6 lg:py-12'>
144+
{/* Page header */}
145+
<motion.div
146+
className='mb-10'
147+
initial={{ opacity: 0, y: -12 }}
148+
animate={{ opacity: 1, y: 0 }}
149+
transition={{ duration: 0.35 }}
150+
>
151+
<h1 className='text-3xl font-bold tracking-tight text-white md:text-4xl'>
152+
My Submissions
153+
</h1>
154+
<p className='mt-2 text-zinc-400'>
155+
Track the full lifecycle of every hackathon submission you&apos;ve
156+
made.
157+
</p>
158+
</motion.div>
159+
160+
{/* Summary strip */}
161+
{rawSubmissions.length > 0 && (
162+
<motion.div
163+
className='mb-6 flex flex-wrap gap-4'
164+
initial={{ opacity: 0 }}
165+
animate={{ opacity: 1 }}
166+
transition={{ delay: 0.15 }}
167+
>
168+
{(
169+
[
170+
{
171+
label: 'Total',
172+
value: rawSubmissions.length,
173+
color: 'text-white',
174+
},
175+
{
176+
label: 'Ranked',
177+
value: rawSubmissions.filter(s => {
178+
const st = (s.status || '').toLowerCase();
179+
return (
180+
st === 'ranked' || st === 'shortlisted' || st === 'winner'
181+
);
182+
}).length,
183+
color: 'text-primary',
184+
},
185+
{
186+
label: 'Under Review',
187+
value: rawSubmissions.filter(s => {
188+
const st = (s.status || '')
189+
.toLowerCase()
190+
.replace(/[\s\-_]+/g, '_');
191+
return st === 'under_review' || st === 'submitted';
192+
}).length,
193+
color: 'text-amber-400',
194+
},
195+
{
196+
label: 'Draft',
197+
value: rawSubmissions.filter(
198+
s => (s.status || '').toLowerCase() === 'draft'
199+
).length,
200+
color: 'text-zinc-400',
201+
},
202+
] as const
203+
).map(stat => (
204+
<div
205+
key={stat.label}
206+
className='flex items-center gap-2 rounded-lg border border-white/5 bg-white/[0.03] px-4 py-2 text-sm'
207+
>
208+
<span className={`text-lg font-bold ${stat.color}`}>
209+
{stat.value}
210+
</span>
211+
<span className='text-zinc-500'>{stat.label}</span>
212+
</div>
213+
))}
214+
</motion.div>
215+
)}
216+
217+
{/* Table */}
218+
<AnimatePresence mode='wait'>
219+
{sorted.length > 0 ? (
220+
<motion.div
221+
key='table'
222+
initial={{ opacity: 0, y: 10 }}
223+
animate={{ opacity: 1, y: 0 }}
224+
exit={{ opacity: 0 }}
225+
transition={{ duration: 0.3 }}
226+
className='overflow-hidden rounded-xl border border-white/5 bg-white/[0.025]'
227+
>
228+
<div className='overflow-x-auto'>
229+
<Table className='min-w-[560px]'>
230+
<TableHeader>
231+
<ShadcnTableRow className='border-white/5 hover:bg-transparent'>
232+
<TableHead
233+
className={`${thClass} pr-3 pl-4`}
234+
aria-sort={getAriaSort('projectName')}
235+
>
236+
<button
237+
type='button'
238+
onClick={() => handleSort('projectName')}
239+
className='flex w-full items-center gap-1 rounded-sm text-xs font-semibold tracking-wider uppercase hover:text-white focus-visible:ring-2 focus-visible:ring-[#a7f950] focus-visible:ring-offset-1 focus-visible:ring-offset-[#0e0c0c] focus-visible:outline-none'
240+
aria-label='Sort by Project Name'
241+
>
242+
Project
243+
<SortIcon
244+
field='projectName'
245+
sortField={sortField}
246+
sortDir={sortDir}
247+
/>
248+
</button>
249+
</TableHead>
250+
<TableHead
251+
className={`${thClass} hidden px-3 sm:table-cell`}
252+
aria-sort={getAriaSort('hackathon')}
253+
>
254+
<button
255+
type='button'
256+
onClick={() => handleSort('hackathon')}
257+
className='flex w-full items-center gap-1 rounded-sm text-xs font-semibold tracking-wider uppercase hover:text-white focus-visible:ring-2 focus-visible:ring-[#a7f950] focus-visible:ring-offset-1 focus-visible:ring-offset-[#0e0c0c] focus-visible:outline-none'
258+
aria-label='Sort by Hackathon'
259+
>
260+
Hackathon
261+
<SortIcon
262+
field='hackathon'
263+
sortField={sortField}
264+
sortDir={sortDir}
265+
/>
266+
</button>
267+
</TableHead>
268+
<TableHead
269+
className={`${thClass} px-3`}
270+
aria-sort={getAriaSort('status')}
271+
>
272+
<button
273+
type='button'
274+
onClick={() => handleSort('status')}
275+
className='flex w-full items-center gap-1 rounded-sm text-xs font-semibold tracking-wider uppercase hover:text-white focus-visible:ring-2 focus-visible:ring-[#a7f950] focus-visible:ring-offset-1 focus-visible:ring-offset-[#0e0c0c] focus-visible:outline-none'
276+
aria-label='Sort by Status'
277+
>
278+
Status
279+
<SortIcon
280+
field='status'
281+
sortField={sortField}
282+
sortDir={sortDir}
283+
/>
284+
</button>
285+
</TableHead>
286+
<TableHead
287+
className={`${thClass} hidden px-3 lg:table-cell`}
288+
aria-sort={getAriaSort('submittedAt')}
289+
>
290+
<button
291+
type='button'
292+
onClick={() => handleSort('submittedAt')}
293+
className='flex w-full items-center gap-1 rounded-sm text-xs font-semibold tracking-wider uppercase hover:text-white focus-visible:ring-2 focus-visible:ring-[#a7f950] focus-visible:ring-offset-1 focus-visible:ring-offset-[#0e0c0c] focus-visible:outline-none'
294+
aria-label='Sort by Submitted Date'
295+
>
296+
Submitted
297+
<SortIcon
298+
field='submittedAt'
299+
sortField={sortField}
300+
sortDir={sortDir}
301+
/>
302+
</button>
303+
</TableHead>
304+
<TableHead
305+
className={`${thClass} hidden px-3 xl:table-cell`}
306+
aria-sort={getAriaSort('rank')}
307+
>
308+
<button
309+
type='button'
310+
onClick={() => handleSort('rank')}
311+
className='flex w-full items-center gap-1 rounded-sm text-xs font-semibold tracking-wider uppercase hover:text-white focus-visible:ring-2 focus-visible:ring-[#a7f950] focus-visible:ring-offset-1 focus-visible:ring-offset-[#0e0c0c] focus-visible:outline-none'
312+
aria-label='Sort by Rank'
313+
>
314+
Rank
315+
<SortIcon
316+
field='rank'
317+
sortField={sortField}
318+
sortDir={sortDir}
319+
/>
320+
</button>
321+
</TableHead>
322+
<TableHead className='py-3 pr-4 pl-3' />
323+
</ShadcnTableRow>
324+
</TableHeader>
325+
<TableBody>
326+
{sorted.map((submission, i) => (
327+
<TableRow
328+
key={submission.id}
329+
submission={submission}
330+
index={i}
331+
onClick={() => handleRowClick(submission)}
332+
/>
333+
))}
334+
</TableBody>
335+
</Table>
336+
</div>
337+
</motion.div>
338+
) : (
339+
<motion.div
340+
key='empty'
341+
initial={{ opacity: 0, y: 10 }}
342+
animate={{ opacity: 1, y: 0 }}
343+
exit={{ opacity: 0 }}
344+
transition={{ duration: 0.3 }}
345+
>
346+
<EmptyState
347+
title='No submissions yet'
348+
description="You haven't made any submission to any hackathons yet. Explore open hackathons and start building!"
349+
buttonText='Explore Hackathons'
350+
onAddClick={() => router.push('/hackathons')}
351+
/>
352+
</motion.div>
353+
)}
354+
</AnimatePresence>
355+
356+
{/* Detail sheet */}
357+
<BoundlessSheet
358+
open={sheetOpen}
359+
setOpen={setSheetOpen}
360+
title={selectedSubmission?.projectName}
361+
size='xl'
362+
minHeight='500px'
363+
>
364+
{selectedSubmission && (
365+
<SubmissionsSheetContent submission={selectedSubmission} />
366+
)}
367+
</BoundlessSheet>
368+
</div>
369+
);
370+
}

0 commit comments

Comments
 (0)