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
+1046 -233

No files matched your search

+395 -181
View File
@@ -29,6 +29,11 @@ export default function AdminCluesPage() {
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") {
@@ -91,17 +96,21 @@ export default function AdminCluesPage() {
try {
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 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",
});
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(() => ({}));
@@ -117,8 +126,11 @@ 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);
// 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());
@@ -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) {
return <div className="admin-page loading">Loading...</div>;
}
@@ -350,199 +494,254 @@ export default function AdminCluesPage() {
{error && <p className="error-message">{error}</p>}
{fixMessage && <p className="success-message">{fixMessage}</p>}
{/* Add New Clue Form */}
{!isEditMode ? (
{/* Reordering Modal */}
{isReorderMode ? (
<div className="admin-card">
<h2>Add New Clue</h2>
<form onSubmit={handleAddClue}>
<h2>Change Clue Position</h2>
<form onSubmit={handleReorderSubmit}>
<div className="form-group">
<label htmlFor="title">Title</label>
<input
type="text"
id="title"
name="title"
value={newClue.title}
onChange={handleInputChange}
required
<label htmlFor="clue-position">
Move clue "{reorderingClue?.title}" to new position:
</label>
<select
id="clue-position"
value={newPosition}
onChange={handlePositionChange}
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>
{/* 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."
/>
>
{clues.map((_, index) => (
<option key={index + 1} value={index + 1}>
{index + 1}
</option>
))}
</select>
<div className="help-text">
QR codes should follow the format QR[number] (e.g., QR1, QR2).
Leave blank to auto-generate.
</div>
</div>
<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.
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">
Update Clue
Change Position
</button>
<button
type="button"
className="admin-button cancel-button"
onClick={handleCancelEdit}
onClick={handleCancelReorder}
>
Cancel
</button>
</div>
</form>
</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>
{/* 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>
) : (
<div className="clues-list">
{clues.map((clue) => (
<div key={clue.id} className="clue-item">
<div className="clue-header">
<h3>{clue.title}</h3>
<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 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>
{/* 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>
<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>
<p className="clue-qr-code">
<strong>QR Code:</strong> {clue.qr_code || `QR${clue.id}`}
</p>
<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>
{reorderMessage && (
<p className="success-message">{reorderMessage}</p>
)}
<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>
) : (
<div className="clues-list">
{clues.map((clue) => (
<div key={clue.id} className="clue-item">
<div className="clue-header">
<h3>{clue.title}</h3>
<div className="clue-actions">
<button
className="reorder-button"
onClick={() => handleReorderStart(clue)}
>
Reorder
</button>
<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 && (
<p className="clue-location">
<strong>Location:</strong> {clue.location}
</p>
)}
<p className="clue-qr-code">
<strong>QR Code:</strong>{" "}
{clue.qr_code || `QR${clue.id}`}
</p>
</div>
))}
</div>
))}
)}
</div>
)}
</div>
</>
)}
</div>
{/* Admin Navigation Bar */}
@@ -898,6 +1097,21 @@ export default function AdminCluesPage() {
text-align: right;
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>
</div>
);