488 lines
18 KiB
TypeScript
Raw Normal View History

2025-08-15 23:03:15 +07:00
"use client"
import { useEffect, useState, Suspense } from "react"
import { useRouter, useSearchParams } 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 { Alert, AlertDescription } from "@/components/ui/alert"
import {
Vote,
ArrowLeft,
CheckCircle2,
Clock,
AlertCircle,
User,
Image as ImageIcon,
Loader2,
Shield,
Timer,
CheckCircle
} from "lucide-react"
import { useAuth } from "@/hooks/use-auth"
import apiClient from "@/lib/api-client"
import { API_CONFIG } from "@/lib/config"
interface Candidate {
id: string
vote_event_id: string
name: string
image_url: string
description: string
created_at: string
updated_at: string
}
interface VoteEvent {
id: string
title: string
description: string
start_date: string
end_date: string
is_active: boolean
is_voting_open: boolean
created_at: string
updated_at: string
candidates: Candidate[]
}
interface VoteStatus {
has_voted: boolean
}
interface VoteRequest {
vote_event_id: string
candidate_id: string
}
function VotePageContent() {
const { user, isAuthenticated, loading } = useAuth()
const router = useRouter()
const searchParams = useSearchParams()
const eventId = searchParams.get('event_id')
const [event, setEvent] = useState<VoteEvent | null>(null)
const [voteStatus, setVoteStatus] = useState<VoteStatus | null>(null)
const [selectedCandidate, setSelectedCandidate] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [isVoting, setIsVoting] = useState(false)
const [error, setError] = useState<string | null>(null)
const [success, setSuccess] = useState<string | null>(null)
// Fetch event details and voting status
useEffect(() => {
if (isAuthenticated && eventId) {
fetchEventDetails()
fetchVoteStatus()
}
}, [isAuthenticated, eventId])
const fetchEventDetails = async () => {
try {
setIsLoading(true)
const response = await apiClient.get(`${API_CONFIG.ENDPOINTS.VOTE_EVENTS}/${eventId}`)
if (response.data.success) {
setEvent(response.data.data)
} else {
setError("Failed to fetch event details")
}
} catch (error: any) {
console.error('Error fetching event details:', error)
setError(error.response?.data?.errors || "Failed to fetch event details")
} finally {
setIsLoading(false)
}
}
const fetchVoteStatus = async () => {
try {
const response = await apiClient.get(`${API_CONFIG.ENDPOINTS.VOTE_EVENTS}/${eventId}/vote-status`)
if (response.data.success) {
setVoteStatus(response.data.data)
} else {
setError("Failed to fetch voting status")
}
} catch (error: any) {
console.error('Error fetching vote status:', error)
setError(error.response?.data?.errors || "Failed to fetch voting status")
}
}
const handleVote = async () => {
if (!selectedCandidate || !event) return
try {
setIsVoting(true)
setError(null)
setSuccess(null)
const voteData: VoteRequest = {
vote_event_id: event.id,
candidate_id: selectedCandidate
}
const response = await apiClient.post(API_CONFIG.ENDPOINTS.VOTES, voteData)
if (response.data.success) {
setSuccess("Your vote has been recorded successfully!")
setVoteStatus({ has_voted: true })
// Refresh vote status
setTimeout(() => {
fetchVoteStatus()
}, 1000)
} else {
setError(response.data.errors || "Failed to submit vote")
}
} catch (error: any) {
console.error('Error submitting vote:', error)
setError(error.response?.data?.errors || "Failed to submit vote. Please try again.")
} finally {
setIsVoting(false)
}
}
const handleCandidateSelect = (candidateId: string) => {
setSelectedCandidate(candidateId)
setError(null)
}
// 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
}
// Redirect to home if no event ID
if (!eventId) {
router.push("/")
return null
}
// Check if event is still active and voting is open
const isEventActive = event && new Date() >= new Date(event.start_date) && new Date() <= new Date(event.end_date)
const canVote = event && event.is_voting_open && isEventActive && !voteStatus?.has_voted
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-purple-50">
{/* Header */}
<header className="bg-white shadow-sm border-b">
<div className="container mx-auto px-4 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Link href="/">
<Button variant="ghost" size="sm" className="gap-2">
<ArrowLeft className="h-4 w-4" />
Back to Home
</Button>
</Link>
<div className="h-6 w-px bg-gray-300"></div>
<h1 className="text-xl font-semibold text-gray-900">Voting Interface</h1>
</div>
<div className="flex items-center gap-3">
<div className="flex items-center gap-2 text-sm text-gray-600">
<User className="h-4 w-4" />
<span>{user?.name}</span>
</div>
<Badge variant="outline" className="bg-blue-50 text-blue-700 border-blue-200">
<Shield className="h-3 w-3 mr-1" />
Authenticated
</Badge>
</div>
</div>
</div>
</header>
<div className="container mx-auto px-4 py-8">
{isLoading ? (
<div className="text-center py-16">
<div className="relative">
<div className="animate-spin rounded-full h-16 w-16 border-4 border-blue-200 border-t-blue-600 mx-auto mb-6"></div>
</div>
<p className="text-gray-600 text-lg">Loading event details...</p>
</div>
) : error ? (
<div className="max-w-2xl mx-auto">
<Alert className="border-red-200 bg-red-50">
<AlertCircle className="h-4 w-4 text-red-600" />
<AlertDescription className="text-red-800">
{error}
</AlertDescription>
</Alert>
<div className="mt-4 text-center">
<Button onClick={() => window.location.reload()} variant="outline">
Try Again
</Button>
</div>
</div>
) : event ? (
<div className="max-w-4xl mx-auto space-y-8">
{/* Event Header */}
<Card className="bg-white shadow-lg border-0">
<CardHeader className="text-center pb-6">
<div className="flex items-center justify-center gap-2 mb-4">
{event.is_voting_open && isEventActive ? (
<Badge className="bg-green-100 text-green-800 border-green-200">
<Timer className="h-3 w-3 mr-1" />
Voting Open
</Badge>
) : !isEventActive ? (
<Badge className="bg-gray-100 text-gray-800 border-gray-200">
<Clock className="h-3 w-3 mr-1" />
Event Ended
</Badge>
) : (
<Badge className="bg-blue-100 text-blue-800 border-blue-200">
<Clock className="h-3 w-3 mr-1" />
Coming Soon
</Badge>
)}
</div>
<CardTitle className="text-3xl font-bold text-gray-900 mb-3">
{event.title}
</CardTitle>
<CardDescription className="text-lg text-gray-600 max-w-2xl mx-auto">
{event.description}
</CardDescription>
</CardHeader>
<CardContent className="text-center">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 text-sm text-gray-600">
<div className="flex items-center justify-center gap-2">
<Clock className="h-4 w-4 text-blue-600" />
<span>Start: {new Date(event.start_date).toLocaleDateString('id-ID', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}</span>
</div>
<div className="flex items-center justify-center gap-2">
<Clock className="h-4 w-4 text-red-600" />
<span>End: {new Date(event.end_date).toLocaleDateString('id-ID', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}</span>
</div>
</div>
</CardContent>
</Card>
{/* Voting Status - Only show if user hasn't voted */}
{voteStatus && !voteStatus.has_voted && (
<Card className="border-0 shadow-lg bg-blue-50 border-blue-200">
<CardContent className="p-6">
<div className="flex items-center justify-center gap-3">
<Vote className="h-6 w-6 text-blue-600" />
<div className="text-center">
<h3 className="text-lg font-semibold text-blue-800">Ready to vote</h3>
<p className="text-blue-600">Please select your preferred candidate below</p>
</div>
</div>
</CardContent>
</Card>
)}
{/* Success Message */}
{success && (
<Alert className="border-green-200 bg-green-50">
<CheckCircle className="h-4 w-4 text-green-600" />
<AlertDescription className="text-green-800">
{success}
</AlertDescription>
</Alert>
)}
{/* Candidates Section - Only show if user hasn't voted */}
{!voteStatus?.has_voted && event.candidates && event.candidates.length > 0 ? (
<div className="space-y-6">
<div className="text-center">
<h2 className="text-2xl font-bold text-gray-900 mb-2">Candidates</h2>
<p className="text-gray-600">
Choose your preferred candidate by clicking on their card
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{event.candidates.map((candidate) => (
<Card
key={candidate.id}
className={`cursor-pointer transition-all duration-300 border-2 hover:shadow-lg ${
selectedCandidate === candidate.id
? 'border-blue-500 bg-blue-50 shadow-lg'
: 'border-gray-200 hover:border-blue-300'
}`}
onClick={() => handleCandidateSelect(candidate.id)}
>
<CardHeader className="text-center pb-4">
<div className="w-20 h-20 mx-auto mb-4 rounded-full bg-gradient-to-br from-blue-100 to-purple-100 flex items-center justify-center">
{candidate.image_url ? (
<img
src={candidate.image_url}
alt={candidate.name}
className="w-full h-full rounded-full object-cover"
/>
) : (
<ImageIcon className="h-8 w-8 text-gray-400" />
)}
</div>
<CardTitle className="text-xl font-bold text-gray-900">
{candidate.name}
</CardTitle>
{candidate.description && (
<CardDescription className="text-gray-600">
{candidate.description}
</CardDescription>
)}
</CardHeader>
<CardContent className="text-center">
{selectedCandidate === candidate.id && (
<div className="mb-4">
<Badge className="bg-blue-100 text-blue-800 border-blue-200">
<CheckCircle2 className="h-3 w-3 mr-1" />
Selected
</Badge>
</div>
)}
</CardContent>
</Card>
))}
</div>
{/* Voting Button */}
{canVote && (
<div className="text-center pt-6">
<Button
onClick={handleVote}
disabled={!selectedCandidate || isVoting}
size="lg"
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-105 active:scale-95 px-8 py-3 text-lg"
>
{isVoting ? (
<>
<Loader2 className="h-5 w-5 mr-2 animate-spin" />
Submitting Vote...
</>
) : (
<>
<Vote className="h-5 w-5 mr-2" />
Submit Vote
</>
)}
</Button>
{!selectedCandidate && (
<p className="text-sm text-gray-500 mt-2">
Please select a candidate first
</p>
)}
</div>
)}
{/* Cannot Vote Messages */}
{!canVote && (
<div className="text-center pt-6">
{!event.is_voting_open && (
<Alert className="border-orange-200 bg-orange-50 max-w-md mx-auto">
<AlertCircle className="h-4 w-4 text-orange-600" />
<AlertDescription className="text-orange-800">
Voting is currently closed for this event
</AlertDescription>
</Alert>
)}
{!isEventActive && (
<Alert className="border-gray-200 bg-gray-50 max-w-md mx-auto">
<Clock className="h-4 w-4 text-gray-600" />
<AlertDescription className="text-gray-800">
This event has ended
</AlertDescription>
</Alert>
)}
</div>
)}
</div>
) : voteStatus?.has_voted ? (
/* Show message when user has already voted */
<Card className="bg-green-50 border-green-200 shadow-lg border-0">
<CardContent className="text-center py-16">
<CheckCircle2 className="h-16 w-16 text-green-600 mx-auto mb-4" />
<h3 className="text-xl font-semibold text-green-800 mb-2">Vote Submitted Successfully!</h3>
<p className="text-green-600 mb-6">
Thank you for participating in this election. Your vote has been recorded.
</p>
<div className="flex flex-col sm:flex-row gap-3 justify-center">
<Button variant="outline" onClick={() => router.push('/')}>
<ArrowLeft className="h-4 w-4 mr-2" />
Back to Home
</Button>
</div>
</CardContent>
</Card>
) : (
/* Show when no candidates available */
<Card className="bg-white shadow-lg border-0">
<CardContent className="text-center py-16">
<User className="h-16 w-16 text-gray-400 mx-auto mb-4" />
<h3 className="text-xl font-semibold text-gray-900 mb-2">No Candidates Available</h3>
<p className="text-gray-600 mb-6">
There are no candidates registered for this voting event yet.
</p>
<Button variant="outline" onClick={() => router.push('/')}>
<ArrowLeft className="h-4 w-4 mr-2" />
Back to Home
</Button>
</CardContent>
</Card>
)}
</div>
) : (
<div className="text-center py-16">
<AlertCircle className="h-16 w-16 text-gray-400 mx-auto mb-4" />
<h3 className="text-xl font-semibold text-gray-900 mb-2">Event Not Found</h3>
<p className="text-gray-600 mb-6">
The voting event you're looking for could not be found.
</p>
<Button onClick={() => router.push('/')} variant="outline">
<ArrowLeft className="h-4 w-4 mr-2" />
Back to Home
</Button>
</div>
)}
</div>
</div>
)
}
export default function VotePage() {
return (
<Suspense fallback={
<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>
}>
<VotePageContent />
</Suspense>
)
}