From 0c1d53c6f8d31fb2c5a09975deb6f077536ddc50 Mon Sep 17 00:00:00 2001 From: hfcRed Date: Sat, 7 Mar 2026 16:41:53 +0100 Subject: [PATCH] Add midpoint overlay sidenav --- app/components/SideNav.module.css | 2 +- app/components/layout/index.module.css | 46 +++- app/components/layout/index.tsx | 287 +++++++++++++++---------- 3 files changed, 211 insertions(+), 124 deletions(-) diff --git a/app/components/SideNav.module.css b/app/components/SideNav.module.css index 29535349c..11cca4d9f 100644 --- a/app/components/SideNav.module.css +++ b/app/components/SideNav.module.css @@ -11,7 +11,7 @@ display: none; flex-direction: column; - @media screen and (min-width: 600px) { + @media screen and (min-width: 1000px) { display: flex; } } diff --git a/app/components/layout/index.module.css b/app/components/layout/index.module.css index 603baecad..d8ace9a88 100644 --- a/app/components/layout/index.module.css +++ b/app/components/layout/index.module.css @@ -164,7 +164,8 @@ } /* Doubled selector increases specificity to beat Button.module.css's display: flex */ -.sideNavCollapseButton.sideNavCollapseButton { +.sideNavCollapseButton.sideNavCollapseButton, +.sideNavModalTrigger.sideNavModalTrigger { display: none; & svg { @@ -172,12 +173,47 @@ min-width: 20px; max-width: 20px; } + + &.sideNavCollapseButton { + @media screen and (min-width: 1000px) { + display: flex; + } + } + + &.sideNavModalTrigger { + @media screen and (min-width: 600px) and (max-width: 999px) { + display: flex; + } + } } -@media screen and (min-width: 600px) { - .sideNavCollapseButton.sideNavCollapseButton { - display: flex; - } +.sideNavModalOverlay { + position: fixed; + inset: 0; + top: var(--layout-nav-height); + background-color: rgba(0, 0, 0, 0.4); + backdrop-filter: blur(4px); + z-index: 20; +} + +.sideNavModal { + height: 100%; + width: var(--layout-sidenav-width); + border-right: 1.5px solid var(--color-border); +} + +.sideNavModalDialog { + outline: none; + height: 100%; +} + +.sideNavInModal { + display: flex; + position: static; + height: 100%; + min-width: unset; + max-width: unset; + border-right: none; } .mobileLogo { diff --git a/app/components/layout/index.tsx b/app/components/layout/index.tsx index cad7c3d4e..ecf2000d7 100644 --- a/app/components/layout/index.tsx +++ b/app/components/layout/index.tsx @@ -10,7 +10,13 @@ import { Users, } from "lucide-react"; import * as React from "react"; -import { Button } from "react-aria-components"; +import { + Button, + Dialog, + DialogTrigger, + Modal, + ModalOverlay, +} from "react-aria-components"; import { Flipped, Flipper } from "react-flip-toolkit"; import { useTranslation } from "react-i18next"; import { Link, useFetcher, useLocation, useMatches } from "react-router"; @@ -170,6 +176,7 @@ export function Layout({ const [sideNavCollapsed, setSideNavCollapsed] = useSideNavCollapsed( data?.sidenavCollapsed ?? false, ); + const [sideNavModalOpen, setSideNavModalOpen] = React.useState(false); const { t, i18n } = useTranslation(["front", "common"]); const { formatRelativeDate } = useTimeFormat(); @@ -177,6 +184,17 @@ export function Layout({ const navOffset = useNavOffset(); const isMounted = useIsMounted(); + React.useEffect(() => { + const handleResize = () => { + if (window.innerWidth < 600 || window.innerWidth >= 1000) { + setSideNavModalOpen(false); + } + }; + + window.addEventListener("resize", handleResize); + return () => window.removeEventListener("resize", handleResize); + }, []); + const user = useUser(); const sidebarData = data?.sidebar; const events = sidebarData?.events ?? []; @@ -190,128 +208,133 @@ export function Layout({ !data?.user?.roles.includes("MINOR_SUPPORT") && !location.pathname.includes("plans"); + const sideNavFooterContent = ( + + + + ); + + const sideNavChildren = ( + <> + }> + {t("front:sideNav.myCalendar")} + + {events.length > 0 ? ( + events.map((event) => ( + + {event.scrimStatus === "booked" + ? t("front:sideNav.scrimVs", { opponent: event.name }) + : event.scrimStatus === "looking" + ? t("front:sideNav.lookingForScrim") + : event.name} + + )) + ) : ( +
{t("front:sideNav.noEvents")}
+ )} + + } + action={ + user ? ( + + {t("common:actions.viewAll")} + + + ) : null + } + > + {t("front:sideNav.friends")} + + {friends.length > 0 ? ( + friends.map((friend) => ) + ) : ( +
+ {user + ? t("front:sideNav.friends.noFriends") + : t("front:sideNav.friends.notLoggedIn")} +
+ )} + + }> + {t("front:sideNav.streams")} + + {streams.length === 0 ? ( +
+ {t("front:sideNav.noStreams")} +
+ ) : null} + {streams.map((stream, i) => { + const startsAtDate = databaseTimestampToDate(stream.startsAt); + const isUpcoming = startsAtDate.getTime() > Date.now(); + const prevStream = streams.at(i - 1); + const prevIsLive = + prevStream && + databaseTimestampToDate(prevStream.startsAt).getTime() <= Date.now(); + const showUpcomingDivider = isMounted && isUpcoming && prevIsLive; + + return ( + + {showUpcomingDivider ? ( +
+ {t("front:sideNav.streams.upcoming")} +
+ ) : null} + + + {stream.peakXp} + + ) : stream.subtitle ? ( + stream.subtitle + ) : isMounted ? ( + isUpcoming ? ( + formatRelativeDate(stream.startsAt) + ) : ( + formatDistanceToNow(startsAtDate, { + addSuffix: true, + language: i18n.language as LanguageCode, + }) + ) + ) : ( + "" + ) + } + badge={ + isMounted && !isUpcoming ? "LIVE" : streamTierBadge(stream) + } + > + {stream.name} + +
+ ); + })} + + ); + return ( <> - - - } + footer={sideNavFooterContent} top={} topCentered={isFrontPage} > - }> - {t("front:sideNav.myCalendar")} - - {events.length > 0 ? ( - events.map((event) => ( - - {event.scrimStatus === "booked" - ? t("front:sideNav.scrimVs", { opponent: event.name }) - : event.scrimStatus === "looking" - ? t("front:sideNav.lookingForScrim") - : event.name} - - )) - ) : ( -
- {t("front:sideNav.noEvents")} -
- )} - - } - action={ - user ? ( - - {t("common:actions.viewAll")} - - - ) : null - } - > - {t("front:sideNav.friends")} - - {friends.length > 0 ? ( - friends.map((friend) => ) - ) : ( -
- {user - ? t("front:sideNav.friends.noFriends") - : t("front:sideNav.friends.notLoggedIn")} -
- )} - - }> - {t("front:sideNav.streams")} - - {streams.length === 0 ? ( -
- {t("front:sideNav.noStreams")} -
- ) : null} - {streams.map((stream, i) => { - const startsAtDate = databaseTimestampToDate(stream.startsAt); - const isUpcoming = startsAtDate.getTime() > Date.now(); - const prevStream = streams.at(i - 1); - const prevIsLive = - prevStream && - databaseTimestampToDate(prevStream.startsAt).getTime() <= - Date.now(); - const showUpcomingDivider = isMounted && isUpcoming && prevIsLive; - - return ( - - {showUpcomingDivider ? ( -
- {t("front:sideNav.streams.upcoming")} -
- ) : null} - - - {stream.peakXp} - - ) : stream.subtitle ? ( - stream.subtitle - ) : isMounted ? ( - isUpcoming ? ( - formatRelativeDate(stream.startsAt) - ) : ( - formatDistanceToNow(startsAtDate, { - addSuffix: true, - language: i18n.language as LanguageCode, - }) - ) - ) : ( - "" - ) - } - badge={ - isMounted && !isUpcoming ? "LIVE" : streamTierBadge(stream) - } - > - {stream.name} - -
- ); - })} + {sideNavChildren}
@@ -324,8 +347,30 @@ export function Layout({ + + + + + + } + topCentered={isFrontPage} + > + {sideNavChildren} + + + + + setSideNavCollapsed(!sideNavCollapsed)} + className={styles.sideNavCollapseButton} /> void }) { +function SideNavCollapseButton({ + onToggle, + className, +}: { + onToggle?: () => void; + className?: string; +}) { return (