diff --git a/shatter-web/src/components/BingoTable.tsx b/shatter-web/src/components/BingoTable.tsx new file mode 100644 index 0000000..9b33770 --- /dev/null +++ b/shatter-web/src/components/BingoTable.tsx @@ -0,0 +1,40 @@ +interface BingoTableProps { + grid: string[][]; + onChange: (row: number, col: number, value: string) => void; +} + +export default function BingoTable({ grid, onChange }: BingoTableProps) { + const size = grid.length; + + return ( +
+ + +
+ {grid.map((row, rowIndex) => + row.map((cell, colIndex) => ( +
+ + onChange(rowIndex, colIndex, e.target.value) + } + placeholder={`${rowIndex + 1}-${colIndex + 1}`} + className="w-full h-24 p-2 rounded-lg bg-white/5 border border-white/20 text-white text-xs placeholder-white/30 focus:outline-none focus:border-[#4DC4FF] focus:ring-2 focus:ring-[#4DC4FF]/20 transition-all font-body" + /> +
+ {rowIndex * size + colIndex + 1} +
+
+ )) + )} +
+
+ ); +} \ No newline at end of file diff --git a/shatter-web/src/components/EventSpotlight.tsx b/shatter-web/src/components/EventSpotlight.tsx new file mode 100644 index 0000000..6fe0be9 --- /dev/null +++ b/shatter-web/src/components/EventSpotlight.tsx @@ -0,0 +1,461 @@ +import { useEffect, useMemo, useState } from "react"; +import type { Participant } from "../types/participant"; + +export interface Connection { + from: string; // participantId + to: string; // participantId +} + +export interface ActivityItem { + id: string; + type: "joined" | "connection"; + participantName?: string; + fromName?: string; + toName?: string; + timestamp: number; +} + +interface ConnectedUserResponse { + participantId: string | { toString(): string }; + participantName?: string | null; + connectionDescription?: string | null; +} + +interface EventSpotlightProps { + participants: Participant[]; + eventId: string | null; + connections?: Connection[]; + activity?: ActivityItem[]; + /** Generate demo connections when API returns no connections */ + useDemoConnections?: boolean; + /** Generate demo activity when none provided */ + useDemoActivity?: boolean; +} + +const BUBBLE_RADIUS = 32; +const CONTAINER_PADDING = 60; + +function getBubblePosition( + index: number, + total: number, + centerX: number, + centerY: number, + radius: number +): { x: number; y: number } { + if (total <= 0) return { x: centerX, y: centerY }; + if (total === 1) return { x: centerX, y: centerY }; + const angle = (index / total) * 2 * Math.PI - Math.PI / 2; // Start from top + return { + x: centerX + radius * Math.cos(angle), + y: centerY + radius * Math.sin(angle), + }; +} + +function getParticipantColor(index: number): string { + const hues = [200, 160, 280, 40, 320, 180, 260, 60]; // Cyan, teal, purple, orange, pink, etc. + const hue = hues[index % hues.length]; + return `hsl(${hue}, 70%, 55%)`; +} + +function normalizeId(id: string | { toString(): string }): string { + return typeof id === "string" ? id : id.toString(); +} + +export default function EventSpotlight({ + participants, + eventId, + connections: connectionsProp = [], + activity = [], + useDemoConnections = false, + useDemoActivity = true, +}: EventSpotlightProps) { + const [hoveredId, setHoveredId] = useState(null); + const [fetchedConnections, setFetchedConnections] = useState([]); + const [connectionsLoading, setConnectionsLoading] = useState(false); + + // Fetch connections from GET /api/participantConnections/connected-users + useEffect(() => { + if (!eventId || participants.length === 0) { + setFetchedConnections([]); + return; + } + + const token = localStorage.getItem("token"); + if (!token) { + setFetchedConnections([]); + return; + } + + const apiUrl = import.meta.env.VITE_API_URL; + const connectionSet = new Set(); + const conns: Connection[] = []; + + const fetchAll = async () => { + setConnectionsLoading(true); + try { + const results = await Promise.all( + participants.map(async (p) => { + const url = `${apiUrl}/participantConnections/connected-users?eventId=${encodeURIComponent(eventId)}&participantId=${encodeURIComponent(p.participantId)}`; + const res = await fetch(url, { + headers: { Authorization: `Bearer ${token}` }, + }); + if (!res.ok) return []; + const data: ConnectedUserResponse[] = await res.json(); + return data.map((item) => ({ + from: p.participantId, + to: normalizeId(item.participantId), + })); + }) + ); + + for (const list of results) { + for (const c of list) { + const key = [c.from, c.to].sort().join("|"); + if (!connectionSet.has(key)) { + connectionSet.add(key); + conns.push(c); + } + } + } + setFetchedConnections(conns); + } catch { + setFetchedConnections([]); + } finally { + setConnectionsLoading(false); + } + }; + + fetchAll(); + }, [eventId, participants]); + + // Use prop connections if provided, else fetched connections + const connections = connectionsProp.length > 0 ? connectionsProp : fetchedConnections; + + // Container dimensions + const size = 400; + const centerX = size / 2; + const centerY = size / 2; + const graphRadius = Math.min(size - CONTAINER_PADDING * 2, 280) / 2; + + // Participant positions + const positions = useMemo(() => { + const map = new Map(); + participants.forEach((p, i) => { + const pos = getBubblePosition(i, participants.length, centerX, centerY, graphRadius); + map.set(p.participantId, { + ...pos, + color: getParticipantColor(i), + }); + }); + return map; + }, [participants, centerX, centerY, graphRadius]); + + // Demo connections: connect participants in a fun pattern (every other, creating a star) + const effectiveConnections = useMemo(() => { + if (connections.length > 0) return connections; + if (!useDemoConnections || participants.length < 2) return []; + + const demo: Connection[] = []; + const n = participants.length; + for (let i = 0; i < n; i++) { + const j = (i + 2) % n; // Skip one for star pattern + if (i < j) { + demo.push({ from: participants[i].participantId, to: participants[j].participantId }); + } + } + return demo.slice(0, Math.min(demo.length, 8)); // Cap demo lines + }, [connections, participants, useDemoConnections]); + + // Demo activity: join events + fake connection events + const effectiveActivity = useMemo(() => { + if (activity.length > 0) return activity; + if (!useDemoActivity) return []; + + const items: ActivityItem[] = participants + .slice(0, 5) + .map((p, i) => ({ + id: `join-${p.participantId}`, + type: "joined" as const, + participantName: p.name, + timestamp: Date.now() - (participants.length - i) * 60000, + })); + + // Add some fake connection events + if (participants.length >= 2) { + items.push({ + id: "conn-1", + type: "connection", + fromName: participants[0].name, + toName: participants[1].name, + timestamp: Date.now() - 120000, + }); + if (participants.length >= 4) { + items.push({ + id: "conn-2", + type: "connection", + fromName: participants[2].name, + toName: participants[3].name, + timestamp: Date.now() - 90000, + }); + } + } + + return items.sort((a, b) => b.timestamp - a.timestamp).slice(0, 8); + }, [activity, participants, useDemoActivity]); + + // Leaderboard: count connections per participant — show ALL participants + const leaderboard = useMemo(() => { + const scores = new Map(); + participants.forEach((p) => scores.set(p.participantId, { name: p.name, count: 0 })); + + effectiveConnections.forEach((c) => { + const from = scores.get(c.from); + const to = scores.get(c.to); + if (from) from.count++; + if (to) to.count++; + }); + + return Array.from(scores.entries()) + .map(([id, { name, count }]) => ({ participantId: id, name, connections: count })) + .sort((a, b) => b.connections - a.connections); + }, [participants, effectiveConnections]); + + const formatTimeAgo = (ts: number) => { + const sec = Math.floor((Date.now() - ts) / 1000); + if (sec < 60) return "just now"; + const min = Math.floor(sec / 60); + if (min < 60) return `${min}m ago`; + const hr = Math.floor(min / 60); + return `${hr}h ago`; + }; + + if (participants.length === 0) { + return ( +
+

+ Live Activity Spotlight +

+

+ When participants join and make connections, you'll see them here as bubbles with lines between connected people. +

+
+ ); + } + + return ( +
+
+

+ Live Activity Spotlight +

+

+ See who's here and who's connecting +

+
+ +
+ {/* Network Graph */} +
+ + {/* Connection lines */} + + {effectiveConnections.map((conn, i) => { + const fromPos = positions.get(conn.from); + const toPos = positions.get(conn.to); + if (!fromPos || !toPos) return null; + return ( + + ); + })} + + {/* Participant bubbles */} + {participants.map((p, i) => { + const pos = positions.get(p.participantId); + if (!pos) return null; + const isHovered = hoveredId === p.participantId; + return ( + setHoveredId(p.participantId)} + onMouseLeave={() => setHoveredId(null)} + style={{ cursor: "pointer" }} + > + {/* Glow on hover */} + {isHovered && ( + + )} + + + {p.name.charAt(0).toUpperCase()} + + {/* Tooltip */} + {isHovered && ( + + + + {p.name} + + + )} + + ); + })} + + +
+ + {/* Right: Activity + Leaderboard */} +
+ {/* Activity Feed */} +
+

+ Recent Activity +

+
+ {effectiveActivity.length === 0 ? ( +

No activity yet

+ ) : ( + effectiveActivity.map((item) => ( +
+ +
+ {item.type === "joined" ? ( + + {item.participantName} joined + + ) : ( + + {item.fromName} connected with{" "} + {item.toName} + + )} +

+ {formatTimeAgo(item.timestamp)} +

+
+
+ )) + )} +
+
+ + {/* Leaderboard */} +
+

+ Connection Leaderboard +

+
+ {leaderboard.map((entry, i) => ( +
+ + {i + 1} + + + {entry.name} + + + {entry.connections} {entry.connections === 1 ? "link" : "links"} + +
+ ))} +
+
+
+
+
+ ); +} diff --git a/shatter-web/src/components/Hero.tsx b/shatter-web/src/components/Hero.tsx index e581684..cbb921c 100644 --- a/shatter-web/src/components/Hero.tsx +++ b/shatter-web/src/components/Hero.tsx @@ -1,5 +1,6 @@ import React, { useState, useEffect, useRef } from 'react'; import QRCard from './QRCard'; +import { ChevronDownIcon } from './icons'; interface HeroProps { qrPayload?: string; @@ -229,19 +230,7 @@ const Hero: React.FC = ({ qrPayload = "hello" }) => { {/* Scroll Indicator */}
- - - +
{/* Custom Animations */} diff --git a/shatter-web/src/components/Navbar.tsx b/shatter-web/src/components/Navbar.tsx index 5d6e5b3..38e634c 100644 --- a/shatter-web/src/components/Navbar.tsx +++ b/shatter-web/src/components/Navbar.tsx @@ -2,6 +2,7 @@ import { useState, useEffect } from "react"; import { useNavigate } from "react-router-dom"; import logo from "../assets/ShatterLogo_White.png"; +import { BarsIcon, XIcon } from "./icons"; export default function Navbar() { const [isMenuOpen, setIsMenuOpen] = useState(false); @@ -194,35 +195,9 @@ export default function Navbar() { aria-label="Toggle menu" > {isMenuOpen ? ( - /* Close Icon */ - - - + ) : ( - /* Hamburger Icon */ - - - + )} diff --git a/shatter-web/src/components/icons/BarsIcon.tsx b/shatter-web/src/components/icons/BarsIcon.tsx new file mode 100644 index 0000000..12787cc --- /dev/null +++ b/shatter-web/src/components/icons/BarsIcon.tsx @@ -0,0 +1,20 @@ +import type { IconProps } from "./types"; + +export function BarsIcon({ className = "w-4 h-4", ...props }: IconProps) { + return ( + + + + ); +} diff --git a/shatter-web/src/components/icons/CalendarIcon.tsx b/shatter-web/src/components/icons/CalendarIcon.tsx new file mode 100644 index 0000000..ed1b0f9 --- /dev/null +++ b/shatter-web/src/components/icons/CalendarIcon.tsx @@ -0,0 +1,20 @@ +import type { IconProps } from "./types"; + +export function CalendarIcon({ className = "w-4 h-4", ...props }: IconProps) { + return ( + + + + ); +} diff --git a/shatter-web/src/components/icons/ChevronDownIcon.tsx b/shatter-web/src/components/icons/ChevronDownIcon.tsx new file mode 100644 index 0000000..53ef640 --- /dev/null +++ b/shatter-web/src/components/icons/ChevronDownIcon.tsx @@ -0,0 +1,20 @@ +import type { IconProps } from "./types"; + +export function ChevronDownIcon({ className = "w-4 h-4", ...props }: IconProps) { + return ( + + + + ); +} diff --git a/shatter-web/src/components/icons/ClipboardCopyIcon.tsx b/shatter-web/src/components/icons/ClipboardCopyIcon.tsx new file mode 100644 index 0000000..9f625c0 --- /dev/null +++ b/shatter-web/src/components/icons/ClipboardCopyIcon.tsx @@ -0,0 +1,20 @@ +import type { IconProps } from "./types"; + +export function ClipboardCopyIcon({ className = "w-4 h-4", ...props }: IconProps) { + return ( + + + + ); +} diff --git a/shatter-web/src/components/icons/ClipboardIcon.tsx b/shatter-web/src/components/icons/ClipboardIcon.tsx new file mode 100644 index 0000000..cc94921 --- /dev/null +++ b/shatter-web/src/components/icons/ClipboardIcon.tsx @@ -0,0 +1,20 @@ +import type { IconProps } from "./types"; + +export function ClipboardIcon({ className = "w-4 h-4", ...props }: IconProps) { + return ( + + + + ); +} diff --git a/shatter-web/src/components/icons/ClockIcon.tsx b/shatter-web/src/components/icons/ClockIcon.tsx new file mode 100644 index 0000000..da14c3b --- /dev/null +++ b/shatter-web/src/components/icons/ClockIcon.tsx @@ -0,0 +1,20 @@ +import type { IconProps } from "./types"; + +export function ClockIcon({ className = "w-4 h-4", ...props }: IconProps) { + return ( + + + + ); +} diff --git a/shatter-web/src/components/icons/GoogleIcon.tsx b/shatter-web/src/components/icons/GoogleIcon.tsx new file mode 100644 index 0000000..1927800 --- /dev/null +++ b/shatter-web/src/components/icons/GoogleIcon.tsx @@ -0,0 +1,24 @@ +import type { IconProps } from "./types"; + +export function GoogleIcon({ className = "w-5 h-5", ...props }: IconProps) { + return ( + + + + + + + ); +} diff --git a/shatter-web/src/components/icons/InformationCircleIcon.tsx b/shatter-web/src/components/icons/InformationCircleIcon.tsx new file mode 100644 index 0000000..4d17882 --- /dev/null +++ b/shatter-web/src/components/icons/InformationCircleIcon.tsx @@ -0,0 +1,20 @@ +import type { IconProps } from "./types"; + +export function InformationCircleIcon({ className = "w-4 h-4", ...props }: IconProps) { + return ( + + + + ); +} diff --git a/shatter-web/src/components/icons/KeyIcon.tsx b/shatter-web/src/components/icons/KeyIcon.tsx new file mode 100644 index 0000000..da05a75 --- /dev/null +++ b/shatter-web/src/components/icons/KeyIcon.tsx @@ -0,0 +1,20 @@ +import type { IconProps } from "./types"; + +export function KeyIcon({ className = "w-4 h-4", ...props }: IconProps) { + return ( + + + + ); +} diff --git a/shatter-web/src/components/icons/PlusIcon.tsx b/shatter-web/src/components/icons/PlusIcon.tsx new file mode 100644 index 0000000..0ebfdeb --- /dev/null +++ b/shatter-web/src/components/icons/PlusIcon.tsx @@ -0,0 +1,20 @@ +import type { IconProps } from "./types"; + +export function PlusIcon({ className = "w-4 h-4", ...props }: IconProps) { + return ( + + + + ); +} diff --git a/shatter-web/src/components/icons/SearchIcon.tsx b/shatter-web/src/components/icons/SearchIcon.tsx new file mode 100644 index 0000000..c1a2cd6 --- /dev/null +++ b/shatter-web/src/components/icons/SearchIcon.tsx @@ -0,0 +1,20 @@ +import type { IconProps } from "./types"; + +export function SearchIcon({ className = "w-4 h-4", ...props }: IconProps) { + return ( + + + + ); +} diff --git a/shatter-web/src/components/icons/UsersIcon.tsx b/shatter-web/src/components/icons/UsersIcon.tsx new file mode 100644 index 0000000..f506fa5 --- /dev/null +++ b/shatter-web/src/components/icons/UsersIcon.tsx @@ -0,0 +1,20 @@ +import type { IconProps } from "./types"; + +export function UsersIcon({ className = "w-4 h-4", ...props }: IconProps) { + return ( + + + + ); +} diff --git a/shatter-web/src/components/icons/XIcon.tsx b/shatter-web/src/components/icons/XIcon.tsx new file mode 100644 index 0000000..05fe53b --- /dev/null +++ b/shatter-web/src/components/icons/XIcon.tsx @@ -0,0 +1,20 @@ +import type { IconProps } from "./types"; + +export function XIcon({ className = "w-4 h-4", ...props }: IconProps) { + return ( + + + + ); +} diff --git a/shatter-web/src/components/icons/index.ts b/shatter-web/src/components/icons/index.ts new file mode 100644 index 0000000..ffc696b --- /dev/null +++ b/shatter-web/src/components/icons/index.ts @@ -0,0 +1,13 @@ +export { BarsIcon } from "./BarsIcon"; +export { CalendarIcon } from "./CalendarIcon"; +export { ChevronDownIcon } from "./ChevronDownIcon"; +export { ClipboardCopyIcon } from "./ClipboardCopyIcon"; +export { ClipboardIcon } from "./ClipboardIcon"; +export { ClockIcon } from "./ClockIcon"; +export { GoogleIcon } from "./GoogleIcon"; +export { InformationCircleIcon } from "./InformationCircleIcon"; +export { KeyIcon } from "./KeyIcon"; +export { PlusIcon } from "./PlusIcon"; +export { SearchIcon } from "./SearchIcon"; +export { UsersIcon } from "./UsersIcon"; +export { XIcon } from "./XIcon"; diff --git a/shatter-web/src/components/icons/types.ts b/shatter-web/src/components/icons/types.ts new file mode 100644 index 0000000..a580585 --- /dev/null +++ b/shatter-web/src/components/icons/types.ts @@ -0,0 +1,6 @@ +import type { SVGProps } from "react"; + +export interface IconProps extends SVGProps { + className?: string; + size?: number; +} diff --git a/shatter-web/src/hooks/useEventData.ts b/shatter-web/src/hooks/useEventData.ts index 79d696e..c583998 100644 --- a/shatter-web/src/hooks/useEventData.ts +++ b/shatter-web/src/hooks/useEventData.ts @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import type { Participant } from "../types/participant"; interface EventDetails { @@ -11,6 +11,7 @@ interface EventDetails { maxParticipant: number; currentState: string; participantIds: Participant[]; + createdBy?: string; } interface EventResponse { @@ -25,17 +26,16 @@ export function useEventData(joinCode: string | undefined) { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - useEffect(() => { + const fetchEvent = useCallback((signal?: AbortSignal) => { if (!joinCode) { setLoading(false); return; } - setLoading(true); setError(""); - fetch(`${import.meta.env.VITE_API_URL}/events/event/${joinCode}`, { cache: "no-store", + signal, }) .then((res) => { if (!res.ok) { @@ -45,9 +45,10 @@ export function useEventData(joinCode: string | undefined) { }) .then((data: EventResponse) => { if (data.success && data.event) { - setEventId(data.event._id); - setEventDetails(data.event); - setParticipants(data.event.participantIds); + const ev = data.event; + setEventId(ev._id); + setEventDetails(ev); + setParticipants(ev.participantIds || []); } else { setEventId(null); setEventDetails(null); @@ -55,6 +56,7 @@ export function useEventData(joinCode: string | undefined) { } }) .catch((err) => { + if (err?.name === "AbortError") return; console.error("Error fetching event:", err); setEventId(null); setEventDetails(null); @@ -63,5 +65,14 @@ export function useEventData(joinCode: string | undefined) { .finally(() => setLoading(false)); }, [joinCode]); - return { eventId, eventDetails, participants, loading, error }; + useEffect(() => { + if (!joinCode) return; + const controller = new AbortController(); + fetchEvent(controller.signal); + return () => controller.abort(); + }, [joinCode, fetchEvent]); + + const refetch = () => fetchEvent(); + + return { eventId, eventDetails, participants, loading, error, refetch }; } diff --git a/shatter-web/src/pages/CreateEventPage.tsx b/shatter-web/src/pages/CreateEventPage.tsx index effc0b2..6cce9c6 100644 --- a/shatter-web/src/pages/CreateEventPage.tsx +++ b/shatter-web/src/pages/CreateEventPage.tsx @@ -1,7 +1,7 @@ import React, { useState } from "react"; -import { useEffect } from "react"; import Navbar from "../components/Navbar"; import Footer from "../components/Footer"; +import BingoTable from "../components/BingoTable"; import { CreateEvent } from "../service/CreateEvent"; import { createBingoGame } from "../service/BingoGame"; // ✅ NEW import { useNavigate } from "react-router-dom"; @@ -16,7 +16,10 @@ function CreateEventPage() { ); // ✅ NEW: Name Bingo selection + const createEmptyGrid = (size: number) => Array.from({ length: size }, () => Array(size).fill("")); const [nameBingoSelected, setNameBingoSelected] = useState(false); + const [bingoGrid, setBingoGrid] = useState(createEmptyGrid(3)); + const [bingoDescription, setBingoDescription] = useState(""); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); @@ -28,6 +31,26 @@ function CreateEventPage() { setLoading(true); setError(null); + // ✅ Validate main form + if (!name.trim() || !description.trim() || !startDate || !maxParticipant || maxParticipant <= 0) { + throw new Error("Please fill in all required fields."); + } + + // ✅ Validate bingo (if selected) + if (nameBingoSelected) { + const hasEmptyCells = bingoGrid.some(row => + row.some(cell => !cell.trim()) + ); + + if (hasEmptyCells) { + throw new Error("Please fill in all bingo grid cells."); + } + + if (!bingoDescription.trim()) { + throw new Error("Please add a bingo description."); + } + } + // 1️⃣ Create event const { eventId, joinCode } = await CreateEvent({ name, @@ -37,19 +60,36 @@ function CreateEventPage() { maxParticipants: maxParticipant ?? 0, }); - // 2️⃣ If Name Bingo selected, create bingo game + // 2️⃣ Create bingo (ONLY ONE CALL) if (nameBingoSelected) { const token = localStorage.getItem("token"); - if (!token) { - throw new Error("Authentication required to create bingo game."); - } + if (!token) throw new Error("Authentication required."); - await createBingoGame(eventId, token); + const response = await fetch(`${import.meta.env.VITE_API_URL}/bingo/createBingo`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + _eventId: eventId, + description: bingoDescription, + grid: bingoGrid, + }), + }); + + if (!response.ok) { + const errData = await response.json().catch(() => ({})); + throw new Error(errData.message || "Failed to create bingo game."); + } } - // 3️⃣ Navigate to the newly created event page + // 3️⃣ Navigate + setLoading(false); navigate(`/events/${joinCode}`); + } catch (err: any) { + console.error("Create event error:", err); setError(err.message || "Failed to create event. Please try again."); setLoading(false); } @@ -74,6 +114,11 @@ function CreateEventPage() { maxParticipant !== undefined && maxParticipant > 0; + const token = localStorage.getItem("token"); + if (!token) { + navigate("/login"); + return; + } return (
setMaxParticipant(Number(e.target.value))} + onChange={(e) => { + const val = e.target.value; + setMaxParticipant(val === "" ? undefined : Number(val)); + }} min="1" disabled={loading} /> @@ -239,6 +287,31 @@ function CreateEventPage() {
+ {/*Icebreaker Properties*/} + {nameBingoSelected && ( +
+
+ + setBingoDescription(e.target.value)} + className="w-full p-3 rounded bg-white border text-black" + /> +
+ + { + const newGrid = bingoGrid.map(r => [...r]); // ✅ deep copy + newGrid[row][col] = value; + setBingoGrid(newGrid); + }} + /> +
+ )} + {/* Action Buttons */}
@@ -447,9 +435,7 @@ function DashboardPage() {
- - - + {formatDate(event.startDate)}
@@ -606,9 +592,7 @@ function DashboardPage() {
- - - + Start Date

{formatDate(selectedEvent.startDate)}

@@ -617,9 +601,7 @@ function DashboardPage() {
- - - + End Date

{formatDate(selectedEvent.endDate)}

@@ -628,9 +610,7 @@ function DashboardPage() {
- - - + Participants

@@ -640,9 +620,7 @@ function DashboardPage() {

- - - + Join Code

{selectedEvent.joinCode}

@@ -656,15 +634,17 @@ function DashboardPage() { {!selectedIcebreaker && (
-
@@ -731,40 +703,19 @@ function DashboardPage() {
- - -
- {bingoGrid.map((row, rowIndex) => - row.map((cell, colIndex) => ( -
- handleBingoGridChange(rowIndex, colIndex, e.target.value)} - placeholder={`${rowIndex + 1}-${colIndex + 1}`} - className="w-full h-24 p-2 rounded-lg bg-white/5 border border-white/20 text-white text-xs placeholder-white/30 focus:outline-none focus:border-[#4DC4FF] focus:ring-2 focus:ring-[#4DC4FF]/20 transition-all font-body resize-none" - style={{ - fontSize: '0.75rem', - lineHeight: '1.2', - }} - /> -
- {rowIndex * 5 + colIndex + 1} -
-
- )) - )} -
+ { + const newGrid = bingoGrid.map(r => [...r]); + newGrid[row][col] = value; + setBingoGrid(newGrid); + }} + />
- - - +

Tips for creating good bingo questions:

    @@ -777,6 +728,26 @@ function DashboardPage() {
+ {bingoSaveMessage && ( +
+ {bingoSaveMessage.text} + +
+ )} +
+ +
+ )}
+ {/* Live Activity Spotlight */} +
+ +
+ {/* Main Content Grid */}
{/* Left Column - Event Details */} @@ -188,20 +316,7 @@ export default function EventPage() { {/* Start Date */}
- {/* Calendar Icon */} - - - +

Start

@@ -217,20 +332,7 @@ export default function EventPage() { {/* End Date */}
- {/* Calendar Icon */} - - - +

End

@@ -328,19 +430,7 @@ export default function EventPage() { className="w-full px-4 py-2.5 rounded-full font-semibold text-white hover:opacity-90 transition-opacity shadow-lg font-body flex items-center justify-center gap-2" style={{ backgroundColor: "#4DC4FF" }} > - - - + Copy Join Code
@@ -364,16 +454,23 @@ export default function EventPage() { Bingo game associated with this event.

-
+
{bingoGame.grid.flat().map((cell, index) => (
- {cell} + {cell}
))}
diff --git a/shatter-web/src/pages/LoginPage.tsx b/shatter-web/src/pages/LoginPage.tsx index fefc6ec..7caf7ef 100644 --- a/shatter-web/src/pages/LoginPage.tsx +++ b/shatter-web/src/pages/LoginPage.tsx @@ -1,5 +1,6 @@ import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; +import { GoogleIcon } from '../components/icons'; export default function LoginPage() { const navigate = useNavigate(); @@ -280,12 +281,7 @@ export default function LoginPage() { type="button" className="w-full py-3 rounded-lg border border-white/20 bg-white/5 text-white hover:bg-white/10 transition-colors font-body flex items-center justify-center gap-3" > - - - - - - + Continue with Google
diff --git a/shatter-web/src/service/BingoGame.ts b/shatter-web/src/service/BingoGame.ts index 63226c3..3191de8 100644 --- a/shatter-web/src/service/BingoGame.ts +++ b/shatter-web/src/service/BingoGame.ts @@ -1,7 +1,6 @@ // services/BingoGame.ts -const CREATE_BINGO_URL = - "https://techstart-shatter-backend.vercel.app/api/bingo/createBingo"; +const BASE_URL = import.meta.env.VITE_API_URL ?? "https://techstart-shatter-backend.vercel.app/api"; export interface BingoGame { _id: string; @@ -10,11 +9,18 @@ export interface BingoGame { grid: string[][]; } +export async function getBingo(eventId: string): Promise { + const res = await fetch(`${BASE_URL}/bingo/getBingo/${eventId}`); + const data = await res.json(); + if (!res.ok || !data?.bingo) return null; + return data.bingo; +} + export async function createBingoGame( eventId: string, token: string ): Promise { - const res = await fetch(CREATE_BINGO_URL, { + const res = await fetch(`${BASE_URL}/bingo/createBingo`, { method: "POST", headers: { "Content-Type": "application/json", @@ -24,11 +30,9 @@ export async function createBingoGame( _eventId: eventId, // ✅ THIS IS REQUIRED description: "Name Bingo", grid: [ - ["A1", "B1", "C1", "D1", "E1"], - ["A2", "B2", "C2", "D2", "E2"], - ["A3", "B3", "C3", "D3", "E3"], - ["A4", "B4", "C4", "D4", "E4"], - ["A5", "B5", "C5", "D5", "E5"], + ["A1", "B1", "C1"], + ["A2", "B2", "C2"], + ["A3", "B3", "C3"] ], }), });