mirror of
https://github.com/sussy-code/smov.git
synced 2024-12-29 16:07:40 +01:00
overlay router
This commit is contained in:
parent
d9855cb244
commit
d485d3200b
6 changed files with 152 additions and 56 deletions
|
@ -3,7 +3,7 @@ import { ReactNode, useCallback, useEffect, useRef, useState } from "react";
|
|||
import { createPortal } from "react-dom";
|
||||
|
||||
import { Transition } from "@/components/Transition";
|
||||
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
|
||||
import { useInternalOverlayRouter } from "@/hooks/useOverlayRouter";
|
||||
|
||||
export interface OverlayProps {
|
||||
id: string;
|
||||
|
@ -16,11 +16,21 @@ export function OverlayDisplay(props: { children: ReactNode }) {
|
|||
}
|
||||
|
||||
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 ref = useRef<HTMLDivElement>(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(() => {
|
||||
function listen(e: MouseEvent) {
|
||||
target.current = e.target as Element;
|
||||
|
|
|
@ -3,32 +3,37 @@ import { ReactNode } from "react";
|
|||
|
||||
import { Transition } from "@/components/Transition";
|
||||
import { useIsMobile } from "@/hooks/useIsMobile";
|
||||
import { useInternalOverlayRouter } from "@/hooks/useOverlayRouter";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
path: string;
|
||||
children?: ReactNode;
|
||||
show?: boolean;
|
||||
className?: string;
|
||||
height?: number;
|
||||
width?: number;
|
||||
active?: boolean; // true if a child view is loaded
|
||||
}
|
||||
|
||||
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 width = !isMobile ? `${props.width}px` : "100%";
|
||||
return (
|
||||
<Transition
|
||||
animation={props.active ? "slide-full-left" : "slide-full-right"}
|
||||
animation={backwards ? "slide-full-left" : "slide-full-right"}
|
||||
className="absolute inset-0"
|
||||
durationClass="duration-[400ms]"
|
||||
show={props.show}
|
||||
show={show}
|
||||
>
|
||||
<div
|
||||
className={classNames([
|
||||
props.className,
|
||||
"grid grid-rows-[auto,minmax(0,1fr)]",
|
||||
])}
|
||||
data-floating-page={props.show ? "true" : undefined}
|
||||
data-floating-page={show ? "true" : undefined}
|
||||
style={{
|
||||
height: props.height ? `${props.height}px` : undefined,
|
||||
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 routeParts = (route ?? "").split("/").filter((v) => v.length > 0);
|
||||
const routerActive = routeParts.length > 0 && routeParts[0] === id;
|
||||
const transition = useOverlayStore((s) => s.transition);
|
||||
const setTransition = useOverlayStore((s) => s.setTransition);
|
||||
const routerActive = !!route && route.startsWith(`/${id}`);
|
||||
|
||||
function navigate(path: string) {
|
||||
const newRoute = [id, ...path.split("/").filter((v) => v.length > 0)];
|
||||
setRoute(newRoute.join("/"));
|
||||
const oldRoute = route;
|
||||
const newRoute = joinPath(splitPath(path, id));
|
||||
setTransition({
|
||||
from: oldRoute ?? "/",
|
||||
to: newRoute,
|
||||
});
|
||||
setRoute(newRoute);
|
||||
}
|
||||
|
||||
function isActive(page: string) {
|
||||
if (page === "/") return true;
|
||||
const index = routeParts.indexOf(page);
|
||||
if (index === -1) return false; // not active
|
||||
if (index === routeParts.length - 1) return false; // active but latest route so shouldnt be counted as active
|
||||
return true;
|
||||
function showBackwardsTransition(path: string) {
|
||||
if (!transition) return false;
|
||||
const current = joinPath(splitPath(path, id));
|
||||
|
||||
if (current === transition.to && transition.from.startsWith(transition.to))
|
||||
return true;
|
||||
if (
|
||||
current === transition.from &&
|
||||
transition.to.startsWith(transition.from)
|
||||
)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function isCurrentPage(page: string) {
|
||||
return routerActive && route === `/${id}${page}`;
|
||||
}
|
||||
|
||||
function isLoaded(page: string) {
|
||||
if (page === "/") return true;
|
||||
return route.includes(page);
|
||||
function isCurrentPage(path: string) {
|
||||
return routerActive && route === joinPath(splitPath(path, id));
|
||||
}
|
||||
|
||||
function isOverlayActive() {
|
||||
return routerActive;
|
||||
}
|
||||
|
||||
function pageProps(page: string) {
|
||||
return {
|
||||
show: isCurrentPage(page),
|
||||
active: isActive(page),
|
||||
};
|
||||
}
|
||||
|
||||
function close() {
|
||||
const close = useCallback(() => {
|
||||
setTransition(null);
|
||||
setRoute(null);
|
||||
}
|
||||
}, [setRoute, setTransition]);
|
||||
|
||||
function open() {
|
||||
const open = useCallback(() => {
|
||||
setTransition(null);
|
||||
setRoute(`/${id}`);
|
||||
}
|
||||
}, [id, setRoute, setTransition]);
|
||||
|
||||
return {
|
||||
showBackwardsTransition,
|
||||
isCurrentPage,
|
||||
isOverlayActive,
|
||||
navigate,
|
||||
close,
|
||||
isLoaded,
|
||||
isCurrentPage,
|
||||
pageProps,
|
||||
isActive,
|
||||
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;
|
||||
}
|
||||
|
||||
export function useQueryParam(param: string) {
|
||||
export function useQueryParam(
|
||||
param: string
|
||||
): [string | null, (a: string | null) => void] {
|
||||
const params = useQueryParams();
|
||||
const location = useLocation();
|
||||
const router = useHistory();
|
||||
const currentValue = params[param];
|
||||
const currentValue = params[param] ?? null;
|
||||
|
||||
const set = useCallback(
|
||||
(value: string | null) => {
|
||||
|
@ -34,5 +36,5 @@ export function useQueryParam(param: string) {
|
|||
[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
|
||||
export default function TestView() {
|
||||
const router = useOverlayRouter("test");
|
||||
const pages = ["", "/one", "/two"];
|
||||
|
||||
return (
|
||||
<OverlayDisplay>
|
||||
|
@ -19,21 +18,57 @@ export default function TestView() {
|
|||
>
|
||||
Open
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
router.navigate(pages[Math.floor(pages.length * Math.random())]);
|
||||
}}
|
||||
>
|
||||
random page
|
||||
</button>
|
||||
<OverlayAnchor id="test">
|
||||
<div className="h-20 w-20 bg-white" />
|
||||
</OverlayAnchor>
|
||||
<Overlay id="test">
|
||||
<OverlayPage {...router.pageProps("")}>Home</OverlayPage>
|
||||
<OverlayPage {...router.pageProps("/one")}>Page one</OverlayPage>
|
||||
<OverlayPage {...router.pageProps("/two")}>Page two</OverlayPage>
|
||||
<OverlayPage id="test" path="/">
|
||||
<div className="bg-blue-900 p-4">
|
||||
<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>
|
||||
</div>
|
||||
</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 a new issue