mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-28 01:31:48 +01:00
overlay router
This commit is contained in:
parent
d9855cb244
commit
d485d3200b
@ -3,7 +3,7 @@ import { ReactNode, useCallback, useEffect, useRef, useState } from "react";
|
|||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
|
|
||||||
import { Transition } from "@/components/Transition";
|
import { Transition } from "@/components/Transition";
|
||||||
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
|
import { useInternalOverlayRouter } from "@/hooks/useOverlayRouter";
|
||||||
|
|
||||||
export interface OverlayProps {
|
export interface OverlayProps {
|
||||||
id: string;
|
id: string;
|
||||||
@ -16,11 +16,21 @@ export function OverlayDisplay(props: { children: ReactNode }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Overlay(props: OverlayProps) {
|
export function Overlay(props: OverlayProps) {
|
||||||
const router = useOverlayRouter(props.id);
|
const router = useInternalOverlayRouter(props.id);
|
||||||
|
const refRouter = useRef(router);
|
||||||
const [portalElement, setPortalElement] = useState<Element | null>(null);
|
const [portalElement, setPortalElement] = useState<Element | null>(null);
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const target = useRef<Element | null>(null);
|
const target = useRef<Element | null>(null);
|
||||||
|
|
||||||
|
// close router on first mount, we dont want persist routes for overlays
|
||||||
|
useEffect(() => {
|
||||||
|
const r = refRouter.current;
|
||||||
|
r.close();
|
||||||
|
return () => {
|
||||||
|
r.close();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function listen(e: MouseEvent) {
|
function listen(e: MouseEvent) {
|
||||||
target.current = e.target as Element;
|
target.current = e.target as Element;
|
||||||
|
@ -3,32 +3,37 @@ import { ReactNode } from "react";
|
|||||||
|
|
||||||
import { Transition } from "@/components/Transition";
|
import { Transition } from "@/components/Transition";
|
||||||
import { useIsMobile } from "@/hooks/useIsMobile";
|
import { useIsMobile } from "@/hooks/useIsMobile";
|
||||||
|
import { useInternalOverlayRouter } from "@/hooks/useOverlayRouter";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
id: string;
|
||||||
|
path: string;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
show?: boolean;
|
|
||||||
className?: string;
|
className?: string;
|
||||||
height?: number;
|
height?: number;
|
||||||
width?: number;
|
width?: number;
|
||||||
active?: boolean; // true if a child view is loaded
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function OverlayPage(props: Props) {
|
export function OverlayPage(props: Props) {
|
||||||
|
const router = useInternalOverlayRouter(props.id);
|
||||||
|
const backwards = router.showBackwardsTransition(props.path);
|
||||||
|
const show = router.isCurrentPage(props.path);
|
||||||
|
|
||||||
const { isMobile } = useIsMobile();
|
const { isMobile } = useIsMobile();
|
||||||
const width = !isMobile ? `${props.width}px` : "100%";
|
const width = !isMobile ? `${props.width}px` : "100%";
|
||||||
return (
|
return (
|
||||||
<Transition
|
<Transition
|
||||||
animation={props.active ? "slide-full-left" : "slide-full-right"}
|
animation={backwards ? "slide-full-left" : "slide-full-right"}
|
||||||
className="absolute inset-0"
|
className="absolute inset-0"
|
||||||
durationClass="duration-[400ms]"
|
durationClass="duration-[400ms]"
|
||||||
show={props.show}
|
show={show}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={classNames([
|
className={classNames([
|
||||||
props.className,
|
props.className,
|
||||||
"grid grid-rows-[auto,minmax(0,1fr)]",
|
"grid grid-rows-[auto,minmax(0,1fr)]",
|
||||||
])}
|
])}
|
||||||
data-floating-page={props.show ? "true" : undefined}
|
data-floating-page={show ? "true" : undefined}
|
||||||
style={{
|
style={{
|
||||||
height: props.height ? `${props.height}px` : undefined,
|
height: props.height ? `${props.height}px` : undefined,
|
||||||
maxHeight: "70vh",
|
maxHeight: "70vh",
|
||||||
|
@ -1,59 +1,80 @@
|
|||||||
import { useQueryParam } from "@/hooks/useQueryParams";
|
import { useCallback } from "react";
|
||||||
|
|
||||||
export function useOverlayRouter(id: string) {
|
import { useQueryParam } from "@/hooks/useQueryParams";
|
||||||
|
import { useOverlayStore } from "@/stores/overlay/store";
|
||||||
|
|
||||||
|
function splitPath(path: string, prefix?: string): string[] {
|
||||||
|
const parts = [prefix ?? "", ...path.split("/")];
|
||||||
|
return parts.filter((v) => v.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function joinPath(path: string[]): string {
|
||||||
|
return `/${path.join("/")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useInternalOverlayRouter(id: string) {
|
||||||
const [route, setRoute] = useQueryParam("r");
|
const [route, setRoute] = useQueryParam("r");
|
||||||
const routeParts = (route ?? "").split("/").filter((v) => v.length > 0);
|
const transition = useOverlayStore((s) => s.transition);
|
||||||
const routerActive = routeParts.length > 0 && routeParts[0] === id;
|
const setTransition = useOverlayStore((s) => s.setTransition);
|
||||||
|
const routerActive = !!route && route.startsWith(`/${id}`);
|
||||||
|
|
||||||
function navigate(path: string) {
|
function navigate(path: string) {
|
||||||
const newRoute = [id, ...path.split("/").filter((v) => v.length > 0)];
|
const oldRoute = route;
|
||||||
setRoute(newRoute.join("/"));
|
const newRoute = joinPath(splitPath(path, id));
|
||||||
|
setTransition({
|
||||||
|
from: oldRoute ?? "/",
|
||||||
|
to: newRoute,
|
||||||
|
});
|
||||||
|
setRoute(newRoute);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isActive(page: string) {
|
function showBackwardsTransition(path: string) {
|
||||||
if (page === "/") return true;
|
if (!transition) return false;
|
||||||
const index = routeParts.indexOf(page);
|
const current = joinPath(splitPath(path, id));
|
||||||
if (index === -1) return false; // not active
|
|
||||||
if (index === routeParts.length - 1) return false; // active but latest route so shouldnt be counted as active
|
if (current === transition.to && transition.from.startsWith(transition.to))
|
||||||
return true;
|
return true;
|
||||||
|
if (
|
||||||
|
current === transition.from &&
|
||||||
|
transition.to.startsWith(transition.from)
|
||||||
|
)
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isCurrentPage(page: string) {
|
function isCurrentPage(path: string) {
|
||||||
return routerActive && route === `/${id}${page}`;
|
return routerActive && route === joinPath(splitPath(path, id));
|
||||||
}
|
|
||||||
|
|
||||||
function isLoaded(page: string) {
|
|
||||||
if (page === "/") return true;
|
|
||||||
return route.includes(page);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isOverlayActive() {
|
function isOverlayActive() {
|
||||||
return routerActive;
|
return routerActive;
|
||||||
}
|
}
|
||||||
|
|
||||||
function pageProps(page: string) {
|
const close = useCallback(() => {
|
||||||
return {
|
setTransition(null);
|
||||||
show: isCurrentPage(page),
|
|
||||||
active: isActive(page),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function close() {
|
|
||||||
setRoute(null);
|
setRoute(null);
|
||||||
}
|
}, [setRoute, setTransition]);
|
||||||
|
|
||||||
function open() {
|
const open = useCallback(() => {
|
||||||
|
setTransition(null);
|
||||||
setRoute(`/${id}`);
|
setRoute(`/${id}`);
|
||||||
}
|
}, [id, setRoute, setTransition]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
showBackwardsTransition,
|
||||||
|
isCurrentPage,
|
||||||
isOverlayActive,
|
isOverlayActive,
|
||||||
navigate,
|
navigate,
|
||||||
close,
|
close,
|
||||||
isLoaded,
|
|
||||||
isCurrentPage,
|
|
||||||
pageProps,
|
|
||||||
isActive,
|
|
||||||
open,
|
open,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useOverlayRouter(id: string) {
|
||||||
|
const router = useInternalOverlayRouter(id);
|
||||||
|
return {
|
||||||
|
open: router.open,
|
||||||
|
close: router.close,
|
||||||
|
navigate: router.navigate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -16,11 +16,13 @@ export function useQueryParams() {
|
|||||||
return queryParams;
|
return queryParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useQueryParam(param: string) {
|
export function useQueryParam(
|
||||||
|
param: string
|
||||||
|
): [string | null, (a: string | null) => void] {
|
||||||
const params = useQueryParams();
|
const params = useQueryParams();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const router = useHistory();
|
const router = useHistory();
|
||||||
const currentValue = params[param];
|
const currentValue = params[param] ?? null;
|
||||||
|
|
||||||
const set = useCallback(
|
const set = useCallback(
|
||||||
(value: string | null) => {
|
(value: string | null) => {
|
||||||
@ -34,5 +36,5 @@ export function useQueryParam(param: string) {
|
|||||||
[param, location, router]
|
[param, location, router]
|
||||||
);
|
);
|
||||||
|
|
||||||
return [currentValue, set] as const;
|
return [currentValue, set];
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,6 @@ import { useOverlayRouter } from "@/hooks/useOverlayRouter";
|
|||||||
// simple empty view, perfect for putting in tests
|
// simple empty view, perfect for putting in tests
|
||||||
export default function TestView() {
|
export default function TestView() {
|
||||||
const router = useOverlayRouter("test");
|
const router = useOverlayRouter("test");
|
||||||
const pages = ["", "/one", "/two"];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OverlayDisplay>
|
<OverlayDisplay>
|
||||||
@ -19,21 +18,57 @@ export default function TestView() {
|
|||||||
>
|
>
|
||||||
Open
|
Open
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
router.navigate(pages[Math.floor(pages.length * Math.random())]);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
random page
|
|
||||||
</button>
|
|
||||||
<OverlayAnchor id="test">
|
<OverlayAnchor id="test">
|
||||||
<div className="h-20 w-20 bg-white" />
|
<div className="h-20 w-20 bg-white" />
|
||||||
</OverlayAnchor>
|
</OverlayAnchor>
|
||||||
<Overlay id="test">
|
<Overlay id="test">
|
||||||
<OverlayPage {...router.pageProps("")}>Home</OverlayPage>
|
<OverlayPage id="test" path="/">
|
||||||
<OverlayPage {...router.pageProps("/one")}>Page one</OverlayPage>
|
<div className="bg-blue-900 p-4">
|
||||||
<OverlayPage {...router.pageProps("/two")}>Page two</OverlayPage>
|
<p>HOME</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
router.navigate("/two");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
open page two
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
router.navigate("/one");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
open page one
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</OverlayPage>
|
||||||
|
<OverlayPage id="test" path="/one">
|
||||||
|
<div className="bg-blue-900 p-4">
|
||||||
|
<p>ONE</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
router.navigate("/");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
back home
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</OverlayPage>
|
||||||
|
<OverlayPage id="test" path="/two">
|
||||||
|
<div className="bg-blue-900 p-4">
|
||||||
|
<p>TWO</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
router.navigate("/");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
back home
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</OverlayPage>
|
||||||
</Overlay>
|
</Overlay>
|
||||||
</div>
|
</div>
|
||||||
</OverlayDisplay>
|
</OverlayDisplay>
|
||||||
|
23
src/stores/overlay/store.ts
Normal file
23
src/stores/overlay/store.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
import { immer } from "zustand/middleware/immer";
|
||||||
|
|
||||||
|
export interface OverlayTransition {
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OverlayStore {
|
||||||
|
transition: null | OverlayTransition;
|
||||||
|
setTransition(newTrans: OverlayTransition | null): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useOverlayStore = create(
|
||||||
|
immer<OverlayStore>((set) => ({
|
||||||
|
transition: null,
|
||||||
|
setTransition(newTrans) {
|
||||||
|
set((s) => {
|
||||||
|
s.transition = newTrans;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
);
|
Loading…
Reference in New Issue
Block a user