902 lines
38 KiB
TypeScript
Raw Normal View History

2025-08-15 23:03:15 +07:00
"use client"
import { useState, useEffect, Suspense } from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Badge } from "@/components/ui/badge"
2025-08-16 01:51:16 +07:00
import { BarChart3, Users, Trophy, TrendingUp, ArrowLeft, RotateCcw, Download, Eye, Maximize2, Minimize2, Table, Clock, RefreshCw } from "lucide-react"
2025-08-15 23:03:15 +07:00
import Link from "next/link"
import Image from "next/image"
import { AuthGuard } from "@/components/auth-guard"
import { useAuth } from "@/hooks/use-auth"
import { DashboardHeader } from "@/components/dashboard-header"
import apiClient from "@/lib/api-client"
import { API_CONFIG } from "@/lib/config"
import { useToast } from "@/hooks/use-toast"
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell, Legend } from 'recharts'
interface VoteEvent {
id: string
title: string
description: string
start_date: string
end_date: string
is_active: boolean
is_voting_open: boolean
2025-08-16 01:51:16 +07:00
results_open: boolean
}
interface VoteEventDetails {
vote_event: VoteEvent
total_participants: number
total_voted: number
total_not_voted: number
2025-08-15 23:03:15 +07:00
}
interface Candidate {
id: string
vote_event_id: string
name: string
image_url: string
description: string
created_at: string
updated_at: string
vote_count: number
}
interface VoteResults {
vote_event_id: string
candidates: Candidate[]
total_votes: number
}
interface ChartData {
name: string
votes: number
percentage: number
fill: string
}
function ResultsPageContent() {
const { user } = useAuth()
const { toast } = useToast()
const [events, setEvents] = useState<VoteEvent[]>([])
const [selectedEventId, setSelectedEventId] = useState<string>("")
const [results, setResults] = useState<VoteResults | null>(null)
2025-08-16 01:51:16 +07:00
const [eventDetails, setEventDetails] = useState<VoteEventDetails | null>(null)
2025-08-15 23:03:15 +07:00
const [loading, setLoading] = useState(false)
const [eventsLoading, setEventsLoading] = useState(true)
const [isFullPageChart, setIsFullPageChart] = useState(false)
const [showCountdown, setShowCountdown] = useState(false)
const [countdown, setCountdown] = useState(10)
const [showResults, setShowResults] = useState(false)
2025-08-16 01:51:16 +07:00
const [isFullscreen, setIsFullscreen] = useState(false)
const [isRefreshing, setIsRefreshing] = useState(false)
const [lastUpdated, setLastUpdated] = useState<Date>(new Date())
2025-08-15 23:03:15 +07:00
// Chart colors
const COLORS = ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#06B6D4', '#84CC16', '#F97316']
useEffect(() => {
fetchEvents()
}, [])
// Note: URL parameter handling removed for now - can be added back later if needed
useEffect(() => {
if (selectedEventId) {
2025-08-16 01:51:16 +07:00
fetchEventDetails(selectedEventId, false)
fetchResults(selectedEventId, false)
// Set up interval for live updates every 10 seconds
const interval = setInterval(() => {
fetchEventDetails(selectedEventId, true)
fetchResults(selectedEventId, true)
}, 10000) // 10 seconds
// Cleanup interval on unmount or when selectedEventId changes
return () => clearInterval(interval)
2025-08-15 23:03:15 +07:00
}
}, [selectedEventId])
useEffect(() => {
let interval: NodeJS.Timeout
if (showCountdown && countdown > 0) {
interval = setInterval(() => {
setCountdown(prev => prev - 1)
}, 1000)
} else if (showCountdown && countdown === 0) {
2025-08-16 01:51:16 +07:00
// Simply transition to results - fullscreen container remains the same
2025-08-15 23:03:15 +07:00
setShowCountdown(false)
setShowResults(true)
}
return () => clearInterval(interval)
}, [showCountdown, countdown])
2025-08-16 01:51:16 +07:00
// Fullscreen change listener
useEffect(() => {
const handleFullscreenChange = () => {
const fullscreenElement = document.fullscreenElement
setIsFullscreen(!!fullscreenElement)
// Add/remove fullscreen styles to the event overview card
const eventOverviewCard = document.querySelector('[data-fullscreen-target="event-overview"]')
if (eventOverviewCard) {
if (fullscreenElement === eventOverviewCard) {
eventOverviewCard.classList.add('fullscreen-active')
} else {
eventOverviewCard.classList.remove('fullscreen-active')
}
}
}
document.addEventListener('fullscreenchange', handleFullscreenChange)
return () => document.removeEventListener('fullscreenchange', handleFullscreenChange)
}, [])
2025-08-15 23:03:15 +07:00
const fetchEvents = async () => {
try {
setEventsLoading(true)
const response = await apiClient.get(API_CONFIG.ENDPOINTS.VOTE_EVENTS)
if (response.data.success) {
const eventList = response.data.data.vote_events || []
setEvents(eventList)
// Auto-select the first event if available
if (eventList.length > 0 && !selectedEventId) {
setSelectedEventId(eventList[0].id)
}
}
} catch (error) {
console.error('Error fetching events:', error)
toast({
title: "Error",
description: "Failed to fetch vote events",
variant: "destructive"
})
} finally {
setEventsLoading(false)
}
}
2025-08-16 01:51:16 +07:00
const fetchEventDetails = async (eventId: string, showRefreshIndicator = false) => {
try {
if (showRefreshIndicator) setIsRefreshing(true)
const response = await apiClient.get(`/api/v1/vote-events/${eventId}/details`)
if (response.data.success) {
setEventDetails(response.data.data)
setLastUpdated(new Date())
}
} catch (error) {
console.error('Error fetching event details:', error)
// Only show toast on initial load, not on refresh
if (!showRefreshIndicator) {
toast({
title: "Error",
description: "Failed to fetch event details",
variant: "destructive"
})
}
} finally {
if (showRefreshIndicator) {
setTimeout(() => setIsRefreshing(false), 500) // Show indicator for at least 500ms
}
}
}
const fetchResults = async (eventId: string, showRefreshIndicator = false) => {
2025-08-15 23:03:15 +07:00
try {
2025-08-16 01:51:16 +07:00
if (!showRefreshIndicator) setLoading(true)
if (showRefreshIndicator) setIsRefreshing(true)
2025-08-15 23:03:15 +07:00
const response = await apiClient.get(`${API_CONFIG.ENDPOINTS.RESULTS}/${eventId}/results`)
if (response.data.success) {
setResults(response.data.data)
2025-08-16 01:51:16 +07:00
setLastUpdated(new Date())
2025-08-15 23:03:15 +07:00
}
} catch (error) {
console.error('Error fetching results:', error)
2025-08-16 01:51:16 +07:00
// Only show toast on initial load, not on refresh
if (!showRefreshIndicator) {
toast({
title: "Error",
description: "Failed to fetch voting results",
variant: "destructive"
})
}
2025-08-15 23:03:15 +07:00
} finally {
2025-08-16 01:51:16 +07:00
if (!showRefreshIndicator) setLoading(false)
if (showRefreshIndicator) {
setTimeout(() => setIsRefreshing(false), 500) // Show indicator for at least 500ms
}
2025-08-15 23:03:15 +07:00
}
}
const getChartData = (): (ChartData & { image_url: string; id: string })[] => {
if (!results) return []
2025-08-16 01:51:16 +07:00
2025-08-15 23:03:15 +07:00
return results.candidates
.sort((a, b) => b.vote_count - a.vote_count) // Sort by vote count descending
.map((candidate, index) => ({
name: candidate.name,
votes: candidate.vote_count,
percentage: results.total_votes > 0 ? (candidate.vote_count / results.total_votes) * 100 : 0,
fill: COLORS[index % COLORS.length],
image_url: candidate.image_url,
id: candidate.id
}))
}
const getWinner = (): Candidate | null => {
if (!results || results.candidates.length === 0) return null
2025-08-16 01:51:16 +07:00
return results.candidates.reduce((prev, current) =>
2025-08-15 23:03:15 +07:00
prev.vote_count > current.vote_count ? prev : current
)
}
const getEventStatus = (event: VoteEvent) => {
const now = new Date()
const start = new Date(event.start_date)
const end = new Date(event.end_date)
if (now < start) return "upcoming"
if (now >= start && now <= end) return "active"
return "ended"
}
const getStatusBadge = (event: VoteEvent) => {
const status = getEventStatus(event)
2025-08-16 01:51:16 +07:00
2025-08-15 23:03:15 +07:00
if (event.is_voting_open && status === "active") {
return <Badge className="bg-green-500 text-white">Live Voting</Badge>
}
2025-08-16 01:51:16 +07:00
2025-08-15 23:03:15 +07:00
switch (status) {
case "active":
return <Badge className="bg-orange-500 text-white">Active</Badge>
case "upcoming":
return <Badge className="bg-blue-500 text-white">Upcoming</Badge>
case "ended":
return <Badge className="bg-gray-500 text-white">Ended</Badge>
default:
return <Badge variant="outline">Unknown</Badge>
}
}
const handleShowResults = () => {
if (!selectedEventId || !results) {
toast({
title: "Error",
description: "Please select an event with available results",
variant: "destructive"
})
return
}
2025-08-16 01:51:16 +07:00
// Check if we're in fullscreen mode from the reveal button
const isInFullscreen = document.fullscreenElement !== null
2025-08-15 23:03:15 +07:00
setShowCountdown(true)
setCountdown(10)
setShowResults(false)
2025-08-16 01:51:16 +07:00
// If we were in fullscreen from reveal, maintain it by making the container fullscreen
if (isInFullscreen) {
setTimeout(() => {
const container = document.querySelector('[data-fullscreen-target="results-container"]')
if (container && !document.fullscreenElement) {
container.requestFullscreen().catch(err => {
console.error('Error maintaining fullscreen:', err)
})
}
}, 100)
}
}
const toggleFullscreen = () => {
if (!document.fullscreenElement) {
// Find the event overview card and make it fullscreen
const eventOverviewCard = document.querySelector('[data-fullscreen-target="event-overview"]')
if (eventOverviewCard) {
eventOverviewCard.requestFullscreen().catch(err => {
console.error('Error attempting to enable fullscreen:', err)
})
setIsFullscreen(true)
}
} else {
document.exitFullscreen()
setIsFullscreen(false)
}
2025-08-15 23:03:15 +07:00
}
const selectedEvent = events.find(e => e.id === selectedEventId)
const chartData = getChartData()
const winner = getWinner()
return (
<div className="min-h-screen bg-gray-50">
<DashboardHeader title="Vote Results" />
<div className="container mx-auto px-4 py-8">
{/* Header Section */}
<div className="mb-8">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div>
<h1 className="text-3xl font-bold text-gray-900">Voting Results</h1>
<p className="text-gray-600 mt-1">View detailed voting results and statistics</p>
2025-08-16 01:51:16 +07:00
{selectedEventId && (
<div className="flex items-center gap-2 mt-2">
<div className={`flex items-center gap-1 text-sm ${isRefreshing ? 'text-blue-600' : 'text-gray-500'}`}>
<RefreshCw className={`h-3 w-3 ${isRefreshing ? 'animate-spin' : ''}`} />
<span>{isRefreshing ? 'Updating...' : 'Live updates every 10s'}</span>
</div>
<span className="text-gray-400"></span>
<span className="text-xs text-gray-500">
Last updated: {lastUpdated.toLocaleTimeString()}
</span>
</div>
)}
2025-08-15 23:03:15 +07:00
</div>
<div className="flex gap-2">
{results && (
<>
2025-08-16 01:51:16 +07:00
<Button
onClick={() => setIsFullPageChart(!isFullPageChart)}
variant={isFullPageChart ? "default" : "outline"}
2025-08-15 23:03:15 +07:00
className="gap-2"
>
{isFullPageChart ? (
<>
<Table className="h-4 w-4" />
Table View
</>
) : (
<>
<Maximize2 className="h-4 w-4" />
Full Chart
</>
)}
</Button>
</>
)}
2025-08-16 01:51:16 +07:00
<Button
onClick={toggleFullscreen}
variant="outline"
className="gap-2"
>
{isFullscreen ? (
<>
<Minimize2 className="h-4 w-4" />
Exit Fullscreen
</>
) : (
<>
<Maximize2 className="h-4 w-4" />
Fullscreen Overview
</>
)}
</Button>
2025-08-15 23:03:15 +07:00
<Button onClick={fetchEvents} variant="outline" className="gap-2">
<RotateCcw className="h-4 w-4" />
Refresh
</Button>
</div>
</div>
</div>
{/* Event Selection */}
<Card className="mb-6">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<BarChart3 className="h-5 w-5" />
Select Voting Event
</CardTitle>
<CardDescription>
Choose an event to view its voting results and statistics
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid md:grid-cols-2 gap-4 items-end">
<div>
<label className="text-sm font-medium text-gray-700 mb-2 block">
Vote Event
</label>
<Select value={selectedEventId} onValueChange={setSelectedEventId}>
<SelectTrigger>
<SelectValue placeholder="Select a vote event" />
</SelectTrigger>
<SelectContent>
{events.map((event) => (
<SelectItem key={event.id} value={event.id}>
<div className="flex items-center justify-between w-full">
<span>{event.title}</span>
<div className="ml-2">
{getStatusBadge(event)}
</div>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
2025-08-16 01:51:16 +07:00
2025-08-15 23:03:15 +07:00
{selectedEvent && (
<div className="space-y-2">
<div className="text-sm text-gray-600">
<strong>Event:</strong> {selectedEvent.title}
</div>
<div className="text-sm text-gray-600">
<strong>Period:</strong> {new Date(selectedEvent.start_date).toLocaleDateString()} - {new Date(selectedEvent.end_date).toLocaleDateString()}
</div>
</div>
)}
</div>
</CardContent>
</Card>
{/* Loading State */}
{loading && (
<div className="text-center py-12">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p className="text-gray-600">Loading results...</p>
</div>
)}
2025-08-16 01:51:16 +07:00
{/* Loading Event Details */}
{!loading && selectedEventId && !eventDetails && (
<div className="text-center py-12">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-green-600 mx-auto mb-4"></div>
<p className="text-gray-600">Loading event details...</p>
</div>
)}
2025-08-15 23:03:15 +07:00
{/* Results Content */}
{!loading && selectedEventId && results && (
<>
2025-08-16 01:51:16 +07:00
{/* Event Statistics */}
2025-08-15 23:03:15 +07:00
{!showCountdown && !showResults && (
2025-08-16 01:51:16 +07:00
<>
{eventDetails ? (
<div className="space-y-6 mb-8 animate-fade-in-up">
{/* Event Overview */}
<Card data-fullscreen-target="event-overview" className="relative">
<CardHeader className="card-header">
<div className="flex items-center justify-center gap-3 mb-2">
{isRefreshing && (
<Badge className="bg-blue-500 text-white animate-pulse">
<RefreshCw className="h-3 w-3 mr-1 animate-spin" />
Updating
</Badge>
)}
<Badge className="bg-green-500 text-white">
<div className="w-2 h-2 bg-white rounded-full animate-pulse mr-2"></div>
Live
</Badge>
</div>
<CardTitle className="text-4xl font-bold text-center">
{eventDetails.vote_event.title}
</CardTitle>
</CardHeader>
<CardContent className="card-content">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="text-center p-4 bg-blue-50 rounded-lg border border-blue-200">
<Users className="h-8 w-8 text-blue-600 mx-auto mb-2" />
<div className="text-2xl font-bold text-blue-600">
{eventDetails.total_participants}
</div>
<p className="text-sm text-blue-700 font-medium">Total Participants</p>
</div>
<div className="text-center p-4 bg-green-50 rounded-lg border border-green-200">
<TrendingUp className="h-8 w-8 text-green-600 mx-auto mb-2" />
<div className="text-2xl font-bold text-green-600">
{eventDetails.total_voted}
</div>
<p className="text-sm text-green-700 font-medium">Total Voted</p>
</div>
<div className="text-center p-4 bg-orange-50 rounded-lg border border-orange-200">
<Clock className="h-8 w-8 text-orange-600 mx-auto mb-2" />
<div className="text-2xl font-bold text-orange-600">
{eventDetails.total_not_voted}
</div>
<p className="text-sm text-orange-700 font-medium">Not Voted</p>
</div>
</div>
{/* Participation Rate */}
<div className="mt-6 p-4 bg-gradient-to-r from-purple-50 to-blue-50 rounded-lg border border-purple-200">
<div className="text-center">
<div className="text-3xl font-bold text-purple-600 mb-2">
{eventDetails.total_participants > 0
? ((eventDetails.total_voted / eventDetails.total_participants) * 100).toFixed(1)
: '0'
}%
</div>
<p className="text-gray-700 font-medium">Participation Rate</p>
<p className="text-sm text-gray-600">
{eventDetails.total_voted} out of {eventDetails.total_participants} participants have voted
</p>
</div>
</div>
</CardContent>
</Card>
</div>
) : (
<div className="mb-8">
<Card>
<CardContent className="text-center py-8">
<BarChart3 className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<p className="text-gray-600">Event details not available</p>
</CardContent>
</Card>
</div>
)}
{/* Show Results Button */}
<Card data-fullscreen-target="reveal-results" className="relative">
<CardContent className="text-center py-16 reveal-content">
<Button
onClick={() => {
const revealCard = document.querySelector('[data-fullscreen-target="reveal-results"]')
if (revealCard && !document.fullscreenElement) {
revealCard.requestFullscreen().catch(err => {
console.error('Error attempting to enable fullscreen:', err)
})
}
}}
variant="outline"
size="sm"
className="absolute top-4 right-4 gap-2 fullscreen-toggle-btn"
>
<Maximize2 className="h-4 w-4" />
Fullscreen
</Button>
{/* Exit fullscreen button (hidden by default, shown in fullscreen) */}
<Button
onClick={() => {
if (document.fullscreenElement) {
document.exitFullscreen()
}
}}
variant="outline"
size="sm"
className="hidden exit-fullscreen-btn"
>
<Minimize2 className="h-4 w-4" />
Exit Fullscreen
</Button>
{/* Event Details Section (hidden by default, shown in fullscreen) */}
{eventDetails && (
<div className="event-details-fullscreen hidden">
<div className="event-stats-grid">
<div className="stat-card participants-card">
<Users className="stat-icon" />
<div className="stat-value">{eventDetails.total_participants}</div>
<p className="stat-label">Total Participants</p>
</div>
<div className="stat-card voted-card">
<TrendingUp className="stat-icon" />
<div className="stat-value">{eventDetails.total_voted}</div>
<p className="stat-label">Total Voted</p>
</div>
<div className="stat-card not-voted-card">
<Clock className="stat-icon" />
<div className="stat-value">{eventDetails.total_not_voted}</div>
<p className="stat-label">Not Voted</p>
</div>
</div>
<div className="participation-rate-card">
<div className="rate-value">
{eventDetails.total_participants > 0
? ((eventDetails.total_voted / eventDetails.total_participants) * 100).toFixed(1)
: '0'
}%
</div>
<p className="rate-label">Participation Rate</p>
<p className="rate-description">
{eventDetails.total_voted} out of {eventDetails.total_participants} participants have voted
</p>
</div>
</div>
)}
<Trophy className="h-16 w-16 text-blue-500 mx-auto mb-6 trophy-icon" />
<h2 className="text-2xl font-bold mb-4 reveal-title">Ready to Reveal Results?</h2>
<p className="text-gray-600 mb-8 reveal-description">
Click the button below to see the voting results for "{selectedEvent?.title}"
</p>
<Button
onClick={(e) => {
e.stopPropagation()
handleShowResults()
}}
size="lg"
className="bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 text-white px-8 py-3 text-lg reveal-button"
>
<BarChart3 className="mr-2 h-5 w-5" />
Show Results
</Button>
</CardContent>
</Card>
</>
)}
{/* Results Container - Wraps both countdown and results for seamless fullscreen */}
<div data-fullscreen-target="results-container" className="results-fullscreen-container">
{/* Countdown Animation */}
{showCountdown && (
<Card className="relative countdown-card">
<CardContent className="text-center py-24 countdown-content">
<Button
onClick={() => {
const container = document.querySelector('[data-fullscreen-target="results-container"]')
if (container && !document.fullscreenElement) {
container.requestFullscreen().catch(err => {
console.error('Error attempting to enable fullscreen:', err)
})
}
}}
variant="outline"
size="sm"
className="absolute top-4 right-4 gap-2 fullscreen-toggle-btn"
>
<Maximize2 className="h-4 w-4" />
Fullscreen
</Button>
<Button
onClick={() => {
if (document.fullscreenElement) {
document.exitFullscreen()
}
}}
variant="outline"
size="sm"
className="hidden exit-fullscreen-btn"
2025-08-15 23:03:15 +07:00
>
2025-08-16 01:51:16 +07:00
<Minimize2 className="h-4 w-4" />
Exit Fullscreen
2025-08-15 23:03:15 +07:00
</Button>
2025-08-16 01:51:16 +07:00
<div className="relative countdown-wrapper">
<div className={`text-8xl font-bold mb-4 transition-all duration-1000 countdown-number ${
2025-08-15 23:03:15 +07:00
countdown <= 3 ? 'text-red-500 scale-125' : 'text-blue-500 scale-100'
}`}>
{countdown}
</div>
<div className="absolute inset-0 flex items-center justify-center">
2025-08-16 01:51:16 +07:00
<div className={`w-32 h-32 border-4 border-blue-200 rounded-full animate-pulse countdown-circle ${
2025-08-15 23:03:15 +07:00
countdown <= 3 ? 'border-red-200' : ''
}`}></div>
</div>
</div>
2025-08-16 01:51:16 +07:00
<p className="text-xl text-gray-600 animate-bounce countdown-text">
{countdown > 5 ? 'Preparing results...' :
2025-08-15 23:03:15 +07:00
countdown > 3 ? 'Almost ready...' : 'Here we go!'}
</p>
</CardContent>
</Card>
2025-08-16 01:51:16 +07:00
)}
2025-08-15 23:03:15 +07:00
2025-08-16 01:51:16 +07:00
{/* Animated Results Display */}
{showResults && (
<div className="space-y-8 results-display">
{/* Fullscreen Results Button */}
<div className="flex justify-end">
<Button
onClick={() => {
const container = document.querySelector('[data-fullscreen-target="results-container"]')
if (container && !document.fullscreenElement) {
container.requestFullscreen().catch(err => {
console.error('Error attempting to enable fullscreen:', err)
})
} else if (document.fullscreenElement) {
document.exitFullscreen()
}
}}
variant="outline"
className="gap-2 fullscreen-results-btn"
>
<Maximize2 className="h-4 w-4 maximize-icon" />
<Minimize2 className="h-4 w-4 minimize-icon hidden" />
<span className="btn-text">Fullscreen Results</span>
</Button>
</div>
2025-08-15 23:03:15 +07:00
{/* Animated Bar Chart with Profile Images */}
2025-08-16 01:51:16 +07:00
<Card className="animate-fade-in-up results-card" style={{ animationDelay: '0.3s' }}>
2025-08-15 23:03:15 +07:00
<CardHeader className="text-center">
2025-08-16 01:51:16 +07:00
<div className="flex items-center justify-center gap-2 mb-2">
{isRefreshing && (
<Badge className="bg-blue-500 text-white animate-pulse">
<RefreshCw className="h-3 w-3 mr-1 animate-spin" />
Updating
</Badge>
)}
<Badge className="bg-green-500 text-white">
<div className="w-2 h-2 bg-white rounded-full animate-pulse mr-2"></div>
Live Results
</Badge>
</div>
<CardTitle className="text-2xl results-title">Final Results</CardTitle>
<CardDescription className="results-subtitle">
Vote distribution by candidate Updated: {lastUpdated.toLocaleTimeString()}
</CardDescription>
2025-08-15 23:03:15 +07:00
</CardHeader>
<CardContent>
{chartData.length > 0 ? (
2025-08-16 01:51:16 +07:00
<div className="vertical-chart-container">
{/* Vertical Bar Chart */}
<div className="flex items-end justify-center gap-8 mb-8" style={{ height: '400px' }}>
{chartData.map((candidate, index) => {
const maxVotes = Math.max(...chartData.map(c => c.votes))
const barHeight = candidate.votes > 0 ? (candidate.votes / maxVotes) * 100 : 0
return (
<div key={candidate.id} className="flex flex-col items-center flex-1 h-full justify-end">
{/* Percentage above bar */}
<div
className="text-2xl font-bold mb-2"
style={{ color: candidate.fill }}
>
{candidate.percentage.toFixed(1)}%
2025-08-15 23:03:15 +07:00
</div>
2025-08-16 01:51:16 +07:00
{/* Vertical Bar Container */}
<div className="relative w-full max-w-[120px] h-full flex items-end">
{/* Background bar */}
<div className="absolute inset-0 bg-gradient-to-t from-gray-200 to-gray-100 rounded-t-2xl"></div>
{/* Animated Bar */}
<div
className="relative w-full rounded-t-2xl shadow-lg overflow-hidden vertical-bar"
style={{
background: `linear-gradient(135deg, ${candidate.fill}, ${candidate.fill}dd)`,
height: `${barHeight}%`,
transition: 'height 2s ease-out',
transitionDelay: `${index * 300}ms`,
minHeight: candidate.votes > 0 ? '40px' : '0'
}}
2025-08-15 23:03:15 +07:00
>
2025-08-16 01:51:16 +07:00
{/* Shimmer Effect */}
<div className="absolute inset-0 bg-gradient-to-t from-transparent via-white/20 to-transparent animate-pulse"></div>
{/* Vote count inside bar */}
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-white font-bold text-lg transform rotate-0">
{candidate.votes}
</span>
</div>
2025-08-15 23:03:15 +07:00
</div>
</div>
2025-08-16 01:51:16 +07:00
{/* Candidate Info Below Bar */}
<div className="mt-4 text-center">
{/* Profile Image */}
<div className="relative w-20 h-20 mx-auto mb-2 profile-image-container">
{candidate.image_url ? (
<Image
src={candidate.image_url}
alt={candidate.name}
fill
className="object-cover rounded-full border-4 shadow-lg"
style={{ borderColor: candidate.fill }}
/>
) : (
<div
className="w-full h-full bg-gray-200 rounded-full flex items-center justify-center border-4 shadow-lg"
style={{ borderColor: candidate.fill }}
>
<Users className="h-8 w-8 text-gray-400" />
</div>
2025-08-15 23:03:15 +07:00
)}
2025-08-16 01:51:16 +07:00
{/* Rank Badge */}
<div className="absolute -top-2 -right-2">
<div className={`px-2 py-1 rounded-full text-white font-bold text-xs shadow-lg ${
index === 0 ? 'bg-gradient-to-r from-yellow-400 to-yellow-600' :
index === 1 ? 'bg-gradient-to-r from-gray-400 to-gray-600' :
index === 2 ? 'bg-gradient-to-r from-orange-400 to-orange-600' :
'bg-gradient-to-r from-blue-400 to-blue-600'
}`}>
#{index + 1}
</div>
</div>
{/* Winner Crown */}
{index === 0 && candidate.votes > 0 && (
<div className="absolute -top-6 left-1/2 transform -translate-x-1/2">
<Trophy className="h-6 w-6 text-yellow-500 animate-bounce" />
</div>
2025-08-15 23:03:15 +07:00
)}
</div>
2025-08-16 01:51:16 +07:00
{/* Candidate Name */}
<h3 className="text-lg font-bold text-gray-900 mb-1">{candidate.name}</h3>
{/* Winner/Place Badge */}
{index < 3 && candidate.votes > 0 && (
<span className="text-xs px-2 py-1 rounded-full bg-gradient-to-r from-indigo-500 to-purple-600 text-white font-medium">
{index === 0 ? '🏆 Winner' : index === 1 ? '🥈 2nd' : '🥉 3rd'}
</span>
)}
2025-08-15 23:03:15 +07:00
</div>
</div>
2025-08-16 01:51:16 +07:00
)
})}
</div>
2025-08-15 23:03:15 +07:00
{/* Total Votes Summary */}
<div className="mt-8 p-4 bg-gradient-to-r from-blue-50 to-purple-50 rounded-lg border-2 border-blue-200">
<div className="text-center">
<div className="text-3xl font-bold text-gray-900 mb-2">
{results.total_votes}
</div>
<p className="text-gray-600">Total Votes Cast</p>
</div>
</div>
</div>
) : (
<div className="h-96 flex items-center justify-center bg-gray-50 rounded-lg">
<div className="text-center">
<BarChart3 className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<p className="text-gray-600">No voting data available</p>
</div>
</div>
)}
</CardContent>
</Card>
</div>
)}
2025-08-16 01:51:16 +07:00
</div>
2025-08-15 23:03:15 +07:00
</>
)}
{/* No Event Selected */}
{!loading && !selectedEventId && (
<Card>
<CardContent className="text-center py-12">
<BarChart3 className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium mb-2">Select a Vote Event</h3>
<p className="text-gray-600">Choose an event from the dropdown above to view its results</p>
</CardContent>
</Card>
)}
{/* No Results Available */}
{!loading && selectedEventId && !results && (
<Card>
<CardContent className="text-center py-12">
<BarChart3 className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium mb-2">No Results Available</h3>
<p className="text-gray-600">Results for this event are not yet available</p>
</CardContent>
</Card>
)}
</div>
</div>
)
}
export default function ResultsPage() {
return (
<AuthGuard requiredRole="superadmin">
<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>
}>
<ResultsPageContent />
</Suspense>
</AuthGuard>
)
2025-08-16 01:51:16 +07:00
}