Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .husky/pre-push
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ echo "🚀 Running pre-push checks..."

# Run security audit
echo "🔒 Running security audit..."
npm audit --audit-level=moderate
npm audit --omit=dev --audit-level=high

# Run build check one more time
# echo "🏗️ Final build check..."
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
'use client';

import React, { useState, useEffect } from 'react';
import { useParams, useRouter } from 'next/navigation';
import { Megaphone, ArrowLeft, Calendar, User, Pin } from 'lucide-react';
import {
getAnnouncementDetails,
GetHackathonBySlug,
type HackathonAnnouncement,
} from '@/lib/api/hackathons/index';
import { useMarkdown } from '@/hooks/use-markdown';
import { BoundlessButton } from '@/components/buttons';
import Loading from '@/components/Loading';
import { Badge } from '@/components/ui/badge';

export default function AnnouncementDetailPage() {
const params = useParams();
const router = useRouter();
const announcementId = params.announcementId as string;
const slug = params.slug as string;

const [announcement, setAnnouncement] =
useState<HackathonAnnouncement | null>(null);
const [hackathonName, setHackathonName] = useState<string>('');
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
async function fetchDetails() {
if (!announcementId) return;
try {
setLoading(true);
const [announcementData, hackathonData] = await Promise.all([
getAnnouncementDetails(announcementId),
GetHackathonBySlug(slug),
]);
setAnnouncement(announcementData);
setHackathonName(hackathonData.data.name);
} catch (err) {
console.error('Failed to fetch details:', err);
setError(
'Failed to load announcement. It may have been deleted or moved.'
);
} finally {
setLoading(false);
}
}
fetchDetails();
}, [announcementId, slug]);

const { styledContent, loading: markdownLoading } = useMarkdown(
announcement?.content || '',
{
breaks: true,
gfm: true,
}
);

if (loading) {
return (
<div className='flex min-h-screen items-center justify-center bg-black'>
<Loading />
</div>
);
}

if (error || !announcement) {
return (
<div className='flex min-h-screen flex-col items-center justify-center bg-black p-6 text-center'>
<div className='mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-red-500/10'>
<Megaphone className='h-10 w-10 text-red-500' />
</div>
<h1 className='mb-2 text-2xl font-bold text-white'>
Announcement Not Found
</h1>
<p className='mb-8 text-zinc-400'>
{error || "We couldn't find the announcement you're looking for."}
</p>
<BoundlessButton onClick={() => router.back()} variant='outline'>
<ArrowLeft className='mr-2 h-4 w-4' />
Go Back
</BoundlessButton>
</div>
);
}

return (
<div className='min-h-screen bg-black pb-24'>
{/* Top Header */}
<div className='sticky top-0 z-10 border-b border-zinc-900 bg-black/80 backdrop-blur-md'>
<div className='mx-auto max-w-4xl items-center justify-between px-6 py-4'>
<button
onClick={() => window.close()}
className='flex items-center gap-2 text-sm text-zinc-400 transition-colors hover:text-white'
>
<ArrowLeft className='h-4 w-4' />
Close Tab
</button>
<div className='flex items-center gap-2 text-zinc-500'>
<Megaphone className='text-primary h-4 w-4' />
<span className='text-xs font-medium tracking-widest uppercase'>
{hackathonName ? `${hackathonName} • ` : ''}Announcement
</span>
</div>
</div>
</div>

<div className='mx-auto max-w-4xl px-6 pt-12'>
{/* Title & Metadata */}
<div className='mb-12 space-y-6 border-b border-zinc-900 pb-12'>
<div className='flex flex-wrap items-center gap-3'>
{hackathonName && (
<Badge
variant='outline'
className='border-primary/30 text-primary bg-primary/5 font-bold tracking-tighter uppercase'
>
{hackathonName}
</Badge>
)}
{announcement.isPinned && (
<Badge
variant='secondary'
className='bg-primary/10 text-primary border-primary/20 flex items-center gap-1 tracking-tighter uppercase'
>
<Pin className='h-3 w-3 fill-current' />
Pinned
</Badge>
)}
<Badge
variant='outline'
className='tracking-tighter text-zinc-500 uppercase'
>
Published
</Badge>
</div>

<h1 className='text-2xl font-bold tracking-tight text-white'>
{announcement.title}
</h1>

<div className='flex flex-wrap items-center gap-6 text-sm text-zinc-400'>
<div className='flex items-center gap-2'>
<div className='flex h-8 w-8 items-center justify-center rounded-full bg-zinc-800 text-zinc-500 uppercase'>
{announcement.author?.name?.charAt(0) || (
<User className='h-4 w-4' />
)}
</div>
<span className='font-medium text-zinc-200'>
{announcement.author?.name || 'Organizer'}
</span>
</div>

<div className='flex items-center gap-2'>
<Calendar className='h-4 w-4 text-zinc-600' />
<span>
{new Date(
announcement.publishedAt || announcement.createdAt
).toLocaleDateString(undefined, {
weekday: 'long',
month: 'long',
day: 'numeric',
year: 'numeric',
})}
</span>
</div>
</div>
</div>

{/* Content */}
<div className='prose prose-invert prose-primary max-w-none'>
{markdownLoading ? (
<div className='flex h-32 items-center justify-center'>
<div className='border-primary h-6 w-6 animate-spin rounded-full border-2 border-t-transparent' />
</div>
) : (
<div className='text-lg leading-relaxed text-zinc-200'>
{styledContent}
</div>
)}
</div>

{/* Footer */}
<div className='mt-24 rounded-2xl border border-zinc-900 bg-zinc-950/50 p-8 text-center'>
<h3 className='mb-2 font-bold text-white'>End of Announcement</h3>
<p className='mb-6 text-sm text-zinc-500'>
This announcement was published by the hackathon organizers.
</p>
<BoundlessButton
onClick={() => window.close()}
variant='outline'
size='sm'
>
Return to Hackathon
</BoundlessButton>
</div>
</div>
</div>
);
}
57 changes: 51 additions & 6 deletions app/(landing)/hackathons/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,13 @@ import { HackathonParticipants } from '@/components/hackathons/participants/hack
import { useCommentSystem } from '@/hooks/use-comment-system';
import { CommentEntityType } from '@/types/comment';
import { useTeamPosts } from '@/hooks/hackathon/use-team-posts';
import {
listAnnouncements,
type HackathonAnnouncement,
} from '@/lib/api/hackathons/index';
import { HackathonWinner } from '@/lib/api/hackathons';
import { Megaphone } from 'lucide-react';
import { AnnouncementsTab } from '@/components/hackathons/announcements/AnnouncementsTab';

export default function HackathonPage() {
const router = useRouter();
Expand Down Expand Up @@ -63,6 +69,29 @@ export default function HackathonPage() {
autoFetch: !!hackathonId,
});

// Fetch announcements for public view
const [announcements, setAnnouncements] = useState<HackathonAnnouncement[]>(
[]
);
const [announcementsLoading, setAnnouncementsLoading] = useState(false);

useEffect(() => {
async function fetchAnnouncements() {
if (!hackathonId) return;
try {
setAnnouncementsLoading(true);
const data = await listAnnouncements(hackathonId);
// Only show published announcements for public view
setAnnouncements(data.filter(a => !a.isDraft));
} catch (error) {
console.error('Failed to fetch announcements:', error);
} finally {
setAnnouncementsLoading(false);
}
}
fetchAnnouncements();
}, [hackathonId]);

const hackathonTabs = useMemo(() => {
const hasParticipants =
Array.isArray(currentHackathon?.participants) &&
Expand All @@ -71,9 +100,7 @@ export default function HackathonPage() {
const hasResources = currentHackathon?.resources?.[0];
const participantType = currentHackathon?.participantType;
const isTeamHackathon =
participantType === 'TEAM' ||
participantType === 'TEAM_OR_INDIVIDUAL' ||
participantType === 'INDIVIDUAL';
participantType === 'TEAM' || participantType === 'TEAM_OR_INDIVIDUAL';
const isTabEnabled =
currentHackathon?.enabledTabs?.includes('joinATeamTab') !== false;

Expand Down Expand Up @@ -110,6 +137,16 @@ export default function HackathonPage() {
},
]
: []),
...(announcements.length > 0
? [
{
id: 'announcements',
label: 'Announcements',
badge: announcements.length,
icon: Megaphone,
},
]
: []),
{
id: 'submission',
label: 'Submissions',
Expand Down Expand Up @@ -149,6 +186,7 @@ export default function HackathonPage() {
teamPosts.length,
hackathonId,
winners,
announcements,
]);

// Refresh hackathon data
Expand Down Expand Up @@ -202,8 +240,7 @@ export default function HackathonPage() {
// Team formation availability
const isTeamHackathon =
currentHackathon?.participantType === 'TEAM' ||
currentHackathon?.participantType === 'TEAM_OR_INDIVIDUAL' ||
currentHackathon?.participantType === 'INDIVIDUAL';
currentHackathon?.participantType === 'TEAM_OR_INDIVIDUAL';
const isTeamFormationEnabled =
isTeamHackathon &&
currentHackathon?.enabledTabs?.includes('joinATeamTab') !== false;
Expand Down Expand Up @@ -300,6 +337,7 @@ export default function HackathonPage() {
isTeamFormationEnabled,
registrationDeadlinePolicy: currentHackathon.registrationDeadlinePolicy, // Now matches API casing
registrationDeadline: currentHackathon.registrationDeadline,
participantType: currentHackathon.participantType,
onJoinClick: handleJoinClick,
onLeaveClick: handleLeaveClick,
isLeaving,
Expand Down Expand Up @@ -342,7 +380,7 @@ export default function HackathonPage() {
currency: tier.currency,
passMark: tier.passMark,
description: tier.description,
prizeAmount: tier.prizeAmount, // Keep as string to match PrizeTier interface
prizeAmount: tier.prizeAmount,
}))}
totalPrizePool={currentHackathon.prizeTiers
.reduce(
Expand Down Expand Up @@ -371,6 +409,13 @@ export default function HackathonPage() {
<HackathonParticipants />
)}

{activeTab === 'announcements' && announcements.length > 0 && (
<AnnouncementsTab
announcements={announcements}
hackathonSlug={hackathonId}
/>
)}

{activeTab === 'submission' && (
<SubmissionTab
organizationId={currentHackathon.organizationId}
Expand Down
10 changes: 9 additions & 1 deletion app/(landing)/hackathons/preview/[orgId]/[draftId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,15 @@ export default function DraftPreviewPage({ params }: PreviewPageProps) {
timezone: draft.data.timeline?.timezone || 'UTC',

startDate: draft.data.timeline?.startDate || '',
endDate: draft.data.timeline?.winnerAnnouncementDate || '',
endDate:
draft.data.timeline?.winnersAnnouncedAt ||
draft.data.timeline?.winnerAnnouncementDate ||
draft.data.timeline?.judgingEnd ||
draft.data.timeline?.judgingDate ||
draft.data.timeline?.judgingStart ||
draft.data.timeline?.submissionDeadline ||
draft.data.timeline?.startDate ||
'',
submissionDeadline: draft.data.timeline?.submissionDeadline || '',
registrationDeadline:
draft.data.participation?.registrationDeadline || '',
Expand Down
Loading
Loading