fixing map timing errors and still trying to make leaderboard and clues pages.

Added two menus to the admin dashboard with a navbar at the bottom:

Added navbar button for timer. Added navbar button to edit clues centrally. Added navbar button to view Admin version of leaderboard.

To add:
- the admin version of the leaderboard. has the same sort of initial view as the normal leaderboard however clicking on a team will create a drop down with their team stats.

Grey = cant click
Red = current
Green = complete
blue = skipped?

Need to add Clue page Need to fix Leaderboard.
This commit is contained in:
bobbert committed 2025-03-15 17:10:11 +00:00
1 parent 30d88407a0
commit c098ad40bf
9 files changed
+1202 -14

No files matched your search

+468
View File
@@ -0,0 +1,468 @@
"use client";
import { useState, useEffect } from "react";
import { useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
export default function AdminCluesPage() {
const { data: session, status } = useSession();
const [clues, setClues] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const router = useRouter();
// Form state for adding new clues - removed coordinates
const [newClue, setNewClue] = useState({
title: "",
description: "",
location: "",
});
// Check if user is admin
useEffect(() => {
if (status === "authenticated") {
if (session?.user?.role !== "admin") {
router.push("/team-selection");
} else {
fetchClues();
}
} else if (status === "unauthenticated") {
router.push("/login");
}
}, [session, status, router]);
// Fetch all clues
const fetchClues = async () => {
try {
setLoading(true);
const response = await fetch("/api/admin/clues", {
method: "GET",
headers: {
"Content-Type": "application/json",
"Cache-Control": "no-cache, no-store, must-revalidate",
},
credentials: "include",
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.error || `Failed to fetch clues (${response.status})`
);
}
const data = await response.json();
// Validate the response data
if (!Array.isArray(data)) {
throw new Error("Invalid response format from server");
}
setClues(data);
setError("");
} catch (err) {
console.error("Error fetching clues:", err);
setError(err.message);
} finally {
setLoading(false);
}
};
// Handle input change for new clue form
const handleInputChange = (e) => {
const { name, value } = e.target;
setNewClue((prev) => ({
...prev,
[name]: value,
}));
};
// Handle form submission to add new clue
const handleAddClue = async (e) => {
e.preventDefault();
try {
setLoading(true);
const response = await fetch("/api/admin/clues", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(newClue),
credentials: "include",
});
if (!response.ok) {
throw new Error("Failed to add clue");
}
// Refresh the clue list
fetchClues();
// Reset the form - removed coordinates
setNewClue({
title: "",
description: "",
location: "",
});
setError("");
} catch (err) {
console.error("Error adding clue:", err);
setError(err.message);
} finally {
setLoading(false);
}
};
// Handle deleting a clue
const handleDeleteClue = async (clueId) => {
if (!confirm("Are you sure you want to delete this clue?")) return;
try {
setLoading(true);
const response = await fetch(`/api/admin/clues/${clueId}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
credentials: "include",
});
if (!response.ok) {
throw new Error("Failed to delete clue");
}
// Refresh the clue list
fetchClues();
setError("");
} catch (err) {
console.error("Error deleting clue:", err);
setError(err.message);
} finally {
setLoading(false);
}
};
if (status === "loading" || loading) {
return <div className="admin-page loading">Loading...</div>;
}
if (status === "unauthenticated") {
return null; // Will redirect via useEffect
}
return (
<div className="admin-page">
<div className="page-header">
<div className="page-title">Manage Clues</div>
<div className="placeholder"></div>
</div>
<div className="admin-content">
{error && <p className="error-message">{error}</p>}
{/* Add New Clue Form - removed coordinate fields */}
<div className="admin-card">
<h2>Add New Clue</h2>
<form onSubmit={handleAddClue}>
<div className="form-group">
<label htmlFor="title">Title</label>
<input
type="text"
id="title"
name="title"
value={newClue.title}
onChange={handleInputChange}
required
className="admin-input"
/>
</div>
<div className="form-group">
<label htmlFor="description">Description</label>
<textarea
id="description"
name="description"
value={newClue.description}
onChange={handleInputChange}
required
className="admin-textarea"
></textarea>
</div>
<div className="form-group">
<label htmlFor="location">Location Hint</label>
<input
type="text"
id="location"
name="location"
value={newClue.location}
onChange={handleInputChange}
className="admin-input"
/>
</div>
<button type="submit" className="admin-button">
Add Clue
</button>
</form>
</div>
{/* Existing Clues List - updated to remove coordinates */}
<div className="admin-card">
<h2>Existing Clues</h2>
{clues.length === 0 ? (
<p className="no-clues">No clues found.</p>
) : (
<div className="clues-list">
{clues.map((clue) => (
<div key={clue.id} className="clue-item">
<div className="clue-header">
<h3>{clue.title}</h3>
<button
className="delete-button"
onClick={() => handleDeleteClue(clue.id)}
>
Delete
</button>
</div>
<p className="clue-description">{clue.description}</p>
{clue.location && (
<p className="clue-location">
<strong>Location:</strong> {clue.location}
</p>
)}
<p className="clue-position">
<strong>Position:</strong> Clue #{clue.id} in sequence
</p>
</div>
))}
</div>
)}
</div>
</div>
{/* Admin Navigation Bar */}
<div className="admin-nav-bar">
<button
className="admin-nav-button"
onClick={() => router.push("/admin")}
>
<div className="admin-nav-icon">⏱️</div>
<div className="admin-nav-label">Timer</div>
</button>
<button
className="admin-nav-button active"
onClick={() => router.push("/admin/clues")}
>
<div className="admin-nav-icon">📝</div>
<div className="admin-nav-label">Clues</div>
</button>
<button
className="admin-nav-button"
onClick={() => router.push("/admin/leaderboard")}
>
<div className="admin-nav-icon">🏆</div>
<div className="admin-nav-label">Leaderboard</div>
</button>
</div>
<style jsx>{`
.admin-page {
display: flex;
flex-direction: column;
min-height: 100vh;
background-color: #f4e9dc;
}
.page-header {
display: flex;
justify-content: center;
align-items: center;
padding: 1rem;
background-color: #68543c;
color: white;
}
.page-title {
font-size: 1.2rem;
font-weight: 600;
}
.admin-content {
flex: 1;
padding: 1rem;
display: flex;
flex-direction: column;
align-items: center;
padding-bottom: 70px; /* Space for navbar */
}
.admin-card {
width: 100%;
max-width: 700px;
background-color: white;
border-radius: 8px;
padding: 1.5rem;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
margin-bottom: 2rem;
}
.form-group {
margin-bottom: 1rem;
}
.form-row {
display: flex;
gap: 1rem;
}
.half {
flex: 1;
}
.admin-input {
width: 100%;
padding: 0.6rem;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 1rem;
}
.admin-textarea {
width: 100%;
padding: 0.6rem;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 1rem;
min-height: 100px;
resize: vertical;
}
label {
display: block;
margin-bottom: 0.4rem;
font-weight: 500;
}
.admin-button {
background-color: #68543c;
color: white;
border: none;
border-radius: 4px;
padding: 0.75rem 1rem;
font-size: 1rem;
cursor: pointer;
margin-top: 0.5rem;
}
.clues-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.clue-item {
border: 1px solid #ddd;
border-radius: 4px;
padding: 1rem;
background-color: #f9f9f9;
}
.clue-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.clue-header h3 {
margin: 0;
font-size: 1.1rem;
}
.delete-button {
background-color: #d32f2f;
color: white;
border: none;
border-radius: 4px;
padding: 0.4rem 0.6rem;
font-size: 0.9rem;
cursor: pointer;
}
.clue-description {
margin-bottom: 0.8rem;
}
.clue-location,
.clue-position {
font-size: 0.9rem;
margin-bottom: 0.4rem;
}
.error-message {
color: red;
margin-bottom: 1rem;
padding: 0.5rem;
border-radius: 4px;
background-color: #ffebee;
width: 100%;
max-width: 700px;
text-align: center;
}
.no-clues {
text-align: center;
color: #666;
padding: 2rem 0;
}
/* Admin Navigation Bar Styles */
.admin-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%;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
z-index: 100;
}
.admin-nav-button {
display: flex;
flex-direction: column;
align-items: center;
background: transparent;
border: none;
padding: 8px 0;
flex: 1;
cursor: pointer;
color: #666;
transition: all 0.2s ease;
}
.admin-nav-button.active {
color: #68543c;
font-weight: bold;
}
.admin-nav-button:hover {
background-color: rgba(104, 84, 60, 0.1);
}
.admin-nav-icon {
font-size: 24px;
margin-bottom: 4px;
}
.admin-nav-label {
font-size: 13px;
}
`}</style>
</div>
);
}
+360
View File
@@ -0,0 +1,360 @@
"use client";
import { useState, useEffect } from "react";
import { useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
export default function AdminLeaderboardPage() {
const { data: session, status } = useSession();
const [teams, setTeams] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const router = useRouter();
// Check if user is admin
useEffect(() => {
if (status === "authenticated") {
if (session?.user?.role !== "admin") {
router.push("/team-selection");
} else {
fetchLeaderboardData();
}
} else if (status === "unauthenticated") {
router.push("/login");
}
}, [session, status, router]);
// Fetch leaderboard data
const fetchLeaderboardData = async () => {
try {
setLoading(true);
setError("");
// This is a placeholder - you'll need to implement the actual API
// For now, we'll just set dummy data
const dummyTeams = [
{
id: 1,
name: "Team Alpha",
code: "ALPHA1",
memberCount: 4,
cluesFound: 5,
totalClues: 10,
lastActivity: new Date().toISOString(),
},
{
id: 2,
name: "Team Beta",
code: "BETA12",
memberCount: 3,
cluesFound: 8,
totalClues: 10,
lastActivity: new Date().toISOString(),
},
{
id: 3,
name: "Team Gamma",
code: "GAMMA3",
memberCount: 5,
cluesFound: 2,
totalClues: 10,
lastActivity: new Date().toISOString(),
},
];
setTeams(dummyTeams);
// Uncomment this when API is ready
/*
const response = await fetch("/api/admin/leaderboard", {
method: "GET",
headers: {
"Content-Type": "application/json",
"Cache-Control": "no-cache, no-store, must-revalidate"
},
credentials: "include"
});
if (!response.ok) {
throw new Error("Failed to fetch leaderboard data");
}
const data = await response.json();
setTeams(data);
*/
} catch (err) {
console.error("Error fetching leaderboard data:", err);
setError(err.message);
} finally {
setLoading(false);
}
};
if (status === "loading" || loading) {
return <div className="admin-page loading">Loading...</div>;
}
if (status === "unauthenticated") {
return null; // Will redirect via useEffect
}
return (
<div className="admin-page">
<div className="page-header">
<div className="page-title">Admin Leaderboard</div>
<div className="placeholder"></div>
</div>
<div className="admin-content">
{error && <p className="error-message">{error}</p>}
<div className="admin-card">
<h2>Team Progress</h2>
<button
className="refresh-button"
onClick={fetchLeaderboardData}
disabled={loading}
>
🔄 Refresh Data
</button>
{teams.length === 0 ? (
<p className="no-data">No team data available.</p>
) : (
<div className="leaderboard-table-container">
<table className="leaderboard-table">
<thead>
<tr>
<th>Rank</th>
<th>Team Name</th>
<th>Members</th>
<th>Progress</th>
<th>Last Activity</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{teams.map((team, index) => (
<tr key={team.id}>
<td>{index + 1}</td>
<td>
{team.name} ({team.code})
</td>
<td>{team.memberCount}</td>
<td>
{team.cluesFound} / {team.totalClues}
</td>
<td>
{team.lastActivity
? new Date(team.lastActivity).toLocaleString()
: "Never"}
</td>
<td>
<button
className="reset-button"
onClick={() =>
alert(`Reset team ${team.name} progress`)
}
>
Reset Progress
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
{/* Admin Navigation Bar */}
<div className="admin-nav-bar">
<button
className="admin-nav-button"
onClick={() => router.push("/admin")}
>
<div className="admin-nav-icon">⏱️</div>
<div className="admin-nav-label">Timer</div>
</button>
<button
className="admin-nav-button"
onClick={() => router.push("/admin/clues")}
>
<div className="admin-nav-icon">📝</div>
<div className="admin-nav-label">Clues</div>
</button>
<button
className="admin-nav-button active"
onClick={() => router.push("/admin/leaderboard")}
>
<div className="admin-nav-icon">🏆</div>
<div className="admin-nav-label">Leaderboard</div>
</button>
</div>
<style jsx>{`
.admin-page {
display: flex;
flex-direction: column;
min-height: 100vh;
background-color: #f4e9dc;
}
.page-header {
display: flex;
justify-content: center;
align-items: center;
padding: 1rem;
background-color: #68543c;
color: white;
}
.page-title {
font-size: 1.2rem;
font-weight: 600;
}
.admin-content {
flex: 1;
padding: 1rem;
display: flex;
flex-direction: column;
align-items: center;
padding-bottom: 70px; /* Space for navbar */
}
.admin-card {
width: 100%;
max-width: 900px;
background-color: white;
border-radius: 8px;
padding: 1.5rem;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
position: relative;
}
h2 {
margin-top: 0;
margin-bottom: 1.5rem;
}
.refresh-button {
position: absolute;
top: 1.5rem;
right: 1.5rem;
background-color: #4caf50;
color: white;
border: none;
border-radius: 4px;
padding: 0.5rem 0.75rem;
cursor: pointer;
}
.refresh-button:disabled {
background-color: #a5d6a7;
cursor: not-allowed;
}
.leaderboard-table-container {
width: 100%;
overflow-x: auto;
}
.leaderboard-table {
width: 100%;
border-collapse: collapse;
}
.leaderboard-table th,
.leaderboard-table td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid #eee;
}
.leaderboard-table th {
background-color: #f5f5f5;
font-weight: 600;
}
.leaderboard-table tr:hover {
background-color: #f9f9f9;
}
.reset-button {
background-color: #f44336;
color: white;
border: none;
border-radius: 4px;
padding: 0.4rem 0.6rem;
cursor: pointer;
font-size: 0.9rem;
}
.error-message {
color: red;
margin-bottom: 1rem;
padding: 0.5rem;
border-radius: 4px;
background-color: #ffebee;
width: 100%;
max-width: 900px;
text-align: center;
}
.no-data {
text-align: center;
color: #666;
padding: 2rem 0;
}
/* Admin Navigation Bar Styles */
.admin-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%;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
z-index: 100;
}
.admin-nav-button {
display: flex;
flex-direction: column;
align-items: center;
background: transparent;
border: none;
padding: 8px 0;
flex: 1;
cursor: pointer;
color: #666;
transition: all 0.2s ease;
}
.admin-nav-button.active {
color: #68543c;
font-weight: bold;
}
.admin-nav-button:hover {
background-color: rgba(104, 84, 60, 0.1);
}
.admin-nav-icon {
font-size: 24px;
margin-bottom: 4px;
}
.admin-nav-label {
font-size: 13px;
}
`}</style>
</div>
);
}
+72
View File
@@ -336,6 +336,31 @@ export default function AdminDashboard() {
</div> </div>
</div> </div>
{/* Admin Navigation Bar */}
<div className="admin-nav-bar">
<button
className="admin-nav-button active"
onClick={() => router.push("/admin")}
>
<div className="admin-nav-icon">⏱️</div>
<div className="admin-nav-label">Timer</div>
</button>
<button
className="admin-nav-button"
onClick={() => router.push("/admin/clues")}
>
<div className="admin-nav-icon">📝</div>
<div className="admin-nav-label">Clues</div>
</button>
<button
className="admin-nav-button"
onClick={() => router.push("/admin/leaderboard")}
>
<div className="admin-nav-icon">🏆</div>
<div className="admin-nav-label">Leaderboard</div>
</button>
</div>
<style jsx>{` <style jsx>{`
.admin-page { .admin-page {
display: flex; display: flex;
@@ -369,6 +394,7 @@ export default function AdminDashboard() {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
padding-bottom: 70px; /* Add space for the navbar */
} }
.timer-display-container { .timer-display-container {
@@ -441,6 +467,52 @@ export default function AdminDashboard() {
color: red; color: red;
margin-top: 1rem; margin-top: 1rem;
} }
/* Admin Navigation Bar Styles */
.admin-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%;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
z-index: 100;
}
.admin-nav-button {
display: flex;
flex-direction: column;
align-items: center;
background: transparent;
border: none;
padding: 8px 0;
flex: 1;
cursor: pointer;
color: #666;
transition: all 0.2s ease;
}
.admin-nav-button.active {
color: #68543c;
font-weight: bold;
}
.admin-nav-button:hover {
background-color: rgba(104, 84, 60, 0.1);
}
.admin-nav-icon {
font-size: 24px;
margin-bottom: 4px;
}
.admin-nav-label {
font-size: 13px;
}
`}</style> `}</style>
</div> </div>
); );
+13 -10
View File
@@ -200,17 +200,20 @@ export default function MapPage() {
// Convert NodeList to Array for easier manipulation // Convert NodeList to Array for easier manipulation
Array.from(circleSvgElements).forEach((circle, index) => { Array.from(circleSvgElements).forEach((circle, index) => {
// Only process if we have a clue for this index // Only process if we have a clue for this index and it's within bounds
if (clues[index]) { if (index < clues.length) {
// Map SVG circle at position index to the clue at index
// This uses sequential order rather than coordinates
const clue = clues[index];
// Check if this clue is available (either found or next in sequence) // Check if this clue is available (either found or next in sequence)
const isAvailable = const isAvailable = clue.found || index <= nextAvailableClueIndex;
clues[index].found || index <= nextAvailableClueIndex;
// Make circle interactive // Make circle interactive
circle.style.cursor = isAvailable ? "pointer" : "not-allowed"; circle.style.cursor = isAvailable ? "pointer" : "not-allowed";
// Set appropriate colors // Set appropriate colors
if (clues[index].found) { if (clue.found) {
// Found clues are green // Found clues are green
circle.style.fill = "#4caf50"; circle.style.fill = "#4caf50";
circle.style.stroke = "#2e7d32"; circle.style.stroke = "#2e7d32";
@@ -232,8 +235,8 @@ export default function MapPage() {
e.stopPropagation(); e.stopPropagation();
if (isAvailable) { if (isAvailable) {
console.log(`Navigating to clue ${clues[index].id}`); console.log(`Navigating to clue ${clue.id}`);
router.push(`/clue/${clues[index].id}`); router.push(`/clue/${clue.id}`);
} else { } else {
alert("Complete previous clues first to unlock this one!"); alert("Complete previous clues first to unlock this one!");
} }
@@ -247,7 +250,7 @@ export default function MapPage() {
}; };
circle.onmouseleave = () => { circle.onmouseleave = () => {
if (clues[index].found) { if (clue.found) {
circle.style.fill = "#4caf50"; // Back to green for found clues circle.style.fill = "#4caf50"; // Back to green for found clues
} else if (isAvailable) { } else if (isAvailable) {
circle.style.fill = "#ff9800"; // Back to orange for available clues circle.style.fill = "#ff9800"; // Back to orange for available clues
@@ -256,13 +259,13 @@ export default function MapPage() {
} }
}; };
// Add tooltip/title // Add tooltip/title with actual clue ID and position information
const label = document.createElementNS( const label = document.createElementNS(
"http://www.w3.org/2000/svg", "http://www.w3.org/2000/svg",
"title" "title"
); );
label.textContent = `Clue #${index + 1}: ${ label.textContent = `Clue #${index + 1}: ${
clues[index].title || "Mystery clue" clue.title || "Mystery clue"
}${!isAvailable ? " (Locked)" : ""}`; }${!isAvailable ? " (Locked)" : ""}`;
circle.appendChild(label); circle.appendChild(label);
} }
+167
View File
@@ -0,0 +1,167 @@
import { getToken } from "next-auth/jwt";
import { queryOne, run } from "@/lib/db";
export const dynamic = "force-dynamic"; // Disable caching
// Ensure the user is an admin
async function verifyAdmin(request) {
const token = await getToken({
req: request,
secret:
process.env.NEXTAUTH_SECRET ||
"your-fallback-secret-should-be-at-least-32-chars",
});
if (!token) {
return { authorized: false, error: "Unauthorized", status: 401 };
}
if (token.role !== "admin") {
return { authorized: false, error: "Admin access required", status: 403 };
}
return { authorized: true, token };
}
// DELETE - Remove a specific clue
export async function DELETE(request, context) {
try {
// Verify admin access
const { authorized, error, status } = await verifyAdmin(request);
if (!authorized) {
return Response.json({ error }, { status });
}
const { clueId } = context.params;
// Validate clueId is a number
if (isNaN(parseInt(clueId))) {
return Response.json({ error: "Invalid clue ID" }, { status: 400 });
}
// Check if clue exists
const clue = await queryOne("SELECT id FROM clues WHERE id = ?", [clueId]);
if (!clue) {
return Response.json({ error: "Clue not found" }, { status: 404 });
}
// Delete clue record
await run("DELETE FROM clues WHERE id = ?", [clueId]);
// Also delete any references in team_clues table
await run(`
CREATE TABLE IF NOT EXISTS team_clues (
team_id INTEGER NOT NULL,
clue_id INTEGER NOT NULL,
found_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (team_id, clue_id),
FOREIGN KEY (team_id) REFERENCES teams(id),
FOREIGN KEY (clue_id) REFERENCES clues(id)
)
`);
await run("DELETE FROM team_clues WHERE clue_id = ?", [clueId]);
return Response.json({
success: true,
message: "Clue deleted successfully",
});
} catch (error) {
console.error("Admin clue deletion error:", error);
return Response.json(
{ error: "Failed to delete clue: " + error.message },
{ status: 500 }
);
}
}
// GET - Get a specific clue
export async function GET(request, context) {
try {
// Verify admin access
const { authorized, error, status } = await verifyAdmin(request);
if (!authorized) {
return Response.json({ error }, { status });
}
const { clueId } = context.params;
// Validate clueId is a number
if (isNaN(parseInt(clueId))) {
return Response.json({ error: "Invalid clue ID" }, { status: 400 });
}
// Get the specific clue - removed coordinates
const clue = await queryOne(
"SELECT id, title, description, location FROM clues WHERE id = ?",
[clueId]
);
if (!clue) {
return Response.json({ error: "Clue not found" }, { status: 404 });
}
return Response.json(clue);
} catch (error) {
console.error("Admin clue fetch error:", error);
return Response.json(
{ error: "Failed to fetch clue: " + error.message },
{ status: 500 }
);
}
}
// PUT - Update a specific clue
export async function PUT(request, context) {
try {
// Verify admin access
const { authorized, error, status } = await verifyAdmin(request);
if (!authorized) {
return Response.json({ error }, { status });
}
const { clueId } = context.params;
// Validate clueId is a number
if (isNaN(parseInt(clueId))) {
return Response.json({ error: "Invalid clue ID" }, { status: 400 });
}
// Parse the request body - removed coordinates
const { title, description, location } = await request.json();
// Validate required fields
if (!title || !description) {
return Response.json(
{ error: "Title and description are required" },
{ status: 400 }
);
}
// Check if clue exists
const clue = await queryOne("SELECT id FROM clues WHERE id = ?", [clueId]);
if (!clue) {
return Response.json({ error: "Clue not found" }, { status: 404 });
}
// Update the clue - removed coordinates
await run(
`
UPDATE clues
SET title = ?, description = ?, location = ?
WHERE id = ?
`,
[title, description, location || "", clueId]
);
return Response.json({
success: true,
message: "Clue updated successfully",
});
} catch (error) {
console.error("Admin clue update error:", error);
return Response.json(
{ error: "Failed to update clue: " + error.message },
{ status: 500 }
);
}
}
+118
View File
@@ -0,0 +1,118 @@
import { getToken } from "next-auth/jwt";
import { query, queryOne, run } from "@/lib/db";
export const dynamic = "force-dynamic"; // Disable caching
// Ensure the user is an admin
async function verifyAdmin(request) {
const token = await getToken({
req: request,
secret:
process.env.NEXTAUTH_SECRET ||
"your-fallback-secret-should-be-at-least-32-chars",
});
if (!token) {
return { authorized: false, error: "Unauthorized", status: 401 };
}
if (token.role !== "admin") {
return { authorized: false, error: "Admin access required", status: 403 };
}
return { authorized: true, token };
}
// GET - Fetch all clues for admin
export async function GET(request) {
try {
// Verify admin access
const { authorized, error, status, token } = await verifyAdmin(request);
if (!authorized) {
return Response.json({ error }, { status });
}
// Ensure clues table exists with simplified structure (no coordinates)
await run(`
CREATE TABLE IF NOT EXISTS clues (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT NOT NULL,
location TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
// Fetch all clues with sequential ordering
const clues = await query(`
SELECT id, title, description, location, created_at
FROM clues
ORDER BY id ASC
`);
console.log(`Admin API: Returning ${clues.length} clues`);
return Response.json(clues, {
headers: {
"Cache-Control": "no-store, no-cache, must-revalidate",
Pragma: "no-cache",
Expires: "0",
},
});
} catch (error) {
console.error("Admin clue fetch error:", error);
return Response.json(
{ error: "Failed to fetch clues: " + error.message },
{ status: 500 }
);
}
}
// POST - Add a new clue
export async function POST(request) {
try {
// Verify admin access
const { authorized, error, status } = await verifyAdmin(request);
if (!authorized) {
return Response.json({ error }, { status });
}
// Parse the request body - simplified without coordinates
const { title, description, location = "" } = await request.json();
// Validate required fields
if (!title || !description) {
return Response.json(
{ error: "Title and description are required" },
{ status: 400 }
);
}
// Insert the new clue - simplified without coordinates
await run(
`
INSERT INTO clues (title, description, location)
VALUES (?, ?, ?)
`,
[title, description, location]
);
// Get the inserted clue ID
const newClue = await queryOne("SELECT last_insert_rowid() as id");
return Response.json(
{
success: true,
message: "Clue added successfully",
clueId: newClue.id,
},
{ status: 201 }
);
} catch (error) {
console.error("Admin clue creation error:", error);
return Response.json(
{ error: "Failed to add clue: " + error.message },
{ status: 500 }
);
}
}
+2 -2
View File
@@ -31,9 +31,9 @@ export async function GET(request, context) {
return Response.json({ error: "Invalid clue ID" }, { status: 400 }); return Response.json({ error: "Invalid clue ID" }, { status: 400 });
} }
// Get the specific clue // Get the specific clue - removed coordinates
const clue = await queryOne( const clue = await queryOne(
"SELECT id, title, description, location, x_coord, y_coord FROM clues WHERE id = ?", "SELECT id, title, description, location FROM clues WHERE id = ?",
[clueId] [clueId]
); );
+2 -2
View File
@@ -24,9 +24,9 @@ export async function GET(request) {
); );
} }
// Get all clues with explicit ordering to ensure consistency // Get all clues with explicit ordering - removed coordinates
const clues = await query(` const clues = await query(`
SELECT id, title, description, location, x_coord, y_coord SELECT id, title, description, location
FROM clues FROM clues
ORDER BY id ASC ORDER BY id ASC
`); `);
BIN
View File
Binary file not shown.