meti-frontend/app/page.tsx
2025-08-15 23:03:15 +07:00

381 lines
15 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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>
)
}