Restructure project to root layout

This commit is contained in:
beatz174-bit
2025-11-21 16:10:41 +10:00
parent e0e26a34ab
commit 3ffe9bbf99
51 changed files with 16 additions and 12 deletions
+90
View File
@@ -0,0 +1,90 @@
import { liveQuery } from 'dexie';
import { useEffect, useState } from 'react';
import { useDatabase } from '../context/DBProvider';
import { Area } from '../models/Area';
import { PickItem } from '../models/PickItem';
import { PickList } from '../models/PickList';
import { Product } from '../models/Product';
export const useProducts = () => {
const db = useDatabase();
const [products, setProducts] = useState<Product[]>([]);
useEffect(() => {
const subscription = liveQuery(() => db.products.toArray()).subscribe({
next: setProducts,
});
return () => subscription.unsubscribe();
}, [db]);
return products;
};
export const useProduct = (id?: string) => {
const db = useDatabase();
const [product, setProduct] = useState<Product | undefined>();
useEffect(() => {
if (!id) return;
const subscription = liveQuery(() => db.products.get(id)).subscribe({
next: (value) => setProduct(value ?? undefined),
});
return () => subscription.unsubscribe();
}, [db, id]);
return product;
};
export const useAreas = () => {
const db = useDatabase();
const [areas, setAreas] = useState<Area[]>([]);
useEffect(() => {
const subscription = liveQuery(() => db.areas.toArray()).subscribe({
next: setAreas,
});
return () => subscription.unsubscribe();
}, [db]);
return areas;
};
export const usePickLists = () => {
const db = useDatabase();
const [lists, setLists] = useState<PickList[]>([]);
useEffect(() => {
const subscription = liveQuery(() => db.pickLists.toArray()).subscribe({
next: setLists,
});
return () => subscription.unsubscribe();
}, [db]);
return lists;
};
export const usePickList = (id?: string) => {
const db = useDatabase();
const [list, setList] = useState<PickList | undefined>();
useEffect(() => {
if (!id) return;
const subscription = liveQuery(() => db.pickLists.get(id)).subscribe({
next: (value) => setList(value ?? undefined),
});
return () => subscription.unsubscribe();
}, [db, id]);
return list;
};
export const usePickItems = (pickListId?: string) => {
const db = useDatabase();
const [items, setItems] = useState<PickItem[]>([]);
useEffect(() => {
if (!pickListId) return undefined;
const subscription = liveQuery(() =>
db.pickItems.where('pick_list_id').equals(pickListId).toArray(),
).subscribe({
next: setItems,
});
return () => subscription.unsubscribe();
}, [db, pickListId]);
return items;
};
+68
View File
@@ -0,0 +1,68 @@
import { useEffect, useRef, useState } from 'react';
import { BrowserMultiFormatReader } from '@zxing/browser';
export interface BarcodeResult {
code?: string;
error?: string;
}
export const useBarcodeScanner = () => {
const videoRef = useRef<HTMLVideoElement | null>(null);
const [result, setResult] = useState<BarcodeResult>({});
useEffect(() => {
const reader = new BrowserMultiFormatReader();
let active = true;
let stop: (() => void) | undefined;
const start = async () => {
try {
if ('BarcodeDetector' in window) {
const detector = new (window as typeof window & { BarcodeDetector: any }).BarcodeDetector();
const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } });
if (videoRef.current) {
videoRef.current.srcObject = stream;
await videoRef.current.play();
}
const scan = async () => {
if (!active || !videoRef.current) return;
const tracks = (videoRef.current.srcObject as MediaStream | null)?.getVideoTracks();
if (!tracks || tracks.length === 0) return;
const frame = await createImageBitmap(videoRef.current);
const codes = await detector.detect(frame);
if (codes.length > 0) {
setResult({ code: codes[0].rawValue });
} else {
requestAnimationFrame(scan);
}
};
void scan();
} else {
const controls = await reader.decodeFromVideoDevice(
undefined,
videoRef.current ?? undefined,
(decoded) => {
if (decoded) {
setResult({ code: decoded.getText() });
}
},
);
stop = () => controls.stop();
}
} catch (error) {
setResult({ error: (error as Error).message });
}
};
void start();
return () => {
active = false;
stop?.();
if (videoRef.current?.srcObject) {
(videoRef.current.srcObject as MediaStream).getTracks().forEach((track) => track.stop());
}
};
}, []);
return { videoRef, result };
};
+44
View File
@@ -0,0 +1,44 @@
import { useCallback, useMemo, useRef } from 'react';
interface LongPressOptions {
delay?: number;
onLongPress: () => void;
onClick?: () => void;
}
export const useLongPress = ({ delay = 500, onLongPress, onClick }: LongPressOptions) => {
const timerRef = useRef<number>();
const handledRef = useRef(false);
const start = useCallback(() => {
handledRef.current = false;
timerRef.current = window.setTimeout(() => {
handledRef.current = true;
onLongPress();
}, delay);
}, [delay, onLongPress]);
const clear = useCallback(() => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
}, []);
const onRelease = useCallback(() => {
clear();
if (!handledRef.current && onClick) {
onClick();
}
}, [clear, onClick]);
return useMemo(
() => ({
onMouseDown: start,
onTouchStart: start,
onMouseUp: onRelease,
onMouseLeave: clear,
onTouchEnd: onRelease,
}),
[start, onRelease, clear],
);
};
+16
View File
@@ -0,0 +1,16 @@
import { useEffect, useState } from 'react';
export const useServiceWorker = () => {
const [registered, setRegistered] = useState(false);
useEffect(() => {
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('/service-worker.js')
.then(() => setRegistered(true))
.catch(() => setRegistered(false));
}
}, []);
return registered;
};
+34
View File
@@ -0,0 +1,34 @@
import { useMemo, useRef } from 'react';
interface SwipeConfig {
onSwipeLeft?: () => void;
onSwipeRight?: () => void;
threshold?: number;
}
export const useSwipe = ({ onSwipeLeft, onSwipeRight, threshold = 50 }: SwipeConfig) => {
const startX = useRef<number | null>(null);
const onTouchStart = (e: React.TouchEvent) => {
startX.current = e.changedTouches[0].screenX;
};
const onTouchEnd = (e: React.TouchEvent) => {
if (startX.current === null) return;
const deltaX = e.changedTouches[0].screenX - startX.current;
if (deltaX < -threshold) {
onSwipeLeft?.();
} else if (deltaX > threshold) {
onSwipeRight?.();
}
startX.current = null;
};
return useMemo(
() => ({
onTouchStart,
onTouchEnd,
}),
[],
);
};