From 24aeb68f5536f5c6b1114d30d1ba1b7f09fc7b0d Mon Sep 17 00:00:00 2001 From: mrjvs Date: Fri, 24 Feb 2023 21:45:14 +0100 Subject: [PATCH] error boundary Co-authored-by: Jip Frijlink --- package.json | 1 + public/ping.txt | 1 + src/components/Banner.tsx | 27 +++ src/components/Icon.tsx | 2 + src/components/layout/Navigation.tsx | 10 +- src/hooks/useBanner.tsx | 61 ++++++ src/hooks/usePing.ts | 41 ++++ src/setup/App.tsx | 58 +++-- src/setup/Layout.tsx | 26 +++ src/setup/locales/en/translation.json | 3 + .../components/parts/VideoPlayerHeader.tsx | 7 +- src/views/search/SearchView.tsx | 8 +- vite.config.ts | 1 + yarn.lock | 205 +++++++++++++++++- 14 files changed, 421 insertions(+), 30 deletions(-) create mode 100644 public/ping.txt create mode 100644 src/components/Banner.tsx create mode 100644 src/hooks/useBanner.tsx create mode 100644 src/hooks/usePing.ts create mode 100644 src/setup/Layout.tsx diff --git a/package.json b/package.json index d8388984..05b75f68 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "react-router-dom": "^5.2.0", "react-stickynode": "^4.1.0", "react-transition-group": "^4.4.5", + "react-use": "^17.4.0", "srt-webvtt": "^2.0.0", "unpacker": "^1.0.1" }, diff --git a/public/ping.txt b/public/ping.txt new file mode 100644 index 00000000..8e554694 --- /dev/null +++ b/public/ping.txt @@ -0,0 +1 @@ +pong diff --git a/src/components/Banner.tsx b/src/components/Banner.tsx new file mode 100644 index 00000000..180f445c --- /dev/null +++ b/src/components/Banner.tsx @@ -0,0 +1,27 @@ +import { Icon, Icons } from "@/components/Icon"; +import { useBanner } from "@/hooks/useBanner"; + +export function Banner(props: { children: React.ReactNode; type: "error" }) { + const [ref] = useBanner("internet"); + const styles = { + error: "bg-[#C93957] text-white", + }; + const icons = { + error: Icons.CIRCLE_EXCLAMATION, + }; + + return ( +
+
+ +
{props.children}
+
+
+ ); +} diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx index bf0a0ae2..5308e3be 100644 --- a/src/components/Icon.tsx +++ b/src/components/Icon.tsx @@ -34,6 +34,7 @@ export enum Icons { CAPTIONS = "captions", LINK = "link", CASTING = "casting", + CIRCLE_EXCLAMATION = "circle_exclamation", } export interface IconProps { @@ -74,6 +75,7 @@ const iconList: Record = { file: ``, captions: ``, link: ``, + circle_exclamation: ``, casting: "", }; diff --git a/src/components/layout/Navigation.tsx b/src/components/layout/Navigation.tsx index 90c8852d..4fe1864f 100644 --- a/src/components/layout/Navigation.tsx +++ b/src/components/layout/Navigation.tsx @@ -3,6 +3,7 @@ import { Link } from "react-router-dom"; import { IconPatch } from "@/components/buttons/IconPatch"; import { Icons } from "@/components/Icon"; import { conf } from "@/setup/config"; +import { useBannerSize } from "@/hooks/useBanner"; import { BrandPill } from "./BrandPill"; export interface NavigationProps { @@ -11,8 +12,15 @@ export interface NavigationProps { } export function Navigation(props: NavigationProps) { + const bannerHeight = useBannerSize(); + return ( -
+
>] +>(null as any); + +export function BannerContextProvider(props: { children: ReactNode }) { + const [state, setState] = useState([]); + const memod = useMemo< + [BannerInstance[], Dispatch>] + >(() => [state, setState], [state]); + + return ( + + {props.children} + + ); +} + +export function useBanner(id: string) { + const [ref, { height }] = useMeasure(); + // 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); +} diff --git a/src/hooks/usePing.ts b/src/hooks/usePing.ts new file mode 100644 index 00000000..cba29c93 --- /dev/null +++ b/src/hooks/usePing.ts @@ -0,0 +1,41 @@ +import { useEffect, useRef, useState } from "react"; + +export function useIsOnline() { + const [online, setOnline] = useState(true); + const ref = useRef(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; +} diff --git a/src/setup/App.tsx b/src/setup/App.tsx index b4a8d52c..e82f57d7 100644 --- a/src/setup/App.tsx +++ b/src/setup/App.tsx @@ -11,36 +11,48 @@ import { DeveloperView } from "@/views/developer/DeveloperView"; import { VideoTesterView } from "@/views/developer/VideoTesterView"; import { ProviderTesterView } from "@/views/developer/ProviderTesterView"; import { EmbedTesterView } from "@/views/developer/EmbedTesterView"; - -// TODO add "you are offline" status bar +import { BannerContextProvider } from "@/hooks/useBanner"; +import { Layout } from "@/setup/Layout"; function App() { return ( - - {/* functional routes */} - - - - + + + + {/* functional routes */} + + + + - {/* pages */} - - - + {/* pages */} + + + - {/* other */} - - - - - - + {/* other */} + + + + + + + + ); diff --git a/src/setup/Layout.tsx b/src/setup/Layout.tsx new file mode 100644 index 00000000..c9a02dc8 --- /dev/null +++ b/src/setup/Layout.tsx @@ -0,0 +1,26 @@ +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 ( +
+
+ {!isOnline ? {t("errors.offline")} : null} +
+
+ {props.children} +
+
+ ); +} diff --git a/src/setup/locales/en/translation.json b/src/setup/locales/en/translation.json index 14bb2845..da015409 100644 --- a/src/setup/locales/en/translation.json +++ b/src/setup/locales/en/translation.json @@ -88,5 +88,8 @@ }, "casting": { "casting": "Casting to device..." + }, + "errors": { + "offline": "Check your internet connection" } } diff --git a/src/video/components/parts/VideoPlayerHeader.tsx b/src/video/components/parts/VideoPlayerHeader.tsx index 741b5640..57bd9e18 100644 --- a/src/video/components/parts/VideoPlayerHeader.tsx +++ b/src/video/components/parts/VideoPlayerHeader.tsx @@ -10,6 +10,7 @@ import { AirplayAction } from "@/video/components/actions/AirplayAction"; import { ChromecastAction } from "@/video/components/actions/ChromecastAction"; import { useTranslation } from "react-i18next"; import { useIsMobile } from "@/hooks/useIsMobile"; +import { useBannerSize } from "@/hooks/useBanner"; interface VideoPlayerHeaderProps { media?: MWMediaMeta; @@ -25,9 +26,13 @@ export function VideoPlayerHeader(props: VideoPlayerHeaderProps) { : false; const showDivider = props.media && props.onClick; const { t } = useTranslation(); + const bannerHeight = useBannerSize(); return ( -
+

{props.onClick ? ( diff --git a/src/views/search/SearchView.tsx b/src/views/search/SearchView.tsx index 5035574d..9dfdee64 100644 --- a/src/views/search/SearchView.tsx +++ b/src/views/search/SearchView.tsx @@ -7,6 +7,7 @@ import { SearchBarInput } from "@/components/SearchBar"; import { Title } from "@/components/text/Title"; import { useSearchQuery } from "@/hooks/useSearchQuery"; import { WideContainer } from "@/components/layout/WideContainer"; +import { useBannerSize } from "@/hooks/useBanner"; import { Helmet } from "react-helmet"; import { SearchResultsPartial } from "./SearchResultsPartial"; @@ -14,6 +15,7 @@ export function SearchView() { const { t } = useTranslation(); const [search, setSearch, setSearchUnFocus] = useSearchQuery(); const [showBg, setShowBg] = useState(false); + const bannerSize = useBannerSize(); const stickStateChanged = useCallback( ({ status }: Sticky.Status) => setShowBg(status === Sticky.STATUS_FIXED), @@ -36,7 +38,11 @@ export function SearchView() { {t("search.title")}

- + =16.6.0: +react-dom@*, "react-dom@^0.14.2 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", "react-dom@^16 || ^17 || ^18", "react-dom@^16.8.0 || ^17.0.0 || ^18.0.0", react-dom@^17.0.2, react-dom@>=16.6.0: version "17.0.2" resolved "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz" integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== @@ -4192,7 +4283,32 @@ react-transition-group@^4.4.5: loose-envify "^1.4.0" prop-types "^15.6.2" -"react@^0.14.2 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", "react@^16 || ^17 || ^18", "react@^16.3.0 || ^17.0.0 || ^18.0.0", react@^17.0.2, "react@>= 16.8.0", react@>=15, react@>=16.3.0, react@>=16.6.0, react@17.0.2: +react-universal-interface@^0.6.2: + version "0.6.2" + resolved "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz" + integrity sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw== + +react-use@^17.4.0: + version "17.4.0" + resolved "https://registry.npmjs.org/react-use/-/react-use-17.4.0.tgz" + integrity sha512-TgbNTCA33Wl7xzIJegn1HndB4qTS9u03QUwyNycUnXaweZkE4Kq2SB+Yoxx8qbshkZGYBDvUXbXWRUmQDcZZ/Q== + dependencies: + "@types/js-cookie" "^2.2.6" + "@xobotyi/scrollbar-width" "^1.9.5" + copy-to-clipboard "^3.3.1" + fast-deep-equal "^3.1.3" + fast-shallow-equal "^1.0.0" + js-cookie "^2.2.1" + nano-css "^5.3.1" + react-universal-interface "^0.6.2" + resize-observer-polyfill "^1.5.1" + screenfull "^5.1.0" + set-harmonic-interval "^1.0.1" + throttle-debounce "^3.0.1" + ts-easing "^0.2.0" + tslib "^2.1.0" + +react@*, "react@^0.14.2 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", "react@^16 || ^17 || ^18", "react@^16.3.0 || ^17.0.0 || ^18.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0", react@^17.0.2, "react@>= 16.8.0", react@>=15, react@>=16.3.0, react@>=16.6.0, react@17.0.2: version "17.0.2" resolved "https://registry.npmjs.org/react/-/react-17.0.2.tgz" integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== @@ -4296,6 +4412,11 @@ requires-port@^1.0.0: resolved "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz" integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== +resize-observer-polyfill@^1.5.1: + version "1.5.1" + resolved "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz" + integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== + resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" @@ -4360,6 +4481,13 @@ rollup@^1.20.0||^2.0.0||^3.0.0, rollup@^3.7.0, rollup@^3.7.2: optionalDependencies: fsevents "~2.3.2" +rtl-css-js@^1.14.0: + version "1.16.1" + resolved "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz" + integrity sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg== + dependencies: + "@babel/runtime" "^7.1.2" + run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz" @@ -4406,6 +4534,11 @@ scheduler@^0.20.2: loose-envify "^1.1.0" object-assign "^4.1.1" +screenfull@^5.1.0: + version "5.2.0" + resolved "https://registry.npmjs.org/screenfull/-/screenfull-5.2.0.tgz" + integrity sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA== + semver@^6.1.1: version "6.3.0" resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" @@ -4466,6 +4599,11 @@ serve@^14.2.0: serve-handler "6.1.5" update-check "1.5.4" +set-harmonic-interval@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/set-harmonic-interval/-/set-harmonic-interval-1.0.1.tgz" + integrity sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g== + shallowequal@^1.0.0: version "1.1.0" resolved "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz" @@ -4540,6 +4678,11 @@ source-map@^0.8.0-beta.0: dependencies: whatwg-url "^7.0.0" +source-map@0.5.6: + version "0.5.6" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz" + integrity sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA== + sourcemap-codec@^1.4.8: version "1.4.8" resolved "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz" @@ -4550,11 +4693,40 @@ srt-webvtt@^2.0.0: resolved "https://registry.npmjs.org/srt-webvtt/-/srt-webvtt-2.0.0.tgz" integrity sha512-G2Z7/Jf2NRKrmLYNSIhSYZZYE6OFlKXFp9Au2/zJBKgrioUzmrAys1x7GT01dwl6d2sEnqr5uahEIOd0JW/Rbw== +stack-generator@^2.0.5: + version "2.0.10" + resolved "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.10.tgz" + integrity sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ== + dependencies: + stackframe "^1.3.4" + stackback@0.0.2: version "0.0.2" resolved "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz" integrity sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw== +stackframe@^1.3.4: + version "1.3.4" + resolved "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz" + integrity sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw== + +stacktrace-gps@^3.0.4: + version "3.1.2" + resolved "https://registry.npmjs.org/stacktrace-gps/-/stacktrace-gps-3.1.2.tgz" + integrity sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ== + dependencies: + source-map "0.5.6" + stackframe "^1.3.4" + +stacktrace-js@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/stacktrace-js/-/stacktrace-js-2.0.2.tgz" + integrity sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg== + dependencies: + error-stack-parser "^2.0.6" + stack-generator "^2.0.5" + stacktrace-gps "^3.0.4" + std-env@^3.3.1: version "3.3.2" resolved "https://registry.npmjs.org/std-env/-/std-env-3.3.2.tgz" @@ -4665,6 +4837,11 @@ strip-literal@^1.0.0: dependencies: acorn "^8.8.2" +stylis@^4.0.6: + version "4.1.3" + resolved "https://registry.npmjs.org/stylis/-/stylis-4.1.3.tgz" + integrity sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA== + subscribe-ui-event@^2.0.6: version "2.0.7" resolved "https://registry.npmjs.org/subscribe-ui-event/-/subscribe-ui-event-2.0.7.tgz" @@ -4762,6 +4939,11 @@ text-table@^0.2.0: resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz" integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== +throttle-debounce@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-3.0.1.tgz" + integrity sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg== + tiny-invariant@^1.0.2, tiny-invariant@^1.1.0: version "1.3.1" resolved "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz" @@ -4799,6 +4981,11 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +toggle-selection@^1.0.6: + version "1.0.6" + resolved "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz" + integrity sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ== + tough-cookie@^4.1.2: version "4.1.2" resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz" @@ -4823,6 +5010,11 @@ tr46@^3.0.0: dependencies: punycode "^2.1.1" +ts-easing@^0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/ts-easing/-/ts-easing-0.2.0.tgz" + integrity sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ== + tsconfig-paths@^3.14.1: version "3.14.1" resolved "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz" @@ -4833,11 +5025,16 @@ tsconfig-paths@^3.14.1: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@^1.8.1: +tslib@*, tslib@^1.8.1: version "1.14.1" resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== +tslib@^2.1.0: + version "2.5.0" + resolved "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz" + integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== + tsutils@^3.21.0: version "3.21.0" resolved "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz"