+
+
),
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',
+ lazy: () => import('./screens/app-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..6382f75 100644
--- a/src/screens/app-config.screen.tsx
+++ b/src/screens/app-config.screen.tsx
@@ -1,7 +1,11 @@
-import React, { useCallback, useEffect } from 'react';
+import React, { lazy, useCallback, useEffect } from 'react';
import { t } from 'i18next';
import { Button, Card } from 'react-daisyui';
import { useNavigate } from 'react-router-dom';
+import { isElectron } from '../shared/constants.ts';
+import { ArrowRight } from 'react-feather';
+
+const ConfigQrCode = lazy(() => import('../components/config.qr-code'));
const configKey = 'lynx:config';
@@ -17,6 +21,7 @@ export const Component: React.FC = () => {
const scanConfigAndPersist = useCallback(() => {
console.log('scanConfigAndPersist');
}, []);
+ const toScans = useCallback(() => navigate('/scans'), [navigate]);
useEffect(() => {
const config = checkConfig();
@@ -27,12 +32,19 @@ export const Component: React.FC = () => {
return (
+ {isElectron && }
{t('config.page')}
{t('config.description')}
-
diff --git a/src/screens/scan-list.screen.tsx b/src/screens/scan-list.screen.tsx
index 1b6d684..818e4d6 100644
--- a/src/screens/scan-list.screen.tsx
+++ b/src/screens/scan-list.screen.tsx
@@ -4,15 +4,10 @@ import { Button, Loading } from 'react-daisyui';
import { ScanListDump } from '../components/scan-list.dump.tsx';
import { Plus } from 'react-feather';
import { useNavigate } from 'react-router-dom';
-import { ErrorDump } from '../components/error.dump.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();
@@ -31,8 +26,6 @@ export const Component: React.FC = () => {
{isLoading && }
- {error && }
-
{scans && (
{
+ dispatch(fetchConfigUrl());
+ }, [dispatch]);
+}
diff --git a/src/store/index.ts b/src/store/index.ts
index cf846ff..ddae117 100644
--- a/src/store/index.ts
+++ b/src/store/index.ts
@@ -1,18 +1,6 @@
-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 './empty.api';
+export * from './hooks';
+export * from './store';
+export * from './slices';
+export * from './types';
+export * from './thunks';
diff --git a/src/store/middlewares.ts b/src/store/middlewares.ts
new file mode 100644
index 0000000..81f956d
--- /dev/null
+++ b/src/store/middlewares.ts
@@ -0,0 +1,20 @@
+import type { Middleware, MiddlewareAPI } from '@reduxjs/toolkit';
+import { isRejectedWithValue } from '@reduxjs/toolkit';
+import { addNotification } from './slices';
+import { AppDispatch } from './types.ts';
+
+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;
+ if (message) dispatch(addNotification(message));
+ }
+
+ return next(action);
+ };
diff --git a/src/store/slices/config.slice.ts b/src/store/slices/config.slice.ts
new file mode 100644
index 0000000..002cb2d
--- /dev/null
+++ b/src/store/slices/config.slice.ts
@@ -0,0 +1,35 @@
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+import { fetchConfigUrl } from '../thunks';
+
+export interface ConfigState {
+ url?: string;
+}
+
+const initialState = { url: undefined } 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;
+ });
+ },
+});
+
+export const { clear: clearConfig, url: setUrlConfig } = configSlice.actions;
+
+export const reducerConfig = configSlice.reducer;
+
+export const urlConfigSelector = ({ config }: { config: ConfigState }) =>
+ JSON.stringify(config);
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..22ac007
--- /dev/null
+++ b/src/store/slices/notification.slice.ts
@@ -0,0 +1,47 @@
+import { fetchConfigUrl } from '../thunks';
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+
+export interface NotificationState {
+ messages: string[];
+}
+
+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 !== action.payload);
+ },
+ add(state, action: PayloadAction) {
+ state.messages.push(action.payload);
+ },
+ },
+ extraReducers: (builder) => {
+ builder.addCase(fetchConfigUrl.rejected, (state, action) => {
+ state.messages.push(
+ action.error?.message ?? JSON.stringify(action.error)
+ );
+ });
+ },
+});
+
+export const {
+ clear: clearNotifications,
+ remove: removeNotification,
+ add: addNotification,
+} = notificationSlice.actions;
+
+export const reducerNotification = notificationSlice.reducer;
+
+export const selectNotifications = ({
+ notification,
+}: {
+ notification: NotificationState;
+}) => notification.messages.filter((p) => !!p).map((p) => p!);
diff --git a/src/store/store.ts b/src/store/store.ts
new file mode 100644
index 0000000..f82eeb1
--- /dev/null
+++ b/src/store/store.ts
@@ -0,0 +1,27 @@
+import { configureStore } from '@reduxjs/toolkit';
+import { reducerConfig, reducerNotification } from './slices';
+import { emptySplitApi } from './empty.api';
+import logger from 'redux-logger';
+import { setupListeners } from '@reduxjs/toolkit/query';
+import { rtkQueryErrorLogger } from './middlewares.ts';
+
+export const store = configureStore({
+ reducer: {
+ notification: reducerNotification,
+ config: reducerConfig,
+ // 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,
+ logger,
+ rtkQueryErrorLogger
+ ),
+});
+
+// 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..92505f1
--- /dev/null
+++ b/src/store/types.ts
@@ -0,0 +1,16 @@
+import { useDispatch, useSelector } from 'react-redux';
+import { store } from './store';
+import { ElectronAPI } from '@electron-toolkit/preload';
+
+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..2019e70 100644
--- a/src/vite-env.d.ts
+++ b/src/vite-env.d.ts
@@ -1 +1,10 @@
///
+
+interface ImportMetaEnv {
+ readonly VITE_ELECTRON: string;
+ // more env variables...
+}
+
+interface ImportMeta {
+ readonly env: ImportMetaEnv;
+}
diff --git a/yarn.lock b/yarn.lock
index 86c3595..064361c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1368,6 +1368,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"
@@ -1990,6 +1995,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"
@@ -3087,6 +3099,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"
@@ -3172,6 +3189,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"
@@ -5857,6 +5879,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 +5932,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 +5989,13 @@ 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-thunk@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-3.1.0.tgz#94aa6e04977c30e14e892eae84978c1af6058ff3"
@@ -5969,7 +6011,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==