Skip to content

Commit d20958b

Browse files
Benjtalkshow0xdevcollinsclaude
authored
Merge to prod (#557)
* fix(submissions): improve link validation UX, banner display, and error toasts (#547) - Reconcile frontend link types with backend enum (github, demo, video, document, presentation, other) - Allow up to 5 "other" links per submission with smart type-picking on Add Link and duplicate guards on type change - Add 500-character cap and live counter on the optional Introduction field - Pass banner through initialData on submission edit so the saved banner displays - Add banner field to ParticipantSubmission so the type compiles - Set mock-fill participation type from myTeam to avoid INDIVIDUAL submissions while on a team - Switch submission error toasts to title + description with 8s duration so backend messages are readable - Update SubmissionLinksTab icon mapping for the new link types * fix(submissions): correct logo cropping and remove duplicate category badge on submission card (#549) - Logo overlay on banner uses object-contain so wordmark logos are not cropped - Render the category badge once instead of twice on the explore submissions card * feat(blog): add Boundless x Trustless Work hackathon post and fit cover images (#551) - Add content/blog/boundless-launches-first-hackathon-campaign.mdx for the May 13 hackathon launch announcement. - BlogCard and BlogPostDetails cover images use object-contain so the full banner is visible (previously object-cover cropped wider banners). - Update SHARE icons on the blog details page from the old brand color #99FF2D to the current brand primary #2eedaa. * feat(judge): dedicated judge portal, organizer invitations, completeness gate UX (#550) * feat: implement judge portal with submission evaluation, invitations, and score calculation workflows * feat: add countdown banner, support read-only score sliders, and integrate submission disqualification into grading modal --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(hackathons): add "hidden until results" submission visibility mode (#555) Surfaces the new HIDDEN_UNTIL_RESULTS option (added in the nestjs PR) in the organizer settings tab. Reorders the three visibility options so the recommended "Shortlisted only" leads, the new "Hidden until results are announced" sits in the middle, and "All submissions" comes last. Rewrites the copy on the "All submissions" choice that incorrectly claimed disqualified projects would be shown -- they never were on the backend, and Phase 2 makes that an explicit guarantee. Aligns the form's default and API-fallback value with the backend default (ACCEPTED_SHORTLISTED, not ALL) so organizers don't see a misleading initial selection. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 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. --------- Co-authored-by: Collins Ikechukwu <collinschristroa@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7894e8d commit d20958b

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)