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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user