"use client"; import { useState, useEffect, useRef } 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(); const [isEditMode, setIsEditMode] = useState(false); const [editingClue, setEditingClue] = useState(null); const [fixMessage, setFixMessage] = useState(""); // Add a lastRefresh timestamp to track when data was last fetched const [lastRefresh, setLastRefresh] = useState(Date.now()); // Add next refresh countdown state const [nextRefresh, setNextRefresh] = useState(10); // Add auto-refresh enabled/disabled state const [autoRefreshEnabled, setAutoRefreshEnabled] = useState(true); // Ref to store interval ID for cleanup const refreshIntervalRef = useRef(null); // Form state for adding new clues - add qrCode field back const [newClue, setNewClue] = useState({ title: "", description: "", location: "", 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 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]); // Controlled auto-refresh effect useEffect(() => { // Only set up auto-refresh if enabled and not in edit mode if (autoRefreshEnabled && !isEditMode && status === "authenticated") { // Clear any existing interval if (refreshIntervalRef.current) { clearInterval(refreshIntervalRef.current); } // Set initial countdown value setNextRefresh(10); // Set up countdown interval refreshIntervalRef.current = setInterval(() => { setNextRefresh((prev) => { // When countdown reaches 0, refresh the data and reset countdown if (prev <= 1) { fetchClues(); return 10; } return prev - 1; }); }, 1000); } // Clean up interval on unmount or when dependencies change return () => { if (refreshIntervalRef.current) { clearInterval(refreshIntervalRef.current); } }; }, [autoRefreshEnabled, isEditMode, status]); // Disable auto-refresh when entering edit mode useEffect(() => { if (isEditMode) { setAutoRefreshEnabled(false); } }, [isEditMode]); // Fetch all clues const fetchClues = async () => { // Skip fetch if in edit mode to prevent data loss if (isEditMode) { return; } try { setLoading(true); // Make sure to get all clues with a high limit to avoid pagination issues const timestamp = Date.now(); const response = await fetch( `/api/admin/clues?t=${timestamp}&limit=100`, { method: "GET", headers: { "Content-Type": "application/json", "Cache-Control": "no-cache, no-store, must-revalidate", Pragma: "no-cache", }, 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"); } // Sort the clues by ID to ensure correct ordering const sortedClues = [...data].sort((a, b) => a.id - b.id); console.log("Fetched clues:", sortedClues); setClues(sortedClues); setError(""); // Update lastRefresh timestamp setLastRefresh(Date.now()); // Reset countdown after manual refresh setNextRefresh(10); } 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); // Now include the QR code in the request const response = await fetch("/api/admin/clues", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(newClue), credentials: "include", }); const data = await response.json(); if (!response.ok) { throw new Error( data.error || `Failed to add clue (Status: ${response.status})` ); } // Refresh the clue list with force refresh await fetchClues(); // Reset the form - now with qrCode setNewClue({ title: "", description: "", location: "", qr_code: "", }); setError(""); } catch (err) { console.error("Error adding clue:", err); setError(err.message); } finally { setLoading(false); } }; // Start editing a clue const handleEditStart = (clue) => { // Disable auto-refresh when starting to edit setAutoRefreshEnabled(false); setEditingClue({ id: clue.id, title: clue.title, description: clue.description, location: clue.location || "", // Use the actual qr_code from the clue object if available qr_code: clue.qr_code || `QR${clue.id}`, }); setIsEditMode(true); }; // Handle input change for editing clue const handleEditChange = (e) => { const { name, value } = e.target; setEditingClue((prev) => ({ ...prev, [name]: value, })); }; // Handle form submission to update clue const handleUpdateClue = async (e) => { e.preventDefault(); try { setLoading(true); const response = await fetch(`/api/admin/clues/${editingClue.id}`, { method: "PUT", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ title: editingClue.title, description: editingClue.description, location: editingClue.location, qr_code: editingClue.qr_code, // Add QR code to the update }), credentials: "include", }); const data = await response.json(); if (!response.ok) { throw new Error( data.error || `Failed to update clue (Status: ${response.status})` ); } // Refresh the clue list with force refresh await fetchClues(); setIsEditMode(false); setEditingClue(null); setError(""); } catch (err) { console.error("Error updating clue:", err); setError(err.message); } finally { setLoading(false); } }; // Cancel editing const handleCancelEdit = () => { setIsEditMode(false); setEditingClue(null); // Re-enable auto-refresh when canceling edit setAutoRefreshEnabled(true); }; // Force refresh the clue list const handleForceRefresh = () => { fetchClues(); }; // Toggle auto-refresh const toggleAutoRefresh = () => { setAutoRefreshEnabled((prev) => !prev); }; // Handle deleting a clue with automatic QR code renumbering const handleDeleteClue = async (clueId, qrCode) => { if ( !confirm( "Are you sure you want to delete this clue?\n\nQR codes will be automatically renumbered to prevent gaps in the sequence." ) ) return; try { setLoading(true); // QR codes will always be renumbered - no need to pass this parameter anymore const response = await fetch(`/api/admin/clues/${clueId}`, { method: "DELETE", headers: { "Content-Type": "application/json", }, credentials: "include", }); // Parse the response data for detailed error messages const data = await response.json().catch(() => ({})); if (!response.ok) { throw new Error( data.error || `Failed to delete clue (Status: ${response.status})` ); } // Success message and refresh console.log("Clue deleted successfully:", data.message || "Success"); // Refresh the clue list to show updated QR codes fetchClues(); setError(""); } catch (err) { console.error("Error deleting clue:", err); setError(`Error deleting clue: ${err.message}`); } finally { setLoading(false); } }; // 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) { return
Loading...
; } if (status === "unauthenticated") { return null; // Will redirect via useEffect } return (
Manage Clues
{error &&

{error}

} {fixMessage &&

{fixMessage}

} {/* Reordering Modal */} {isReorderMode ? (

Change Clue Position

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.
) : ( <> {/* Add New Clue Form */} {!isEditMode ? (

Add New Clue

{/* Add QR code field with new label */}
QR codes should follow the format QR[number] (e.g., QR1, QR2). Leave blank to auto-generate.
) : (

Edit Clue

QR codes should follow the format QR[number] (e.g., QR1, QR2). The QR code value determines what users scan to find this clue.
)} {/* Existing Clues List */}

Existing Clues

{reorderMessage && (

{reorderMessage}

)}

QR codes are the values that users scan to find each clue. You can customize these values, but they should usually follow the format QR[number].

Last updated: {new Date(lastRefresh).toLocaleTimeString()} {autoRefreshEnabled && !isEditMode && ( (next refresh in {nextRefresh}s) )}
{clues.length === 0 ? (

No clues found.

) : (
{clues.map((clue) => (

{clue.title}

{clue.description}

{clue.location && (

Location: {clue.location}

)}

QR Code:{" "} {clue.qr_code || `QR${clue.id}`}

))}
)}
)}
{/* Admin Navigation Bar */}
); }