From ad220d96084325c8c56422bc2c3a74118e133b5b Mon Sep 17 00:00:00 2001 From: Maurice Date: Thu, 8 Oct 2026 21:08:31 +0000 Subject: [PATCH] refactor(packing): share the bag editor and the bag weight totals between desktop and phone useBagCardEditor holds the editing of a bag's name and weight limit for the desktop bag card and MBagsSheet, and bagLoadSummary computes the weight totals for the bag sidebar, the bag modal and the phone sheet. The desktop card follows the bag when it changes elsewhere, the phone row resets a name it did not save. --- .../Packing/PackingListPanelBagCard.tsx | 46 +----- .../Packing/PackingListPanelBagModal.tsx | 25 ++- .../Packing/PackingListPanelBagSidebar.tsx | 19 +-- .../Packing/packingListPanel.helpers.test.ts | 44 +++++- .../Packing/packingListPanel.helpers.ts | 24 +++ .../Packing/useBagCardEditor.test.ts | 147 ++++++++++++++++++ .../components/Packing/useBagCardEditor.ts | 96 ++++++++++++ .../mobile/screens/trip/tabs/MBagsSheet.tsx | 57 ++----- 8 files changed, 347 insertions(+), 111 deletions(-) create mode 100644 client/src/components/Packing/useBagCardEditor.test.ts create mode 100644 client/src/components/Packing/useBagCardEditor.ts diff --git a/client/src/components/Packing/PackingListPanelBagCard.tsx b/client/src/components/Packing/PackingListPanelBagCard.tsx index 1d8adab01..5a5a8f5d9 100644 --- a/client/src/components/Packing/PackingListPanelBagCard.tsx +++ b/client/src/components/Packing/PackingListPanelBagCard.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef, useCallback } from 'react' +import { useState, useRef, useCallback } from 'react' import { createPortal } from 'react-dom' import { X, Plus } from 'lucide-react' import type { PackingItem, PackingBag } from '../../types' @@ -6,6 +6,7 @@ import type { TripMember } from './usePackingListPanel' import { PopoverItem } from './PackingPopover' import { POPOVER, POPOVER_CAPTION, useDismissOnOutside } from './packingPopoverStyles' import { useAnchoredPosition } from '../../hooks/useAnchoredPosition' +import { useBagCardEditor } from './useBagCardEditor' interface BagCardProps { bag: PackingBag; bagItems: PackingItem[]; totalWeight: number; pct: number; tripId: number @@ -15,8 +16,6 @@ interface BagCardProps { } export function BagCard({ bag, bagItems, totalWeight, pct, tripId, tripMembers, canEdit, onDelete, onUpdate, onSetMembers, t, compact }: BagCardProps) { - const [editingName, setEditingName] = useState(false) - const [nameVal, setNameVal] = useState(bag.name) const [showUserPicker, setShowUserPicker] = useState(false) const membersRef = useRef(null) const pickerRef = useRef(null) @@ -24,39 +23,10 @@ export function BagCard({ bag, bagItems, totalWeight, pct, tripId, tripMembers, useDismissOnOutside(membersRef, showUserPicker, closePicker, pickerRef) // The bag sidebar scrolls and would clip the list, so it hangs off the body under the faces. const pickerBox = useAnchoredPosition(membersRef, showUserPicker, { estimatedHeight: 260, offset: 6 }) - useEffect(() => setNameVal(bag.name), [bag.name]) - - const saveName = () => { - if (nameVal.trim() && nameVal.trim() !== bag.name) onUpdate(bag.id, { name: nameVal.trim() }) - setEditingName(false) - } - - // Limits are entered in kg — that is how airlines state them — and stored in grams. - const limitToInput = (grams?: number | null) => (grams ? String(grams / 1000) : '') - const [editingLimit, setEditingLimit] = useState(false) - const [limitVal, setLimitVal] = useState(limitToInput(bag.weight_limit_grams)) - useEffect(() => setLimitVal(limitToInput(bag.weight_limit_grams)), [bag.weight_limit_grams]) - - const saveLimit = () => { - setEditingLimit(false) - const raw = limitVal.trim().replace(',', '.') - if (raw === '') { - // Clearing the field removes the limit and puts the bar back on relative scaling. - if (bag.weight_limit_grams != null) onUpdate(bag.id, { weight_limit_grams: null }) - return - } - const kg = Number(raw) - // Anything unparseable or negative leaves the stored limit alone rather than wiping it. - if (!Number.isFinite(kg) || kg <= 0) { setLimitVal(limitToInput(bag.weight_limit_grams)); return } - const grams = Math.round(kg * 1000) - if (grams !== bag.weight_limit_grams) onUpdate(bag.id, { weight_limit_grams: grams }) - } - - const memberIds = (bag.members || []).map(m => m.user_id) - const toggleMember = (userId: number) => { - const next = memberIds.includes(userId) ? memberIds.filter(id => id !== userId) : [...memberIds, userId] - onSetMembers(bag.id, next) - } + const { + editingName, setEditingName, nameVal, setNameVal, saveName, cancelName, + editingLimit, setEditingLimit, limitVal, setLimitVal, saveLimit, cancelLimit, memberIds, toggleMember, + } = useBagCardEditor({ bag, onUpdate: data => onUpdate(bag.id, data), onSetMembers: ids => onSetMembers(bag.id, ids), followBag: true }) const sz = compact ? { dot: 10, name: 12, weight: 11, bar: 6, count: 10, gap: 6, mb: 14, icon: 11, avatar: 18 } : { dot: 12, name: 14, weight: 13, bar: 8, count: 11, gap: 8, mb: 16, icon: 13, avatar: 22 } @@ -68,7 +38,7 @@ export function BagCard({ bag, bagItems, totalWeight, pct, tripId, tripMembers, {editingName && canEdit ? ( setNameVal(e.target.value)} - onBlur={saveName} onKeyDown={e => { if (e.key === 'Enter') saveName(); if (e.key === 'Escape') { setEditingName(false); setNameVal(bag.name) } }} + onBlur={saveName} onKeyDown={e => { if (e.key === 'Enter') saveName(); if (e.key === 'Escape') cancelName() }} style={{ flex: 1, fontSize: sz.name, fontWeight: 600, padding: '1px 4px', borderRadius: 4, border: '1px solid var(--border-primary)', outline: 'none', fontFamily: 'inherit', color: 'var(--text-primary)', background: 'transparent' }} /> ) : (