Files
peworkshop/app/(main)/stats/page.js
T
bobbert 8d0bca1d87 fixed a bunch of styling and component usage
made sure almost all viewer pages use the navbar component properly instead of using their hardcoded version, started to match the styling of the design for a lot of pages
2025-03-23 14:33:53 +00:00

333 lines
9.3 KiB
JavaScript

"use client";
import { useState, useEffect } from "react";
import { useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import styles from "./stats.module.css";
import NavBar from "@/components/NavBar";
export default function StatsPage() {
const { data: session, status } = useSession();
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [teamStats, setTeamStats] = useState(null);
const [expanded, setExpanded] = useState(false);
const router = useRouter();
useEffect(() => {
// Redirect to login if not authenticated
if (status === "unauthenticated") {
router.push("/login");
return;
}
if (status === "authenticated") {
fetchTeamStats();
}
}, [status, router]);
const fetchTeamStats = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch("/api/team-stats", {
method: "GET",
headers: {
"Content-Type": "application/json",
"Cache-Control": "no-cache, no-store, must-revalidate",
},
credentials: "include",
});
if (!response.ok) {
// Handle specific error cases
const errorData = await response.json().catch(() => ({}));
if (response.status === 401) {
throw new Error("Authentication error: Not logged in");
} else if (
response.status === 404 ||
errorData.error?.includes("not part of a team")
) {
throw new Error("You need to join a team before viewing stats");
} else {
throw new Error(errorData.error || "Failed to fetch team stats");
}
}
const data = await response.json();
setTeamStats(data);
} catch (err) {
console.error("Error fetching team stats:", err);
setError(err.message);
} finally {
setLoading(false);
}
};
const toggleExpanded = () => {
setExpanded(!expanded);
};
// Format timestamp to a more readable format
const formatTimestamp = (timestamp) => {
const date = new Date(timestamp);
return date.toLocaleString(undefined, {
month: "short",
day: "numeric",
hour: "numeric",
minute: "2-digit",
});
};
const renderCluesList = () => {
if (!teamStats) return null;
const { foundClues, skippedClues } = teamStats;
return (
<div className="clues-section">
<h3 className="section-title">
Found Clues ({foundClues?.length || 0})
</h3>
<ul className="clue-list">
{foundClues?.length > 0 ? (
foundClues.map((clue) => (
<li key={clue.id} className="clue-item">
<div className="clue-details">
<div className="clue-title">
{clue.title || `Clue #${clue.id}`}
</div>
<div className="clue-time">
{new Date(clue.found_at).toLocaleTimeString()}
</div>
</div>
</li>
))
) : (
<li className="empty-state">No clues found yet</li>
)}
</ul>
<h3 className="section-title">
Skipped Clues ({skippedClues?.length || 0})
</h3>
<ul className="clue-list">
{skippedClues?.length > 0 ? (
skippedClues.map((clue) => (
<li key={clue.id} className="clue-item skipped-clue">
<div className="clue-details">
<div className="clue-title">
{clue.title || `Clue #${clue.id}`}
</div>
<div className="clue-time">
Skipped at {new Date(clue.found_at).toLocaleTimeString()}
</div>
</div>
</li>
))
) : (
<li className="empty-state">No clues skipped</li>
)}
</ul>
</div>
);
};
if (loading) {
return (
<div className={styles.container}>
<div className={styles.loadingSpinner}></div>
<p className={styles.loadingText}>Loading team stats...</p>
</div>
);
}
if (error) {
return (
<div className={styles.container}>
<div className={styles.errorContainer}>
<h2 className={styles.errorTitle}>Error</h2>
<p className={styles.errorMessage}>{error}</p>
<div className={styles.buttonContainer}>
<button onClick={fetchTeamStats} className={styles.retryButton}>
Retry
</button>
<button
onClick={() => router.push("/team-selection")}
className={styles.teamButton}
>
Join a Team
</button>
</div>
</div>
</div>
);
}
if (!teamStats) {
return (
<div className={styles.container}>
<p className={styles.noData}>No team stats available.</p>
</div>
);
}
return (
<div className={styles.container}>
{/* Replace header with team banner that matches team page */}
<div className="team-banner">
<div className="team-info">
<h1 className="team-name">Team Name: {teamStats.team.name}</h1>
<div className="team-code">{teamStats.team.code}</div>
</div>
</div>
<div className={styles.statsCard}>
<div className={styles.summaryStats}>
<div className={styles.statItem}>
<span className={styles.statLabel}>Total Clues:</span>
<span className={styles.statValue}>{teamStats.totalClues}</span>
</div>
<div className={styles.statItem}>
<span className={styles.statLabel}>Clues Found:</span>
<span className={styles.statValue}>
{teamStats.foundClues.length} of {teamStats.totalClues}
</span>
</div>
<div className={styles.statItem}>
<span className={styles.statLabel}>Clues Remaining:</span>
<span className={styles.statValue}>{teamStats.remainingClues}</span>
</div>
<div className={styles.statItem}>
<span className={styles.statLabel}>Clues Skipped:</span>
<span className={styles.statValue}>
{teamStats.skippedCluesCount}
</span>
</div>
<div className={`${styles.statItem} ${styles.rankItem}`}>
<span className={styles.statLabel}>Current Rank:</span>
<span className={styles.statValue}>{teamStats.currentRank}</span>
</div>
</div>
<div className={styles.collapsibleSection}>
<button
className={styles.collapsibleButton}
onClick={toggleExpanded}
aria-expanded={expanded}
>
<span className={styles.collapsibleTitle}>
Clues Found: {teamStats.foundClues.length}
</span>
<span className={styles.collapsibleIcon}>
{expanded ? "▼" : "►"}
</span>
</button>
{expanded && (
<div className={styles.cluesList}>{renderCluesList()}</div>
)}
</div>
</div>
{/* Replace hardcoded navbar with NavBar component */}
<NavBar activeLink="stats" teamData={teamStats.team} />
<style jsx>{`
.team-banner {
background-image: url("/images/Bottom-Bar.svg");
background-repeat: no-repeat;
background-size: cover;
background-position: center;
color: white;
padding: 20px;
position: relative;
display: flex;
align-items: center;
}
.team-info {
flex: 1;
display: flex;
justify-content: space-between;
align-items: center;
}
.team-name {
margin: 0;
font-size: 24px;
font-weight: bold;
color: white;
}
.team-code {
font-size: 24px;
font-weight: bold;
margin-left: auto;
color: white;
}
.clues-section {
padding: 10px 15px;
}
.section-title {
font-size: 16px;
margin: 15px 0 10px 0;
color: #444;
font-weight: 600;
}
.clue-list {
list-style: none;
padding: 0;
margin: 0 0 20px 0;
}
.clue-item {
padding: 12px 15px;
border-bottom: 1px solid #eee;
background-color: #f9f9f9;
margin-bottom: 5px;
border-radius: 4px;
border-left: 3px solid #4caf50;
}
.skipped-clue {
border-left: 3px solid #2196f3;
}
.clue-details {
display: flex;
justify-content: space-between;
align-items: center;
}
.clue-title {
font-size: 14px;
color: #333;
flex: 1;
}
.clue-time {
font-size: 12px;
color: #666;
text-align: right;
margin-left: 10px;
white-space: nowrap;
}
.empty-state {
padding: 15px;
text-align: center;
color: #888;
font-style: italic;
background-color: #f3f3f3;
border-radius: 4px;
}
`}</style>
</div>
);
}