Files
valknar b3b9fb7ac6 Initial commit — Bar Pivoine cocktail recipe site
Hugo Extended site with 426 cocktail recipes from the open cocktail dataset.
Dark amber/gold editorial aesthetic, Tailwind CSS v4, Alpine.js client-side
search and filtering, HTMX page transitions, Docker + nginx production build.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 11:53:45 +02:00

263 lines
13 KiB
HTML
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{{ define "main" }}
<div
x-data="cocktailSearch()"
x-init="init()"
class="max-w-[1280px] mx-auto px-8 max-[860px]:px-5 pt-12 pb-[90px]"
>
<!-- ══ SEARCH BAR ════════════════════════════════════════════════════════ -->
<div class="mb-5">
<div class="eyebrow">The cellar — {{ len .Pages }} recipes</div>
</div>
<div
class="flex items-center gap-[18px] pb-5 pt-1 cursor-text mb-0"
@click="$refs.searchInput.focus()"
>
{{- partial "icon.html" "search" -}}
<input
x-ref="searchInput"
type="text"
:value="q"
@input="setQuery($event.target.value)"
placeholder="Name, spirit, ingredient, method…"
class="arch-input flex-1 min-w-0 bg-transparent border-0 outline-none text-ink font-serif text-[clamp(26px,3.6vw,50px)] font-medium tracking-[-0.01em] leading-[1.15] py-[10px] caret-gold"
aria-label="Search recipes"
/>
<button
x-show="q"
x-cloak
@click.stop="setQuery('')"
class="bg-transparent border-0 text-ink-mute text-[18px] px-[10px] py-1.5 cursor-pointer transition-colors duration-[150ms] leading-none flex-none hover:text-gold-2"
aria-label="Clear search"
>
{{- partial "icon.html" "x" -}}
</button>
<span class="font-mono text-[11.5px] tracking-[0.12em] uppercase text-gold whitespace-nowrap flex-none">
<span x-text="isFiltered ? (filtered.length + '/' + all.length) : (all.length + ' recipes')"></span>
</span>
</div>
<div class="mb-7 h-px bg-[linear-gradient(90deg,transparent_0%,rgba(233,210,180,0.18)_12%,rgba(233,210,180,0.18)_88%,transparent_100%)]"></div>
<!-- ══ FILTER BAR ════════════════════════════════════════════════════════ -->
<div class="flex flex-wrap gap-[9px] items-center mb-7">
<!-- Spirit Style dropdown -->
<div class="relative" data-fgroup>
<button
@click="toggleFilter('alcoholic')"
:class="active.alcoholic ? 'border-gold text-gold-2' : (openFilter === 'alcoholic' ? 'border-gold' : 'border-warm/10 text-ink-soft')"
class="inline-flex items-center gap-[9px] bg-surface border rounded-full px-[15px] py-[9px] transition-all duration-[180ms] hover:border-warm/18 hover:text-ink font-sans"
type="button"
>
<span class="font-mono text-[11px] tracking-[0.06em] uppercase">Spirit Style</span>
<span x-show="active.alcoholic" x-cloak class="font-sans text-[12.5px] text-gold-2" x-text="valLabel('alcoholic', active.alcoholic)"></span>
<span class="text-[9px] opacity-70">{{- partial "icon.html" "chevron-down" -}}</span>
</button>
<div x-show="openFilter === 'alcoholic'" x-cloak class="fmenu">
<template x-for="it in tax.alcoholic" :key="it.slug">
<button
@click="toggle('alcoholic', it.slug)"
:class="active.alcoholic === it.slug ? 'text-gold-2 bg-gold/16' : 'text-ink-soft hover:bg-warm/[0.06] hover:text-ink'"
class="flex justify-between items-center w-full text-left bg-transparent border-0 text-[14px] px-[11px] py-[9px] rounded-lg transition-all duration-[140ms] cursor-pointer"
type="button"
>
<span x-text="it.value"></span>
<span class="font-mono text-[11px] text-ink-faint" x-text="it.count"></span>
</button>
</template>
</div>
</div>
<!-- Category dropdown -->
<div class="relative" data-fgroup>
<button
@click="toggleFilter('category')"
:class="active.category ? 'border-gold text-gold-2' : (openFilter === 'category' ? 'border-gold' : 'border-warm/10 text-ink-soft')"
class="inline-flex items-center gap-[9px] bg-surface border rounded-full px-[15px] py-[9px] transition-all duration-[180ms] hover:border-warm/18 hover:text-ink font-sans"
type="button"
>
<span class="font-mono text-[11px] tracking-[0.06em] uppercase">Category</span>
<span x-show="active.category" x-cloak class="font-sans text-[12.5px] text-gold-2" x-text="valLabel('category', active.category)"></span>
<span class="text-[9px] opacity-70">{{- partial "icon.html" "chevron-down" -}}</span>
</button>
<div x-show="openFilter === 'category'" x-cloak class="fmenu">
<template x-for="it in tax.category" :key="it.slug">
<button
@click="toggle('category', it.slug)"
:class="active.category === it.slug ? 'text-gold-2 bg-gold/16' : 'text-ink-soft hover:bg-warm/[0.06] hover:text-ink'"
class="flex justify-between items-center w-full text-left bg-transparent border-0 text-[14px] px-[11px] py-[9px] rounded-lg transition-all duration-[140ms] cursor-pointer"
type="button"
>
<span x-text="it.value"></span>
<span class="font-mono text-[11px] text-ink-faint" x-text="it.count"></span>
</button>
</template>
</div>
</div>
<!-- Glassware dropdown -->
<div class="relative" data-fgroup>
<button
@click="toggleFilter('glass')"
:class="active.glass ? 'border-gold text-gold-2' : (openFilter === 'glass' ? 'border-gold' : 'border-warm/10 text-ink-soft')"
class="inline-flex items-center gap-[9px] bg-surface border rounded-full px-[15px] py-[9px] transition-all duration-[180ms] hover:border-warm/18 hover:text-ink font-sans"
type="button"
>
<span class="font-mono text-[11px] tracking-[0.06em] uppercase">Glassware</span>
<span x-show="active.glass" x-cloak class="font-sans text-[12.5px] text-gold-2" x-text="valLabel('glass', active.glass)"></span>
<span class="text-[9px] opacity-70">{{- partial "icon.html" "chevron-down" -}}</span>
</button>
<div x-show="openFilter === 'glass'" x-cloak class="fmenu">
<template x-for="it in tax.glass" :key="it.slug">
<button
@click="toggle('glass', it.slug)"
:class="active.glass === it.slug ? 'text-gold-2 bg-gold/16' : 'text-ink-soft hover:bg-warm/[0.06] hover:text-ink'"
class="flex justify-between items-center w-full text-left bg-transparent border-0 text-[14px] px-[11px] py-[9px] rounded-lg transition-all duration-[140ms] cursor-pointer"
type="button"
>
<span x-text="it.value"></span>
<span class="font-mono text-[11px] text-ink-faint" x-text="it.count"></span>
</button>
</template>
</div>
</div>
<!-- Active ingredient chip -->
<template x-if="active.ingredient">
<button
@click="toggle('ingredient', active.ingredient)"
class="chip on"
type="button"
>
<span class="dot"></span>
<span x-text="valLabel('ingredient', active.ingredient)"></span>
<span></span>
</button>
</template>
<!-- Clear all -->
<button
x-show="activeCount > 0"
x-cloak
@click="clearAll()"
class="bg-transparent border-0 font-mono text-[11px] tracking-[0.08em] uppercase text-ink-faint underline underline-offset-[3px] ml-1 cursor-pointer transition-colors duration-[160ms] hover:text-gold-2 px-0 py-0.5"
type="button"
>
Clear (<span x-text="activeCount"></span>)
</button>
</div>
<!-- ══ GRID ══════════════════════════════════════════════════════════════ -->
<template x-if="paged.length > 0">
<div>
<div class="grid [grid-template-columns:repeat(auto-fill,minmax(258px,1fr))] gap-[22px] max-[560px]:[grid-template-columns:repeat(auto-fill,minmax(150px,1fr))] max-[560px]:gap-3.5">
<template x-for="c in paged" :key="c.slug">
<a
:href="c.slug"
class="block border border-warm/10 rounded-[14px] overflow-hidden bg-surface transition-[transform,border-color,box-shadow] duration-[260ms] ease-[cubic-bezier(.2,.7,.3,1)] relative hover:-translate-y-1 hover:border-warm/18 hover:shadow-[0_24px_50px_-24px_rgba(0,0,0,.8)] card-wrap"
:aria-label="c.name"
>
<div class="aspect-square relative overflow-hidden">
<img
:src="c.photo || c.thumb || ''"
:alt="c.name"
loading="lazy"
decoding="async"
class="card-img"
onerror="this.style.background='#1c1611'"
/>
<span class="absolute top-[11px] left-[11px] z-[2] font-mono text-[9px] tracking-[0.12em] uppercase px-[9px] py-1 rounded-full bg-[rgba(13,10,7,0.6)] backdrop-blur-[6px] border border-warm/10 text-ink-soft" x-text="nobLabel(c)"></span>
</div>
<div class="px-[17px] pt-4 pb-[18px]">
<div class="font-mono text-[10px] tracking-[0.16em] uppercase text-gold" x-text="c.category"></div>
<h3 class="font-serif font-semibold text-[23px] leading-[1.05] tracking-tight mt-[7px] mb-[10px]" x-text="c.name"></h3>
<div class="font-mono text-[10px] text-ink-mute tracking-[0.04em]" x-text="c.glass + (c.ingredients.length ? ' · ' + c.ingredients.length + ' ingredients' : '')"></div>
</div>
</a>
</template>
</div>
<!-- Pagination -->
<template x-if="totalPages > 1">
<nav class="flex items-center justify-center gap-[6px] pt-14 flex-wrap" aria-label="pages">
<button
@click="changePage(page - 1)"
:disabled="page === 1"
class="inline-flex items-center gap-[7px] font-mono text-[12px] tracking-[0.1em] uppercase text-ink-soft bg-transparent border border-warm/10 rounded-full px-5 py-[11px] transition-all duration-[180ms] disabled:opacity-[0.28] disabled:cursor-default enabled:hover:border-gold enabled:hover:text-gold-2"
type="button"
>
{{- partial "icon.html" "arrow-left" -}} Prev
</button>
<div class="flex items-center gap-[2px]">
<template x-for="item in pagerItems()" :key="item.type + (item.n || '')">
<span class="contents">
<template x-if="item.type === 'dot'">
<span class="text-ink-faint font-mono w-6 text-center text-[14px] leading-[38px]"></span>
</template>
<template x-if="item.type === 'page'">
<button
@click="changePage(item.n)"
:class="item.n === page ? 'bg-gold text-[#1a1206] font-semibold pointer-events-none' : 'bg-transparent text-ink-mute hover:text-ink hover:bg-warm/[0.07]'"
class="font-mono text-[13px] w-[38px] h-[38px] rounded-full border-0 cursor-pointer transition-all duration-[150ms] flex items-center justify-center"
x-text="item.n"
type="button"
></button>
</template>
</span>
</template>
</div>
<button
@click="changePage(page + 1)"
:disabled="page === totalPages"
class="inline-flex items-center gap-[7px] font-mono text-[12px] tracking-[0.1em] uppercase text-ink-soft bg-transparent border border-warm/10 rounded-full px-5 py-[11px] transition-all duration-[180ms] disabled:opacity-[0.28] disabled:cursor-default enabled:hover:border-gold enabled:hover:text-gold-2"
type="button"
>
Next {{- partial "icon.html" "arrow-right" -}}
</button>
</nav>
</template>
</div>
</template>
<!-- Empty state -->
<template x-if="paged.length === 0">
<div class="text-center py-[90px]">
<div class="font-serif text-[54px] text-ink-faint leading-none"></div>
<p class="font-serif text-[26px] text-ink-soft mt-2 mb-6">Nothing matches that pour.</p>
<button @click="clearAll()" class="btn btn-ghost" type="button">Reset the search</button>
</div>
</template>
</div>
<!-- Cocktail data for Alpine search -->
{{- $data := slice -}}
{{- range .Pages -}}
{{- $ings := .Params.ingredients | default (slice) -}}
{{- $meas := .Params.ingredientMeasures | default (slice) -}}
{{- $ingList := slice -}}
{{- range $i, $ing := $ings -}}
{{- $m := "" -}}{{- if lt $i (len $meas) -}}{{- $m = index $meas $i -}}{{- end -}}
{{- $ingList = $ingList | append (dict "name" $ing "measure" $m) -}}
{{- end -}}
{{- $cat := "" -}}{{- with .Params.categories -}}{{- $cat = index . 0 -}}{{- end -}}
{{- $glass := "" -}}{{- with .Params.glasses -}}{{- $glass = index . 0 -}}{{- end -}}
{{- $photo := .Params.drinkThumbnail | default "" -}}
{{- with .Resources.GetMatch "cocktail.*" -}}{{- $photo = (.Resize "500x webp").RelPermalink -}}{{- end -}}
{{- $data = $data | append (dict
"slug" .RelPermalink
"name" .Title
"alcoholic" (.Params.alcoholic | default "")
"category" $cat
"glass" $glass
"ingredients" $ingList
"photo" $photo
) -}}
{{- end -}}
<script>
window.__COCKTAILS__ = {{ $data | jsonify | safeJS }};
</script>
{{ end }}