diff --git a/packages/base-shell/src/containers/Layout/index.tsx b/packages/base-shell/src/containers/Layout/index.tsx new file mode 100644 index 000000000..7ef86f64d --- /dev/null +++ b/packages/base-shell/src/containers/Layout/index.tsx @@ -0,0 +1,97 @@ +import React, { Suspense, useEffect, useState } from "react"; +import { useRoutes } from "react-router-dom"; +import { IntlProvider } from "react-intl"; +import { useLocale, useConfig, getLocaleMessages } from "@ecronix/base-shell"; +import { + AddToHomeScreenProvider, + AuthProvider, + UpdateProvider, + OnlineProvider, + SimpleValuesProvider, + LocaleProvider, +} from "@ecronix/base-shell"; + +interface LayoutContentProps { + appConfig?: any; +} + +export const LayoutContent: React.FC = ({ appConfig = {} }) => { + const [messages, setMessages] = useState([]); + const { + components, + routes = [], + containers, + locale: confLocale, + getDefaultRoutes, + auth, + update, + } = appConfig || {}; + const { persistKey } = auth || {}; + const { checkInterval = 5000 } = update || {}; + const { Menu, Loading = () =>
Loading...
} = components || {}; + const { locales, onError } = confLocale || {}; + const { LayoutContainer = React.Fragment } = containers || {}; + const defaultRoutes = getDefaultRoutes ? getDefaultRoutes(appConfig) : []; + const { locale = {} } = useLocale(); + + useEffect(() => { + const loadPolyfills = async () => { + if (locale.locales && locale.locales.length > 0) { + for (let i = 0; i < locales.length; i++) { + const l = locales[i]; + if (l.locale === locale) { + if (l.loadData) { + await l.loadData; + } + } + } + } + }; + loadPolyfills(); + }, [locale, locales]); + + useEffect(() => { + const loadMessages = async () => { + const messages = await getLocaleMessages(locale, locales); + setMessages(messages); + }; + loadMessages(); + }, [locale, locales]); + + return ( + + + + + + + + }>{Menu && } + }> + {useRoutes([...routes, ...defaultRoutes])} + + + + + + + + + ); +}; + +export const LayoutContainer: React.FC = () => { + const { appConfig } = useConfig(); + const { locale } = appConfig || {}; + const { defaultLocale, persistKey } = locale || {}; + return ( + + + + ); +}; diff --git a/packages/base-shell/src/providers/AddToHomeScreen/Context.tsx b/packages/base-shell/src/providers/AddToHomeScreen/Context.tsx new file mode 100644 index 000000000..3e1af0864 --- /dev/null +++ b/packages/base-shell/src/providers/AddToHomeScreen/Context.tsx @@ -0,0 +1,5 @@ +import React from "react"; + +const Context = React.createContext(null); + +export default Context; diff --git a/packages/base-shell/src/providers/AddToHomeScreen/Provider.tsx b/packages/base-shell/src/providers/AddToHomeScreen/Provider.tsx new file mode 100644 index 000000000..1abae18b5 --- /dev/null +++ b/packages/base-shell/src/providers/AddToHomeScreen/Provider.tsx @@ -0,0 +1,35 @@ +import React, { useState } from "react"; +import Context from "./Context"; + +interface A2HPState { + deferredPrompt: any; + isAppInstallable: boolean; + isAppInstalled: boolean; +} + +const initialState: A2HPState = { + deferredPrompt: () => {}, + isAppInstallable: false, + isAppInstalled: false, +}; + +const Provider: React.FC = ({ children }) => { + const [state, setA2HPState] = useState(initialState); + + window.addEventListener("beforeinstallprompt", (e) => { + e.preventDefault(); + setA2HPState({ deferredPrompt: e, isAppInstallable: true }); + }); + + window.addEventListener("appinstalled", () => { + setA2HPState({ isAppInstalled: true }); + }); + + return ( + + {children} + + ); +}; + +export default Provider; diff --git a/packages/base-shell/src/providers/AddToHomeScreen/index.ts b/packages/base-shell/src/providers/AddToHomeScreen/index.ts new file mode 100644 index 000000000..19977875d --- /dev/null +++ b/packages/base-shell/src/providers/AddToHomeScreen/index.ts @@ -0,0 +1,13 @@ +import { useContext } from "react"; +import Context from "./Context"; +import Provider from "./Provider"; + +function useAddToHomeScreen() { + return useContext(Context); +} + +export { + useAddToHomeScreen, + Context as AddToHomeScreenContext, + Provider as AddToHomeScreenProvider, +}; diff --git a/packages/base-shell/src/providers/Auth/Provider.tsx b/packages/base-shell/src/providers/Auth/Provider.tsx new file mode 100644 index 000000000..f6ba0df84 --- /dev/null +++ b/packages/base-shell/src/providers/Auth/Provider.tsx @@ -0,0 +1,60 @@ +import React, { useEffect, useReducer } from "react"; +import Context from "./Context"; + +interface AuthState { + [key: string]: any; +} + +interface AuthAction { + type: "SET_AUTH" | "UPDATE_AUTH"; + auth: AuthState; +} + +function reducer(state: AuthState, action: AuthAction): AuthState { + const { type, auth } = action; + switch (type) { + case "SET_AUTH": + return auth; + case "UPDATE_AUTH": + return { ...state, ...auth }; + default: + throw new Error(); + } +} + +interface ProviderProps { + persistKey?: string; + children: React.ReactNode; +} + +const Provider: React.FC = ({ persistKey = "auth", children }) => { + const persistAuth = JSON.parse( + localStorage.getItem(persistKey)?.replace("undefined", "{}") || "{}" + ); + + const [auth, dispatch] = useReducer(reducer, persistAuth || {}); + + useEffect(() => { + try { + localStorage.setItem(persistKey, JSON.stringify(auth)); + } catch (error) { + console.warn(error); + } + }, [auth, persistKey]); + + const setAuth = (auth: AuthState) => { + dispatch({ type: "SET_AUTH", auth }); + }; + + const updateAuth = (auth: AuthState) => { + dispatch({ type: "UPDATE_AUTH", auth }); + }; + + return ( + + {children} + + ); +}; + +export default Provider; diff --git a/packages/base-shell/src/providers/Locale/Context.tsx b/packages/base-shell/src/providers/Locale/Context.tsx new file mode 100644 index 000000000..3e1af0864 --- /dev/null +++ b/packages/base-shell/src/providers/Locale/Context.tsx @@ -0,0 +1,5 @@ +import React from "react"; + +const Context = React.createContext(null); + +export default Context; diff --git a/packages/base-shell/src/providers/Locale/Provider.tsx b/packages/base-shell/src/providers/Locale/Provider.tsx new file mode 100644 index 000000000..2fb8fcaed --- /dev/null +++ b/packages/base-shell/src/providers/Locale/Provider.tsx @@ -0,0 +1,33 @@ +import React, { useState, useEffect } from "react"; +import Context from "./Context"; + +interface ProviderProps { + children: React.ReactNode; + defaultLocale?: string; + persistKey?: string; +} + +const Provider: React.FC = ({ + children, + defaultLocale = "en", + persistKey = "locale", +}) => { + const persistLocale = localStorage.getItem(persistKey); + const [locale, setLocale] = useState(persistLocale || defaultLocale); + + useEffect(() => { + try { + localStorage.setItem(persistKey, locale); + } catch (error) { + console.warn(error); + } + }, [locale, persistKey]); + + return ( + + {children} + + ); +}; + +export default Provider; diff --git a/packages/base-shell/src/providers/Locale/index.ts b/packages/base-shell/src/providers/Locale/index.ts new file mode 100644 index 000000000..3a17a99b1 --- /dev/null +++ b/packages/base-shell/src/providers/Locale/index.ts @@ -0,0 +1,9 @@ +import { useContext } from "react"; +import Context from "./Context"; +import Provider from "./Provider"; + +function useLocale() { + return useContext(Context); +} + +export { useLocale, Context as LocaleContext, Provider as LocaleProvider }; diff --git a/packages/base-shell/src/providers/Online/Context.tsx b/packages/base-shell/src/providers/Online/Context.tsx new file mode 100644 index 000000000..124e7c87c --- /dev/null +++ b/packages/base-shell/src/providers/Online/Context.tsx @@ -0,0 +1,5 @@ +import React from "react"; + +const Context = React.createContext(null); + +export default Context; diff --git a/packages/base-shell/src/providers/Online/Provider.tsx b/packages/base-shell/src/providers/Online/Provider.tsx new file mode 100644 index 000000000..2f5ae4eef --- /dev/null +++ b/packages/base-shell/src/providers/Online/Provider.tsx @@ -0,0 +1,17 @@ +import React, { useState } from 'react' +import Context from './Context' + +interface ProviderProps { + children: React.ReactNode; +} + +const Provider: React.FC = ({ children }) => { + const [isOnline, setOnline] = useState(navigator.onLine) + + window.addEventListener('online', () => setOnline(true)) + window.addEventListener('offline', () => setOnline(false)) + + return {children} +} + +export default Provider diff --git a/packages/base-shell/src/providers/Online/index.ts b/packages/base-shell/src/providers/Online/index.ts new file mode 100644 index 000000000..60d773ff9 --- /dev/null +++ b/packages/base-shell/src/providers/Online/index.ts @@ -0,0 +1,9 @@ +import { useContext } from "react"; +import Context from "./Context"; +import Provider from "./Provider"; + +function useOnline() { + return useContext(Context); +} + +export { useOnline, Context as OnlineContext, Provider as OnlineProvider }; diff --git a/packages/base-shell/src/providers/SimpleValues/Context.tsx b/packages/base-shell/src/providers/SimpleValues/Context.tsx new file mode 100644 index 000000000..3e1af0864 --- /dev/null +++ b/packages/base-shell/src/providers/SimpleValues/Context.tsx @@ -0,0 +1,5 @@ +import React from "react"; + +const Context = React.createContext(null); + +export default Context; diff --git a/packages/base-shell/src/providers/SimpleValues/Provider.tsx b/packages/base-shell/src/providers/SimpleValues/Provider.tsx new file mode 100644 index 000000000..459edbf55 --- /dev/null +++ b/packages/base-shell/src/providers/SimpleValues/Provider.tsx @@ -0,0 +1,105 @@ +import React, { useEffect, useReducer } from "react"; +import Context from "./Context"; + +interface SimpleValue { + value: any; + persist: boolean; +} + +interface SimpleValuesState { + [key: string]: SimpleValue; +} + +interface SimpleValuesAction { + type: "add" | "clear" | "clear_all"; + key?: string; + value?: any; + persist?: boolean; +} + +function reducer(state: SimpleValuesState, action: SimpleValuesAction): SimpleValuesState { + const { type, key, value, persist } = action; + switch (type) { + case "add": + if (key) { + return { ...state, [key]: { value, persist } }; + } + return state; + case "clear": + if (key) { + const { [key]: clearedKey, ...rest } = state; + return { ...rest }; + } + return state; + case "clear_all": + return {}; + default: + throw new Error(); + } +} + +function getInitState(persistKey: string): SimpleValuesState { + let persistedValues: SimpleValuesState = {}; + try { + persistedValues = + JSON.parse( + localStorage.getItem(persistKey)?.replace("undefined", "{}") || "{}" + ) || {}; + } catch (error) { + console.warn(error); + } + return persistedValues; +} + +interface ProviderProps { + children: React.ReactNode; + persistKey?: string; +} + +const Provider: React.FC = ({ children, persistKey = "simple_values" }) => { + const [state, dispatch] = useReducer(reducer, getInitState(persistKey)); + + useEffect(() => { + try { + const persistValues: SimpleValuesState = {}; + + Object.keys(state).forEach((k) => { + if (state[k].persist) { + persistValues[k] = { value: state[k].value, persist: true }; + } + }); + + localStorage.setItem(persistKey, JSON.stringify(persistValues)); + } catch (error) { + console.warn(error); + } + }, [state, persistKey]); + + const setValue = (key: string, value: any, persist = false) => { + dispatch({ type: "add", key, value, persist }); + }; + + const getValue = (key: string, defaultValue: any) => { + if (state[key] !== undefined) { + return state[key].value; + } else { + return defaultValue; + } + }; + + const clearValue = (key: string) => { + dispatch({ type: "clear", key }); + }; + + const clearAll = () => { + dispatch({ type: "clear_all" }); + }; + + return ( + + {children} + + ); +}; + +export default Provider; diff --git a/packages/base-shell/src/providers/SimpleValues/index.ts b/packages/base-shell/src/providers/SimpleValues/index.ts new file mode 100644 index 000000000..1efc4f654 --- /dev/null +++ b/packages/base-shell/src/providers/SimpleValues/index.ts @@ -0,0 +1,13 @@ +import { useContext } from "react"; +import Context from "./Context"; +import Provider from "./Provider"; + +function useSimpleValues() { + return useContext(Context); +} + +export { + useSimpleValues, + Context as SimpleValuesContext, + Provider as SimpleValuesProvider, +}; diff --git a/packages/base-shell/src/providers/Update/Context.tsx b/packages/base-shell/src/providers/Update/Context.tsx new file mode 100644 index 000000000..3e1af0864 --- /dev/null +++ b/packages/base-shell/src/providers/Update/Context.tsx @@ -0,0 +1,5 @@ +import React from "react"; + +const Context = React.createContext(null); + +export default Context; diff --git a/packages/base-shell/src/providers/Update/Provider.tsx b/packages/base-shell/src/providers/Update/Provider.tsx new file mode 100644 index 000000000..5ee56d859 --- /dev/null +++ b/packages/base-shell/src/providers/Update/Provider.tsx @@ -0,0 +1,43 @@ +import React, { useState, useEffect } from 'react' +import Context from './Context' + +interface ProviderProps { + children: React.ReactNode; + checkInterval: number; +} + +const runUpdate = (registration: ServiceWorkerRegistration | undefined) => { + try { + if (registration) { + registration.waiting?.postMessage({ type: 'SKIP_WAITING' }) + } + if (window.update) { + window.update && window.update() + } + } catch (error) { + console.log(error) + } +} + +const Provider: React.FC = ({ children, checkInterval }) => { + const [isUpdateAvailable, setUpdateAvailable] = useState(false) + + const checkUpdate = () => { + if (window.update) { + setUpdateAvailable(true) + } else { + setUpdateAvailable(false) + setTimeout(checkUpdate, checkInterval) + } + } + + useEffect(checkUpdate, [checkUpdate]) + + return ( + + {children} + + ) +} + +export default Provider diff --git a/packages/base-shell/src/providers/Update/index.ts b/packages/base-shell/src/providers/Update/index.ts new file mode 100644 index 000000000..3e9df510e --- /dev/null +++ b/packages/base-shell/src/providers/Update/index.ts @@ -0,0 +1,9 @@ +import { useContext } from "react"; +import Context from "./Context"; +import Provider from "./Provider"; + +function useUpdate() { + return useContext(Context); +} + +export { useUpdate, Context as UpdateContext, Provider as UpdateProvider }; diff --git a/packages/base-shell/src/providers/index.ts b/packages/base-shell/src/providers/index.ts new file mode 100644 index 000000000..01e01be5c --- /dev/null +++ b/packages/base-shell/src/providers/index.ts @@ -0,0 +1,7 @@ +export * from "./Update"; +export * from "./SimpleValues"; +export * from "./Online"; +export * from "./Locale"; +export * from "./Config"; +export * from "./Auth"; +export * from "./AddToHomeScreen"; diff --git a/packages/base-shell/src/utils/config.ts b/packages/base-shell/src/utils/config.ts new file mode 100644 index 000000000..faa48414c --- /dev/null +++ b/packages/base-shell/src/utils/config.ts @@ -0,0 +1,11 @@ +export const merge = (obj1: Record, obj2: Record): Record => { + let temp = { ...obj1, ...obj2 }; + + Object.keys(temp).forEach((key) => { + if (typeof temp[key] === "object" && !(temp[key] instanceof Array)) { + temp[key] = { ...obj1[key], ...obj2[key] }; + } + }); + + return temp; +}; diff --git a/packages/base-shell/src/utils/index.ts b/packages/base-shell/src/utils/index.ts new file mode 100644 index 000000000..ef8e0ed5d --- /dev/null +++ b/packages/base-shell/src/utils/index.ts @@ -0,0 +1,2 @@ +export { merge } from "./config"; +export * from "./locale"; diff --git a/packages/base-shell/src/utils/locale.ts b/packages/base-shell/src/utils/locale.ts new file mode 100644 index 000000000..9e8babe7a --- /dev/null +++ b/packages/base-shell/src/utils/locale.ts @@ -0,0 +1,49 @@ +import { defineMessages } from 'react-intl' + +const getUsersPreferredLanguages = (): string[] | undefined => { + if (navigator.languages !== undefined) { + return navigator.languages + } else if (navigator.language !== undefined) { + return [navigator.language] + } else { + return undefined + } +} + +const parseLanguages = (acceptedLangs: string[], defaultLang: string | false = false): string | false | undefined => { + const userPref = getUsersPreferredLanguages() + + const match = userPref + ? userPref.find((lang) => acceptedLangs.includes(lang)) + : undefined + + if (match === undefined && defaultLang !== false) { + return defaultLang + } + + return match +} + +const getLocaleMessages = async (l: string, ls: { locale: string, messages: any }[]): Promise => { + if (ls) { + for (let i = 0; i < ls.length; i++) { + if (ls[i]['locale'] === l) { + const { default: messages } = await defineMessages(ls[i].messages) + + return messages + } + } + } + + return {} +} + +const formatMessage = (messages: Record = {}, id: string): string => { + return messages[id] || id +} + +export { + formatMessage, + getLocaleMessages, + parseLanguages +} \ No newline at end of file