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:
maulanasdqn
2026-04-10 21:42:43 +07:00
co-authored by Claude Opus 4.6
parent 61b650bb25
commit e945da4b7f
49 changed files with 266 additions and 2115 deletions
@@ -2,64 +2,63 @@
import { Button, Card, CardContent, CardHeader, CardTitle } from '@components';
import { AnimatePresence, motion } from 'framer-motion';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { BiUpvote } from 'react-icons/bi';
import { FiCheckCircle } from 'react-icons/fi';
import { MdOutlineOpenInNew } from 'react-icons/md';
interface RoadmapItem {
id: string;
title: string;
description: string;
status: 'upcoming' | 'in_progress' | 'completed';
votes: number;
created_at: string;
}
export default function ProjectsVote() {
const [upcomingItems, setUpcomingItems] = useState([
{
title: 'IMPHNEN Project Showcase',
description: 'Showcase projectmu ke member lain dan dapatkan feedback',
votes: 42,
voted: false,
},
{
title: 'IMPHNEN Meme Generator',
description: 'Bikin meme kocak kapanpun dengan mudah',
votes: 42,
voted: false,
},
]);
const [items, setItems] = useState<RoadmapItem[]>([]);
const [loading, setLoading] = useState(true);
const [votedIds, setVotedIds] = useState<Set<string>>(new Set());
const inProgressItems = [
{
title: 'IMPHNEN Twibbon',
description: 'Buat Twibbon kece untuk profile media sosialmu',
},
{
title: 'IMPHNEN Certificate',
description: 'Cetak sertifikat keren secara instan untuk anggota IMPHNEN',
},
];
useEffect(() => {
fetch('https://api.imphnen.dev/v1/landing/cms/roadmap')
.then((r) => r.json())
.then((json) => {
setItems(json.data || []);
})
.catch(() => {})
.finally(() => setLoading(false));
}, []);
const completedItems = [
{
title: 'IMPHNEN List Event',
description:
'Koleksi daftar event dan kolaborasi seru yang bisa kamu ikuti',
},
{
title: 'IMPHNEN Testimoni',
description: 'Berikan testimonial gokil buat komunitas IMPHNEN',
},
{
title: 'IMPHNEN Roadmap by Vote',
description: 'Usulkan ide fitur seru dan ajak anggota lain buat voting',
},
];
const upcomingItems = items.filter((i) => i.status === 'upcoming');
const inProgressItems = items.filter((i) => i.status === 'in_progress');
const completedItems = items.filter((i) => i.status === 'completed');
const handleVote = (index: number) => {
const newItems = [...upcomingItems];
newItems[index] = {
...newItems[index],
votes: newItems[index].voted
? newItems[index].votes - 1
: newItems[index].votes + 1,
voted: !newItems[index].voted,
};
setUpcomingItems(newItems);
const handleVote = (id: string) => {
const alreadyVoted = votedIds.has(id);
// Optimistic update
setItems((prev) =>
prev.map((item) =>
item.id === id
? { ...item, votes: item.votes + (alreadyVoted ? -1 : 1) }
: item
)
);
if (alreadyVoted) {
setVotedIds((prev) => {
const next = new Set(prev);
next.delete(id);
return next;
});
} else {
setVotedIds((prev) => new Set(prev).add(id));
fetch(`https://api.imphnen.dev/v1/landing/cms/roadmap/vote/${id}`, {
method: 'POST',
}).catch(() => {});
}
};
const containerVariants = {
@@ -77,6 +76,14 @@ export default function ProjectsVote() {
visible: { opacity: 1, y: 0, transition: { duration: 0.2 } },
};
if (loading) {
return (
<div className="flex items-center justify-center py-20">
<div className="w-8 h-8 border-3 border-gray-200 border-t-primary-500 rounded-full animate-spin" />
</div>
);
}
return (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 max-w-7xl mx-auto">
@@ -95,8 +102,8 @@ export default function ProjectsVote() {
animate="visible"
className="space-y-5"
>
{upcomingItems.map((item, index) => (
<motion.div key={index} variants={itemVariants} layout>
{upcomingItems.map((item) => (
<motion.div key={item.id} variants={itemVariants} layout>
<Card className="bg-white border border-gray-200 hover:border-primary-200 shadow-sm hover:shadow-md transition-all h-full flex flex-col">
<CardHeader className="pb-3">
<CardTitle className="text-lg font-semibold text-gray-900 flex items-start">
@@ -110,24 +117,24 @@ export default function ProjectsVote() {
<div className="flex items-center justify-between border-t border-gray-100 pt-3">
<motion.button
whileTap={{ scale: 0.95 }}
onClick={() => handleVote(index)}
onClick={() => handleVote(item.id)}
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all ${
item.voted
votedIds.has(item.id)
? 'bg-primary-500 text-white hover:bg-primary-600'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
<motion.span
animate={{ scale: item.voted ? [1, 1.2, 1] : 1 }}
animate={{ scale: votedIds.has(item.id) ? [1, 1.2, 1] : 1 }}
transition={{ duration: 0.2 }}
>
<BiUpvote
className={`w-4 h-4 ${
item.voted ? 'text-white' : 'text-gray-600'
votedIds.has(item.id) ? 'text-white' : 'text-gray-600'
}`}
/>
</motion.span>
<span>{item.voted ? 'Voted' : 'Vote'}</span>
<span>{votedIds.has(item.id) ? 'Voted' : 'Vote'}</span>
</motion.button>
<div className="flex items-center gap-2 bg-gray-50 px-3 py-1.5 rounded-lg">
<BiUpvote className="w-4 h-4 text-gray-500" />
@@ -140,6 +147,9 @@ export default function ProjectsVote() {
</Card>
</motion.div>
))}
{upcomingItems.length === 0 && (
<p className="text-gray-500 text-sm px-2">No upcoming items yet.</p>
)}
</motion.div>
</AnimatePresence>
</div>
@@ -160,7 +170,7 @@ export default function ProjectsVote() {
className="space-y-5"
>
{inProgressItems.map((item, index) => (
<motion.div key={index} variants={itemVariants} layout>
<motion.div key={item.id} variants={itemVariants} layout>
<Card className="bg-white border border-gray-200 hover:border-primary-200 shadow-sm hover:shadow-md transition-all h-full">
<CardHeader className="pb-3">
<CardTitle className="text-lg font-semibold text-gray-900 flex items-start">
@@ -176,14 +186,15 @@ export default function ProjectsVote() {
style={{ width: `${(index + 1) * 33}%` }}
></div>
</div>
<p className="text-xs text-gray-500 mt-2">
{index === 0 ? 'Development started' : 'In development'}
</p>
<p className="text-xs text-gray-500 mt-2">In development</p>
</div>
</CardContent>
</Card>
</motion.div>
))}
{inProgressItems.length === 0 && (
<p className="text-gray-500 text-sm px-2">Nothing in progress.</p>
)}
</motion.div>
</AnimatePresence>
</div>
@@ -203,8 +214,8 @@ export default function ProjectsVote() {
animate="visible"
className="space-y-5"
>
{completedItems.map((item, index) => (
<motion.div key={index} variants={itemVariants} layout>
{completedItems.map((item) => (
<motion.div key={item.id} variants={itemVariants} layout>
<Card className="bg-white border border-gray-200 hover:border-green-200 shadow-sm hover:shadow-md transition-all h-full">
<CardHeader className="pb-3">
<CardTitle className="text-lg font-semibold text-gray-900 flex items-start">
@@ -229,6 +240,9 @@ export default function ProjectsVote() {
</Card>
</motion.div>
))}
{completedItems.length === 0 && (
<p className="text-gray-500 text-sm px-2">No completed items yet.</p>
)}
</motion.div>
</AnimatePresence>
</div>