| import { useState } from "react"; |
| import { Button } from "@/components/ui/button"; |
| import { Input } from "@/components/ui/input"; |
| import { supabase } from "@/integrations/supabase/client"; |
| import { useQuery } from "@tanstack/react-query"; |
| import { |
| Table, |
| TableBody, |
| TableCell, |
| TableHead, |
| TableHeader, |
| TableRow, |
| } from "@/components/ui/table"; |
| import { useToast } from "@/components/ui/use-toast"; |
| import { |
| Pagination, |
| PaginationContent, |
| PaginationItem, |
| PaginationLink, |
| PaginationNext, |
| PaginationPrevious, |
| } from "@/components/ui/pagination"; |
|
|
| interface HighScore { |
| id: string; |
| player_name: string; |
| score: number; |
| avg_words_per_round: number; |
| created_at: string; |
| } |
|
|
| interface HighScoreBoardProps { |
| currentScore: number; |
| avgWordsPerRound: number; |
| onClose: () => void; |
| onPlayAgain: () => void; |
| } |
|
|
| const ITEMS_PER_PAGE = 10; |
|
|
| const getRankMedal = (rank: number) => { |
| switch (rank) { |
| case 1: |
| return "🥇"; |
| case 2: |
| return "🥈"; |
| case 3: |
| return "🥉"; |
| default: |
| return null; |
| } |
| }; |
|
|
| export const HighScoreBoard = ({ |
| currentScore, |
| avgWordsPerRound, |
| onClose, |
| onPlayAgain, |
| }: HighScoreBoardProps) => { |
| const [playerName, setPlayerName] = useState(""); |
| const [isSubmitting, setIsSubmitting] = useState(false); |
| const [hasSubmitted, setHasSubmitted] = useState(false); |
| const [currentPage, setCurrentPage] = useState(1); |
| const { toast } = useToast(); |
|
|
| const { data: highScores, refetch } = useQuery({ |
| queryKey: ["highScores"], |
| queryFn: async () => { |
| const { data, error } = await supabase |
| .from("high_scores") |
| .select("*") |
| .order("score", { ascending: false }) |
| .order("avg_words_per_round", { ascending: true }); |
|
|
| if (error) throw error; |
| return data as HighScore[]; |
| }, |
| }); |
|
|
| const handleSubmitScore = async () => { |
| if (!playerName.trim()) { |
| toast({ |
| title: "Error", |
| description: "Please enter your name", |
| variant: "destructive", |
| }); |
| return; |
| } |
|
|
| if (currentScore < 1) { |
| toast({ |
| title: "Error", |
| description: "You need to complete at least one round to submit a score", |
| variant: "destructive", |
| }); |
| return; |
| } |
|
|
| if (hasSubmitted) { |
| toast({ |
| title: "Error", |
| description: "You have already submitted your score for this game", |
| variant: "destructive", |
| }); |
| return; |
| } |
|
|
| setIsSubmitting(true); |
| try { |
| const { error } = await supabase.from("high_scores").insert({ |
| player_name: playerName.trim(), |
| score: currentScore, |
| avg_words_per_round: avgWordsPerRound, |
| }); |
|
|
| if (error) throw error; |
|
|
| toast({ |
| title: "Success!", |
| description: "Your score has been recorded", |
| }); |
| |
| setHasSubmitted(true); |
| await refetch(); |
| setPlayerName(""); |
| } catch (error) { |
| console.error("Error submitting score:", error); |
| toast({ |
| title: "Error", |
| description: "Failed to submit score. Please try again.", |
| variant: "destructive", |
| }); |
| } finally { |
| setIsSubmitting(false); |
| } |
| }; |
|
|
| const totalPages = highScores ? Math.ceil(highScores.length / ITEMS_PER_PAGE) : 0; |
| const startIndex = (currentPage - 1) * ITEMS_PER_PAGE; |
| const paginatedScores = highScores?.slice(startIndex, startIndex + ITEMS_PER_PAGE); |
|
|
| const handlePreviousPage = () => { |
| if (currentPage > 1) { |
| setCurrentPage(p => p - 1); |
| } |
| }; |
|
|
| const handleNextPage = () => { |
| if (currentPage < totalPages) { |
| setCurrentPage(p => p + 1); |
| } |
| }; |
|
|
| return ( |
| <div className="space-y-6"> |
| <div className="text-center"> |
| <h2 className="text-2xl font-bold mb-2">Leaderboard</h2> |
| <p className="text-gray-600"> |
| Your score: {currentScore} rounds |
| {currentScore > 0 && ` (${avgWordsPerRound.toFixed(1)} words/round)`} |
| </p> |
| </div> |
| |
| {!hasSubmitted && currentScore > 0 && ( |
| <div className="flex gap-4 mb-6"> |
| <Input |
| placeholder="Enter your name" |
| value={playerName} |
| onChange={(e) => setPlayerName(e.target.value)} |
| className="flex-1" |
| /> |
| <Button |
| onClick={handleSubmitScore} |
| disabled={isSubmitting || !playerName.trim() || hasSubmitted} |
| > |
| {isSubmitting ? "Submitting..." : "Submit Score"} |
| </Button> |
| </div> |
| )} |
| |
| <div className="rounded-md border"> |
| <Table> |
| <TableHeader> |
| <TableRow> |
| <TableHead>Rank</TableHead> |
| <TableHead>Player</TableHead> |
| <TableHead>Rounds</TableHead> |
| <TableHead>Avg Words/Round</TableHead> |
| </TableRow> |
| </TableHeader> |
| <TableBody> |
| {paginatedScores?.map((score, index) => { |
| const absoluteRank = startIndex + index + 1; |
| const medal = getRankMedal(absoluteRank); |
| return ( |
| <TableRow key={score.id}> |
| <TableCell> |
| {absoluteRank} |
| {medal && <span className="ml-2">{medal}</span>} |
| </TableCell> |
| <TableCell>{score.player_name}</TableCell> |
| <TableCell>{score.score}</TableCell> |
| <TableCell>{score.avg_words_per_round.toFixed(1)}</TableCell> |
| </TableRow> |
| ); |
| })} |
| {!paginatedScores?.length && ( |
| <TableRow> |
| <TableCell colSpan={4} className="text-center"> |
| No high scores yet. Be the first! |
| </TableCell> |
| </TableRow> |
| )} |
| </TableBody> |
| </Table> |
| </div> |
| |
| {totalPages > 1 && ( |
| <Pagination> |
| <PaginationContent> |
| <PaginationItem> |
| <PaginationPrevious |
| onClick={handlePreviousPage} |
| className={currentPage === 1 ? "pointer-events-none opacity-50" : ""} |
| /> |
| </PaginationItem> |
| {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => ( |
| <PaginationItem key={page}> |
| <PaginationLink |
| onClick={() => setCurrentPage(page)} |
| isActive={currentPage === page} |
| > |
| {page} |
| </PaginationLink> |
| </PaginationItem> |
| ))} |
| <PaginationItem> |
| <PaginationNext |
| onClick={handleNextPage} |
| className={currentPage === totalPages ? "pointer-events-none opacity-50" : ""} |
| /> |
| </PaginationItem> |
| </PaginationContent> |
| </Pagination> |
| )} |
| |
| <div className="flex justify-end gap-4"> |
| <Button variant="outline" onClick={onClose}> |
| Close |
| </Button> |
| <Button onClick={onPlayAgain}>Play Again</Button> |
| </div> |
| </div> |
| ); |
| }; |