- TanStack Start (React 19, Tailwind 4) frontend recreated from Stitch design - PocketBase backend with schema migration + idempotent content seed - Single Docker image running both services (non-root, healthchecked) - deploy.sh to build & push to registry.tanshu.com - Ansible playbook (roles: app, caddy) deploying to www.greatbear.in with Caddy TLS entries for the site and the PocketBase admin
108 lines
3.8 KiB
TypeScript
108 lines
3.8 KiB
TypeScript
import * as React from 'react'
|
|
import type { MenuCategory, MenuItem } from '~/lib/types'
|
|
|
|
export function MenuBrowser({
|
|
categories,
|
|
items,
|
|
}: {
|
|
categories: MenuCategory[]
|
|
items: MenuItem[]
|
|
}) {
|
|
const [active, setActive] = React.useState<string>('all')
|
|
|
|
const visible = items.filter((item) => active === 'all' || item.category === active)
|
|
|
|
return (
|
|
<>
|
|
{/* Sticky Menu Navigation Filter */}
|
|
<section className="sticky top-20 z-40 bg-surface/90 py-space-md shadow-md backdrop-blur-md">
|
|
<div className="no-scrollbar mx-auto flex max-w-7xl items-center justify-start gap-space-sm overflow-x-auto px-grid-margin md:justify-center">
|
|
<FilterTab
|
|
key="all"
|
|
label="All Offerings"
|
|
active={active === 'all'}
|
|
onClick={() => setActive('all')}
|
|
/>
|
|
{categories.map((category) => (
|
|
<FilterTab
|
|
key={category.slug}
|
|
label={category.name}
|
|
active={active === category.slug}
|
|
onClick={() => setActive(category.slug)}
|
|
/>
|
|
))}
|
|
</div>
|
|
</section>
|
|
|
|
{/* Main Menu Grid */}
|
|
<section className="mx-auto w-full max-w-7xl px-grid-margin py-space-xl">
|
|
<div className="grid grid-cols-1 gap-grid-gutter md:grid-cols-2">
|
|
{visible.map((item) => (
|
|
<div
|
|
key={item.uid}
|
|
className="flex flex-col overflow-hidden rounded-xl bg-surface-container shadow-md transition-all duration-300 hover:shadow-xl"
|
|
>
|
|
<div
|
|
className="h-64 w-full bg-cover bg-center"
|
|
style={{ backgroundImage: `url('${item.image}')` }}
|
|
/>
|
|
<div className="flex flex-grow flex-col justify-between">
|
|
<div>
|
|
<div className="mb-space-xs flex items-center justify-between">
|
|
<h3 className="font-headline-md text-on-surface">{item.name}</h3>
|
|
<span className="font-headline-md text-primary">₹{item.price}</span>
|
|
</div>
|
|
<p className="mb-space-md text-body-md text-on-surface-variant">
|
|
{item.description}
|
|
</p>
|
|
</div>
|
|
<div className="-mx-space-lg -mb-space-lg flex items-center justify-between bg-surface-container-low p-space-md pt-space-md">
|
|
<div className="flex items-center gap-space-sm">
|
|
<span
|
|
className="material-symbols-outlined text-[20px] text-primary"
|
|
style={{ fontVariationSettings: "'FILL' 1" }}
|
|
>
|
|
sports_bar
|
|
</span>
|
|
<span className="text-label-md text-on-surface">
|
|
Perfect Pairing:{' '}
|
|
<strong className="text-primary">{item.pairing}</strong>
|
|
</span>
|
|
</div>
|
|
<span className="rounded bg-surface-container-highest px-space-sm py-space-xs text-label-md uppercase text-on-surface-variant">
|
|
{item.tag}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
</>
|
|
)
|
|
}
|
|
|
|
function FilterTab({
|
|
label,
|
|
active,
|
|
onClick,
|
|
}: {
|
|
label: string
|
|
active: boolean
|
|
onClick: () => void
|
|
}) {
|
|
return (
|
|
<button
|
|
type="button"
|
|
onClick={onClick}
|
|
className={
|
|
active
|
|
? 'whitespace-nowrap rounded-lg bg-primary-container px-space-md py-space-sm text-label-lg font-bold text-on-primary-container transition-all'
|
|
: 'whitespace-nowrap rounded-lg bg-surface-container px-space-md py-space-sm text-label-lg text-on-surface-variant transition-all hover:bg-surface-container-high'
|
|
}
|
|
>
|
|
{label}
|
|
</button>
|
|
)
|
|
}
|