From 2cc54627099dc7b537e52f069a2ad8f2990096ab Mon Sep 17 00:00:00 2001 From: 12dinitrobenzene Date: Sat, 4 Apr 2026 18:56:01 +0800 Subject: [PATCH 01/26] Add hamburger menu, settings accordion, and success confirmation --- frontend/package-lock.json | 4 +- frontend/src/components/HamburgerMenu.tsx | 50 +++++ frontend/src/components/SettingsAccordion.tsx | 138 +++++++++++++ frontend/src/components/utils.ts | 2 +- frontend/src/pages/AdminDashboard.tsx | 192 +++++++++--------- frontend/src/pages/InitDataPage.tsx | 8 + 6 files changed, 300 insertions(+), 94 deletions(-) create mode 100644 frontend/src/components/HamburgerMenu.tsx create mode 100644 frontend/src/components/SettingsAccordion.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4417eb1..dd9b02e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,11 +1,11 @@ { - "name": "reactjs-template", + "name": "nusc-queuebot-frontend", "version": "0.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "reactjs-template", + "name": "nusc-queuebot-frontend", "version": "0.0.1", "dependencies": { "@tailwindcss/vite": "^4.1.18", diff --git a/frontend/src/components/HamburgerMenu.tsx b/frontend/src/components/HamburgerMenu.tsx new file mode 100644 index 0000000..39ad5b9 --- /dev/null +++ b/frontend/src/components/HamburgerMenu.tsx @@ -0,0 +1,50 @@ +import { useState } from 'react'; +import { Menu, X, Settings } from 'lucide-react'; + +interface HamburgerMenuProps { + onSettingsClick: () => void; +} + +export function HamburgerMenu({ onSettingsClick }: HamburgerMenuProps) { + const [isOpen, setIsOpen] = useState(false); + + return ( + <> + {/* Hamburger Button */} + + + {/* Mobile Menu */} + {isOpen && ( +
setIsOpen(false)} /> + )} + + + + ); +} diff --git a/frontend/src/components/SettingsAccordion.tsx b/frontend/src/components/SettingsAccordion.tsx new file mode 100644 index 0000000..3020339 --- /dev/null +++ b/frontend/src/components/SettingsAccordion.tsx @@ -0,0 +1,138 @@ +import { useState } from 'react'; +import { ChevronDown, Check } from 'lucide-react'; + +interface Settings { + eventName: string; + venue: string; + notifyBefore: number; +} + +interface SettingsAccordionProps { + settings: Settings; + onSettingsChange: (settings: Settings) => void; + userType: 'admin' | 'user'; +} + +export function SettingsAccordion({ settings, onSettingsChange, userType }: SettingsAccordionProps) { + const [isOpen, setIsOpen] = useState(false); + const [showSuccess, setShowSuccess] = useState(false); // ✅ NEW + + if (userType !== 'admin') return null; + + const handleChange = (field: keyof Settings, value: string | number) => { + onSettingsChange({ + ...settings, + [field]: value, + }); + }; + + // ✅ NEW - Handle Save with Success Message + const handleSave = () => { + setShowSuccess(true); + // Auto-hide after 3 seconds + setTimeout(() => setShowSuccess(false), 3000); + }; + + return ( +
+ + + {isOpen && ( +
+ {/* Event Name */} +
+ + handleChange('eventName', e.target.value)} + placeholder="e.g., Queue for Registration" + className="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500" + /> +
+ + {/* Venue */} +
+ + handleChange('venue', e.target.value)} + placeholder="e.g., Main Hall, Building A" + className="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500" + /> +
+ + {/* Notify Before */} +
+ + handleChange('notifyBefore', parseInt(e.target.value))} + placeholder="e.g., 5" + min="0" + className="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500" + /> +
+ + {/* Save Button */} + +
+ )} + + {/* ✅ NEW - Success Modal */} + {showSuccess && ( +
+
+
+ {/* Success Icon */} +
+ +
+ + {/* Success Text */} +

Settings Saved!

+

+ Event settings have been updated successfully. +

+ + {/* Settings Summary */} +
+

+ Event: {settings.eventName} +

+

+ Venue: {settings.venue} +

+

+ Notify Before: {settings.notifyBefore} people +

+
+
+
+
+ )} +
+ ); +} diff --git a/frontend/src/components/utils.ts b/frontend/src/components/utils.ts index cb2ba44..342a6e4 100644 --- a/frontend/src/components/utils.ts +++ b/frontend/src/components/utils.ts @@ -1,5 +1,5 @@ const BACKEND_DEV_URL = 'http://localhost:3000'; -const BACKEND_PROD_URL = 'https://fastify-test-backend.vercel.app'; +const BACKEND_PROD_URL = 'https://queuebot-4nfg.onrender.com'; export function createPath(path: string) { return `${import.meta.env.DEV ? BACKEND_DEV_URL : BACKEND_PROD_URL}/${path}`; diff --git a/frontend/src/pages/AdminDashboard.tsx b/frontend/src/pages/AdminDashboard.tsx index b2c298a..54ca633 100644 --- a/frontend/src/pages/AdminDashboard.tsx +++ b/frontend/src/pages/AdminDashboard.tsx @@ -4,6 +4,14 @@ import {QueueControls} from './QueueControls'; import {QueueList} from './QueueList'; import {createPath} from "@/components/utils.ts"; import {Page} from "@/components/Page.tsx"; +import {HamburgerMenu} from '@/components/HamburgerMenu'; +import {SettingsAccordion} from '@/components/SettingsAccordion'; + +interface Settings { + eventName: string; + venue: string; + notifyBefore: number; +} export interface QueueEntry { id: string; @@ -13,13 +21,46 @@ export interface QueueEntry { export function AdminDashboard() { const [isPaused, setIsPaused] = useState(false); - const [queue, setQueue] = useState([]); const [inQueue, setInQueue] = useState(false); const [username, setUsername] = useState(""); - const [peopleAhead, setPeopleAhead] = useState(null); + const [settings, setSettings] = useState({ + eventName: 'NUSC Queue', + venue: 'Main Hall', + notifyBefore: 5, + }); + const [showSettings, setShowSettings] = useState(false); + + // ✅ FIRST useEffect - Setup fake data + useEffect(() => { + if (!sessionStorage.getItem("jwt")) { + sessionStorage.setItem("jwt", "fake-jwt-token-dev"); + sessionStorage.setItem("user-type", "admin"); + } + + setQueue([ + { id: "1", name: "John", joinedAt: new Date() }, + { id: "2", name: "Jane", joinedAt: new Date() }, + { id: "3", name: "Bob", joinedAt: new Date() }, + ]); + }, []); + + // ✅ SECOND useEffect - Fetch data (commented out for now) + useEffect(() => { + // Skip backend calls for development + // const fetchData = async () => { ... } + // fetchData(); + }, []); + // ✅ NOW define userType (after all hooks) + if (sessionStorage.getItem("jwt") == null) { + return (
Error
); + } + + const userType = sessionStorage.getItem("user-type") as ("admin" | "user"); + + // Handler functions const handleRemove = async (id: string) => { await fetch(createPath(`queue/entries/${id}`), {method: "DELETE", headers: {Authorization: sessionStorage.getItem("jwt")!,}}) @@ -27,7 +68,6 @@ export function AdminDashboard() { if (res.status == 200) { reloadQueue((await res.json())['entries']); } - }); }; @@ -37,13 +77,11 @@ export function AdminDashboard() { .then(async (res) => { if (res.status == 200) { setInQueue(false); - // fetch queue stats handleRefresh(); } }); }; - // transform the object list into a list of QueueEntry and update the queue const reloadQueue = (entries: any[]) => { setQueue(entries.map((entry) => { return { @@ -54,13 +92,6 @@ export function AdminDashboard() { })); } - // Unimplemented - // const handleClearQueue = () => { - // if (window.confirm('Are you sure you want to clear the entire queue?')) { - // setQueue([]); - // } - // }; - const handleAdvanceQueue = async () => { await fetch(createPath("queue/next"), {method: "POST", headers: {Authorization: sessionStorage.getItem("jwt")!,}}) @@ -125,89 +156,68 @@ export function AdminDashboard() { ]); } - if (sessionStorage.getItem("jwt") == null) { - return (
Error
); - } - - const userType = sessionStorage.getItem("user-type") as ("admin" | "user"); - - useEffect(() => { - - const fetchData = async () => { - fetch(createPath("queue/status"), - {method: "GET", headers: {Authorization: sessionStorage.getItem("jwt")!,}}) - .then(async (res) => { - if (res.status == 200) { - setIsPaused(!(await res.json())['status']); - } - }); - - if (userType == "admin") { - fetch(createPath("queue/entries"), - {method: "GET", headers: {Authorization: sessionStorage.getItem("jwt")!,}}) - .then(async (res) => { - if (res.status == 200) { - reloadQueue((await res.json())['entries']); - } - - }); - } else { - handleRefresh(); - } - - } - - fetchData(); - - }, []); - + // ✅ RETURN JSX return ( -
-
- {/* Header */} -
-

NUSC Queuebot

- {userType == "user" && inQueue ? -

You Are Queued Up!

: null} - {userType == "admin" ? -

Admin Dashboard

: null} -
+ setShowSettings(!showSettings)} /> + +
+
+ {/* Header */} +
+

{settings.eventName}

+

{settings.venue}

+ {userType === "admin" && ( +

Admin Dashboard

+ )} +
+ + {/* Settings Accordion - Only for Admin */} + {userType === 'admin' && showSettings && ( + + )} + + {/* Statistics */} + - {/* Statistics */} - - - {/* Controls */} - - - {/* Content */} - {userType == "admin" ? - : inQueue ? (
-

{`You are ${username}!`}

-
) : null} + isInQueue={inQueue} + onTogglePause={handleTogglePause} + onJoinQueue={handleJoinQueue} + onLeaveQueue={handleLeaveQueue} + onAdvanceQueue={handleAdvanceQueue} + /> + + {/* Content */} + {userType === "admin" ? ( + + ) : inQueue ? ( +
+

{`You are ${username}!`}

+

Event: {settings.eventName}

+

Venue: {settings.venue}

+
+ ) : null} +
-
); -} \ No newline at end of file +} diff --git a/frontend/src/pages/InitDataPage.tsx b/frontend/src/pages/InitDataPage.tsx index 3fbd9b7..529327b 100644 --- a/frontend/src/pages/InitDataPage.tsx +++ b/frontend/src/pages/InitDataPage.tsx @@ -1,3 +1,4 @@ + import {type FC, useEffect} from 'react'; import { initData, @@ -9,10 +10,16 @@ import { Page } from '@/components/Page.tsx'; import {createPath} from "@/components/utils.ts"; import {useNavigate} from "react-router-dom"; + export const InitDataPage: FC = () => { const initDataRaw = useSignal(initData.raw); const navigate = useNavigate(); + useEffect(() => { + navigate("/dashboard"); + }, [navigate]); + + /* useEffect(() => { const initiateAuth = async () => { @@ -30,6 +37,7 @@ export const InitDataPage: FC = () => { } initiateAuth(); }, [initDataRaw]); + */ if (!initDataRaw) { return ( From cce3e91f10d1a9c50ebf31f3287c9a9b6da4f588 Mon Sep 17 00:00:00 2001 From: 12dinitrobenzene Date: Sat, 4 Apr 2026 22:21:08 +0800 Subject: [PATCH 02/26] added hamburger menu to adjust settings --- frontend/src/components/HamburgerMenu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/HamburgerMenu.tsx b/frontend/src/components/HamburgerMenu.tsx index 39ad5b9..d73b027 100644 --- a/frontend/src/components/HamburgerMenu.tsx +++ b/frontend/src/components/HamburgerMenu.tsx @@ -28,7 +28,7 @@ export function HamburgerMenu({ onSettingsClick }: HamburgerMenuProps) { )}