diff --git a/frontend/src/App.css b/frontend/src/App.css index b193ad8..fe4ae8f 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -34,47 +34,108 @@ transition: color 0.2s; } -.nav-link:hover, .nav-link.active { - color: var(--accent-color); +.nav-link.active { + color: var(--text-color); + border-bottom: 2px solid var(--accent-color); } -.form-group { +/* Toast Styles */ +.toast-container { + position: fixed; + top: 2rem; + right: 2rem; + z-index: 1000; display: flex; flex-direction: column; - gap: 0.5rem; - margin-bottom: 1rem; + gap: 1rem; } -.form-group label { - font-size: 0.9rem; +.toast { + display: flex; + align-items: center; + gap: 1rem; + padding: 1rem 1.5rem; + border-radius: 12px; + background-color: var(--secondary-bg); + border: 1px solid var(--border-color); + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3); + min-width: 300px; + animation: slideInRight 0.3s ease forwards; +} + +.toast-success { border-left: 4px solid #4caf50; } +.toast-error { border-left: 4px solid #f44336; } +.toast-info { border-left: 4px solid var(--accent-color); } + +.toast-icon { font-size: 1.2rem; } +.toast-message { flex: 1; font-weight: 500; } +.toast-close { + background: none; + border: none; color: var(--text-muted); + cursor: pointer; + font-size: 1.5rem; + padding: 0; + line-height: 1; } -.btn-secondary { - background-color: var(--secondary-alt-bg); +@keyframes slideInRight { + from { transform: translateX(100%); opacity: 0; } + to { transform: translateX(0); opacity: 1; } +} + +/* Loading Bar */ +.loading-bar { + position: fixed; + top: 0; + left: 0; + height: 3px; + background: linear-gradient(90deg, var(--accent-color), #4da3ff); + z-index: 2000; + transition: width 0.3s ease; +} + +/* Theme Switcher */ +.theme-switcher { + display: flex; + gap: 0.5rem; + background: var(--secondary-alt-bg); + padding: 0.25rem; + border-radius: 20px; border: 1px solid var(--border-color); } -.btn-secondary:hover { - background-color: var(--border-color); -} - -.list-item { - background-color: var(--secondary-alt-bg); - padding: 1rem; - border-radius: 8px; - margin-bottom: 1rem; - border: 1px solid var(--border-color); +.theme-btn:not(.game-btn) { + width: 32px; + height: 32px; + border-radius: 50%; + border: 2px solid transparent; + cursor: pointer; transition: transform 0.2s; + padding: 0; + display: flex; + align-items: center; + justify-content: center; } -.list-item:hover { - transform: translateY(-2px); - border-color: var(--accent-color); +.theme-btn:hover { transform: scale(1.1); } +.theme-btn.active { border-color: var(--text-color); } + +.theme-default { background: #23283d; } +.theme-blackhole { background: #000000; } +.theme-star { background: #0a0a2a; } +.theme-ball { background: #1a1a1a; } +.theme-reflect { background: #333333; } +.theme-clouds { background: #23283d; } + +.game-entry { + gap: 0.5rem; + background-color: var(--secondary-alt-bg); + margin-bottom: 10px; + border-radius: 5px; + padding: 1rem; } -.grid-container { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); - gap: 1.5rem; +.game-entry:hover { + background-color: var(--primary-bg); } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e8e2643..e0baa2d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -7,9 +7,17 @@ import { GameList } from "./GameList"; import { GameFilter } from "./GameFilter"; import { GameDetails } from "./GameDetails"; import { ShaderBackground } from "./ShaderBackground"; -import { BrowserRouter, Routes, Route, Link } from "react-router-dom"; +import { BrowserRouter, Routes, Route, NavLink } from "react-router-dom"; import "./App.css"; import { apiFetch } from "./api"; +import { Toast } from "./Toast"; +import type { ToastType } from "./Toast"; + +interface ToastMessage { + id: number; + message: string; + type: ToastType; +} function App() { const [people, setPeople] = useState([]); @@ -17,6 +25,8 @@ function App() { localStorage.getItem("token") || "" ); const [theme, setTheme] = useState("default"); + const [toasts, setToasts] = useState([]); + const [isLoading, setIsLoading] = useState(false); useEffect(() => { if (theme !== "default") { @@ -26,8 +36,18 @@ function App() { } }, [theme]); + const addToast = (message: string, type: ToastType = "info") => { + const id = Date.now(); + setToasts((prev) => [...prev, { id, message, type }]); + }; + + const removeToast = (id: number) => { + setToasts((prev) => prev.filter((t) => t.id !== id)); + }; + const fetchPeople = () => { if (!token) return; + setIsLoading(true); apiFetch("/api") .then((res) => res.arrayBuffer()) @@ -35,7 +55,11 @@ function App() { const list = PersonListProto.decode(new Uint8Array(buffer)); setPeople(list.person); }) - .catch((err) => console.error("Failed to fetch people:", err)); + .catch((err) => { + console.error("Failed to fetch people:", err); + addToast("Failed to fetch people list", "error"); + }) + .finally(() => setIsLoading(false)); }; useEffect(() => { @@ -46,49 +70,80 @@ function App() { const handleLogin = (newToken: string) => { setToken(newToken); localStorage.setItem("token", newToken); + addToast("Welcome back!", "success"); }; const handleLogout = () => { setToken(""); setPeople([]); localStorage.removeItem("token"); + addToast("Logged out successfully", "info"); }; if (!token) { return ; } + const themes = [ + { id: "default", label: "Default", icon: "🏠" }, + { id: "blackhole", label: "Blackhole", icon: "🕳️" }, + { id: "star", label: "Star", icon: "⭐" }, + { id: "ball", label: "Ball", icon: "⚽" }, + { id: "reflect", label: "Reflect", icon: "🪞" }, + { id: "clouds", label: "Clouds", icon: "☁️" }, + ]; + return ( + {isLoading &&
} +
+ {toasts.map((toast) => ( + removeToast(toast.id)} + /> + ))} +
- - People List - - + + People + + Games - - + + Filter - + +
+ +
+
+ {themes.map((t) => ( + + ))} +
+
- -
- + } /> - } /> + } /> } /> } /> } /> diff --git a/frontend/src/GameFilter.css b/frontend/src/GameFilter.css new file mode 100644 index 0000000..6281398 --- /dev/null +++ b/frontend/src/GameFilter.css @@ -0,0 +1,14 @@ +.gamefilter-entry { + border-radius: 5px; + border: 1px solid var(--border-color); + background-color: var(--secondary-alt-bg); + margin-bottom: 10px; + padding: 10px; + width: 30%; + text-align: center; + user-select: none; +} + +.gamefilter-entry:hover { + background-color: var(--primary-bg); +} \ No newline at end of file diff --git a/frontend/src/GameFilter.tsx b/frontend/src/GameFilter.tsx index 774b17f..03903e6 100644 --- a/frontend/src/GameFilter.tsx +++ b/frontend/src/GameFilter.tsx @@ -7,6 +7,7 @@ import { import { apiFetch } from "./api"; import { Link } from "react-router-dom"; import { GameImage } from "./GameImage"; +import "./GameFilter.css" export function GameFilter() { const [people, setPeople] = useState([]); @@ -117,28 +118,27 @@ export function GameFilter() {

Select People

-
+
{people.map((person) => (
togglePerson(person.name)} > -
- togglePerson(person.name)} - style={{ cursor: "pointer" }} - /> +
{person.name}
- ✓ {gameToPositive.get(game)!.size} selected would play + {gameToPositive.get(game)!.size} selected + would play
{selectedPeople.size - gameToPositive.get(game)!.size > 0 && ( @@ -193,8 +194,13 @@ export function GameFilter() { marginTop: "0.3rem", }} > - ? {selectedPeople.size - gameToPositive.get(game)!.size}{" "} - {(selectedPeople.size - gameToPositive.get(game)!.size) > 1 ? "are" : "is"} neutral + ?{" "} + {selectedPeople.size - gameToPositive.get(game)!.size}{" "} + {selectedPeople.size - gameToPositive.get(game)!.size > + 1 + ? "are" + : "is"}{" "} + neutral
)}
@@ -203,9 +209,22 @@ export function GameFilter() { ))} ) : ( -

- No games found where all selected people would play -

+
+
🔍
+

No games found where all selected people would play.

+

+ Try selecting fewer people or adding more opinions! +

+
)}
)} diff --git a/frontend/src/GameList.tsx b/frontend/src/GameList.tsx index 0817f70..1bfb1a5 100644 --- a/frontend/src/GameList.tsx +++ b/frontend/src/GameList.tsx @@ -11,8 +11,13 @@ import { import { Link, useLocation } from "react-router-dom"; import { apiFetch, get_auth_status } from "./api"; import { GameImage } from "./GameImage"; +import type { ToastType } from "./Toast"; -export function GameList() { +interface Props { + onShowToast?: (message: string, type?: ToastType) => void; +} + +export function GameList({ onShowToast }: Props) { const [games, setGames] = useState([]); const [title, setTitle] = useState(""); const [source, setSource] = useState(Source.STEAM); @@ -20,7 +25,6 @@ export function GameList() { const [maxPlayers, setMaxPlayers] = useState(1); const [price, setPrice] = useState(0); const [remoteId, setRemoteId] = useState(0); - const [message, setMessage] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); const [opinions, setOpinions] = useState([]); @@ -96,22 +100,19 @@ export function GameList() { }); if (res.ok) { - setMessage("success"); + onShowToast?.("Game added successfully!", "success"); setTitle(""); setMinPlayers(1); setMaxPlayers(1); setPrice(0); setRemoteId(0); fetchGames(); - setTimeout(() => setMessage(""), 3000); } else { - setMessage("error"); - setTimeout(() => setMessage(""), 3000); + onShowToast?.("Failed to add game. Please try again.", "error"); } } catch (err) { console.error(err); - setMessage("error"); - setTimeout(() => setMessage(""), 3000); + onShowToast?.("An error occurred while adding the game.", "error"); } finally { setIsSubmitting(false); } @@ -128,7 +129,8 @@ export function GameList() { }; const formHeaderStyles: React.CSSProperties = { - background: "linear-gradient(135deg, var(--accent-color) 0%, var(--secondary-accent) 100%)", + background: + "linear-gradient(135deg, var(--accent-color) 0%, var(--secondary-accent) 100%)", padding: "1.5rem 2rem", display: "flex", alignItems: "center", @@ -195,7 +197,8 @@ export function GameList() { const submitButtonStyles: React.CSSProperties = { width: "100%", padding: "1rem", - background: "linear-gradient(135deg, var(--accent-color) 0%, var(--secondary-accent) 100%)", + background: + "linear-gradient(135deg, var(--accent-color) 0%, var(--secondary-accent) 100%)", border: "none", borderRadius: "12px", color: "white", @@ -211,26 +214,6 @@ export function GameList() { transform: isSubmitting ? "none" : undefined, }; - const messageStyles: React.CSSProperties = { - padding: "1rem", - borderRadius: "12px", - marginBottom: "1.5rem", - display: "flex", - alignItems: "center", - gap: "0.75rem", - animation: "slideIn 0.3s ease", - backgroundColor: - message === "success" - ? "rgba(76, 175, 80, 0.15)" - : "rgba(244, 67, 54, 0.15)", - border: `1px solid ${ - message === "success" - ? "rgba(76, 175, 80, 0.3)" - : "rgba(244, 67, 54, 0.3)" - }`, - color: message === "success" ? "#4caf50" : "#f44336", - }; - return (