From 82348bc8347c6bedac3c16937815131e6ac8701e Mon Sep 17 00:00:00 2001 From: jtricerolph Date: Tue, 14 Jul 2026 08:57:54 +0000 Subject: [PATCH] Mobile-responsive layout and camera upload improvements - Hamburger menu with slide-in nav for viewports < 700px; auto-closes on route change - PhotoUploader: separate camera trigger on mobile (capture="environment"), file input hidden on mobile; useCallback to fix stale-closure reset - Add .gitignore to exclude compiled JS artefacts and package-lock Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 8 ++ frontend/src/components/Layout.tsx | 130 ++++++++++++++++++++--------- frontend/src/pages/DailyCashUp.tsx | 48 +++++++++-- 3 files changed, 137 insertions(+), 49 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b451162 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +dist/ +frontend/dist/ +*.js.map +# compiled JS artefacts next to TS sources +frontend/src/**/*.js +frontend/package-lock.json +.env diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index f830a89..461d0a0 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -1,9 +1,21 @@ -import { NavLink, useNavigate } from 'react-router-dom' +import { useState, useEffect } from 'react' +import { NavLink, useNavigate, useLocation } from 'react-router-dom' import { - Banknote, ClipboardList, BarChart2, Wallet, Vault, FileText, Settings, LogOut, + Banknote, ClipboardList, BarChart2, Wallet, Vault, FileText, Settings, LogOut, Menu, } from 'lucide-react' import { can, type User, type CashupCap } from '../types' +function useIsMobile() { + const [isMobile, setIsMobile] = useState(() => window.innerWidth < 700) + useEffect(() => { + const mq = window.matchMedia('(max-width: 699px)') + const handler = (e: MediaQueryListEvent) => setIsMobile(e.matches) + mq.addEventListener('change', handler) + return () => mq.removeEventListener('change', handler) + }, []) + return isMobile +} + interface Props { user: User children: React.ReactNode @@ -22,6 +34,11 @@ const navItems: { to: string; label: string; icon: typeof Banknote; cap?: Cashup export function Layout({ user, children }: Props) { const navigate = useNavigate() + const location = useLocation() + const isMobile = useIsMobile() + const [menuOpen, setMenuOpen] = useState(false) + + useEffect(() => { setMenuOpen(false) }, [location.pathname]) async function logout() { await fetch('/cashup/api/auth/logout', { method: 'POST', credentials: 'include' }) @@ -29,49 +46,80 @@ export function Layout({ user, children }: Props) { window.location.reload() } + const sidebar = ( + + ) + return ( -
- {/* Sidebar */} - + )} + + {isMobile && menuOpen && ( +
setMenuOpen(false)} style={{ + position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', zIndex: 199, + }} /> + )} + + {sidebar} {/* Content */}
diff --git a/frontend/src/pages/DailyCashUp.tsx b/frontend/src/pages/DailyCashUp.tsx index 8a1845d..4ebc403 100644 --- a/frontend/src/pages/DailyCashUp.tsx +++ b/frontend/src/pages/DailyCashUp.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef } from 'react' +import { useState, useEffect, useRef, useCallback } from 'react' import { RefreshCw, Save, CheckCircle, Loader, Camera, FileText, X } from 'lucide-react' import { api, uploadAttachment } from '../api' import { PageHeader, Card, Btn, StatusBadge } from '../components/Layout' @@ -677,6 +677,17 @@ function DenomGrid({ denoms, onChange, disabled, tabBase = 0 }: { ) } +function useIsMobile() { + const [isMobile, setIsMobile] = useState(() => window.innerWidth < 700) + useEffect(() => { + const mq = window.matchMedia('(max-width: 699px)') + const handler = (e: MediaQueryListEvent) => setIsMobile(e.matches) + mq.addEventListener('change', handler) + return () => mq.removeEventListener('change', handler) + }, []) + return isMobile +} + async function compressImage(file: File, maxWidth = 1600, quality = 0.85): Promise { if (!file.type.startsWith('image/')) return file return new Promise(resolve => { @@ -714,13 +725,15 @@ function PhotoUploader({ }) { const [uploading, setUploading] = useState(false) const inputRef = useRef(null) + const cameraRef = useRef(null) + const isMobile = useIsMobile() const mine = attachments.filter(a => a.attachment_type === attachmentType && (label === null ? !a.label : a.label === label) ) - async function handleFiles(files: FileList) { + const handleFiles = useCallback(async (files: FileList, inputEl?: HTMLInputElement | null) => { setUploading(true) for (const file of Array.from(files)) { try { @@ -732,14 +745,20 @@ function PhotoUploader({ } } setUploading(false) - if (inputRef.current) inputRef.current.value = '' - } + if (inputEl) inputEl.value = '' + }, [cashUpId, attachmentType, label, onAdded]) async function remove(id: number) { await api.delete(`/attachments/${id}`) onRemoved(id) } + const thumbBtn: React.CSSProperties = { + width: '88px', height: '88px', border: '2px dashed var(--card-border)', borderRadius: '6px', + background: 'var(--body-bg)', cursor: 'pointer', display: 'flex', flexDirection: 'column', + alignItems: 'center', justifyContent: 'center', gap: '4px', color: 'var(--text-mid)', fontSize: '0.7rem', + } + return (
{mine.map(a => ( @@ -766,14 +785,27 @@ function PhotoUploader({ )}
))} - {!disabled && ( - )} + {!disabled && isMobile && ( + <> + + + + )} + {/* Desktop / library picker — supports multiple files */} e.target.files?.length && handleFiles(e.target.files)} /> + onChange={e => e.target.files?.length && handleFiles(e.target.files, inputRef.current)} /> + {/* Mobile camera — single capture, goes straight to rear camera */} + e.target.files?.length && handleFiles(e.target.files, cameraRef.current)} />
) }