381 lines
15 KiB
TypeScript
381 lines
15 KiB
TypeScript
"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>
|
||
)
|
||
}
|