diff --git a/app/(landing)/hackathons/[slug]/HackathonPageClient.tsx b/app/(landing)/hackathons/[slug]/HackathonPageClient.tsx
index 0c4d1adf..3f38dce0 100644
--- a/app/(landing)/hackathons/[slug]/HackathonPageClient.tsx
+++ b/app/(landing)/hackathons/[slug]/HackathonPageClient.tsx
@@ -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';
@@ -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' },
@@ -232,7 +241,7 @@ export default function HackathonPageClient() {
// Registration status
const {
isRegistered,
- hasSubmitted,
+ hasSubmitted: participantHasSubmitted,
setParticipant,
register: registerForHackathon,
} = useRegisterHackathon({
@@ -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 || '',
@@ -296,7 +307,7 @@ export default function HackathonPageClient() {
};
const handleSubmitClick = () => {
- router.push('?tab=submission');
+ router.push(`/hackathons/${currentHackathon?.slug}/submit`);
};
const handleViewSubmissionClick = () => {
@@ -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
@@ -349,7 +375,7 @@ export default function HackathonPageClient() {
};
// Loading state
- if (loading) {
+ if (loading || isInitializing) {
return ;
}
diff --git a/app/(landing)/hackathons/[slug]/hackathon-detail-design.md b/app/(landing)/hackathons/[slug]/hackathon-detail-design.md
new file mode 100644
index 00000000..cdb81ff1
--- /dev/null
+++ b/app/(landing)/hackathons/[slug]/hackathon-detail-design.md
@@ -0,0 +1,30 @@
+Hackathon Detail Page Redesign
+Issue: #414
+
+Figma Design
+https://www.figma.com/design/EMNGAQl1SGObXcsoa24krt/Boundless_Project-Details?node-id=0-1&t=A1ywRcn60Xyw0X6h-1
+
+This design proposes a cleaner and more professional UI/UX for the hackathon detail page.
+
+Included in the Figma file:
+- Desktop layout
+- Mobile layout
+- Banner / hero placement proposal
+- Redesigned hero section
+- Sticky sidebar card
+- Tab navigation
+- All tab layouts (overview, participants, resources, announcements, submissions, discussions, find team, winners)
+- Loading state
+- Hackathon not found state
+
+Banner Placement Proposal
+The hackathon banner is placed as a full-width hero image at the top of the page, allowing it to visually represent the hackathon and improve page identity.
+
+The sidebar becomes a compact summary card with key information and actions.
+
+Design Goals
+- Simpler UI and improved visual hierarchy
+- Clear primary actions (Join, Submit, View Submission)
+- Consistent spacing and typography
+- Better mobile usability
+- Professional and product-quality look
diff --git a/app/(landing)/hackathons/[slug]/submit/page.tsx b/app/(landing)/hackathons/[slug]/submit/page.tsx
new file mode 100644
index 00000000..f0721551
--- /dev/null
+++ b/app/(landing)/hackathons/[slug]/submit/page.tsx
@@ -0,0 +1,140 @@
+'use client';
+
+import { use, useEffect, useState } 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`);
+ };
+
+ const [hasInitialLoaded, setHasInitialLoaded] = useState(false);
+
+ useEffect(() => {
+ if (!isLoading && !hackathonLoading && !isLoadingMySubmission) {
+ setHasInitialLoaded(true);
+ }
+ }, [isLoading, hackathonLoading, isLoadingMySubmission]);
+
+ if (!hasInitialLoaded) {
+ return ;
+ }
+
+ if (!currentHackathon) {
+ return (
+
+
Hackathon Not Found
+
+
+ Go Back
+
+
+ );
+ }
+ return (
+
+
+
+
+ Back to Hackathon
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/(landing)/newsletter/confirm/error/page.tsx b/app/(landing)/newsletter/confirm/error/page.tsx
new file mode 100644
index 00000000..84de7a0e
--- /dev/null
+++ b/app/(landing)/newsletter/confirm/error/page.tsx
@@ -0,0 +1,32 @@
+'use client';
+import { useSearchParams } from 'next/navigation';
+import Link from 'next/link';
+import { Suspense } from 'react';
+
+const msgs: Record = {
+ expired: 'This confirmation link has expired.',
+ invalid: 'This confirmation link is invalid or already used.',
+};
+
+function Content() {
+ const p = useSearchParams();
+ return (
+
+ Confirmation failed
+
+ {msgs[p.get('reason') ?? ''] ?? 'An unexpected error occurred.'}
+
+
+ Back to home
+
+
+ );
+}
+
+export default function Page() {
+ return (
+
+
+
+ );
+}
diff --git a/app/(landing)/newsletter/confirmed/page.tsx b/app/(landing)/newsletter/confirmed/page.tsx
new file mode 100644
index 00000000..67718529
--- /dev/null
+++ b/app/(landing)/newsletter/confirmed/page.tsx
@@ -0,0 +1,14 @@
+import Link from 'next/link';
+export default function NewsletterConfirmedPage() {
+ return (
+
+ You're subscribed! đ
+
+ Your subscription has been confirmed. Welcome aboard!
+
+
+ Back to home
+
+
+ );
+}
diff --git a/app/(landing)/newsletter/unsubscribe/error/page.tsx b/app/(landing)/newsletter/unsubscribe/error/page.tsx
new file mode 100644
index 00000000..b957980a
--- /dev/null
+++ b/app/(landing)/newsletter/unsubscribe/error/page.tsx
@@ -0,0 +1,31 @@
+'use client';
+import { useSearchParams } from 'next/navigation';
+import Link from 'next/link';
+import { Suspense } from 'react';
+
+const msgs: Record = {
+ invalid: 'This unsubscribe link is invalid or has already been used.',
+};
+
+function Content() {
+ const p = useSearchParams();
+ return (
+
+ Unsubscribe failed
+
+ {msgs[p.get('reason') ?? ''] ?? 'An unexpected error occurred.'}
+
+
+ Back to home
+
+
+ );
+}
+
+export default function Page() {
+ return (
+
+
+
+ );
+}
diff --git a/app/(landing)/newsletter/unsubscribed/page.tsx b/app/(landing)/newsletter/unsubscribed/page.tsx
new file mode 100644
index 00000000..0d758a3c
--- /dev/null
+++ b/app/(landing)/newsletter/unsubscribed/page.tsx
@@ -0,0 +1,14 @@
+import Link from 'next/link';
+export default function NewsletterUnsubscribedPage() {
+ return (
+
+ You've been unsubscribed
+
+ You won't receive any more emails from us.
+
+
+ Back to home
+
+
+ );
+}
diff --git a/app/(landing)/organizations/[id]/hackathons/[hackathonId]/announcement/page.tsx b/app/(landing)/organizations/[id]/hackathons/[hackathonId]/announcement/page.tsx
index 60116acb..20a2d817 100644
--- a/app/(landing)/organizations/[id]/hackathons/[hackathonId]/announcement/page.tsx
+++ b/app/(landing)/organizations/[id]/hackathons/[hackathonId]/announcement/page.tsx
@@ -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;
@@ -298,7 +315,7 @@ export default function AnnouncementPage() {
)}
- {item.content.replace(/<[^>]*>/g, '')}
+ {stripMarkdown(item.content)}
diff --git a/app/api/newsletter/confirm/[token]/route.ts b/app/api/newsletter/confirm/[token]/route.ts
new file mode 100644
index 00000000..774ba058
--- /dev/null
+++ b/app/api/newsletter/confirm/[token]/route.ts
@@ -0,0 +1,21 @@
+import { NextRequest, NextResponse } from 'next/server';
+
+const backendUrl = process.env.NEXT_PUBLIC_API_URL;
+const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? '';
+
+export async function GET(
+ _req: NextRequest,
+ { params }: { params: Promise<{ token: string }> }
+) {
+ const { token } = await params;
+ const res = await fetch(`${backendUrl}/api/newsletter/confirm/${token}`, {
+ redirect: 'manual',
+ });
+ if (res.status === 302) {
+ return NextResponse.redirect(`${appUrl}/newsletter/confirmed`);
+ }
+ const reason = res.status === 400 ? 'expired' : 'invalid';
+ return NextResponse.redirect(
+ `${appUrl}/newsletter/confirm/error?reason=${reason}`
+ );
+}
diff --git a/app/api/newsletter/preferences/route.ts b/app/api/newsletter/preferences/route.ts
new file mode 100644
index 00000000..36acc11b
--- /dev/null
+++ b/app/api/newsletter/preferences/route.ts
@@ -0,0 +1,28 @@
+import { NextRequest, NextResponse } from 'next/server';
+
+function normalizeBackendUrl(raw: string | undefined): string | undefined {
+ if (!raw) return undefined;
+ return raw.replace(/\/$/, '').replace(/\/api$/i, '');
+}
+
+const backendUrl = normalizeBackendUrl(process.env.NEXT_PUBLIC_API_URL);
+
+export async function PATCH(req: NextRequest) {
+ const body = await req.json();
+
+ if (!backendUrl) {
+ return NextResponse.json(
+ { message: 'Server configuration error: NEXT_PUBLIC_API_URL is not set' },
+ { status: 500 }
+ );
+ }
+
+ const res = await fetch(`${backendUrl}/api/newsletter/preferences`, {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(body),
+ });
+ return NextResponse.json(await res.json().catch(() => ({})), {
+ status: res.status,
+ });
+}
diff --git a/app/api/newsletter/subscribe/route.ts b/app/api/newsletter/subscribe/route.ts
index 9b7b0fca..31f0f067 100644
--- a/app/api/newsletter/subscribe/route.ts
+++ b/app/api/newsletter/subscribe/route.ts
@@ -4,17 +4,14 @@ export async function POST(request: NextRequest) {
try {
const body = await request.json();
- // Normalize API URL: remove trailing slash and /api if present
- // The env var should be base URL without /api (e.g., https://api.boundlessfi.xyz)
let backendUrl =
process.env.NEXT_PUBLIC_API_URL || 'https://staging-api.boundlessfi.xyz';
backendUrl = backendUrl.replace(/\/$/, '').replace(/\/api$/i, '');
- const response = await fetch(`${backendUrl}/api/waitlist/subscribe`, {
+ const response = await fetch(`${backendUrl}/api/newsletter/subscribe`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
-
...(request.headers.get('user-agent') && {
'User-Agent': request.headers.get('user-agent')!,
}),
@@ -22,25 +19,11 @@ export async function POST(request: NextRequest) {
body: JSON.stringify(body),
});
- if (!response.ok) {
- const errorData = await response.json().catch(() => ({}));
- return NextResponse.json(
- {
- message: errorData.message || 'Failed to subscribe to waitlist',
- status: response.status,
- },
- { status: response.status }
- );
- }
-
- const data = await response.json();
- return NextResponse.json(data, { status: 200 });
+ const data = await response.json().catch(() => ({}));
+ return NextResponse.json(data, { status: response.status });
} catch {
return NextResponse.json(
- {
- message: 'Internal server error',
- status: 500,
- },
+ { message: 'Internal server error', status: 500 },
{ status: 500 }
);
}
diff --git a/app/api/newsletter/unsubscribe/[token]/route.ts b/app/api/newsletter/unsubscribe/[token]/route.ts
new file mode 100644
index 00000000..dfd12ccd
--- /dev/null
+++ b/app/api/newsletter/unsubscribe/[token]/route.ts
@@ -0,0 +1,20 @@
+import { NextRequest, NextResponse } from 'next/server';
+
+const backendUrl = process.env.NEXT_PUBLIC_API_URL;
+const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? '';
+
+export async function GET(
+ _req: NextRequest,
+ { params }: { params: Promise<{ token: string }> }
+) {
+ const { token } = await params;
+ const res = await fetch(`${backendUrl}/api/newsletter/unsubscribe/${token}`, {
+ redirect: 'manual',
+ });
+ if (res.status === 302) {
+ return NextResponse.redirect(`${appUrl}/newsletter/unsubscribed`);
+ }
+ return NextResponse.redirect(
+ `${appUrl}/newsletter/unsubscribe/error?reason=invalid`
+ );
+}
diff --git a/app/api/newsletter/unsubscribe/route.ts b/app/api/newsletter/unsubscribe/route.ts
new file mode 100644
index 00000000..64e31d8d
--- /dev/null
+++ b/app/api/newsletter/unsubscribe/route.ts
@@ -0,0 +1,15 @@
+import { NextRequest, NextResponse } from 'next/server';
+
+const backendUrl = process.env.NEXT_PUBLIC_API_URL;
+
+export async function POST(req: NextRequest) {
+ const body = await req.json();
+ const res = await fetch(`${backendUrl}/api/newsletter/unsubscribe`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(body),
+ });
+ return NextResponse.json(await res.json().catch(() => ({})), {
+ status: res.status,
+ });
+}
diff --git a/app/globals.css b/app/globals.css
index f26eec3e..9d5ab0c8 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -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;
diff --git a/app/me/crowdfunding/[slug]/milestones/[milestoneIndex]/page.tsx b/app/me/crowdfunding/[slug]/milestones/[milestoneIndex]/page.tsx
index 2d346395..7aa3c0bf 100644
--- a/app/me/crowdfunding/[slug]/milestones/[milestoneIndex]/page.tsx
+++ b/app/me/crowdfunding/[slug]/milestones/[milestoneIndex]/page.tsx
@@ -162,31 +162,31 @@ export default function MilestoneDetailPage({ params }: PageProps) {
// Step 3: Perform blockchain transaction with Trustless Work SDK
toast('Please confirm the transaction in your wallet');
- const { unsignedTransaction } = await changeMilestoneStatus(
- {
- contractId: campaign.escrowAddress,
- milestoneIndex: (milestone.orderIndex ?? milestoneIndex).toString(),
- newStatus: data.status === 'completed' ? 'completed' : 'in_progress',
- newEvidence: data.submissionNotes, // Use submission notes as evidence description
- serviceProvider: walletAddress || '',
- },
- 'multi-release'
- );
- if (!unsignedTransaction) {
- throw new Error(
- 'Unsigned transaction is missing from useChangeMilestoneStatusresponse.'
- );
- }
-
- const signedXdr = await signTransaction({
- unsignedTransaction,
- address: walletAddress || '',
- });
-
- const trxsent = await sendTransaction(signedXdr);
- if (trxsent.status === 'SUCCESS') {
- toast.success('Transaction confirmed on blockchain');
- }
+ // const { unsignedTransaction } = await changeMilestoneStatus(
+ // {
+ // contractId: campaign.escrowAddress,
+ // milestoneIndex: (milestone.orderIndex ?? milestoneIndex).toString(),
+ // newStatus: data.status === 'completed' ? 'completed' : 'in_progress',
+ // newEvidence: data.submissionNotes, // Use submission notes as evidence description
+ // serviceProvider: walletAddress || '',
+ // },
+ // 'multi-release'
+ // );
+ // if (!unsignedTransaction) {
+ // throw new Error(
+ // 'Unsigned transaction is missing from useChangeMilestoneStatusresponse.'
+ // );
+ // }
+
+ // const signedXdr = await signTransaction({
+ // unsignedTransaction,
+ // address: walletAddress || '',
+ // });
+
+ // const trxsent = await sendTransaction(signedXdr);
+ // if (trxsent.status === 'SUCCESS') {
+ // toast.success('Transaction confirmed on blockchain');
+ // }
toast('Updating milestone...');
diff --git a/app/me/projects/page.tsx b/app/me/projects/page.tsx
index 4c61e8f1..fd6c819b 100644
--- a/app/me/projects/page.tsx
+++ b/app/me/projects/page.tsx
@@ -102,14 +102,23 @@ export default function MyProjectsPage() {
) : (
- {sortedProjects.map(project => (
-
{
+ // Find if this project is a hackathon submission
+ const submission =
+ meData.user.hackathonSubmissionsAsParticipant?.find(
+ s => s.projectId === project.id
+ );
+
+ // If it's a submission, use the submission ID for the slug to ensure correct redirection from ProjectCard
+ const displayId = submission?.id || project.id;
+
+ return (
+
- ))}
+ isSubmission: !!submission,
+ submissionStatus: submission?.status,
+ }}
+ />
+ );
+ })}
)}
diff --git a/components/hackathons/announcements/AnnouncementsTab.tsx b/components/hackathons/announcements/AnnouncementsTab.tsx
index 621b2304..e10c9b69 100644
--- a/components/hackathons/announcements/AnnouncementsTab.tsx
+++ b/components/hackathons/announcements/AnnouncementsTab.tsx
@@ -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 No content ;
+ if (loading) return ⌠;
+
+ return (
+
+ {styledContent}
+
+ );
+}
interface AnnouncementsTabProps {
announcements: HackathonAnnouncement[];
@@ -90,9 +110,7 @@ export function AnnouncementsTab({
-
- {announcement.content.replace(/<[^>]*>/g, '')}
-
+
diff --git a/components/hackathons/hackathonBanner.tsx b/components/hackathons/hackathonBanner.tsx
index dcb8ccd5..f08d27b0 100644
--- a/components/hackathons/hackathonBanner.tsx
+++ b/components/hackathons/hackathonBanner.tsx
@@ -315,14 +315,14 @@ export function HackathonBanner({
{status === 'ongoing' &&
isRegistered &&
hasSubmitted &&
- onViewSubmissionClick && (
+ onSubmitClick && (
- View Submission
+ Edit Submission
)}
diff --git a/components/hackathons/hackathonStickyCard.tsx b/components/hackathons/hackathonStickyCard.tsx
index 47056240..9a4a9c2a 100644
--- a/components/hackathons/hackathonStickyCard.tsx
+++ b/components/hackathons/hackathonStickyCard.tsx
@@ -238,20 +238,17 @@ export function HackathonStickyCard(props: HackathonStickyCardProps) {
)}
- {/* View Submission Button */}
- {status === 'ongoing' &&
- isRegistered &&
- hasSubmitted &&
- onViewSubmissionClick && (
-
-
- View Submission
-
- )}
+ {/* Edit / View Submission Button */}
+ {status === 'ongoing' && isRegistered && hasSubmitted && (
+
+
+ Edit Submission
+
+ )}
{/* Find Team Button */}
{status === 'ongoing' &&
diff --git a/components/hackathons/submissions/SubmissionForm.tsx b/components/hackathons/submissions/SubmissionForm.tsx
index abb2e0e4..cabc7abe 100644
--- a/components/hackathons/submissions/SubmissionForm.tsx
+++ b/components/hackathons/submissions/SubmissionForm.tsx
@@ -34,6 +34,7 @@ import {
ExpandableScreenContent,
ExpandableScreenTrigger,
useExpandableScreen,
+ useOptionalExpandableScreen,
} from '@/components/ui/expandable-screen';
import Stepper from '@/components/stepper/Stepper';
import { uploadService } from '@/lib/api/upload';
@@ -125,6 +126,7 @@ interface SubmissionFormContentProps {
initialData?: Partial;
submissionId?: string;
onSuccess?: () => void;
+ onClose?: () => void;
}
const INITIAL_STEPS: Step[] = [
@@ -198,8 +200,12 @@ const SubmissionFormContent: React.FC = ({
initialData,
submissionId,
onSuccess,
+ onClose,
}) => {
- const { collapse, isExpanded: open } = useExpandableScreen();
+ const expandableCtx = useOptionalExpandableScreen();
+ const collapse = expandableCtx?.collapse ?? (() => {});
+ const open = expandableCtx?.isExpanded ?? true;
+
const { currentHackathon } = useHackathonData();
const { user } = useAuthStatus();
@@ -404,7 +410,7 @@ const SubmissionFormContent: React.FC = ({
description:
'An intelligent task management application that uses machine learning to prioritize tasks...',
logo: '',
- videoUrl: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
+ videoUrl: 'https://youtu.be/rOSZhhblE_8?si=Hf_YvPTMmyWTUOKQ',
introduction: 'This project leverages advanced AI algorithms...',
links: [
{ type: 'github', url: 'https://github.com/example/ai-task-manager' },
@@ -773,8 +779,13 @@ const SubmissionFormContent: React.FC = ({
} else {
await create(submissionData);
}
- collapse();
- onSuccess?.();
+ if (onSuccess) {
+ onSuccess();
+ } else if (onClose) {
+ onClose();
+ } else {
+ collapse();
+ }
} catch {
// Error handled in hook
}
@@ -1081,15 +1092,17 @@ const SubmissionFormContent: React.FC = ({
-
- Fill with Mock Data
-
+ {process.env.NODE_ENV === 'development' && (
+
+ Fill with Mock Data
+
+ )}
= ({