-
Company logo
+
+
+
+ >
);
}
diff --git a/app/create-card/createCardFormReducer.spec.ts b/app/create-card/createCardFormReducer.spec.ts
new file mode 100644
index 0000000..c1bbedc
--- /dev/null
+++ b/app/create-card/createCardFormReducer.spec.ts
@@ -0,0 +1,80 @@
+import { describe, expect, it } from '@jest/globals';
+import {
+ CreateCardFormActionTypes,
+ createCardFormReducer,
+ CreateCardFormState,
+ SetDevicesAction,
+ ToggleActiveDeviceAction,
+ UpdateModalVisibilityAction,
+} from './createCardFormReducer';
+
+describe('createCardFormReducer', () => {
+ const initialState: CreateCardFormState = {
+ devices: [],
+ activeDevice: null,
+ isModalVisible: false,
+ };
+
+ it('should handle SET_DEVICES action', () => {
+ const devices = [
+ { id: '1', label: 'Camera 1' },
+ { id: '2', label: 'Camera 2' },
+ ];
+ const action: SetDevicesAction = {
+ type: CreateCardFormActionTypes.SET_DEVICES,
+ payload: devices,
+ };
+ const state = createCardFormReducer(initialState, action);
+ expect(state.devices).toEqual(devices);
+ expect(state.activeDevice).toEqual(devices[0]);
+ });
+
+ it('should handle TOGGLE_ACTIVE_DEVICE action', () => {
+ const devices = [
+ { id: '1', label: 'Camera 1' },
+ { id: '2', label: 'Camera 2' },
+ ];
+ const stateWithDevices: CreateCardFormState = {
+ ...initialState,
+ devices: devices,
+ activeDevice: devices[0],
+ };
+ const action: ToggleActiveDeviceAction = {
+ type: CreateCardFormActionTypes.TOGGLE_ACTIVE_DEVICE,
+ };
+ const stateAfterToggle = createCardFormReducer(stateWithDevices, action);
+ expect(stateAfterToggle.activeDevice).toEqual(devices[1]);
+
+ // Toggle again to loop back to the first device
+ const stateAfterSecondToggle = createCardFormReducer(
+ stateAfterToggle,
+ action
+ );
+ expect(stateAfterSecondToggle.activeDevice).toEqual(devices[0]);
+ });
+
+ it('should handle TOGGLE_ACTIVE_DEVICE action for initial state', () => {
+ const action: ToggleActiveDeviceAction = {
+ type: CreateCardFormActionTypes.TOGGLE_ACTIVE_DEVICE,
+ };
+ const stateAfterToggle = createCardFormReducer(initialState, action);
+ expect(stateAfterToggle.activeDevice).toEqual(null);
+ });
+
+ it('should handle UPDATE_MODAL_VISIBILITY action for initial state', () => {
+ const action: UpdateModalVisibilityAction = {
+ type: CreateCardFormActionTypes.UPDATE_MODAL_VISIBILITY,
+ payload: true,
+ };
+ const stateAfterToggle = createCardFormReducer(initialState, action);
+ expect(stateAfterToggle.isModalVisible).toEqual(true);
+ });
+
+ it('should return the initial state for an unknown action type', () => {
+ const unknownAction = { type: 'UNKNOWN_ACTION' };
+
+ // @ts-expect-error: Testing for unknown action type
+ const state = createCardFormReducer(initialState, unknownAction);
+ expect(state).toEqual(initialState);
+ });
+});
diff --git a/app/create-card/createCardFormReducer.ts b/app/create-card/createCardFormReducer.ts
new file mode 100644
index 0000000..040edc9
--- /dev/null
+++ b/app/create-card/createCardFormReducer.ts
@@ -0,0 +1,70 @@
+import { CameraDevice } from 'html5-qrcode';
+
+export type CreateCardFormState = {
+ devices: CameraDevice[];
+ activeDevice: CameraDevice | null;
+ isModalVisible: boolean;
+};
+
+export enum CreateCardFormActionTypes {
+ SET_DEVICES = 'SET_DEVICES',
+ TOGGLE_ACTIVE_DEVICE = 'TOGGLE_ACTIVE_DEVICE',
+ UPDATE_MODAL_VISIBILITY = 'UPDATE_MODAL_VISIBILITY',
+}
+
+export type SetDevicesAction = {
+ type: CreateCardFormActionTypes.SET_DEVICES;
+ payload: CameraDevice[];
+};
+
+export type ToggleActiveDeviceAction = {
+ type: CreateCardFormActionTypes.TOGGLE_ACTIVE_DEVICE;
+};
+export type UpdateModalVisibilityAction = {
+ type: CreateCardFormActionTypes.UPDATE_MODAL_VISIBILITY;
+ payload: boolean;
+};
+
+export type CreateCardFormActions =
+ | SetDevicesAction
+ | ToggleActiveDeviceAction
+ | UpdateModalVisibilityAction;
+
+export const initialState: CreateCardFormState = {
+ activeDevice: null,
+ devices: [],
+ isModalVisible: false,
+};
+
+export const createCardFormReducer = (
+ state: CreateCardFormState,
+ action: CreateCardFormActions
+): CreateCardFormState => {
+ switch (action.type) {
+ case CreateCardFormActionTypes.SET_DEVICES:
+ return {
+ ...state,
+ devices: action.payload,
+ activeDevice: action.payload[0] ?? null,
+ };
+ case CreateCardFormActionTypes.TOGGLE_ACTIVE_DEVICE:
+ if (state.activeDevice === null || state.devices.length < 2) {
+ return state;
+ }
+ const currentIndex = state.devices.findIndex(
+ device => device.id === state.activeDevice!.id
+ );
+ const nextIndex = (currentIndex + 1) % state.devices.length;
+ return {
+ ...state,
+ activeDevice: state.devices[nextIndex],
+ };
+ case CreateCardFormActionTypes.UPDATE_MODAL_VISIBILITY:
+ return {
+ ...state,
+ isModalVisible: action.payload,
+ };
+ default:
+ return state;
+ }
+};
diff --git a/app/create-card/scanner.tsx b/app/create-card/scanner.tsx
new file mode 100644
index 0000000..c1df915
--- /dev/null
+++ b/app/create-card/scanner.tsx
@@ -0,0 +1,127 @@
+'use client';
+
+import {
+ CreateCardFormActions,
+ CreateCardFormActionTypes,
+} from '@/app/create-card/createCardFormReducer';
+import {
+ Html5Qrcode,
+ Html5QrcodeResult,
+ Html5QrcodeScannerState,
+ Html5QrcodeCameraScanConfig,
+ CameraDevice,
+} from 'html5-qrcode';
+import { Dispatch, useCallback, useEffect, useRef } from 'react';
+
+const getConfig = (boundingRect?: DOMRect): Html5QrcodeCameraScanConfig => {
+ let qrbox = {
+ width: 250,
+ height: 250,
+ };
+ if (boundingRect) {
+ const width = Math.max(50, Math.ceil(boundingRect.width * 0.8));
+ qrbox = {
+ width,
+ height: Math.max(
+ 50,
+ Math.min(Math.ceil(boundingRect.height * 0.8), width)
+ ),
+ };
+ }
+ return {
+ fps: 10,
+ qrbox,
+ };
+};
+
+export default function Scanner({
+ onCodeDetected,
+ activeDevice,
+ dispatch,
+}: {
+ onCodeDetected: (decodedText: string, result: Html5QrcodeResult) => void;
+ activeDevice: CameraDevice | null;
+ dispatch: Dispatch
;
+}) {
+ const parentDiv = useRef(null);
+
+ const getCameraDevices = useCallback(
+ async function (): Promise {
+ let devices: CameraDevice[];
+ try {
+ devices = await Html5Qrcode.getCameras();
+ } catch (e) {
+ console.log('error while querying cameras', e);
+ devices = [];
+ }
+ dispatch({
+ type: CreateCardFormActionTypes.SET_DEVICES,
+ payload: devices,
+ });
+ },
+ [dispatch]
+ );
+
+ const startScanning = useCallback(
+ (activeDeviceId: string): (() => void) => {
+ const reader: Html5Qrcode = new Html5Qrcode('reader');
+ reader
+ .start(
+ activeDeviceId,
+ getConfig(parentDiv.current?.getBoundingClientRect()),
+ onCodeDetected,
+ undefined
+ )
+ .catch(e => {
+ console.log('error', e);
+ });
+ return () => {
+ if (reader.getState() === Html5QrcodeScannerState.SCANNING) {
+ reader.stop().finally(() => reader.clear());
+ } else {
+ reader.clear();
+ }
+ };
+ },
+ [onCodeDetected]
+ );
+
+ useEffect(() => {
+ getCameraDevices();
+ }, [getCameraDevices]);
+
+ useEffect(() => {
+ let cleanup: () => void;
+ if (activeDevice?.id) {
+ cleanup = startScanning(activeDevice.id);
+ }
+ return () => {
+ cleanup?.();
+ };
+ }, [activeDevice?.id, startScanning]);
+
+ return (
+
+
+ {!activeDevice && (
+
+
Please grant camera permissions.
+
+ Without those permissions, it will be impossible to scan and save
+ your cards.
+
+
+
+ )}
+
+ );
+}
diff --git a/app/layout.tsx b/app/layout.tsx
index bd5563a..2e35bcd 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,4 +1,4 @@
-import type { Metadata } from 'next';
+import type { Metadata, Viewport } from 'next';
import localFont from 'next/font/local';
import './globals.css';
@@ -21,13 +21,18 @@ export const metadata: Metadata = {
authors: [{ name: 'Lukas Bicus', url: 'https://github.com/LukasBicus' }],
};
+export const viewport: Viewport = {
+ width: 'device-width',
+ initialScale: 1,
+};
+
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
-
+
{
@@ -24,7 +25,7 @@ describe('appReducer', () => {
const newCard: Omit = {
bgColor: '#4523C9',
icon: CardIcon.Retail,
- codeType: CodeType.QrCode,
+ codeFormat: Html5QrcodeSupportedFormats.QR_CODE,
name: 'Test Card',
code: 'ABC123',
};
diff --git a/app/lib/app-state/reducer.ts b/app/lib/app-state/reducer.ts
index 86bfe2c..2be0ff6 100644
--- a/app/lib/app-state/reducer.ts
+++ b/app/lib/app-state/reducer.ts
@@ -1,4 +1,5 @@
-import { CardIcon, CodeType, SvgProps } from '@/app/lib/shared';
+import { CardIcon, SvgProps } from '@/app/lib/shared';
+import { Html5QrcodeSupportedFormats } from 'html5-qrcode/src/core';
import { v4 as uuid } from 'uuid';
export type Card = {
@@ -9,7 +10,7 @@ export type Card = {
bgColor: string | null;
icon: CardIcon | null | SvgProps;
favorite?: boolean;
- codeType: CodeType;
+ codeFormat: Html5QrcodeSupportedFormats;
};
export type AppState = {
diff --git a/app/lib/predefined-companies.ts b/app/lib/predefined-companies.ts
index f985379..f1c0891 100644
--- a/app/lib/predefined-companies.ts
+++ b/app/lib/predefined-companies.ts
@@ -1,9 +1,10 @@
-import { CodeType, SvgProps } from '@/app/lib/shared';
+import { SvgProps } from '@/app/lib/shared';
+import { Html5QrcodeSupportedFormats } from 'html5-qrcode';
export const predefinedCompanies: {
name: string;
svg: SvgProps;
- codeType: CodeType;
+ codeFormat: Html5QrcodeSupportedFormats;
}[] = [
{
name: 'DM',
@@ -12,7 +13,7 @@ export const predefinedCompanies: {
height: 1620,
width: 2500,
},
- codeType: CodeType.Barcode,
+ codeFormat: Html5QrcodeSupportedFormats.EAN_13,
},
{
name: 'Albert',
@@ -21,7 +22,7 @@ export const predefinedCompanies: {
width: 2500,
height: 2500,
},
- codeType: CodeType.Barcode,
+ codeFormat: Html5QrcodeSupportedFormats.EAN_13,
},
{
name: 'Bata',
@@ -30,7 +31,7 @@ export const predefinedCompanies: {
width: 2500,
height: 2500,
},
- codeType: CodeType.Barcode,
+ codeFormat: Html5QrcodeSupportedFormats.EAN_13,
},
{
name: 'Biedronka',
@@ -39,7 +40,7 @@ export const predefinedCompanies: {
width: 2500,
height: 2500,
},
- codeType: CodeType.Barcode,
+ codeFormat: Html5QrcodeSupportedFormats.EAN_13,
},
{
name: 'Fresh',
@@ -48,7 +49,7 @@ export const predefinedCompanies: {
width: 510.5,
height: 182.7,
},
- codeType: CodeType.Barcode,
+ codeFormat: Html5QrcodeSupportedFormats.EAN_13,
},
{
name: 'Ikea',
@@ -57,7 +58,7 @@ export const predefinedCompanies: {
width: 2500,
height: 2500,
},
- codeType: CodeType.Barcode,
+ codeFormat: Html5QrcodeSupportedFormats.EAN_13,
},
{
name: 'Kaufland',
@@ -66,7 +67,7 @@ export const predefinedCompanies: {
width: 2500,
height: 2500,
},
- codeType: CodeType.Barcode,
+ codeFormat: Html5QrcodeSupportedFormats.EAN_13,
},
{
name: 'Lidl',
@@ -75,7 +76,7 @@ export const predefinedCompanies: {
width: 2500,
height: 2500,
},
- codeType: CodeType.QrCode,
+ codeFormat: Html5QrcodeSupportedFormats.QR_CODE,
},
{
name: 'Billa',
@@ -84,7 +85,7 @@ export const predefinedCompanies: {
width: 2500,
height: 2500,
},
- codeType: CodeType.Barcode,
+ codeFormat: Html5QrcodeSupportedFormats.EAN_13,
},
{
name: 'O2',
@@ -93,7 +94,7 @@ export const predefinedCompanies: {
width: 2500,
height: 2500,
},
- codeType: CodeType.Barcode,
+ codeFormat: Html5QrcodeSupportedFormats.EAN_13,
},
{
name: 'Rossmann',
@@ -102,7 +103,7 @@ export const predefinedCompanies: {
width: 116.6,
height: 14.3,
},
- codeType: CodeType.Barcode,
+ codeFormat: Html5QrcodeSupportedFormats.EAN_13,
},
{
name: 'Tesco',
@@ -111,6 +112,6 @@ export const predefinedCompanies: {
width: 2500,
height: 2500,
},
- codeType: CodeType.Barcode,
+ codeFormat: Html5QrcodeSupportedFormats.EAN_13,
},
];
diff --git a/app/lib/shared.ts b/app/lib/shared.ts
index 3f19a74..d053686 100644
--- a/app/lib/shared.ts
+++ b/app/lib/shared.ts
@@ -115,11 +115,6 @@ export const colorNames = {
[Colors.Burlywood]: 'Burlywood',
};
-export enum CodeType {
- QrCode = 'QrCode',
- Barcode = 'Barcode',
-}
-
export type SvgProps = {
src: string;
width: number;
diff --git a/app/ui/text-field.tsx b/app/ui/text-field.tsx
index 12cef9a..806060d 100644
--- a/app/ui/text-field.tsx
+++ b/app/ui/text-field.tsx
@@ -7,10 +7,12 @@ export function TextField({
className,
name,
register,
+ disabled,
}: {
label: string;
placeholder?: string;
className?: string;
+ disabled?: boolean;
name: Path;
register: UseFormRegister;
}) {
@@ -23,6 +25,7 @@ export function TextField({
type="text"
placeholder={placeholder}
{...register(name)}
+ disabled={disabled}
className="input input-bordered w-full"
/>
diff --git a/next.config.mjs b/next.config.mjs
index b012115..880bb90 100644
--- a/next.config.mjs
+++ b/next.config.mjs
@@ -1,7 +1,9 @@
import withSerwistInit from '@serwist/next';
/** @type {import('next').NextConfig} */
-const nextConfig = {};
+const nextConfig = {
+ // reactStrictMode: false,
+};
const withSerwist = withSerwistInit({
swSrc: 'app/sw.ts',
diff --git a/package-lock.json b/package-lock.json
index 52435b7..530936d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,6 +11,7 @@
"@serwist/next": "^9.0.9",
"@tabler/icons-react": "^3.19.0",
"clsx": "^2.1.1",
+ "html5-qrcode": "^2.3.8",
"next": "14.2.15",
"next-pwa": "^5.6.0",
"react": "^18",
@@ -7258,6 +7259,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/html5-qrcode": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/html5-qrcode/-/html5-qrcode-2.3.8.tgz",
+ "integrity": "sha512-jsr4vafJhwoLVEDW3n1KvPnCCXWaQfRng0/EEYk1vNcQGcG/htAdhJX0be8YyqMoSz7+hZvOZSTAepsabiuhiQ==",
+ "license": "Apache-2.0"
+ },
"node_modules/http-proxy-agent": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
diff --git a/package.json b/package.json
index ceaa66d..e48e667 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,7 @@
"@serwist/next": "^9.0.9",
"@tabler/icons-react": "^3.19.0",
"clsx": "^2.1.1",
+ "html5-qrcode": "^2.3.8",
"next": "14.2.15",
"next-pwa": "^5.6.0",
"react": "^18",
diff --git a/plan.md b/plan.md
index c014ce9..94cdfd0 100644
--- a/plan.md
+++ b/plan.md
@@ -27,9 +27,9 @@ Features:
- [ ] _implement search in predefined companies_
- [x] **implement add card button + navigation to scan code page**
- [x] _implement click on predefined company + navigation to create card page_
-- [ ] _Scan code page_
- - [ ] _implement barcode scan_
- - [ ] _implement navigation to qrcode scan + scan itself_
+- [x] _Scan code page_
+ - [x] _implement barcode scan_
+ - [x] _implement navigation to qrcode scan + scan itself_
- [ ] **Create card page**
- [x] **implement form**
- [x] _implement navigation to scan code page_
diff --git a/tsconfig.json b/tsconfig.json
index b953ef8..090f6bb 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -23,5 +23,5 @@
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
- "exclude": ["node_modules", "public/sw.js"]
+ "exclude": ["node_modules", "public/sw.js", ".next"]
}