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
38 changes: 32 additions & 6 deletions app/(landing)/hackathons/[slug]/HackathonPageClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { useRouter, useSearchParams, useParams } from 'next/navigation';
import { useHackathonData } from '@/lib/providers/hackathonProvider';
import { useRegisterHackathon } from '@/hooks/hackathon/use-register-hackathon';
import { useLeaveHackathon } from '@/hooks/hackathon/use-leave-hackathon';
import { useSubmission } from '@/hooks/hackathon/use-submission';
import { useAuthStatus } from '@/hooks/use-auth';
import { RegisterHackathonModal } from '@/components/hackathons/overview/RegisterHackathonModal';
import { HackathonBanner } from '@/components/hackathons/hackathonBanner';
import { HackathonNavTabs } from '@/components/hackathons/hackathonNavTabs';
Expand Down Expand Up @@ -45,6 +47,13 @@ export default function HackathonPageClient() {
refreshCurrentHackathon,
} = useHackathonData();

const { isAuthenticated } = useAuthStatus();

const { submission: mySubmission } = useSubmission({
hackathonSlugOrId: currentHackathon?.id || '',
autoFetch: !!currentHackathon && isAuthenticated,
});

const timeline_Events = useTimelineEvents(currentHackathon, {
includeEndDate: false,
dateFormat: { month: 'short', day: 'numeric', year: 'numeric' },
Expand Down Expand Up @@ -232,7 +241,7 @@ export default function HackathonPageClient() {
// Registration status
const {
isRegistered,
hasSubmitted,
hasSubmitted: participantHasSubmitted,
setParticipant,
register: registerForHackathon,
} = useRegisterHackathon({
Expand All @@ -246,6 +255,8 @@ export default function HackathonPageClient() {
organizationId: undefined,
});

const hasSubmitted = !!mySubmission || participantHasSubmitted;

// Leave hackathon functionality
const { isLeaving, leave: leaveHackathon } = useLeaveHackathon({
hackathonSlugOrId: currentHackathon?.id || '',
Expand Down Expand Up @@ -296,7 +307,7 @@ export default function HackathonPageClient() {
};

const handleSubmitClick = () => {
router.push('?tab=submission');
router.push(`/hackathons/${currentHackathon?.slug}/submit`);
};

const handleViewSubmissionClick = () => {
Expand All @@ -308,10 +319,25 @@ export default function HackathonPageClient() {
};

// Set current hackathon on mount
const [isInitializing, setIsInitializing] = useState(true);

useEffect(() => {
if (hackathonId) {
setCurrentHackathon(hackathonId);
}
let isMounted = true;

const initHackathon = async () => {
if (hackathonId) {
await setCurrentHackathon(hackathonId);
}
if (isMounted) {
setIsInitializing(false);
}
};

initHackathon();

return () => {
isMounted = false;
};
}, [hackathonId, setCurrentHackathon]);

// Handle tab changes from URL
Expand Down Expand Up @@ -349,7 +375,7 @@ export default function HackathonPageClient() {
};

// Loading state
if (loading) {
if (loading || isInitializing) {
return <LoadingScreen />;
}

Expand Down
120 changes: 120 additions & 0 deletions app/(landing)/hackathons/[slug]/submit/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
'use client';

import { use, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useHackathonData } from '@/lib/providers/hackathonProvider';
import { useAuthStatus } from '@/hooks/use-auth';
import { useSubmission } from '@/hooks/hackathon/use-submission';
import { SubmissionFormContent } from '@/components/hackathons/submissions/SubmissionForm';
import LoadingScreen from '@/features/projects/components/CreateProjectModal/LoadingScreen';
import { Button } from '@/components/ui/button';
import { ArrowLeft } from 'lucide-react';
import { toast } from 'sonner';

export default function SubmitProjectPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const router = useRouter();
const { isAuthenticated, isLoading } = useAuthStatus();

const resolvedParams = use(params);
const hackathonSlug = resolvedParams.slug;

const {
currentHackathon,
loading: hackathonLoading,
setCurrentHackathon,
} = useHackathonData();

useEffect(() => {
if (hackathonSlug) {
setCurrentHackathon(hackathonSlug);
}
}, [hackathonSlug, setCurrentHackathon]);

const hackathonId = currentHackathon?.id || '';
const orgId = currentHackathon?.organizationId || undefined;

const {
submission: mySubmission,
isFetching: isLoadingMySubmission,
fetchMySubmission,
} = useSubmission({
hackathonSlugOrId: hackathonId || '',
autoFetch: isAuthenticated && !!hackathonId,
});

// Authentication check
useEffect(() => {
if (!isLoading && !isAuthenticated) {
toast.error('You must be logged in to submit a project');
router.push(
`/auth?mode=signin&callbackUrl=/hackathons/${hackathonSlug}/submit`
);
}
}, [isAuthenticated, isLoading, router, hackathonSlug]);

const handleClose = () => {
router.push(`/hackathons/${hackathonSlug}`);
};

const handleSuccess = () => {
fetchMySubmission();
toast.success(
mySubmission
? 'Submission updated successfully!'
: 'Project submitted successfully!'
);
router.push(`/hackathons/${hackathonSlug}?tab=submission`);
};

if (
isLoading ||
hackathonLoading ||
isLoadingMySubmission ||
!currentHackathon
) {
return <LoadingScreen />;
}

return (
<div className='min-h-screen bg-black px-5 py-5 text-white md:px-[50px] lg:px-[100px]'>
<div className='mx-auto max-w-[1200px] pb-10'>
<Button
variant='ghost'
className='mb-6 pl-0 text-gray-400 hover:text-white'
onClick={handleClose}
>
<ArrowLeft className='mr-2 h-4 w-4' />
Back to Hackathon
</Button>

<div className='min-h-[700px] overflow-hidden rounded-xl border border-gray-800 bg-gray-900/50 shadow-2xl'>
<SubmissionFormContent
hackathonSlugOrId={hackathonId}
organizationId={orgId}
submissionId={mySubmission?.id}
initialData={
mySubmission
? {
projectName: mySubmission.projectName,
category: mySubmission.category,
description: mySubmission.description,
logo: mySubmission.logo,
videoUrl: mySubmission.videoUrl,
introduction: mySubmission.introduction,
links: mySubmission.links,
participationType: (mySubmission as any).participationType,
}
: undefined
}
onSuccess={handleSuccess}
onClose={handleClose}
/>
</div>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,23 @@ import {
import { Switch } from '@/components/ui/switch';
import { reportError } from '@/lib/error-reporting';

/** Strip Markdown to plain text for list preview (headings, bold, links, etc.). */
function stripMarkdown(md: string): string {
if (!md || typeof md !== 'string') return '';
return md
.replace(/!\[[^\]]*\]\([^)]*\)/g, '')
.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
.replace(/#{1,6}\s*/g, '')
.replace(/\*\*([^*]+)\*\*/g, '$1')
.replace(/\*([^*]+)\*/g, '$1')
.replace(/__([^_]+)__/g, '$1')
.replace(/_([^_]+)_/g, '$1')
.replace(/`([^`]+)`/g, '$1')
.replace(/<[^>]*>/g, '')
.replace(/\n+/g, ' ')
.trim();
}

export default function AnnouncementPage() {
const params = useParams();
const organizationId = params.id as string;
Expand Down Expand Up @@ -298,7 +315,7 @@ export default function AnnouncementPage() {
)}
</div>
<p className='line-clamp-2 text-sm text-zinc-400'>
{item.content.replace(/<[^>]*>/g, '')}
{stripMarkdown(item.content)}
</p>
<div className='flex items-center gap-4 text-xs text-zinc-500'>
<span>
Expand Down
15 changes: 15 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,21 @@ input[type='number'] {
transition: all 0.5s ease-out;
}

/* Navbar active state – brand color via CSS variables (Tailwind-safe) */
:root {
--nav-active-bg: rgba(167, 249, 80, 0.1);
--nav-active-color: #a7f950;
--nav-active-border: rgba(167, 249, 80, 0.2);
--nav-active-shadow: 0 1px 2px rgba(167, 249, 80, 0.05);
}

.navbar-link-active {
background-color: var(--nav-active-bg);
color: var(--nav-active-color);
border: 1px solid var(--nav-active-border);
box-shadow: var(--nav-active-shadow);
}

/* Navbar character animations */
.nav-char {
display: inline-block;
Expand Down
24 changes: 21 additions & 3 deletions components/hackathons/announcements/AnnouncementsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,26 @@ import { Megaphone, Pin, ArrowUpDown, ExternalLink } from 'lucide-react';
import { cn } from '@/lib/utils';
import type { HackathonAnnouncement } from '@/lib/api/hackathons/index';
import Link from 'next/link';
import { useMarkdown } from '@/hooks/use-markdown';

/** Renders announcement body as Markdown (supports both Markdown and legacy HTML). */
function AnnouncementPreview({ content }: { content: string }) {
const raw = content?.trim() || '';
const isLikelyHtml = raw.startsWith('<');
const markdown = isLikelyHtml ? raw.replace(/<[^>]*>/g, ' ') : raw;
const { styledContent, loading } = useMarkdown(markdown, {
loadingDelay: 0,
});

if (!raw) return <span className='text-zinc-500'>No content</span>;
if (loading) return <span className='animate-pulse text-zinc-500'>…</span>;

return (
<div className='[&_.markdown-content]:line-clamp-2 [&_.markdown-content]:overflow-hidden [&_.markdown-content]:text-sm [&_.markdown-content]:text-zinc-400 [&_.markdown-content_h1]:text-base [&_.markdown-content_h2]:text-base! [&_.markdown-content_h3]:text-base [&_.markdown-content_p]:my-0 [&_.markdown-content_p]:text-zinc-400'>
{styledContent}
</div>
);
}

interface AnnouncementsTabProps {
announcements: HackathonAnnouncement[];
Expand Down Expand Up @@ -90,9 +110,7 @@ export function AnnouncementsTab({
</h3>
</div>

<p className='line-clamp-2 text-sm text-zinc-400'>
{announcement.content.replace(/<[^>]*>/g, '')}
</p>
<AnnouncementPreview content={announcement.content} />

<div className='flex items-center gap-4 text-xs text-zinc-500'>
<span className='flex items-center gap-1.5'>
Expand Down
6 changes: 3 additions & 3 deletions components/hackathons/hackathonBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -315,14 +315,14 @@ export function HackathonBanner({
{status === 'ongoing' &&
isRegistered &&
hasSubmitted &&
onViewSubmissionClick && (
onSubmitClick && (
<Button
onClick={onViewSubmissionClick}
onClick={onSubmitClick}
variant='outline'
className='w-full border-gray-700 bg-transparent py-5 text-gray-300 hover:bg-gray-900'
>
<FileText className='mr-2 h-4 w-4' />
View Submission
Edit Submission
</Button>
)}

Expand Down
25 changes: 11 additions & 14 deletions components/hackathons/hackathonStickyCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -238,20 +238,17 @@ export function HackathonStickyCard(props: HackathonStickyCardProps) {
</Button>
)}

{/* View Submission Button */}
{status === 'ongoing' &&
isRegistered &&
hasSubmitted &&
onViewSubmissionClick && (
<Button
onClick={onViewSubmissionClick}
variant='outline'
className='w-full border-gray-700 py-4 text-sm text-gray-300 hover:bg-gray-900'
>
<FileText className='mr-1.5 h-3.5 w-3.5' />
View Submission
</Button>
)}
{/* Edit / View Submission Button */}
{status === 'ongoing' && isRegistered && hasSubmitted && (
<Button
onClick={onSubmitClick}
variant='outline'
className='w-full border-gray-700 py-4 text-sm text-gray-300 hover:bg-gray-900'
>
<FileText className='mr-1.5 h-3.5 w-3.5' />
Edit Submission
</Button>
)}

{/* Find Team Button */}
{status === 'ongoing' &&
Expand Down
Loading
Loading