From befaccdfa6579c34bcce5c6ef6ca0ea8d46eb97c Mon Sep 17 00:00:00 2001 From: Stephane SEGNING LAMBOU Date: Sat, 27 Jul 2024 04:07:37 +0100 Subject: [PATCH] chore: copied data from electron (#60) * chore: copied data from electron * chore: added qr-code * fix: selector * chore: axios base query with retry; error handling; qr-code scan * chore: axios base query with retry; error handling; qr-code scan * chore: i18n; better bootstrap; redux persist --- index.html | 2 +- openapi-config.ts | 4 +- package.json | 13 +- public/i18n/en/common.json | 6 + public/i18n/en/config.json | 7 + src/app.tsx | 8 +- src/components/config-scan.button.tsx | 19 ++ src/components/config.qr-code.tsx | 16 ++ src/components/floating-config.tsx | 38 +++- src/components/header.tsx | 50 +++++ src/components/notification.tsx | 37 ++++ ...can-list.dump.tsx => scan-list.simple.tsx} | 12 +- src/components/theme-wrapper.tsx | 10 - src/components/to-list-scan.button.tsx | 16 ++ src/i18n.ts | 60 ++++-- src/i18n/de.json | 6 - src/i18n/en.json | 13 -- src/main.tsx | 83 ++++---- src/router.tsx | 48 +++-- src/screens/app-config.screen.tsx | 45 ++--- src/screens/scan-config.screen.tsx | 90 +++++++++ src/screens/scan-list.screen.tsx | 38 ++-- src/screens/scan.screen.tsx | 21 +- src/shared/constants.ts | 2 + src/shared/index.ts | 8 + src/shared/logging.ts | 8 + src/store/api/axios-base.ts | 54 ++++++ src/store/api/empty.api.ts | 10 + src/store/api/index.ts | 3 + src/store/emptyApi.ts | 8 - src/store/hooks.ts | 15 ++ src/store/index.ts | 25 +-- src/store/middlewares.ts | 25 +++ src/store/selectors.ts | 11 ++ src/store/slices/config.slice.ts | 47 +++++ src/store/slices/index.ts | 2 + src/store/slices/notification.slice.ts | 50 +++++ src/store/store.ts | 54 ++++++ src/store/thunks/fetch.config-url.ts | 6 + src/store/thunks/index.ts | 1 + src/store/types.ts | 16 ++ src/vite-env.d.ts | 9 + tsconfig.json | 15 +- yarn.lock | 183 +++++++++++++++++- 44 files changed, 983 insertions(+), 211 deletions(-) create mode 100644 public/i18n/en/common.json create mode 100644 public/i18n/en/config.json create mode 100644 src/components/config-scan.button.tsx create mode 100644 src/components/config.qr-code.tsx create mode 100644 src/components/header.tsx create mode 100644 src/components/notification.tsx rename src/components/{scan-list.dump.tsx => scan-list.simple.tsx} (69%) delete mode 100644 src/components/theme-wrapper.tsx create mode 100644 src/components/to-list-scan.button.tsx delete mode 100644 src/i18n/de.json delete mode 100644 src/i18n/en.json create mode 100644 src/screens/scan-config.screen.tsx create mode 100644 src/shared/constants.ts create mode 100644 src/shared/index.ts create mode 100644 src/shared/logging.ts create mode 100644 src/store/api/axios-base.ts create mode 100644 src/store/api/empty.api.ts create mode 100644 src/store/api/index.ts delete mode 100644 src/store/emptyApi.ts create mode 100644 src/store/hooks.ts create mode 100644 src/store/middlewares.ts create mode 100644 src/store/selectors.ts create mode 100644 src/store/slices/config.slice.ts create mode 100644 src/store/slices/index.ts create mode 100644 src/store/slices/notification.slice.ts create mode 100644 src/store/store.ts create mode 100644 src/store/thunks/fetch.config-url.ts create mode 100644 src/store/thunks/index.ts create mode 100644 src/store/types.ts diff --git a/index.html b/index.html index e4b78ea..37af710 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - Vite + React + TS + Lynx Scanner
diff --git a/openapi-config.ts b/openapi-config.ts index c866b57..a56faea 100644 --- a/openapi-config.ts +++ b/openapi-config.ts @@ -1,10 +1,10 @@ import type { ConfigFile } from '@rtk-query/codegen-openapi'; -const baseDir = process.env.GEN_FOLDER!; +const baseDir = './src/store/api/gen'; const config: ConfigFile = { schemaFile: './openapi.yaml', - apiFile: './src/store/emptyApi.ts', + apiFile: './src/store/api/empty.api.ts', apiImport: 'emptySplitApi', hooks: true, outputFiles: { diff --git a/package.json b/package.json index d9c6ac9..8c06280 100644 --- a/package.json +++ b/package.json @@ -14,15 +14,20 @@ "prettier": "prettier --write .", "stylelint:actions": "stylelint --fix \"src/**/*.{css,scss}\"", "convert-md-to-pdf": "yarn run md-to-pdf ./docs/*.md ./docs/**/*.md", - "rtk:gen": "export GEN_FOLDER=./src/store/gen && rimraf $GEN_FOLDER && mkdir $GEN_FOLDER && rtk-query-codegen-openapi openapi-config.ts && prettier -w $GEN_FOLDER", + "rtk:gen": "rimraf ./src/store/api/gen && mkdir ./src/store/api/gen && rtk-query-codegen-openapi openapi-config.ts && prettier -w ./src/store/api/gen", "postinstall": "yarn run rtk:gen" }, "dependencies": { "@reduxjs/toolkit": "^2.2.6", "@sentry/react": "^7.114.0", "@sentry/tracing": "^7.114.0", + "@yudiel/react-qr-scanner": "^2.0.4", "autoprefixer": "^10.4.19", + "axios": "^1.7.2", "i18next": "^23.11.4", + "i18next-chained-backend": "^4.6.2", + "i18next-http-backend": "^2.5.2", + "i18next-localstorage-backend": "^4.2.0", "md-to-pdf": "^5.2.4", "postcss": "^8.4.38", "react": "^18.3.1", @@ -30,9 +35,12 @@ "react-dom": "^18.2.0", "react-feather": "^2.0.10", "react-i18next": "^14.1.1", + "react-qr-code": "^2.0.15", "react-redux": "^9.1.2", "react-router-dom": "^6.23.0", "redux": "^5.0.1", + "redux-logger": "^3.0.6", + "redux-persist": "^6.0.0", "redux-toolkit": "^1.1.2", "tailwindcss": "^3.4.3", "theme-change": "^2.5.0" @@ -41,15 +49,18 @@ "@commitlint/cli": "^19.3.0", "@commitlint/config-conventional": "^19.2.2", "@commitlint/format": "^19.3.0", + "@electron-toolkit/preload": "^3.0.1", "@eslint/eslintrc": "^3.0.2", "@rtk-query/codegen-openapi": "^1.2.0", "@types/i18next-browser-languagedetector": "^3.0.0", "@types/react": "^18.2.66", "@types/react-dom": "^18.2.22", + "@types/redux-logger": "^3.0.13", "@typescript-eslint/eslint-plugin": "^7.7.1", "@typescript-eslint/parser": "^7.7.1", "@vitejs/plugin-react": "^4.2.1", "daisyui": "^4.10.5", + "electron-log": "^5.1.7", "esbuild-runner": "^2.2.2", "eslint": "8", "eslint-config-prettier": "^9.1.0", diff --git a/public/i18n/en/common.json b/public/i18n/en/common.json new file mode 100644 index 0000000..3db8bcd --- /dev/null +++ b/public/i18n/en/common.json @@ -0,0 +1,6 @@ +{ + "action": { + "scan": "Scan" + }, + "welcome": "Welcome to the Lynx-Scanner" +} diff --git a/public/i18n/en/config.json b/public/i18n/en/config.json new file mode 100644 index 0000000..52de329 --- /dev/null +++ b/public/i18n/en/config.json @@ -0,0 +1,7 @@ +{ + "page": "Welcome to Lynx!", + "description": "Please scan the QR Code for configuration", + "dark": "Dark Mode", + "light": "Light Mode", + "valantine": "Valantine Mode" +} diff --git a/src/app.tsx b/src/app.tsx index 35f0f42..e1a6c75 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,16 +1,16 @@ import { JSX } from 'react'; import { RouterProvider } from 'react-router-dom'; import { router } from './router.tsx'; -import { FloatingConfig } from './components/floating-config.tsx'; +import { Notification } from '@components/notification.tsx'; /** * The main application component. */ export function App(): JSX.Element { return ( -
- + <> -
+ + ); } diff --git a/src/components/config-scan.button.tsx b/src/components/config-scan.button.tsx new file mode 100644 index 0000000..d0dfa28 --- /dev/null +++ b/src/components/config-scan.button.tsx @@ -0,0 +1,19 @@ +import { useCallback } from 'react'; +import { Button } from 'react-daisyui'; +import { useNavigate } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; + +export default function ConfigScanButton() { + const { t } = useTranslation(); + const navigate = useNavigate(); + const scanConfigAndPersist = useCallback( + () => navigate('/config/scan'), + [navigate] + ); + + return ( + + ); +} diff --git a/src/components/config.qr-code.tsx b/src/components/config.qr-code.tsx new file mode 100644 index 0000000..8a6dc29 --- /dev/null +++ b/src/components/config.qr-code.tsx @@ -0,0 +1,16 @@ +import { useConfigData, useFetchConfigUrl } from '@store'; +import { Loading } from 'react-daisyui'; +import QRCode from 'react-qr-code'; +import { useEffect } from 'react'; + +export default function ConfigQrCode() { + const getUrl = useFetchConfigUrl(); + const url = useConfigData(); + useEffect(() => getUrl(), [getUrl]); + return ( +
+ {!url && } + {url && } +
+ ); +} diff --git a/src/components/floating-config.tsx b/src/components/floating-config.tsx index 4d39694..40914b7 100644 --- a/src/components/floating-config.tsx +++ b/src/components/floating-config.tsx @@ -1,25 +1,32 @@ import { Button, Divider, Dropdown } from 'react-daisyui'; -import { useCallback } from 'react'; +import { useCallback, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { Menu } from 'react-feather'; +import { BarChart2, Globe, List, Menu, Settings } from 'react-feather'; +import { Link } from 'react-router-dom'; +import { themeChange } from 'theme-change'; interface ThemeButtonProps { themeName: 'valantine' | 'light' | 'dark'; } function ThemeButton({ themeName }: ThemeButtonProps) { - const { t } = useTranslation(); + const { t: tC } = useTranslation('config'); return ( ); } export function FloatingConfig() { + useEffect(() => { + themeChange(false); + }, []); + const { i18n } = useTranslation(); const changeLanguageHandler = useCallback( @@ -38,7 +45,7 @@ export function FloatingConfig() { - + @@ -50,14 +57,29 @@ export function FloatingConfig() { - + - + - Config + + + Config + + + + + + Scans + diff --git a/src/components/header.tsx b/src/components/header.tsx new file mode 100644 index 0000000..bfa5fe3 --- /dev/null +++ b/src/components/header.tsx @@ -0,0 +1,50 @@ +import { Button } from 'react-daisyui'; +import { ArrowLeft, Icon } from 'react-feather'; +import { useNavigate } from 'react-router-dom'; +import { ReactNode } from 'react'; + +interface HeaderProps { + title: string; + Icon?: Icon; + onIconClick: () => void; + back: false; +} + +interface BackHeaderProps { + title: string; + back: string; +} + +interface RawTrail { + trailing?: ReactNode; +} + +export function Header({ + trailing, + title, + back, + ...rest +}: RawTrail & (HeaderProps | BackHeaderProps)) { + const nav = useNavigate(); + const { Icon, onIconClick } = rest as HeaderProps; + const navBack = () => { + nav('..'); + }; + return ( +
+ {back && ( + + )} +

{title}

+
+ {!back && Icon && ( + + )} + {trailing} +
+ ); +} diff --git a/src/components/notification.tsx b/src/components/notification.tsx new file mode 100644 index 0000000..a71a127 --- /dev/null +++ b/src/components/notification.tsx @@ -0,0 +1,37 @@ +import { + selectNotification, + removeNotification, + useAppDispatch, + useAppSelector, +} from '@store'; +import { Alert, Button, Toast } from 'react-daisyui'; +import { useCallback } from 'react'; +import { X } from 'react-feather'; + +export function Notification() { + const notifications = useAppSelector(selectNotification); + const dispatch = useAppDispatch(); + const remove = useCallback( + (id: string) => () => { + dispatch(removeNotification(id)); + }, + [dispatch] + ); + return ( + + {notifications.map((notification) => ( + + + {notification.message} + + ))} + + ); +} diff --git a/src/components/scan-list.dump.tsx b/src/components/scan-list.simple.tsx similarity index 69% rename from src/components/scan-list.dump.tsx rename to src/components/scan-list.simple.tsx index af77bb6..40f7fe4 100644 --- a/src/components/scan-list.dump.tsx +++ b/src/components/scan-list.simple.tsx @@ -1,4 +1,4 @@ -import type { Scan } from '@api/scans.api.gen.ts'; +import type { Scan } from '@api'; import { Button, Pagination } from 'react-daisyui'; import { ArrowLeft, ArrowRight } from 'react-feather'; @@ -9,7 +9,7 @@ export interface ScanListDumpProps { onPrev: () => void; } -export function ScanListDump({ +export function ScanListSimple({ scans, onPrev, onNext, @@ -25,11 +25,13 @@ export function ScanListDump({ ))} - - - + diff --git a/src/components/theme-wrapper.tsx b/src/components/theme-wrapper.tsx deleted file mode 100644 index 5ff81ef..0000000 --- a/src/components/theme-wrapper.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { PropsWithChildren, useEffect } from 'react'; -import { themeChange } from 'theme-change'; - -export function ThemeWrapper({ children }: PropsWithChildren) { - useEffect(() => { - themeChange(true); - }, []); - - return children; -} diff --git a/src/components/to-list-scan.button.tsx b/src/components/to-list-scan.button.tsx new file mode 100644 index 0000000..6cfc8b5 --- /dev/null +++ b/src/components/to-list-scan.button.tsx @@ -0,0 +1,16 @@ +import { useCallback } from 'react'; +import { Button } from 'react-daisyui'; +import { ArrowRight } from 'react-feather'; +import { useNavigate } from 'react-router-dom'; + +export default function ToListScanButton() { + const navigate = useNavigate(); + const toScans = useCallback(() => navigate('/scans'), [navigate]); + + return ( + + ); +} diff --git a/src/i18n.ts b/src/i18n.ts index d34f0e8..55a6b24 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -1,25 +1,47 @@ import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; import LanguageDetector from 'i18next-browser-languagedetector'; -import enJSon from './i18n/en.json'; -import deJSon from './i18n/de.json'; +import ChainedBackend, { ChainedBackendOptions } from 'i18next-chained-backend'; +import LocalStorageBackend, { + LocalStorageBackendOptions, +} from 'i18next-localstorage-backend'; +import HttpApi, { HttpBackendOptions } from 'i18next-http-backend'; +import axios from 'axios'; -i18n - .use(LanguageDetector) - .use(initReactI18next) - .init({ - fallbackLng: 'en', - interpolation: { - escapeValue: false, - }, - resources: { - en: { - translation: enJSon, +const axiosInstance = axios.create(); + +export async function i18nFn() { + await i18n + .use(LanguageDetector) + .use(initReactI18next) + .use(ChainedBackend) + .init({ + fallbackLng: 'en', + ns: ['common'], + defaultNS: ['common'], + supportedLngs: ['en', 'de', 'fr'], + interpolation: { + escapeValue: false, }, - de: { - translation: deJSon, + backend: { + backends: [LocalStorageBackend, HttpApi], + backendOptions: [ + { + expirationTime: 7 * 24 * 60 * 60 * 1000, // 7 days + } as LocalStorageBackendOptions, + { + loadPath: '/i18n/{{lng}}/{{ns}}.json', + request: async (_, url, __, callback) => { + try { + const { data, status } = await axiosInstance.get(url); + callback(null, { status, data }); + } catch (error) { + callback(error, { status: 500, data: {} }); + } + }, + } as HttpBackendOptions, + ], }, - }, - }); - -export default i18n; + }); + return i18n; +} diff --git a/src/i18n/de.json b/src/i18n/de.json deleted file mode 100644 index 74e0274..0000000 --- a/src/i18n/de.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "welcome": "Willkommen bei Lynx-Scanner", - "config": { - "page": "Konfigurationseite" - } -} diff --git a/src/i18n/en.json b/src/i18n/en.json deleted file mode 100644 index 5659441..0000000 --- a/src/i18n/en.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "welcome": "Welcome to the Lynx-Scanner", - "config": { - "page": "Welcome to Lynx!", - "description": "Please scan the QR Code for configuration", - "dark": "Dark Mode", - "light": "Light Mode", - "valantine": "Valantine Mode" - }, - "action": { - "scan": "Scan" - } -} diff --git a/src/main.tsx b/src/main.tsx index c1f0dc2..c9254a4 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,40 +1,53 @@ -import React from 'react'; -import ReactDOM from 'react-dom/client'; -import { App } from './app.tsx'; +import './index.scss'; +import { isElectron, setupLogging } from '@shared'; import * as Sentry from '@sentry/react'; +import { i18nFn } from '@i18n'; +import ReactDOM from 'react-dom/client'; +import React from 'react'; import { I18nextProvider } from 'react-i18next'; -import i18n from './i18n'; - -import './index.scss'; import { Provider } from 'react-redux'; -import { store } from './store'; -import { ThemeWrapper } from './components/theme-wrapper.tsx'; +import { persistor, store } from '@store'; +import { App } from './app.tsx'; +import { PersistGate } from 'redux-persist/integration/react'; + +async function main() { + if (isElectron) { + await setupLogging(); + } + + const sentryDSN = import.meta.env.VITE_SENTRY_DSN; + if (sentryDSN) { + Sentry.init({ + dsn: sentryDSN, + integrations: [ + Sentry.browserTracingIntegration(), + Sentry.replayIntegration(), + ], + // Performance Monitoring + tracesSampleRate: 1.0, // Capture 100% of the transactions + // Set 'tracePropagationTargets' to control for which URLs distributed tracing should be enabled + tracePropagationTargets: ['localhost', /^https:\/\/localhost:\d+\/api/], + // Session Replay + replaysSessionSampleRate: 0.1, + replaysOnErrorSampleRate: 1.0, + }); + } + + const i18n = await i18nFn(); -Sentry.init({ - dsn: 'https://9fd06d22381ef360013d83b6b0c8375e@o4507214219313152.ingest.de.sentry.io/4507214225801296', - integrations: [ - Sentry.browserTracingIntegration(), - Sentry.replayIntegration(), - ], - // Performance Monitoring - tracesSampleRate: 1.0, // Capture 100% of the transactions - // Set 'tracePropagationTargets' to control for which URLs distributed tracing should be enabled - tracePropagationTargets: ['localhost', /^https:\/\/localhost:\d+\/api/], - // Session Replay - replaysSessionSampleRate: 0.1, - replaysOnErrorSampleRate: 1.0, -}); + const rootElement = document.getElementById('root') as HTMLElement; + const root = ReactDOM.createRoot(rootElement); + root.render( + + + + + + + + + + ); +} -const rootElement = document.getElementById('root') as HTMLElement; -const root = ReactDOM.createRoot(rootElement); -root.render( - - - - - - - - - -); +main(); diff --git a/src/router.tsx b/src/router.tsx index 2a3cbd6..4283619 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -1,28 +1,52 @@ import { createBrowserRouter, Navigate, Outlet } from 'react-router-dom'; +import { FloatingConfig } from '@components/floating-config.tsx'; export const router = createBrowserRouter([ { - path: '/scans', element: ( -
- -
+ <> + +
+ +
+ ), children: [ { - path: 'add', - lazy: () => import('./screens/scan.screen'), + path: '/scans', + element: ( +
+
+ +
+
+ ), + children: [ + { + path: 'add', + lazy: () => import('@screens/scan.screen'), + }, + { + path: '', + lazy: () => import('@screens/scan-list.screen'), + }, + ], }, { - path: '', - lazy: () => import('./screens/scan-list.screen'), + path: '/config', + children: [ + { + path: '', + lazy: () => import('@screens/app-config.screen'), + }, + { + path: 'scan', + lazy: () => import('@screens/scan-config.screen'), + }, + ], }, ], }, - { - path: '/config', - lazy: () => import('./screens/app-config.screen'), - }, { path: '*', Component: () => , diff --git a/src/screens/app-config.screen.tsx b/src/screens/app-config.screen.tsx index a21e9d6..006017f 100644 --- a/src/screens/app-config.screen.tsx +++ b/src/screens/app-config.screen.tsx @@ -1,39 +1,26 @@ -import React, { useCallback, useEffect } from 'react'; -import { t } from 'i18next'; -import { Button, Card } from 'react-daisyui'; -import { useNavigate } from 'react-router-dom'; +import React, { lazy } from 'react'; +import { Card } from 'react-daisyui'; +import { isElectron } from '@shared'; +import { useTranslation } from 'react-i18next'; -const configKey = 'lynx:config'; +const ConfigQrCode = lazy(() => import('../components/config.qr-code')); +const ConfigScanButton = lazy(() => import('../components/config-scan.button')); +const ToListScanButton = lazy( + () => import('../components/to-list-scan.button.tsx') +); export const Component: React.FC = () => { - const navigate = useNavigate(); - - const checkConfig = () => { - const item = localStorage.getItem(configKey); - if (item) { - return JSON.parse(item); - } - }; - const scanConfigAndPersist = useCallback(() => { - console.log('scanConfigAndPersist'); - }, []); - - useEffect(() => { - const config = checkConfig(); - if (config) { - navigate('/scans'); - } - }, [navigate]); + const { t } = useTranslation('config'); return ( -
+
+ {isElectron && } - {t('config.page')} -

{t('config.description')}

+ {t('page')} +

{t('description')}

- + {!isElectron && } + {isElectron && }
diff --git a/src/screens/scan-config.screen.tsx b/src/screens/scan-config.screen.tsx new file mode 100644 index 0000000..6634899 --- /dev/null +++ b/src/screens/scan-config.screen.tsx @@ -0,0 +1,90 @@ +import React, { useState } from 'react'; +import { Header } from '../components/header.tsx'; +import { + IDetectedBarcode, + Scanner, + useDevices, +} from '@yudiel/react-qr-scanner'; +import { Button, Dropdown } from 'react-daisyui'; +import { Camera } from 'react-feather'; +import { setUrlConfig, useAppDispatch } from '@store'; +import { useNavigate } from 'react-router-dom'; + +/** + * ScanConfig screen component + * @constructor React.FC + */ +export const Component: React.FC = () => { + const dispatch = useAppDispatch(); + const navigate = useNavigate(); + const devices = useDevices(); + const [deviceId, setDeviceId] = useState( + devices.length > 0 ? devices[0].deviceId : '' + ); + const onScan = (detectedCodes: IDetectedBarcode[]) => { + for (const { rawValue, format } of detectedCodes) { + if (['qr_code', 'rm_qr_code', 'micro_qr_code'].includes(format)) { + const config = JSON.parse(rawValue) as Record; + console.log({ config }); + dispatch(setUrlConfig(config.url)); + navigate('/scans'); + } + } + }; + return ( +
+
+
+ + + + + {devices.map(({ deviceId, label }) => ( + + + + ))} + + + } + /> +
+
+ { + for (const detectedCode of detectedCodes) { + const { cornerPoints } = detectedCode; + ctx.strokeStyle = '#f5f'; + ctx.lineWidth = 4; + ctx.beginPath(); + ctx.moveTo(cornerPoints[0].x, cornerPoints[0].y); + ctx.lineTo(cornerPoints[1].x, cornerPoints[1].y); + ctx.lineTo(cornerPoints[2].x, cornerPoints[2].y); + ctx.lineTo(cornerPoints[3].x, cornerPoints[3].y); + ctx.lineTo(cornerPoints[0].x, cornerPoints[0].y); + ctx.stroke(); + } + }, + }} + classNames={{ video: 'object-cover' }} + onScan={onScan} + constraints={{ + deviceId, + }} + /> +
+
+ ); +}; diff --git a/src/screens/scan-list.screen.tsx b/src/screens/scan-list.screen.tsx index 1b6d684..c61b87f 100644 --- a/src/screens/scan-list.screen.tsx +++ b/src/screens/scan-list.screen.tsx @@ -1,46 +1,36 @@ import React, { useState } from 'react'; -import { useGetScansQuery } from '@api/scans.api.gen.ts'; -import { Button, Loading } from 'react-daisyui'; -import { ScanListDump } from '../components/scan-list.dump.tsx'; +import { useGetScansQuery } from '@api'; +import { Loading } from 'react-daisyui'; +import { ScanListSimple } from '@components/scan-list.simple.tsx'; import { Plus } from 'react-feather'; import { useNavigate } from 'react-router-dom'; -import { ErrorDump } from '../components/error.dump.tsx'; +import { Header } from '../components/header.tsx'; export const Component: React.FC = () => { const [page, setPage] = useState(0); - const { - data: scans, - error, - isLoading, - } = useGetScansQuery({ page: page, size: 10 }); + const { data: scans, isLoading } = useGetScansQuery({ page: page, size: 10 }); const navigate = useNavigate(); return ( -
-
-

Scans

- -
+ <> +
navigate('/scans/add')} + /> {isLoading && } - {error && } - {scans && ( - setPage((prevState) => ++prevState)} onPrev={() => setPage((prevState) => --prevState)} /> )} -
+ ); }; diff --git a/src/screens/scan.screen.tsx b/src/screens/scan.screen.tsx index f2a59f9..69c1e57 100644 --- a/src/screens/scan.screen.tsx +++ b/src/screens/scan.screen.tsx @@ -1,20 +1,15 @@ import React from 'react'; -import { Button } from 'react-daisyui'; -import { ArrowLeft } from 'react-feather'; -import { useNavigate } from 'react-router-dom'; +import { Header } from '../components/header.tsx'; +/** + * Scan screen + * @constructor React.FC + */ export const Component: React.FC = () => { - const navigate = useNavigate(); return ( -
-
- -

Scan

-
- + <> +
TODO: Form & Camera will be here
-
+ ); }; diff --git a/src/shared/constants.ts b/src/shared/constants.ts new file mode 100644 index 0000000..5f31a1e --- /dev/null +++ b/src/shared/constants.ts @@ -0,0 +1,2 @@ +export const isElectron: boolean = window.electron !== undefined; +export const isDev: boolean = import.meta.env.DEV; diff --git a/src/shared/index.ts b/src/shared/index.ts new file mode 100644 index 0000000..fb47d8a --- /dev/null +++ b/src/shared/index.ts @@ -0,0 +1,8 @@ +export * from './constants'; +export const setupLogging = async () => { + try { + await import('./logging'); + } catch (e) { + console.error(e); + } +}; diff --git a/src/shared/logging.ts b/src/shared/logging.ts new file mode 100644 index 0000000..8966c42 --- /dev/null +++ b/src/shared/logging.ts @@ -0,0 +1,8 @@ +import log from 'electron-log/renderer'; + +console.log({ log }); +console.log = log.log; +console.debug = log.debug; +console.error = log.error; +console.warn = log.warn; +console.trace = log.verbose; diff --git a/src/store/api/axios-base.ts b/src/store/api/axios-base.ts new file mode 100644 index 0000000..c80e167 --- /dev/null +++ b/src/store/api/axios-base.ts @@ -0,0 +1,54 @@ +import type { BaseQueryFn } from '@reduxjs/toolkit/query'; +import axios, { AxiosError, AxiosRequestConfig } from 'axios'; +import { RootState } from '@store'; + +const axiosInstance = axios.create({}); + +export const axiosBaseQuery = + (): BaseQueryFn< + { + url: string; + method?: AxiosRequestConfig['method']; + body?: AxiosRequestConfig['data']; + params?: AxiosRequestConfig['params']; + headers?: AxiosRequestConfig['headers']; + }, + unknown, + unknown + > => + async ({ url, method, body: data, params, headers }, { getState }) => { + try { + const baseUrl = (getState() as RootState).config.url; + const result = await axiosInstance({ + url: baseUrl + url, + method, + data, + params, + headers, + }); + if (typeof result.data === 'string') { + return { data: JSON.parse(result.data) }; + } + return { data: result.data }; + } catch (error) { + if (error instanceof SyntaxError) { + return { + error: { + status: 500, + data: error.message, + }, + }; + } + if (error instanceof AxiosError) { + return { + error: { + status: error.response?.status, + data: error.response?.data || error.message, + }, + }; + } + return { + error, + }; + } + }; diff --git a/src/store/api/empty.api.ts b/src/store/api/empty.api.ts new file mode 100644 index 0000000..35d4d38 --- /dev/null +++ b/src/store/api/empty.api.ts @@ -0,0 +1,10 @@ +import { createApi, retry } from '@reduxjs/toolkit/query/react'; +import { axiosBaseQuery } from './axios-base.ts'; + +// initialize an empty api service that we'll inject endpoints into later as needed +export const emptySplitApi = createApi({ + baseQuery: retry(axiosBaseQuery(), { + maxRetries: 3, + }), + endpoints: () => ({}), +}); diff --git a/src/store/api/index.ts b/src/store/api/index.ts new file mode 100644 index 0000000..8abe34a --- /dev/null +++ b/src/store/api/index.ts @@ -0,0 +1,3 @@ +export * from './empty.api.ts'; +export * from './gen/files.api.gen.ts'; +export * from './gen/scans.api.gen.ts'; diff --git a/src/store/emptyApi.ts b/src/store/emptyApi.ts deleted file mode 100644 index c421f86..0000000 --- a/src/store/emptyApi.ts +++ /dev/null @@ -1,8 +0,0 @@ -// Or from '@reduxjs/toolkit/query' if not using the auto-generated hooks -import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; - -// initialize an empty api service that we'll inject endpoints into later as needed -export const emptySplitApi = createApi({ - baseQuery: fetchBaseQuery({ baseUrl: '/' }), - endpoints: () => ({}), -}); diff --git a/src/store/hooks.ts b/src/store/hooks.ts new file mode 100644 index 0000000..2ee8aa7 --- /dev/null +++ b/src/store/hooks.ts @@ -0,0 +1,15 @@ +import { useCallback } from 'react'; +import { fetchConfigUrl } from './thunks'; +import { useAppDispatch, useAppSelector } from './types'; +import { selectConfigUrl } from '@store/selectors.ts'; + +export function useFetchConfigUrl() { + const dispatch = useAppDispatch(); + return useCallback(() => { + dispatch(fetchConfigUrl()); + }, [dispatch]); +} + +export function useConfigData() { + return useAppSelector(selectConfigUrl); +} diff --git a/src/store/index.ts b/src/store/index.ts index cf846ff..7b75440 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1,18 +1,7 @@ -import { setupListeners } from '@reduxjs/toolkit/query'; -import { configureStore } from '@reduxjs/toolkit'; -import { emptySplitApi } from './emptyApi.ts'; - -export const store = configureStore({ - reducer: { - // Add the generated reducer as a specific top-level slice - [emptySplitApi.reducerPath]: emptySplitApi.reducer, - }, - // Adding the api middleware enables caching, invalidation, polling, - // and other useful features of `rtk-query`. - middleware: (getDefaultMiddleware) => - getDefaultMiddleware().concat(emptySplitApi.middleware), -}); - -// optional, but required for refetchOnFocus/refetchOnReconnect behaviors -// see `setupListeners` docs - takes an optional callback as the 2nd arg for customization -setupListeners(store.dispatch); +export * from './api/empty.api.ts'; +export * from './hooks'; +export * from './store'; +export * from './slices'; +export * from './types'; +export * from './thunks'; +export * from './selectors.ts'; diff --git a/src/store/middlewares.ts b/src/store/middlewares.ts new file mode 100644 index 0000000..ce4fa08 --- /dev/null +++ b/src/store/middlewares.ts @@ -0,0 +1,25 @@ +import type { Middleware, MiddlewareAPI } from '@reduxjs/toolkit'; +import { isRejectedWithValue } from '@reduxjs/toolkit'; +import { AppDispatch } from '@store/types.ts'; +import { addNotification } from '@store/slices'; + +export const rtkQueryErrorLogger: Middleware = + ({ dispatch }: MiddlewareAPI) => + (next) => + (action) => { + // RTK Query uses `createAsyncThunk` from redux-toolkit under the hood, so we're able to utilize these matchers! + if (isRejectedWithValue(action)) { + const message = + 'data' in action.error + ? (action.error.data as { message: string }).message + : action.error.message; + + const msg = + 'data' in (action.payload as Record) + ? (action.payload as Record).data + : message; + if (msg) dispatch(addNotification(msg)); + } + + return next(action); + }; diff --git a/src/store/selectors.ts b/src/store/selectors.ts new file mode 100644 index 0000000..644328f --- /dev/null +++ b/src/store/selectors.ts @@ -0,0 +1,11 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { RootState } from '@store/types.ts'; + +export const selectNotification = createSelector( + (ro: RootState) => ro.notification.messages, + (p) => p.filter(({ message }) => message.length > 0) +); +export const selectConfigUrl = createSelector( + (ro: RootState) => ro.config, + ({ url, loading }) => (loading ? undefined : JSON.stringify({ url })) +); diff --git a/src/store/slices/config.slice.ts b/src/store/slices/config.slice.ts new file mode 100644 index 0000000..af70848 --- /dev/null +++ b/src/store/slices/config.slice.ts @@ -0,0 +1,47 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { fetchConfigUrl } from '@store/thunks'; +import { PURGE } from 'redux-persist'; + +export interface ConfigState { + url?: string; + loading: boolean; +} + +const initialState = { + url: undefined, + loading: true, +} satisfies ConfigState as ConfigState; + +const configSlice = createSlice({ + name: 'config', + initialState, + reducers: { + clear(state) { + delete state.url; + }, + url(state, action: PayloadAction) { + state.url = action.payload; + }, + }, + extraReducers: (builder) => { + // Add reducers for additional action types here, and handle loading state as needed + builder + .addCase(fetchConfigUrl.fulfilled, (state, action) => { + // Add user to the state array + state.url = action.payload; + state.loading = false; + }) + .addCase(fetchConfigUrl.pending, (state) => { + // Add user to the state array + state.loading = true; + }) + .addCase(PURGE, (state) => { + state.url = undefined; + state.loading = true; + }); + }, +}); + +export const { clear: clearConfig, url: setUrlConfig } = configSlice.actions; + +export const reducerConfig = configSlice.reducer; diff --git a/src/store/slices/index.ts b/src/store/slices/index.ts new file mode 100644 index 0000000..40fb1e3 --- /dev/null +++ b/src/store/slices/index.ts @@ -0,0 +1,2 @@ +export * from './config.slice'; +export * from './notification.slice'; diff --git a/src/store/slices/notification.slice.ts b/src/store/slices/notification.slice.ts new file mode 100644 index 0000000..760b344 --- /dev/null +++ b/src/store/slices/notification.slice.ts @@ -0,0 +1,50 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { fetchConfigUrl } from '@store/thunks'; + +interface NotifMessage { + id: string; + message: string; +} + +export interface NotificationState { + messages: NotifMessage[]; +} + +const initialState = { + messages: [], +} satisfies NotificationState as NotificationState; + +const notificationSlice = createSlice({ + name: 'notification', + initialState, + reducers: { + clear(state) { + state.messages = []; + }, + remove(state, action: PayloadAction) { + state.messages = state.messages.filter((p) => p.id !== action.payload); + }, + add(state, action: PayloadAction) { + state.messages.push({ + id: new Date().getTime().toString(), + message: action.payload, + }); + }, + }, + extraReducers: (builder) => { + builder.addCase(fetchConfigUrl.rejected, (state, action) => { + state.messages.push({ + id: new Date().getTime().toString(), + message: action.error?.message ?? JSON.stringify(action.error), + }); + }); + }, +}); + +export const { + clear: clearNotifications, + remove: removeNotification, + add: addNotification, +} = notificationSlice.actions; + +export const reducerNotification = notificationSlice.reducer; diff --git a/src/store/store.ts b/src/store/store.ts new file mode 100644 index 0000000..ca5ed8b --- /dev/null +++ b/src/store/store.ts @@ -0,0 +1,54 @@ +import { combineReducers, configureStore } from '@reduxjs/toolkit'; +import logger from 'redux-logger'; +import { setupListeners } from '@reduxjs/toolkit/query'; +import { reducerConfig, reducerNotification } from '@store/slices'; +import { emptySplitApi } from '@store/api/empty.api.ts'; +import { rtkQueryErrorLogger } from '@store/middlewares.ts'; +import { + FLUSH, + PAUSE, + PERSIST, + persistReducer, + persistStore, + PURGE, + REGISTER, + REHYDRATE, +} from 'redux-persist'; +import storage from 'redux-persist/lib/storage'; +import { PersistConfig } from 'redux-persist/es/types'; + +const rootReducer = combineReducers({ + notification: reducerNotification, + config: reducerConfig, + // Add the generated reducer as a specific top-level slice + [emptySplitApi.reducerPath]: emptySplitApi.reducer, +}); + +type State = ReturnType; + +const persistConfig: PersistConfig = { + key: 'root', + storage, + version: 1, + whitelist: ['config'] as (keyof State)[], +}; + +const persistedReducer = persistReducer(persistConfig, rootReducer); + +export const store = configureStore({ + reducer: persistedReducer, + // Adding the api middleware enables caching, invalidation, polling, + // and other useful features of `rtk-query`. + middleware: (getDefaultMiddleware) => + getDefaultMiddleware({ + serializableCheck: { + ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER], + }, + }).concat(emptySplitApi.middleware, logger, rtkQueryErrorLogger), +}); + +export const persistor = persistStore(store); + +// optional, but required for refetchOnFocus/refetchOnReconnect behaviors +// see `setupListeners` docs - takes an optional callback as the 2nd arg for customization +setupListeners(store.dispatch); diff --git a/src/store/thunks/fetch.config-url.ts b/src/store/thunks/fetch.config-url.ts new file mode 100644 index 0000000..f481b8e --- /dev/null +++ b/src/store/thunks/fetch.config-url.ts @@ -0,0 +1,6 @@ +import { createAsyncThunk } from '@reduxjs/toolkit'; + +export const fetchConfigUrl = createAsyncThunk('config/fetchUrl', async () => { + const rest: string = await window.electron.ipcRenderer.invoke('server-url'); + return rest; +}); diff --git a/src/store/thunks/index.ts b/src/store/thunks/index.ts new file mode 100644 index 0000000..9041023 --- /dev/null +++ b/src/store/thunks/index.ts @@ -0,0 +1 @@ +export * from './fetch.config-url'; diff --git a/src/store/types.ts b/src/store/types.ts new file mode 100644 index 0000000..846bb3f --- /dev/null +++ b/src/store/types.ts @@ -0,0 +1,16 @@ +import { useDispatch, useSelector } from 'react-redux'; +import { ElectronAPI } from '@electron-toolkit/preload'; +import { store } from '@store/store.ts'; + +export type RootState = ReturnType; +export type AppDispatch = typeof store.dispatch; + +export const useAppDispatch = useDispatch.withTypes(); +export const useAppSelector = useSelector.withTypes(); + +declare global { + interface Window { + electron: ElectronAPI; + api: unknown; + } +} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 11f02fe..8b40bdd 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -1 +1,10 @@ /// + +interface ImportMetaEnv { + readonly VITE_BACKEND_URL: string; + // more env variables... +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/tsconfig.json b/tsconfig.json index 3c66774..ba09020 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,7 +19,20 @@ "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, "paths": { - "@api/*": ["src/store/gen/*"] + "@api/*": ["src/store/api/*"], + "@api": ["src/store/api"], + "@shared/*": ["src/shared/*"], + "@shared": ["src/shared"], + "@store/*": ["src/store/*"], + "@store": ["src/store"], + "@screens/*": ["src/screens/*"], + "@screens": ["src/screens"], + "@i18n/*": ["src/i18n/*"], + "@i18n": ["src/i18n"], + "@components/*": ["src/components/*"], + "@components": ["src/components"], + "@assets/*": ["src/assets/*"], + "@assets": ["src/assets"] } }, "include": ["src"], diff --git a/yarn.lock b/yarn.lock index 86c3595..d07584d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1146,6 +1146,13 @@ dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.22.15": + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.25.0.tgz#3af9a91c1b739c569d5d80cc917280919c544ecb" + integrity sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/runtime@^7.23.9": version "7.24.5" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.5.tgz#230946857c053a36ccc66e1dd03b17dd0c4ed02c" @@ -1368,6 +1375,11 @@ resolved "https://registry.yarnpkg.com/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.0.0.tgz#df79b7ea62c55094dc129880387864cdf41eca7c" integrity sha512-ZKXyJeFAzcpKM2kk8ipoGIPUqx9BX52omTGnfwjJvxOCaZTM2wtDK7zN0aIgPRbT9XYAlha0HtmZ+XKteuh0Gw== +"@electron-toolkit/preload@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@electron-toolkit/preload/-/preload-3.0.1.tgz#8bae193fd851f3d38c56eec13a5dd602744e8064" + integrity sha512-EzoQmpK8jqqU8YnM5jRe0GJjGVJPke2KtANqz8QtN2JPT96ViOvProBdK5C6riCm0j1T8jjAGVQCZLQy9OVoIA== + "@esbuild/aix-ppc64@0.20.2": version "0.20.2" resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz#a70f4ac11c6a1dfc18b8bbb13284155d933b9537" @@ -1928,6 +1940,16 @@ dependencies: "@types/node" "*" +"@types/dom-webcodecs@^0.1.11": + version "0.1.11" + resolved "https://registry.yarnpkg.com/@types/dom-webcodecs/-/dom-webcodecs-0.1.11.tgz#2e36e5cc71789551f107e2fe15d956845fa19567" + integrity sha512-yPEZ3z7EohrmOxbk/QTAa0yonMFkNkjnVXqbGb7D4rMr+F1dGQ8ZUFxXkyLLJuiICPejZ0AZE9Rrk9wUCczx4A== + +"@types/emscripten@^1.39.13": + version "1.39.13" + resolved "https://registry.yarnpkg.com/@types/emscripten/-/emscripten-1.39.13.tgz#afeb1648648dc096efe57983e20387627306e2aa" + integrity sha512-cFq+fO/isvhvmuP/+Sl4K4jtU6E23DoivtbO4r50e3odaxAiVdbfSYRDdJ4gCdxx+3aRjhphS5ZMwIH4hFy/Cw== + "@types/eslint@^8.4.5": version "8.56.10" resolved "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz" @@ -1990,6 +2012,13 @@ "@types/prop-types" "*" csstype "^3.0.2" +"@types/redux-logger@^3.0.13": + version "3.0.13" + resolved "https://registry.yarnpkg.com/@types/redux-logger/-/redux-logger-3.0.13.tgz#473e98428cdcc6dc93c908de66732bf932e36bc8" + integrity sha512-jylqZXQfMxahkuPcO8J12AKSSCQngdEWQrw7UiLUJzMBcv1r4Qg77P6mjGLjM27e5gFQDPD8vwUMJ9AyVxFSsg== + dependencies: + redux "^5.0.0" + "@types/semver@^7.3.12", "@types/semver@^7.5.8": version "7.5.8" resolved "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz" @@ -2200,6 +2229,14 @@ "@types/babel__core" "^7.20.5" react-refresh "^0.14.0" +"@yudiel/react-qr-scanner@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@yudiel/react-qr-scanner/-/react-qr-scanner-2.0.4.tgz#7f86edc6c3c6bd5c5f06879802361d858bb2422d" + integrity sha512-ZVSyT6F2S8mdURUCy2Oz9/khZ3DfzsbU8OO5lXYzLzJLVwfhLX+jo2Gd80FyBMqBxM0fbwNNvAawTajcJ3hb8w== + dependencies: + barcode-detector "^2.2.7" + webrtc-adapter "9.0.1" + JSONStream@^1.3.5: version "1.3.5" resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" @@ -2487,6 +2524,11 @@ astral-regex@^2.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + autoprefixer@^10.4.19: version "10.4.19" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.19.tgz#ad25a856e82ee9d7898c59583c1afeb3fa65f89f" @@ -2511,6 +2553,15 @@ axe-core@=4.7.0: resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.7.0.tgz" integrity sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ== +axios@^1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.2.tgz#b625db8a7051fbea61c35a3cbb3a1daa7b9c7621" + integrity sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + axobject-query@^3.2.1: version "3.2.1" resolved "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz" @@ -2593,6 +2644,14 @@ balanced-match@^2.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-2.0.0.tgz#dc70f920d78db8b858535795867bf48f820633d9" integrity sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA== +barcode-detector@^2.2.7: + version "2.2.7" + resolved "https://registry.yarnpkg.com/barcode-detector/-/barcode-detector-2.2.7.tgz#ab47e3e36727a9f5d1e7183240f88831f2451efa" + integrity sha512-+6PJNcMtdVehX5i2LQUE9L+mS6C3cG00Vsuc4Ynj3Mls5GNKIAFkE0IFGtw4s6vu8SXeogrzTj4btm44oD+gNw== + dependencies: + "@types/dom-webcodecs" "^0.1.11" + zxing-wasm "1.2.11" + bare-events@^2.0.0, bare-events@^2.2.0: version "2.2.2" resolved "https://registry.yarnpkg.com/bare-events/-/bare-events-2.2.2.tgz#a98a41841f98b2efe7ecc5c5468814469b018078" @@ -2862,6 +2921,13 @@ colorette@^2.0.20: resolved "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz" integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + commander@11.1.0: version "11.1.0" resolved "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz" @@ -2964,6 +3030,13 @@ cosmiconfig@^7.0.0: path-type "^4.0.0" yaml "^1.10.0" +cross-fetch@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-4.0.0.tgz#f037aef1580bb3a1a35164ea2a848ba81b445983" + integrity sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g== + dependencies: + node-fetch "^2.6.12" + cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz" @@ -3087,6 +3160,11 @@ debug@^3.2.7: dependencies: ms "^2.1.1" +deep-diff@^0.3.5: + version "0.3.8" + resolved "https://registry.yarnpkg.com/deep-diff/-/deep-diff-0.3.8.tgz#c01de63efb0eec9798801d40c7e0dae25b582c84" + integrity sha512-yVn6RZmHiGnxRKR9sJb3iVV2XTF1Ghh2DiWRZ3dMnGc43yUdWWF/kX6lQyk3+P84iprfWKU/8zFTrlkvtFm1ug== + deep-is@^0.1.3: version "0.1.4" resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" @@ -3119,6 +3197,11 @@ degenerator@^5.0.0: escodegen "^2.1.0" esprima "^4.0.1" +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + dequal@^2.0.3: version "2.0.3" resolved "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz" @@ -3172,6 +3255,11 @@ eastasianwidth@^0.2.0: resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== +electron-log@^5.1.7: + version "5.1.7" + resolved "https://registry.yarnpkg.com/electron-log/-/electron-log-5.1.7.tgz#73c7ddc1602b3a9ee355bc09d1dc490864add0eb" + integrity sha512-/PjrS9zGkrZCDTHt6IgNE3FeciBbi4wd7U76NG9jAoNXF99E9IJdvBkqvaUJ1NjLojYDKs0kTvn9YhKy1/Zi+Q== + electron-to-chromium@^1.4.668: version "1.4.746" resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.746.tgz" @@ -3879,6 +3967,11 @@ flux-standard-action@^0.6.0: dependencies: lodash.isplainobject "^3.2.0" +follow-redirects@^1.15.6: + version "1.15.6" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" + integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== + for-each@^0.3.3: version "0.3.3" resolved "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz" @@ -3894,6 +3987,15 @@ foreground-child@^3.1.0: cross-spawn "^7.0.0" signal-exit "^4.0.1" +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + fraction.js@^4.3.7: version "4.3.7" resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" @@ -4286,6 +4388,27 @@ i18next-browser-languagedetector@*: dependencies: "@babel/runtime" "^7.23.2" +i18next-chained-backend@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/i18next-chained-backend/-/i18next-chained-backend-4.6.2.tgz#96bd1fd8c0f719154040665b435cf3f032d7ccec" + integrity sha512-2P092fR+nAPQlGzPUoIIxbwo7PTBqQYgLxwv1XhSTQUAUoelLo5LkX+FqRxxSDg9WEAsrc8+2WL6mJtMGIa6WQ== + dependencies: + "@babel/runtime" "^7.23.2" + +i18next-http-backend@^2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/i18next-http-backend/-/i18next-http-backend-2.5.2.tgz#3d846cc239987fe7700d1cf0f17975807bfd25d3" + integrity sha512-+K8HbDfrvc1/2X8jpb7RLhI9ZxBDpx3xogYkQwGKlWAUXLSEGXzgdt3EcUjLlBCdMwdQY+K+EUF6oh8oB6rwHw== + dependencies: + cross-fetch "4.0.0" + +i18next-localstorage-backend@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/i18next-localstorage-backend/-/i18next-localstorage-backend-4.2.0.tgz#b25e8943f4e135bf55da016cf4bddc26588ba609" + integrity sha512-vglEQF0AnLriX7dLA2drHnqAYzHxnLwWQzBDw8YxcIDjOvYZz5rvpal59Dq4In+IHNmGNM32YgF0TDjBT0fHmA== + dependencies: + "@babel/runtime" "^7.22.15" + i18next@^23.11.4: version "23.11.4" resolved "https://registry.yarnpkg.com/i18next/-/i18next-23.11.4.tgz#3f0e620fd2cff3825324191615d0ab0a1eec3baf" @@ -5184,6 +5307,11 @@ micromatch@4.0.5, micromatch@^4.0.4, micromatch@^4.0.5: braces "^3.0.2" picomatch "^2.3.1" +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + mime-db@~1.33.0: version "1.33.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" @@ -5196,6 +5324,13 @@ mime-types@2.1.18: dependencies: mime-db "~1.33.0" +mime-types@^2.1.12: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + mimic-fn@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" @@ -5303,7 +5438,7 @@ node-fetch-h2@^2.3.0: dependencies: http2-client "^1.2.5" -node-fetch@^2.6.1: +node-fetch@^2.6.1, node-fetch@^2.6.12: version "2.7.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== @@ -5857,6 +5992,11 @@ puppeteer@>=8.0.0: devtools-protocol "0.0.1273771" puppeteer-core "22.8.0" +qr.js@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/qr.js/-/qr.js-0.0.0.tgz#cace86386f59a0db8050fa90d9b6b0e88a1e364f" + integrity sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ== + queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" @@ -5905,6 +6045,14 @@ react-is@^16.13.1, react-is@^16.7.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-qr-code@^2.0.15: + version "2.0.15" + resolved "https://registry.yarnpkg.com/react-qr-code/-/react-qr-code-2.0.15.tgz#fbfc12952c504bcd64275647e9d1ea63251742ce" + integrity sha512-MkZcjEXqVKqXEIMVE0mbcGgDpkfSdd8zhuzXEl9QzYeNcw8Hq2oVIzDLWuZN2PQBwM5PWjc2S31K8Q1UbcFMfw== + dependencies: + prop-types "^15.8.1" + qr.js "0.0.0" + react-redux@^9.1.2: version "9.1.2" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-9.1.2.tgz#deba38c64c3403e9abd0c3fbeab69ffd9d8a7e4b" @@ -5954,6 +6102,18 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +redux-logger@^3.0.6: + version "3.0.6" + resolved "https://registry.yarnpkg.com/redux-logger/-/redux-logger-3.0.6.tgz#f7555966f3098f3c88604c449cf0baf5778274bf" + integrity sha512-JoCIok7bg/XpqA1JqCqXFypuqBbQzGQySrhFzewB7ThcnysTO30l4VCst86AuB9T9tuT03MAA56Jw2PNhRSNCg== + dependencies: + deep-diff "^0.3.5" + +redux-persist@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/redux-persist/-/redux-persist-6.0.0.tgz#b4d2972f9859597c130d40d4b146fecdab51b3a8" + integrity sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ== + redux-thunk@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-3.1.0.tgz#94aa6e04977c30e14e892eae84978c1af6058ff3" @@ -5969,7 +6129,7 @@ redux-toolkit@^1.1.2: invariant "^2.1.1" lodash "^3.10.1" -redux@^5.0.1: +redux@^5.0.0, redux@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/redux/-/redux-5.0.1.tgz#97fa26881ce5746500125585d5642c77b6e9447b" integrity sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w== @@ -6215,6 +6375,11 @@ scheduler@^0.23.0: dependencies: loose-envify "^1.1.0" +sdp@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/sdp/-/sdp-3.2.0.tgz#8961420552b36663b4d13ddba6f478d1461896a5" + integrity sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw== + section-matter@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/section-matter/-/section-matter-1.0.0.tgz#e9041953506780ec01d59f292a19c7b850b84167" @@ -7197,6 +7362,13 @@ webidl-conversions@^3.0.0: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== +webrtc-adapter@9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/webrtc-adapter/-/webrtc-adapter-9.0.1.tgz#d4efa22ca9604cb2c8cdb9e492815ba37acfa0b2" + integrity sha512-1AQO+d4ElfVSXyzNVTOewgGT/tAomwwztX/6e3totvyyzXPvXIIuUUjAmyZGbKBKbZOXauuJooZm3g6IuFuiNQ== + dependencies: + sdp "^3.2.0" + whatwg-url@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" @@ -7406,3 +7578,10 @@ zod@3.22.4: version "3.22.4" resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff" integrity sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg== + +zxing-wasm@1.2.11: + version "1.2.11" + resolved "https://registry.yarnpkg.com/zxing-wasm/-/zxing-wasm-1.2.11.tgz#8263ba27ea6d209d7a8440fac15a1fb6838d4630" + integrity sha512-rNSMkIU310sK5cCPSjZA58FEhGZUtNx+f0CmtZ3SZzpdwZE6IzzKFdkbFkl8CFnxiUrx1VMVl/2WULDnwwJbfg== + dependencies: + "@types/emscripten" "^1.39.13"