cannot fix issue with elapsed timer on admin dashboard. fixing issues with QR code editing.

Grey = cant click
Red = current
Green = complete
blue = skipped?
This commit is contained in:
bobbert committed 2025-03-15 20:01:51 +00:00
1 parent 7d6b73ffb4
commit 0afa6817a8
12 files changed
+1529 -211

No files matched your search

+501 -65
View File
@@ -1,5 +1,5 @@
"use client";
import { useState, useEffect } from "react";
import { useState, useEffect, useRef } from "react";
import { useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
@@ -9,12 +9,24 @@ export default function AdminCluesPage() {
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 - removed coordinates
// Form state for adding new clues - add qrCode field back
const [newClue, setNewClue] = useState({
title: "",
description: "",
location: "",
qr_code: "", // Add QR code field
});
// Check if user is admin
@@ -30,15 +42,63 @@ export default function AdminCluesPage() {
}
}, [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);
const response = await fetch("/api/admin/clues", {
// Add timestamp parameter to bust cache
const timestamp = Date.now();
const response = await fetch(`/api/admin/clues?t=${timestamp}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
"Cache-Control": "no-cache, no-store, must-revalidate",
Pragma: "no-cache",
},
credentials: "include",
});
@@ -57,8 +117,13 @@ export default function AdminCluesPage() {
throw new Error("Invalid response format from server");
}
console.log("Fetched clues:", data); // Debug log to see what's coming from API
setClues(data);
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);
@@ -81,6 +146,8 @@ export default function AdminCluesPage() {
e.preventDefault();
try {
setLoading(true);
// Now include the QR code in the request
const response = await fetch("/api/admin/clues", {
method: "POST",
headers: {
@@ -90,18 +157,23 @@ export default function AdminCluesPage() {
credentials: "include",
});
const data = await response.json();
if (!response.ok) {
throw new Error("Failed to add clue");
throw new Error(
data.error || `Failed to add clue (Status: ${response.status})`
);
}
// Refresh the clue list
fetchClues();
// Refresh the clue list with force refresh
await fetchClues();
// Reset the form - removed coordinates
// Reset the form - now with qrCode
setNewClue({
title: "",
description: "",
location: "",
qr_code: "",
});
setError("");
@@ -113,12 +185,102 @@ export default function AdminCluesPage() {
}
};
// Handle deleting a clue
const handleDeleteClue = async (clueId) => {
if (!confirm("Are you sure you want to delete this clue?")) return;
// 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: {
@@ -127,16 +289,23 @@ export default function AdminCluesPage() {
credentials: "include",
});
// Parse the response data for detailed error messages
const data = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error("Failed to delete clue");
throw new Error(
data.error || `Failed to delete clue (Status: ${response.status})`
);
}
// Refresh the clue list
// 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(err.message);
setError(`Error deleting clue: ${err.message}`);
} finally {
setLoading(false);
}
@@ -154,62 +323,186 @@ export default function AdminCluesPage() {
<div className="admin-page">
<div className="page-header">
<div className="page-title">Manage Clues</div>
<div className="placeholder"></div>
<div className="header-controls">
<button
onClick={toggleAutoRefresh}
className={`auto-refresh-toggle ${
autoRefreshEnabled ? "active" : ""
}`}
title={autoRefreshEnabled ? "Auto-refresh on" : "Auto-refresh off"}
>
{autoRefreshEnabled ? "🔄 Auto" : "⏸️ Auto"}
{autoRefreshEnabled && (
<span className="refresh-countdown">{nextRefresh}s</span>
)}
</button>
<button
onClick={handleForceRefresh}
className="refresh-button"
title="Force refresh"
>
🔄
</button>
</div>
</div>
<div className="admin-content">
{error && <p className="error-message">{error}</p>}
{fixMessage && <p className="success-message">{fixMessage}</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>
{/* Add New Clue Form */}
{!isEditMode ? (
<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="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>
<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>
{/* Add QR code field with new label */}
<div className="form-group">
<label htmlFor="qr_code">QR Answer Value</label>
<input
type="text"
id="qr_code"
name="qr_code"
value={newClue.qr_code}
onChange={handleInputChange}
className="admin-input"
placeholder="QR1, QR2, etc."
/>
<div className="help-text">
QR codes should follow the format QR[number] (e.g., QR1, QR2).
Leave blank to auto-generate.
</div>
</div>
{/* Existing Clues List - updated to remove coordinates */}
<button type="submit" className="admin-button">
Add Clue
</button>
</form>
</div>
) : (
<div className="admin-card">
<h2>Edit Clue</h2>
<form onSubmit={handleUpdateClue}>
<div className="form-group">
<label htmlFor="edit-title">Title</label>
<input
type="text"
id="edit-title"
name="title"
value={editingClue.title}
onChange={handleEditChange}
required
className="admin-input"
/>
</div>
<div className="form-group">
<label htmlFor="edit-description">Description</label>
<textarea
id="edit-description"
name="description"
value={editingClue.description}
onChange={handleEditChange}
required
className="admin-textarea"
></textarea>
</div>
<div className="form-group">
<label htmlFor="edit-location">Location Hint</label>
<input
type="text"
id="edit-location"
name="location"
value={editingClue.location}
onChange={handleEditChange}
className="admin-input"
/>
</div>
<div className="form-group">
<label htmlFor="edit-qrcode">QR Answer Value</label>
<input
type="text"
id="edit-qrcode"
name="qr_code"
value={editingClue.qr_code}
onChange={handleEditChange}
className="admin-input"
/>
<div className="help-text">
QR codes should follow the format QR[number] (e.g., QR1, QR2).
The QR code value determines what users scan to find this
clue.
</div>
</div>
<div className="form-actions">
<button type="submit" className="admin-button">
Update Clue
</button>
<button
type="button"
className="admin-button cancel-button"
onClick={handleCancelEdit}
>
Cancel
</button>
</div>
</form>
</div>
)}
{/* Existing Clues List */}
<div className="admin-card">
<h2>Existing Clues</h2>
<p className="info-text">
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].
</p>
<div className="last-updated">
Last updated: {new Date(lastRefresh).toLocaleTimeString()}
{autoRefreshEnabled && !isEditMode && (
<span> (next refresh in {nextRefresh}s)</span>
)}
</div>
{clues.length === 0 ? (
<p className="no-clues">No clues found.</p>
) : (
@@ -218,12 +511,20 @@ export default function AdminCluesPage() {
<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 className="clue-actions">
<button
className="edit-button"
onClick={() => handleEditStart(clue)}
>
Edit
</button>
<button
className="delete-button"
onClick={() => handleDeleteClue(clue.id, clue.qr_code)}
>
Delete
</button>
</div>
</div>
<p className="clue-description">{clue.description}</p>
{clue.location && (
@@ -234,6 +535,9 @@ export default function AdminCluesPage() {
<p className="clue-position">
<strong>Position:</strong> Clue #{clue.id} in sequence
</p>
<p className="clue-qr-code">
<strong>QR Code:</strong> {clue.qr_code || `QR${clue.id}`}
</p>
</div>
))}
</div>
@@ -276,7 +580,7 @@ export default function AdminCluesPage() {
.page-header {
display: flex;
justify-content: center;
justify-content: space-between;
align-items: center;
padding: 1rem;
background-color: #68543c;
@@ -288,6 +592,39 @@ export default function AdminCluesPage() {
font-weight: 600;
}
.header-controls {
display: flex;
gap: 10px;
align-items: center;
}
.auto-refresh-toggle {
display: flex;
align-items: center;
gap: 5px;
background-color: #5a4832;
color: white;
border: none;
border-radius: 4px;
padding: 5px 10px;
cursor: pointer;
font-size: 0.9rem;
opacity: 0.8;
}
.auto-refresh-toggle.active {
background-color: #4caf50;
opacity: 1;
}
.refresh-countdown {
font-size: 0.8rem;
background-color: rgba(255, 255, 255, 0.25);
border-radius: 3px;
padding: 0 5px;
margin-left: 3px;
}
.admin-content {
flex: 1;
padding: 1rem;
@@ -400,6 +737,11 @@ export default function AdminCluesPage() {
margin-bottom: 0.4rem;
}
.clue-qr-code {
font-size: 0.9rem;
margin-bottom: 0.4rem;
}
.error-message {
color: red;
margin-bottom: 1rem;
@@ -462,6 +804,100 @@ export default function AdminCluesPage() {
.admin-nav-label {
font-size: 13px;
}
.help-text {
font-size: 0.8rem;
color: #666;
margin-top: 4px;
}
.form-actions {
display: flex;
gap: 1rem;
margin-top: 1rem;
}
.cancel-button {
background-color: #757575;
}
.edit-button {
background-color: #2196f3;
color: white;
border: none;
border-radius: 4px;
padding: 0.4rem 0.6rem;
font-size: 0.9rem;
cursor: pointer;
margin-right: 8px;
}
.clue-actions {
display: flex;
gap: 0.5rem;
}
.admin-input.disabled {
background-color: #f5f5f5;
color: #666;
}
.admin-tools {
display: none;
}
.tool-button,
.sequence-button,
.fix-button {
display: none;
}
.tool-description {
display: none;
}
.success-message {
color: #4caf50;
margin-bottom: 1rem;
padding: 0.5rem;
border-radius: 4px;
background-color: #e8f5e9;
width: 100%;
max-width: 700px;
text-align: center;
}
.info-text {
font-size: 0.9rem;
color: #666;
margin-bottom: 1rem;
padding: 0.5rem;
background-color: #f5f5f5;
border-left: 3px solid #2196f3;
}
.refresh-button {
background: transparent;
border: none;
font-size: 1.2rem;
cursor: pointer;
padding: 0.5rem;
margin-left: 1rem;
color: white;
}
.refresh-button:hover {
transform: rotate(90deg);
transition: transform 0.3s ease;
}
.last-updated {
font-size: 0.8rem;
color: #666;
margin-bottom: 1rem;
text-align: right;
font-style: italic;
}
`}</style>
</div>
);