fix+ux(shopping): suppression via bottom sheet, majuscule auto, icône FAB v0.4.6

- Bug: décrémentation à 0 d'un article pré-chargé (existingItemId) conserve
  la sélection à qty=0 (marqué pour DELETE) → bouton ✓ devient accessible
  Visuel: fond rouge + barré + opacité 0.6 pour les articles à supprimer
- UX: première lettre en majuscule auto lors de l'ajout d'un article libre
- UX: FAB remplace '+' par fa-cart-plus pour mieux signifier l'ajout à la liste

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 09:39:37 +02:00
parent a1ecd0945e
commit 880f7f2125
2 changed files with 30 additions and 13 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "homehub-frontend",
"private": true,
"version": "0.4.5",
"version": "0.4.6",
"type": "module",
"scripts": {
"dev": "vite",
+29 -12
View File
@@ -173,14 +173,20 @@ export default function ShoppingPage() {
setSelections(prev => {
const idx = prev.findIndex(s => s.type === 'product' && s.product.id === p.id)
if (idx === -1) return prev
if (prev[idx].qty <= 1) return prev.filter((_, i) => i !== idx)
const sel = prev[idx]
if (sel.qty <= 1) {
// article pré-chargé : qty=0 = marqué pour suppression
if (sel.existingItemId) return prev.map((s, i) => i === idx ? { ...s, qty: 0 } : s)
return prev.filter((_, i) => i !== idx)
}
return prev.map((s, i) => i === idx ? { ...s, qty: s.qty - 1 } : s)
})
}
function addCustomItem() {
const name = itemSearch.trim()
if (!name) return
const raw = itemSearch.trim()
if (!raw) return
const name = raw.charAt(0).toUpperCase() + raw.slice(1)
setSelections(prev => {
const exists = prev.some(s => s.type === 'custom' && s.name === name)
if (exists) return prev
@@ -203,7 +209,11 @@ export default function ShoppingPage() {
setSelections(prev => {
const idx = prev.findIndex(s => s.type === 'custom' && s.name === name)
if (idx === -1) return prev
if (prev[idx].qty <= 1) return prev.filter((_, i) => i !== idx)
const sel = prev[idx]
if (sel.qty <= 1) {
if (sel.existingItemId) return prev.map((s, i) => i === idx ? { ...s, qty: 0 } : s)
return prev.filter((_, i) => i !== idx)
}
return prev.map((s, i) => i === idx ? { ...s, qty: s.qty - 1 } : s)
})
}
@@ -217,7 +227,8 @@ export default function ShoppingPage() {
for (const sel of toProcess) {
if (sel.type === 'product') {
if (sel.existingItemId) {
await updateItem(currentList.id, sel.existingItemId, { quantity: String(sel.qty) })
if (sel.qty === 0) await deleteItem(currentList.id, sel.existingItemId)
else await updateItem(currentList.id, sel.existingItemId, { quantity: String(sel.qty) })
} else {
await addItem(currentList.id, {
product_id: sel.product.id,
@@ -227,7 +238,8 @@ export default function ShoppingPage() {
}
} else {
if (sel.existingItemId) {
await updateItem(currentList.id, sel.existingItemId, { quantity: String(sel.qty) })
if (sel.qty === 0) await deleteItem(currentList.id, sel.existingItemId)
else await updateItem(currentList.id, sel.existingItemId, { quantity: String(sel.qty) })
} else if (sel.addToCatalogue) {
const newProduct = await createProduct({ name: sel.name })
await addItem(currentList.id, { product_id: newProduct.id, quantity: String(sel.qty) })
@@ -514,7 +526,7 @@ export default function ShoppingPage() {
)}
</div>
{/* FAB + — masqué quand le sheet est ouvert */}
{/* FAB — masqué quand le sheet est ouvert */}
{!showAddSheet && (
<button
onClick={openAddSheet}
@@ -523,10 +535,12 @@ export default function ShoppingPage() {
position: 'fixed', bottom: 72, right: 20,
width: 56, height: 56, borderRadius: '50%',
background: 'var(--accent)', color: '#1d2021', border: 'none',
fontSize: 28, cursor: 'pointer', boxShadow: '0 4px 12px rgba(0,0,0,0.5)',
fontSize: 22, cursor: 'pointer', boxShadow: '0 4px 12px rgba(0,0,0,0.5)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}
>+</button>
>
<i className="fa-solid fa-cart-plus" />
</button>
)}
</>
)}
@@ -620,18 +634,21 @@ export default function ShoppingPage() {
<div style={{ flex: 1, overflowY: 'auto' }}>
{/* Articles libres sélectionnés */}
{(selections.filter(s => s.type === 'custom') as Extract<typeof selections[number], { type: 'custom' }>[]).map(s => (
{(selections.filter(s => s.type === 'custom') as Extract<typeof selections[number], { type: 'custom' }>[]).map(s => {
const willDelete = s.qty === 0 && !!s.existingItemId
return (
<div
key={`custom:${s.name}`}
style={{
display: 'flex', alignItems: 'center', gap: 12,
padding: '10px 16px', minHeight: 52,
background: 'rgba(142,192,124,0.12)',
background: willDelete ? 'rgba(251,73,52,0.08)' : 'rgba(142,192,124,0.12)',
borderBottom: '1px solid var(--bg-4)',
opacity: willDelete ? 0.6 : 1,
}}
>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontFamily: 'var(--font-ui)', fontSize: 15, color: 'var(--ink-1)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{s.name}</div>
<div style={{ fontFamily: 'var(--font-ui)', fontSize: 15, color: 'var(--ink-1)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textDecoration: willDelete ? 'line-through' : 'none' }}>{s.name}</div>
{!s.existingItemId ? (
<label style={{ display: 'flex', alignItems: 'center', gap: 5, marginTop: 3, cursor: 'pointer' }} onClick={e => e.stopPropagation()}>
<input