Skip to content

Commit 54c36bf

Browse files
authored
feat(hackathons): owner submission visibility, team UX, organizer teams page (#552)
Submission visibility on the public submissions tab - Surface the user's own submission first on the hackathon submissions tab, regardless of submissionStatusVisibility setting. Team members (non-leader) see their team's submission too. Find Team tab UX - Stop replacing the whole tab with MyTeamView when the user has a team. Render MyTeamView at the top and the open-teams list below. - Filter the user's own team out of the "Other Teams" list to avoid duplication. - Wire the team count badge on the team-formation tab. Team details - New /hackathons/[slug]/teams/[teamId] page with team header, members list (clickable links to profiles), roles needed, leader card, and contact card. - TeamCard click opens the team details page in a new tab. - MyTeamView team name is now a clickable link to the same page. - Add useTeam hook + query key. Organizer submissions page - Replace raw Buttons on review actions with BoundlessButton (default / outline / destructive) so colors come from the design system. - Remove the "Before Deadline" gating on the Approve action; backend organizer override now allows it. Organizer participants page - Add Solo Participants and Teams metric cards next to the existing Total Participants and Total Submissions cards. Organizer submissions page - Add a metric strip with Total / Solo / Team Submissions sourced from the statistics endpoint. Organizer Teams page (new) - New route /organizations/[id]/hackathons/[hackathonId]/teams. - Stats (Total / Open / Submitted / Not submitted), search by team or leader (server-side), status and submission filters (client-side), server-side pagination matching the participants page pattern, click to view team details in a new tab. - Add "Teams" entry to the existing organizer hackathon sidebar (HackathonSidebar.tsx). Image fit fixes for cropped logos/banners - SubmissionCard banner and logo overlay use object-contain (banner was object-cover, cropping wordmarks and tall banners). - /me/hackathons/submissions: banner, logo overlay, and small table thumbnail all use object-contain. - ProjectSidebarHeader logo uses object-contain. Type changes - lib/api/hackathons/teams.ts Team interface gains optional hasSubmission flag.
1 parent 8f1c753 commit 54c36bf

16 files changed

Lines changed: 853 additions & 64 deletions

File tree

app/(landing)/hackathons/[slug]/components/tabs/contents/FindTeam.tsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,9 @@ const FindTeam = () => {
8888
!!hackathon?.id && hackathon.participantType !== 'INDIVIDUAL'
8989
);
9090

91-
const teams = teamsResponse?.data?.teams || [];
91+
const teams = (teamsResponse?.data?.teams || []).filter(
92+
t => !myTeam || t.id !== myTeam.id
93+
);
9294

9395
const handleJoin = (team: Team) => {
9496
setSelectedTeam(team);
@@ -99,14 +101,6 @@ const FindTeam = () => {
99101

100102
const isIndividualOnly = hackathon.participantType === 'INDIVIDUAL';
101103

102-
if (myTeam) {
103-
return (
104-
<TabsContent value='team-formation' className='mt-0 w-full outline-none'>
105-
<MyTeamView team={myTeam} hackathonSlug={slug} />
106-
</TabsContent>
107-
);
108-
}
109-
110104
return (
111105
<TabsContent
112106
value='team-formation'
@@ -129,9 +123,18 @@ const FindTeam = () => {
129123
</div>
130124
) : (
131125
<>
126+
{myTeam && (
127+
<div className='space-y-4'>
128+
<MyTeamView team={myTeam} hackathonSlug={slug} />
129+
<div className='h-px w-full bg-white/5' />
130+
</div>
131+
)}
132+
132133
<div className='flex items-center justify-between'>
133134
<div>
134-
<h2 className='text-2xl font-bold text-white'>Open Teams</h2>
135+
<h2 className='text-2xl font-bold text-white'>
136+
{myTeam ? 'Other Teams' : 'Open Teams'}
137+
</h2>
135138
<p className='mt-1 text-sm text-gray-500'>
136139
Find builders to collaborate with on your project.
137140
</p>

app/(landing)/hackathons/[slug]/components/tabs/contents/submissions/SubmissionCard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ const SubmissionCard = ({ submission }: SubmissionCardProps) => {
9494
<img
9595
src={banner}
9696
alt={`${projectName} banner`}
97-
className='h-full w-full object-cover transition-transform duration-300 group-hover:scale-105'
97+
className='h-full w-full object-contain transition-transform duration-300 group-hover:scale-105'
9898
/>
9999
) : logo ? (
100100
// eslint-disable-next-line @next/next/no-img-element

app/(landing)/hackathons/[slug]/components/tabs/contents/teams/MyTeamView.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,14 @@ const MyTeamView = ({ team, hackathonSlug }: MyTeamViewProps) => {
186186
</div>
187187
<div>
188188
<h2 className='text-xl font-bold text-white sm:text-2xl'>
189-
{team.teamName}
189+
<a
190+
href={`/hackathons/${hackathonSlug}/teams/${team.id}`}
191+
target='_blank'
192+
rel='noopener noreferrer'
193+
className='hover:text-primary transition-colors'
194+
>
195+
{team.teamName}
196+
</a>
190197
</h2>
191198
<div className='mt-1 flex flex-wrap items-center gap-2 sm:gap-3'>
192199
<span className='text-primary text-sm font-bold'>

app/(landing)/hackathons/[slug]/components/tabs/contents/teams/TeamCard.tsx

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use client';
22

3-
import React from 'react';
3+
import React, { useCallback } from 'react';
4+
import { useParams } from 'next/navigation';
45
import { cn } from '@/lib/utils';
56
import { Team, TeamMember } from '@/lib/api/hackathons/teams';
67
import GroupAvatar from '@/components/avatars/GroupAvatar';
@@ -14,6 +15,16 @@ interface TeamCardProps {
1415
}
1516

1617
const TeamCard = ({ team, onJoin, onMessageLeader }: TeamCardProps) => {
18+
const { slug } = useParams<{ slug: string }>();
19+
20+
const openTeamDetails = useCallback(() => {
21+
if (!slug || !team.id) return;
22+
window.open(
23+
`/hackathons/${slug}/teams/${team.id}`,
24+
'_blank',
25+
'noopener,noreferrer'
26+
);
27+
}, [slug, team.id]);
1728
const {
1829
teamName,
1930
description,
@@ -32,7 +43,18 @@ const TeamCard = ({ team, onJoin, onMessageLeader }: TeamCardProps) => {
3243
};
3344

3445
return (
35-
<div className='group hover:border-primary/20 flex h-full flex-col overflow-hidden rounded-2xl border border-white/5 bg-[#0A0B0D] p-8 transition-all hover:bg-[#0D0F12]'>
46+
<div
47+
role='link'
48+
tabIndex={0}
49+
onClick={openTeamDetails}
50+
onKeyDown={e => {
51+
if (e.key === 'Enter' || e.key === ' ') {
52+
e.preventDefault();
53+
openTeamDetails();
54+
}
55+
}}
56+
className='group hover:border-primary/20 flex h-full cursor-pointer flex-col overflow-hidden rounded-2xl border border-white/5 bg-[#0A0B0D] p-8 transition-all hover:bg-[#0D0F12]'
57+
>
3658
<div className='mb-6 flex flex-col gap-6 sm:flex-row sm:items-start sm:justify-between'>
3759
<div className='flex items-start gap-4'>
3860
<div className='text-primary flex h-14 w-14 shrink-0 items-center justify-center rounded-xl bg-[#232B20] text-xl font-black'>
@@ -70,7 +92,10 @@ const TeamCard = ({ team, onJoin, onMessageLeader }: TeamCardProps) => {
7092
variant='outline'
7193
size='sm'
7294
className='h-11 shrink-0 rounded-xl border-white/10 bg-white/5 px-4 text-sm font-bold text-white transition-all hover:bg-white/10'
73-
onClick={e => onMessageLeader(team, e.currentTarget)}
95+
onClick={e => {
96+
e.stopPropagation();
97+
onMessageLeader(team, e.currentTarget);
98+
}}
7499
aria-label='Message team leader'
75100
>
76101
<MessageCircle className='mr-2 h-4 w-4' />
@@ -81,7 +106,10 @@ const TeamCard = ({ team, onJoin, onMessageLeader }: TeamCardProps) => {
81106
variant='outline'
82107
size='sm'
83108
className='border-primary/20 text-primary hover:bg-primary h-11 w-full rounded-xl bg-[#232B20]/30 px-6 text-sm font-bold transition-all hover:text-black sm:w-auto'
84-
onClick={() => onJoin?.(team)}
109+
onClick={e => {
110+
e.stopPropagation();
111+
onJoin?.(team);
112+
}}
85113
disabled={memberCount >= maxSize}
86114
>
87115
Join Team

app/(landing)/hackathons/[slug]/components/tabs/index.tsx

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ import FindTeam from './contents/FindTeam';
1414
import SponsorsTab from './contents/SponsorsTab';
1515
import { useEffect, useState, useMemo } from 'react';
1616
import { useHackathonData } from '@/lib/providers/hackathonProvider';
17-
import { useHackathonAnnouncements } from '@/hooks/hackathon/use-hackathon-queries';
17+
import {
18+
useHackathonAnnouncements,
19+
useHackathonTeams,
20+
} from '@/hooks/hackathon/use-hackathon-queries';
1821
import { useCommentSystem } from '@/hooks/use-comment-system';
1922
import { CommentEntityType } from '@/types/comment';
2023
import { Megaphone } from 'lucide-react';
@@ -46,6 +49,17 @@ const HackathonTabs = ({ sidebar }: HackathonTabsProps) => {
4649
enabled: !!currentHackathon?.id,
4750
});
4851

52+
const participantType = currentHackathon?.participantType;
53+
const isTeamHackathon =
54+
participantType === 'TEAM' || participantType === 'TEAM_OR_INDIVIDUAL';
55+
56+
const { data: teamsCountResponse } = useHackathonTeams(
57+
slug,
58+
{ page: 1, limit: 1 },
59+
!!slug && isTeamHackathon
60+
);
61+
const teamsTotal = teamsCountResponse?.data?.pagination?.total ?? 0;
62+
4963
const [activeTab, setActiveTab] = useState('overview');
5064

5165
const hackathonTabs = useMemo(() => {
@@ -55,10 +69,6 @@ const HackathonTabs = ({ sidebar }: HackathonTabsProps) => {
5569
const hasWinners = !!(winners && winners.length > 0);
5670
const hasAnnouncements = announcements.length > 0;
5771

58-
const participantType = currentHackathon.participantType;
59-
const isTeamHackathon =
60-
participantType === 'TEAM' || participantType === 'TEAM_OR_INDIVIDUAL';
61-
6272
const tabs = [
6373
{ id: 'overview', label: 'Overview' },
6474
...(hasParticipants
@@ -105,6 +115,7 @@ const HackathonTabs = ({ sidebar }: HackathonTabsProps) => {
105115
tabs.push({
106116
id: 'team-formation',
107117
label: 'Find Team',
118+
badge: teamsTotal,
108119
});
109120
}
110121

@@ -159,6 +170,8 @@ const HackathonTabs = ({ sidebar }: HackathonTabsProps) => {
159170
announcements,
160171
announcementsLoading,
161172
generalLoading,
173+
isTeamHackathon,
174+
teamsTotal,
162175
]);
163176

164177
useEffect(() => {

0 commit comments

Comments
 (0)