Restructure project to root layout
This commit is contained in:
@@ -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;
|
||||
};
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -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],
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user