28 changed files with 1995 additions and 1674 deletions
@ -1,5 +1,8 @@ |
|||||||
{ |
{ |
||||||
"editor.formatOnSave": true, |
"editor.formatOnSave": true, |
||||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint", |
"editor.defaultFormatter": "dbaeumer.vscode-eslint", |
||||||
"eslint.format.enable": true |
"eslint.format.enable": true, |
||||||
|
"[json]": { |
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode" |
||||||
|
} |
||||||
} |
} |
||||||
|
@ -1,20 +0,0 @@ |
|||||||
{ |
|
||||||
"name": "movie-web", |
|
||||||
"short_name": "movie-web", |
|
||||||
"icons": [ |
|
||||||
{ |
|
||||||
"src": "/android-chrome-192x192.png", |
|
||||||
"sizes": "192x192", |
|
||||||
"type": "image/png" |
|
||||||
}, |
|
||||||
{ |
|
||||||
"src": "/android-chrome-512x512.png", |
|
||||||
"sizes": "512x512", |
|
||||||
"type": "image/png" |
|
||||||
} |
|
||||||
], |
|
||||||
"theme_color": "#E880C5", |
|
||||||
"background_color": "#16171D", |
|
||||||
"display": "standalone", |
|
||||||
"start_url": "/" |
|
||||||
} |
|
@ -0,0 +1,28 @@ |
|||||||
|
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> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,61 @@ |
|||||||
|
import { |
||||||
|
ReactNode, |
||||||
|
createContext, |
||||||
|
useState, |
||||||
|
useMemo, |
||||||
|
Dispatch, |
||||||
|
SetStateAction, |
||||||
|
useEffect, |
||||||
|
useContext, |
||||||
|
} 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); |
||||||
|
} |
@ -0,0 +1,41 @@ |
|||||||
|
import { useEffect, useRef, useState } from "react"; |
||||||
|
|
||||||
|
export function useIsOnline() { |
||||||
|
const [online, setOnline] = useState<boolean | null>(true); |
||||||
|
const ref = useRef<boolean>(true); |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
let counter = 0; |
||||||
|
|
||||||
|
let abort: null | AbortController = null; |
||||||
|
const interval = setInterval(() => { |
||||||
|
// if online try once every 10 iterations intead of every iteration
|
||||||
|
counter += 1; |
||||||
|
if (ref.current) { |
||||||
|
if (counter < 10) return; |
||||||
|
} |
||||||
|
counter = 0; |
||||||
|
|
||||||
|
if (abort) abort.abort(); |
||||||
|
abort = new AbortController(); |
||||||
|
const signal = abort.signal; |
||||||
|
fetch("/ping.txt", { signal }) |
||||||
|
.then(() => { |
||||||
|
setOnline(true); |
||||||
|
ref.current = true; |
||||||
|
}) |
||||||
|
.catch((err) => { |
||||||
|
if (err.name === "AbortError") return; |
||||||
|
setOnline(false); |
||||||
|
ref.current = false; |
||||||
|
}); |
||||||
|
}, 5000); |
||||||
|
|
||||||
|
return () => { |
||||||
|
clearInterval(interval); |
||||||
|
if (abort) abort.abort(); |
||||||
|
}; |
||||||
|
}, []); |
||||||
|
|
||||||
|
return online; |
||||||
|
} |
@ -0,0 +1,27 @@ |
|||||||
|
import { Banner } from "@/components/Banner"; |
||||||
|
import { useBannerSize } from "@/hooks/useBanner"; |
||||||
|
import { useIsOnline } from "@/hooks/usePing"; |
||||||
|
import { ReactNode } from "react"; |
||||||
|
import { useTranslation } from "react-i18next"; |
||||||
|
|
||||||
|
export function Layout(props: { children: ReactNode }) { |
||||||
|
const { t } = useTranslation(); |
||||||
|
const isOnline = useIsOnline(); |
||||||
|
const bannerSize = useBannerSize(); |
||||||
|
|
||||||
|
return ( |
||||||
|
<div> |
||||||
|
<div className="fixed inset-x-0 z-[1000]"> |
||||||
|
{!isOnline ? <Banner type="error">{t("errors.offline")}</Banner> : null} |
||||||
|
</div> |
||||||
|
<div |
||||||
|
style={{ |
||||||
|
paddingTop: `${bannerSize}px`, |
||||||
|
}} |
||||||
|
className="flex min-h-screen flex-col" |
||||||
|
> |
||||||
|
{props.children} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
Loading…
Reference in new issue