From 6a8b909afbe1560be95f7ad0a3e19cfe4717aec6 Mon Sep 17 00:00:00 2001 From: FifthWit Date: Thu, 14 Aug 2025 14:01:01 -0500 Subject: Switched to tailwind/vite --- frontend/src/App.css | 229 ++++++++++++++++- frontend/src/api/Api.ts | 2 +- frontend/src/components/ConfirmDialog.tsx | 16 +- frontend/src/components/GameCategory.tsx | 15 +- frontend/src/components/GameEntry.tsx | 27 +- frontend/src/components/Leaderboards.tsx | 6 +- frontend/src/components/Login.tsx | 53 ++-- frontend/src/components/ModMenu.tsx | 1 - frontend/src/components/Sidebar.tsx | 386 ++++++++++++++-------------- frontend/src/components/Summary.tsx | 1 - frontend/src/components/UploadRunDialog.tsx | 1 - frontend/src/images/Images.tsx | 4 +- frontend/src/images/svgs/steam.tsx | 7 + frontend/src/pages/About.tsx | 4 +- frontend/src/pages/Games.tsx | 32 +-- frontend/src/pages/Homepage.tsx | 12 +- frontend/src/pages/Maplist.tsx | 131 +++++----- frontend/src/pages/Maps.tsx | 15 +- frontend/src/pages/Profile.tsx | 1 - frontend/src/pages/Rules.tsx | 4 +- frontend/src/pages/User.tsx | 1 - 21 files changed, 582 insertions(+), 366 deletions(-) create mode 100644 frontend/src/images/svgs/steam.tsx (limited to 'frontend/src') diff --git a/frontend/src/App.css b/frontend/src/App.css index 14a9972..a4c058b 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1,3 +1,54 @@ +@import url('https://fonts.googleapis.com/css2?family=Barlow+Condensed:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Montserrat+Alternates:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap'); +@import "tailwindcss"; + +@theme { + --color-rosewater: #f2d5cf; + --color-flamingo: #eebebe; + --color-pink: #f4b8e4; + --color-mauve: #ca9ee6; + --color-red: #e78284; + --color-maroon: #ea999c; + --color-peach: #ef9f76; + --color-yellow: #e5c890; + --color-green: #a6d189; + --color-teal: #81c8be; + --color-sky: #99d1db; + --color-sapphire: #85c1dc; + --color-blue: #8caaee; + --color-lavender: #babbf1; + --color-text: #c6d0f5; + --color-subtext1: #b5bfe2; + --color-subtext0: #a5adce; + --color-overlay2: #949cbb; + --color-overlay1: #838ba7; + --color-overlay0: #737994; + --color-surface2: #626880; + --color-surface1: #51576d; + --color-surface0: #414559; + --color-base: #303446; + --color-mantle: #292c3c; + --color-crust: #232634; + + --color-primary: var(--color-mauve); + --color-secondary: var(--color-blue); + --color-accent: var(--color-peach); + --color-background: var(--color-base); + --color-surface: var(--color-surface0); + --color-muted: var(--color-overlay0); + --color-border: var(--color-surface2); + --color-input: var(--color-surface1); + --color-foreground: var(--color-text); + --color-success: var(--color-green); + --color-warning: var(--color-yellow); + --color-error: var(--color-red); + --color-info: var(--color-blue); + + --font-barlow-condensed-regular: 'BarlowCondensed-Regular'; + --font-barlow-condensed-bold: 'BarlowCondensed-Bold'; + --font-barlow-semicondensed-regular: 'BarlowSemiCondensed-Regular'; + --font-barlow-semicondensed-semibold: 'BarlowSemiCondensed-SemiBold'; +} + main { overflow: auto; overflow-x: hidden; @@ -10,33 +61,32 @@ main { padding-right: 30px; font-size: 40px; - font-family: BarlowSemiCondensed-Regular; - color: #cdcfdf; + font-family: var(--font-barlow-semicondensed-regular); + color: var(--color-text); } a { color: inherit; - width: fit-content; } body { overflow: hidden; - background-color: #141520; + background-color: var(--color-crust); margin: 0; } .loader { animation: loader 1.2s ease infinite; background-size: 400% 300%; - background-image: linear-gradient(-90deg, #202232 0%, #202232 25%, #2a2c41 50%, #202232 75%, #202232 100%); + background-image: linear-gradient(-90deg, var(--color-mantle) 0%, var(--color-mantle) 25%, var(--color-surface1) 50%, var(--color-mantle) 75%, var(--color-mantle) 100%); user-select: none; } .loader-text { animation: loader 1.2s ease infinite; background-size: 400% 300%; - background-image: linear-gradient(-90deg, #202232 0%, #202232 25%, #2a2c41 50%, #202232 75%, #202232 100%); + background-image: linear-gradient(-90deg, var(--color-mantle) 0%, var(--color-mantle) 25%, var(--color-surface1) 50%, var(--color-mantle) 75%, var(--color-mantle) 100%); user-select: none; color: #00000000; border-radius: 1000px; @@ -76,6 +126,173 @@ body { } } +/* Custom Tailwind utilities for Catppuccin Frappe theme */ +@layer utilities { + .bg-primary { + background-color: var(--color-primary); + } + + .bg-secondary { + background-color: var(--color-secondary); + } + + .bg-accent { + background-color: var(--color-accent); + } + + .bg-background { + background-color: var(--color-background); + } + + .bg-surface { + background-color: var(--color-surface); + } + + .bg-muted { + background-color: var(--color-muted); + } + + .text-primary { + color: var(--color-primary); + } + + .text-secondary { + color: var(--color-secondary); + } + + .text-accent { + color: var(--color-accent); + } + + .text-foreground { + color: var(--color-foreground); + } + + .text-muted { + color: var(--color-muted); + } + + .border-primary { + border-color: var(--color-primary); + } + + .border-secondary { + border-color: var(--color-secondary); + } + + .border-muted { + border-color: var(--color-border); + } + + .hover\:bg-primary:hover { + background-color: var(--color-primary); + } + + .hover\:bg-secondary:hover { + background-color: var(--color-secondary); + } + + .hover\:bg-surface:hover { + background-color: var(--color-surface); + } + + .hover\:text-primary:hover { + color: var(--color-primary); + } + + .focus\:ring-primary:focus { + --tw-ring-color: var(--color-primary); + } + + .triangle { + width: 0; + height: 0; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-bottom: 8px solid var(--color-foreground); + display: inline-block; + } + + .sidebar-button-selected { + background-color: var(--color-primary) !important; + color: var(--color-background) !important; + } + + .sidebar-button-deselected { + background-color: var(--color-surface) !important; + color: var(--color-foreground) !important; + } + + .profileboard-record { + background-color: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: 0.5rem; + padding: 0.75rem; + margin-bottom: 0.5rem; + transition: all 0.2s ease; + } + + .profileboard-record:hover { + background-color: var(--color-surface1); + } + + .difficulty-rating { + width: 20px; + height: 20px; + background-color: var(--color-muted); + border-radius: 50%; + margin: 0 2px; + display: inline-block; + } + + .nav-button { + background-color: var(--color-surface); + color: var(--color-foreground); + border: 1px solid var(--color-border); + border-radius: 0.5rem; + padding: 0.5rem 1rem; + transition: all 0.2s ease; + display: inline-flex; + align-items: center; + gap: 0.5rem; + text-decoration: none; + } + + .nav-button:hover { + background-color: var(--color-surface1); + } + + .record { + background-color: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: 0.5rem; + padding: 0.5rem; + margin: 0.25rem; + cursor: pointer; + transition: all 0.2s ease; + } + + .record:hover { + background-color: var(--color-surface1); + } + + .portal-count { + font-size: 3rem; + font-weight: bold; + color: var(--color-primary); + } + + .titles { + background-color: var(--color-accent); + color: var(--color-background); + padding: 0.25rem 0.5rem; + border-radius: 1rem; + font-size: 0.875rem; + margin-right: 0.5rem; + display: inline-block; + } +} + @font-face { font-family: 'BarlowCondensed-Bold'; src: local('BarlowCondensed-Bold'), url(./fonts/BarlowCondensed-Bold.ttf) format('truetype'); diff --git a/frontend/src/api/Api.ts b/frontend/src/api/Api.ts index 0e1658c..b782d17 100644 --- a/frontend/src/api/Api.ts +++ b/frontend/src/api/Api.ts @@ -91,7 +91,7 @@ export const API = { delete_map_summary(token, map_id, route_id), }; -const BASE_API_URL: string = "/api/v1/"; +const BASE_API_URL: string = "https://lp.portal2.sr/api/v1/" export function url(path: string): string { return BASE_API_URL + path; diff --git a/frontend/src/components/ConfirmDialog.tsx b/frontend/src/components/ConfirmDialog.tsx index c89d9ea..8f2ce7a 100644 --- a/frontend/src/components/ConfirmDialog.tsx +++ b/frontend/src/components/ConfirmDialog.tsx @@ -1,7 +1,5 @@ import React from "react"; -import "@css/Dialog.css"; - interface ConfirmDialogProps { title: string; subtitle: string; @@ -16,17 +14,17 @@ const ConfirmDialog: React.FC = ({ onCancel, }) => { return ( -
-
-
+
+
+
{title}
-
+
{subtitle}
-
- - +
+ +
diff --git a/frontend/src/components/GameCategory.tsx b/frontend/src/components/GameCategory.tsx index 2bb6d42..b18c9d9 100644 --- a/frontend/src/components/GameCategory.tsx +++ b/frontend/src/components/GameCategory.tsx @@ -2,7 +2,6 @@ import React from "react"; import { Link } from "react-router-dom"; import { Game, GameCategoryPortals } from "@customTypes/Game"; -import "@css/Games.css"; interface GameCategoryProps { game: Game; @@ -12,18 +11,12 @@ interface GameCategoryProps { const GameCategory: React.FC = ({ cat, game }) => { return ( - {cat.category.name} - -
- - {cat.portal_count} - -
+

{cat.category.name}

+
+

{cat.portal_count}

); }; diff --git a/frontend/src/components/GameEntry.tsx b/frontend/src/components/GameEntry.tsx index 04c3483..f8fd179 100644 --- a/frontend/src/components/GameEntry.tsx +++ b/frontend/src/components/GameEntry.tsx @@ -2,7 +2,6 @@ import React from "react"; import { Link } from "react-router-dom"; import { Game, GameCategoryPortals } from "@customTypes/Game"; -import "@css/Games.css"; import GameCategory from "@components/GameCategory"; @@ -18,23 +17,25 @@ const GameEntry: React.FC = ({ game }) => { }, [game.category_portals]); return ( - -
-
+ +
+
- - {game.name} + + {game.name}
-
- {catInfo.map((cat, index) => { - return ( - - ); - })} +
+
+ {catInfo.map((cat, index) => { + return ( + + ); + })} +
diff --git a/frontend/src/components/Leaderboards.tsx b/frontend/src/components/Leaderboards.tsx index b388aba..99481a2 100644 --- a/frontend/src/components/Leaderboards.tsx +++ b/frontend/src/components/Leaderboards.tsx @@ -36,7 +36,7 @@ const Leaderboards: React.FC = ({ mapID }) => { return (

- Map is not available for competitive boards. + Loading...

); @@ -195,6 +195,7 @@ const Leaderboards: React.FC = ({ mapID }) => { filter: "hue-rotate(160deg) contrast(60%) saturate(1000%)", }} + className="w-6 h-6 mx-4" /> @@ -227,7 +229,7 @@ const Leaderboards: React.FC = ({ mapID }) => { (window.location.href = `/api/v1/demos?uuid=${r.demo_id}`) } > - download + download ) diff --git a/frontend/src/components/Login.tsx b/frontend/src/components/Login.tsx index 1858c48..ba85aeb 100644 --- a/frontend/src/components/Login.tsx +++ b/frontend/src/components/Login.tsx @@ -1,18 +1,18 @@ import React from "react"; import { Link, useNavigate } from "react-router-dom"; -import { ExitIcon, UserIcon, LoginIcon } from "@images/Images"; +import { ExitIcon, UserIcon, LoginIcon } from "../images/Images"; import { UserProfile } from "@customTypes/Profile"; import { API } from "@api/Api"; -import "@css/Login.css"; interface LoginProps { setToken: React.Dispatch>; profile?: UserProfile; setProfile: React.Dispatch>; + isOpen: boolean; } -const Login: React.FC = ({ setToken, profile, setProfile }) => { +const Login: React.FC = ({ setToken, profile, setProfile, isOpen }) => { const navigate = useNavigate(); const _login = () => { @@ -32,16 +32,16 @@ const Login: React.FC = ({ setToken, profile, setProfile }) => { <> {profile.profile ? ( <> - - - @@ -49,16 +49,16 @@ const Login: React.FC = ({ setToken, profile, setProfile }) => { ) : ( <> - - - @@ -67,11 +67,28 @@ const Login: React.FC = ({ setToken, profile, setProfile }) => { )} ) : ( - - diff --git a/frontend/src/components/ModMenu.tsx b/frontend/src/components/ModMenu.tsx index 618d1a7..a0d7eb7 100644 --- a/frontend/src/components/ModMenu.tsx +++ b/frontend/src/components/ModMenu.tsx @@ -5,7 +5,6 @@ import { useNavigate } from "react-router-dom"; import { MapSummary } from "@customTypes/Map"; import { ModMenuContent } from "@customTypes/Content"; import { API } from "@api/Api"; -import "@css/ModMenu.css"; import useConfirm from "@hooks/UseConfirm"; interface ModMenuProps { diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index b55d56b..88a5297 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from "react"; +import React, { useCallback, useRef } from "react"; import { Link, useLocation } from "react-router-dom"; import { @@ -10,12 +10,11 @@ import { PortalIcon, SearchIcon, UploadIcon, -} from "@images/Images"; +} from "../images/Images"; import Login from "@components/Login"; import { UserProfile } from "@customTypes/Profile"; import { Search } from "@customTypes/Search"; import { API } from "@api/Api"; -import "@css/Sidebar.css"; interface SidebarProps { setToken: React.Dispatch>; @@ -24,6 +23,17 @@ interface SidebarProps { onUploadRun: () => void; } +function OpenSidebarIcon(){ + return ( + + ) +} + +function ClosedSidebarIcon(){ + return ( + ) +} + const Sidebar: React.FC = ({ setToken, profile, @@ -34,100 +44,38 @@ const Sidebar: React.FC = ({ undefined ); const [isSidebarLocked, setIsSidebarLocked] = React.useState(false); - const [isSidebarOpen, setSidebarOpen] = React.useState(true); + const [isSidebarOpen, setSidebarOpen] = React.useState(false); + const [selectedButtonIndex, setSelectedButtonIndex] = React.useState(1); const location = useLocation(); const path = location.pathname; - const _handle_sidebar_hide = useCallback(() => { - var btn = document.querySelectorAll( - "button.sidebar-button" - ) as NodeListOf; - const span = document.querySelectorAll( - "button.sidebar-button>span" - ) as NodeListOf; - const side = document.querySelector("#sidebar-list") as HTMLElement; - const searchbar = document.querySelector("#searchbar") as HTMLInputElement; - const uploadRunBtn = document.querySelector( - "#upload-run" - ) as HTMLInputElement; - const uploadRunSpan = document.querySelector( - "#upload-run>span" - ) as HTMLInputElement; + const sidebarRef = useRef(null); + const searchbarRef = useRef(null); + const uploadRunRef = useRef(null); + const sidebarButtonRefs = useRef<(HTMLButtonElement | null)[]>([]); + + const _handle_sidebar_toggle = useCallback(() => { + if (!sidebarRef.current) return; if (isSidebarOpen) { - if (profile) { - const login = document.querySelectorAll( - ".login>button" - )[1] as HTMLElement; - login.style.opacity = "1"; - uploadRunBtn.style.width = "310px"; - uploadRunBtn.style.padding = "0.4em 0 0 11px"; - uploadRunSpan.style.opacity = "0"; - setTimeout(() => { - uploadRunSpan.style.opacity = "1"; - }, 100); - } setSidebarOpen(false); - side.style.width = "320px"; - btn.forEach((e, i) => { - e.style.width = "310px"; - e.style.padding = "0.4em 0 0 11px"; - setTimeout(() => { - span[i].style.opacity = "1"; - }, 100); - }); - side.style.zIndex = "2"; } else { - if (profile) { - const login = document.querySelectorAll( - ".login>button" - )[1] as HTMLElement; - login.style.opacity = "0"; - uploadRunBtn.style.width = "40px"; - uploadRunBtn.style.padding = "0.4em 0 0 5px"; - uploadRunSpan.style.opacity = "0"; - } setSidebarOpen(true); - side.style.width = "40px"; - searchbar.focus(); - btn.forEach((e, i) => { - e.style.width = "40px"; - e.style.padding = "0.4em 0 0 5px"; - span[i].style.opacity = "0"; - }); - setTimeout(() => { - side.style.zIndex = "0"; - }, 300); + searchbarRef.current?.focus(); } - }, [isSidebarOpen, profile]); + }, [isSidebarOpen]); const handle_sidebar_click = useCallback( (clicked_sidebar_idx: number) => { - const btn = document.querySelectorAll("button.sidebar-button"); + setSelectedButtonIndex(clicked_sidebar_idx); if (isSidebarOpen) { setSidebarOpen(false); - _handle_sidebar_hide(); } - // clusterfuck - btn.forEach((e, i) => { - btn[i].classList.remove("sidebar-button-selected"); - btn[i].classList.add("sidebar-button-deselected"); - }); - btn[clicked_sidebar_idx].classList.add("sidebar-button-selected"); - btn[clicked_sidebar_idx].classList.remove("sidebar-button-deselected"); }, - [isSidebarOpen, _handle_sidebar_hide] + [isSidebarOpen] ); - const _handle_sidebar_lock = () => { - if (!isSidebarLocked) { - _handle_sidebar_hide(); - setIsSidebarLocked(true); - setTimeout(() => setIsSidebarLocked(false), 300); - } - }; - const _handle_search_change = async (q: string) => { const searchResponse = await API.get_search(q); setSearchData(searchResponse); @@ -135,149 +83,199 @@ const Sidebar: React.FC = ({ React.useEffect(() => { if (path === "/") { - handle_sidebar_click(1); + setSelectedButtonIndex(1); } else if (path.includes("games")) { - handle_sidebar_click(2); + setSelectedButtonIndex(2); } else if (path.includes("rankings")) { - handle_sidebar_click(3); - } - // else if (path.includes("news")) { handle_sidebar_click(4) } - // else if (path.includes("scorelog")) { handle_sidebar_click(5) } - else if (path.includes("profile")) { - handle_sidebar_click(4); + setSelectedButtonIndex(3); + } else if (path.includes("profile")) { + setSelectedButtonIndex(4); } else if (path.includes("rules")) { - handle_sidebar_click(5); + setSelectedButtonIndex(5); } else if (path.includes("about")) { - handle_sidebar_click(6); + setSelectedButtonIndex(6); } - }, [path, handle_sidebar_click]); + }, [path]); + + const getButtonClasses = (buttonIndex: number) => { + const baseClasses = "flex items-center gap-3 w-full text-left bg-inherit cursor-pointer border-none rounded-lg py-3 px-3 transition-all duration-300 hover:bg-surface1"; + const selectedClasses = selectedButtonIndex === buttonIndex ? "bg-primary text-background" : "bg-transparent text-foreground"; + + return `${baseClasses} ${selectedClasses}`; + }; + + const iconClasses = "w-6 h-6 flex-shrink-0"; return ( -