fix: QR menu branding fields — display name, header image, empty-to-hide

- menu_display_name is now separate from the site's internal name (was
  incorrectly showing the sysadmin-only site name on the public menu).
- Header image now resolves against the cloud API origin instead of
  the menu-app's own origin, fixing the broken-image icon.
- Tagline/blurb/hours now genuinely hide when cleared: the sysadmin
  save handler was converting empty fields to null, which the PUT
  endpoint's exclude_none silently drops instead of clearing.
- Added a settable blurb (EN/GR) below the tagline, previously
  hardcoded ("Fresh seasonal plates, served with care.").
- Collapsed hours from two fields (EN/GR) to one — opening hours don't
  vary by language.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-19 10:44:28 +03:00
parent f5736b85cb
commit a73f081ca1
7 changed files with 101 additions and 59 deletions

View File

@@ -1,8 +1,8 @@
import axios from 'axios'
const BASE = import.meta.env.VITE_CLOUD_URL || ''
export const CLOUD_URL = import.meta.env.VITE_CLOUD_URL || ''
const api = axios.create({ baseURL: BASE })
const api = axios.create({ baseURL: CLOUD_URL })
export async function fetchMenu(siteSlug) {
const { data } = await api.get(`/api/menu/${siteSlug}`)

View File

@@ -1,25 +1,19 @@
import { useState, useEffect, useRef, useMemo } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import {
MapPin, Search, Leaf, X, Plus, ShoppingBag,
Search, Leaf, X, Plus, ShoppingBag,
ChevronLeft, ArrowRight, Send, Loader2, Info,
Armchair, Check, SearchX, UtensilsCrossed, AlertCircle,
Soup, Salad, Wheat, IceCream2, Wine,
} from 'lucide-react'
import { fetchMenu, submitOrder } from '../api'
import { fetchMenu, submitOrder, CLOUD_URL } from '../api'
import {
DishArt, Badge, DietChip, TagIcons, Price, DiscountFlag, Stepper,
eur, discountedPrice, discountPct,
} from '../components/primitives'
// Used when a site hasn't configured a field yet (API sends nulls until set in sysadmin)
const RESTAURANT_FALLBACK = {
name: 'Our Menu',
tagline: { en: 'Kitchen & Bar', gr: 'Κουζίνα & Μπαρ' },
blurb: { en: 'Fresh seasonal plates, served with care.', gr: 'Εποχιακά πιάτα, με αγάπη.' },
hours: { en: 'Open today · 12:00 23:30', gr: 'Ανοιχτά σήμερα · 12:00 23:30' },
location: { en: '', gr: '' },
}
// Shown only when a site has never been configured at all (brand-new site, restaurant === null)
const RESTAURANT_FALLBACK = { name: 'Our Menu' }
// Category glyph icons — mapped by category id or index
const GLYPH_BY_ID = { starters: Soup, salads: Salad, mains: UtensilsCrossed, sides: Wheat, desserts: IceCream2, drinks: Wine }
@@ -121,7 +115,12 @@ function SheetHandle() {
// ── Hero ──────────────────────────────────────────────────────────────────────
function Hero({ lang, setLang, restaurant }) {
const r = { ...RESTAURANT_FALLBACK, ...restaurant }
const name = restaurant?.name || RESTAURANT_FALLBACK.name
const tagline = restaurant?.tagline?.[lang]
const blurb = restaurant?.blurb?.[lang]
const hours = restaurant?.hours
const headerImageUrl = restaurant?.headerImageUrl ? `${CLOUD_URL}${restaurant.headerImageUrl}` : null
return (
<header className="relative px-6 pt-7 pb-6 text-center">
{/* Language toggle */}
@@ -141,27 +140,22 @@ function Hero({ lang, setLang, restaurant }) {
</div>
</div>
{r.location?.[lang] && (
<div className="mx-auto inline-flex items-center gap-1.5 rounded-full bg-white/60 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.22em] text-[#8a7f5e] ring-1 ring-[#e8e1d1]">
<MapPin className="h-3 w-3" strokeWidth={2} />
{r.location[lang]}
</div>
)}
{r.headerImageUrl ? (
{headerImageUrl ? (
<img
src={r.headerImageUrl}
alt={r.name}
src={headerImageUrl}
alt={name}
className="mx-auto mt-4 block max-w-[80%] max-h-[120px] w-auto object-contain"
/>
) : (
<h1 className="mt-4 font-display text-[42px] font-semibold leading-[0.95] tracking-tight text-[#2d3b2d]">
{r.name}
{name}
</h1>
)}
<div className="mt-1.5 font-sans text-[14px] font-medium uppercase tracking-[0.18em] text-[#9caf88]">
{r.tagline?.[lang] || RESTAURANT_FALLBACK.tagline[lang] || ''}
</div>
{tagline && (
<div className="mt-1.5 font-sans text-[14px] font-medium uppercase tracking-[0.18em] text-[#9caf88]">
{tagline}
</div>
)}
{/* Ornament */}
<div className="mx-auto my-4 flex w-40 items-center gap-2">
@@ -170,17 +164,19 @@ function Hero({ lang, setLang, restaurant }) {
<span className="h-px flex-1 bg-gradient-to-l from-transparent to-[#d8cfb6]" />
</div>
<p className="mx-auto max-w-[300px] text-[13px] leading-relaxed text-[#7d7660]">
{r.blurb?.[lang] || RESTAURANT_FALLBACK.blurb[lang] || ''}
</p>
{blurb && (
<p className="mx-auto max-w-[300px] text-[13px] leading-relaxed text-[#7d7660]">
{blurb}
</p>
)}
{r.hours?.[lang] && (
{hours && (
<div className="mt-2.5 inline-flex items-center gap-1.5 text-[12px] font-medium text-[#3f7d4e]">
<span className="relative flex h-1.5 w-1.5">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[#3f7d4e] opacity-60" />
<span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-[#3f7d4e]" />
</span>
{r.hours[lang]}
{hours}
</div>
)}
</header>