bug fixes and added stats viewer to admin leaderboard.

This commit is contained in:
bobbert committed 2025-03-20 19:19:28 +00:00
1 parent 7075a816c3
commit d60f52fef3
7 files changed
+851 -38

No files matched your search

+232 -18
View File
@@ -29,6 +29,11 @@ export default function AdminCluesPage() {
qr_code: "", // Add QR code field qr_code: "", // Add QR code field
}); });
const [isReorderMode, setIsReorderMode] = useState(false);
const [reorderingClue, setReorderingClue] = useState(null);
const [newPosition, setNewPosition] = useState(1);
const [reorderMessage, setReorderMessage] = useState("");
// Check if user is admin // Check if user is admin
useEffect(() => { useEffect(() => {
if (status === "authenticated") { if (status === "authenticated") {
@@ -91,9 +96,12 @@ export default function AdminCluesPage() {
try { try {
setLoading(true); setLoading(true);
// Add timestamp parameter to bust cache
// Make sure to get all clues with a high limit to avoid pagination issues
const timestamp = Date.now(); const timestamp = Date.now();
const response = await fetch(`/api/admin/clues?t=${timestamp}`, { const response = await fetch(
`/api/admin/clues?t=${timestamp}&limit=100`,
{
method: "GET", method: "GET",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -101,7 +109,8 @@ export default function AdminCluesPage() {
Pragma: "no-cache", Pragma: "no-cache",
}, },
credentials: "include", credentials: "include",
}); }
);
if (!response.ok) { if (!response.ok) {
const errorData = await response.json().catch(() => ({})); const errorData = await response.json().catch(() => ({}));
@@ -117,8 +126,11 @@ export default function AdminCluesPage() {
throw new Error("Invalid response format from server"); throw new Error("Invalid response format from server");
} }
console.log("Fetched clues:", data); // Debug log to see what's coming from API // Sort the clues by ID to ensure correct ordering
setClues(data); const sortedClues = [...data].sort((a, b) => a.id - b.id);
console.log("Fetched clues:", sortedClues);
setClues(sortedClues);
setError(""); setError("");
// Update lastRefresh timestamp // Update lastRefresh timestamp
setLastRefresh(Date.now()); setLastRefresh(Date.now());
@@ -311,6 +323,138 @@ export default function AdminCluesPage() {
} }
}; };
// Start reordering a clue
const handleReorderStart = (clue) => {
// Disable auto-refresh when starting to reorder
setAutoRefreshEnabled(false);
setReorderingClue(clue);
setNewPosition(clue.id); // Default to current position
setIsReorderMode(true);
};
// Handle position selection change
const handlePositionChange = (e) => {
setNewPosition(parseInt(e.target.value));
};
// Cancel reordering
const handleCancelReorder = () => {
setIsReorderMode(false);
setReorderingClue(null);
// Re-enable auto-refresh
setAutoRefreshEnabled(true);
};
// Submit reorder request
const handleReorderSubmit = async (e) => {
e.preventDefault();
if (!reorderingClue || newPosition < 1 || newPosition > clues.length) {
setError("Invalid position selected");
return;
}
try {
setLoading(true);
setReorderMessage("");
// Store reordering info for success message
const clueTitle = reorderingClue.title;
const targetPosition = newPosition;
const response = await fetch("/api/admin/clues/reorder", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
clueId: reorderingClue.id,
newPosition: newPosition,
}),
credentials: "include",
});
const data = await response.json();
if (!response.ok) {
throw new Error(
data.error || `Failed to reorder clue (Status: ${response.status})`
);
}
// Success message
setReorderMessage(
`Clue "${clueTitle}" moved to position ${targetPosition} successfully`
);
// Reset all state to ensure fresh data
setClues([]);
setIsReorderMode(false);
setReorderingClue(null);
setError("");
// Force a complete refresh with delay to allow database to complete operations
setTimeout(async () => {
try {
await fetchClues();
console.log("Clues refreshed after reordering");
} catch (refreshError) {
console.error("Error refreshing clues after reorder:", refreshError);
setError(
"Reordering succeeded but failed to refresh the list. Please reload the page."
);
}
}, 800);
} catch (err) {
console.error("Error reordering clue:", err);
setError(err.message);
setLoading(false);
}
};
// Add a function to force a hard refresh that bypasses cache
const forceHardRefresh = async () => {
try {
setLoading(true);
setClues([]);
// Clear browser cache for this specific request
const timestamp = Date.now();
const response = await fetch(
`/api/admin/clues?t=${timestamp}&forceRefresh=true`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
"Cache-Control": "no-cache, no-store, must-revalidate",
Pragma: "no-cache",
Expires: "0",
},
credentials: "include",
}
);
if (!response.ok) {
throw new Error(`Failed to refresh clues (Status: ${response.status})`);
}
const data = await response.json();
// Sort the clues by ID to ensure correct ordering
const sortedClues = [...data].sort((a, b) => a.id - b.id);
console.log("Force refreshed clues:", sortedClues);
setClues(sortedClues);
setLastRefresh(Date.now());
setNextRefresh(10);
} catch (err) {
console.error("Error during hard refresh:", err);
setError(`Refresh failed: ${err.message}. Try reloading the page.`);
} finally {
setLoading(false);
}
};
if (status === "loading" || loading) { if (status === "loading" || loading) {
return <div className="admin-page loading">Loading...</div>; return <div className="admin-page loading">Loading...</div>;
} }
@@ -350,6 +494,50 @@ export default function AdminCluesPage() {
{error && <p className="error-message">{error}</p>} {error && <p className="error-message">{error}</p>}
{fixMessage && <p className="success-message">{fixMessage}</p>} {fixMessage && <p className="success-message">{fixMessage}</p>}
{/* Reordering Modal */}
{isReorderMode ? (
<div className="admin-card">
<h2>Change Clue Position</h2>
<form onSubmit={handleReorderSubmit}>
<div className="form-group">
<label htmlFor="clue-position">
Move clue "{reorderingClue?.title}" to new position:
</label>
<select
id="clue-position"
value={newPosition}
onChange={handlePositionChange}
className="admin-input"
>
{clues.map((_, index) => (
<option key={index + 1} value={index + 1}>
{index + 1}
</option>
))}
</select>
<div className="help-text">
This will change the sequence of this clue and update all clue
positions accordingly. QR codes will also be reorganized to
maintain the same sequence.
</div>
</div>
<div className="form-actions">
<button type="submit" className="admin-button">
Change Position
</button>
<button
type="button"
className="admin-button cancel-button"
onClick={handleCancelReorder}
>
Cancel
</button>
</div>
</form>
</div>
) : (
<>
{/* Add New Clue Form */} {/* Add New Clue Form */}
{!isEditMode ? ( {!isEditMode ? (
<div className="admin-card"> <div className="admin-card">
@@ -405,8 +593,8 @@ export default function AdminCluesPage() {
placeholder="QR1, QR2, etc." placeholder="QR1, QR2, etc."
/> />
<div className="help-text"> <div className="help-text">
QR codes should follow the format QR[number] (e.g., QR1, QR2). QR codes should follow the format QR[number] (e.g., QR1,
Leave blank to auto-generate. QR2). Leave blank to auto-generate.
</div> </div>
</div> </div>
@@ -467,9 +655,9 @@ export default function AdminCluesPage() {
className="admin-input" className="admin-input"
/> />
<div className="help-text"> <div className="help-text">
QR codes should follow the format QR[number] (e.g., QR1, QR2). QR codes should follow the format QR[number] (e.g., QR1,
The QR code value determines what users scan to find this QR2). The QR code value determines what users scan to find
clue. this clue.
</div> </div>
</div> </div>
@@ -492,10 +680,13 @@ export default function AdminCluesPage() {
{/* Existing Clues List */} {/* Existing Clues List */}
<div className="admin-card"> <div className="admin-card">
<h2>Existing Clues</h2> <h2>Existing Clues</h2>
{reorderMessage && (
<p className="success-message">{reorderMessage}</p>
)}
<p className="info-text"> <p className="info-text">
QR codes are the values that users scan to find each clue. You can QR codes are the values that users scan to find each clue. You
customize these values, but they should usually follow the format can customize these values, but they should usually follow the
QR[number]. format QR[number].
</p> </p>
<div className="last-updated"> <div className="last-updated">
Last updated: {new Date(lastRefresh).toLocaleTimeString()} Last updated: {new Date(lastRefresh).toLocaleTimeString()}
@@ -512,6 +703,12 @@ export default function AdminCluesPage() {
<div className="clue-header"> <div className="clue-header">
<h3>{clue.title}</h3> <h3>{clue.title}</h3>
<div className="clue-actions"> <div className="clue-actions">
<button
className="reorder-button"
onClick={() => handleReorderStart(clue)}
>
Reorder
</button>
<button <button
className="edit-button" className="edit-button"
onClick={() => handleEditStart(clue)} onClick={() => handleEditStart(clue)}
@@ -520,7 +717,9 @@ export default function AdminCluesPage() {
</button> </button>
<button <button
className="delete-button" className="delete-button"
onClick={() => handleDeleteClue(clue.id, clue.qr_code)} onClick={() =>
handleDeleteClue(clue.id, clue.qr_code)
}
> >
Delete Delete
</button> </button>
@@ -532,17 +731,17 @@ export default function AdminCluesPage() {
<strong>Location:</strong> {clue.location} <strong>Location:</strong> {clue.location}
</p> </p>
)} )}
<p className="clue-position">
<strong>Position:</strong> Clue #{clue.id} in sequence
</p>
<p className="clue-qr-code"> <p className="clue-qr-code">
<strong>QR Code:</strong> {clue.qr_code || `QR${clue.id}`} <strong>QR Code:</strong>{" "}
{clue.qr_code || `QR${clue.id}`}
</p> </p>
</div> </div>
))} ))}
</div> </div>
)} )}
</div> </div>
</>
)}
</div> </div>
{/* Admin Navigation Bar */} {/* Admin Navigation Bar */}
@@ -898,6 +1097,21 @@ export default function AdminCluesPage() {
text-align: right; text-align: right;
font-style: italic; font-style: italic;
} }
.reorder-button {
background-color: #9c27b0;
color: white;
border: none;
border-radius: 4px;
padding: 0.4rem 0.6rem;
cursor: pointer;
font-size: 0.9rem;
margin-right: 8px;
}
.reorder-button:hover {
background-color: #7b1fa2;
}
`}</style> `}</style>
</div> </div>
); );
+247 -1
View File
@@ -9,6 +9,10 @@ export default function AdminLeaderboardPage() {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(""); const [error, setError] = useState("");
const router = useRouter(); const router = useRouter();
// Add state for expanded team details
const [expandedTeam, setExpandedTeam] = useState(null);
const [teamDetails, setTeamDetails] = useState({});
const [loadingDetails, setLoadingDetails] = useState(false);
// Check if user is admin // Check if user is admin
useEffect(() => { useEffect(() => {
@@ -100,6 +104,45 @@ export default function AdminLeaderboardPage() {
} }
}, [session]); }, [session]);
// New function to toggle team details dropdown and fetch data
const toggleTeamDetails = async (teamId) => {
if (expandedTeam === teamId) {
// Close dropdown if already open
setExpandedTeam(null);
} else {
// Open dropdown for this team
setExpandedTeam(teamId);
// Only fetch if we don't already have data for this team
if (!teamDetails[teamId]) {
setLoadingDetails(true);
try {
const response = await fetch(`/api/admin/teams/${teamId}/stats`, {
headers: {
"Content-Type": "application/json",
"Cache-Control": "no-cache, no-store, must-revalidate",
},
credentials: "include",
});
if (!response.ok) {
throw new Error("Failed to fetch team details");
}
const data = await response.json();
setTeamDetails((prev) => ({
...prev,
[teamId]: data,
}));
} catch (err) {
console.error("Error fetching team details:", err);
} finally {
setLoadingDetails(false);
}
}
}
};
if (status === "loading" || loading) { if (status === "loading" || loading) {
return <div className="admin-page loading">Loading...</div>; return <div className="admin-page loading">Loading...</div>;
} }
@@ -146,6 +189,7 @@ export default function AdminLeaderboardPage() {
</thead> </thead>
<tbody> <tbody>
{teams.map((team, index) => ( {teams.map((team, index) => (
<>
<tr key={team.id}> <tr key={team.id}>
<td>{index + 1}</td> <td>{index + 1}</td>
<td> <td>
@@ -170,16 +214,127 @@ export default function AdminLeaderboardPage() {
: "Never"} : "Never"}
</td> </td>
<td> <td>
<div className="action-buttons">
<button <button
className="reset-button" className="reset-button"
onClick={() => onClick={() =>
handleResetTeamProgress(team.id, team.name) handleResetTeamProgress(team.id, team.name)
} }
> >
Reset Progress Reset
</button> </button>
<button
className="details-button"
onClick={() => toggleTeamDetails(team.id)}
>
{expandedTeam === team.id
? "Hide Details"
: "View Details"}
</button>
</div>
</td> </td>
</tr> </tr>
{/* Team details dropdown */}
{expandedTeam === team.id && (
<tr
key={`details-${team.id}`}
className="team-details-row"
>
<td colSpan={6}>
<div className="team-details-container">
{loadingDetails && !teamDetails[team.id] ? (
<div className="details-loading">
Loading team details...
</div>
) : teamDetails[team.id] ? (
<>
<h3>Team Stats for {team.name}</h3>
{/* Found clues section */}
<div className="details-section">
<h4>
Found Clues (
{teamDetails[team.id].foundClues.length})
</h4>
{teamDetails[team.id].foundClues.length >
0 ? (
<table className="details-table">
<thead>
<tr>
<th>Title</th>
<th>Found At</th>
</tr>
</thead>
<tbody>
{teamDetails[team.id].foundClues.map(
(clue) => (
<tr key={`found-${clue.id}`}>
<td>{clue.title}</td>
<td>
{new Date(
clue.found_at
).toLocaleString()}
</td>
</tr>
)
)}
</tbody>
</table>
) : (
<p className="no-data-message">
No clues found yet
</p>
)}
</div>
{/* Skipped clues section */}
<div className="details-section">
<h4>
Skipped Clues (
{teamDetails[team.id].skippedClues.length}
)
</h4>
{teamDetails[team.id].skippedClues.length >
0 ? (
<table className="details-table">
<thead>
<tr>
<th>Title</th>
<th>Skipped At</th>
</tr>
</thead>
<tbody>
{teamDetails[
team.id
].skippedClues.map((clue) => (
<tr key={`skipped-${clue.id}`}>
<td>{clue.title}</td>
<td>
{new Date(
clue.found_at
).toLocaleString()}
</td>
</tr>
))}
</tbody>
</table>
) : (
<p className="no-data-message">
No clues skipped
</p>
)}
</div>
</>
) : (
<div className="details-error">
Error loading team details
</div>
)}
</div>
</td>
</tr>
)}
</>
))} ))}
</tbody> </tbody>
</table> </table>
@@ -401,6 +556,97 @@ export default function AdminLeaderboardPage() {
font-size: 0.8rem; font-size: 0.8rem;
text-shadow: 0 0 2px rgba(255, 255, 255, 0.7); text-shadow: 0 0 2px rgba(255, 255, 255, 0.7);
} }
.action-buttons {
display: flex;
gap: 0.5rem;
}
.details-button {
background-color: #3f51b5;
color: white;
border: none;
border-radius: 4px;
padding: 0.4rem 0.6rem;
cursor: pointer;
font-size: 0.9rem;
}
.details-button:hover {
background-color: #303f9f;
}
.team-details-row {
background-color: #f9f9f9;
}
.team-details-container {
padding: 1.5rem;
margin: 0.5rem 0;
border: 1px solid #e0e0e0;
border-radius: 4px;
background-color: white;
}
.team-details-container h3 {
color: #3f51b5;
margin-top: 0;
margin-bottom: 1rem;
border-bottom: 1px solid #e0e0e0;
padding-bottom: 0.5rem;
}
.details-section {
margin-bottom: 1.5rem;
}
.details-section h4 {
color: #68543c;
margin-bottom: 0.75rem;
}
.details-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 1rem;
font-size: 0.9rem;
}
.details-table th {
background-color: #f5f5f5;
font-weight: 600;
text-align: left;
padding: 0.6rem;
border-bottom: 1px solid #ddd;
}
.details-table td {
padding: 0.6rem;
border-bottom: 1px solid #eee;
}
.details-table tr:nth-child(even) {
background-color: #fafafa;
}
.details-loading {
padding: 1rem;
text-align: center;
color: #666;
}
.details-error {
padding: 1rem;
text-align: center;
color: #d32f2f;
background-color: #ffebee;
border-radius: 4px;
}
.no-data-message {
font-style: italic;
color: #666;
}
`}</style> `}</style>
</div> </div>
); );
+34 -17
View File
@@ -354,32 +354,49 @@ export default function CluePage({ params }) {
} }
try { try {
// Handle multiple QR code formats: "QR123", "123", or even "Clue 123" if (!data || !clue) return;
let scannedClueId;
// Try regex patterns // Normalize the scanned QR code value
const normalizedInput = data.trim().toUpperCase();
// Try to use the QR code field, but fall back to a generated one if it's missing or empty
let expectedQrCode = (clue.qr_code || "").trim().toUpperCase();
// If QR code is missing or empty, generate a fallback based on the clue ID
if (!expectedQrCode) {
expectedQrCode = `QR${clue.id}`.toUpperCase();
}
// First try a direct match with the QR code
if (normalizedInput === expectedQrCode) {
// Exact match with the clue's QR code
setScanMessage("✓ Correct QR code found!");
markClueAsFound(clueId);
return;
}
// If not a direct match, try to extract the numeric value
const qrPattern = /^QR[^0-9]*(\d+)$/i; const qrPattern = /^QR[^0-9]*(\d+)$/i;
const cluePattern = /^clue[^0-9]*(\d+)$/i; const cluePattern = /^clue[^0-9]*(\d+)$/i;
const numberPattern = /^(\d+)$/; const numberPattern = /^(\d+)$/;
let match = // Extract the number from the scanned input
data.match(qrPattern) || const inputMatch =
data.match(cluePattern) || normalizedInput.match(qrPattern) ||
data.match(numberPattern); normalizedInput.match(cluePattern) ||
normalizedInput.match(numberPattern);
if (match) { // Extract the number from the expected QR code
scannedClueId = parseInt(match[1], 10); const expectedMatch = expectedQrCode.match(qrPattern);
} else {
// Try direct parsing as a last resort
scannedClueId = parseInt(data, 10);
}
if (!isNaN(scannedClueId) && scannedClueId === parseInt(clueId)) { if (inputMatch && expectedMatch && inputMatch[1] === expectedMatch[1]) {
// Correct QR code scanned, mark as found // The numeric parts match
setScanMessage("✓ Correct QR code found!"); setScanMessage("✓ Correct QR code found!");
markClueAsFound(clueId); markClueAsFound(clueId);
} else if (!isNaN(scannedClueId)) { } else if (inputMatch) {
// Found a number in the input, but it doesn't match
setScanMessage( setScanMessage(
`QR code for clue ${scannedClueId} scanned, but you need clue ${clueId}. Please scan the correct code.` `QR code "${data}" scanned, but you need to scan "${expectedQrCode}". Please scan the correct code.`
); );
} else { } else {
setScanMessage( setScanMessage(
+238
View File
@@ -0,0 +1,238 @@
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 };
}
// POST - Reorder a clue to a new position
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
const { clueId, newPosition } = await request.json();
// Validate required parameters
if (!clueId || !newPosition) {
return Response.json(
{ error: "Clue ID and new position are required" },
{ status: 400 }
);
}
// Validate parameters are numbers
if (isNaN(parseInt(clueId)) || isNaN(parseInt(newPosition))) {
return Response.json(
{ error: "Clue ID and new position must be numbers" },
{ status: 400 }
);
}
// Get all clues to validate new position is in range
const allClues = await query(
"SELECT id, qr_code FROM clues ORDER BY id ASC"
);
console.log("All clues before reordering:", allClues);
if (parseInt(newPosition) < 1 || parseInt(newPosition) > allClues.length) {
return Response.json(
{ error: `New position must be between 1 and ${allClues.length}` },
{ status: 400 }
);
}
// Check if clue exists
const clue = await queryOne("SELECT id, title FROM clues WHERE id = ?", [
clueId,
]);
if (!clue) {
return Response.json({ error: "Clue not found" }, { status: 404 });
}
// Start a transaction for the reordering operation
await run("BEGIN TRANSACTION");
try {
// Get current position (1-based index)
const currentPosition =
allClues.findIndex((c) => c.id === parseInt(clueId)) + 1;
console.log(
`Reordering clue ${clueId} from position ${currentPosition} to ${newPosition}`
);
if (currentPosition === parseInt(newPosition)) {
// No change needed, clue is already in the desired position
await run("ROLLBACK");
return Response.json({
success: true,
message: "Clue is already in the requested position",
});
}
// Create a temporary table with all clue data
await run(`
CREATE TEMPORARY TABLE temp_clues AS
SELECT id, title, description, location, qr_code, created_at
FROM clues
ORDER BY id ASC
`);
// Create a new array with the reordered clues
const reorderedClues = [...allClues];
const [clueToMove] = reorderedClues.splice(currentPosition - 1, 1);
reorderedClues.splice(parseInt(newPosition) - 1, 0, clueToMove);
console.log(
"Reordered clues array:",
reorderedClues.map((c) => c.id)
);
// Create ID mapping before deleting anything
const idMapping = {};
for (let i = 0; i < reorderedClues.length; i++) {
idMapping[reorderedClues[i].id] = i + 1; // New sequential IDs
}
console.log("ID Mapping:", idMapping);
// Get all team_clues entries before deleting
const teamClues = await query(
"SELECT team_id, clue_id, found_at, skipped FROM team_clues"
);
console.log(`Found ${teamClues.length} team_clues records to update`);
// Reset SQLite sequence before deletion to avoid autoincrement issues
await run("DELETE FROM sqlite_sequence WHERE name='clues'");
// Clear the clues table
await run("DELETE FROM clues");
console.log("Deleted all clues for reordering");
// Insert the clues back in their new order with new IDs
for (let i = 0; i < reorderedClues.length; i++) {
const originalClue = reorderedClues[i];
// Get the full clue data from the temp table
const clueData = await queryOne(
`
SELECT title, description, location, created_at FROM temp_clues WHERE id = ?
`,
[originalClue.id]
);
if (!clueData) {
throw new Error(
`Failed to retrieve data for clue ID ${originalClue.id} from temp table`
);
}
// Determine the new QR code based on new position
const newQrCode = `QR${i + 1}`;
const newId = i + 1;
// Insert with the new ID (position)
const insertResult = await run(
`
INSERT INTO clues (id, title, description, location, qr_code, created_at)
VALUES (?, ?, ?, ?, ?, ?)
`,
[
newId,
clueData.title,
clueData.description,
clueData.location || "",
newQrCode,
clueData.created_at,
]
);
console.log(
`Inserted clue with new ID ${newId}, was previously ${originalClue.id}, result:`,
insertResult
);
}
// Delete all team_clues entries
await run("DELETE FROM team_clues");
console.log("Deleted all team_clues records for updating");
// Reinsert with updated clue_id values
let updatedCount = 0;
for (const tc of teamClues) {
if (idMapping[tc.clue_id]) {
await run(
`
INSERT INTO team_clues (team_id, clue_id, found_at, skipped)
VALUES (?, ?, ?, ?)
`,
[tc.team_id, idMapping[tc.clue_id], tc.found_at, tc.skipped]
);
updatedCount++;
}
}
console.log(
`Reinserted ${updatedCount} team_clues records with updated IDs`
);
// Drop the temporary table
await run("DROP TABLE temp_clues");
// Update SQLite sequence to match our new highest ID
await run(`INSERT INTO sqlite_sequence (name, seq) VALUES ('clues', ${reorderedClues.length})
ON CONFLICT(name) DO UPDATE SET seq = ${reorderedClues.length}`);
// Force SQLite to update all internal references
await run("VACUUM");
// Commit transaction
await run("COMMIT");
console.log("Transaction committed successfully");
// Verify the changes
const verifyClues = await query(
"SELECT id, title FROM clues ORDER BY id ASC"
);
console.log("Clues after reordering:", verifyClues);
return Response.json({
success: true,
message: `Clue successfully moved from position ${currentPosition} to position ${newPosition}`,
});
} catch (error) {
// Rollback transaction on error
await run("ROLLBACK");
console.error("Error during reordering transaction:", error);
throw error;
}
} catch (error) {
console.error("Error reordering clue:", error);
return Response.json(
{ error: "Failed to reorder clue: " + error.message },
{ status: 500 }
);
}
}
@@ -0,0 +1,98 @@
import { getToken } from "next-auth/jwt";
import { queryOne, query, 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 };
}
// GET - Fetch detailed stats for a specific team
export async function GET(request, context) {
try {
// Verify admin access
const { authorized, error, status } = await verifyAdmin(request);
if (!authorized) {
return Response.json({ error }, { status });
}
// Get teamId parameter
const { teamId } = await context.params;
// Validate teamId is a number
if (isNaN(parseInt(teamId))) {
return Response.json({ error: "Invalid team ID" }, { status: 400 });
}
// Check if team exists
const team = await queryOne("SELECT id, name FROM teams WHERE id = ?", [
teamId,
]);
if (!team) {
return Response.json({ error: "Team not found" }, { status: 404 });
}
// Get all found clues (not skipped)
const foundClues = await query(
`
SELECT c.id, c.title, tc.found_at
FROM team_clues tc
JOIN clues c ON tc.clue_id = c.id
WHERE tc.team_id = ? AND (tc.skipped = 0 OR tc.skipped IS NULL)
ORDER BY tc.found_at ASC
`,
[teamId]
);
// Get all skipped clues
const skippedClues = await query(
`
SELECT c.id, c.title, tc.found_at
FROM team_clues tc
JOIN clues c ON tc.clue_id = c.id
WHERE tc.team_id = ? AND tc.skipped = 1
ORDER BY tc.found_at ASC
`,
[teamId]
);
// Return the data
return Response.json(
{
teamId: parseInt(teamId),
teamName: team.name,
foundClues,
skippedClues,
},
{
headers: {
"Cache-Control": "no-store, no-cache, must-revalidate",
Pragma: "no-cache",
Expires: "0",
},
}
);
} catch (error) {
console.error("Error fetching team stats:", error);
return Response.json(
{ error: "Failed to fetch team stats: " + error.message },
{ status: 500 }
);
}
}
+2 -2
View File
@@ -33,9 +33,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 - UPDATED to include qr_code field
const clue = await queryOne( const clue = await queryOne(
"SELECT id, title, description, location FROM clues WHERE id = ?", "SELECT id, title, description, location, qr_code FROM clues WHERE id = ?",
[clueId] [clueId]
); );
BIN
View File
Binary file not shown.