Files
imphnen-frontend-service/apps/landing/src/app/(public)/hackathon/page.tsx
T
maulanasdqnandClaude Opus 4.6 e945da4b7f feat: integrate landing with real APIs, remove auth pages
Landing page changes:
- Removed entire (auth) directory — landing doesn't need login/signup
- Removed auth buttons (Masuk/Daftar) from header navigation
- Hackathon page now fetches teams + winners from real API
- Roadmap page now fetches from /v1/landing/cms/roadmap API with
  real-time voting via POST /roadmap/vote/{id}
- Deleted dummy JSON files: events.json, testimonials.json, hackathons.json
- Kept static config: hero-content, hero-stats, navigations, socials

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 21:42:43 +07:00

160 lines
5.7 KiB
TypeScript

'use client';
import { buttonVariants } from '@components';
import { cn } from '@utils';
import { useEffect, useState } from 'react';
import { HiOutlineCode, HiOutlineTrophy } from 'react-icons/hi';
import { motion } from 'framer-motion';
interface TeamItem {
id: string;
name: string;
description: string;
city: string;
logo: string | null;
banner: string | null;
}
interface WinnerItem {
id: string;
team_id: string;
team_name: string;
rank: number;
prize: string | null;
}
export default function HackathonsPage() {
const [teams, setTeams] = useState<TeamItem[]>([]);
const [winners, setWinners] = useState<WinnerItem[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
Promise.all([
fetch('https://api.imphnen.dev/v1/hackathon/teams/browse?per_page=20')
.then((r) => r.json())
.then((j) => j.data?.data || [])
.catch(() => []),
fetch('https://api.imphnen.dev/v1/hackathon/winners')
.then((r) => r.json())
.then((j) => j.data || [])
.catch(() => []),
]).then(([t, w]) => {
setTeams(t);
setWinners(w);
setLoading(false);
});
}, []);
const getWinnerRank = (teamId: string) => {
const w = winners.find((x) => x.team_id === teamId);
return w ? w.rank : null;
};
if (loading) {
return (
<section className="min-h-screen bg-background container py-10 flex items-center justify-center">
<div className="w-8 h-8 border-3 border-gray-200 border-t-primary-500 rounded-full animate-spin" />
</section>
);
}
if (teams.length === 0) {
return (
<section className="min-h-screen bg-background container py-10 flex items-center justify-center">
<div className="text-center">
<p className="text-muted-foreground text-lg mb-2">No hackathon teams yet.</p>
<a href="https://hackathon.imphnen.dev" className={cn(buttonVariants(), 'mt-4')}>
Join Hackathon
</a>
</div>
</section>
);
}
return (
<section className="min-h-screen bg-background container py-10">
<div className="text-center mb-12">
<h1 className="text-4xl font-bold mb-3 text-foreground">IMPHNEN Hackathon</h1>
<p className="text-muted-foreground text-lg max-w-2xl mx-auto">
Tim-tim yang berpartisipasi dalam hackathon IMPHNEN
</p>
</div>
{winners.length > 0 && (
<div className="mb-12">
<h2 className="text-2xl font-bold mb-6 text-foreground flex items-center gap-2">
<HiOutlineTrophy className="text-amber-500" /> Pemenang
</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{winners.sort((a, b) => a.rank - b.rank).map((w) => (
<div key={w.id} className="bg-amber-50 border-2 border-amber-200 rounded-xl p-6 text-center">
<div className="text-4xl mb-2">{w.rank === 1 ? '🥇' : w.rank === 2 ? '🥈' : '🥉'}</div>
<h3 className="text-lg font-bold text-gray-900">{w.team_name}</h3>
<p className="text-sm text-amber-700 mt-1">Juara {w.rank}</p>
{w.prize && <p className="text-xs text-amber-600 mt-1">{w.prize}</p>}
</div>
))}
</div>
</div>
)}
<h2 className="text-2xl font-bold mb-6 text-foreground">Semua Tim</h2>
<motion.div
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"
initial="hidden"
animate="visible"
variants={{ visible: { transition: { staggerChildren: 0.08 } } }}
>
{teams.map((team, idx) => {
const rank = getWinnerRank(team.id);
return (
<motion.div
key={team.id}
className={cn(
'rounded-xl shadow-sm hover:shadow-lg transition-all duration-300 bg-card group overflow-hidden',
rank ? 'ring-2 ring-amber-300' : ''
)}
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: idx * 0.06 }}
>
{team.banner ? (
<div className="h-40 bg-muted relative overflow-hidden">
<img src={team.banner} alt={team.name} className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500" />
</div>
) : (
<div className="h-40 bg-gradient-to-br from-primary-100 to-primary-200 flex items-center justify-center">
<HiOutlineCode className="w-12 h-12 text-primary-400" />
</div>
)}
<div className="p-6">
<div className="flex items-start justify-between mb-2">
<h3 className="text-lg font-semibold text-foreground">{team.name}</h3>
{rank && (
<span className="text-xs bg-amber-100 text-amber-700 px-2 py-1 rounded-full font-medium">
Juara {rank}
</span>
)}
</div>
{team.city && (
<p className="text-xs text-muted-foreground mb-2">{team.city}</p>
)}
{team.description && (
<p className="text-muted-foreground text-sm mb-4 line-clamp-3">{team.description}</p>
)}
<a
href={`https://hackathon.imphnen.dev/teams/${team.id}`}
target="_blank"
className={cn(buttonVariants({ variant: 'bordered' }), 'w-full text-sm')}
>
Lihat Tim
</a>
</div>
</motion.div>
);
})}
</motion.div>
</section>
);
}