diff --git a/apps/landing/src/app/(public)/hackathon/[slug]/_components/HackathonContent.tsx b/apps/landing/src/app/(public)/hackathon/[slug]/_components/HackathonContent.tsx new file mode 100644 index 0000000..69e2eb4 --- /dev/null +++ b/apps/landing/src/app/(public)/hackathon/[slug]/_components/HackathonContent.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { MDXRemote } from 'next-mdx-remote/rsc'; + +interface HackathonContentProps { + content: string; +} + +export function HackathonContent({ content }: HackathonContentProps) { + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/apps/landing/src/app/(public)/hackathon/[slug]/_components/HackathonDetailPage.tsx b/apps/landing/src/app/(public)/hackathon/[slug]/_components/HackathonDetailPage.tsx deleted file mode 100644 index c245ce9..0000000 --- a/apps/landing/src/app/(public)/hackathon/[slug]/_components/HackathonDetailPage.tsx +++ /dev/null @@ -1,224 +0,0 @@ -'use client'; - -import React from 'react'; -import { motion } from 'framer-motion'; -import { MDXRemote } from 'next-mdx-remote/rsc'; -import { HackathonContent } from '@/content/hackathons/types'; -import { - getDaysLeftText, - getProgressPercent, - formatDateRange -} from '@/content/hackathons/utils'; - -interface Props { - hackathon: HackathonContent; -} - -export function HackathonDetailPage({ hackathon }: Props) { - const { metadata, content } = hackathon; - const progressPercent = getProgressPercent(metadata); - const daysLeftText = getDaysLeftText(metadata); - - return ( -
- {/* Hero Section */} -
-
-
-
-
- -

- {metadata.name} -

- {metadata.theme && ( -

- {metadata.theme} -

- )} - {metadata.description && ( -

- {metadata.description} -

- )} -
-
-
- - {/* Status Bar */} - {(progressPercent !== null && daysLeftText && parseInt(daysLeftText) > -5) && ( -
-
-
-
- - {metadata.status?.charAt(0).toUpperCase() + metadata.status?.slice(1)} - - {metadata.submissionWindow && ( - - {formatDateRange(metadata.submissionWindow)} - - )} -
- {daysLeftText && ( - - {daysLeftText} - - )} -
- {progressPercent !== null && ( -
-
-
-
-
- )} -
-
- )} - -
-
- {/* Main Content */} -
- - - -
- - {/* Sidebar */} -
- - {/* Quick Info */} -
-

Quick Info

-
- {metadata.prize && ( -
- Prize Pool: - {metadata.prize} -
- )} - {metadata.minTeamSize && metadata.maxTeamSize && ( -
- Team Size: - - {metadata.minTeamSize === metadata.maxTeamSize - ? `${metadata.minTeamSize} person${metadata.minTeamSize > 1 ? 's' : ''}` - : `${metadata.minTeamSize}-${metadata.maxTeamSize} people` - } - -
- )} - {metadata.submissionsCount && ( -
- Submissions: - {metadata.submissionsCount} -
- )} - {metadata.partnersCount && ( -
- Partners: - {metadata.partnersCount} -
- )} -
-
- - {/* Tags */} - {metadata.tags && metadata.tags.length > 0 && ( -
-

Tags

-
- {metadata.tags.map((tag) => ( - - {tag} - - ))} -
-
- )} - - {/* Requirements */} - {metadata.requirements && metadata.requirements.length > 0 && ( -
-

Requirements

-
- {metadata.requirements.map((req) => ( -
-
-
-
{req.name}
-
- {req.description} -
-
-
- ))} -
-
- )} - - {/* Partners */} - {metadata.partners && metadata.partners.length > 0 && ( -
-

Partners

- -
- )} - -
-
-
-
- ); -} \ No newline at end of file diff --git a/apps/landing/src/app/(public)/hackathon/[slug]/_components/HackathonHeader.tsx b/apps/landing/src/app/(public)/hackathon/[slug]/_components/HackathonHeader.tsx new file mode 100644 index 0000000..55546a6 --- /dev/null +++ b/apps/landing/src/app/(public)/hackathon/[slug]/_components/HackathonHeader.tsx @@ -0,0 +1,36 @@ +'use client'; + +import React from 'react'; +import { motion } from 'framer-motion'; +import { HackathonContent } from '@/content/hackathons/types'; + +interface HackathonHeaderProps { + metadata: HackathonContent['metadata']; +} + +export function HackathonHeader({ metadata }: HackathonHeaderProps) { + return ( + +
+

+ {metadata.name} +

+ {metadata.theme && ( +

+ {metadata.theme} +

+ )} +
+ {metadata.description && ( +

+ {metadata.description} +

+ )} +
+ ); +} \ No newline at end of file diff --git a/apps/landing/src/app/(public)/hackathon/[slug]/_components/HackathonHero.tsx b/apps/landing/src/app/(public)/hackathon/[slug]/_components/HackathonHero.tsx new file mode 100644 index 0000000..4e0b572 --- /dev/null +++ b/apps/landing/src/app/(public)/hackathon/[slug]/_components/HackathonHero.tsx @@ -0,0 +1,29 @@ +'use client'; + +import React from 'react'; +import { motion } from 'framer-motion'; +import Image from 'next/image'; + +interface HackathonHeroProps { + coverImage: string; + hackathonName: string; +} + +export function HackathonHero({ coverImage, hackathonName }: HackathonHeroProps) { + return ( + + {hackathonName} + + ); +} \ No newline at end of file diff --git a/apps/landing/src/app/(public)/hackathon/[slug]/_components/HackathonPartners.tsx b/apps/landing/src/app/(public)/hackathon/[slug]/_components/HackathonPartners.tsx new file mode 100644 index 0000000..9dd0cf2 --- /dev/null +++ b/apps/landing/src/app/(public)/hackathon/[slug]/_components/HackathonPartners.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import { HackathonContent } from '@/content/hackathons/types'; +import { BiBuilding } from 'react-icons/bi'; + +interface HackathonPartnersProps { + partners: HackathonContent['metadata']['partners']; +} + +export function HackathonPartners({ partners }: HackathonPartnersProps) { + if (!partners || partners.length === 0) { + return ( +
+
+ +
+

No Sponsors Yet

+

+ Sponsor information will appear here once partnerships are announced. +

+
+ ); + } + + return ( +
+
+
+

Event Sponsors

+ + {partners.length} sponsor{partners.length !== 1 ? 's' : ''} + +
+

+ Kami berterima kasih kepada sponsor-sponsor berikut yang telah mendukung hackathon ini, tanpa dukungan mereka, acara ini tidak akan mungkin terlaksana. +

+
+ +
+ {partners.map((partner, idx) => ( + + {/* eslint-disable-next-line @next/next/no-img-element */} + {partner.name} +
+ {partner.name} + Sponsor +
+
+ ))} +
+
+ ); +} \ No newline at end of file diff --git a/apps/landing/src/app/(public)/hackathon/[slug]/_components/HackathonQuickInfo.tsx b/apps/landing/src/app/(public)/hackathon/[slug]/_components/HackathonQuickInfo.tsx new file mode 100644 index 0000000..6b78167 --- /dev/null +++ b/apps/landing/src/app/(public)/hackathon/[slug]/_components/HackathonQuickInfo.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { HackathonContent } from '@/content/hackathons/types'; + +interface HackathonQuickInfoProps { + metadata: HackathonContent['metadata']; +} + +export function HackathonQuickInfo({ metadata }: HackathonQuickInfoProps) { + return ( +
+

Quick Info

+
+ {metadata.prize && ( +
+ Prize Pool: + {metadata.prize} +
+ )} + {metadata.minTeamSize && metadata.maxTeamSize && ( +
+ Team Size: + + {metadata.minTeamSize === metadata.maxTeamSize + ? `${metadata.minTeamSize} person${metadata.minTeamSize > 1 ? 's' : ''}` + : `${metadata.minTeamSize}-${metadata.maxTeamSize} people` + } + +
+ )} + {metadata.submissionsCount && ( +
+ Submissions: + {metadata.submissionsCount} +
+ )} + {metadata.partnersCount && ( +
+ Partners: + {metadata.partnersCount} +
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/apps/landing/src/app/(public)/hackathon/[slug]/_components/HackathonRequirements.tsx b/apps/landing/src/app/(public)/hackathon/[slug]/_components/HackathonRequirements.tsx new file mode 100644 index 0000000..85c5a77 --- /dev/null +++ b/apps/landing/src/app/(public)/hackathon/[slug]/_components/HackathonRequirements.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { HackathonContent } from '@/content/hackathons/types'; + +interface HackathonRequirementsProps { + requirements: HackathonContent['metadata']['requirements']; +} + +export function HackathonRequirements({ requirements }: HackathonRequirementsProps) { + if (!requirements || requirements.length === 0) { + return null; + } + + return ( +
+

Requirements

+
+ {requirements.map((req) => ( +
+
+
+
{req.name}
+
+ {req.description} +
+
+
+ ))} +
+
+ ); +} \ No newline at end of file diff --git a/apps/landing/src/app/(public)/hackathon/[slug]/_components/HackathonSidebar.tsx b/apps/landing/src/app/(public)/hackathon/[slug]/_components/HackathonSidebar.tsx new file mode 100644 index 0000000..7371539 --- /dev/null +++ b/apps/landing/src/app/(public)/hackathon/[slug]/_components/HackathonSidebar.tsx @@ -0,0 +1,36 @@ +'use client'; + +import React from 'react'; +import { motion } from 'framer-motion'; +import { HackathonContent } from '@/content/hackathons/types'; +import { HackathonHeader } from './HackathonHeader'; +import { HackathonQuickInfo } from './HackathonQuickInfo'; +import { HackathonRequirements } from './HackathonRequirements'; + +interface HackathonSidebarProps { + metadata: HackathonContent['metadata']; +} + +export function HackathonSidebar({ metadata }: HackathonSidebarProps) { + return ( +
+ + + + + + + + +
+ ); +} \ No newline at end of file diff --git a/apps/landing/src/app/(public)/hackathon/[slug]/_components/HackathonStatusBar.tsx b/apps/landing/src/app/(public)/hackathon/[slug]/_components/HackathonStatusBar.tsx new file mode 100644 index 0000000..868a3eb --- /dev/null +++ b/apps/landing/src/app/(public)/hackathon/[slug]/_components/HackathonStatusBar.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { HackathonContent } from '@/content/hackathons/types'; +import { formatDateRange } from '@/content/hackathons/utils'; + +interface HackathonStatusBarProps { + metadata: HackathonContent['metadata']; + progressPercent: number | null; + daysLeftText: string | null; +} + +export function HackathonStatusBar({ metadata, progressPercent, daysLeftText }: HackathonStatusBarProps) { + // Only show if we have progress and days left, and it's not too far in the past + if (!(progressPercent !== null && daysLeftText && parseInt(daysLeftText) > -5)) { + return null; + } + + return ( +
+
+
+
+ + {metadata.status?.charAt(0).toUpperCase() + metadata.status?.slice(1)} + + {metadata.submissionWindow && ( + + {formatDateRange(metadata.submissionWindow)} + + )} +
+ {daysLeftText && ( + + {daysLeftText} + + )} +
+ {progressPercent !== null && ( +
+
+
+
+
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/apps/landing/src/app/(public)/hackathon/[slug]/_components/HackathonSubmissions.tsx b/apps/landing/src/app/(public)/hackathon/[slug]/_components/HackathonSubmissions.tsx new file mode 100644 index 0000000..1598449 --- /dev/null +++ b/apps/landing/src/app/(public)/hackathon/[slug]/_components/HackathonSubmissions.tsx @@ -0,0 +1,107 @@ +'use client'; + +import React from 'react'; +import { BiLinkExternal, BiLogoGithub, BiImage, BiGroup } from 'react-icons/bi'; + +interface Submission { + team_name: string; + project_title: string; + description: string; + repo_link: string; + screenshot?: string; + file_name?: string; +} + +interface HackathonSubmissionsProps { + submissions?: Submission[]; + submissionsCount?: number; +} + +export function HackathonSubmissions({ submissions, submissionsCount }: HackathonSubmissionsProps) { + if (!submissions || submissions.length === 0) { + return ( +
+
+ +
+

No Submissions Yet

+

+ Submissions will appear here once participants start submitting their projects. +

+ {submissionsCount && ( +

+ Expected submissions: {submissionsCount} +

+ )} +
+ ); + } + + return ( +
+
+

Project Submissions

+ + {submissions.length} project{submissions.length !== 1 ? 's' : ''} + +
+ +
+ {submissions.map((submission, index) => ( +
+
+ {/* Header */} +
+ {submission.screenshot ? ( + // eslint-disable-next-line @next/next/no-img-element + {submission.project_title} + ) : ( +
+ No Screenshot Available +
+ )} +
+ +
+
+
+

{submission.project_title}

+
+
+ + {submission.team_name} +
+
+ + {/* Description */} +

+ {submission.description} +

+ + {/* Links */} +
+ {submission.repo_link && ( + + + Repository + + + )} +
+
+
+
+ ))} +
+
+ ); +} \ No newline at end of file diff --git a/apps/landing/src/app/(public)/hackathon/[slug]/_components/HackathonTabs.tsx b/apps/landing/src/app/(public)/hackathon/[slug]/_components/HackathonTabs.tsx new file mode 100644 index 0000000..d95cc3a --- /dev/null +++ b/apps/landing/src/app/(public)/hackathon/[slug]/_components/HackathonTabs.tsx @@ -0,0 +1,103 @@ +'use client'; + +import React, { useState } from 'react'; +import { motion } from 'framer-motion'; +import { HackathonContent } from '@/content/hackathons/types'; +import { HackathonPartners } from './HackathonPartners'; +import { HackathonSubmissions } from './HackathonSubmissions'; +import { BiGroup, BiFile, BiInfoCircle } from 'react-icons/bi'; + +interface Submission { + team_name: string; + project_title: string; + description: string; + repo_link: string; + screenshot?: string; + file_name?: string; +} + +interface HackathonTabsProps { + metadata: HackathonContent['metadata']; + submissions?: Submission[]; + contentElement: React.ReactNode; +} + +export function HackathonTabs({ metadata, submissions, contentElement }: HackathonTabsProps) { + const [activeTab, setActiveTab] = useState('content'); + + const tabs = [ + { + id: 'content', + label: 'Details', + icon: BiInfoCircle, + count: 0 // No count for content tab + }, + { + id: 'sponsors', + label: 'Sponsors', + icon: BiGroup, + count: metadata.partners?.length || 0 + }, + { + id: 'submissions', + label: 'Submissions', + icon: BiFile, + count: submissions?.length || metadata.submissionsCount || 0 + } + ]; + + return ( +
+ {/* Tab Navigation */} +
+ +
+ + {/* Tab Content */} +
+ + {activeTab === 'content' && contentElement} + {activeTab === 'sponsors' && ( + + )} + {activeTab === 'submissions' && ( + + )} + +
+
+ ); +} \ No newline at end of file diff --git a/apps/landing/src/app/(public)/hackathon/[slug]/_components/ServerHackathonContent.tsx b/apps/landing/src/app/(public)/hackathon/[slug]/_components/ServerHackathonContent.tsx new file mode 100644 index 0000000..d41b62b --- /dev/null +++ b/apps/landing/src/app/(public)/hackathon/[slug]/_components/ServerHackathonContent.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { MDXRemote } from 'next-mdx-remote/rsc'; + +interface ServerHackathonContentProps { + content: string; +} + +export async function ServerHackathonContent({ content }: ServerHackathonContentProps) { + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/apps/landing/src/app/(public)/hackathon/[slug]/page.tsx b/apps/landing/src/app/(public)/hackathon/[slug]/page.tsx index 7035b42..12d8b9e 100644 --- a/apps/landing/src/app/(public)/hackathon/[slug]/page.tsx +++ b/apps/landing/src/app/(public)/hackathon/[slug]/page.tsx @@ -1,7 +1,15 @@ import { notFound } from 'next/navigation'; import { Metadata } from 'next'; import { getHackathonBySlug, getAllHackathonSlugs } from '@/content/hackathons/content'; -import { HackathonDetailPage } from '@/app/(public)/hackathon/[slug]/_components/HackathonDetailPage'; +import { getDaysLeftText, getProgressPercent } from '@/content/hackathons/utils'; +import Link from 'next/link'; +import { Button } from '@components'; +import { BsArrowLeft } from 'react-icons/bs'; +import { HackathonHero } from './_components/HackathonHero'; +import { HackathonStatusBar } from './_components/HackathonStatusBar'; +import { HackathonSidebar } from './_components/HackathonSidebar'; +import { HackathonTabs } from './_components/HackathonTabs'; +import { MDXRemote } from 'next-mdx-remote/rsc'; interface Props { params: { @@ -21,7 +29,7 @@ export async function generateStaticParams() { export async function generateMetadata({ params }: Props): Promise { const resolvedParams = await params; const hackathon = await getHackathonBySlug(resolvedParams.slug); - + if (!hackathon) { return { title: 'Hackathon Not Found', @@ -30,7 +38,7 @@ export async function generateMetadata({ params }: Props): Promise { } const { metadata } = hackathon; - + return { title: metadata.seoTitle || `${metadata.name} - IMPHNEN`, description: metadata.seoDescription || metadata.description, @@ -58,5 +66,56 @@ export default async function HackathonPage({ params }: Props) { notFound(); } - return ; + const { metadata, content } = hackathon; + + // Return 404 if no content is found + if (!content || content.trim() === '') { + notFound(); + } + + const progressPercent = getProgressPercent(metadata); + const daysLeftText = getDaysLeftText(metadata); + + return ( +
+ + + + + + + + +
+ {/* Sidebar only */} +
+ +
+ + {/* Tabs section */} +
+ + +
+ } + /> +
+ + +
+
+ ); } \ No newline at end of file diff --git a/apps/landing/src/app/(public)/hackathon/page.tsx b/apps/landing/src/app/(public)/hackathon/page.tsx index 507d9f4..6bf8875 100644 --- a/apps/landing/src/app/(public)/hackathon/page.tsx +++ b/apps/landing/src/app/(public)/hackathon/page.tsx @@ -6,11 +6,11 @@ import { motion } from 'framer-motion'; import Link from 'next/link'; import { HackathonSummary } from '@/content/hackathons/types'; import { hackathonSummaries } from '@/content/hackathons/index'; -import { - getDaysLeftText, - getProgressPercent, - filterHackathons, - sortHackathons +import { + getDaysLeftText, + getProgressPercent, + filterHackathons, + sortHackathons } from '@/content/hackathons/utils'; const HackathonTags: React.FC<{ tags?: string[] }> = ({ tags }) => { @@ -98,34 +98,29 @@ const HackathonCard: React.FC<{ hackathon: HackathonSummary; idx: number }> = ({ }; export default function HackathonsPage() { - // Use static data instead of API calls const [filteredItems, setFilteredItems] = React.useState(hackathonSummaries); const [searchTerm, setSearchTerm] = React.useState(''); const [statusFilter, setStatusFilter] = React.useState('all'); - // Filter and sort hackathons + React.useEffect(() => { let filtered = [...hackathonSummaries]; - - // Apply search filter if (searchTerm) { filtered = filterHackathons(filtered, { search: searchTerm }); } - // Apply status filter if (statusFilter !== 'all') { filtered = filterHackathons(filtered, { status: [statusFilter] }); } - - // Sort by registration start date (newest first) + filtered = sortHackathons(filtered, 'registrationStart', 'desc'); - setFilteredItems(filtered); }, [searchTerm, statusFilter]); return (
{/* Search and Filter Controls */} + {/*
setSearchTerm(e.target.value)} - className="flex-1 px-4 py-2 border border-muted rounded-md focus:outline-none focus:ring-2 focus:ring-primary" + className="flex-1 px-4 py-2 border border-gray-400 rounded-md focus:outline-none focus:ring-2 focus:ring-primary" />
- - {/* Results summary */} -
- Showing {filteredItems.length} of {hackathonSummaries.length} hackathons -
-
+ +
*/} ))} - + {/* No results message */} {filteredItems.length === 0 && (
diff --git a/apps/landing/src/app/layout.tsx b/apps/landing/src/app/layout.tsx index d5484f1..cc1b7bc 100644 --- a/apps/landing/src/app/layout.tsx +++ b/apps/landing/src/app/layout.tsx @@ -2,6 +2,7 @@ import { poppinsFont } from '@/lib/fonts'; import '@/styles/globals.css'; import { cn } from '@utils'; import { type Metadata } from 'next'; +import NextTopLoader from 'nextjs-toploader'; import { Providers } from './_components/providers'; import { Toaster } from './_components/toaster'; @@ -18,6 +19,17 @@ export default function RootLayout({ return ( + = 6.0.0", + "react": ">= 16.0.0", + "react-dom": ">= 16.0.0" + } + }, "node_modules/no-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", @@ -27417,6 +27435,12 @@ "node": ">=8" } }, + "node_modules/nprogress": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz", + "integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==", + "license": "MIT" + }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -27978,7 +28002,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -29887,7 +29910,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", @@ -29899,7 +29921,6 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, "license": "MIT" }, "node_modules/property-information": { diff --git a/package.json b/package.json index 6541e13..029b3d5 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "next": "~15.2.4", "next-mdx-remote": "^5.0.0", "next-themes": "^0.4.6", + "nextjs-toploader": "^3.9.17", "openapi-fetch": "^0.14.0", "openapi-react-query": "^0.5.0", "react": "^19.1.0",