15 changed files with 257 additions and 207 deletions
@ -1,28 +0,0 @@
@@ -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> |
||||
); |
||||
} |
||||
@ -1,61 +0,0 @@
@@ -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 @@
@@ -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,17 +0,0 @@
@@ -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(), |
||||
], |
||||
}); |
||||
@ -0,0 +1,63 @@
@@ -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> |
||||
); |
||||
} |
||||
@ -0,0 +1,88 @@
@@ -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