Files
peworkshop/app/(main)/team/[teamId]/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

611 lines
18 KiB
JavaScript

"use client";
import { useState, useEffect, useCallback } from "react";
import { useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import NavBar from "@/components/NavBar";
export default function TeamPage({ params }) {
const { status, data: session } = useSession();
const [team, setTeam] = useState(null);
const [teamMembers, setTeamMembers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [huntActive, setHuntActive] = useState(false);
const router = useRouter();
const { teamId } = params;
// Use the authenticated teamId if available
const effectiveTeamId = session?.user?.teamId || teamId;
const checkHuntStatus = useCallback(async () => {
try {
const response = await fetch("/api/hunt-status", {
method: "GET",
headers: { "Content-Type": "application/json" },
credentials: "include",
});
if (response.ok) {
const data = await response.json();
setHuntActive(data.active || false);
}
} catch (err) {
console.error("Failed to check hunt status:", err);
}
}, []);
const fetchTeamData = useCallback(async () => {
try {
if (!effectiveTeamId || isNaN(parseInt(effectiveTeamId, 10))) {
throw new Error("Invalid team ID");
}
// Validate current user data against effectiveTeamId
const userResponse = await fetch("/api/user", {
method: "GET",
headers: {
"Content-Type": "application/json",
},
credentials: "include",
cache: "no-store",
});
const userData = await userResponse.json();
// If the user is not in this team anymore, redirect to team selection
if (
!userData.teamId ||
parseInt(userData.teamId, 10) !== parseInt(effectiveTeamId, 10)
) {
console.log("User no longer in team, redirecting to team selection");
window.location.href = "/team-selection";
return;
}
console.log("Fetching team data for team ID:", effectiveTeamId);
// Add timestamp to URL to ensure fresh data on every request
const timestamp = new Date().getTime();
const response = await fetch(
`/api/teams/${effectiveTeamId}?_t=${timestamp}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
"Cache-Control": "no-cache, no-store, must-revalidate",
Pragma: "no-cache",
},
credentials: "include",
cache: "no-store",
}
);
if (!response.ok) {
const text = await response.text();
let errorMessage;
try {
const errorData = JSON.parse(text);
errorMessage = errorData.error || "Failed to fetch team data";
} catch (unused) {
errorMessage = `Failed to fetch team data: ${
response.status
} ${text.slice(0, 100)}`;
}
throw new Error(errorMessage);
}
const data = await response.json();
console.log("Team data received:", data);
if (!data.team) {
throw new Error("Team not found");
}
setTeam(data.team);
setTeamMembers(data.members || []);
setError("");
await checkHuntStatus();
} catch (err) {
console.error("Team page error:", err);
setError(err.message);
// If we get a 401 error, redirect to login
if (err.message.includes("401")) {
router.push("/login");
}
} finally {
setLoading(false);
}
}, [effectiveTeamId, router, checkHuntStatus]);
useEffect(() => {
// Only attempt to fetch data when session is ready and we have a teamId
if (status === "authenticated" && effectiveTeamId) {
console.log("Session authenticated, fetching team data");
fetchTeamData();
// Use a shorter interval (3 seconds) for more responsive updates
const refreshInterval = setInterval(fetchTeamData, 3000);
// Clean up the interval when component unmounts
return () => clearInterval(refreshInterval);
} else if (status === "unauthenticated") {
console.log("User not authenticated, redirecting to login");
router.push("/login");
}
}, [effectiveTeamId, status, fetchTeamData, router]);
// Add a function to properly handle the "Continue Hunt" button click
const continueTreasureHunt = async (e) => {
e.preventDefault();
setLoading(true);
try {
// Check if hunt is active first
const timestamp = Date.now();
const response = await fetch(`/api/hunt-status?t=${timestamp}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
"Cache-Control": "no-store, no-cache, must-revalidate",
Pragma: "no-cache",
},
credentials: "include",
});
if (response.ok) {
const data = await response.json();
if (data.active) {
// Hunt is active, redirect to map
router.push("/map");
} else {
// Hunt is not active, redirect to waiting page
router.push("/waiting");
}
} else {
throw new Error("Failed to check hunt status");
}
} catch (err) {
console.error("Error checking hunt status:", err);
setError("Failed to check hunt status. Please try again.");
setLoading(false);
}
};
if (status === "loading" || loading) {
return (
<div className="loading-container">
<div className="loading-spinner"></div>
<div className="loading-text">Loading team data...</div>
</div>
);
}
if (error) {
return (
<div className="error-container">
<div className="error-icon">⚠️</div>
<div className="error-message">Error: {error}</div>
<Link href="/team-selection">
<button className="back-button">Back to Team Selection</button>
</Link>
</div>
);
}
if (!team) {
return (
<div className="error-container">
<div className="error-icon">🔍</div>
<div className="error-message">Team not found</div>
<Link href="/team-selection">
<button className="back-button">Back to Team Selection</button>
</Link>
</div>
);
}
return (
<div className="team-page">
<div className="team-banner">
<div className="team-info">
<h1 className="team-name">Team Name: {team.name}</h1>
<div className="team-code">{team.code}</div>
</div>
</div>
<div className="members-container">
<h2 className="members-title">Team Members</h2>
<div className="members-background">
{teamMembers.length > 0 ? (
<div className="members-list">
{teamMembers.map((member) => (
<div key={member.id} className="member-card">
<div className="member-avatar">👤</div>
<div className="member-details">
<div className="member-name">
{member.username}
{member.id === session?.user?.id && " (You)"}
</div>
</div>
</div>
))}
</div>
) : (
<div className="empty-state">No team members found</div>
)}
</div>
</div>
<div className="team-actions">
<button
className="action-button leave-button"
onClick={async () => {
if (confirm("Are you sure you want to leave this team?")) {
try {
setLoading(true);
const response = await fetch(`/api/teams/${teamId}/leave`, {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
});
if (response.ok) {
// Immediately remove team data from session state
localStorage.removeItem("teamData");
sessionStorage.removeItem("teamData");
// Set a flag so we know the user has left this team
localStorage.setItem("userLeftTeam", "true");
// Force redirect to team selection
window.location.href = "/team-selection";
} else {
const errorData = await response.json();
throw new Error(errorData.error || "Failed to leave team");
}
} catch (err) {
setError(err.message);
setLoading(false);
}
}
}}
>
Leave Team
</button>
{session?.user?.role !== "admin" && (
<button
className="action-button start-button"
onClick={async () => {
try {
setLoading(true);
// First, immediately check hunt status
const timestamp = Date.now();
const huntStatusResponse = await fetch(
`/api/hunt-status?t=${timestamp}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
"Cache-Control": "no-store, no-cache, must-revalidate",
Pragma: "no-cache",
},
}
);
const huntData = await huntStatusResponse.json();
// If hunt is active, immediately redirect to map
if (huntData.active) {
console.log("Hunt already active! Redirecting to map...");
document.body.innerHTML =
'<div style="display:flex;justify-content:center;align-items:center;height:100vh;font-family:sans-serif;"><div style="text-align:center"><div style="font-size:24px">Loading Map...</div><div style="margin-top:20px">Please wait...</div></div></div>';
window.location.href = "/map";
return;
}
// If hunt isn't active, trigger it for the entire team
const response = await fetch(
`/api/teams/${teamId}/start-team-hunt`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
"Cache-Control": "no-store, no-cache, must-revalidate",
},
credentials: "include",
}
);
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || "Failed to start hunt");
}
const data = await response.json();
console.log("Hunt started response:", data);
// Store hunt start time in localStorage for persistence
if (data.startTime) {
localStorage.setItem("teamHuntStartTime", data.startTime);
}
// Force redirect to map page to ensure immediate navigation
document.body.innerHTML =
'<div style="display:flex;justify-content:center;align-items:center;height:100vh;font-family:sans-serif;"><div style="text-align:center"><div style="font-size:24px">Loading Map...</div><div style="margin-top:20px">Please wait...</div></div></div>';
window.location.href = "/map";
} catch (err) {
console.error("Error starting hunt:", err);
setError(err.message);
setLoading(false);
}
}}
>
Start Hunt
</button>
)}
{session?.user?.role === "admin" && (
<button
className="action-button start-button"
onClick={async () => {
try {
setLoading(true);
const response = await fetch(
`/api/teams/${teamId}/start-hunt`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
}
);
if (response.ok) {
fetchTeamData();
} else {
const error = await response.json();
throw new Error(error.message || "Failed to start hunt");
}
} catch (err) {
setError(err.message);
setLoading(false);
}
}}
>
Start Hunt
</button>
)}
</div>
{team && team.hasStarted && (
<div className="button-container">
<button
className="continue-button"
onClick={continueTreasureHunt}
disabled={loading}
>
{loading ? "Loading..." : "Continue Hunt"}
</button>
</div>
)}
{/* Replace hardcoded navbar with NavBar component */}
<NavBar activeLink="team" teamData={team} />
<style jsx>{`
.team-page {
display: flex;
flex-direction: column;
min-height: 100vh;
padding-bottom: 70px; /* Add space for the navbar */
}
.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;
}
.members-container {
padding: 20px;
flex: 1;
display: flex;
flex-direction: column;
}
.members-title {
font-size: 18px;
margin-bottom: 15px;
color: #333;
}
/* New background div that holds the scroll background image */
.members-background {
position: relative;
background-image: url("/images/scroll.svg");
background-repeat: no-repeat;
background-size: cover;
background-position: center;
padding: 30px 20px;
display: flex;
flex-direction: column;
align-items: center;
min-height: 300px;
}
.members-list {
display: flex;
flex-direction: column;
gap: 10px;
width: 100%;
align-items: center;
}
.member-card {
background-color: white;
border-radius: 8px;
padding: 15px;
display: flex;
align-items: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
width: 80vw;
max-width: 500px;
}
.member-avatar {
font-size: 24px;
margin-right: 15px;
}
.member-name {
font-weight: 600;
}
.empty-state {
text-align: center;
padding: 30px;
color: #888;
width: 80vw;
max-width: 500px;
background-color: rgba(255, 255, 255, 0.7);
border-radius: 8px;
}
.team-actions {
display: flex;
gap: 10px;
padding: 20px;
}
.action-button {
flex: 1;
padding: 15px;
border: none;
border-radius: 8px;
font-weight: bold;
cursor: pointer;
}
.leave-button {
background-color: #f6bd60;
color: #333;
}
.start-button {
background-color: #588157;
color: white;
}
.nav-bar {
display: flex;
justify-content: space-around;
background-color: white;
border-top: 1px solid #ddd;
padding: 10px 0;
position: fixed;
bottom: 0;
left: 0;
width: 100%;
z-index: 100;
}
.nav-item {
display: flex;
flex-direction: column;
align-items: center;
text-decoration: none;
color: #666;
flex: 1;
padding: 10px 0;
}
.nav-item.active {
color: #68543c;
}
.nav-icon {
font-size: 24px;
margin-bottom: 5px;
}
.nav-label {
font-size: 12px;
}
.loading-container,
.error-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
padding: 20px;
text-align: center;
}
.loading-spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #68543c;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin-bottom: 20px;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.error-icon {
font-size: 48px;
margin-bottom: 20px;
color: #d32f2f;
}
.error-message {
margin-bottom: 20px;
color: #d32f2f;
}
`}</style>
</div>
);
}