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({
+
+
+
+
+
+
+
+
setSideNavCollapsed(!sideNavCollapsed)}
+ className={styles.sideNavCollapseButton}
/>
void }) {
+function SideNavCollapseButton({
+ onToggle,
+ className,
+}: {
+ onToggle?: () => void;
+ className?: string;
+}) {
return (