feat: flyout left nav (mobile)

closes #155
This commit is contained in:
2024-07-18 13:36:04 +01:00
parent e81a2f25b8
commit b033370322
4 changed files with 110 additions and 54 deletions

40
src/element/flyout.tsx Normal file
View File

@ -0,0 +1,40 @@
import { CSSProperties, ReactNode } from "react";
import { IconButton } from "./buttons";
import { createPortal } from "react-dom";
import classNames from "classnames";
export default function Flyout({
show,
children,
onClose,
side,
}: {
show: boolean;
children: ReactNode;
onClose: () => void;
side: "left" | "right";
}) {
const styles = {
"--flyout-w": "200px",
transition: "all 0.2s ease-in-out",
transform:
side === "right"
? `translate(${show ? "0" : "var(--flyout-w)"},0)`
: `translate(${show ? "0" : "calc(-1 * var(--flyout-w))"},0)`,
} as CSSProperties;
return createPortal(
<div
className={classNames("absolute z-20 top-0 overflow-hidden", {
"pointer-events-none": !show,
"right-0": side == "right",
"left-0": side === "left",
})}>
<div className="bg-layer-2/90 h-[100dvh] px-3 py-4" style={styles}>
<IconButton iconName="x" className="rounded-xl w-10 h-10 mb-6" iconSize={16} onClick={onClose} />
{children}
</div>
</div>,
document.body,
) as React.ReactNode;
}

View File

@ -142,7 +142,7 @@ export function HeaderNav() {
{layoutState.leftNav && ( {layoutState.leftNav && (
<NavLinkIcon <NavLinkIcon
name="hamburger" name="hamburger"
className="!opacity-100 max-xl:hidden" className="!opacity-100"
onClick={() => { onClick={() => {
layoutState.update(c => { layoutState.update(c => {
c.leftNavExpand = !c.leftNavExpand; c.leftNavExpand = !c.leftNavExpand;

View File

@ -1,41 +1,77 @@
import { useLayout } from "./context"; import { useLayout } from "./context";
import { NavLinkIcon } from "./nav-icon"; import { NavLinkIcon } from "./nav-icon";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import { useMediaQuery } from "usehooks-ts";
import Flyout from "@/element/flyout";
export function LeftNav() { export function LeftNav() {
const layout = useLayout(); const layout = useLayout();
const isDesktop = useMediaQuery("(min-width: 1280px)");
const expandLabels = !isDesktop || layout.leftNavExpand;
function hideAfterMobileNav() {
if (isDesktop) return;
layout.update(c => {
c.leftNavExpand = false;
return { ...c };
});
}
if (layout.leftNav === false) return; if (layout.leftNav === false) return;
return ( function navInner() {
<div className="flex flex-col gap-4 p-2 max-xl:hidden"> return (
<NavLinkIcon name="signal" route="/streams" className="flex gap-2 items-center"> <div className="flex flex-col gap-4 p-2">
{layout.leftNavExpand && ( <NavLinkIcon name="signal" route="/streams" className="flex gap-2 items-center" onClick={hideAfterMobileNav}>
<span className="pr-3"> {expandLabels && (
<FormattedMessage defaultMessage="Streams" /> <span className="pr-3">
</span> <FormattedMessage defaultMessage="Streams" />
)} </span>
</NavLinkIcon> )}
<NavLinkIcon name="play-circle" route="/videos" className="flex gap-2 items-center"> </NavLinkIcon>
{layout.leftNavExpand && ( <NavLinkIcon
<span className="pr-3"> name="play-circle"
<FormattedMessage defaultMessage="Videos" /> route="/videos"
</span> className="flex gap-2 items-center"
)} onClick={hideAfterMobileNav}>
</NavLinkIcon> {expandLabels && (
<NavLinkIcon name="film" route="/shorts" className="flex gap-2 items-center"> <span className="pr-3">
{layout.leftNavExpand && ( <FormattedMessage defaultMessage="Videos" />
<span className="pr-3"> </span>
<FormattedMessage defaultMessage="Shorts" /> )}
</span> </NavLinkIcon>
)} <NavLinkIcon name="film" route="/shorts" className="flex gap-2 items-center" onClick={hideAfterMobileNav}>
</NavLinkIcon> {expandLabels && (
<NavLinkIcon name="grid" route="/category" className="flex gap-2 items-center"> <span className="pr-3">
{layout.leftNavExpand && ( <FormattedMessage defaultMessage="Shorts" />
<span className="pr-3"> </span>
<FormattedMessage defaultMessage="Categories" /> )}
</span> </NavLinkIcon>
)} <NavLinkIcon name="grid" route="/category" className="flex gap-2 items-center" onClick={hideAfterMobileNav}>
</NavLinkIcon> {expandLabels && (
</div> <span className="pr-3">
); <FormattedMessage defaultMessage="Categories" />
</span>
)}
</NavLinkIcon>
</div>
);
}
if (isDesktop) {
return navInner();
} else {
return (
<Flyout
side="left"
show={layout.leftNavExpand}
onClose={() => {
layout.update(c => {
c.leftNavExpand = !c.leftNavExpand;
return { ...c };
});
}}>
{navInner()}
</Flyout>
);
}
} }

View File

@ -1,6 +1,6 @@
import { NostrLink, TaggedNostrEvent } from "@snort/system"; import { NostrLink, TaggedNostrEvent } from "@snort/system";
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { Suspense, lazy, useEffect } from "react"; import { Suspense, lazy } from "react";
import { useMediaQuery } from "usehooks-ts"; import { useMediaQuery } from "usehooks-ts";
const LiveVideoPlayer = lazy(() => import("@/element/stream/live-video-player")); const LiveVideoPlayer = lazy(() => import("@/element/stream/live-video-player"));
@ -12,7 +12,6 @@ import { ContentWarningOverlay, useContentWarning } from "@/element/nsfw";
import { useCurrentStreamFeed } from "@/hooks/current-stream-feed"; import { useCurrentStreamFeed } from "@/hooks/current-stream-feed";
import { StreamState } from "@/const"; import { StreamState } from "@/const";
import { StreamInfo } from "@/element/stream/stream-info"; import { StreamInfo } from "@/element/stream/stream-info";
import { useLayout } from "./layout/context";
import { StreamContextProvider } from "@/element/stream/stream-state"; import { StreamContextProvider } from "@/element/stream/stream-state";
export function StreamPage({ link, evPreload }: { evPreload?: TaggedNostrEvent; link: NostrLink }) { export function StreamPage({ link, evPreload }: { evPreload?: TaggedNostrEvent; link: NostrLink }) {
@ -33,25 +32,6 @@ export function StreamPage({ link, evPreload }: { evPreload?: TaggedNostrEvent;
const goal = useZapGoal(goalTag); const goal = useZapGoal(goalTag);
const isDesktop = useMediaQuery("(min-width: 1280px)"); const isDesktop = useMediaQuery("(min-width: 1280px)");
const isGrownUp = useContentWarning(); const isGrownUp = useContentWarning();
const layout = useLayout();
useEffect(() => {
if (layout.leftNav) {
layout.update(c => {
c.leftNav = false;
return { ...c };
});
}
}, [layout]);
useEffect(() => {
return () => {
layout.update(c => {
c.leftNav = true;
return { ...c };
});
};
}, []);
if (contentWarning && !isGrownUp) { if (contentWarning && !isGrownUp) {
return <ContentWarningOverlay />; return <ContentWarningOverlay />;