Skip to content

Commit c4bb5b0

Browse files
Benjtalkshow0xdevcollinsclaude
authored
Merge to main (#569)
* fix(hackathons): single-column teams tab and primary-colored pager (#562) * fix(hackathons): single-column teams tab and primary-colored pager Revert the teams tab grid to a single column and rework the shared Pagination component to match the icon-chevron layout used by the organizer submissions and participants pages, styled with the primary color. * feat(submissions): link submission card avatars to profile pages Wrap the individual avatar on SubmissionCard in a profile link and forward team-member usernames to GroupAvatar so each clustered avatar opens that user's profile in a new tab. * feat(hackathons): track-based prize structure + submission polish + tracks UI (#564) * feat(hackathons): add "hidden until results" submission visibility mode 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): track-based prize structure, submission polish, tracks UI Wires the frontend for the new track-based prize flow: - New TracksSettingsTab with full CRUD: name/slug/description/eligibility/ prompt/customQuestions/requiredArtifacts; per-row bulk-opt-in action with confirmation dialog for retrofitting existing submissions - RewardsTab gains a 3-card prize structure picker, per-tier kind toggle and track dropdown, amber "tracks unbound" banner, and an inline Manage Tracks dialog embedding the settings table - SubmissionForm: track picker + per-track answers (prompt / custom questions / required artifacts), tagline, builtWith chips, screenshots, license, code attestation, with soft compliance gate for already-submitted submissions. trackIds hydrate from trackEntries on edit so bulk-opted-in submitters don't strip themselves out - SubmissionDetailModal renders tagline, screenshots, built-with, license badge, and per-track answers - Public hackathon page: Overview splits prizes into Overall/Track sections; sidebar tier list shows TRACK prefix and looks up track names; Winners tab gets a Track Winners section with per-track cards - API client: lib/api/hackathons/tracks.ts with listTracks / listOrganizerTracks / createTrack / updateTrack / deleteTrack / bulkOptInAllSubmissions, plus types for HackathonTrack, TrackCustomQuestion, TrackRequiredArtifact, TrackAnswer, SubmissionTrackEntry, BulkOptInResult - Hackathon provider/hooks expose trackWinners and per-track entries Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Feat/submission visibility hidden until results (#566) * feat(hackathons): add "hidden until results" submission visibility mode 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): track-based prize structure, submission polish, tracks UI Wires the frontend for the new track-based prize flow: - New TracksSettingsTab with full CRUD: name/slug/description/eligibility/ prompt/customQuestions/requiredArtifacts; per-row bulk-opt-in action with confirmation dialog for retrofitting existing submissions - RewardsTab gains a 3-card prize structure picker, per-tier kind toggle and track dropdown, amber "tracks unbound" banner, and an inline Manage Tracks dialog embedding the settings table - SubmissionForm: track picker + per-track answers (prompt / custom questions / required artifacts), tagline, builtWith chips, screenshots, license, code attestation, with soft compliance gate for already-submitted submissions. trackIds hydrate from trackEntries on edit so bulk-opted-in submitters don't strip themselves out - SubmissionDetailModal renders tagline, screenshots, built-with, license badge, and per-track answers - Public hackathon page: Overview splits prizes into Overall/Track sections; sidebar tier list shows TRACK prefix and looks up track names; Winners tab gets a Track Winners section with per-track cards - API client: lib/api/hackathons/tracks.ts with listTracks / listOrganizerTracks / createTrack / updateTrack / deleteTrack / bulkOptInAllSubmissions, plus types for HackathonTrack, TrackCustomQuestion, TrackRequiredArtifact, TrackAnswer, SubmissionTrackEntry, BulkOptInResult - Hackathon provider/hooks expose trackWinners and per-track entries Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(submissions): tighten Zod schema + surface backend debug info - Add max constraints that previously only existed on the backend DTO so validation fires inline (projectName 100, description 5000, URL 500). - ApiErrorField gains an optional `debug` field that the backend Prisma filter populates outside production. - useSubmission's error formatter prefers `debug` over the generic field message when present, so toasts show the real Prisma reason behind "Data validation failed" instead of a blank "validation: …" line. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(submit-page): hydrate Phase A fields + stop wiping user input on re-render The submit page mapped `initialData` inline on every render, which (a) recreated the object reference on every render so the form's reset effect fired continuously and wiped values the user was typing, and (b) dropped the Phase A fields entirely (tagline, builtWith, screenshots, license, codeAttestedAt) plus trackEntries. The combined effect was the documented symptom — only logo and videoUrl survived the save because those round-tripped through the type-narrowed object literal, while tagline / builtWith / license kept appearing to "switch to empty". - Memoize `initialData` against `mySubmission` so the reference only changes when the underlying submission actually changes. - Pass through Phase A fields and trackEntries so the form can hydrate the saved values, and so a follow-up save doesn't write back empties. - Widen SubmissionFormContent's `initialData` prop to accept the raw server-side extras (trackEntries, codeAttestedAt) that the form already consumes via cast — keeps the parent's hydration explicit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(hackathons): always open submissions in a new tab (#568) * fix(hackathons): single-column teams tab and primary-colored pager Revert the teams tab grid to a single column and rework the shared Pagination component to match the icon-chevron layout used by the organizer submissions and participants pages, styled with the primary color. * feat(submissions): link submission card avatars to profile pages Wrap the individual avatar on SubmissionCard in a profile link and forward team-member usernames to GroupAvatar so each clustered avatar opens that user's profile in a new tab. * fix(hackathons): always open submissions in a new tab Across the hackathon, organizer, judge, and profile surfaces, clicking a submission now opens the project page in a new tab so reviewers and participants do not lose their list/queue context. Switched the remaining anchor tags to next/link Link components. --------- Co-authored-by: Collins Ikechukwu <collinschristroa@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 256ae35 commit c4bb5b0

10 files changed

Lines changed: 45 additions & 21 deletions

File tree

app/(landing)/hackathons/[slug]/components/sidebar/MySubmissionPanel.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,11 @@ export default function MySubmissionPanel() {
9292
status !== 'WITHDRAWN';
9393

9494
const handleView = () => {
95-
router.push(`/projects/${submission.id}?type=submission`);
95+
window.open(
96+
`/projects/${submission.id}?type=submission`,
97+
'_blank',
98+
'noopener,noreferrer'
99+
);
96100
};
97101

98102
const handleEdit = () => {

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,15 +194,15 @@ const SubmissionCard = ({ submission }: SubmissionCardProps) => {
194194
</>
195195
)}
196196

197-
<a href={projectUrl} target='_blank' rel='noopener noreferrer'>
197+
<Link href={projectUrl} target='_blank' rel='noopener noreferrer'>
198198
<BoundlessButton
199199
variant='outline'
200200
size='sm'
201201
className='hover:bg-primary h-9 rounded-xl border-white/5 bg-white/5 px-4 text-xs font-bold transition-all hover:text-black'
202202
>
203203
View Project
204204
</BoundlessButton>
205-
</a>
205+
</Link>
206206
</div>
207207
</div>
208208
</div>

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import Link from 'next/link';
12
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
23
import { HackathonWinner } from '@/lib/api/hackathons';
34
import { SubmissionCardProps } from '@/types/hackathon';
@@ -14,7 +15,7 @@ export const GeneralWinnerCard = ({
1415
const projectUrl = `/projects/${winner.submissionId}?type=submission`;
1516

1617
return (
17-
<a
18+
<Link
1819
href={projectUrl}
1920
target='_blank'
2021
rel='noopener noreferrer'
@@ -35,6 +36,6 @@ export const GeneralWinnerCard = ({
3536
</div>
3637

3738
<div className='text-primary text-xs font-bold'>{winner.prize}</div>
38-
</a>
39+
</Link>
3940
);
4041
};

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import Link from 'next/link';
12
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
23
import { HackathonWinner } from '@/lib/api/hackathons';
34
import { Trophy } from 'lucide-react';
@@ -53,15 +54,15 @@ export const PodiumWinnerCard = ({
5354
</div>
5455
</div>
5556

56-
<a href={projectUrl} target='_blank' rel='noopener noreferrer'>
57+
<Link href={projectUrl} target='_blank' rel='noopener noreferrer'>
5758
<BoundlessButton
5859
variant='outline'
5960
size='sm'
6061
className='hover:bg-primary h-8 rounded-lg border-white/5 bg-white/5 px-3 text-[10px] font-bold transition-all hover:text-black'
6162
>
6263
View Project
6364
</BoundlessButton>
64-
</a>
65+
</Link>
6566
</div>
6667
</div>
6768
);

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import Link from 'next/link';
12
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
23
import { HackathonWinner } from '@/lib/api/hackathons';
34
import { Trophy } from 'lucide-react';
@@ -79,15 +80,15 @@ export const TopWinnerCard = ({ winner, submission }: TopWinnerCardProps) => {
7980
</div>
8081
</div>
8182

82-
<a href={projectUrl} target='_blank' rel='noopener noreferrer'>
83+
<Link href={projectUrl} target='_blank' rel='noopener noreferrer'>
8384
<BoundlessButton
8485
variant='outline'
8586
size='sm'
8687
className='hover:bg-primary h-9 rounded-xl border-white/5 bg-white/5 px-4 text-xs font-bold transition-all hover:text-black'
8788
>
8889
View Project
8990
</BoundlessButton>
90-
</a>
91+
</Link>
9192
</div>
9293
</div>
9394
</div>

app/judge/[hackathonId]/submissions/page.tsx

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

33
import Link from 'next/link';
4-
import { useParams, useRouter } from 'next/navigation';
4+
import { useParams } from 'next/navigation';
55
import { useEffect, useMemo, useState } from 'react';
66
import {
77
CheckCircle2,
@@ -29,7 +29,6 @@ export default function JudgeSubmissionsPage() {
2929

3030
function SubmissionsList() {
3131
const params = useParams<{ hackathonId: string }>();
32-
const router = useRouter();
3332
const hackathonId = params?.hackathonId ?? '';
3433

3534
const [filter, setFilter] = useState<Filter>('all');
@@ -88,13 +87,17 @@ function SubmissionsList() {
8887
} else if (e.key === 'Enter') {
8988
const s = visible[focusIdx];
9089
if (s) {
91-
router.push(`/judge/${hackathonId}/submissions/${s.submission.id}`);
90+
window.open(
91+
`/judge/${hackathonId}/submissions/${s.submission.id}`,
92+
'_blank',
93+
'noopener,noreferrer'
94+
);
9295
}
9396
}
9497
};
9598
window.addEventListener('keydown', onKey);
9699
return () => window.removeEventListener('keydown', onKey);
97-
}, [visible, focusIdx, router, hackathonId]);
100+
}, [visible, focusIdx, hackathonId]);
98101

99102
return (
100103
<div className='space-y-5'>
@@ -277,6 +280,8 @@ function Row({
277280
return (
278281
<Link
279282
href={`/judge/${hackathonId}/submissions/${submission.submission.id}`}
283+
target='_blank'
284+
rel='noopener noreferrer'
280285
onMouseEnter={onHover}
281286
className={cn(
282287
'group flex items-center gap-3 rounded-xl border bg-[#101010] px-4 py-3 transition-all',

app/me/hackathons/submissions/submission-components.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
import { Separator } from '@/components/ui/separator';
1717
import { TableCell, TableRow as ShadcnTableRow } from '@/components/ui/table';
1818
import Image from 'next/image';
19+
import Link from 'next/link';
1920
import { format } from 'date-fns';
2021
import React, { useState } from 'react';
2122

@@ -206,7 +207,7 @@ export function SubmissionsSheetContent({
206207
</div>
207208
<div className='flex items-center gap-3'>
208209
<StatusBadge status={submission.status} />
209-
<a
210+
<Link
210211
href={viewUrl}
211212
target='_blank'
212213
rel='noopener noreferrer'
@@ -216,7 +217,7 @@ export function SubmissionsSheetContent({
216217
>
217218
<ExternalLink className='h-3.5 w-3.5' />
218219
View Page
219-
</a>
220+
</Link>
220221
</div>
221222
</div>
222223

@@ -463,7 +464,7 @@ export function TableRow({
463464
{submission.projectName}
464465
</span>
465466
{/* Always in DOM — fade via opacity so no layout shift */}
466-
<a
467+
<Link
467468
href={viewUrl}
468469
target='_blank'
469470
rel='noopener noreferrer'
@@ -477,7 +478,7 @@ export function TableRow({
477478
aria-label={`Open ${submission.projectName} in new tab`}
478479
>
479480
<ExternalLink className='h-3.5 w-3.5' />
480-
</a>
481+
</Link>
481482
</div>
482483
</div>
483484
</TableCell>

components/hackathons/winners/WinnersTab.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,12 @@ const TrackWinnerCard = ({
200200
</div>
201201

202202
{projectUrl ? (
203-
<Link href={projectUrl}>
203+
<Link
204+
href={projectUrl}
205+
target='_blank'
206+
rel='noopener noreferrer'
207+
className='block no-underline'
208+
>
204209
<div className='flex items-center justify-between rounded-lg border border-gray-900 bg-black/20 p-2 transition-colors hover:bg-black/40'>
205210
<p className='line-clamp-1 text-sm font-medium text-white'>
206211
{trackWinner.projectName}

components/organization/cards/JudgingParticipant.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,8 @@ const JudgingParticipant = ({
258258
<span className='h-1 w-1 rounded-full bg-gray-700' />
259259
<Link
260260
href={`/projects/${submissionData.id}?type=submission`}
261+
target='_blank'
262+
rel='noopener noreferrer'
261263
className='flex items-center gap-1 transition-colors hover:text-white'
262264
>
263265
View Details <ExternalLink className='h-3 w-3' />
@@ -433,6 +435,8 @@ const JudgingParticipant = ({
433435
)}
434436
<Link
435437
href={`/projects/${submissionData.id}?type=submission`}
438+
target='_blank'
439+
rel='noopener noreferrer'
436440
className='flex h-7 w-7 items-center justify-center rounded-md border border-gray-800 text-gray-500 hover:bg-white/5 hover:text-white'
437441
>
438442
<ExternalLink className='h-3 w-3' />

components/organization/hackathons/submissions/SubmissionsList.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { useState } from 'react';
2-
import { useRouter } from 'next/navigation';
32
import {
43
Users,
54
User,
@@ -56,7 +55,6 @@ export function SubmissionsList({
5655
onSelectionChange,
5756
hackathon,
5857
}: SubmissionsListProps) {
59-
const router = useRouter();
6058
const [reviewingId, setReviewingId] = useState<string | null>(null);
6159
const [disqualifyingId, setDisqualifyingId] = useState<string | null>(null);
6260
const [isDisqualifying, setIsDisqualifying] = useState(false);
@@ -155,7 +153,11 @@ export function SubmissionsList({
155153
};
156154

157155
const handleSubmissionClick = (submissionId: string) => {
158-
router.push(`/projects/${submissionId}?type=submission`);
156+
window.open(
157+
`/projects/${submissionId}?type=submission`,
158+
'_blank',
159+
'noopener,noreferrer'
160+
);
159161
};
160162

161163
if (submissions.length === 0 && !loading) {

0 commit comments

Comments
 (0)