-
Notifications
You must be signed in to change notification settings - Fork 86
Submission page #430
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Benjtalkshow
merged 15 commits into
boundlessfi:main
from
Michaelkingsdev:submission-page
Mar 3, 2026
+1,055
−117
Merged
Submission page #430
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
68cbe21
feat: implement the participating page
Michaelkingsdev ebbca82
Merge branch 'main' of https://github.com/Michaelkingsdev/boundless
Michaelkingsdev 504a581
fix: fix participating page and ui refinements
Michaelkingsdev 8f2010d
fix: fix sidebar active links and rstored earnings page
Michaelkingsdev 7a3e0b5
Merge branch 'main' into participating-page
Michaelkingsdev e038b15
fix: fix coderabbit corrections
Michaelkingsdev 955b8d3
fix: fix coderabbit corrections
Michaelkingsdev f1d7add
Merge branch 'main' of https://github.com/Michaelkingsdev/boundless i…
Michaelkingsdev 0197f93
Merge branch 'main' of https://github.com/Michaelkingsdev/boundless i…
Michaelkingsdev da8de17
feat: Implement unified hackathon submissions dashbaord
Michaelkingsdev 4788c79
fix: fix coderabbit corrections
Michaelkingsdev 26129b2
fix: fix coderabbit corrections
Michaelkingsdev 1dd31a1
fix: normalize the status by collapsing spaces,underscores,dashes bef…
Michaelkingsdev 08b4ce6
fix: normalize the status by collapsing spaces,underscores,dashes bef…
Michaelkingsdev 8860d57
fix: fix coderabbit corrections
Michaelkingsdev File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,370 @@ | ||
| import { redirect } from 'next/navigation'; | ||
| 'use client'; | ||
|
|
||
| const page = () => { | ||
| redirect('/coming-soon'); | ||
| }; | ||
| import { useState, useMemo } from 'react'; | ||
| import { motion, AnimatePresence } from 'framer-motion'; | ||
| import { useAuthStatus } from '@/hooks/use-auth'; | ||
| import BoundlessSheet from '@/components/sheet/boundless-sheet'; | ||
| import EmptyState from '@/components/EmptyState'; | ||
| import { useRouter } from 'next/navigation'; | ||
| import { Trophy } from 'lucide-react'; | ||
| import { | ||
| Table, | ||
| TableBody, | ||
| TableHead, | ||
| TableHeader, | ||
| TableRow as ShadcnTableRow, | ||
| } from '@/components/ui/table'; | ||
| import { | ||
| SortField, | ||
| SortDir, | ||
| SubmissionRow, | ||
| SortIcon, | ||
| SubmissionsSheetContent, | ||
| TableRow, | ||
| } from './submission-components'; | ||
|
|
||
| export default page; | ||
| export default function SubmissionsPage() { | ||
| const router = useRouter(); | ||
| const { user, isLoading } = useAuthStatus(); | ||
|
|
||
| const [sortField, setSortField] = useState<SortField>('submittedAt'); | ||
| const [sortDir, setSortDir] = useState<SortDir>('desc'); | ||
| const [selectedSubmission, setSelectedSubmission] = | ||
| useState<SubmissionRow | null>(null); | ||
| const [sheetOpen, setSheetOpen] = useState(false); | ||
|
|
||
| // Pull submissions data from auth state — no extra API calls | ||
| const rawSubmissions: SubmissionRow[] = useMemo(() => { | ||
| const profile = (user as any)?.profile; | ||
| if (!profile) return []; | ||
|
|
||
| // Primary path: profile.user.hackathonSubmissionsAsParticipant | ||
| const fromUser: any[] = | ||
| profile?.user?.hackathonSubmissionsAsParticipant || []; | ||
| // Secondary alias (some API shapes expose it at profile level) | ||
| const fromProfile: any[] = profile?.hackathonSubmissionsAsParticipant || []; | ||
|
|
||
| // Merge & deduplicate by id | ||
| const merged = [...fromUser, ...fromProfile]; | ||
| const seen = new Set<string>(); | ||
| const deduped = merged.filter(s => { | ||
| const id = s?.id || s?._id; | ||
| if (!id || seen.has(id)) return false; | ||
| seen.add(id); | ||
| return true; | ||
| }); | ||
|
|
||
| return deduped.map((s: any) => ({ | ||
| id: s.id || s._id || '', | ||
| projectName: s.projectName || s.title || s.name || 'Untitled Submission', | ||
| description: s.description, | ||
| introduction: s.introduction, | ||
| logo: s.logo, | ||
| videoUrl: s.videoUrl, | ||
| category: s.category, | ||
| links: s.links, | ||
| status: s.status || 'draft', | ||
| rank: s.rank ?? null, | ||
| submittedAt: s.submittedAt || s.submissionDate || s.createdAt || '', | ||
| votes: s.votes, | ||
| comments: s.comments, | ||
| hackathon: s.hackathon, | ||
| disqualificationReason: s.disqualificationReason, | ||
| })); | ||
| }, [user]); | ||
|
|
||
| const sorted = useMemo(() => { | ||
| return [...rawSubmissions].sort((a, b) => { | ||
| let aVal: any; | ||
| let bVal: any; | ||
|
|
||
| switch (sortField) { | ||
| case 'projectName': | ||
| aVal = (a.projectName || '').toLowerCase(); | ||
| bVal = (b.projectName || '').toLowerCase(); | ||
| break; | ||
| case 'hackathon': | ||
| aVal = (a.hackathon?.title || a.hackathon?.name || '').toLowerCase(); | ||
| bVal = (b.hackathon?.title || b.hackathon?.name || '').toLowerCase(); | ||
| break; | ||
| case 'status': | ||
| aVal = (a.status || '').toLowerCase(); | ||
| bVal = (b.status || '').toLowerCase(); | ||
| break; | ||
| case 'submittedAt': | ||
| aVal = a.submittedAt ? new Date(a.submittedAt).getTime() : 0; | ||
| bVal = b.submittedAt ? new Date(b.submittedAt).getTime() : 0; | ||
| break; | ||
Benjtalkshow marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| case 'rank': | ||
| aVal = a.rank ?? Infinity; | ||
| bVal = b.rank ?? Infinity; | ||
| break; | ||
| default: | ||
| return 0; | ||
| } | ||
|
|
||
| if (aVal < bVal) return sortDir === 'asc' ? -1 : 1; | ||
| if (aVal > bVal) return sortDir === 'asc' ? 1 : -1; | ||
| return 0; | ||
| }); | ||
| }, [rawSubmissions, sortField, sortDir]); | ||
|
|
||
| const handleSort = (field: SortField) => { | ||
| if (sortField === field) { | ||
| setSortDir(d => (d === 'asc' ? 'desc' : 'asc')); | ||
| } else { | ||
| setSortField(field); | ||
| setSortDir('asc'); | ||
| } | ||
| }; | ||
|
|
||
| const handleRowClick = (submission: SubmissionRow) => { | ||
| setSelectedSubmission(submission); | ||
| setSheetOpen(true); | ||
| }; | ||
|
|
||
| const getAriaSort = (field: SortField) => { | ||
| if (sortField !== field) return 'none'; | ||
| return sortDir === 'asc' ? 'ascending' : 'descending'; | ||
| }; | ||
|
|
||
| const thClass = | ||
| 'h-12 px-2 text-left align-middle font-medium text-zinc-400 [&:has([role=checkbox])]:pr-0'; | ||
|
|
||
| if (isLoading) { | ||
| return ( | ||
| <div className='flex h-[400px] items-center justify-center'> | ||
| <div className='border-primary h-8 w-8 animate-spin rounded-full border-2 border-t-transparent' /> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| return ( | ||
| <div className='container mx-auto max-w-7xl px-4 py-8 md:px-6 lg:py-12'> | ||
| {/* Page header */} | ||
| <motion.div | ||
| className='mb-10' | ||
| initial={{ opacity: 0, y: -12 }} | ||
| animate={{ opacity: 1, y: 0 }} | ||
| transition={{ duration: 0.35 }} | ||
| > | ||
| <h1 className='text-3xl font-bold tracking-tight text-white md:text-4xl'> | ||
| My Submissions | ||
| </h1> | ||
| <p className='mt-2 text-zinc-400'> | ||
| Track the full lifecycle of every hackathon submission you've | ||
| made. | ||
| </p> | ||
| </motion.div> | ||
|
|
||
| {/* Summary strip */} | ||
| {rawSubmissions.length > 0 && ( | ||
| <motion.div | ||
| className='mb-6 flex flex-wrap gap-4' | ||
| initial={{ opacity: 0 }} | ||
| animate={{ opacity: 1 }} | ||
| transition={{ delay: 0.15 }} | ||
| > | ||
| {( | ||
| [ | ||
| { | ||
| label: 'Total', | ||
| value: rawSubmissions.length, | ||
| color: 'text-white', | ||
| }, | ||
| { | ||
| label: 'Ranked', | ||
| value: rawSubmissions.filter(s => { | ||
| const st = (s.status || '').toLowerCase(); | ||
| return ( | ||
| st === 'ranked' || st === 'shortlisted' || st === 'winner' | ||
| ); | ||
| }).length, | ||
| color: 'text-primary', | ||
| }, | ||
| { | ||
| label: 'Under Review', | ||
| value: rawSubmissions.filter(s => { | ||
| const st = (s.status || '') | ||
| .toLowerCase() | ||
| .replace(/[\s\-_]+/g, '_'); | ||
| return st === 'under_review' || st === 'submitted'; | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }).length, | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| color: 'text-amber-400', | ||
| }, | ||
| { | ||
| label: 'Draft', | ||
| value: rawSubmissions.filter( | ||
| s => (s.status || '').toLowerCase() === 'draft' | ||
| ).length, | ||
| color: 'text-zinc-400', | ||
| }, | ||
| ] as const | ||
| ).map(stat => ( | ||
| <div | ||
| key={stat.label} | ||
| className='flex items-center gap-2 rounded-lg border border-white/5 bg-white/[0.03] px-4 py-2 text-sm' | ||
| > | ||
| <span className={`text-lg font-bold ${stat.color}`}> | ||
| {stat.value} | ||
| </span> | ||
| <span className='text-zinc-500'>{stat.label}</span> | ||
| </div> | ||
| ))} | ||
| </motion.div> | ||
| )} | ||
|
|
||
| {/* Table */} | ||
| <AnimatePresence mode='wait'> | ||
| {sorted.length > 0 ? ( | ||
| <motion.div | ||
| key='table' | ||
| initial={{ opacity: 0, y: 10 }} | ||
| animate={{ opacity: 1, y: 0 }} | ||
| exit={{ opacity: 0 }} | ||
| transition={{ duration: 0.3 }} | ||
| className='overflow-hidden rounded-xl border border-white/5 bg-white/[0.025]' | ||
| > | ||
| <div className='overflow-x-auto'> | ||
| <Table className='min-w-[560px]'> | ||
| <TableHeader> | ||
| <ShadcnTableRow className='border-white/5 hover:bg-transparent'> | ||
| <TableHead | ||
| className={`${thClass} pr-3 pl-4`} | ||
| aria-sort={getAriaSort('projectName')} | ||
| > | ||
| <button | ||
| type='button' | ||
| onClick={() => handleSort('projectName')} | ||
| 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' | ||
| aria-label='Sort by Project Name' | ||
| > | ||
| Project | ||
| <SortIcon | ||
| field='projectName' | ||
| sortField={sortField} | ||
| sortDir={sortDir} | ||
| /> | ||
| </button> | ||
| </TableHead> | ||
| <TableHead | ||
| className={`${thClass} hidden px-3 sm:table-cell`} | ||
| aria-sort={getAriaSort('hackathon')} | ||
| > | ||
| <button | ||
| type='button' | ||
| onClick={() => handleSort('hackathon')} | ||
| 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' | ||
| aria-label='Sort by Hackathon' | ||
| > | ||
| Hackathon | ||
| <SortIcon | ||
| field='hackathon' | ||
| sortField={sortField} | ||
| sortDir={sortDir} | ||
| /> | ||
| </button> | ||
| </TableHead> | ||
| <TableHead | ||
| className={`${thClass} px-3`} | ||
| aria-sort={getAriaSort('status')} | ||
| > | ||
| <button | ||
| type='button' | ||
| onClick={() => handleSort('status')} | ||
| 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' | ||
| aria-label='Sort by Status' | ||
| > | ||
| Status | ||
| <SortIcon | ||
| field='status' | ||
| sortField={sortField} | ||
| sortDir={sortDir} | ||
| /> | ||
| </button> | ||
| </TableHead> | ||
| <TableHead | ||
| className={`${thClass} hidden px-3 lg:table-cell`} | ||
| aria-sort={getAriaSort('submittedAt')} | ||
| > | ||
| <button | ||
| type='button' | ||
| onClick={() => handleSort('submittedAt')} | ||
| 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' | ||
| aria-label='Sort by Submitted Date' | ||
| > | ||
| Submitted | ||
| <SortIcon | ||
| field='submittedAt' | ||
| sortField={sortField} | ||
| sortDir={sortDir} | ||
| /> | ||
| </button> | ||
| </TableHead> | ||
| <TableHead | ||
| className={`${thClass} hidden px-3 xl:table-cell`} | ||
| aria-sort={getAriaSort('rank')} | ||
| > | ||
| <button | ||
| type='button' | ||
| onClick={() => handleSort('rank')} | ||
| 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' | ||
| aria-label='Sort by Rank' | ||
| > | ||
| Rank | ||
| <SortIcon | ||
| field='rank' | ||
| sortField={sortField} | ||
| sortDir={sortDir} | ||
| /> | ||
| </button> | ||
| </TableHead> | ||
| <TableHead className='py-3 pr-4 pl-3' /> | ||
| </ShadcnTableRow> | ||
| </TableHeader> | ||
| <TableBody> | ||
| {sorted.map((submission, i) => ( | ||
| <TableRow | ||
| key={submission.id} | ||
| submission={submission} | ||
| index={i} | ||
| onClick={() => handleRowClick(submission)} | ||
| /> | ||
| ))} | ||
| </TableBody> | ||
| </Table> | ||
| </div> | ||
| </motion.div> | ||
| ) : ( | ||
| <motion.div | ||
| key='empty' | ||
| initial={{ opacity: 0, y: 10 }} | ||
| animate={{ opacity: 1, y: 0 }} | ||
| exit={{ opacity: 0 }} | ||
| transition={{ duration: 0.3 }} | ||
| > | ||
| <EmptyState | ||
| title='No submissions yet' | ||
| description="You haven't made any submission to any hackathons yet. Explore open hackathons and start building!" | ||
| buttonText='Explore Hackathons' | ||
| onAddClick={() => router.push('/hackathons')} | ||
| /> | ||
| </motion.div> | ||
| )} | ||
| </AnimatePresence> | ||
|
|
||
| {/* Detail sheet */} | ||
| <BoundlessSheet | ||
| open={sheetOpen} | ||
| setOpen={setSheetOpen} | ||
| title={selectedSubmission?.projectName} | ||
| size='xl' | ||
| minHeight='500px' | ||
| > | ||
| {selectedSubmission && ( | ||
| <SubmissionsSheetContent submission={selectedSubmission} /> | ||
| )} | ||
| </BoundlessSheet> | ||
| </div> | ||
| ); | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.