mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-25 20:51:49 +01:00
banners in video player
This commit is contained in:
parent
294f31c567
commit
b5dae824c8
@ -1,28 +0,0 @@
|
|||||||
import { Icon, Icons } from "@/components/Icon";
|
|
||||||
import { useBanner } from "@/hooks/useBanner";
|
|
||||||
|
|
||||||
export function Banner(props: { children: React.ReactNode; type: "error" }) {
|
|
||||||
const [ref] = useBanner<HTMLDivElement>("internet");
|
|
||||||
const styles = {
|
|
||||||
error: "bg-[#C93957] text-white",
|
|
||||||
};
|
|
||||||
const icons = {
|
|
||||||
error: Icons.CIRCLE_EXCLAMATION,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={ref}>
|
|
||||||
<div
|
|
||||||
className={[
|
|
||||||
styles[props.type],
|
|
||||||
"flex items-center justify-center p-1",
|
|
||||||
].join(" ")}
|
|
||||||
>
|
|
||||||
<div className="flex items-center space-x-3">
|
|
||||||
<Icon icon={icons[props.type]} />
|
|
||||||
<div>{props.children}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -2,7 +2,7 @@ import {
|
|||||||
Transition as HeadlessTransition,
|
Transition as HeadlessTransition,
|
||||||
TransitionClasses,
|
TransitionClasses,
|
||||||
} from "@headlessui/react";
|
} from "@headlessui/react";
|
||||||
import { Fragment, ReactNode } from "react";
|
import { CSSProperties, Fragment, ReactNode } from "react";
|
||||||
|
|
||||||
export type TransitionAnimations =
|
export type TransitionAnimations =
|
||||||
| "slide-down"
|
| "slide-down"
|
||||||
@ -19,6 +19,7 @@ interface Props {
|
|||||||
className?: string;
|
className?: string;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
isChild?: boolean;
|
isChild?: boolean;
|
||||||
|
style?: CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getClasses(
|
function getClasses(
|
||||||
@ -90,14 +91,18 @@ export function Transition(props: Props) {
|
|||||||
if (props.isChild) {
|
if (props.isChild) {
|
||||||
return (
|
return (
|
||||||
<HeadlessTransition.Child as={Fragment} {...classes}>
|
<HeadlessTransition.Child as={Fragment} {...classes}>
|
||||||
<div className={props.className}>{props.children}</div>
|
<div className={props.className} style={props.style}>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
</HeadlessTransition.Child>
|
</HeadlessTransition.Child>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HeadlessTransition show={props.show} as={Fragment} {...classes}>
|
<HeadlessTransition show={props.show} as={Fragment} {...classes}>
|
||||||
<div className={props.className}>{props.children}</div>
|
<div className={props.className} style={props.style}>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
</HeadlessTransition>
|
</HeadlessTransition>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -4,8 +4,8 @@ import { Link } from "react-router-dom";
|
|||||||
import { IconPatch } from "@/components/buttons/IconPatch";
|
import { IconPatch } from "@/components/buttons/IconPatch";
|
||||||
import { Icons } from "@/components/Icon";
|
import { Icons } from "@/components/Icon";
|
||||||
import { Lightbar } from "@/components/utils/Lightbar";
|
import { Lightbar } from "@/components/utils/Lightbar";
|
||||||
import { useBannerSize } from "@/hooks/useBanner";
|
|
||||||
import { conf } from "@/setup/config";
|
import { conf } from "@/setup/config";
|
||||||
|
import { useBannerSize } from "@/stores/banner";
|
||||||
|
|
||||||
import { BrandPill } from "./BrandPill";
|
import { BrandPill } from "./BrandPill";
|
||||||
|
|
||||||
@ -20,27 +20,32 @@ export function Navigation(props: NavigationProps) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!props.noLightbar ? (
|
{!props.noLightbar ? (
|
||||||
<div className="absolute inset-x-0 top-0 flex h-[88px] items-center justify-center">
|
<div
|
||||||
|
className="absolute inset-x-0 top-0 flex h-[88px] items-center justify-center"
|
||||||
|
style={{
|
||||||
|
top: `${bannerHeight}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div className="absolute inset-x-0 -mt-[22%] flex items-center sm:mt-0">
|
<div className="absolute inset-x-0 -mt-[22%] flex items-center sm:mt-0">
|
||||||
<Lightbar />
|
<Lightbar />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div
|
<div
|
||||||
className="fixed left-0 right-0 top-0 z-10 min-h-[150px]"
|
className="fixed pointer-events-none left-0 right-0 top-0 z-10 min-h-[150px]"
|
||||||
style={{
|
style={{
|
||||||
top: `${bannerHeight}px`,
|
top: `${bannerHeight}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="fixed left-0 right-0 flex items-center justify-between px-7 py-5">
|
<div className="fixed left-0 right-0 flex items-center">
|
||||||
<div
|
<div
|
||||||
className={`${
|
className={`${
|
||||||
props.bg ? "opacity-100" : "opacity-0"
|
props.bg ? "opacity-100" : "opacity-0"
|
||||||
} absolute inset-0 block bg-background-main transition-opacity duration-300`}
|
} absolute inset-0 block bg-background-main transition-opacity duration-300`}
|
||||||
>
|
>
|
||||||
<div className="pointer-events-none absolute -bottom-24 h-24 w-full bg-gradient-to-b from-background-main to-transparent" />
|
<div className="absolute -bottom-24 h-24 w-full bg-gradient-to-b from-background-main to-transparent" />
|
||||||
</div>
|
</div>
|
||||||
<div className="relative flex w-full items-center sm:w-fit space-x-3">
|
<div className="pointer-events-auto px-7 py-5 relative flex flex-1 items-center space-x-3">
|
||||||
<Link className="block" to="/">
|
<Link className="block" to="/">
|
||||||
<BrandPill clickable />
|
<BrandPill clickable />
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
import { Transition } from "@/components/Transition";
|
import { Transition } from "@/components/Transition";
|
||||||
|
import { useBannerSize } from "@/stores/banner";
|
||||||
|
import { BannerLocation } from "@/stores/banner/BannerLocation";
|
||||||
import { usePlayerStore } from "@/stores/player/store";
|
import { usePlayerStore } from "@/stores/player/store";
|
||||||
|
|
||||||
export function TopControls(props: {
|
export function TopControls(props: {
|
||||||
show?: boolean;
|
show?: boolean;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
|
const bannerSize = useBannerSize("player");
|
||||||
const setHoveringAnyControls = usePlayerStore(
|
const setHoveringAnyControls = usePlayerStore(
|
||||||
(s) => s.setHoveringAnyControls
|
(s) => s.setHoveringAnyControls
|
||||||
);
|
);
|
||||||
@ -22,12 +25,21 @@ export function TopControls(props: {
|
|||||||
<Transition
|
<Transition
|
||||||
animation="fade"
|
animation="fade"
|
||||||
show={props.show}
|
show={props.show}
|
||||||
|
style={{
|
||||||
|
top: `${bannerSize}px`,
|
||||||
|
}}
|
||||||
className="pointer-events-none flex justify-end pb-32 bg-gradient-to-b from-black to-transparent [margin-bottom:env(safe-area-inset-bottom)] transition-opacity duration-200 absolute top-0 w-full"
|
className="pointer-events-none flex justify-end pb-32 bg-gradient-to-b from-black to-transparent [margin-bottom:env(safe-area-inset-bottom)] transition-opacity duration-200 absolute top-0 w-full"
|
||||||
/>
|
/>
|
||||||
|
<div className="relative z-10">
|
||||||
|
<BannerLocation location="player" />
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
onMouseOver={() => setHoveringAnyControls(true)}
|
onMouseOver={() => setHoveringAnyControls(true)}
|
||||||
onMouseOut={() => setHoveringAnyControls(false)}
|
onMouseOut={() => setHoveringAnyControls(false)}
|
||||||
className="pointer-events-auto pl-[calc(2rem+env(safe-area-inset-left))] pr-[calc(2rem+env(safe-area-inset-right))] pt-6 absolute top-0 w-full"
|
className="pointer-events-auto pl-[calc(2rem+env(safe-area-inset-left))] pr-[calc(2rem+env(safe-area-inset-right))] pt-6 absolute top-0 w-full"
|
||||||
|
style={{
|
||||||
|
top: `${bannerSize}px`,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Transition
|
<Transition
|
||||||
animation="slide-down"
|
animation="slide-down"
|
||||||
|
@ -1,61 +0,0 @@
|
|||||||
import {
|
|
||||||
Dispatch,
|
|
||||||
ReactNode,
|
|
||||||
SetStateAction,
|
|
||||||
createContext,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import { useMeasure } from "react-use";
|
|
||||||
|
|
||||||
interface BannerInstance {
|
|
||||||
id: string;
|
|
||||||
height: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const BannerContext = createContext<
|
|
||||||
[BannerInstance[], Dispatch<SetStateAction<BannerInstance[]>>]
|
|
||||||
>(null as any);
|
|
||||||
|
|
||||||
export function BannerContextProvider(props: { children: ReactNode }) {
|
|
||||||
const [state, setState] = useState<BannerInstance[]>([]);
|
|
||||||
const memod = useMemo<
|
|
||||||
[BannerInstance[], Dispatch<SetStateAction<BannerInstance[]>>]
|
|
||||||
>(() => [state, setState], [state]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BannerContext.Provider value={memod}>
|
|
||||||
{props.children}
|
|
||||||
</BannerContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useBanner<T extends Element>(id: string) {
|
|
||||||
const [ref, { height }] = useMeasure<T>();
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const [_, set] = useContext(BannerContext);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
set((v) => [...v, { id, height: 0 }]);
|
|
||||||
set((value) => {
|
|
||||||
const v = value.find((item) => item.id === id);
|
|
||||||
if (v) {
|
|
||||||
v.height = height;
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
});
|
|
||||||
return () => {
|
|
||||||
set((v) => v.filter((item) => item.id !== id));
|
|
||||||
};
|
|
||||||
}, [height, id, set]);
|
|
||||||
|
|
||||||
return [ref];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useBannerSize() {
|
|
||||||
const [val] = useContext(BannerContext);
|
|
||||||
|
|
||||||
return val.reduce((a, v) => a + v.height, 0);
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
import { useCallback } from "react";
|
|
||||||
import { useHistory } from "react-router-dom";
|
|
||||||
|
|
||||||
export function useGoBack() {
|
|
||||||
const reactHistory = useHistory();
|
|
||||||
|
|
||||||
const goBack = useCallback(() => {
|
|
||||||
if (reactHistory.action !== "POP") reactHistory.goBack();
|
|
||||||
else reactHistory.push("/");
|
|
||||||
}, [reactHistory]);
|
|
||||||
return goBack;
|
|
||||||
}
|
|
@ -1,7 +1,9 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
export function useIsOnline() {
|
import { useBannerStore } from "@/stores/banner";
|
||||||
const [online, setOnline] = useState<boolean | null>(true);
|
|
||||||
|
export function useOnlineListener() {
|
||||||
|
const updateOnline = useBannerStore((s) => s.updateOnline);
|
||||||
const ref = useRef<boolean>(true);
|
const ref = useRef<boolean>(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -21,12 +23,12 @@ export function useIsOnline() {
|
|||||||
const signal = abort.signal;
|
const signal = abort.signal;
|
||||||
fetch("/ping.txt", { signal })
|
fetch("/ping.txt", { signal })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setOnline(true);
|
updateOnline(true);
|
||||||
ref.current = true;
|
ref.current = true;
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (err.name === "AbortError") return;
|
if (err.name === "AbortError") return;
|
||||||
setOnline(false);
|
updateOnline(false);
|
||||||
ref.current = false;
|
ref.current = false;
|
||||||
});
|
});
|
||||||
}, 5000);
|
}, 5000);
|
||||||
@ -35,7 +37,5 @@ export function useIsOnline() {
|
|||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
if (abort) abort.abort();
|
if (abort) abort.abort();
|
||||||
};
|
};
|
||||||
}, []);
|
}, [updateOnline]);
|
||||||
|
|
||||||
return online;
|
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,6 @@ import { assertConfig, conf } from "@/setup/config";
|
|||||||
import i18n from "@/setup/i18n";
|
import i18n from "@/setup/i18n";
|
||||||
|
|
||||||
import "@/setup/ga";
|
import "@/setup/ga";
|
||||||
import "@/setup/sentry";
|
|
||||||
import "@/setup/index.css";
|
import "@/setup/index.css";
|
||||||
import { initializeChromecast } from "./setup/chromecast";
|
import { initializeChromecast } from "./setup/chromecast";
|
||||||
import { SettingsStore } from "./state/settings/store";
|
import { SettingsStore } from "./state/settings/store";
|
||||||
|
@ -40,7 +40,7 @@ export function HomePage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<HomeLayout showBg={showBg}>
|
<HomeLayout showBg={showBg}>
|
||||||
<div className="relative z-10 mb-16 sm:mb-24">
|
<div className="mb-16 sm:mb-24">
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>{t("global.name")}</title>
|
<title>{t("global.name")}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
|
@ -5,8 +5,8 @@ import Sticky from "react-stickynode";
|
|||||||
import { ThinContainer } from "@/components/layout/ThinContainer";
|
import { ThinContainer } from "@/components/layout/ThinContainer";
|
||||||
import { SearchBarInput } from "@/components/SearchBar";
|
import { SearchBarInput } from "@/components/SearchBar";
|
||||||
import { Title } from "@/components/text/Title";
|
import { Title } from "@/components/text/Title";
|
||||||
import { useBannerSize } from "@/hooks/useBanner";
|
|
||||||
import { useSearchQuery } from "@/hooks/useSearchQuery";
|
import { useSearchQuery } from "@/hooks/useSearchQuery";
|
||||||
|
import { useBannerSize } from "@/stores/banner";
|
||||||
|
|
||||||
export interface HeroPartProps {
|
export interface HeroPartProps {
|
||||||
setIsSticky: (val: boolean) => void;
|
setIsSticky: (val: boolean) => void;
|
||||||
|
@ -10,7 +10,7 @@ import {
|
|||||||
|
|
||||||
import { convertLegacyUrl, isLegacyUrl } from "@/backend/metadata/getmeta";
|
import { convertLegacyUrl, isLegacyUrl } from "@/backend/metadata/getmeta";
|
||||||
import { generateQuickSearchMediaUrl } from "@/backend/metadata/tmdb";
|
import { generateQuickSearchMediaUrl } from "@/backend/metadata/tmdb";
|
||||||
import { BannerContextProvider } from "@/hooks/useBanner";
|
import { useOnlineListener } from "@/hooks/usePing";
|
||||||
import { AboutPage } from "@/pages/About";
|
import { AboutPage } from "@/pages/About";
|
||||||
import { DmcaPage } from "@/pages/Dmca";
|
import { DmcaPage } from "@/pages/Dmca";
|
||||||
import { NotFoundPage } from "@/pages/errors/NotFoundPage";
|
import { NotFoundPage } from "@/pages/errors/NotFoundPage";
|
||||||
@ -57,76 +57,75 @@ function QuickSearch() {
|
|||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
useHistoryListener();
|
useHistoryListener();
|
||||||
|
useOnlineListener();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsProvider>
|
<SettingsProvider>
|
||||||
<WatchedContextProvider>
|
<WatchedContextProvider>
|
||||||
<BookmarkContextProvider>
|
<BookmarkContextProvider>
|
||||||
<BannerContextProvider>
|
<Layout>
|
||||||
<Layout>
|
<Switch>
|
||||||
<Switch>
|
{/* functional routes */}
|
||||||
{/* functional routes */}
|
<Route exact path="/s/:query">
|
||||||
<Route exact path="/s/:query">
|
<QuickSearch />
|
||||||
<QuickSearch />
|
</Route>
|
||||||
</Route>
|
<Route exact path="/search/:type">
|
||||||
<Route exact path="/search/:type">
|
<Redirect to="/browse" push={false} />
|
||||||
<Redirect to="/browse" push={false} />
|
</Route>
|
||||||
</Route>
|
<Route exact path="/search/:type/:query?">
|
||||||
<Route exact path="/search/:type/:query?">
|
{({ match }) => {
|
||||||
{({ match }) => {
|
if (match?.params.query)
|
||||||
if (match?.params.query)
|
return (
|
||||||
return (
|
<Redirect
|
||||||
<Redirect
|
to={`/browse/${match?.params.query}`}
|
||||||
to={`/browse/${match?.params.query}`}
|
push={false}
|
||||||
push={false}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
return <Redirect to="/browse" push={false} />;
|
||||||
return <Redirect to="/browse" push={false} />;
|
}}
|
||||||
}}
|
</Route>
|
||||||
</Route>
|
|
||||||
|
|
||||||
{/* pages */}
|
{/* pages */}
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
path={["/media/:media", "/media/:media/:season/:episode"]}
|
path={["/media/:media", "/media/:media/:season/:episode"]}
|
||||||
>
|
>
|
||||||
<LegacyUrlView>
|
<LegacyUrlView>
|
||||||
<PlayerView />
|
<PlayerView />
|
||||||
</LegacyUrlView>
|
</LegacyUrlView>
|
||||||
</Route>
|
</Route>
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
path={["/browse/:query?", "/"]}
|
path={["/browse/:query?", "/"]}
|
||||||
component={HomePage}
|
component={HomePage}
|
||||||
/>
|
/>
|
||||||
<Route exact path="/faq" component={AboutPage} />
|
<Route exact path="/faq" component={AboutPage} />
|
||||||
<Route exact path="/dmca" component={DmcaPage} />
|
<Route exact path="/dmca" component={DmcaPage} />
|
||||||
|
|
||||||
{/* other */}
|
{/* other */}
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
path="/dev"
|
||||||
|
component={lazy(() => import("@/pages/DeveloperPage"))}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
path="/dev/video"
|
||||||
|
component={lazy(
|
||||||
|
() => import("@/pages/developer/VideoTesterView")
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{/* developer routes that can abuse workers are disabled in production */}
|
||||||
|
{process.env.NODE_ENV === "development" ? (
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
path="/dev"
|
path="/dev/test"
|
||||||
component={lazy(() => import("@/pages/DeveloperPage"))}
|
component={lazy(() => import("@/pages/developer/TestView"))}
|
||||||
/>
|
/>
|
||||||
<Route
|
) : null}
|
||||||
exact
|
<Route path="*" component={NotFoundPage} />
|
||||||
path="/dev/video"
|
</Switch>
|
||||||
component={lazy(
|
</Layout>
|
||||||
() => import("@/pages/developer/VideoTesterView")
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{/* developer routes that can abuse workers are disabled in production */}
|
|
||||||
{process.env.NODE_ENV === "development" ? (
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path="/dev/test"
|
|
||||||
component={lazy(() => import("@/pages/developer/TestView"))}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
<Route path="*" component={NotFoundPage} />
|
|
||||||
</Switch>
|
|
||||||
</Layout>
|
|
||||||
</BannerContextProvider>
|
|
||||||
</BookmarkContextProvider>
|
</BookmarkContextProvider>
|
||||||
</WatchedContextProvider>
|
</WatchedContextProvider>
|
||||||
</SettingsProvider>
|
</SettingsProvider>
|
||||||
|
@ -1,23 +1,20 @@
|
|||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
import { Banner } from "@/components/Banner";
|
import { useBannerSize, useBannerStore } from "@/stores/banner";
|
||||||
import { useBannerSize } from "@/hooks/useBanner";
|
import { BannerLocation } from "@/stores/banner/BannerLocation";
|
||||||
import { useIsOnline } from "@/hooks/usePing";
|
|
||||||
|
|
||||||
export function Layout(props: { children: ReactNode }) {
|
export function Layout(props: { children: ReactNode }) {
|
||||||
const { t } = useTranslation();
|
|
||||||
const isOnline = useIsOnline();
|
|
||||||
const bannerSize = useBannerSize();
|
const bannerSize = useBannerSize();
|
||||||
|
const location = useBannerStore((s) => s.location);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="fixed inset-x-0 z-[1000]">
|
<div className="fixed inset-x-0 z-[1000]">
|
||||||
{!isOnline ? <Banner type="error">{t("errors.offline")}</Banner> : null}
|
<BannerLocation />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
paddingTop: `${bannerSize}px`,
|
paddingTop: location === null ? `${bannerSize}px` : "0px",
|
||||||
}}
|
}}
|
||||||
className="flex min-h-screen flex-col"
|
className="flex min-h-screen flex-col"
|
||||||
>
|
>
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
import { CaptureConsole, HttpClient } from "@sentry/integrations";
|
|
||||||
import * as Sentry from "@sentry/react";
|
|
||||||
|
|
||||||
import { conf } from "@/setup/config";
|
|
||||||
import { SENTRY_DSN } from "@/setup/constants";
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== "development")
|
|
||||||
Sentry.init({
|
|
||||||
dsn: SENTRY_DSN,
|
|
||||||
release: `movie-web@${conf().APP_VERSION}`,
|
|
||||||
sampleRate: 0.5,
|
|
||||||
integrations: [
|
|
||||||
new Sentry.BrowserTracing(),
|
|
||||||
new CaptureConsole(),
|
|
||||||
new HttpClient(),
|
|
||||||
],
|
|
||||||
});
|
|
63
src/stores/banner/BannerLocation.tsx
Normal file
63
src/stores/banner/BannerLocation.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
|
import { useBannerStore, useRegisterBanner } from "@/stores/banner";
|
||||||
|
|
||||||
|
export function Banner(props: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
type: "error";
|
||||||
|
id: string;
|
||||||
|
}) {
|
||||||
|
const [ref] = useRegisterBanner<HTMLDivElement>(props.id);
|
||||||
|
const styles = {
|
||||||
|
error: "bg-[#C93957] text-white",
|
||||||
|
};
|
||||||
|
const icons = {
|
||||||
|
error: Icons.CIRCLE_EXCLAMATION,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref}>
|
||||||
|
<div
|
||||||
|
className={[
|
||||||
|
styles[props.type],
|
||||||
|
"flex items-center justify-center p-1",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<Icon icon={icons[props.type]} />
|
||||||
|
<div>{props.children}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BannerLocation(props: { location?: string }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const isOnline = useBannerStore((s) => s.isOnline);
|
||||||
|
const setLocation = useBannerStore((s) => s.setLocation);
|
||||||
|
const currentLocation = useBannerStore((s) => s.location);
|
||||||
|
const loc = props.location ?? null;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!loc) return;
|
||||||
|
setLocation(loc);
|
||||||
|
return () => {
|
||||||
|
setLocation(null);
|
||||||
|
};
|
||||||
|
}, [setLocation, loc]);
|
||||||
|
|
||||||
|
if (currentLocation !== loc) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{!isOnline ? (
|
||||||
|
<Banner id="offline" type="error">
|
||||||
|
{t("errors.offline")}
|
||||||
|
</Banner>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
88
src/stores/banner/index.ts
Normal file
88
src/stores/banner/index.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { useMeasure } from "react-use";
|
||||||
|
import { create } from "zustand";
|
||||||
|
import { immer } from "zustand/middleware/immer";
|
||||||
|
|
||||||
|
interface BannerInstance {
|
||||||
|
id: string;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BannerStore {
|
||||||
|
banners: BannerInstance[];
|
||||||
|
isOnline: boolean;
|
||||||
|
location: string | null;
|
||||||
|
updateHeight(id: string, height: number): void;
|
||||||
|
showBanner(id: string): void;
|
||||||
|
hideBanner(id: string): void;
|
||||||
|
setLocation(loc: string | null): void;
|
||||||
|
updateOnline(isOnline: boolean): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useBannerStore = create(
|
||||||
|
immer<BannerStore>((set) => ({
|
||||||
|
banners: [],
|
||||||
|
isOnline: true,
|
||||||
|
location: null,
|
||||||
|
updateOnline(isOnline) {
|
||||||
|
set((s) => {
|
||||||
|
s.isOnline = isOnline;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setLocation(loc) {
|
||||||
|
set((s) => {
|
||||||
|
s.location = loc;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
showBanner(id) {
|
||||||
|
set((s) => {
|
||||||
|
if (s.banners.find((v) => v.id === id)) return;
|
||||||
|
s.banners.push({
|
||||||
|
id,
|
||||||
|
height: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
hideBanner(id) {
|
||||||
|
set((s) => {
|
||||||
|
s.banners = s.banners.filter((v) => v.id !== id);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
updateHeight(id, height) {
|
||||||
|
set((s) => {
|
||||||
|
const found = s.banners.find((v) => v.id === id);
|
||||||
|
if (found) found.height = height;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
export function useBannerSize(location?: string) {
|
||||||
|
const loc = location ?? null;
|
||||||
|
const banners = useBannerStore((s) => s.banners);
|
||||||
|
const currentLocation = useBannerStore((s) => s.location);
|
||||||
|
|
||||||
|
const size = banners.reduce((a, v) => a + v.height, 0);
|
||||||
|
if (loc !== currentLocation) return 0;
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useRegisterBanner<T extends Element>(id: string) {
|
||||||
|
const [ref, { height }] = useMeasure<T>();
|
||||||
|
const updateHeight = useBannerStore((s) => s.updateHeight);
|
||||||
|
const showBanner = useBannerStore((s) => s.showBanner);
|
||||||
|
const hideBanner = useBannerStore((s) => s.hideBanner);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
showBanner(id);
|
||||||
|
return () => {
|
||||||
|
hideBanner(id);
|
||||||
|
};
|
||||||
|
}, [showBanner, hideBanner, id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateHeight(id, height);
|
||||||
|
}, [height, id, updateHeight]);
|
||||||
|
|
||||||
|
return [ref];
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user