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>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
61b650bb25
commit
e945da4b7f
@@ -1,78 +1,158 @@
|
||||
'use client';
|
||||
|
||||
import hackathons from '@/data/hackathons.json';
|
||||
import { buttonVariants } from '@components';
|
||||
import { cn } from '@utils';
|
||||
import Image from 'next/image';
|
||||
import { HiOutlineCode } from 'react-icons/hi';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { HiOutlineCode, HiOutlineTrophy } from 'react-icons/hi';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
export default function HackathonsPage() {
|
||||
const sortedHackathons = [...hackathons];
|
||||
interface TeamItem {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
city: string;
|
||||
logo: string | null;
|
||||
banner: string | null;
|
||||
}
|
||||
|
||||
if (sortedHackathons.length === 0) {
|
||||
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">
|
||||
<p className="text-muted-foreground text-lg">No hackathon projects available yet.</p>
|
||||
<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.12 } },
|
||||
}}
|
||||
variants={{ visible: { transition: { staggerChildren: 0.08 } } }}
|
||||
>
|
||||
{sortedHackathons.map((hackathon, idx) => (
|
||||
<motion.div
|
||||
key={hackathon.project_title}
|
||||
className="rounded-xl shadow-sm hover:shadow-lg transition-all duration-300 bg-card group"
|
||||
initial={{ opacity: 0, y: 40 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: idx * 0.08, type: 'spring', stiffness: 60 }}
|
||||
whileHover={{ scale: 1.03, boxShadow: '0 8px 32px rgba(0,0,0,0.10)' }}
|
||||
>
|
||||
<motion.div className="h-48 bg-muted relative overflow-hidden rounded-t-xl">
|
||||
<Image
|
||||
src={`https://cdn.asepharyana.tech/imphnen/hackatons/${hackathon.file_name}`}
|
||||
alt={hackathon.project_title}
|
||||
fill
|
||||
className="object-cover object-top group-hover:scale-105 transition-transform duration-500"
|
||||
sizes="(max-width: 768px) 100vw, 33vw"
|
||||
unoptimized
|
||||
/>
|
||||
</motion.div>
|
||||
<div className="p-6">
|
||||
<h3 className="text-lg font-semibold mb-3 text-foreground">
|
||||
{hackathon.project_title}
|
||||
</h3>
|
||||
<div className="space-y-2 mb-4 text-sm text-muted-foreground">
|
||||
<div className="flex items-center gap-2">
|
||||
<HiOutlineCode className="w-4 h-4" />
|
||||
<span>{hackathon.team_name}</span>
|
||||
{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>
|
||||
<p className="text-muted-foreground text-sm mb-4 line-clamp-3">
|
||||
{hackathon.description}
|
||||
</p>
|
||||
<a
|
||||
href={hackathon.repo_link}
|
||||
target="_blank"
|
||||
className={cn(
|
||||
buttonVariants({ variant: 'bordered' }),
|
||||
'w-full text-sm'
|
||||
) : (
|
||||
<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>
|
||||
)}
|
||||
>
|
||||
Lihat Proyek
|
||||
</a>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
{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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user