@php /** * A TV menu board, rendered full-screen from a restaurant's live menu. * Standalone document (no site chrome) — this is what a wall screen shows. * * The board is a row of COLUMNS (1–4). Each resolved column is one of: * menu → heading + item list (per-column images / numbers / descriptions) * image → a cover image * video → an uploaded clip or a YouTube embed * feature → hero product card(s) * * @var array $restaurant the restaurant row * @var array $columns resolved columns (see TvController::resolveColumns) * @var array $config board config (orientation/theme/accent/header/footer/spacing/…) * @var string $style the template/preview key (for the title) */ $c = $config; $vertical = ($c['orientation'] ?? 'horizontal') === 'vertical'; $logo = trim((string) ($restaurant['logo'] ?? '')); // A board shows the RESTAURANT's own logo — never the platform mark. A restaurant that has no // logo of its own (or was seeded pointing at the platform file) gets its own branded initials // monogram instead, exactly like restaurant_logo_html() does across the dashboard. if ($logo === '' || $logo === (string) setting('logo', 'assets/admin/logo.png') || $logo === (string) setting('logo_light', 'assets/admin/logo-light.png')) { $logo = ''; } $logoInitials = ''; foreach (preg_split('/\s+/', trim((string) ($restaurant['name'] ?? ''))) ?: [] as $w) { if ($w !== '') { $logoInitials .= mb_strtoupper(mb_substr($w, 0, 1)); } if (mb_strlen($logoInitials) >= 2) { break; } } if ($logoInitials === '') { $logoInitials = mb_strtoupper(mb_substr((string) ($restaurant['name'] ?? 'M'), 0, 1)); } $tagline = trim((string) ($restaurant['tagline'] ?? ($restaurant['description'] ?? ''))); $headerText = trim((string) ($c['headerText'] ?? '')); $footerText = trim((string) ($c['footerText'] ?? '')); $liveDevice = $liveDevice ?? null; // set when a paired TV is displaying this board (self-refresh) $liveVersion = $liveVersion ?? null; $cuisineFlag = $cuisineFlag ?? null; // the cuisine's flag, shown beside the logo when 'showCuisine' is on // Does this board actually play a clip? A decoding video composited over the ambient aurora layer is // what drops frames on a cheap HDMI stick, so the board is marked and tv.css parks the animation. $hasVideo = false; foreach ($columns as $__col) { if (($__col['type'] ?? '') !== 'video') { continue; } $__src = ($__col['provider'] ?? '') === 'youtube' ? (string) ($__col['youtubeId'] ?? '') : (string) ($__col['src'] ?? ''); if ($__src !== '') { $hasVideo = true; break; } } unset($__col, $__src); // A media column with no source (a template, or a not-yet-filled column) shows a labelled placeholder // instead of an empty gap — so a board template reads as complete. Suppressed on a live wall board. $ph = empty($liveDevice); $phBox = static function (string $label): string { $svg = ''; return '
' . $svg . '' . e($label) . '
'; }; @endphp {{ $title ?? 'Menu' }} @php /* On a real TV the stage letterboxes with black bars, as a TV does. Inside the vendor's preview cards (screens list, editor thumbnails) those bars sit on a white admin page and read as a broken card — a vertical board in a 16:9 thumbnail looked mostly black. A preview stage therefore wears the board's own background so the slack blends into it. */ $stageBg = ''; if (($style ?? '') === 'preview') { $stageBg = !empty($c['bgColor']) ? (string) $c['bgColor'] : (\App\Models\TvScreen::THEME_BG[$c['theme'] ?? 'light'] ?? ''); } @endphp
@if (!empty($c['header']))
@if ($logo !== '') @else{{ $logoInitials }}@endif @if ($cuisineFlag)@endif

{{ $restaurant['name'] }}

@if ($tagline !== '')

{{ $tagline }}

@endif
@if ($headerText !== ''){{ $headerText }}@endif
@endif
@foreach ($columns as $col) @php $type = $col['type']; @endphp
@if ($type === 'menu') @if (($col['heading'] ?? '') !== '')

{{ $col['heading'] }}

@endif
@php $no = 0; @endphp @foreach ($col['items'] as $it) @php $no++; $hasImg = !empty($col['images']) && !empty($it['image']); @endphp
@php /* A column that shows dish photos KEEPS the slot when a dish has none: it is DRAWN as an outline rather than dropped. Dropping it collapsed the row, so a single dish without a photo pulled the whole list out of alignment — and a TEMPLATE, which renders against the bundled sample menu where no dish carries a photo, lost every thumbnail and read as a design that never had images. Same outlined-slot idea a print-menu template already uses, so the two surfaces present an unfilled design the same way. */ @endphp @if (!empty($col['images']))
@if ($hasImg) @else @endif @if (!empty($col['numbers'])){{ $no }}@endif
@elseif (!empty($col['numbers'])) {{ $no }} @endif @php // Calories, opt-in per column and placed where the design wants them: beside the // name, under the description, or under the price. A dish WITHOUT a figure keeps // its slot as a muted dash rather than collapsing, so rows stay aligned and the // board shows how it will read once the figure is filled in. $calOn = !empty($col['calories']); $calPos = in_array($col['caloriesPos'] ?? 'desc', ['name', 'desc', 'price'], true) ? $col['caloriesPos'] : 'desc'; $calN = (int) ($it['calories'] ?? 0); $calTag = static fn (string $extra): string => '' . ($calN > 0 ? e((string) $calN) . ' ' . e(t_raw('vendor.print_menu.kcal')) : '–') . ''; @endphp

{{ $it['name'] }}{{ $calOn && $calPos === 'name' ? ' ' . $calTag(' is-inline') : '' }}

@if (!empty($col['descriptions']) && !empty($it['description']))

{{ $it['description'] }}

@endif @if ($calOn && $calPos === 'desc'){!! $calTag(' is-block') !!}@endif
{{ money($it['price']) }} @if ($calOn && $calPos === 'price'){!! $calTag(' is-block') !!}@endif
@endforeach
@elseif ($type === 'image') @if (($col['src'] ?? '') !== '') @elseif ($ph){!! $phBox(t_raw('tv.ph_image')) !!}@endif @elseif ($type === 'video') @if (($col['provider'] ?? '') === 'youtube' && ($col['youtubeId'] ?? '') !== '') @php $yt = $col['youtubeId']; @endphp @elseif (($col['src'] ?? '') !== '') @elseif ($ph){!! $phBox(t_raw('tv.ph_video')) !!}@endif @elseif ($type === 'feature')
@if (empty($col['items']) && $ph){!! $phBox(t_raw('tv.ph_feature')) !!}@endif @foreach ($col['items'] as $it)
@if (!empty($it['image']))@endif

{{ $it['name'] }}

@if (!empty($it['description']))

{{ $it['description'] }}

@endif {{ money($it['price']) }}
@endforeach
@endif @php // A media column can carry a product overlay: dish name + description + a price pill, // anchored to one of nine points and drawn over the image/video (each part hideable). @endphp @if (!empty($col['product'])) @php $p = $col['product']; $pscale = round(0.7 + (int) ($p['size'] ?? 5) * 0.13, 3); // size 0–10 → 0.7×–2.0× @endphp
@if (!empty($p['showName']))
{{ $p['name'] }}
@endif @if (!empty($p['showDesc']) && $p['desc'] !== '')

{{ $p['desc'] }}

@endif @if (!empty($p['showPrice'])){{ money($p['price']) }}@endif
@endif
@endforeach
@if (!empty($c['footer'])) @endif
@if ($liveDevice) @php // A paired TV self-heals: it reloads when the menu or layout changes, and // returns to the connect screen if the vendor unpairs it.; @endphp @endif