Line data Source code
1 : import * as React from 'react';
2 : import symbols from '@ankoh/dashql-svg-symbols';
3 : import { useLocation } from 'react-router-dom';
4 :
5 : import { AppFlavor, DASHQL_FLAVOR } from './flavors.js';
6 : import { applyFlavorToDocument, selectFlavor, withFlavorParameter } from './flavor_resolution.js';
7 :
8 : interface FlavorState {
9 : /// The flavor of the app
10 : flavor: AppFlavor;
11 : /// The flavor requested by the url, null if the url requests none
12 : requested: AppFlavor | null;
13 : /// Select the flavor of the app settings, the flavor requested by the url still wins
14 : select: (id: string | undefined) => void;
15 : }
16 :
17 1 : const FLAVOR_CTX = React.createContext<FlavorState>({ flavor: DASHQL_FLAVOR, requested: null, select: () => { } });
18 :
19 : type Props = {
20 : /// The flavor requested by the url, it wins over the flavor of the app settings
21 : requested: AppFlavor | null;
22 : /// The children
23 : children: React.ReactElement;
24 : };
25 :
26 : /// Provides the flavor of the app and brands the document with it
27 1 : export const FlavorProvider: React.FC<Props> = (props: Props) => {
28 1 : const [settingId, setSettingId] = React.useState<string | undefined>(undefined);
29 1 : const flavor = selectFlavor(props.requested, settingId);
30 1 : React.useLayoutEffect(() => {
31 1 : applyFlavorToDocument(flavor);
32 1 : }, [flavor]);
33 1 : const state = React.useMemo<FlavorState>(() => ({
34 1 : flavor,
35 1 : requested: props.requested,
36 1 : select: setSettingId,
37 1 : }), [flavor, props.requested]);
38 1 : return <FLAVOR_CTX.Provider value={state}>{props.children}</FLAVOR_CTX.Provider>;
39 1 : };
40 :
41 1 : export const useAppFlavor = (): AppFlavor => React.useContext(FLAVOR_CTX).flavor;
42 1 : export const useRequestedAppFlavor = (): AppFlavor | null => React.useContext(FLAVOR_CTX).requested;
43 1 : export const useAppFlavorSelect = (): ((id: string | undefined) => void) => React.useContext(FLAVOR_CTX).select;
44 :
45 : /// Keeps the flavor query parameter in the url when the router navigates, so that reloads keep the flavor.
46 : /// With the hash router, the routes live in the hash and the query parameter is never dropped.
47 : /// A flavor of the app settings is not added to the url, it would otherwise pin the flavor.
48 1 : export function FlavorUrlKeeper() {
49 1 : const requested = useRequestedAppFlavor();
50 1 : const location = useLocation();
51 1 : React.useLayoutEffect(() => {
52 1 : const next = requested == null ? null : withFlavorParameter(new URL(globalThis.location.href), requested);
53 1 : if (next != null) {
54 : // Replace the url without the router, the history state still holds the router state
55 1 : globalThis.history.replaceState(globalThis.history.state, '', next);
56 : }
57 1 : }, [requested, location]);
58 1 : return null;
59 1 : }
60 :
61 : /// The logo of the app flavor, sized by either its height or its width
62 1 : export function FlavorLogo(props: { height?: number; width?: number; ariaHidden?: boolean }) {
63 1 : const logo = useAppFlavor().logo;
64 1 : const height = props.height ?? (props.width ?? 0) / logo.aspectRatio;
65 1 : const width = props.width ?? height * logo.aspectRatio;
66 1 : return (
67 1 : <svg width={`${width}px`} height={`${height}px`} aria-hidden={props.ariaHidden}>
68 1 : <use xlinkHref={`${symbols}#${logo.symbol}`} />
69 1 : </svg>
70 : );
71 1 : }
|