This commit is contained in:
pika 2025-01-04 02:40:01 +01:00
parent 071a4d8a97
commit 4d3ca1c554
39 changed files with 3386 additions and 0 deletions

61
assets/css/main.css Normal file
View file

@ -0,0 +1,61 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--primary-color: theme('colors.primary.DEFAULT');
--accent-color: theme('colors.accent.DEFAULT');
--text-color: theme('colors.gray.800');
--background-color: theme('colors.white');
--card-background: theme('colors.white');
--header-background: theme('colors.white');
}
.dark {
--primary-color: theme('colors.primary.400');
--accent-color: theme('colors.accent.400');
--text-color: theme('colors.gray.200');
--background-color: theme('colors.gray.900');
--card-background: theme('colors.gray.800');
--header-background: theme('colors.gray.900');
}
}
@keyframes slideInLeft {
from {
transform: translateX(-100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideInRight {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.slide-in-left {
animation: slideInLeft 1s ease-out forwards;
}
.slide-in-right {
animation: slideInRight 1s ease-out forwards;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}

44
assets/js/menu.js Normal file
View file

@ -0,0 +1,44 @@
document.addEventListener('DOMContentLoaded', function() {
// Menu category switching
const tabButtons = document.querySelectorAll('[data-tab]');
const menuContents = document.querySelectorAll('.menu-content');
tabButtons.forEach(button => {
button.addEventListener('click', () => {
const targetId = `${button.dataset.tab}-content`;
// Update button styles
tabButtons.forEach(btn => {
const isActive = btn === button;
btn.classList.toggle('bg-pizza-red', isActive);
btn.classList.toggle('text-white', isActive);
btn.classList.toggle('bg-gray-200', !isActive);
btn.classList.toggle('dark:bg-pizza-darker', !isActive);
btn.classList.toggle('text-gray-700', !isActive);
btn.classList.toggle('dark:text-white', !isActive);
});
// Show/hide content
menuContents.forEach(content => {
content.classList.toggle('hidden', content.id !== targetId);
});
});
});
// Pizza price group toggles
const priceGroupHeaders = document.querySelectorAll('.price-group-header');
priceGroupHeaders.forEach(header => {
header.addEventListener('click', () => {
const content = header.nextElementSibling;
const arrow = header.querySelector('svg');
// Toggle content visibility
content.classList.toggle('hidden');
// Rotate arrow
arrow.style.transform = content.classList.contains('hidden')
? 'rotate(0deg)'
: 'rotate(180deg)';
});
});
});

View file

@ -0,0 +1,95 @@
function updateTimeAndStatus() {
const now = new Date();
const timeString = now.toLocaleTimeString('de-DE', {
hour: '2-digit',
minute: '2-digit',
hour12: false
});
// Update time display
const timeDisplay = document.getElementById('current-time');
if (timeDisplay) {
timeDisplay.textContent = timeString;
}
fetch('/api/check-open-status')
.then(response => response.json())
.then(status => {
const statusBar = document.getElementById('status-bar');
const statusText = document.getElementById('status-text');
const deliveryText = document.querySelector('.delivery-text');
if (statusBar && statusText) {
statusBar.className = status.isOpen
? 'bg-status-green transition-colors duration-300'
: 'bg-status-red transition-colors duration-300';
const textOpen = statusText.dataset.textOpen;
const textClosed = statusText.dataset.textClosed;
statusText.textContent = status.isOpen ? textOpen : textClosed;
}
// Debug information
const debugInfo = document.getElementById('debug-info');
if (debugInfo && !status.isOpen) {
const nextOpen = document.getElementById('next-open');
const currentDay = document.getElementById('current-day');
if (currentDay) {
currentDay.textContent = now.toLocaleDateString('de-DE', { weekday: 'long' });
}
if (nextOpen) {
const hoursRows = document.querySelectorAll('.hours-row');
let nextOpenTime = null;
let foundToday = false;
// First check today's remaining times
hoursRows.forEach(row => {
if (row.dataset.day === status.currentDay) {
const openTime = row.dataset.open;
if (openTime > timeString) {
nextOpenTime = `Today at ${openTime}`;
foundToday = true;
}
}
});
// If no times found today, find next day's opening
if (!foundToday) {
hoursRows.forEach(row => {
const dayIndex = parseInt(row.dataset.dayIndex || 0);
const currentDayIndex = now.getDay();
if (dayIndex > currentDayIndex || (dayIndex === 0 && currentDayIndex !== 0)) {
const openTime = row.dataset.open;
const dayName = row.dataset.dayName;
if (!nextOpenTime) {
nextOpenTime = `${dayName} at ${openTime}`;
}
}
});
}
nextOpen.textContent = nextOpenTime || 'Check opening hours';
}
}
// Update other elements
if (deliveryText) {
deliveryText.className = status.isOpen
? 'delivery-text text-status-green'
: 'delivery-text text-status-red';
}
document.querySelectorAll('.hours-row').forEach(row => {
const isCurrentDay = row.dataset.day === status.currentDay;
row.classList.toggle('border-2', isCurrentDay && status.isOpen);
row.classList.toggle('border-status-green', isCurrentDay && status.isOpen);
});
});
}
// Update immediately and then every minute
updateTimeAndStatus();
setInterval(updateTimeAndStatus, 60000);

60
layouts/404.html Normal file
View file

@ -0,0 +1,60 @@
{{ define "main" }}
<div class="min-h-screen flex items-center justify-center bg-white dark:bg-pizza-dark p-4">
<div class="text-center">
<h1 class="text-9xl font-bold tracking-widest text-gray-900 dark:text-white">
4
<span class="inline-block animate-spin-slow origin-center">
<svg class="w-32 h-32 text-pizza-red" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="45" fill="currentColor"/>
<!-- Pepperoni -->
<circle cx="30" cy="35" r="8" fill="#8B0000"/>
<circle cx="60" cy="40" r="8" fill="#8B0000"/>
<circle cx="45" cy="65" r="8" fill="#8B0000"/>
<!-- Cheese drips -->
<path d="M20,50 Q30,70 40,50" fill="none" stroke="#FFD700" stroke-width="3"/>
<path d="M60,50 Q70,75 80,50" fill="none" stroke="#FFD700" stroke-width="3"/>
</svg>
</span>
4
</h1>
<div class="mt-8 space-y-4">
<p class="text-2xl font-medium text-gray-600 dark:text-gray-300">
{{ i18n "404_oops" | default "Oops! Looks like this slice is missing!" }}
</p>
<p class="text-gray-500 dark:text-gray-400">
{{ i18n "404_text" | default "The page you're looking for has been eaten or never existed." }}
</p>
<!-- Animated delivery guy -->
<div class="mt-12 relative">
<div class="animate-drive-by">
<svg class="w-24 h-24 mx-auto text-pizza-red" viewBox="0 0 100 100">
<!-- Scooter -->
<path d="M10,70 Q30,65 50,70 Q70,75 90,70" fill="none" stroke="currentColor" stroke-width="3"/>
<circle cx="25" cy="80" r="10" fill="currentColor"/>
<circle cx="75" cy="80" r="10" fill="currentColor"/>
<!-- Driver -->
<circle cx="50" cy="40" r="15" fill="currentColor"/>
<path d="M40,55 L60,55 L50,70 Z" fill="currentColor"/>
</svg>
</div>
</div>
<div class="mt-12">
<a href="/"
class="inline-flex items-center px-6 py-3 text-lg font-medium text-white
bg-pizza-red rounded-full hover:bg-red-600
transform hover:scale-105 transition-all duration-200
animate-bounce-subtle">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
</svg>
{{ i18n "404_back_home" | default "Back to Home" }}
</a>
</div>
</div>
</div>
</div>
{{ end }}

View file

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="{{ .Site.Language.Lang }}" class="scroll-smooth dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ if .IsHome }}{{ .Site.Title }}{{ else }}{{ .Title }} - {{ .Site.Title }}{{ end }}</title>
<link rel="stylesheet" href="{{ "css/style.css" | relURL }}">
</head>
<body class="min-h-screen bg-white text-gray-900 dark:bg-pizza-dark dark:text-white">
{{ partial "header.html" . }}
<main class="pt-24">
{{ block "main" . }}{{ end }}
</main>
{{ partial "footer.html" . }}
<!-- JavaScript -->
<script src="{{ "js/main.js" | relURL }}"></script>
<script src="{{ "js/theme.js" | relURL }}" defer></script>
<script src="/js/menu.js"></script>
{{ with resources.Get "js/opening-status.js" }}
{{ with . | resources.Minify }}
<script src="{{ .RelPermalink }}"></script>
{{ end }}
{{ end }}
</body>
</html>

View file

@ -0,0 +1,5 @@
{
"isOpen": {{ $status := partial "check-open-status.html" . }}{{ $status.isOpen }},
"currentDay": "{{ now.Format "Monday" }}",
"currentTime": "{{ now.Format "15:04" }}"
}

View file

@ -0,0 +1,12 @@
{{ define "main" }}
<h1>{{ .Title }}</h1>
<div class="posts-list">
{{ range .Pages }}
<article class="post-preview">
<h2><a href="{{ .Permalink }}">{{ .Title }}</a></h2>
<time>{{ .Date.Format "January 2, 2006" }}</time>
<p>{{ .Summary }}</p>
</article>
{{ end }}
</div>
{{ end }}

View file

@ -0,0 +1,9 @@
{{ define "main" }}
<article>
<h1>{{ .Title }}</h1>
<time>{{ .Date.Format "January 2, 2006" }}</time>
<div class="content">
{{ .Content }}
</div>
</article>
{{ end }}

View file

@ -0,0 +1,5 @@
{
"layout": "{{ .Layout }}",
"title": "{{ .Title }}",
"type": "{{ .Type }}"
}

105
layouts/about/about.html Normal file
View file

@ -0,0 +1,105 @@
{{ define "main" }}
<div class="min-h-screen">
<!-- Hero Section with Background -->
<div class="relative h-[40vh] mb-16">
<div class="absolute inset-0 bg-[url('/images/pizza-hero.jpg')] bg-cover bg-center">
<div class="absolute inset-0 bg-black/50"></div>
</div>
<div class="relative container mx-auto h-full flex items-center justify-center px-4">
<div class="text-center text-white space-y-4">
<h1 class="text-5xl md:text-6xl font-bold">{{ .Site.Data.about.hero.title }}</h1>
<p class="text-xl md:text-2xl max-w-2xl mx-auto">{{ .Site.Data.about.hero.description }}</p>
</div>
</div>
</div>
<!-- Story Section -->
<div class="container mx-auto px-4 py-12">
<div class="max-w-4xl mx-auto space-y-16">
<!-- Our Story -->
<div class="grid md:grid-cols-2 gap-8 items-center">
<div class="space-y-6">
<h2 class="text-3xl font-bold text-gray-900 dark:text-white">{{ .Site.Data.about.story.title }}</h2>
<div class="prose dark:prose-invert">
<p class="text-lg text-gray-600 dark:text-gray-300">
{{ .Site.Data.about.story.content }}
</p>
</div>
<ul class="space-y-3">
{{ range .Site.Data.about.story.features }}
<li class="flex items-center space-x-3 text-gray-600 dark:text-gray-300">
<svg class="w-5 h-5 text-pizza-red" fill="currentColor" viewBox="0 0 20 20">
<path d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"/>
</svg>
<span>{{ . }}</span>
</li>
{{ end }}
</ul>
</div>
<div class="relative h-80 rounded-xl overflow-hidden">
<img src="/images/pizza-innen.webp" alt="Restaurant Interior"
class="absolute inset-0 w-full h-full object-cover">
</div>
</div>
<!-- Team Section -->
<div class="space-y-8">
<div class="text-center">
<h2 class="text-3xl font-bold text-gray-900 dark:text-white mb-4">{{ .Site.Data.about.team.title }}</h2>
<p class="text-lg text-gray-600 dark:text-gray-300 max-w-2xl mx-auto">
{{ .Site.Data.about.team.description }}
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
{{ range .Site.Data.about.team.members }}
<div class="group">
<div class="relative overflow-hidden rounded-xl aspect-square mb-4">
<img src="/images/{{ .image }}" alt="{{ .name }}"
class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500">
</div>
<div class="text-center">
<h3 class="text-xl font-semibold text-gray-900 dark:text-white">{{ .name }}</h3>
<p class="text-pizza-red">{{ .role }}</p>
<p class="mt-2 text-gray-600 dark:text-gray-300">
{{ .description }}
</p>
</div>
</div>
{{ end }}
</div>
</div>
<!-- Contact Section -->
<div class="bg-white/5 dark:bg-pizza-darker/5 backdrop-blur-sm rounded-xl p-8 shadow-lg">
<div class="grid md:grid-cols-2 gap-8">
<div class="space-y-4">
<h3 class="text-2xl font-bold text-gray-900 dark:text-white">{{ .Site.Data.about.contact.title }}</h3>
<div class="space-y-2 text-gray-600 dark:text-gray-300">
<p class="flex items-center">
<svg class="w-5 h-5 mr-2 text-pizza-red" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
{{ .Site.Data.about.contact.address }}
</p>
<p class="flex items-center">
<svg class="w-5 h-5 mr-2 text-pizza-red" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"/>
</svg>
{{ .Site.Params.contact.phone }}
</p>
</div>
</div>
<div class="h-64 rounded-xl overflow-hidden">
<img src="/images/pizza-aussen.webp" alt="Restaurant Exterior"
class="w-full h-full object-cover">
</div>
</div>
</div>
</div>
</div>
</div>
{{ end }}

105
layouts/en/about/about.html Normal file
View file

@ -0,0 +1,105 @@
{{ define "main" }}
<div class="min-h-screen">
<!-- Hero Section with Background -->
<div class="relative h-[40vh] mb-16">
<div class="absolute inset-0 bg-[url('/images/pizza-hero.jpg')] bg-cover bg-center">
<div class="absolute inset-0 bg-black/50"></div>
</div>
<div class="relative container mx-auto h-full flex items-center justify-center px-4">
<div class="text-center text-white space-y-4">
<h1 class="text-5xl md:text-6xl font-bold">{{ .Site.Data.en.about.hero.title }}</h1>
<p class="text-xl md:text-2xl max-w-2xl mx-auto">{{ .Site.Data.en.about.hero.description }}</p>
</div>
</div>
</div>
<!-- Story Section -->
<div class="container mx-auto px-4 py-12">
<div class="max-w-4xl mx-auto space-y-16">
<!-- Our Story -->
<div class="grid md:grid-cols-2 gap-8 items-center">
<div class="space-y-6">
<h2 class="text-3xl font-bold text-gray-900 dark:text-white">{{ .Site.Data.en.about.story.title }}</h2>
<div class="prose dark:prose-invert">
<p class="text-lg text-gray-600 dark:text-gray-300">
{{ .Site.Data.en.about.story.content }}
</p>
</div>
<ul class="space-y-3">
{{ range .Site.Data.en.about.story.features }}
<li class="flex items-center space-x-3 text-gray-600 dark:text-gray-300">
<svg class="w-5 h-5 text-pizza-red" fill="currentColor" viewBox="0 0 20 20">
<path d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"/>
</svg>
<span>{{ . }}</span>
</li>
{{ end }}
</ul>
</div>
<div class="relative h-80 rounded-xl overflow-hidden">
<img src="/images/pizza-innen.webp" alt="Restaurant Interior"
class="absolute inset-0 w-full h-full object-cover">
</div>
</div>
<!-- Team Section -->
<div class="space-y-8">
<div class="text-center">
<h2 class="text-3xl font-bold text-gray-900 dark:text-white mb-4">{{ .Site.Data.en.about.team.title }}</h2>
<p class="text-lg text-gray-600 dark:text-gray-300 max-w-2xl mx-auto">
{{ .Site.Data.en.about.team.description }}
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
{{ range .Site.Data.en.about.team.members }}
<div class="group">
<div class="relative overflow-hidden rounded-xl aspect-square mb-4">
<img src="/images/{{ .image }}" alt="{{ .name }}"
class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500">
</div>
<div class="text-center">
<h3 class="text-xl font-semibold text-gray-900 dark:text-white">{{ .name }}</h3>
<p class="text-pizza-red">{{ .role }}</p>
<p class="mt-2 text-gray-600 dark:text-gray-300">
{{ .description }}
</p>
</div>
</div>
{{ end }}
</div>
</div>
<!-- Contact Section -->
<div class="bg-white/5 dark:bg-pizza-darker/5 backdrop-blur-sm rounded-xl p-8 shadow-lg">
<div class="grid md:grid-cols-2 gap-8">
<div class="space-y-4">
<h3 class="text-2xl font-bold text-gray-900 dark:text-white">{{ .Site.Data.en.about.contact.title }}</h3>
<div class="space-y-2 text-gray-600 dark:text-gray-300">
<p class="flex items-center">
<svg class="w-5 h-5 mr-2 text-pizza-red" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
{{ .Site.Data.en.about.contact.address }}
</p>
<p class="flex items-center">
<svg class="w-5 h-5 mr-2 text-pizza-red" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"/>
</svg>
{{ .Site.Params.contact.phone }}
</p>
</div>
</div>
<div class="h-64 rounded-xl overflow-hidden">
<img src="/images/pizza-aussen.webp" alt="Restaurant Exterior"
class="w-full h-full object-cover">
</div>
</div>
</div>
</div>
</div>
</div>
{{ end }}

25
layouts/en/index.html Normal file
View file

@ -0,0 +1,25 @@
{{ define "main" }}
<div class="min-h-screen bg-white dark:bg-pizza-dark text-gray-900 dark:text-white">
<div class="container mx-auto px-4 py-8">
<!-- Hero Section -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-12">
<!-- Opening Hours Card -->
<div class="bg-white dark:bg-pizza-darker rounded-xl p-6 shadow-lg lg:-ml-16 lg:-mt-8 relative z-10 slide-in-left">
{{ partial "opening-hours.html" . }}
</div>
<!-- Pizza Image -->
<div class="rounded-xl overflow-hidden shadow-lg slide-in-right">
<img src="/images/pizza.webp" alt="Delicious Pizza" class="w-full h-full object-cover transform hover:scale-105 transition-transform duration-700">
</div>
</div>
<!-- Menu Section -->
{{ partial "menu-card.html" . }}
</div>
</div>
<!-- Scripts -->
{{ $menu := resources.Get "js/menu.js" | resources.Minify | resources.Fingerprint }}
<script src="{{ $menu.RelPermalink }}" integrity="{{ $menu.Data.Integrity }}"></script>
{{ end }}

26
layouts/index.html Normal file
View file

@ -0,0 +1,26 @@
{{ define "main" }}
<div class="min-h-screen bg-white dark:bg-pizza-dark text-gray-900 dark:text-white">
<div class="container mx-auto px-4 py-8">
<!-- Hero Section -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-12">
<!-- Opening Hours Card -->
<div class="bg-white dark:bg-pizza-darker rounded-xl p-6 shadow-lg lg:-ml-16 lg:-mt-8 relative z-10 slide-in-left">
{{ partial "opening-hours.html" . }}
</div>
<!-- Pizza Image -->
<div class="rounded-xl overflow-hidden shadow-lg slide-in-right">
<img src="/images/pizza.webp" alt="Delicious Pizza" class="w-full h-full object-cover transform hover:scale-105 transition-transform duration-700">
</div>
</div>
<!-- Menu Section -->
{{ partial "menu-card.html" . }}
</div>
</div>
<!-- Scripts -->
{{ $menu := resources.Get "js/menu.js" | resources.Minify | resources.Fingerprint }}
<script src="{{ $menu.RelPermalink }}" integrity="{{ $menu.Data.Integrity }}"></script>
{{ end }}

View file

@ -0,0 +1,32 @@
{{ $currentTime := now.Format "15:04" }}
{{ $currentDay := now.Format "Monday" }}
{{ $isOpen := false }}
{{ $holidayInfo := dict }}
{{ $isHoliday := false }}
<!-- Check if today is a holiday -->
{{ range .Site.Data.hours.holidays }}
{{ $holidayDate := time .date }}
{{ if eq (now.Format "2006-01-02") ($holidayDate.Format "2006-01-02") }}
{{ $isHoliday = true }}
{{ $holidayInfo = . }}
{{ if not .closed }}
{{ if and (ge $currentTime .open) (le $currentTime .close) }}
{{ $isOpen = true }}
{{ end }}
{{ end }}
{{ end }}
{{ end }}
<!-- If not a holiday, check regular hours -->
{{ if not $isHoliday }}
{{ range .Site.Data.hours.regular_hours }}
{{ if in .days $currentDay }}
{{ if and (ge $currentTime .open) (le $currentTime .close) }}
{{ $isOpen = true }}
{{ end }}
{{ end }}
{{ end }}
{{ end }}
{{ return dict "isOpen" $isOpen "isHoliday" $isHoliday "holidayInfo" $holidayInfo "currentDay" $currentDay "currentTime" $currentTime }}

View file

@ -0,0 +1,13 @@
<div class="{{ .Class }}">
{{ if eq .Site.Language.Lang "en" }}
<a href="/de/" class="lang-btn">
<img src="/icons/de.svg" alt="Deutsch" class="flag-icon">
<span>DE</span>
</a>
{{ else }}
<a href="/en/" class="lang-btn">
<img src="/icons/gb.svg" alt="English" class="flag-icon">
<span>EN</span>
</a>
{{ end }}
</div>

View file

@ -0,0 +1,70 @@
<!-- theme switcher -->
{{ $class := .Class }}
{{ if site.Params.theme_switcher }}
<div class="theme-switcher {{ $class }} hidden">
<input id="theme-switcher" data-theme-switcher type="checkbox" />
<label for="theme-switcher">
<span class="sr-only">theme switcher</span>
<span>
<!-- sun -->
<svg
class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-10 opacity-100 dark:opacity-0"
viewBox="0 0 56 56"
fill="#fff"
height="16"
width="16">
<path
d="M30 4.6c0-1-.9-2-2-2a2 2 0 0 0-2 2v5c0 1 .9 2 2 2s2-1 2-2Zm9.6 9a2 2 0 0 0 0 2.8c.8.8 2 .8 2.9 0L46 13a2 2 0 0 0 0-2.9 2 2 0 0 0-3 0Zm-26 2.8c.7.8 2 .8 2.8 0 .8-.7.8-2 0-2.9L13 10c-.7-.7-2-.8-2.9 0-.7.8-.7 2.1 0 3ZM28 16a12 12 0 0 0-12 12 12 12 0 0 0 12 12 12 12 0 0 0 12-12 12 12 0 0 0-12-12Zm23.3 14c1.1 0 2-.9 2-2s-.9-2-2-2h-4.9a2 2 0 0 0-2 2c0 1.1 1 2 2 2ZM4.7 26a2 2 0 0 0-2 2c0 1.1.9 2 2 2h4.9c1 0 2-.9 2-2s-1-2-2-2Zm37.8 13.6a2 2 0 0 0-3 0 2 2 0 0 0 0 2.9l3.6 3.5a2 2 0 0 0 2.9 0c.8-.8.8-2.1 0-3ZM10 43.1a2 2 0 0 0 0 2.9c.8.7 2.1.8 3 0l3.4-3.5c.8-.8.8-2.1 0-2.9-.8-.8-2-.8-2.9 0Zm20 3.4c0-1.1-.9-2-2-2a2 2 0 0 0-2 2v4.9c0 1 .9 2 2 2s2-1 2-2Z" />
</svg>
<!-- moon -->
<svg
class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-10 opacity-0 dark:opacity-100"
viewBox="0 0 24 24"
fill="none"
height="16"
width="16">
<path
fill="#000"
fill-rule="evenodd"
clip-rule="evenodd"
d="M8.2 2.2c1-.4 2 .6 1.6 1.5-1 3-.4 6.4 1.8 8.7a8.4 8.4 0 0 0 8.7 1.8c1-.3 2 .5 1.5 1.5v.1a10.3 10.3 0 0 1-9.4 6.2A10.3 10.3 0 0 1 3.2 6.7c1-2 2.9-3.5 4.9-4.4Z" />
</svg>
</span>
</label>
</div>
<!-- theme switcher -->
<script>
var darkMode = {{if eq site.Params.theme_default "dark"}}true{{else}}false{{end}};
{{ if eq site.Params.theme_default "system" }}
if (window.matchMedia("(prefers-color-scheme: dark)").matches){darkMode = true}
{{ end }}
if (localStorage.getItem("theme") === "dark"){darkMode = true}
else if (localStorage.getItem("theme") === "light"){darkMode = false}
if (darkMode){document.documentElement.classList.add("dark")}
else {document.documentElement.classList.remove("dark")}
// Show theme switcher after applying theme
document.addEventListener("DOMContentLoaded", () => {
var themeSwitch = document.querySelectorAll("[data-theme-switcher]");
var themeSwitcherContainer = document.querySelector('.theme-switcher');
[].forEach.call(themeSwitch, function (ts) {
ts.checked = darkMode;
ts.addEventListener("click", () => {
document.documentElement.classList.toggle("dark");
localStorage.setItem(
"theme",
document.documentElement.classList.contains("dark") ? "dark" : "light"
);
});
});
// Now make the switcher visible
themeSwitcherContainer.classList.remove('hidden');
});
</script>
{{ end }}

View file

@ -0,0 +1,27 @@
<footer class="bg-white dark:bg-pizza-dark mt-auto">
<div class="container mx-auto px-4 py-8">
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<!-- Contact -->
<div>
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-4">{{ i18n "contact" }}</h3>
{{ with .Site.Params.contact }}
{{ if .phone }}
<a href="tel:{{ .phone }}" class="flex items-center text-status-green dark:text-gray-400 hover:text-status-green dark:hover:text-status-green mb-2">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"/>
</svg>
{{ i18n "call_now" }}: {{ .phone }}
</a>
{{ end }}
{{ end }}
</div>
</div>
<!-- Copyright -->
<div class="mt-8 pt-8 border-t border-gray-200 dark:border-gray-700">
<p class="text-center text-gray-600 dark:text-gray-400">
© {{ now.Format "2006" }} {{ .Site.Title }}
</p>
</div>
</div>
</footer>

View file

@ -0,0 +1 @@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">

View file

@ -0,0 +1,161 @@
<header class="fixed top-0 left-0 right-0 z-50 transition-all duration-300">
<!-- Status Bar -->
{{ $status := partial "check-open-status.html" . }}
<div id="status-bar" class="{{ if $status.isOpen }}bg-status-green{{ else }}bg-status-red{{ end }} transition-colors duration-300">
<div class="container mx-auto px-4">
<div class="flex justify-center items-center h-8 sm:h-10">
<div id="status-text" class="text-xs sm:text-sm text-white font-medium">
{{ if $status.isOpen }}
{{ i18n "open" }}
{{ else }}
{{ i18n "closed" }}
{{ end }}
</div>
<div class="ml-2 text-xs sm:text-sm text-white">
(<span id="current-time">{{ now.Format "15:04" }}</span> {{ i18n "time_suffix" }})
</div>
</div>
</div>
</div>
<!-- Navigation -->
<nav class="bg-white dark:bg-pizza-darker shadow-md">
<div class="container mx-auto px-4">
<div class="flex justify-between items-center h-16 sm:h-20">
<!-- Logo -->
<div class="flex-shrink-0">
<a href="{{ .Site.Home.RelPermalink }}" class="flex items-center">
<img src="/images/logo.webp"
alt="{{ .Site.Title }}"
class="h-12 sm:h-16 w-auto transition-transform duration-300 hover:scale-105">
</a>
</div>
<!-- Desktop Navigation Links -->
<div class="hidden md:flex items-center space-x-8">
{{ range .Site.Menus.main }}
<a href="{{ .URL | relLangURL }}"
class="text-gray-700 dark:text-gray-100 hover:text-pizza-red transition-colors duration-300">
{{ .Name }}
</a>
{{ end }}
</div>
<!-- Language Switcher - Desktop -->
<div class="hidden md:flex items-center space-x-4">
{{ if eq .Site.Language.Lang "de" }}
<a href="{{ relLangURL "en" }}"
class="text-gray-700 dark:text-gray-100 hover:text-pizza-red transition-colors duration-300">
English
</a>
{{ else }}
<a href="/"
class="text-gray-700 dark:text-gray-100 hover:text-pizza-red transition-colors duration-300">
Deutsch
</a>
{{ end }}
</div>
<!-- Dark mode toggle - Desktop -->
<div class="hidden md:flex items-center">
<button id="darkModeToggle"
class="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 focus:outline-none">
<!-- Sun icon -->
<svg class="h-6 w-6 text-gray-700 dark:text-gray-200 hidden dark:block"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
<!-- Moon icon -->
<svg class="h-6 w-6 text-gray-700 dark:text-gray-200 block dark:hidden"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
</button>
</div>
<!-- Mobile Controls -->
<div class="md:hidden flex items-center space-x-3">
<!-- Dark mode toggle - Mobile -->
<button id="darkModeToggle"
onclick="toggleTheme()"
class="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 focus:outline-none">
<!-- Sun icon -->
<svg class="h-6 w-6 text-gray-700 dark:text-gray-200 hidden dark:block"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
<!-- Moon icon -->
<svg class="h-6 w-6 text-gray-700 dark:text-gray-200 block dark:hidden"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
</button>
<!-- Mobile Menu Button -->
<button type="button"
id="mobile-menu-button"
class="text-gray-700 dark:text-gray-100 hover:text-pizza-red p-2">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
</svg>
</button>
</div>
</div>
<!-- Mobile Menu -->
<div id="mobile-menu" class="md:hidden hidden">
<div class="px-2 pt-2 pb-3 space-y-1 bg-white dark:bg-pizza-darker border-t border-gray-200 dark:border-gray-700">
{{ range .Site.Menus.main }}
<a href="{{ .URL | relLangURL }}"
class="block px-3 py-2.5 text-base font-medium text-gray-700 dark:text-gray-100 hover:text-pizza-red transition-colors duration-300">
{{ .Name }}
</a>
{{ end }}
<!-- Language Options in Mobile Menu -->
<div class="border-t border-gray-200 dark:border-gray-700 mt-2 pt-2">
{{ if eq .Site.Language.Lang "de" }}
<a href="{{ relLangURL "en" }}"
class="block px-3 py-2.5 text-base font-medium text-gray-700 dark:text-gray-100 hover:text-pizza-red transition-colors duration-300">
English
</a>
{{ else }}
<a href="/"
class="block px-3 py-2.5 text-base font-medium text-gray-700 dark:text-gray-100 hover:text-pizza-red transition-colors duration-300">
Deutsch
</a>
{{ end }}
</div>
</div>
</div>
</div>
</nav>
</header>
<!-- Mobile Menu JavaScript -->
<script>
document.getElementById('mobile-menu-button').addEventListener('click', function() {
const mobileMenu = document.getElementById('mobile-menu');
mobileMenu.classList.toggle('hidden');
});
</script>

View file

@ -0,0 +1,5 @@
{{ if .Site.Params.logo }}
<img src="{{ .Site.Params.logo | relURL }}" alt="{{ .Site.Title }}" class="logo">
{{ else }}
<span class="logo-text">{{ .Site.Title }}</span>
{{ end }}

View file

@ -0,0 +1,180 @@
<!-- Menu Section -->
<div id="menu-section" class="mt-8 sm:mt-16">
<h2 class="text-2xl sm:text-3xl font-bold mb-4 sm:mb-8">{{ i18n "menu" }}</h2>
<!-- Mobile Horizontal Scroll -->
<div class="md:hidden overflow-x-auto whitespace-nowrap pb-2 mb-4 scrollbar-hide">
<div class="inline-flex gap-2">
{{ $categories := .Site.Data.menu.categories }}
{{ $sortedCategories := sort $categories "sort_order" "asc" }}
{{ range $sortedCategories }}
<button
data-tab="{{ .id }}"
class="tab-button inline-block px-3 py-1.5 text-sm rounded-full transition-colors duration-200 whitespace-nowrap
{{ if eq .id "pizzas" }}
bg-pizza-red text-white
{{ else }}
bg-gray-200 dark:bg-pizza-darker text-gray-700 dark:text-white hover:bg-gray-300 dark:hover:bg-pizza-dark
{{ end }}">
{{ .name }}
</button>
{{ end }}
</div>
</div>
<!-- Desktop Menu Tabs -->
<div class="hidden md:flex flex-wrap gap-4 mb-8">
{{ $categories := .Site.Data.menu.categories }}
{{ $sortedCategories := sort $categories "sort_order" "asc" }}
{{ range $sortedCategories }}
<button
data-tab="{{ .id }}"
class="tab-button px-8 py-2 text-base rounded-full transition-colors duration-200
{{ if eq .id "pizzas" }}
bg-pizza-red text-white
{{ else }}
bg-gray-200 dark:bg-pizza-darker text-gray-700 dark:text-white hover:bg-gray-300 dark:hover:bg-pizza-dark
{{ end }}">
{{ .name }}
</button>
{{ end }}
</div>
<!-- Price Legend -->
<div class="legend-container fixed bottom-4 right-4 z-50 group">
<!-- The actual legend -->
<div class="price-legend bg-white/90 dark:bg-pizza-dark/90 backdrop-blur-sm shadow-lg rounded-lg p-4
transition-all duration-300 group-hover:opacity-0 group-hover:translate-y-1">
<div class="flex flex-col space-y-3">
<div class="flex items-center">
<span class="w-3 h-3 bg-status-green rounded-full mr-2 animate-pulse"></span>
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Abholung</span>
</div>
<div class="flex items-center">
<span class="w-3 h-3 bg-status-red rounded-full mr-2 animate-pulse"></span>
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Lieferung</span>
</div>
</div>
</div>
<!-- Info icon -->
<div class="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100
transition-all duration-300 group-hover:-translate-y-1";
title="Zeigt die Preise für Abholung und Lieferung">
<svg class="w-6 h-6 text-gray-600 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
</div>
<!-- Menu Content -->
{{ $categories := .Site.Data.menu.categories }}
{{ $sortedCategories := sort $categories "sort_order" "asc" }}
{{ range $sortedCategories }}
<div id="{{ .id }}-content" class="menu-content {{ if ne .id "pizzas" }}hidden{{ end }} space-y-4 sm:space-y-6">
{{ if eq .id "pizzas" }}
{{ range .price_groups }}
<div class="bg-gray-100 dark:bg-pizza-darker rounded-xl p-6">
<div class="flex justify-between items-center mb-8 cursor-pointer price-group-header">
<h3 class="text-xl font-bold">{{ .name }}</h3>
<svg class="w-6 h-6 transform transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</div>
<div class="price-group-content hidden">
<!-- Size Options -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
{{ range .prices }}
<div class="text-center">
<h4 class="font-medium mb-2">{{ .size }}</h4>
<div class="flex justify-center space-x-4">
<span class="text-emerald-400">{{ printf "%.2f" .pickup }}€</span>
<span class="text-pizza-red">/</span>
<span class="text-pizza-red">{{ printf "%.2f" .delivery }}€</span>
</div>
</div>
{{ end }}
</div>
<!-- Menu Items -->
<div class="space-y-4 mt-6 border-t border-gray-200 dark:border-pizza-dark pt-6">
{{ range .items }}
<div class="flex justify-between">
<div>
<h4 class="font-medium">{{ .name }}</h4>
<p class="text-gray-600 dark:text-pizza-gray text-sm">{{ .description }}</p>
</div>
</div>
{{ end }}
</div>
</div>
</div>
{{ end }}
{{ else if eq .id "menus" }}
{{ range .price_groups }}
<div class="bg-gray-100 dark:bg-pizza-darker rounded-xl p-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-bold">{{ .name }}</h3>
</div>
<!-- Menu Description -->
<p class="text-gray-600 dark:text-pizza-gray mb-4">{{ .description }}</p>
<!-- Included Items -->
<ul class="list-disc list-inside text-gray-600 dark:text-pizza-gray mb-6 space-y-2">
{{ range .included }}
<li>{{ . }}</li>
{{ end }}
</ul>
<!-- Price Options -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-4 border-t border-gray-200 dark:border-pizza-dark pt-4">
{{ range .prices }}
<div class="flex justify-between items-center py-2 border-b border-gray-200 dark:border-pizza-dark last:border-0">
<span class="font-medium">{{ .base }}</span>
<div class="font-medium">
{{ if and (isset . "pickup") (isset . "delivery") }}
<span class="text-status-green">{{ printf "%.2f" .pickup }}€</span>
<span class="text-gray-400 mx-1">/</span>
<span class="text-status-red">{{ printf "%.2f" .delivery }}€</span>
{{ else }}
<span class="text-gray-600 dark:text-gray-400">{{ printf "%.2f" .price }}€</span>
{{ end }}
</div>
</div>
{{ end }}
</div>
</div>
{{ end }}
{{ else }}
<div class="bg-gray-100 dark:bg-pizza-darker rounded-xl p-6">
<div class="space-y-4">
{{ range .items }}
<div class="flex justify-between items-center">
<div>
<h4 class="font-medium">{{ .name }}</h4>
<p class="text-gray-600 dark:text-pizza-gray text-sm">{{ .description }}</p>
</div>
<div class="font-medium">
{{ if and (isset . "pickup") (isset . "delivery") }}
<span class="text-status-green">{{ printf "%.2f" .pickup }}€</span>
<span class="text-gray-400 mx-1">/</span>
<span class="text-status-red">{{ printf "%.2f" .delivery }}€</span>
{{ else if and (isset . "price_small") (isset . "price_large") }}
<span class="text-gray-600 dark:text-gray-400 text-sm">{{ i18n "portion_small"}}: {{ printf "%.2f" .price_small }}€</span>
<span class="text-gray-600 mx-1">/</span>
<span class="text-gray-600 dark:text-gray-400 text-base">{{ i18n "portion_large"}}: {{ printf "%.2f" .price_large }}€</span>
{{ else }}
<span class="text-gray-600 dark:text-gray-400">{{ printf "%.2f" .price }}€</span>
{{ end }}
</div>
</div>
{{ end }}
</div>
</div>
{{ end }}
</div>
{{ end }}
</div>

View file

@ -0,0 +1,83 @@
{{ $now := now }}
{{ $currentDay := $now.Format "Monday" }}
{{ $currentTime := $now.Format "15:04" }}
<div class="opening-hours bg-white/5 dark:bg-pizza-darker/5 backdrop-blur-sm rounded-xl p-4 sm:p-6 mx-0 sm:mx-0">
<h3 class="text-2xl font-bold mb-6 sm:mb-8 text-gray-900 dark:text-white">{{ i18n "opening_hours" }}</h3>
<!-- Regular Hours -->
<div class="space-y-2">
{{ range .Site.Data.hours.regular_hours }}
{{ $isOpen := and (in .days $currentDay) (ge $currentTime .open) (le $currentTime .close) }}
<div class="hours-row {{ if $isOpen }}border-2 border-status-green{{ end }}
bg-white dark:bg-pizza-darker rounded-xl p-4 transition-all duration-300"
data-day="{{ $currentDay }}"
data-open="{{ .open }}"
data-close="{{ .close }}"
data-delivery-until="{{ .delivery_until }}">
<div class="flex justify-around items-center">
<div class="days-group">
{{ $numDays := len .days }}
{{ if eq $numDays 1 }}
<span class="text-lg font-medium text-gray-800 dark:text-gray-200">{{ index .days 0 }}</span>
{{ else if eq $numDays 2 }}
<span class="text-lg font-medium text-gray-800 dark:text-gray-200">{{ i18n (index .days 0) }} & {{ i18n (index .days 1) }}</span>
{{ else }}
{{ $lastIndex := sub $numDays 1 }}
{{ range $i, $day := .days }}
{{ if eq $i 0 }}
<span class="text-lg font-medium text-gray-800 dark:text-gray-200">{{ i18n $day }}</span>
{{ else if eq $i $lastIndex }}
<span class="text-lg font-medium text-gray-800 dark:text-gray-200"> - {{ i18n $day }}</span>
{{ end }}
{{ end }}
{{ end }}
</div>
<div class="text-right">
<div class="text-lg font-medium {{ if $isOpen }}text-status-green{{ else }}text-gray-700 dark:text-gray-300{{ end }}">
{{ .open }} - {{ .close }}
</div>
<div class="delivery-text {{ if $isOpen }}text-status-green{{ else }}text-status-red{{ end }}">
{{ i18n "delivery_until" }} {{ .close }}
</div>
</div>
</div>
<!-- Phone number - only shown when this time slot is open -->
{{ if $isOpen }}
<div class="mt-3 pt-3 border-t border-gray-100 dark:border-gray-700">
<a href="tel:{{ $.Site.Params.contact.phone }}"
class="flex items-center text-status-green hover:text-status-green/80 transition-colors duration-300">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"/>
</svg>
<span class="font-medium"> {{ i18n "call_now" }}: {{ $.Site.Params.contact.phone }}</span>
</a>
</div>
{{ end }}
</div>
{{ end }}
</div>
<!-- Holiday Notice -->
{{ range .Site.Data.hours.holidays }}
{{ $holidayDate := time .date }}
{{ if eq ($now.Format "2006-01-02") ($holidayDate.Format "2006-01-02") }}
<div class="mt-6 p-4 bg-pizza-red/10 dark:bg-pizza-red/20 rounded-xl border border-pizza-red/20">
<div class="font-medium text-pizza-red">
{{ .name }}:
{{ if .closed }}
<span class="ml-2">{{ i18n "closed" }}</span>
{{ else }}
<span class="ml-2">{{ .open }} - {{ .close }} Uhr</span>
<div class="text-sm">
{{ i18n "delivery_until" }} {{ .delivery_until }} Uhr
</div>
{{ end }}
</div>
</div>
{{ end }}
{{ end }}
</div>

View file

@ -0,0 +1,5 @@
<div class="price-header">
<span>{{ i18n "size" }}</span>
<span>{{ i18n "pickup" }}</span>
<span>{{ i18n "delivery" }}</span>
</div>

1760
static/css/style.css Normal file

File diff suppressed because it is too large Load diff

5
static/icons/de.svg Normal file
View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 5 3">
<rect width="5" height="3" y="0" x="0" fill="#000"/>
<rect width="5" height="2" y="1" x="0" fill="#D00"/>
<rect width="5" height="1" y="2" x="0" fill="#FFCE00"/>
</svg>

After

Width:  |  Height:  |  Size: 234 B

11
static/icons/gb.svg Normal file
View file

@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 30">
<clipPath id="a"><path d="M0 0v30h60V0z"/></clipPath>
<clipPath id="b"><path d="M30 15h30v15zv15H0zH0V0zV0h30z"/></clipPath>
<g clip-path="url(#a)">
<path d="M0 0v30h60V0z" fill="#012169"/>
<path d="M0 0l60 30m0-30L0 30" stroke="#fff" stroke-width="6"/>
<path d="M0 0l60 30m0-30L0 30" clip-path="url(#b)" stroke="#C8102E" stroke-width="4"/>
<path d="M30 0v30M0 15h60" stroke="#fff" stroke-width="10"/>
<path d="M30 0v30M0 15h60" stroke="#C8102E" stroke-width="6"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 566 B

BIN
static/images/logo.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

BIN
static/images/pizza.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 576 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

38
static/js/header.js Normal file
View file

@ -0,0 +1,38 @@
document.addEventListener('DOMContentLoaded', function() {
const header = document.querySelector('.header');
const navToggle = document.getElementById('nav-toggle');
const navMenu = document.getElementById('nav-menu');
// Scroll behavior
let lastScroll = 0;
window.addEventListener('scroll', () => {
const currentScroll = window.pageYOffset;
if (currentScroll <= 0) {
header.classList.remove('scrolled');
return;
}
if (currentScroll > lastScroll && !header.contains(document.activeElement)) {
header.style.transform = 'translateY(-100%)';
} else {
header.style.transform = 'translateY(0)';
header.classList.add('scrolled');
}
lastScroll = currentScroll;
});
// Close mobile menu when clicking outside
document.addEventListener('click', (e) => {
if (!header.contains(e.target)) {
navToggle.checked = false;
}
});
// Close mobile menu when window is resized to desktop
window.addEventListener('resize', () => {
if (window.innerWidth >= 1024) {
navToggle.checked = false;
}
});
});

23
static/js/language.js Normal file
View file

@ -0,0 +1,23 @@
document.addEventListener('DOMContentLoaded', function() {
// Only run on the home page to avoid redirect loops
if (window.location.pathname === '/') {
const userLang = navigator.language || navigator.userLanguage;
const currentLang = document.documentElement.lang;
// Simple language mapping
const supportedLanguages = {
'de': '/de/',
'de-DE': '/de/',
'de-AT': '/de/',
'de-CH': '/de/',
'en': '/en/',
'en-US': '/en/',
'en-GB': '/en/'
};
// Check if user's language is different from current and is supported
if (supportedLanguages[userLang] && currentLang !== userLang.substring(0,2)) {
window.location.href = supportedLanguages[userLang];
}
}
});

274
static/js/main.js Normal file
View file

@ -0,0 +1,274 @@
document.addEventListener('DOMContentLoaded', function() {
// Mobile menu toggle
const mobileMenuButton = document.querySelector('.mobile-menu-button');
const mobileMenu = document.querySelector('.mobile-menu');
mobileMenuButton.addEventListener('click', () => {
mobileMenu.classList.toggle('hidden');
// Update button state
mobileMenuButton.setAttribute('aria-expanded', !isExpanded);
// Toggle icons
menuIconOpen.classList.toggle('hidden');
menuIconClosed.classList.toggle('hidden');
});
// Close mobile menu when clicking outside
document.addEventListener('click', (e) => {
if (!mobileMenuButton.contains(e.target) && !mobileMenu.contains(e.target)) {
mobileMenu.classList.add('hidden');
mobileMenuButton.setAttribute('aria-expanded', 'false');
menuIconOpen.classList.remove('hidden');
menuIconClosed.classList.add('hidden');
}
});
// Theme toggle functionality
function updateTheme(isDark) {
const html = document.documentElement;
if (isDark) {
html.classList.add('dark');
} else {
html.classList.remove('dark');
}
localStorage.setItem('theme', isDark ? 'dark' : 'light');
}
// Desktop theme toggle
const darkModeToggle = document.getElementById('darkModeToggle');
darkModeToggle.addEventListener('click', () => {
const isDark = document.documentElement.classList.contains('dark');
updateTheme(!isDark);
});
// Mobile theme toggle
const darkModeToggleMobile = document.getElementById('darkModeToggleMobile');
darkModeToggleMobile.addEventListener('click', () => {
const isDark = document.documentElement.classList.contains('dark');
updateTheme(!isDark);
});
// Initialize theme
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
updateTheme(savedTheme === 'dark');
} else {
// Default to dark theme
updateTheme(true);
}
// Header scroll behavior
const header = document.querySelector('header');
let lastScroll = 0;
window.addEventListener('scroll', () => {
const currentScroll = window.pageYOffset;
if (currentScroll <= 0) {
header.classList.remove('shadow-lg', 'bg-white/95', 'dark:bg-gray-900/95');
header.style.transform = 'translateY(0)';
return;
}
if (currentScroll > lastScroll && !header.contains(document.activeElement)) {
// Scrolling down
header.style.transform = 'translateY(-100%)';
} else {
// Scrolling up
header.style.transform = 'translateY(0)';
header.classList.add('shadow-lg', 'bg-white/95', 'dark:bg-gray-900/95');
}
lastScroll = currentScroll;
});
// Menu tab functionality
const tabButtons = document.querySelectorAll('.tab-button');
const menuContents = document.querySelectorAll('.menu-content');
tabButtons.forEach(button => {
button.addEventListener('click', () => {
// Remove active state from all buttons
tabButtons.forEach(btn => {
btn.classList.remove('bg-pizza-red', 'text-white');
btn.classList.add('bg-gray-200', 'dark:bg-pizza-darker', 'text-gray-700', 'dark:text-white');
});
// Add active state to clicked button
button.classList.remove('bg-gray-200', 'dark:bg-pizza-darker', 'text-gray-700', 'dark:text-white');
button.classList.add('bg-pizza-red', 'text-white');
// Hide all content sections
menuContents.forEach(content => {
content.classList.add('hidden');
});
// Show selected content
const targetContent = document.getElementById(`${button.dataset.tab}-content`);
targetContent.classList.remove('hidden');
});
});
// Price group toggles
const priceGroups = document.querySelectorAll('.price-group-header');
priceGroups.forEach(group => {
group.addEventListener('click', () => {
const content = group.nextElementSibling;
const arrow = group.querySelector('svg');
// Toggle content visibility
content.classList.toggle('hidden');
// Rotate arrow
if (content.classList.contains('hidden')) {
arrow.style.transform = 'rotate(0deg)';
} else {
arrow.style.transform = 'rotate(180deg)';
}
});
});
// Auto-expand first price group
const firstPriceGroup = document.querySelector('.price-group-header');
if (firstPriceGroup) {
firstPriceGroup.click();
}
// Legend hover effect
const legendContainer = document.querySelector('.legend-container');
const legend = legendContainer.querySelector('.price-legend');
legendContainer.addEventListener('mouseenter', () => {
legend.style.opacity = '0';
});
legendContainer.addEventListener('mouseleave', () => {
legend.style.opacity = '1';
});
// Smooth scroll for menu links
const menuLinks = document.querySelectorAll('a[href*="#menu-section"]');
menuLinks.forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
console.log('Menu link clicked'); // Debug log
const menuSection = document.getElementById('menu-section');
if (menuSection) {
const headerHeight = document.querySelector('header').offsetHeight;
const menuPosition = menuSection.offsetTop - headerHeight;
window.scrollTo({
top: menuPosition,
behavior: 'smooth'
});
} else {
console.error('Menu section not found'); // Debug log
}
});
});
// Language dropdown functionality
const languageButton = document.getElementById('languageButton');
const languageDropdown = document.getElementById('languageDropdown');
if (languageButton && languageDropdown) {
// Toggle dropdown
languageButton.addEventListener('click', (e) => {
e.stopPropagation();
languageDropdown.classList.toggle('hidden');
});
// Close dropdown when clicking outside
document.addEventListener('click', (e) => {
if (!languageButton.contains(e.target) && !languageDropdown.contains(e.target)) {
languageDropdown.classList.add('hidden');
}
});
// Close dropdown when clicking a language option
languageDropdown.querySelectorAll('a').forEach(link => {
link.addEventListener('click', () => {
languageDropdown.classList.add('hidden');
});
});
}
// Auto-detect browser language
function detectLanguage() {
const browserLang = navigator.language.split('-')[0];
const supportedLangs = ['de', 'en']; // Add your supported languages here
if (!localStorage.getItem('userLanguage')) {
const detectedLang = supportedLangs.includes(browserLang) ? browserLang : 'de';
localStorage.setItem('userLanguage', detectedLang);
// Redirect if needed
const currentLang = document.documentElement.lang;
if (currentLang !== detectedLang) {
const newPath = window.location.pathname.replace(`/${currentLang}/`, `/${detectedLang}/`);
window.location.href = newPath;
}
}
}
detectLanguage();
function checkOpenStatus() {
const now = new Date();
const currentTime = now.toLocaleTimeString('de-DE', {
hour: '2-digit',
minute: '2-digit',
hour12: false
});
const currentDay = now.toLocaleDateString('de-DE', { weekday: 'long' });
// Get opening hours from data attributes
const hoursRows = document.querySelectorAll('.hours-row');
let isOpen = false;
hoursRows.forEach(row => {
const rowDay = row.dataset.day;
const openTime = row.dataset.open;
const closeTime = row.dataset.close;
// Convert times to comparable numbers (e.g., "14:30" -> 1430)
const currentNum = parseInt(currentTime.replace(':', ''));
const openNum = parseInt(openTime.replace(':', ''));
const closeNum = parseInt(closeTime.replace(':', ''));
if (rowDay === currentDay && currentNum >= openNum && currentNum <= closeNum) {
isOpen = true;
row.classList.add('border-2', 'border-status-green');
} else {
row.classList.remove('border-2', 'border-status-green');
}
});
// Update status bar
const statusBar = document.getElementById('status-bar');
const statusText = document.getElementById('status-text');
if (statusBar && statusText) {
if (isOpen) {
statusBar.classList.remove('bg-status-red');
statusBar.classList.add('bg-status-green');
statusText.textContent = statusText.dataset.textOpen;
} else {
statusBar.classList.remove('bg-status-green');
statusBar.classList.add('bg-status-red');
statusText.textContent = statusText.dataset.textClosed;
}
}
return { isOpen, currentDay, currentTime };
}
// Update status every minute
setInterval(checkOpenStatus, 60000);
// Initial check
checkOpenStatus();
});

29
static/js/menu.js Normal file
View file

@ -0,0 +1,29 @@
// Menu category handling
document.addEventListener('DOMContentLoaded', function() {
// Handle tab switching
const allTabButtons = document.querySelectorAll('[data-tab]');
allTabButtons.forEach(button => {
button.addEventListener('click', function() {
const tabId = this.getAttribute('data-tab');
// Hide all content
document.querySelectorAll('.menu-content').forEach(content => {
content.classList.add('hidden');
});
// Show selected content
document.getElementById(tabId + '-content').classList.remove('hidden');
// Update active states for all buttons with same data-tab
allTabButtons.forEach(btn => {
if (btn.getAttribute('data-tab') === tabId) {
btn.classList.add('bg-pizza-red', 'text-white');
btn.classList.remove('bg-gray-200', 'dark:bg-pizza-darker', 'text-gray-700');
} else {
btn.classList.remove('bg-pizza-red', 'text-white');
btn.classList.add('bg-gray-200', 'dark:bg-pizza-darker', 'text-gray-700');
}
});
});
});
});

View file

@ -0,0 +1,49 @@
function updateTime() {
const now = new Date();
const currentTime = now.toLocaleTimeString('de-DE', {
hour: '2-digit',
minute: '2-digit',
hour12: false
});
// Update displayed time
const timeDisplay = document.getElementById('current-time');
if (timeDisplay) {
timeDisplay.textContent = currentTime;
}
// Check opening status
fetch('/api/check-open-status')
.then(response => response.json())
.then(status => {
// Update status bar
const statusBar = document.getElementById('status-bar');
const statusText = document.getElementById('status-text');
if (statusBar) {
statusBar.className = status.isOpen
? 'bg-status-green transition-colors duration-300'
: 'bg-status-red transition-colors duration-300';
}
if (statusText) {
const textOpen = statusText.dataset.textOpen;
const textClosed = statusText.dataset.textClosed;
statusText.textContent = status.isOpen ? textOpen : textClosed;
}
// Update opening hours rows
document.querySelectorAll('.hours-row').forEach(row => {
const open = row.dataset.open;
const close = row.dataset.close;
const isCurrentTimeSlot = currentTime >= open && currentTime <= close;
row.classList.toggle('border-2', isCurrentTimeSlot && status.isOpen);
row.classList.toggle('border-status-green', isCurrentTimeSlot && status.isOpen);
});
});
}
// Update immediately and then every minute
updateTime();
setInterval(updateTime, 60000);

42
static/js/theme.js Normal file
View file

@ -0,0 +1,42 @@
// Theme handling
function getPreferredTheme() {
if (localStorage.getItem('theme') === 'dark' ||
(!localStorage.getItem('theme') && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
return 'dark';
}
return 'light';
}
function setTheme(theme) {
if (theme === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
localStorage.setItem('theme', theme);
}
function toggleTheme() {
const isDark = document.documentElement.classList.contains('dark');
setTheme(isDark ? 'light' : 'dark');
}
// Initialize theme
if (getPreferredTheme() === 'dark') {
setTheme('dark');
}
// Add click event listener
document.addEventListener('DOMContentLoaded', () => {
const darkModeToggle = document.getElementById('darkModeToggle');
if (darkModeToggle) {
darkModeToggle.addEventListener('click', toggleTheme);
}
});
// Watch for system theme changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
if (!localStorage.getItem('theme')) {
setTheme(e.matches ? 'dark' : 'light');
}
});