meti-frontend/app/page.tsx

381 lines
15 KiB
TypeScript
Raw Normal View History

2025-08-15 23:03:15 +07:00
"use client"
import { useEffect, useState } from "react"
import { useRouter } from "next/navigation"
import Link from "next/link"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Vote, Users, Settings, BarChart3, LogOut, Clock, Calendar, Play, StopCircle, Timer, CheckCircle2, AlertCircle, Sparkles } from "lucide-react"
import { useAuth } from "@/hooks/use-auth"
import apiClient from "@/lib/api-client"
import { API_CONFIG } from "@/lib/config"
import { DashboardHeader } from "@/components/dashboard-header"
interface VoteEvent {
id: string
title: string
description: string
start_date: string
end_date: string
is_active: boolean
is_voting_open: boolean
status?: string
}
interface Countdown {
days: number
hours: number
minutes: number
seconds: number
isActive: boolean
isEnded: boolean
isUpcoming: boolean
}
export default function HomePage() {
const { user, isAuthenticated, isAdmin, isSuperAdmin, logout, loading } = useAuth()
const [voteEvents, setVoteEvents] = useState<VoteEvent[]>([])
const [eventsLoading, setEventsLoading] = useState(true)
const [countdowns, setCountdowns] = useState<Record<string, Countdown>>({})
const router = useRouter()
// Fetch vote events
useEffect(() => {
if (isAuthenticated) {
fetchVoteEvents()
}
}, [isAuthenticated])
// Update countdowns every second
useEffect(() => {
if (voteEvents.length > 0) {
const interval = setInterval(() => {
updateCountdowns()
}, 1000)
return () => clearInterval(interval)
}
}, [voteEvents])
const fetchVoteEvents = async () => {
try {
setEventsLoading(true)
const response = await apiClient.get(API_CONFIG.ENDPOINTS.VOTE_EVENTS)
if (response.data.success) {
setVoteEvents(response.data.data.vote_events || [])
// Initialize countdowns for all events
const initialCountdowns: Record<string, Countdown> = {}
response.data.data.vote_events.forEach((event: VoteEvent) => {
initialCountdowns[event.id] = calculateCountdown(event.start_date, event.end_date)
})
setCountdowns(initialCountdowns)
}
} catch (error) {
console.error('Error fetching vote events:', error)
} finally {
setEventsLoading(false)
}
}
const calculateCountdown = (startDate: string, endDate: string): Countdown => {
const now = new Date().getTime()
const start = new Date(startDate).getTime()
const end = new Date(endDate).getTime()
let targetTime: number
let isActive = false
let isEnded = false
let isUpcoming = false
if (now < start) {
// Event hasn't started yet
targetTime = start
isUpcoming = true
} else if (now >= start && now <= end) {
// Event is active
targetTime = end
isActive = true
} else {
// Event has ended
targetTime = end
isEnded = true
}
const timeLeft = Math.max(0, targetTime - now)
const days = Math.floor(timeLeft / (1000 * 60 * 60 * 24))
const hours = Math.floor((timeLeft % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))
const minutes = Math.floor((timeLeft % (1000 * 60 * 60)) / (1000 * 60))
const seconds = Math.floor((timeLeft % (1000 * 60)) / 1000)
return {
days,
hours,
minutes,
seconds,
isActive,
isEnded,
isUpcoming
}
}
const updateCountdowns = () => {
const updatedCountdowns: Record<string, Countdown> = {}
voteEvents.forEach((event) => {
updatedCountdowns[event.id] = calculateCountdown(event.start_date, event.end_date)
})
setCountdowns(updatedCountdowns)
}
const formatCountdown = (countdown: Countdown) => {
if (countdown.isEnded) {
return "Event Ended"
}
if (countdown.days > 0) {
return `${countdown.days}d ${countdown.hours}h ${countdown.minutes}m ${countdown.seconds}s`
} else if (countdown.hours > 0) {
return `${countdown.hours}h ${countdown.minutes}m ${countdown.seconds}s`
} else if (countdown.minutes > 0) {
return `${countdown.minutes}m ${countdown.seconds}s`
} else {
return `${countdown.seconds}s`
}
}
const getEventStatus = (countdown: Countdown) => {
if (countdown.isEnded) return "ended"
if (countdown.isActive) return "active"
if (countdown.isUpcoming) return "upcoming"
return "unknown"
}
const getStatusBadge = (status: string, isVotingOpen: boolean) => {
if (isVotingOpen && status === "active") {
return (
<Badge className="bg-gradient-to-r from-green-500 to-emerald-500 text-white border-0 shadow-lg">
<Play className="h-3 w-3 mr-1" />
Live Voting
</Badge>
)
}
switch (status) {
case "active":
return (
<Badge className="bg-orange-100 text-orange-800 border-orange-200">
<AlertCircle className="h-3 w-3 mr-1" />
Active (Voting Closed)
</Badge>
)
case "upcoming":
return (
<Badge className="bg-blue-100 text-blue-800 border-blue-200">
<Clock className="h-3 w-3 mr-1" />
Coming Soon
</Badge>
)
case "ended":
return (
<Badge className="bg-gray-100 text-gray-800 border-gray-200">
<CheckCircle2 className="h-3 w-3 mr-1" />
Completed
</Badge>
)
default:
return <Badge variant="outline">Unknown</Badge>
}
}
// Show loading while checking authentication
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="text-center">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p className="text-gray-600">Loading...</p>
</div>
</div>
)
}
// Redirect to login if not authenticated
if (!isAuthenticated) {
router.push("/login")
return null
}
return (
<div className="min-h-screen bg-gray-50">
{/* Dashboard Header */}
<DashboardHeader
title="E-Voting Platform"
showStats={true}
stats={{
totalEvents: voteEvents.length,
activeEvents: voteEvents.filter(event => event.is_voting_open).length
}}
/>
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Page Title Section */}
<div className="text-center mb-8">
<div className="inline-flex items-center gap-2 bg-blue-50 rounded-full px-4 py-2 mb-4">
<Sparkles className="h-4 w-4 text-blue-600" />
<span className="text-blue-700 text-sm font-medium">Democratic Participation</span>
</div>
<h2 className="text-2xl sm:text-3xl font-bold text-gray-900 mb-3">
Available Vote Events
</h2>
<p className="text-base text-gray-600 max-w-xl mx-auto px-4">
Participate in democratic decisions and make your voice heard
</p>
</div>
{/* Vote Events Section */}
<div className="">
{eventsLoading ? (
<div className="text-center py-16">
<div className="animate-spin rounded-full h-16 w-16 border-4 border-gray-200 border-t-blue-600 mx-auto mb-6"></div>
<p className="text-gray-900 text-lg font-medium">Loading vote events...</p>
<p className="text-gray-600 text-sm mt-2">Please wait while we fetch the latest information</p>
</div>
) : voteEvents.length > 0 ? (
<div className="grid gap-6 max-w-6xl mx-auto">
{voteEvents.map((event) => {
const countdown = countdowns[event.id]
const status = getEventStatus(countdown)
return (
<Card key={event.id} className="bg-white hover:shadow-lg transition-all duration-300 border overflow-hidden">
<CardHeader className="pb-4">
<div className="flex flex-col sm:flex-row justify-between items-start gap-4">
<div className="flex-1">
<div className="flex items-start gap-3 mb-2">
<CardTitle className="text-2xl font-bold text-gray-900">{event.title}</CardTitle>
{getStatusBadge(status, event.is_voting_open)}
</div>
<CardDescription className="text-base text-gray-600 leading-relaxed">
{event.description}
</CardDescription>
</div>
<div className="flex gap-2">
{event.is_voting_open ? (
<Link href={`/vote?event_id=${event.id}`} className="block">
<Button className="bg-gradient-to-r from-green-600 to-emerald-600 hover:from-green-700 hover:to-emerald-700 text-white shadow-lg hover:shadow-xl transition-all duration-300 transform hover:scale-[1.02] active:scale-[0.98] h-12 px-6">
<Vote className="h-5 w-5 mr-2" />
<span className="font-bold">🗳 Vote Now</span>
</Button>
</Link>
) : status === "upcoming" ? (
<Button variant="outline" className="h-12 px-6 border-2 border-blue-200 text-blue-700 cursor-not-allowed" disabled>
<Clock className="h-5 w-5 mr-2" />
<span className="font-semibold"> Coming Soon</span>
</Button>
) : (
<Button variant="outline" className="h-12 px-6 border-2 border-gray-200 text-gray-600 bg-gray-50 cursor-not-allowed" disabled>
<StopCircle className="h-5 w-5 mr-2" />
<span className="font-semibold">🔒 Voting Closed</span>
</Button>
)}
</div>
</div>
</CardHeader>
<CardContent>
{/* Event Dates and Countdown */}
<div className="grid sm:grid-cols-2 gap-4 text-sm">
<div className="flex items-center gap-2">
<Calendar className="h-4 w-4 text-blue-600" />
<span className="text-gray-600">Start:</span>
<span className="font-medium">
{new Date(event.start_date).toLocaleDateString('id-ID', {
weekday: 'short',
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
</span>
</div>
<div className="flex items-center gap-2">
<Calendar className="h-4 w-4 text-red-600" />
<span className="text-gray-600">End:</span>
<span className="font-medium">
{new Date(event.end_date).toLocaleDateString('id-ID', {
weekday: 'short',
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
</span>
</div>
</div>
{/* Countdown Display for Active Events */}
{!countdown?.isEnded && (event.is_voting_open || status === "upcoming") && (
<div className={`mt-4 text-center p-4 rounded-lg border-2 ${
event.is_voting_open && status === "active"
? "bg-gradient-to-br from-green-50 to-emerald-50 border-green-200"
: "bg-gradient-to-br from-blue-50 to-indigo-50 border-blue-200"
}`}>
<div className="flex items-center justify-center gap-2 mb-2">
{event.is_voting_open && status === "active" ? (
<Timer className="h-4 w-4 text-green-600 animate-pulse" />
) : (
<Clock className="h-4 w-4 text-blue-600" />
)}
<span className="text-sm font-semibold text-gray-900">
{event.is_voting_open && status === "active"
? "Voting ends in:"
: "Starts in:"}
</span>
</div>
<div className="text-lg font-bold text-blue-700">
{formatCountdown(countdown)}
</div>
</div>
)}
</CardContent>
</Card>
)
})}
</div>
) : (
<Card className="bg-white border shadow-lg max-w-2xl mx-auto">
<CardContent className="text-center py-16">
<div className="relative mb-8">
<div className="w-20 h-20 bg-gradient-to-br from-gray-100 to-gray-200 rounded-full mx-auto flex items-center justify-center">
<Vote className="h-10 w-10 text-gray-400" />
</div>
<div className="absolute -top-2 -right-2 w-6 h-6 bg-blue-100 rounded-full flex items-center justify-center">
<div className="w-2 h-2 bg-blue-500 rounded-full animate-pulse"></div>
</div>
</div>
<h3 className="text-2xl font-bold text-gray-900 mb-3">No Vote Events Available</h3>
<p className="text-gray-600 mb-6 max-w-md mx-auto">
There are currently no active or upcoming vote events. Check back soon for new voting opportunities.
</p>
<Button
onClick={fetchVoteEvents}
variant="outline"
className="bg-blue-50 border-blue-200 text-blue-700 hover:bg-blue-100"
>
<Calendar className="h-4 w-4 mr-2" />
Refresh Events
</Button>
</CardContent>
</Card>
)}
</div>
</div>
</div>
)
}