feat(restaurant): customer-facing restaurant bundle (v1) #54
7 changed files with 493 additions and 13 deletions
feat(restaurant): cart store + cart page + add-to-cart wiring
stores/cart.ts — Pinia store keyed by restaurant_id (multi- restaurant ready for the festival aggregator, aiolabs/restaurant#8, without schema changes). Persists to STORAGE_SERVICE under 'restaurant.cart.v1' (debounced 200ms); hydrates on creation. Money is integer msat-ish (the cart stores unit_msat as the per-unit value pulled from MenuItem.price; the buildCreateOrder helper in commit 6 owns the canonical msat conversion at order-place time). State: { lines: Record<restaurant_id, CartLine[]>, activeRestaurantId: string | null } Lines that match item + modifier set + note merge into the same line with quantity++ rather than duplicating. Getters: restaurantsInCart, itemCount, restaurantTotalsMsat, grandTotalMsat, linesFor(rid). Actions: addLine, setQty, incrementQty, decrementQty, removeLine, clearRestaurant, clear, setActiveRestaurant. components/CartLineItem.vue — single line with modifier summary, qty stepper, note display, remove button. views/CartPage.vue — lines grouped by restaurant. Multi-restaurant display already works (each restaurant is its own card). Empty state, subtotal, clear-cart, Checkout CTA (lands in commit 6). Wiring: - ItemPage 'Add to cart' now actually adds, then routes to /cart. - RestaurantPage's quick-add (the '+' on cards with NO modifier groups) adds directly without opening ItemPage; cards WITH modifier groups still open the detail page so the customer can satisfy required choices. - App.vue bottom-nav 'Cart' badge reflects cart.itemCount. - New route /cart registered on the module. Verified: vue-tsc -b clean.
commit
27d98ce851
97
src/modules/restaurant/components/CartLineItem.vue
Normal file
97
src/modules/restaurant/components/CartLineItem.vue
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
<script setup lang="ts">
|
||||
/**
|
||||
* Single line in the cart: name, modifier summary, qty stepper,
|
||||
* remove button.
|
||||
*/
|
||||
import { computed } from 'vue'
|
||||
import { Plus, Minus, Trash2 } from 'lucide-vue-next'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import type { CartLine } from '../stores/cart'
|
||||
|
||||
const props = defineProps<{
|
||||
line: CartLine
|
||||
currencyHint?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'increment'): void
|
||||
(e: 'decrement'): void
|
||||
(e: 'remove'): void
|
||||
}>()
|
||||
|
||||
const modifierSummary = computed(() =>
|
||||
props.line.selected_modifiers.map((m) => m.name).join(' · ')
|
||||
)
|
||||
|
||||
const lineTotal = computed(() => {
|
||||
// unit_msat is base + modifier delta snapshot; the cart store
|
||||
// currently stores it as a sat-major number (price * 1000-less
|
||||
// because the extension's `price` is already in the declared
|
||||
// currency, not msat — see useCheckout's buildCreateOrder for the
|
||||
// canonical conversion at order-place time).
|
||||
const fmt = new Intl.NumberFormat(undefined, { maximumFractionDigits: 2 })
|
||||
const total = props.line.unit_msat * props.line.quantity
|
||||
return `${fmt.format(total)} ${props.currencyHint || 'sat'}`
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li class="flex items-stretch gap-3 rounded-xl border border-border bg-card p-3">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<h4 class="line-clamp-1 font-semibold text-foreground">
|
||||
{{ line.name }}
|
||||
</h4>
|
||||
<span class="shrink-0 font-mono text-sm text-primary">
|
||||
{{ lineTotal }}
|
||||
</span>
|
||||
</div>
|
||||
<p
|
||||
v-if="modifierSummary"
|
||||
class="mt-1 line-clamp-2 text-xs text-muted-foreground"
|
||||
>
|
||||
{{ modifierSummary }}
|
||||
</p>
|
||||
<p
|
||||
v-if="line.note"
|
||||
class="mt-1 line-clamp-2 text-xs italic text-muted-foreground"
|
||||
>
|
||||
Note: {{ line.note }}
|
||||
</p>
|
||||
<div class="mt-2 flex items-center gap-2">
|
||||
<div class="inline-flex items-center rounded-full border border-border bg-background">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7 rounded-full"
|
||||
aria-label="Decrease quantity"
|
||||
@click="emit('decrement')"
|
||||
>
|
||||
<Minus class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<span class="w-7 text-center font-mono text-sm" aria-live="polite">
|
||||
{{ line.quantity }}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7 rounded-full"
|
||||
aria-label="Increase quantity"
|
||||
@click="emit('increment')"
|
||||
>
|
||||
<Plus class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7 text-destructive hover:text-destructive"
|
||||
aria-label="Remove line"
|
||||
@click="emit('remove')"
|
||||
>
|
||||
<Trash2 class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
|
@ -88,6 +88,12 @@ export const restaurantModule: ModulePlugin = {
|
|||
component: () => import('./views/ItemPage.vue'),
|
||||
meta: { requiresAuth: false, title: 'Item' },
|
||||
},
|
||||
{
|
||||
path: '/cart',
|
||||
name: 'restaurant-cart',
|
||||
component: () => import('./views/CartPage.vue'),
|
||||
meta: { requiresAuth: false, title: 'Cart' },
|
||||
},
|
||||
] as RouteRecordRaw[],
|
||||
}
|
||||
|
||||
|
|
|
|||
230
src/modules/restaurant/stores/cart.ts
Normal file
230
src/modules/restaurant/stores/cart.ts
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
/**
|
||||
* Restaurant cart store (Pinia).
|
||||
*
|
||||
* Multi-restaurant ready by design — `lines` is keyed by
|
||||
* `restaurant_id`. v1 only exercises the single-restaurant path
|
||||
* (URL-driven /r/:slug), but the festival aggregator
|
||||
* (aiolabs/restaurant#8) lands on top of this same store with
|
||||
* **zero schema changes** — it just adds lines from more restaurants.
|
||||
*
|
||||
* Persistence: state is mirrored to STORAGE_SERVICE under
|
||||
* `restaurant.cart.v1` (debounced). Re-hydrated on store creation.
|
||||
* Money is integer **msat** end-to-end (matches the extension).
|
||||
*/
|
||||
|
||||
import { computed, watch } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { tryInjectService, SERVICE_TOKENS } from '@/core/di-container'
|
||||
import type { StorageService } from '@/core/services/StorageService'
|
||||
import type { SelectedModifier } from '../types/restaurant'
|
||||
|
||||
const STORAGE_KEY = 'restaurant.cart.v1'
|
||||
|
||||
export interface CartLine {
|
||||
/** UUID for dedup of identical items + modifier sets. */
|
||||
line_id: string
|
||||
restaurant_id: string
|
||||
restaurant_slug: string
|
||||
menu_item_id: string
|
||||
/** Snapshot at add-time so the cart still renders if the menu
|
||||
* item is later edited / deleted. */
|
||||
name: string
|
||||
/** Base price + selected modifier deltas, in msat. */
|
||||
unit_msat: number
|
||||
quantity: number
|
||||
selected_modifiers: SelectedModifier[]
|
||||
note?: string | null
|
||||
}
|
||||
|
||||
export interface CartState {
|
||||
lines: Record<string, CartLine[]>
|
||||
activeRestaurantId: string | null
|
||||
}
|
||||
|
||||
const blank: CartState = { lines: {}, activeRestaurantId: null }
|
||||
|
||||
function uuid(): string {
|
||||
// crypto.randomUUID is available in modern browsers + Node 19+.
|
||||
if (typeof crypto !== 'undefined' && 'randomUUID' in crypto) {
|
||||
return crypto.randomUUID()
|
||||
}
|
||||
return `line-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
|
||||
}
|
||||
|
||||
/** Two lines are merge-equivalent iff same item + same modifier set
|
||||
* + same note. Modifier id sort gives an order-independent compare. */
|
||||
function modifierKey(mods: SelectedModifier[]): string {
|
||||
return mods
|
||||
.map((m) => m.modifier_id ?? m.name)
|
||||
.filter(Boolean)
|
||||
.sort()
|
||||
.join('|')
|
||||
}
|
||||
|
||||
export const useCartStore = defineStore('restaurant-cart', () => {
|
||||
const storage = tryInjectService<StorageService>(
|
||||
SERVICE_TOKENS.STORAGE_SERVICE
|
||||
)
|
||||
|
||||
// Hydrate from persistence (if available).
|
||||
const initial = storage?.getUserData<CartState>(STORAGE_KEY, blank) ?? blank
|
||||
|
||||
const lines = ref<Record<string, CartLine[]>>(
|
||||
structuredClone(initial.lines || {})
|
||||
)
|
||||
const activeRestaurantId = ref<string | null>(
|
||||
initial.activeRestaurantId ?? null
|
||||
)
|
||||
|
||||
// -------------------------- getters --------------------------------
|
||||
|
||||
const restaurantsInCart = computed<string[]>(() =>
|
||||
Object.keys(lines.value).filter((rid) => lines.value[rid].length > 0)
|
||||
)
|
||||
|
||||
const itemCount = computed<number>(() => {
|
||||
let n = 0
|
||||
for (const rid of Object.keys(lines.value)) {
|
||||
for (const l of lines.value[rid]) n += l.quantity
|
||||
}
|
||||
return n
|
||||
})
|
||||
|
||||
const restaurantTotalsMsat = computed<Record<string, number>>(() => {
|
||||
const out: Record<string, number> = {}
|
||||
for (const rid of Object.keys(lines.value)) {
|
||||
out[rid] = lines.value[rid].reduce(
|
||||
(s, l) => s + l.unit_msat * l.quantity,
|
||||
0
|
||||
)
|
||||
}
|
||||
return out
|
||||
})
|
||||
|
||||
const grandTotalMsat = computed<number>(() =>
|
||||
Object.values(restaurantTotalsMsat.value).reduce((s, v) => s + v, 0)
|
||||
)
|
||||
|
||||
function linesFor(restaurantId: string): CartLine[] {
|
||||
return lines.value[restaurantId] || []
|
||||
}
|
||||
|
||||
// -------------------------- actions --------------------------------
|
||||
|
||||
function setActiveRestaurant(restaurantId: string | null): void {
|
||||
activeRestaurantId.value = restaurantId
|
||||
}
|
||||
|
||||
function addLine(line: Omit<CartLine, 'line_id'>): CartLine {
|
||||
const bucket = (lines.value[line.restaurant_id] ||= [])
|
||||
const myKey = modifierKey(line.selected_modifiers)
|
||||
const existing = bucket.find(
|
||||
(l) =>
|
||||
l.menu_item_id === line.menu_item_id &&
|
||||
modifierKey(l.selected_modifiers) === myKey &&
|
||||
(l.note ?? '') === (line.note ?? '')
|
||||
)
|
||||
if (existing) {
|
||||
existing.quantity += line.quantity
|
||||
return existing
|
||||
}
|
||||
const newLine: CartLine = { line_id: uuid(), ...line }
|
||||
bucket.push(newLine)
|
||||
if (!activeRestaurantId.value) {
|
||||
activeRestaurantId.value = line.restaurant_id
|
||||
}
|
||||
return newLine
|
||||
}
|
||||
|
||||
function setQty(restaurantId: string, lineId: string, qty: number): void {
|
||||
const bucket = lines.value[restaurantId]
|
||||
if (!bucket) return
|
||||
const line = bucket.find((l) => l.line_id === lineId)
|
||||
if (!line) return
|
||||
if (qty <= 0) {
|
||||
removeLine(restaurantId, lineId)
|
||||
return
|
||||
}
|
||||
line.quantity = qty
|
||||
}
|
||||
|
||||
function incrementQty(restaurantId: string, lineId: string): void {
|
||||
const bucket = lines.value[restaurantId]
|
||||
if (!bucket) return
|
||||
const line = bucket.find((l) => l.line_id === lineId)
|
||||
if (line) line.quantity++
|
||||
}
|
||||
|
||||
function decrementQty(restaurantId: string, lineId: string): void {
|
||||
const bucket = lines.value[restaurantId]
|
||||
if (!bucket) return
|
||||
const line = bucket.find((l) => l.line_id === lineId)
|
||||
if (!line) return
|
||||
if (line.quantity <= 1) {
|
||||
removeLine(restaurantId, lineId)
|
||||
} else {
|
||||
line.quantity--
|
||||
}
|
||||
}
|
||||
|
||||
function removeLine(restaurantId: string, lineId: string): void {
|
||||
const bucket = lines.value[restaurantId]
|
||||
if (!bucket) return
|
||||
lines.value[restaurantId] = bucket.filter((l) => l.line_id !== lineId)
|
||||
if (lines.value[restaurantId].length === 0) {
|
||||
delete lines.value[restaurantId]
|
||||
if (activeRestaurantId.value === restaurantId) {
|
||||
activeRestaurantId.value = restaurantsInCart.value[0] ?? null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clearRestaurant(restaurantId: string): void {
|
||||
delete lines.value[restaurantId]
|
||||
if (activeRestaurantId.value === restaurantId) {
|
||||
activeRestaurantId.value = restaurantsInCart.value[0] ?? null
|
||||
}
|
||||
}
|
||||
|
||||
function clear(): void {
|
||||
lines.value = {}
|
||||
activeRestaurantId.value = null
|
||||
}
|
||||
|
||||
// -------------------------- persistence ----------------------------
|
||||
|
||||
let writeTimer: ReturnType<typeof setTimeout> | null = null
|
||||
function persist(): void {
|
||||
if (!storage) return
|
||||
if (writeTimer) clearTimeout(writeTimer)
|
||||
writeTimer = setTimeout(() => {
|
||||
storage.setUserData<CartState>(STORAGE_KEY, {
|
||||
lines: lines.value,
|
||||
activeRestaurantId: activeRestaurantId.value,
|
||||
})
|
||||
}, 200)
|
||||
}
|
||||
watch([lines, activeRestaurantId], persist, { deep: true })
|
||||
|
||||
return {
|
||||
// state
|
||||
lines,
|
||||
activeRestaurantId,
|
||||
// getters
|
||||
restaurantsInCart,
|
||||
itemCount,
|
||||
restaurantTotalsMsat,
|
||||
grandTotalMsat,
|
||||
linesFor,
|
||||
// actions
|
||||
setActiveRestaurant,
|
||||
addLine,
|
||||
setQty,
|
||||
incrementQty,
|
||||
decrementQty,
|
||||
removeLine,
|
||||
clearRestaurant,
|
||||
clear,
|
||||
}
|
||||
})
|
||||
118
src/modules/restaurant/views/CartPage.vue
Normal file
118
src/modules/restaurant/views/CartPage.vue
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
<script setup lang="ts">
|
||||
/**
|
||||
* Cart review. v1 shows lines grouped by restaurant (multi-
|
||||
* restaurant ready — single bucket today). "Checkout" jumps to
|
||||
* /checkout where useCheckout runs the place-order + bolt11 pay
|
||||
* sequence (commit 6).
|
||||
*/
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ArrowLeft, ShoppingCart } from 'lucide-vue-next'
|
||||
import { Alert, AlertTitle, AlertDescription } from '@/components/ui/alert'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import CartLineItem from '../components/CartLineItem.vue'
|
||||
import { useCartStore } from '../stores/cart'
|
||||
|
||||
const router = useRouter()
|
||||
const cart = useCartStore()
|
||||
|
||||
const buckets = computed(() =>
|
||||
cart.restaurantsInCart.map((rid) => ({
|
||||
restaurantId: rid,
|
||||
// restaurant_slug comes from the line snapshot so we don't need
|
||||
// a separate fetch for the cart page header.
|
||||
restaurantSlug: cart.linesFor(rid)[0]?.restaurant_slug ?? '',
|
||||
lines: cart.linesFor(rid),
|
||||
totalMsat: cart.restaurantTotalsMsat[rid] ?? 0,
|
||||
}))
|
||||
)
|
||||
|
||||
function fmtSat(value: number): string {
|
||||
return `${new Intl.NumberFormat(undefined, { maximumFractionDigits: 2 }).format(value)} sat`
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="container mx-auto max-w-2xl px-4 pb-24 pt-3 sm:py-6">
|
||||
<Button variant="ghost" size="sm" class="mb-3" @click="router.back()">
|
||||
<ArrowLeft class="mr-2 h-4 w-4" />
|
||||
Back
|
||||
</Button>
|
||||
|
||||
<header class="mb-4 flex items-baseline justify-between">
|
||||
<h1 class="text-2xl font-bold text-foreground">Cart</h1>
|
||||
<span class="text-sm text-muted-foreground">
|
||||
{{ cart.itemCount }} item{{ cart.itemCount === 1 ? '' : 's' }}
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<Alert v-if="!buckets.length" class="border-border">
|
||||
<ShoppingCart class="h-4 w-4" />
|
||||
<AlertTitle>Your cart is empty</AlertTitle>
|
||||
<AlertDescription>
|
||||
Browse a menu and tap “+” to add items.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<template v-else>
|
||||
<div class="space-y-4">
|
||||
<Card v-for="b in buckets" :key="b.restaurantId">
|
||||
<CardHeader class="flex flex-row items-baseline justify-between space-y-0">
|
||||
<CardTitle class="text-base">
|
||||
<button
|
||||
type="button"
|
||||
class="text-left text-foreground hover:text-primary"
|
||||
@click="router.push(`/r/${b.restaurantSlug}`)"
|
||||
>
|
||||
{{ b.restaurantSlug }}
|
||||
</button>
|
||||
</CardTitle>
|
||||
<span class="font-mono text-sm text-primary">
|
||||
{{ fmtSat(b.totalMsat) }}
|
||||
</span>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul class="space-y-2">
|
||||
<CartLineItem
|
||||
v-for="line in b.lines"
|
||||
:key="line.line_id"
|
||||
:line="line"
|
||||
@increment="cart.incrementQty(b.restaurantId, line.line_id)"
|
||||
@decrement="cart.decrementQty(b.restaurantId, line.line_id)"
|
||||
@remove="cart.removeLine(b.restaurantId, line.line_id)"
|
||||
/>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Separator class="my-6" />
|
||||
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-muted-foreground">Subtotal</span>
|
||||
<span class="font-mono text-lg font-semibold text-foreground">
|
||||
{{ fmtSat(cart.grandTotalMsat) }}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
class="w-full"
|
||||
size="lg"
|
||||
:disabled="!buckets.length"
|
||||
@click="router.push('/checkout')"
|
||||
>
|
||||
Checkout
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="w-full text-destructive hover:text-destructive"
|
||||
@click="cart.clear()"
|
||||
>
|
||||
Clear cart
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</main>
|
||||
</template>
|
||||
|
|
@ -15,6 +15,7 @@ import { Button } from '@/components/ui/button'
|
|||
import { Textarea } from '@/components/ui/textarea'
|
||||
import ModifierSelector from '../components/ModifierSelector.vue'
|
||||
import { useMenu } from '../composables/useMenu'
|
||||
import { useCartStore } from '../stores/cart'
|
||||
import type {
|
||||
EnrichedMenuItem,
|
||||
SelectedModifier,
|
||||
|
|
@ -22,6 +23,7 @@ import type {
|
|||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const cart = useCartStore()
|
||||
|
||||
const slug = computed(() => String(route.params.slug || ''))
|
||||
const itemId = computed(() => String(route.params.itemId || ''))
|
||||
|
|
@ -74,16 +76,23 @@ function decrement() {
|
|||
if (quantity.value > 1) quantity.value--
|
||||
}
|
||||
|
||||
// Cart store is added in commit 5 — placeholder for now.
|
||||
const canAdd = computed(
|
||||
() => !!item.value && !isSoldOut.value && modifiersValid.value
|
||||
)
|
||||
|
||||
function addToCart() {
|
||||
// Wired in commit 5.
|
||||
console.warn(
|
||||
'[restaurant] Cart store not yet wired — commit 5 adds this.'
|
||||
)
|
||||
if (!item.value || !restaurant.value) return
|
||||
cart.addLine({
|
||||
restaurant_id: restaurant.value.id,
|
||||
restaurant_slug: restaurant.value.slug,
|
||||
menu_item_id: item.value.id,
|
||||
name: item.value.name,
|
||||
unit_msat: unitPrice.value,
|
||||
quantity: quantity.value,
|
||||
selected_modifiers: selectedModifiers.value,
|
||||
note: note.value || null,
|
||||
})
|
||||
router.push('/cart')
|
||||
}
|
||||
|
||||
function back() {
|
||||
|
|
|
|||
|
|
@ -20,9 +20,11 @@ import RestaurantHeader from '../components/RestaurantHeader.vue'
|
|||
import CategoryNav from '../components/CategoryNav.vue'
|
||||
import MenuTree from '../components/MenuTree.vue'
|
||||
import { useMenu } from '../composables/useMenu'
|
||||
import { useCartStore } from '../stores/cart'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const cart = useCartStore()
|
||||
|
||||
const slug = computed(() => String(route.params.slug || ''))
|
||||
const { restaurant, tree, items, isLoading, error, refresh } = useMenu(
|
||||
|
|
@ -33,11 +35,28 @@ function openItem(itemId: string) {
|
|||
router.push(`/r/${slug.value}/item/${itemId}`)
|
||||
}
|
||||
|
||||
// "Quick-add" is forwarded by MenuItemCard when an item has no
|
||||
// modifier groups. Cart store lands in commit 5 — until then it
|
||||
// behaves the same as opening the item detail.
|
||||
/**
|
||||
* Quick-add bypasses ItemPage for items that have no modifier groups.
|
||||
* Items with modifier groups always route through ItemPage so the
|
||||
* customer can pick required options before the line is added.
|
||||
*/
|
||||
function quickAdd(itemId: string) {
|
||||
const it = items.value.find((i) => i.id === itemId)
|
||||
if (!it || !restaurant.value) return
|
||||
if (it.modifier_groups.length > 0) {
|
||||
openItem(itemId)
|
||||
return
|
||||
}
|
||||
cart.addLine({
|
||||
restaurant_id: restaurant.value.id,
|
||||
restaurant_slug: restaurant.value.slug,
|
||||
menu_item_id: it.id,
|
||||
name: it.name,
|
||||
unit_msat: it.price,
|
||||
quantity: 1,
|
||||
selected_modifiers: [],
|
||||
note: null,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -4,13 +4,14 @@ import { useRoute } from 'vue-router'
|
|||
import { Utensils, ShoppingCart, ReceiptText } from 'lucide-vue-next'
|
||||
import AppShell from '@/components/layout/AppShell.vue'
|
||||
import type { BottomTab } from '@/components/layout/BottomNav.vue'
|
||||
import { useCartStore } from '@/modules/restaurant/stores/cart'
|
||||
|
||||
const route = useRoute()
|
||||
const cart = useCartStore()
|
||||
|
||||
// The cart store hooks in during commit 5; v1 skeleton renders the
|
||||
// badge as null. Bottom-nav lives in AppShell and works fine with
|
||||
// zero badge values.
|
||||
const cartItemCount = computed<number | null>(() => null)
|
||||
const cartItemCount = computed<number | null>(() =>
|
||||
cart.itemCount > 0 ? cart.itemCount : null
|
||||
)
|
||||
|
||||
const tabs = computed<BottomTab[]>(() => [
|
||||
{ name: 'Browse', icon: Utensils, path: '/' },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue