tangled
alpha
login
or
join now
dunkirk.sh
/
pstream-ng
1
fork
atom
pstream is dead; long live pstream
taciturnaxolotl.github.io/pstream-ng/
1
fork
atom
overview
issues
pulls
pipelines
banners in video player
mrjvs
2 years ago
b5dae824
294f31c5
+257
-207
15 changed files
expand all
collapse all
unified
split
src
components
Banner.tsx
Transition.tsx
layout
Navigation.tsx
player
base
TopControls.tsx
hooks
useBanner.tsx
useGoBack.ts
usePing.ts
index.tsx
pages
HomePage.tsx
parts
home
HeroPart.tsx
setup
App.tsx
Layout.tsx
sentry.tsx
stores
banner
BannerLocation.tsx
index.ts
-28
src/components/Banner.tsx
···
1
1
-
import { Icon, Icons } from "@/components/Icon";
2
2
-
import { useBanner } from "@/hooks/useBanner";
3
3
-
4
4
-
export function Banner(props: { children: React.ReactNode; type: "error" }) {
5
5
-
const [ref] = useBanner<HTMLDivElement>("internet");
6
6
-
const styles = {
7
7
-
error: "bg-[#C93957] text-white",
8
8
-
};
9
9
-
const icons = {
10
10
-
error: Icons.CIRCLE_EXCLAMATION,
11
11
-
};
12
12
-
13
13
-
return (
14
14
-
<div ref={ref}>
15
15
-
<div
16
16
-
className={[
17
17
-
styles[props.type],
18
18
-
"flex items-center justify-center p-1",
19
19
-
].join(" ")}
20
20
-
>
21
21
-
<div className="flex items-center space-x-3">
22
22
-
<Icon icon={icons[props.type]} />
23
23
-
<div>{props.children}</div>
24
24
-
</div>
25
25
-
</div>
26
26
-
</div>
27
27
-
);
28
28
-
}
+8
-3
src/components/Transition.tsx
···
2
2
Transition as HeadlessTransition,
3
3
TransitionClasses,
4
4
} from "@headlessui/react";
5
5
-
import { Fragment, ReactNode } from "react";
5
5
+
import { CSSProperties, Fragment, ReactNode } from "react";
6
6
7
7
export type TransitionAnimations =
8
8
| "slide-down"
···
19
19
className?: string;
20
20
children?: ReactNode;
21
21
isChild?: boolean;
22
22
+
style?: CSSProperties;
22
23
}
23
24
24
25
function getClasses(
···
90
91
if (props.isChild) {
91
92
return (
92
93
<HeadlessTransition.Child as={Fragment} {...classes}>
93
93
-
<div className={props.className}>{props.children}</div>
94
94
+
<div className={props.className} style={props.style}>
95
95
+
{props.children}
96
96
+
</div>
94
97
</HeadlessTransition.Child>
95
98
);
96
99
}
97
100
98
101
return (
99
102
<HeadlessTransition show={props.show} as={Fragment} {...classes}>
100
100
-
<div className={props.className}>{props.children}</div>
103
103
+
<div className={props.className} style={props.style}>
104
104
+
{props.children}
105
105
+
</div>
101
106
</HeadlessTransition>
102
107
);
103
108
}
+11
-6
src/components/layout/Navigation.tsx
···
4
4
import { IconPatch } from "@/components/buttons/IconPatch";
5
5
import { Icons } from "@/components/Icon";
6
6
import { Lightbar } from "@/components/utils/Lightbar";
7
7
-
import { useBannerSize } from "@/hooks/useBanner";
8
7
import { conf } from "@/setup/config";
8
8
+
import { useBannerSize } from "@/stores/banner";
9
9
10
10
import { BrandPill } from "./BrandPill";
11
11
···
20
20
return (
21
21
<>
22
22
{!props.noLightbar ? (
23
23
-
<div className="absolute inset-x-0 top-0 flex h-[88px] items-center justify-center">
23
23
+
<div
24
24
+
className="absolute inset-x-0 top-0 flex h-[88px] items-center justify-center"
25
25
+
style={{
26
26
+
top: `${bannerHeight}px`,
27
27
+
}}
28
28
+
>
24
29
<div className="absolute inset-x-0 -mt-[22%] flex items-center sm:mt-0">
25
30
<Lightbar />
26
31
</div>
27
32
</div>
28
33
) : null}
29
34
<div
30
30
-
className="fixed left-0 right-0 top-0 z-10 min-h-[150px]"
35
35
+
className="fixed pointer-events-none left-0 right-0 top-0 z-10 min-h-[150px]"
31
36
style={{
32
37
top: `${bannerHeight}px`,
33
38
}}
34
39
>
35
35
-
<div className="fixed left-0 right-0 flex items-center justify-between px-7 py-5">
40
40
+
<div className="fixed left-0 right-0 flex items-center">
36
41
<div
37
42
className={`${
38
43
props.bg ? "opacity-100" : "opacity-0"
39
44
} absolute inset-0 block bg-background-main transition-opacity duration-300`}
40
45
>
41
41
-
<div className="pointer-events-none absolute -bottom-24 h-24 w-full bg-gradient-to-b from-background-main to-transparent" />
46
46
+
<div className="absolute -bottom-24 h-24 w-full bg-gradient-to-b from-background-main to-transparent" />
42
47
</div>
43
43
-
<div className="relative flex w-full items-center sm:w-fit space-x-3">
48
48
+
<div className="pointer-events-auto px-7 py-5 relative flex flex-1 items-center space-x-3">
44
49
<Link className="block" to="/">
45
50
<BrandPill clickable />
46
51
</Link>
+12
src/components/player/base/TopControls.tsx
···
1
1
import { useEffect } from "react";
2
2
3
3
import { Transition } from "@/components/Transition";
4
4
+
import { useBannerSize } from "@/stores/banner";
5
5
+
import { BannerLocation } from "@/stores/banner/BannerLocation";
4
6
import { usePlayerStore } from "@/stores/player/store";
5
7
6
8
export function TopControls(props: {
7
9
show?: boolean;
8
10
children: React.ReactNode;
9
11
}) {
12
12
+
const bannerSize = useBannerSize("player");
10
13
const setHoveringAnyControls = usePlayerStore(
11
14
(s) => s.setHoveringAnyControls
12
15
);
···
22
25
<Transition
23
26
animation="fade"
24
27
show={props.show}
28
28
+
style={{
29
29
+
top: `${bannerSize}px`,
30
30
+
}}
25
31
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"
26
32
/>
33
33
+
<div className="relative z-10">
34
34
+
<BannerLocation location="player" />
35
35
+
</div>
27
36
<div
28
37
onMouseOver={() => setHoveringAnyControls(true)}
29
38
onMouseOut={() => setHoveringAnyControls(false)}
30
39
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"
40
40
+
style={{
41
41
+
top: `${bannerSize}px`,
42
42
+
}}
31
43
>
32
44
<Transition
33
45
animation="slide-down"
-61
src/hooks/useBanner.tsx
···
1
1
-
import {
2
2
-
Dispatch,
3
3
-
ReactNode,
4
4
-
SetStateAction,
5
5
-
createContext,
6
6
-
useContext,
7
7
-
useEffect,
8
8
-
useMemo,
9
9
-
useState,
10
10
-
} from "react";
11
11
-
import { useMeasure } from "react-use";
12
12
-
13
13
-
interface BannerInstance {
14
14
-
id: string;
15
15
-
height: number;
16
16
-
}
17
17
-
18
18
-
const BannerContext = createContext<
19
19
-
[BannerInstance[], Dispatch<SetStateAction<BannerInstance[]>>]
20
20
-
>(null as any);
21
21
-
22
22
-
export function BannerContextProvider(props: { children: ReactNode }) {
23
23
-
const [state, setState] = useState<BannerInstance[]>([]);
24
24
-
const memod = useMemo<
25
25
-
[BannerInstance[], Dispatch<SetStateAction<BannerInstance[]>>]
26
26
-
>(() => [state, setState], [state]);
27
27
-
28
28
-
return (
29
29
-
<BannerContext.Provider value={memod}>
30
30
-
{props.children}
31
31
-
</BannerContext.Provider>
32
32
-
);
33
33
-
}
34
34
-
35
35
-
export function useBanner<T extends Element>(id: string) {
36
36
-
const [ref, { height }] = useMeasure<T>();
37
37
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
38
38
-
const [_, set] = useContext(BannerContext);
39
39
-
40
40
-
useEffect(() => {
41
41
-
set((v) => [...v, { id, height: 0 }]);
42
42
-
set((value) => {
43
43
-
const v = value.find((item) => item.id === id);
44
44
-
if (v) {
45
45
-
v.height = height;
46
46
-
}
47
47
-
return value;
48
48
-
});
49
49
-
return () => {
50
50
-
set((v) => v.filter((item) => item.id !== id));
51
51
-
};
52
52
-
}, [height, id, set]);
53
53
-
54
54
-
return [ref];
55
55
-
}
56
56
-
57
57
-
export function useBannerSize() {
58
58
-
const [val] = useContext(BannerContext);
59
59
-
60
60
-
return val.reduce((a, v) => a + v.height, 0);
61
61
-
}
-12
src/hooks/useGoBack.ts
···
1
1
-
import { useCallback } from "react";
2
2
-
import { useHistory } from "react-router-dom";
3
3
-
4
4
-
export function useGoBack() {
5
5
-
const reactHistory = useHistory();
6
6
-
7
7
-
const goBack = useCallback(() => {
8
8
-
if (reactHistory.action !== "POP") reactHistory.goBack();
9
9
-
else reactHistory.push("/");
10
10
-
}, [reactHistory]);
11
11
-
return goBack;
12
12
-
}
+8
-8
src/hooks/usePing.ts
···
1
1
-
import { useEffect, useRef, useState } from "react";
1
1
+
import { useEffect, useRef } from "react";
2
2
3
3
-
export function useIsOnline() {
4
4
-
const [online, setOnline] = useState<boolean | null>(true);
3
3
+
import { useBannerStore } from "@/stores/banner";
4
4
+
5
5
+
export function useOnlineListener() {
6
6
+
const updateOnline = useBannerStore((s) => s.updateOnline);
5
7
const ref = useRef<boolean>(true);
6
8
7
9
useEffect(() => {
···
21
23
const signal = abort.signal;
22
24
fetch("/ping.txt", { signal })
23
25
.then(() => {
24
24
-
setOnline(true);
26
26
+
updateOnline(true);
25
27
ref.current = true;
26
28
})
27
29
.catch((err) => {
28
30
if (err.name === "AbortError") return;
29
29
-
setOnline(false);
31
31
+
updateOnline(false);
30
32
ref.current = false;
31
33
});
32
34
}, 5000);
···
35
37
clearInterval(interval);
36
38
if (abort) abort.abort();
37
39
};
38
38
-
}, []);
39
39
-
40
40
-
return online;
40
40
+
}, [updateOnline]);
41
41
}
-1
src/index.tsx
···
12
12
import i18n from "@/setup/i18n";
13
13
14
14
import "@/setup/ga";
15
15
-
import "@/setup/sentry";
16
15
import "@/setup/index.css";
17
16
import { initializeChromecast } from "./setup/chromecast";
18
17
import { SettingsStore } from "./state/settings/store";
+1
-1
src/pages/HomePage.tsx
···
40
40
41
41
return (
42
42
<HomeLayout showBg={showBg}>
43
43
-
<div className="relative z-10 mb-16 sm:mb-24">
43
43
+
<div className="mb-16 sm:mb-24">
44
44
<Helmet>
45
45
<title>{t("global.name")}</title>
46
46
</Helmet>
+1
-1
src/pages/parts/home/HeroPart.tsx
···
5
5
import { ThinContainer } from "@/components/layout/ThinContainer";
6
6
import { SearchBarInput } from "@/components/SearchBar";
7
7
import { Title } from "@/components/text/Title";
8
8
-
import { useBannerSize } from "@/hooks/useBanner";
9
8
import { useSearchQuery } from "@/hooks/useSearchQuery";
9
9
+
import { useBannerSize } from "@/stores/banner";
10
10
11
11
export interface HeroPartProps {
12
12
setIsSticky: (val: boolean) => void;
+60
-61
src/setup/App.tsx
···
10
10
11
11
import { convertLegacyUrl, isLegacyUrl } from "@/backend/metadata/getmeta";
12
12
import { generateQuickSearchMediaUrl } from "@/backend/metadata/tmdb";
13
13
-
import { BannerContextProvider } from "@/hooks/useBanner";
13
13
+
import { useOnlineListener } from "@/hooks/usePing";
14
14
import { AboutPage } from "@/pages/About";
15
15
import { DmcaPage } from "@/pages/Dmca";
16
16
import { NotFoundPage } from "@/pages/errors/NotFoundPage";
···
57
57
58
58
function App() {
59
59
useHistoryListener();
60
60
+
useOnlineListener();
60
61
61
62
return (
62
63
<SettingsProvider>
63
64
<WatchedContextProvider>
64
65
<BookmarkContextProvider>
65
65
-
<BannerContextProvider>
66
66
-
<Layout>
67
67
-
<Switch>
68
68
-
{/* functional routes */}
69
69
-
<Route exact path="/s/:query">
70
70
-
<QuickSearch />
71
71
-
</Route>
72
72
-
<Route exact path="/search/:type">
73
73
-
<Redirect to="/browse" push={false} />
74
74
-
</Route>
75
75
-
<Route exact path="/search/:type/:query?">
76
76
-
{({ match }) => {
77
77
-
if (match?.params.query)
78
78
-
return (
79
79
-
<Redirect
80
80
-
to={`/browse/${match?.params.query}`}
81
81
-
push={false}
82
82
-
/>
83
83
-
);
84
84
-
return <Redirect to="/browse" push={false} />;
85
85
-
}}
86
86
-
</Route>
66
66
+
<Layout>
67
67
+
<Switch>
68
68
+
{/* functional routes */}
69
69
+
<Route exact path="/s/:query">
70
70
+
<QuickSearch />
71
71
+
</Route>
72
72
+
<Route exact path="/search/:type">
73
73
+
<Redirect to="/browse" push={false} />
74
74
+
</Route>
75
75
+
<Route exact path="/search/:type/:query?">
76
76
+
{({ match }) => {
77
77
+
if (match?.params.query)
78
78
+
return (
79
79
+
<Redirect
80
80
+
to={`/browse/${match?.params.query}`}
81
81
+
push={false}
82
82
+
/>
83
83
+
);
84
84
+
return <Redirect to="/browse" push={false} />;
85
85
+
}}
86
86
+
</Route>
87
87
88
88
-
{/* pages */}
89
89
-
<Route
90
90
-
exact
91
91
-
path={["/media/:media", "/media/:media/:season/:episode"]}
92
92
-
>
93
93
-
<LegacyUrlView>
94
94
-
<PlayerView />
95
95
-
</LegacyUrlView>
96
96
-
</Route>
97
97
-
<Route
98
98
-
exact
99
99
-
path={["/browse/:query?", "/"]}
100
100
-
component={HomePage}
101
101
-
/>
102
102
-
<Route exact path="/faq" component={AboutPage} />
103
103
-
<Route exact path="/dmca" component={DmcaPage} />
88
88
+
{/* pages */}
89
89
+
<Route
90
90
+
exact
91
91
+
path={["/media/:media", "/media/:media/:season/:episode"]}
92
92
+
>
93
93
+
<LegacyUrlView>
94
94
+
<PlayerView />
95
95
+
</LegacyUrlView>
96
96
+
</Route>
97
97
+
<Route
98
98
+
exact
99
99
+
path={["/browse/:query?", "/"]}
100
100
+
component={HomePage}
101
101
+
/>
102
102
+
<Route exact path="/faq" component={AboutPage} />
103
103
+
<Route exact path="/dmca" component={DmcaPage} />
104
104
105
105
-
{/* other */}
106
106
-
<Route
107
107
-
exact
108
108
-
path="/dev"
109
109
-
component={lazy(() => import("@/pages/DeveloperPage"))}
110
110
-
/>
105
105
+
{/* other */}
106
106
+
<Route
107
107
+
exact
108
108
+
path="/dev"
109
109
+
component={lazy(() => import("@/pages/DeveloperPage"))}
110
110
+
/>
111
111
+
<Route
112
112
+
exact
113
113
+
path="/dev/video"
114
114
+
component={lazy(
115
115
+
() => import("@/pages/developer/VideoTesterView")
116
116
+
)}
117
117
+
/>
118
118
+
{/* developer routes that can abuse workers are disabled in production */}
119
119
+
{process.env.NODE_ENV === "development" ? (
111
120
<Route
112
121
exact
113
113
-
path="/dev/video"
114
114
-
component={lazy(
115
115
-
() => import("@/pages/developer/VideoTesterView")
116
116
-
)}
122
122
+
path="/dev/test"
123
123
+
component={lazy(() => import("@/pages/developer/TestView"))}
117
124
/>
118
118
-
{/* developer routes that can abuse workers are disabled in production */}
119
119
-
{process.env.NODE_ENV === "development" ? (
120
120
-
<Route
121
121
-
exact
122
122
-
path="/dev/test"
123
123
-
component={lazy(() => import("@/pages/developer/TestView"))}
124
124
-
/>
125
125
-
) : null}
126
126
-
<Route path="*" component={NotFoundPage} />
127
127
-
</Switch>
128
128
-
</Layout>
129
129
-
</BannerContextProvider>
125
125
+
) : null}
126
126
+
<Route path="*" component={NotFoundPage} />
127
127
+
</Switch>
128
128
+
</Layout>
130
129
</BookmarkContextProvider>
131
130
</WatchedContextProvider>
132
131
</SettingsProvider>
+5
-8
src/setup/Layout.tsx
···
1
1
import { ReactNode } from "react";
2
2
-
import { useTranslation } from "react-i18next";
3
2
4
4
-
import { Banner } from "@/components/Banner";
5
5
-
import { useBannerSize } from "@/hooks/useBanner";
6
6
-
import { useIsOnline } from "@/hooks/usePing";
3
3
+
import { useBannerSize, useBannerStore } from "@/stores/banner";
4
4
+
import { BannerLocation } from "@/stores/banner/BannerLocation";
7
5
8
6
export function Layout(props: { children: ReactNode }) {
9
9
-
const { t } = useTranslation();
10
10
-
const isOnline = useIsOnline();
11
7
const bannerSize = useBannerSize();
8
8
+
const location = useBannerStore((s) => s.location);
12
9
13
10
return (
14
11
<div>
15
12
<div className="fixed inset-x-0 z-[1000]">
16
16
-
{!isOnline ? <Banner type="error">{t("errors.offline")}</Banner> : null}
13
13
+
<BannerLocation />
17
14
</div>
18
15
<div
19
16
style={{
20
20
-
paddingTop: `${bannerSize}px`,
17
17
+
paddingTop: location === null ? `${bannerSize}px` : "0px",
21
18
}}
22
19
className="flex min-h-screen flex-col"
23
20
>
-17
src/setup/sentry.tsx
···
1
1
-
import { CaptureConsole, HttpClient } from "@sentry/integrations";
2
2
-
import * as Sentry from "@sentry/react";
3
3
-
4
4
-
import { conf } from "@/setup/config";
5
5
-
import { SENTRY_DSN } from "@/setup/constants";
6
6
-
7
7
-
if (process.env.NODE_ENV !== "development")
8
8
-
Sentry.init({
9
9
-
dsn: SENTRY_DSN,
10
10
-
release: `movie-web@${conf().APP_VERSION}`,
11
11
-
sampleRate: 0.5,
12
12
-
integrations: [
13
13
-
new Sentry.BrowserTracing(),
14
14
-
new CaptureConsole(),
15
15
-
new HttpClient(),
16
16
-
],
17
17
-
});
+63
src/stores/banner/BannerLocation.tsx
···
1
1
+
import { useEffect } from "react";
2
2
+
import { useTranslation } from "react-i18next";
3
3
+
4
4
+
import { Icon, Icons } from "@/components/Icon";
5
5
+
import { useBannerStore, useRegisterBanner } from "@/stores/banner";
6
6
+
7
7
+
export function Banner(props: {
8
8
+
children: React.ReactNode;
9
9
+
type: "error";
10
10
+
id: string;
11
11
+
}) {
12
12
+
const [ref] = useRegisterBanner<HTMLDivElement>(props.id);
13
13
+
const styles = {
14
14
+
error: "bg-[#C93957] text-white",
15
15
+
};
16
16
+
const icons = {
17
17
+
error: Icons.CIRCLE_EXCLAMATION,
18
18
+
};
19
19
+
20
20
+
return (
21
21
+
<div ref={ref}>
22
22
+
<div
23
23
+
className={[
24
24
+
styles[props.type],
25
25
+
"flex items-center justify-center p-1",
26
26
+
].join(" ")}
27
27
+
>
28
28
+
<div className="flex items-center space-x-3">
29
29
+
<Icon icon={icons[props.type]} />
30
30
+
<div>{props.children}</div>
31
31
+
</div>
32
32
+
</div>
33
33
+
</div>
34
34
+
);
35
35
+
}
36
36
+
37
37
+
export function BannerLocation(props: { location?: string }) {
38
38
+
const { t } = useTranslation();
39
39
+
const isOnline = useBannerStore((s) => s.isOnline);
40
40
+
const setLocation = useBannerStore((s) => s.setLocation);
41
41
+
const currentLocation = useBannerStore((s) => s.location);
42
42
+
const loc = props.location ?? null;
43
43
+
44
44
+
useEffect(() => {
45
45
+
if (!loc) return;
46
46
+
setLocation(loc);
47
47
+
return () => {
48
48
+
setLocation(null);
49
49
+
};
50
50
+
}, [setLocation, loc]);
51
51
+
52
52
+
if (currentLocation !== loc) return null;
53
53
+
54
54
+
return (
55
55
+
<div>
56
56
+
{!isOnline ? (
57
57
+
<Banner id="offline" type="error">
58
58
+
{t("errors.offline")}
59
59
+
</Banner>
60
60
+
) : null}
61
61
+
</div>
62
62
+
);
63
63
+
}
+88
src/stores/banner/index.ts
···
1
1
+
import { useEffect } from "react";
2
2
+
import { useMeasure } from "react-use";
3
3
+
import { create } from "zustand";
4
4
+
import { immer } from "zustand/middleware/immer";
5
5
+
6
6
+
interface BannerInstance {
7
7
+
id: string;
8
8
+
height: number;
9
9
+
}
10
10
+
11
11
+
interface BannerStore {
12
12
+
banners: BannerInstance[];
13
13
+
isOnline: boolean;
14
14
+
location: string | null;
15
15
+
updateHeight(id: string, height: number): void;
16
16
+
showBanner(id: string): void;
17
17
+
hideBanner(id: string): void;
18
18
+
setLocation(loc: string | null): void;
19
19
+
updateOnline(isOnline: boolean): void;
20
20
+
}
21
21
+
22
22
+
export const useBannerStore = create(
23
23
+
immer<BannerStore>((set) => ({
24
24
+
banners: [],
25
25
+
isOnline: true,
26
26
+
location: null,
27
27
+
updateOnline(isOnline) {
28
28
+
set((s) => {
29
29
+
s.isOnline = isOnline;
30
30
+
});
31
31
+
},
32
32
+
setLocation(loc) {
33
33
+
set((s) => {
34
34
+
s.location = loc;
35
35
+
});
36
36
+
},
37
37
+
showBanner(id) {
38
38
+
set((s) => {
39
39
+
if (s.banners.find((v) => v.id === id)) return;
40
40
+
s.banners.push({
41
41
+
id,
42
42
+
height: 0,
43
43
+
});
44
44
+
});
45
45
+
},
46
46
+
hideBanner(id) {
47
47
+
set((s) => {
48
48
+
s.banners = s.banners.filter((v) => v.id !== id);
49
49
+
});
50
50
+
},
51
51
+
updateHeight(id, height) {
52
52
+
set((s) => {
53
53
+
const found = s.banners.find((v) => v.id === id);
54
54
+
if (found) found.height = height;
55
55
+
});
56
56
+
},
57
57
+
}))
58
58
+
);
59
59
+
60
60
+
export function useBannerSize(location?: string) {
61
61
+
const loc = location ?? null;
62
62
+
const banners = useBannerStore((s) => s.banners);
63
63
+
const currentLocation = useBannerStore((s) => s.location);
64
64
+
65
65
+
const size = banners.reduce((a, v) => a + v.height, 0);
66
66
+
if (loc !== currentLocation) return 0;
67
67
+
return size;
68
68
+
}
69
69
+
70
70
+
export function useRegisterBanner<T extends Element>(id: string) {
71
71
+
const [ref, { height }] = useMeasure<T>();
72
72
+
const updateHeight = useBannerStore((s) => s.updateHeight);
73
73
+
const showBanner = useBannerStore((s) => s.showBanner);
74
74
+
const hideBanner = useBannerStore((s) => s.hideBanner);
75
75
+
76
76
+
useEffect(() => {
77
77
+
showBanner(id);
78
78
+
return () => {
79
79
+
hideBanner(id);
80
80
+
};
81
81
+
}, [showBanner, hideBanner, id]);
82
82
+
83
83
+
useEffect(() => {
84
84
+
updateHeight(id, height);
85
85
+
}, [height, id, updateHeight]);
86
86
+
87
87
+
return [ref];
88
88
+
}