(createEmptyGrid(3));
const [bingoDescription, setBingoDescription] = useState("");
const [loading, setLoading] = useState(false);
@@ -39,7 +46,7 @@ function CreateEventPage() {
// ✅ Validate bingo (if selected)
if (nameBingoSelected) {
const hasEmptyCells = bingoGrid.some(row =>
- row.some(cell => !cell.trim())
+ row.some(cell => !cell.question.trim() || !cell.shortQuestion.trim())
);
if (hasEmptyCells) {
@@ -115,10 +122,12 @@ function CreateEventPage() {
maxParticipant > 0;
const token = localStorage.getItem("token");
- if (!token) {
- navigate("/login");
- return;
- }
+ useEffect(() => {
+ if (!token) {
+ navigate("/login");
+ return;
+ }
+ }, [token, navigate])
return (
{
- const newGrid = bingoGrid.map(r => [...r]); // ✅ deep copy
+ const newGrid = bingoGrid.map(r => [...r]);
newGrid[row][col] = value;
setBingoGrid(newGrid);
}}
diff --git a/shatter-web/src/pages/DashboardPage.tsx b/shatter-web/src/pages/DashboardPage.tsx
index 67e9487..d23f06f 100644
--- a/shatter-web/src/pages/DashboardPage.tsx
+++ b/shatter-web/src/pages/DashboardPage.tsx
@@ -4,6 +4,12 @@ import Navbar from "../components/Navbar";
import Footer from "../components/Footer";
import BingoTable from "../components/BingoTable";
import { getBingo } from "../service/BingoGame";
+
+export interface BingoCell {
+ question: string;
+ shortQuestion: string;
+}
+
import {
CalendarIcon,
ClipboardIcon,
@@ -28,6 +34,18 @@ interface Event {
createdBy?: string;
}
+async function readApiErrorMessage(res: Response): Promise {
+ const text = await res.text().catch(() => "");
+ try {
+ const j = JSON.parse(text) as Record;
+ const msg = j.message ?? j.error ?? j.msg;
+ if (typeof msg === "string" && msg.trim()) return msg;
+ } catch {
+ /* ignore */
+ }
+ return text.trim() || `Request failed (${res.status})`;
+}
+
function getUserIdFromToken(token: string): string | null {
try {
const parts = token.split(".");
@@ -57,15 +75,23 @@ function DashboardPage() {
startDate: "",
endDate: "",
maxParticipant: 0,
- currentState: "Upcoming",
});
- const createEmptyGrid = (size: number) => Array.from({ length: size }, () => Array(size).fill(""));
- const [selectedIcebreaker, setSelectedIcebreaker] = useState(null);
+const createEmptyGrid = (size: number): BingoCell[][] =>
+ Array.from({ length: size }, () =>
+ Array.from({ length: size }, () => ({
+ question: "",
+ shortQuestion: "",
+ }))
+ ); const [selectedIcebreaker, setSelectedIcebreaker] = useState(null);
const GRID_SIZE = 3;
- const [bingoGrid, setBingoGrid] = useState(createEmptyGrid(GRID_SIZE)); const [bingoDescription, setBingoDescription] = useState("");
+ const [bingoGrid, setBingoGrid] = useState(createEmptyGrid(GRID_SIZE)); const [bingoDescription, setBingoDescription] = useState("");
const [isSavingBingo, setIsSavingBingo] = useState(false);
const [bingoSaveMessage, setBingoSaveMessage] = useState<{ type: "success" | "error"; text: string } | null>(null);
+ const [isDeletingEvent, setIsDeletingEvent] = useState(false);
+ const [deleteEventMessage, setDeleteEventMessage] = useState<{ type: "success" | "error"; text: string } | null>(null);
+ const [isDeleteConfirmOpen, setIsDeleteConfirmOpen] = useState(false);
+ const [editFormMessage, setEditFormMessage] = useState<{ type: "success" | "error"; text: string } | null>(null);
const navigate = useNavigate();
@@ -91,15 +117,18 @@ function DashboardPage() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedEvent?._id]);
- const fetchUserEvents = async (signal?: AbortSignal) => {
+ const fetchUserEvents = async (
+ signal?: AbortSignal,
+ opts?: { skipLoading?: boolean }
+ ): Promise => {
try {
- setLoading(true);
+ if (!opts?.skipLoading) setLoading(true);
setError(null);
const token = localStorage.getItem("token");
if (!token) {
navigate("/login");
- return;
+ return null;
}
const userId = getUserIdFromToken(token);
@@ -107,7 +136,7 @@ function DashboardPage() {
localStorage.removeItem("token");
window.dispatchEvent(new Event("authChange"));
navigate("/login");
- return;
+ return null;
}
const baseUrl = import.meta.env.VITE_API_URL;
@@ -131,14 +160,17 @@ function DashboardPage() {
throw new Error(data?.message || "Backend did not return success.");
}
- setEvents(Array.isArray(data.events) ? data.events : []);
+ const list = Array.isArray(data.events) ? data.events : [];
+ setEvents(list);
+ return list;
} catch (err: any) {
- if (err?.name === "AbortError") return;
+ if (err?.name === "AbortError") return null;
console.error("Error fetching events:", err);
setError(err?.message || "Failed to load events");
setEvents([]);
+ return null;
} finally {
- setLoading(false);
+ if (!opts?.skipLoading) setLoading(false);
}
};
@@ -146,27 +178,35 @@ function DashboardPage() {
setSelectedEvent(event);
setIsEditing(false);
setSelectedIcebreaker(null);
-
- // Load bingo data if it exists
- loadBingoData(event._id);
+ setDeleteEventMessage(null);
+ setEditFormMessage(null);
+ setIsDeleteConfirmOpen(false);
};
- const loadBingoData = async (eventId: string) => {
- try {
- const bingo = await getBingo(eventId);
- if (bingo) {
- setBingoGrid(bingo.grid);
- setBingoDescription(bingo.description ?? "");
- } else {
- setBingoGrid(createEmptyGrid(GRID_SIZE));
- setBingoDescription("");
- }
- } catch (err) {
- console.error("Error loading bingo data:", err);
+const loadBingoData = async (eventId: string) => {
+ try {
+ const bingo = await getBingo(eventId);
+
+ if (bingo) {
+ const formattedGrid = bingo.grid.map(row =>
+ row.map(cell => ({
+ question: cell.question || "",
+ shortQuestion: cell.shortQuestion || "",
+ }))
+ );
+
+ setBingoGrid(formattedGrid);
+ setBingoDescription(bingo.description ?? "");
+ } else {
setBingoGrid(createEmptyGrid(GRID_SIZE));
setBingoDescription("");
}
- };
+ } catch (err) {
+ console.error("Error loading bingo data:", err);
+ setBingoGrid(createEmptyGrid(GRID_SIZE));
+ setBingoDescription("");
+ }
+};
const handleEditClick = () => {
if (!selectedEvent) return;
@@ -178,17 +218,20 @@ function DashboardPage() {
startDate: selectedEvent.startDate?.substring(0, 16) || "",
endDate: selectedEvent.endDate?.substring(0, 16) || "",
maxParticipant: selectedEvent.maxParticipant || 0,
- currentState: selectedEvent.currentState || "Upcoming",
});
+ setEditFormMessage(null);
};
const handleCancelEdit = () => {
setIsEditing(false);
+ setEditFormMessage(null);
};
const handleSaveEdit = async () => {
if (!selectedEvent) return;
+ setEditFormMessage(null);
+
try {
const token = localStorage.getItem("token");
if (!token) {
@@ -196,47 +239,96 @@ function DashboardPage() {
return;
}
- const response = await fetch(`${import.meta.env.VITE_API_URL}/events/${selectedEvent._id}`, {
+ const name = editForm.name.trim();
+ const description = editForm.description.trim();
+ if (!name) {
+ setEditFormMessage({ type: "error", text: "Name must be a non-empty string." });
+ return;
+ }
+ if (!description) {
+ setEditFormMessage({ type: "error", text: "Description must be a non-empty string." });
+ return;
+ }
+
+ const eventId = selectedEvent._id;
+ const baseUrl = import.meta.env.VITE_API_URL;
+
+ const updateBody: Record = {
+ name,
+ description,
+ startDate: new Date(editForm.startDate).toISOString(),
+ endDate: new Date(editForm.endDate).toISOString(),
+ maxParticipant: editForm.maxParticipant,
+ };
+
+ const response = await fetch(`${baseUrl}/events/${eventId}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
- body: JSON.stringify({
- name: editForm.name,
- description: editForm.description,
- startDate: new Date(editForm.startDate).toISOString(),
- endDate: new Date(editForm.endDate).toISOString(),
- maxParticipant: editForm.maxParticipant,
- currentState: editForm.currentState,
- }),
+ body: JSON.stringify(updateBody),
});
if (!response.ok) {
- const txt = await response.text().catch(() => "");
- throw new Error(`Failed to update event (status ${response.status}). ${txt ? txt.slice(0, 120) : ""}`.trim());
+ throw new Error(await readApiErrorMessage(response));
}
- await fetchUserEvents();
+ const list = await fetchUserEvents(undefined, { skipLoading: true });
setIsEditing(false);
+ if (list?.length) {
+ const updated = list.find((e) => e._id === eventId);
+ if (updated) setSelectedEvent(updated);
+ }
- // reselect AFTER fetch (using fresh data)
- setSelectedEvent(prev => {
- if (!prev) return prev;
- return events.find(e => e._id === prev._id) || prev;
- });
+ setEditFormMessage({ type: "success", text: "Event updated successfully." });
} catch (err: any) {
console.error("Error updating event:", err);
- alert(err?.message || "Failed to update event. The backend may not support updates yet.");
+ setEditFormMessage({
+ type: "error",
+ text: err?.message || "Failed to update event.",
+ });
}
};
- const handleBingoGridChange = (row: number, col: number, value: string) => {
- const newGrid = bingoGrid.map(r => [...r]);
- newGrid[row][col] = value;
- setBingoGrid(newGrid);
+ const handleDeleteEvent = async () => {
+ if (!selectedEvent || isDeletingEvent) return;
+ setDeleteEventMessage(null);
+
+ try {
+ setIsDeletingEvent(true);
+ const token = localStorage.getItem("token");
+ if (!token) {
+ navigate("/login");
+ return;
+ }
+
+ const response = await fetch(`${import.meta.env.VITE_API_URL}/events/${selectedEvent._id}`, {
+ method: "DELETE",
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(errorData?.msg || errorData?.error || "Failed to delete event");
+ }
+
+ await fetchUserEvents();
+ setSelectedEvent(null);
+ setSelectedIcebreaker(null);
+ setIsEditing(false);
+ setIsDeleteConfirmOpen(false);
+ setDeleteEventMessage({ type: "success", text: "Event deleted successfully." });
+ } catch (err: any) {
+ setDeleteEventMessage({ type: "error", text: err?.message || "Failed to delete event" });
+ } finally {
+ setIsDeletingEvent(false);
+ }
};
+
const handleSaveBingo = async () => {
if (!selectedEvent) return;
@@ -247,7 +339,7 @@ function DashboardPage() {
const token = localStorage.getItem("token");
// Check if all grid cells are filled
- const hasEmptyCells = bingoGrid.some(row => row.some(cell => !cell.trim()));
+ const hasEmptyCells = bingoGrid.some(row => row.some(cell => !cell.question.trim() || !cell.shortQuestion.trim()));
if (hasEmptyCells) {
setBingoSaveMessage({ type: "error", text: "Please fill in all bingo grid cells before saving." });
setIsSavingBingo(false);
@@ -490,10 +582,83 @@ function DashboardPage() {
>
Edit
+
)}