feat: Graffen Foodtruck Website – Initial Release
SvelteKit Single-Page Website für den rumänischen Foodtruck "Graffen": - Hero mit Vanta.js Fog-Effekt und ornamentiertem Scroll-Button - Speisekarte mit Sommer-/Winterkarte (JSON-basiert), Allergenen, Vegetarisch/Vegan-Kennzeichnung - Catering-Sektion mit Anlass-Karten und CTAs - Geschichte-Sektion mit Brand-Story - Kontakt mit Instagram/E-Mail - Ornamentierte Buttons und Modal im mittelalterlichen Stil - Responsive Design mit warmem Sepia/Gold-Farbschema - Lokale Fonts (Inter + UnifrakturMaguntia via @fontsource) - Static Adapter für einfaches Hosting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
0a2f79ee5e
26 changed files with 3639 additions and 0 deletions
43
.gitignore
vendored
Normal file
43
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# Dependencies
|
||||
node_modules
|
||||
|
||||
# IDE
|
||||
.idea
|
||||
.vscode
|
||||
|
||||
# Dotfiles (symlinked from home)
|
||||
.bash_profile
|
||||
.bashrc
|
||||
.zshrc
|
||||
.zprofile
|
||||
.profile
|
||||
.gitconfig
|
||||
.gitmodules
|
||||
.npmrc
|
||||
.ripgreprc
|
||||
.mcp.json
|
||||
|
||||
# Claude
|
||||
.claude
|
||||
|
||||
# Output
|
||||
.output
|
||||
.vercel
|
||||
.netlify
|
||||
.wrangler
|
||||
/.svelte-kit
|
||||
/build
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Env
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.test
|
||||
|
||||
# Vite
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
42
README.md
Normal file
42
README.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# sv
|
||||
|
||||
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
|
||||
|
||||
## Creating a project
|
||||
|
||||
If you're seeing this, you've probably already done this step. Congrats!
|
||||
|
||||
```sh
|
||||
# create a new project
|
||||
npx sv create my-app
|
||||
```
|
||||
|
||||
To recreate this project with the same configuration:
|
||||
|
||||
```sh
|
||||
# recreate this project
|
||||
npx sv@0.12.8 create --template minimal --types ts --no-install .
|
||||
```
|
||||
|
||||
## Developing
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
|
||||
# or start the server and open the app in a new browser tab
|
||||
npm run dev -- --open
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To create a production version of your app:
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
|
||||
You can preview the production build with `npm run preview`.
|
||||
|
||||
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
|
||||
2312
package-lock.json
generated
Normal file
2312
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
32
package.json
Normal file
32
package.json
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"name": "graffen-foodtruck",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"prepare": "svelte-kit sync || echo ''",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-static": "^3.0.0",
|
||||
"@sveltejs/kit": "^2.50.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"svelte": "^5.51.0",
|
||||
"svelte-check": "^4.4.2",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
"@fontsource/unifrakturmaguntia": "^5.2.8",
|
||||
"lucide-svelte": "^1.0.1",
|
||||
"three": "^0.183.2",
|
||||
"vanta": "^0.5.24"
|
||||
}
|
||||
}
|
||||
32
src/app.css
Normal file
32
src/app.css
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
@import '@fontsource/inter/300.css';
|
||||
@import '@fontsource/inter/400.css';
|
||||
@import '@fontsource/inter/500.css';
|
||||
@import '@fontsource/inter/600.css';
|
||||
@import '@fontsource/inter/700.css';
|
||||
@import '@fontsource/unifrakturmaguntia/400.css';
|
||||
@import 'tailwindcss';
|
||||
|
||||
@theme {
|
||||
--color-parchment: #f5e6c8;
|
||||
--color-gold: #c9a84c;
|
||||
--color-sepia: #8b6914;
|
||||
--color-brown: #4a3728;
|
||||
--color-charcoal: #2a1f14;
|
||||
--color-forest: #3d4a2e;
|
||||
--color-cream: #faf3e3;
|
||||
|
||||
--font-gothic: 'UnifrakturMaguntia', serif;
|
||||
--font-sans: 'Inter', system-ui, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
color: var(--color-brown);
|
||||
background-color: var(--color-parchment);
|
||||
}
|
||||
|
||||
button,
|
||||
[role='button'],
|
||||
a {
|
||||
cursor: pointer;
|
||||
}
|
||||
15
src/app.d.ts
vendored
Normal file
15
src/app.d.ts
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'vanta/dist/vanta.fog.min';
|
||||
|
||||
export {};
|
||||
11
src/app.html
Normal file
11
src/app.html
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<!doctype html>
|
||||
<html lang="de" class="scroll-smooth">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
39
src/lib/components/About.svelte
Normal file
39
src/lib/components/About.svelte
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<section id="geschichte" class="bg-charcoal py-20 text-cream">
|
||||
<div class="mx-auto max-w-4xl px-6">
|
||||
<h2 class="font-gothic mb-12 text-center text-5xl text-gold md:text-6xl">
|
||||
Unsere Geschichte
|
||||
</h2>
|
||||
|
||||
<div class="space-y-6 text-lg leading-relaxed text-cream/85">
|
||||
<p>
|
||||
<strong class="text-gold">Graffen</strong> – das ist rumänische Seele auf vier Rädern.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Aufgewachsen mit den Rezepten unserer Bunică aus Siebenbürgen, bringen wir die Aromen
|
||||
Transsilvaniens auf die Straßen Deutschlands. Unsere Gerichte erzählen Geschichten von
|
||||
nebelverhangenen Karpaten, von Bauernhöfen und von einer Küche, in der es immer nach frisch
|
||||
gebackenem Brot und Kräutersuppe duftete.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Jedes Gericht wird frisch zubereitet – mit ehrlichen Zutaten, ohne Abkürzungen. Genau
|
||||
so, wie es Bunică getan hat. Wir kochen nicht für Trends, sondern für den Geschmack, der
|
||||
bleibt.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Der Name <em class="text-gold">„Graffen“</em> verbindet zwei Welten: den
|
||||
deutschen Adel mit der Mystik Transsilvaniens – eine Hommage an die Grafen, die einst
|
||||
in den Burgen Siebenbürgens residierten.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Decorative divider -->
|
||||
<div class="mt-12 flex items-center justify-center gap-4">
|
||||
<div class="h-px w-16 bg-gold/40"></div>
|
||||
<span class="font-gothic text-2xl text-gold/60">✦</span>
|
||||
<div class="h-px w-16 bg-gold/40"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
94
src/lib/components/Catering.svelte
Normal file
94
src/lib/components/Catering.svelte
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
<script lang="ts">
|
||||
import OrnateButton from './OrnateButton.svelte';
|
||||
</script>
|
||||
|
||||
<section id="catering" class="bg-brown py-20 text-cream">
|
||||
<div class="mx-auto max-w-4xl px-6">
|
||||
<h2 class="font-gothic mb-4 text-center text-5xl text-gold md:text-6xl">Catering</h2>
|
||||
<p class="mx-auto mb-14 max-w-2xl text-center text-lg text-cream/80">
|
||||
Euer Event, unser Foodtruck – für unvergessliche Momente mit authentischer rumänischer
|
||||
Küche.
|
||||
</p>
|
||||
|
||||
<!-- Anlässe Grid -->
|
||||
<div class="mb-14 grid gap-6 md:grid-cols-3">
|
||||
<div class="rounded-lg border border-gold/20 bg-cream/5 p-6 text-center">
|
||||
<div class="mb-4 text-3xl">🍺</div>
|
||||
<h3 class="mb-2 text-lg font-semibold text-gold">Hochzeiten</h3>
|
||||
<p class="text-sm text-cream/70">
|
||||
Macht euren großen Tag besonders – mit rumänischen Spezialitäten frisch vom Grill.
|
||||
Rustikales Flair trifft auf Festlichkeit.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-gold/20 bg-cream/5 p-6 text-center">
|
||||
<div class="mb-4 text-3xl">🏢</div>
|
||||
<h3 class="mb-2 text-lg font-semibold text-gold">Firmenfeiern</h3>
|
||||
<p class="text-sm text-cream/70">
|
||||
Sommerfest, Jubiläum oder Teambuilding? Wir sorgen für zufriedene Bäuche und gute
|
||||
Stimmung bei euren Mitarbeitern.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-gold/20 bg-cream/5 p-6 text-center">
|
||||
<div class="mb-4 text-3xl">🎇</div>
|
||||
<h3 class="mb-2 text-lg font-semibold text-gold">Private Feiern</h3>
|
||||
<p class="text-sm text-cream/70">
|
||||
Geburtstag, Gartenparty oder Familientreffen – wir kommen mit dem Foodtruck direkt
|
||||
zu euch und kümmern uns ums Essen.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- USPs -->
|
||||
<div class="mb-14 rounded-lg border border-gold/30 bg-gold/5 p-8">
|
||||
<h3 class="mb-6 text-center text-xl font-semibold text-gold">Was wir mitbringen</h3>
|
||||
<div class="grid gap-4 text-sm text-cream/80 md:grid-cols-2">
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="mt-0.5 text-gold">✓</span>
|
||||
<span>Unser voll ausgestatteter Foodtruck – autark und flexibel</span>
|
||||
</div>
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="mt-0.5 text-gold">✓</span>
|
||||
<span>Frische Zubereitung vor Ort – euer Event wird zum Erlebnis</span>
|
||||
</div>
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="mt-0.5 text-gold">✓</span>
|
||||
<span>Individuelle Menüplanung abgestimmt auf eure Gästezahl</span>
|
||||
</div>
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="mt-0.5 text-gold">✓</span>
|
||||
<span>Flexible Anpassung an Allergien und Ernährungswünsche</span>
|
||||
</div>
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="mt-0.5 text-gold">✓</span>
|
||||
<span>Events ab 30 Personen – von intim bis groß</span>
|
||||
</div>
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="mt-0.5 text-gold">✓</span>
|
||||
<span>Einsatzbereit in der gesamten Region</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CTA -->
|
||||
<div class="text-center">
|
||||
<p class="mb-8 text-cream/70">
|
||||
Erzählt uns von eurem Event – wir erstellen euch ein unverbindliches Angebot.
|
||||
</p>
|
||||
<div class="flex flex-col items-center justify-center gap-6 sm:flex-row">
|
||||
<OrnateButton href="mailto:hallo@graffen-foodtruck.de?subject=Catering-Anfrage">
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
Catering anfragen
|
||||
</OrnateButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
45
src/lib/components/Contact.svelte
Normal file
45
src/lib/components/Contact.svelte
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<script lang="ts">
|
||||
import OrnateButton from './OrnateButton.svelte';
|
||||
</script>
|
||||
|
||||
<section id="kontakt" class="bg-parchment py-20">
|
||||
<div class="mx-auto max-w-4xl px-6 text-center">
|
||||
<h2 class="font-gothic mb-12 text-5xl text-brown md:text-6xl">Wo ihr uns findet</h2>
|
||||
|
||||
<p class="mb-10 text-lg text-sepia">
|
||||
Wir sind mit unserem Foodtruck auf Wochenmärkten, Street-Food-Festivals und Events in der
|
||||
Region unterwegs. <br> <br> Folgt uns auf Instagram für aktuelle Standorte und Speisekarten.
|
||||
</p>
|
||||
|
||||
<div class="flex flex-col items-center justify-center gap-6 sm:flex-row">
|
||||
<OrnateButton
|
||||
href="https://instagram.com/graffen.foodtruck"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z"
|
||||
/>
|
||||
</svg>
|
||||
@graffen.foodtruck
|
||||
</OrnateButton>
|
||||
|
||||
<OrnateButton href="mailto:hallo@graffen-foodtruck.de" variant="secondary">
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
hallo@graffen-foodtruck.de
|
||||
</OrnateButton>
|
||||
</div>
|
||||
|
||||
<p class="mt-10 text-sepia/70">
|
||||
Catering-Anfragen für private Feiern, Firmenfeste oder Events? Schreibt uns einfach!
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
13
src/lib/components/Footer.svelte
Normal file
13
src/lib/components/Footer.svelte
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<footer class="bg-charcoal py-10 text-cream/60">
|
||||
<div class="mx-auto max-w-6xl px-6 text-center">
|
||||
<p class="font-gothic mb-4 text-3xl text-gold/70">Graffen</p>
|
||||
<p class="mb-6 text-sm">Rumänische Hausmannskost aus dem Foodtruck</p>
|
||||
|
||||
<div class="mb-6 flex justify-center gap-6 text-sm">
|
||||
<a href="/impressum" class="transition-colors hover:text-gold">Impressum</a>
|
||||
<a href="/datenschutz" class="transition-colors hover:text-gold">Datenschutz</a>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-cream/40">© 2026 Graffen – Alle Rechte vorbehalten.</p>
|
||||
</div>
|
||||
</footer>
|
||||
155
src/lib/components/Hero.svelte
Normal file
155
src/lib/components/Hero.svelte
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let fogEl: HTMLDivElement;
|
||||
|
||||
onMount(async () => {
|
||||
const THREE = await import('three');
|
||||
const FOG = (await import('vanta/dist/vanta.fog.min')).default;
|
||||
|
||||
const effect = FOG({
|
||||
el: fogEl,
|
||||
THREE,
|
||||
mouseControls: false,
|
||||
touchControls: false,
|
||||
gyroControls: false,
|
||||
minHeight: 200.0,
|
||||
minWidth: 200.0,
|
||||
highlightColor: 0x8b6914,
|
||||
midtoneColor: 0x4a3728,
|
||||
lowlightColor: 0x2a1f14,
|
||||
baseColor: 0x1a1008,
|
||||
blurFactor: 0.5,
|
||||
speed: 0.8,
|
||||
zoom: 1.2
|
||||
});
|
||||
|
||||
return () => effect.destroy();
|
||||
});
|
||||
</script>
|
||||
|
||||
<section id="hero" class="relative flex h-screen items-center justify-center overflow-hidden">
|
||||
<!-- Background Image -->
|
||||
<div
|
||||
class="absolute inset-0 bg-cover bg-center"
|
||||
style="background-image: url('/graffen-hero.png');"
|
||||
></div>
|
||||
|
||||
<!-- Overlay -->
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-charcoal via-charcoal/60 to-charcoal/30"></div>
|
||||
|
||||
<!-- Vanta Fog Layer -->
|
||||
<div
|
||||
bind:this={fogEl}
|
||||
class="absolute inset-0"
|
||||
style="mix-blend-mode: screen;"
|
||||
></div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="relative z-10 text-center">
|
||||
<h1 class="font-gothic text-7xl text-gold drop-shadow-lg md:text-9xl">Graffen</h1>
|
||||
<p class="mt-4 text-lg font-light tracking-wide text-cream/90 md:text-xl">
|
||||
Rumänische Hausmannskost aus dem Foodtruck
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Ornate Scroll Indicator -->
|
||||
<div class="absolute bottom-20 z-10 flex w-full justify-center">
|
||||
<a
|
||||
href="#speisekarte"
|
||||
class="scroll-indicator relative flex h-24 w-24 items-center justify-center rounded-full bg-charcoal/40 backdrop-blur-sm"
|
||||
aria-label="Nach unten scrollen"
|
||||
>
|
||||
<svg
|
||||
class="pointer-events-none absolute inset-0 h-full w-full overflow-visible"
|
||||
viewBox="0 0 80 80"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<!-- Inner circle -->
|
||||
<circle cx="40" cy="40" r="28" stroke="var(--color-gold)" stroke-width="0.8" stroke-opacity="0.35" />
|
||||
<!-- Outer circle -->
|
||||
<circle cx="40" cy="40" r="38" stroke="var(--color-gold)" stroke-width="1" stroke-opacity="0.5" />
|
||||
|
||||
<!-- Top ornament: fleur -->
|
||||
<path d="M40 2 L40 -3" stroke="var(--color-gold)" stroke-width="1" stroke-opacity="0.6" />
|
||||
<path d="M36 1 C38 -4, 42 -4, 44 1" stroke="var(--color-gold)" stroke-width="0.8" stroke-opacity="0.5" fill="none" />
|
||||
<circle cx="40" cy="-4" r="1.5" fill="var(--color-gold)" fill-opacity="0.5" />
|
||||
|
||||
<!-- Bottom ornament: fleur -->
|
||||
<path d="M40 78 L40 83" stroke="var(--color-gold)" stroke-width="1" stroke-opacity="0.6" />
|
||||
<path d="M36 79 C38 84, 42 84, 44 79" stroke="var(--color-gold)" stroke-width="0.8" stroke-opacity="0.5" fill="none" />
|
||||
<circle cx="40" cy="84" r="1.5" fill="var(--color-gold)" fill-opacity="0.5" />
|
||||
|
||||
<!-- Left ornament -->
|
||||
<path d="M2 40 L-3 40" stroke="var(--color-gold)" stroke-width="1" stroke-opacity="0.6" />
|
||||
<path d="M1 36 C-4 38, -4 42, 1 44" stroke="var(--color-gold)" stroke-width="0.8" stroke-opacity="0.5" fill="none" />
|
||||
<circle cx="-4" cy="40" r="1.5" fill="var(--color-gold)" fill-opacity="0.5" />
|
||||
|
||||
<!-- Right ornament -->
|
||||
<path d="M78 40 L83 40" stroke="var(--color-gold)" stroke-width="1" stroke-opacity="0.6" />
|
||||
<path d="M79 36 C84 38, 84 42, 79 44" stroke="var(--color-gold)" stroke-width="0.8" stroke-opacity="0.5" fill="none" />
|
||||
<circle cx="84" cy="40" r="1.5" fill="var(--color-gold)" fill-opacity="0.5" />
|
||||
|
||||
<!-- Diagonal accents -->
|
||||
<path d="M12 12 C14 9, 9 14, 12 12" stroke="var(--color-gold)" stroke-width="0.8" stroke-opacity="0.3" fill="none" />
|
||||
<path d="M68 12 C66 9, 71 14, 68 12" stroke="var(--color-gold)" stroke-width="0.8" stroke-opacity="0.3" fill="none" />
|
||||
<path d="M12 68 C14 71, 9 66, 12 68" stroke="var(--color-gold)" stroke-width="0.8" stroke-opacity="0.3" fill="none" />
|
||||
<path d="M68 68 C66 71, 71 66, 68 68" stroke="var(--color-gold)" stroke-width="0.8" stroke-opacity="0.3" fill="none" />
|
||||
|
||||
<!-- Tiny dots between inner and outer ring -->
|
||||
<circle cx="40" cy="8" r="0.8" fill="var(--color-gold)" fill-opacity="0.3" />
|
||||
<circle cx="40" cy="72" r="0.8" fill="var(--color-gold)" fill-opacity="0.3" />
|
||||
<circle cx="8" cy="40" r="0.8" fill="var(--color-gold)" fill-opacity="0.3" />
|
||||
<circle cx="72" cy="40" r="0.8" fill="var(--color-gold)" fill-opacity="0.3" />
|
||||
<circle cx="16" cy="16" r="0.8" fill="var(--color-gold)" fill-opacity="0.25" />
|
||||
<circle cx="64" cy="16" r="0.8" fill="var(--color-gold)" fill-opacity="0.25" />
|
||||
<circle cx="16" cy="64" r="0.8" fill="var(--color-gold)" fill-opacity="0.25" />
|
||||
<circle cx="64" cy="64" r="0.8" fill="var(--color-gold)" fill-opacity="0.25" />
|
||||
</svg>
|
||||
|
||||
<!-- Arrow -->
|
||||
<svg
|
||||
class="scroll-arrow relative z-10 h-7 w-7 text-gold/80"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.5"
|
||||
d="M12 5v14m-6-6l6 6 6-6"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.scroll-indicator {
|
||||
animation: float 2.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.scroll-arrow {
|
||||
animation: arrow-pulse 2.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(8px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes arrow-pulse {
|
||||
0%, 100% {
|
||||
opacity: 0.8;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
265
src/lib/components/Menu.svelte
Normal file
265
src/lib/components/Menu.svelte
Normal file
|
|
@ -0,0 +1,265 @@
|
|||
<script lang="ts">
|
||||
import type { MenuData } from '$lib/data/menu';
|
||||
import menuJson from '$lib/data/menu.json';
|
||||
import { Leaf, X } from 'lucide-svelte';
|
||||
|
||||
const menuData: MenuData = menuJson;
|
||||
let activeTab = $state(0);
|
||||
let showAllergens = $state(false);
|
||||
</script>
|
||||
|
||||
<section id="speisekarte" class="bg-cream py-20">
|
||||
<div class="mx-auto max-w-4xl px-6">
|
||||
<h2 class="font-gothic mb-2 text-center text-5xl text-brown md:text-6xl">Speisekarte</h2>
|
||||
<p class="mx-auto mb-10 max-w-2xl text-center text-sepia italic">
|
||||
Traditionelle rumänische Rezepte – ehrlich und herzhaft. <br>
|
||||
Alle
|
||||
Gerichte werden frisch im Foodtruck zubereitet.
|
||||
</p>
|
||||
|
||||
<!-- Season Tabs (Ornate Buttongroup) -->
|
||||
<div class="mb-12 flex justify-center">
|
||||
<div class="ornate-tabgroup relative inline-flex">
|
||||
<svg
|
||||
class="pointer-events-none absolute inset-0 h-full w-full"
|
||||
viewBox="0 0 300 50"
|
||||
preserveAspectRatio="none"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect x="3" y="3" width="294" height="44" rx="4" stroke="var(--color-gold)" stroke-width="1.2" stroke-opacity="0.6" />
|
||||
<path d="M3 13 C3 13, 0 8, 0 3 C0 3, 5 0, 10 0 L10 2.5 C6.5 2.5, 4 4, 3 6.5 Z" fill="var(--color-gold)" fill-opacity="0.7" />
|
||||
<path d="M297 13 C297 13, 300 8, 300 3 C300 3, 295 0, 290 0 L290 2.5 C293.5 2.5, 296 4, 297 6.5 Z" fill="var(--color-gold)" fill-opacity="0.7" />
|
||||
<path d="M3 37 C3 37, 0 42, 0 47 C0 47, 5 50, 10 50 L10 47.5 C6.5 47.5, 4 46, 3 43.5 Z" fill="var(--color-gold)" fill-opacity="0.7" />
|
||||
<path d="M297 37 C297 37, 300 42, 300 47 C300 47, 295 50, 290 50 L290 47.5 C293.5 47.5, 296 46, 297 43.5 Z" fill="var(--color-gold)" fill-opacity="0.7" />
|
||||
<path d="M130 3 C137 -2, 143 -1, 150 2 C157 -1, 163 -2, 170 3" stroke="var(--color-gold)" stroke-width="1" stroke-opacity="0.6" fill="none" />
|
||||
<path d="M130 47 C137 52, 143 51, 150 48 C157 51, 163 52, 170 47" stroke="var(--color-gold)" stroke-width="1" stroke-opacity="0.6" fill="none" />
|
||||
<path d="M3 21 L0 25 L3 29" stroke="var(--color-gold)" stroke-width="1" stroke-opacity="0.6" fill="none" />
|
||||
<path d="M297 21 L300 25 L297 29" stroke="var(--color-gold)" stroke-width="1" stroke-opacity="0.6" fill="none" />
|
||||
<line x1="150" y1="8" x2="150" y2="42" stroke="var(--color-gold)" stroke-width="0.8" stroke-opacity="0.3" />
|
||||
<path d="M147 8 L150 4 L153 8" stroke="var(--color-gold)" stroke-width="0.8" stroke-opacity="0.5" fill="none" />
|
||||
<path d="M147 42 L150 46 L153 42" stroke="var(--color-gold)" stroke-width="0.8" stroke-opacity="0.5" fill="none" />
|
||||
</svg>
|
||||
{#each menuData.seasons as menu, i}
|
||||
<button
|
||||
class="ornate-tab relative z-10 px-12 py-4 text-base font-semibold transition-colors {activeTab === i
|
||||
? 'text-gold'
|
||||
: 'text-brown/60 hover:text-gold/80'}"
|
||||
onclick={() => (activeTab = i)}
|
||||
>
|
||||
{menu.season}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Menu Content -->
|
||||
{#each menuData.seasons as menu, menuIndex}
|
||||
{#if activeTab === menuIndex}
|
||||
{#each menu.categories as category}
|
||||
<div class="mb-14 last:mb-0">
|
||||
<h3
|
||||
class="mb-6 border-b border-gold/40 pb-2 text-center text-2xl font-bold tracking-wide text-brown"
|
||||
>
|
||||
{category.title}
|
||||
</h3>
|
||||
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
{#each category.items as item}
|
||||
<div class="rounded-lg border border-gold/15 bg-parchment/40 p-5">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-lg font-semibold text-brown">{item.name}</span>
|
||||
{#if item.vegetarian}
|
||||
<span class="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[#009036]" title="vegetarisch">
|
||||
<Leaf size={14} color="#FEDD00" strokeWidth={2.5} />
|
||||
</span>
|
||||
{/if}
|
||||
{#if item.vegan}
|
||||
<span class="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[#FEDD00]" title="vegan">
|
||||
<Leaf size={14} color="#009036" strokeWidth={2.5} />
|
||||
</span>
|
||||
{/if}
|
||||
{#if item.allergens || item.additives}
|
||||
<sup class="text-[10px] font-normal text-sepia/50">{item.allergens}{#if item.additives}, {item.additives}{/if}</sup>
|
||||
{/if}
|
||||
</div>
|
||||
{#if item.description}
|
||||
<p class="mt-2 text-sm leading-relaxed text-sepia">{item.description}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
<!-- Allergen Legend Trigger -->
|
||||
<div class="mt-10 text-center">
|
||||
<button
|
||||
class="inline-flex items-center gap-2 text-sm text-sepia/60 transition-colors hover:text-gold"
|
||||
onclick={() => (showAllergens = true)}
|
||||
>
|
||||
<svg class="h-4 w-4" 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>
|
||||
Allergene & Zusatzstoffe anzeigen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Allergen Modal -->
|
||||
{#if showAllergens}
|
||||
<!-- Backdrop -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-charcoal/70 p-4 backdrop-blur-sm"
|
||||
onkeydown={(e) => e.key === 'Escape' && (showAllergens = false)}
|
||||
onclick={(e) => e.target === e.currentTarget && (showAllergens = false)}
|
||||
>
|
||||
<!-- Modal -->
|
||||
<div class="ornate-modal relative w-full max-w-lg" role="dialog" aria-label="Allergene & Zusatzstoffe">
|
||||
<!-- Ornate border (CSS) -->
|
||||
<div class="pointer-events-none absolute inset-0 rounded-lg border border-gold/50"></div>
|
||||
|
||||
<!-- Corner ornaments (fixed-size SVGs, no distortion) -->
|
||||
<!-- Top-left -->
|
||||
<svg class="pointer-events-none absolute -top-2 -left-2 h-10 w-10 overflow-visible" viewBox="0 0 40 40" fill="none">
|
||||
<path d="M8 32 C8 32, 2 20, 2 8 C2 8, 14 2, 26 2 L26 6 C16 6, 10 10, 8 16 Z" fill="var(--color-gold)" fill-opacity="0.7" />
|
||||
<path d="M2 8 L0 0 L8 2" stroke="var(--color-gold)" stroke-width="1" stroke-opacity="0.4" fill="none" />
|
||||
</svg>
|
||||
<!-- Top-right -->
|
||||
<svg class="pointer-events-none absolute -top-2 -right-2 h-10 w-10 overflow-visible" viewBox="0 0 40 40" fill="none">
|
||||
<path d="M32 32 C32 32, 38 20, 38 8 C38 8, 26 2, 14 2 L14 6 C24 6, 30 10, 32 16 Z" fill="var(--color-gold)" fill-opacity="0.7" />
|
||||
<path d="M38 8 L40 0 L32 2" stroke="var(--color-gold)" stroke-width="1" stroke-opacity="0.4" fill="none" />
|
||||
</svg>
|
||||
<!-- Bottom-left -->
|
||||
<svg class="pointer-events-none absolute -bottom-2 -left-2 h-10 w-10 overflow-visible" viewBox="0 0 40 40" fill="none">
|
||||
<path d="M8 8 C8 8, 2 20, 2 32 C2 32, 14 38, 26 38 L26 34 C16 34, 10 30, 8 24 Z" fill="var(--color-gold)" fill-opacity="0.7" />
|
||||
<path d="M2 32 L0 40 L8 38" stroke="var(--color-gold)" stroke-width="1" stroke-opacity="0.4" fill="none" />
|
||||
</svg>
|
||||
<!-- Bottom-right -->
|
||||
<svg class="pointer-events-none absolute -bottom-2 -right-2 h-10 w-10 overflow-visible" viewBox="0 0 40 40" fill="none">
|
||||
<path d="M32 8 C32 8, 38 20, 38 32 C38 32, 26 38, 14 38 L14 34 C24 34, 30 30, 32 24 Z" fill="var(--color-gold)" fill-opacity="0.7" />
|
||||
<path d="M38 32 L40 40 L32 38" stroke="var(--color-gold)" stroke-width="1" stroke-opacity="0.4" fill="none" />
|
||||
</svg>
|
||||
|
||||
<!-- Top center flourish -->
|
||||
<svg class="pointer-events-none absolute -top-3 left-1/2 h-6 w-20 -translate-x-1/2 overflow-visible" viewBox="0 0 80 24" fill="none">
|
||||
<path d="M20 12 C28 0, 36 2, 40 10 C44 2, 52 0, 60 12" stroke="var(--color-gold)" stroke-width="1.5" stroke-opacity="0.6" fill="none" />
|
||||
<circle cx="40" cy="2" r="2.5" fill="var(--color-gold)" fill-opacity="0.5" />
|
||||
<path d="M10 12 L20 12" stroke="var(--color-gold)" stroke-width="0.8" stroke-opacity="0.3" />
|
||||
<path d="M60 12 L70 12" stroke="var(--color-gold)" stroke-width="0.8" stroke-opacity="0.3" />
|
||||
</svg>
|
||||
<!-- Bottom center flourish -->
|
||||
<svg class="pointer-events-none absolute -bottom-3 left-1/2 h-6 w-20 -translate-x-1/2 overflow-visible" viewBox="0 0 80 24" fill="none">
|
||||
<path d="M20 12 C28 24, 36 22, 40 14 C44 22, 52 24, 60 12" stroke="var(--color-gold)" stroke-width="1.5" stroke-opacity="0.6" fill="none" />
|
||||
<circle cx="40" cy="22" r="2.5" fill="var(--color-gold)" fill-opacity="0.5" />
|
||||
<path d="M10 12 L20 12" stroke="var(--color-gold)" stroke-width="0.8" stroke-opacity="0.3" />
|
||||
<path d="M60 12 L70 12" stroke="var(--color-gold)" stroke-width="0.8" stroke-opacity="0.3" />
|
||||
</svg>
|
||||
|
||||
<!-- Side diamonds: left -->
|
||||
<svg class="pointer-events-none absolute top-1/2 -left-3 h-8 w-6 -translate-y-1/2 overflow-visible" viewBox="0 0 24 32" fill="none">
|
||||
<path d="M20 8 L4 16 L20 24" stroke="var(--color-gold)" stroke-width="1.5" stroke-opacity="0.6" fill="none" />
|
||||
<circle cx="4" cy="16" r="2" fill="var(--color-gold)" fill-opacity="0.4" />
|
||||
</svg>
|
||||
<!-- Side diamonds: right -->
|
||||
<svg class="pointer-events-none absolute top-1/2 -right-3 h-8 w-6 -translate-y-1/2 overflow-visible" viewBox="0 0 24 32" fill="none">
|
||||
<path d="M4 8 L20 16 L4 24" stroke="var(--color-gold)" stroke-width="1.5" stroke-opacity="0.6" fill="none" />
|
||||
<circle cx="20" cy="16" r="2" fill="var(--color-gold)" fill-opacity="0.4" />
|
||||
</svg>
|
||||
|
||||
<!-- Modal Content -->
|
||||
<div class="relative z-10 rounded-lg bg-cream p-8 pt-6">
|
||||
<!-- Close button -->
|
||||
<button
|
||||
class="absolute top-4 right-4 text-sepia/50 transition-colors hover:text-gold"
|
||||
onclick={() => (showAllergens = false)}
|
||||
aria-label="Schließen"
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
|
||||
<h3 class="font-gothic mb-6 text-center text-3xl text-brown">
|
||||
Allergene & Zusatzstoffe
|
||||
</h3>
|
||||
|
||||
<!-- Decorative divider -->
|
||||
<div class="mb-6 flex items-center justify-center gap-3">
|
||||
<div class="h-px w-12 bg-gold/40"></div>
|
||||
<span class="font-gothic text-lg text-gold/50">✦</span>
|
||||
<div class="h-px w-12 bg-gold/40"></div>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<p class="mb-3 text-center text-xs font-semibold tracking-wider text-brown/60 uppercase">Allergene</p>
|
||||
<div class="flex flex-wrap justify-center gap-2">
|
||||
{#each menuData.allergens as a}
|
||||
<span class="inline-flex items-center gap-1.5 rounded-full border border-gold/25 bg-parchment px-3 py-1.5 text-xs">
|
||||
<span class="font-bold text-brown">{a.code}</span>
|
||||
<span class="text-sepia">{a.label}</span>
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<p class="mb-3 text-center text-xs font-semibold tracking-wider text-brown/60 uppercase">Zusatzstoffe</p>
|
||||
<div class="flex flex-wrap justify-center gap-2">
|
||||
{#each menuData.additives as a}
|
||||
<span class="inline-flex items-center gap-1.5 rounded-full border border-sepia/20 bg-parchment px-3 py-1.5 text-xs">
|
||||
<span class="font-bold text-brown">{a.code}</span>
|
||||
<span class="text-sepia">{a.label}</span>
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Decorative divider -->
|
||||
<div class="mb-6 flex items-center justify-center gap-3">
|
||||
<div class="h-px w-8 bg-gold/30"></div>
|
||||
<span class="text-xs text-gold/40">♦</span>
|
||||
<div class="h-px w-8 bg-gold/30"></div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-center gap-3">
|
||||
<span class="inline-flex items-center gap-1.5 rounded-full border border-[#009036]/30 bg-parchment px-3 py-1.5 text-xs">
|
||||
<span class="inline-flex h-5 w-5 items-center justify-center rounded-full bg-[#009036]">
|
||||
<Leaf size={12} color="#FEDD00" strokeWidth={2.5} />
|
||||
</span>
|
||||
<span class="text-sepia">vegetarisch</span>
|
||||
</span>
|
||||
<span class="inline-flex items-center gap-1.5 rounded-full border border-[#FEDD00]/40 bg-parchment px-3 py-1.5 text-xs">
|
||||
<span class="inline-flex h-5 w-5 items-center justify-center rounded-full bg-[#FEDD00]">
|
||||
<Leaf size={12} color="#009036" strokeWidth={2.5} />
|
||||
</span>
|
||||
<span class="text-sepia">vegan</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p class="mt-5 text-center text-xs text-sepia/60">
|
||||
Angaben in Klammern = kann Spuren enthalten
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.ornate-modal {
|
||||
animation: modal-in 0.25s ease-out;
|
||||
}
|
||||
|
||||
@keyframes modal-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
89
src/lib/components/Nav.svelte
Normal file
89
src/lib/components/Nav.svelte
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
<script lang="ts">
|
||||
let scrolled = $state(false);
|
||||
let mobileOpen = $state(false);
|
||||
|
||||
function onScroll() {
|
||||
scrolled = window.scrollY > 100;
|
||||
}
|
||||
|
||||
function closeMobile() {
|
||||
mobileOpen = false;
|
||||
}
|
||||
|
||||
const links = [
|
||||
{ href: '/#speisekarte', label: 'Speisekarte' },
|
||||
{ href: '/#geschichte', label: 'Unsere Geschichte' },
|
||||
{ href: '/#catering', label: 'Catering' },
|
||||
{ href: '/#kontakt', label: 'Kontakt' }
|
||||
];
|
||||
</script>
|
||||
|
||||
<svelte:window onscroll={onScroll} />
|
||||
|
||||
<nav
|
||||
class="fixed top-0 left-0 z-50 w-full transition-all duration-300 {scrolled
|
||||
? 'bg-charcoal shadow-lg'
|
||||
: 'bg-transparent'}"
|
||||
>
|
||||
<div class="mx-auto flex max-w-6xl items-center justify-between px-6 py-4">
|
||||
<a
|
||||
href="/"
|
||||
class="font-gothic text-2xl transition-colors duration-300 {scrolled ? 'text-gold' : 'text-charcoal'}"
|
||||
onclick={closeMobile}
|
||||
>
|
||||
Graffen
|
||||
</a>
|
||||
|
||||
<!-- Desktop -->
|
||||
<div class="hidden gap-8 md:flex">
|
||||
{#each links as link}
|
||||
<a
|
||||
href={link.href}
|
||||
class="text-sm font-bold tracking-wide transition-colors {scrolled ? 'text-cream/80 hover:text-gold' : 'text-charcoal hover:text-cream'}"
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Mobile Toggle -->
|
||||
<button
|
||||
class="md:hidden {scrolled ? 'text-cream' : 'text-charcoal'}"
|
||||
onclick={() => (mobileOpen = !mobileOpen)}
|
||||
aria-label="Menü"
|
||||
>
|
||||
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
{#if mobileOpen}
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
{:else}
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16"
|
||||
/>
|
||||
{/if}
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu -->
|
||||
{#if mobileOpen}
|
||||
<div class="border-t border-gold/20 bg-charcoal/95 px-6 pb-4 md:hidden">
|
||||
{#each links as link}
|
||||
<a
|
||||
href={link.href}
|
||||
class="block py-3 text-cream/80 transition-colors hover:text-gold"
|
||||
onclick={closeMobile}
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</nav>
|
||||
146
src/lib/components/OrnateButton.svelte
Normal file
146
src/lib/components/OrnateButton.svelte
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
href: string;
|
||||
variant?: 'primary' | 'secondary';
|
||||
target?: string;
|
||||
rel?: string;
|
||||
children: Snippet;
|
||||
}
|
||||
|
||||
let { href, variant = 'primary', target, rel, children }: Props = $props();
|
||||
</script>
|
||||
|
||||
<a {href} {target} {rel} class="ornate-btn group inline-block" data-variant={variant}>
|
||||
<svg
|
||||
class="ornate-frame absolute inset-0 h-full w-full"
|
||||
viewBox="0 0 200 60"
|
||||
preserveAspectRatio="none"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<!-- Main border -->
|
||||
<rect
|
||||
x="4"
|
||||
y="4"
|
||||
width="192"
|
||||
height="52"
|
||||
rx="4"
|
||||
class="ornate-rect"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
<!-- Corner ornaments: top-left -->
|
||||
<path d="M4 16 C4 16, 0 10, 0 4 C0 4, 6 0, 12 0 L12 3 C8 3, 5 5, 4 8 Z" class="ornate-corner" />
|
||||
<!-- top-right -->
|
||||
<path d="M196 16 C196 16, 200 10, 200 4 C200 4, 194 0, 188 0 L188 3 C192 3, 195 5, 196 8 Z" class="ornate-corner" />
|
||||
<!-- bottom-left -->
|
||||
<path d="M4 44 C4 44, 0 50, 0 56 C0 56, 6 60, 12 60 L12 57 C8 57, 5 55, 4 52 Z" class="ornate-corner" />
|
||||
<!-- bottom-right -->
|
||||
<path d="M196 44 C196 44, 200 50, 200 56 C200 56, 194 60, 188 60 L188 57 C192 57, 195 55, 196 52 Z" class="ornate-corner" />
|
||||
<!-- Top center flourish -->
|
||||
<path d="M85 4 C90 -2, 95 -1, 100 2 C105 -1, 110 -2, 115 4" class="ornate-flourish" stroke-width="1.2" />
|
||||
<!-- Bottom center flourish -->
|
||||
<path d="M85 56 C90 62, 95 61, 100 58 C105 61, 110 62, 115 56" class="ornate-flourish" stroke-width="1.2" />
|
||||
<!-- Side diamonds: left -->
|
||||
<path d="M4 26 L0 30 L4 34" class="ornate-flourish" stroke-width="1.2" />
|
||||
<!-- right -->
|
||||
<path d="M196 26 L200 30 L196 34" class="ornate-flourish" stroke-width="1.2" />
|
||||
</svg>
|
||||
<span class="ornate-content relative z-10 inline-flex items-center gap-2">
|
||||
{@render children()}
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<style>
|
||||
.ornate-btn {
|
||||
position: relative;
|
||||
padding: 0.85rem 2.2rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ornate-frame {
|
||||
pointer-events: none;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/* ── Primary ── */
|
||||
.ornate-btn[data-variant='primary'] .ornate-rect {
|
||||
stroke: var(--color-gold);
|
||||
fill: var(--color-gold);
|
||||
fill-opacity: 0.1;
|
||||
transition: fill-opacity 0.25s ease;
|
||||
}
|
||||
.ornate-btn[data-variant='primary']:hover .ornate-rect {
|
||||
fill-opacity: 0.22;
|
||||
}
|
||||
|
||||
.ornate-btn[data-variant='primary'] .ornate-corner {
|
||||
fill: var(--color-gold);
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
.ornate-btn[data-variant='primary']:hover .ornate-corner {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.ornate-btn[data-variant='primary'] .ornate-flourish {
|
||||
stroke: var(--color-gold);
|
||||
fill: none;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
.ornate-btn[data-variant='primary']:hover .ornate-flourish {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.ornate-btn[data-variant='primary'] .ornate-content {
|
||||
color: var(--color-gold);
|
||||
font-weight: 600;
|
||||
opacity: 0.85;
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
.ornate-btn[data-variant='primary']:hover .ornate-content {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* ── Secondary ── */
|
||||
.ornate-btn[data-variant='secondary'] .ornate-rect {
|
||||
stroke: var(--color-gold);
|
||||
fill: none;
|
||||
opacity: 0.45;
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
.ornate-btn[data-variant='secondary']:hover .ornate-rect {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.ornate-btn[data-variant='secondary'] .ornate-corner {
|
||||
fill: var(--color-gold);
|
||||
opacity: 0.4;
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
.ornate-btn[data-variant='secondary']:hover .ornate-corner {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.ornate-btn[data-variant='secondary'] .ornate-flourish {
|
||||
stroke: var(--color-gold);
|
||||
fill: none;
|
||||
opacity: 0.4;
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
.ornate-btn[data-variant='secondary']:hover .ornate-flourish {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.ornate-btn[data-variant='secondary'] .ornate-content {
|
||||
color: var(--color-gold);
|
||||
font-weight: 600;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
.ornate-btn[data-variant='secondary']:hover .ornate-content {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
112
src/lib/data/menu.json
Normal file
112
src/lib/data/menu.json
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
{
|
||||
"seasons": [
|
||||
{
|
||||
"season": "Sommerkarte",
|
||||
"categories": [
|
||||
{
|
||||
"title": "Vom Grill",
|
||||
"items": [
|
||||
{
|
||||
"name": "Traditionelle rumänische Cevapi (Mici)",
|
||||
"description": "Hausgemachte Hackfleischröllchen aus Rind und Lamm vom Grill, serviert mit Senf und Brot",
|
||||
"allergens": "A1, J"
|
||||
},
|
||||
{
|
||||
"name": "Pulled Beef Sandwich mit Coleslaw",
|
||||
"description": "Zart gegartes Rindfleisch, serviert im rustikalen Brot mit hausgemachtem Coleslaw-Salat",
|
||||
"allergens": "A1, C, G, J, (L)",
|
||||
"additives": "2, 3"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Getränke",
|
||||
"items": [
|
||||
{ "name": "Kaffee" },
|
||||
{ "name": "Tee" },
|
||||
{ "name": "Café Frappé" },
|
||||
{ "name": "Limonade", "description": "Hausgemacht" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"season": "Winterkarte",
|
||||
"categories": [
|
||||
{
|
||||
"title": "Suppen",
|
||||
"items": [
|
||||
{
|
||||
"name": "Bohnensuppe im Brotleib nach siebenbürgischer Art",
|
||||
"description": "Traditionelle Bohnensuppe mit geräuchertem Fleisch, serviert im Bauernbrot",
|
||||
"allergens": "A1, (A2), I, (G)",
|
||||
"additives": "2, 3, 8"
|
||||
},
|
||||
{
|
||||
"name": "Hühnersuppe à la Grec",
|
||||
"description": "Hühnersuppe mit Reis, verfeinert mit Eigelb, Sahne und Zitrone",
|
||||
"allergens": "A1, C, G, I",
|
||||
"additives": "3"
|
||||
},
|
||||
{
|
||||
"name": "Kartoffelsuppe nach siebenbürgischer Art",
|
||||
"description": "Herzhafte Suppe mit Kartoffelwürfeln, geräuchertem Schweinefleisch, verfeinert mit Schmand und Estragon",
|
||||
"allergens": "A1, G, I, (L)",
|
||||
"additives": "2, 3, 8"
|
||||
},
|
||||
{
|
||||
"name": "Salatsuppe nach siebenbürgischer Art",
|
||||
"description": "Salatsuppe mit Speck, Omelett und Schmand",
|
||||
"allergens": "A1, C, G, I"
|
||||
},
|
||||
{
|
||||
"name": "Siebenbürgische Gemüsesuppe",
|
||||
"description": "Frisches Marktgemüse, verfeinert mit Schmand und Kräutern",
|
||||
"allergens": "A1, G, I",
|
||||
"additives": "3, 5",
|
||||
"vegetarian": true
|
||||
},
|
||||
{
|
||||
"name": "Ungarische Gulaschsuppe (Gulyás)",
|
||||
"description": "Herzhafte Rindersuppe mit Kartoffeln, Paprika und Zupfnudeln",
|
||||
"allergens": "A1, C, I, L",
|
||||
"additives": "3"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Vom Grill",
|
||||
"items": [
|
||||
{
|
||||
"name": "Traditionelle rumänische Cevapi (Mici)",
|
||||
"description": "Hausgemachte Hackfleischröllchen aus Rind und Lamm vom Grill, serviert mit Senf und Weißbrot",
|
||||
"allergens": "A1, J"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Getränke",
|
||||
"items": [
|
||||
{ "name": "Kaffee" },
|
||||
{ "name": "Tee" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"allergens": [
|
||||
{ "code": "A1", "label": "Weizen" },
|
||||
{ "code": "A2", "label": "Roggen" },
|
||||
{ "code": "C", "label": "Eier" },
|
||||
{ "code": "G", "label": "Milch/Laktose" },
|
||||
{ "code": "I", "label": "Sellerie" },
|
||||
{ "code": "J", "label": "Senf" },
|
||||
{ "code": "L", "label": "Lupinen" }
|
||||
],
|
||||
"additives": [
|
||||
{ "code": "2", "label": "Konservierungsstoffe" },
|
||||
{ "code": "3", "label": "Antioxidationsmittel" },
|
||||
{ "code": "5", "label": "Geschmacksverstärker" },
|
||||
{ "code": "8", "label": "Phosphat" }
|
||||
]
|
||||
}
|
||||
29
src/lib/data/menu.ts
Normal file
29
src/lib/data/menu.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
export interface MenuItem {
|
||||
name: string;
|
||||
description?: string;
|
||||
allergens?: string;
|
||||
additives?: string;
|
||||
vegetarian?: boolean;
|
||||
vegan?: boolean;
|
||||
}
|
||||
|
||||
export interface MenuCategory {
|
||||
title: string;
|
||||
items: MenuItem[];
|
||||
}
|
||||
|
||||
export interface SeasonMenu {
|
||||
season: string;
|
||||
categories: MenuCategory[];
|
||||
}
|
||||
|
||||
export interface LegendEntry {
|
||||
code: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface MenuData {
|
||||
seasons: SeasonMenu[];
|
||||
allergens: LegendEntry[];
|
||||
additives: LegendEntry[];
|
||||
}
|
||||
1
src/routes/+layout.js
Normal file
1
src/routes/+layout.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const prerender = true;
|
||||
15
src/routes/+layout.svelte
Normal file
15
src/routes/+layout.svelte
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<script lang="ts">
|
||||
import '../app.css';
|
||||
import Nav from '$lib/components/Nav.svelte';
|
||||
import Footer from '$lib/components/Footer.svelte';
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<link rel="icon" href="/graffen-hero.png" />
|
||||
</svelte:head>
|
||||
|
||||
<Nav />
|
||||
{@render children()}
|
||||
<Footer />
|
||||
27
src/routes/+page.svelte
Normal file
27
src/routes/+page.svelte
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<script lang="ts">
|
||||
import Hero from '$lib/components/Hero.svelte';
|
||||
import Menu from '$lib/components/Menu.svelte';
|
||||
import About from '$lib/components/About.svelte';
|
||||
import Catering from '$lib/components/Catering.svelte';
|
||||
import Contact from '$lib/components/Contact.svelte';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Graffen – Rumänische Hausmannskost | Foodtruck</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Graffen Foodtruck: Traditionelle rumänische Küche aus Siebenbürgen. Sarmale, Mici, Ciorbă und mehr – frisch zubereitet auf Märkten und Events."
|
||||
/>
|
||||
<meta property="og:title" content="Graffen – Rumänische Hausmannskost" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Transsilvanische Küche auf Rädern. Ehrliche Rezepte, frische Zutaten."
|
||||
/>
|
||||
<meta property="og:image" content="/graffen-hero.png" />
|
||||
</svelte:head>
|
||||
|
||||
<Hero />
|
||||
<Menu />
|
||||
<Catering />
|
||||
<About />
|
||||
<Contact />
|
||||
42
src/routes/datenschutz/+page.svelte
Normal file
42
src/routes/datenschutz/+page.svelte
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<svelte:head>
|
||||
<title>Datenschutz – Graffen Foodtruck</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="mx-auto min-h-screen max-w-4xl px-6 pt-28 pb-20">
|
||||
<h1 class="font-gothic mb-8 text-4xl text-brown">Datenschutzerklärung</h1>
|
||||
|
||||
<div class="space-y-6 text-sepia">
|
||||
<div>
|
||||
<h2 class="mb-2 text-xl font-semibold text-brown">1. Verantwortlicher</h2>
|
||||
<p>
|
||||
Daniel Baciu<br />
|
||||
Graffen Foodtruck<br />
|
||||
Ob. Gartenstr. 6<br />
|
||||
92237 Sulzbach-Rosenberg<br />
|
||||
E-Mail: hallo@graffen-foodtruck.de
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="mb-2 text-xl font-semibold text-brown">2. Hosting</h2>
|
||||
<p>
|
||||
Diese Webseite wird als statische Seite gehostet. Der Hoster erhebt in sogenannten
|
||||
Logfiles Informationen, die Ihr Browser automatisch übermittelt (IP-Adresse, Datum und
|
||||
Uhrzeit des Zugriffs, Browsertyp). Die Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="mb-2 text-xl font-semibold text-brown">3. Google Fonts</h2>
|
||||
<p>
|
||||
Diese Seite nutzt Google Fonts zur einheitlichen Schriftdarstellung. Beim Aufruf einer
|
||||
Seite lädt Ihr Browser die benötigten Schriftarten von Google-Servern. Dabei wird Ihre
|
||||
IP-Adresse an Google übermittelt. Die Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-sepia/60">
|
||||
Bitte ergänze hier die vollständige Datenschutzerklärung vor Veröffentlichung der Webseite.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
27
src/routes/impressum/+page.svelte
Normal file
27
src/routes/impressum/+page.svelte
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<script>
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Impressum – Graffen Foodtruck</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="mx-auto min-h-screen max-w-4xl px-6 pt-28 pb-20">
|
||||
<h1 class="font-gothic mb-8 text-4xl text-brown">Impressum</h1>
|
||||
|
||||
<div class="space-y-4 text-sepia">
|
||||
|
||||
<p>
|
||||
Daniel Baciu<br />
|
||||
Graffen Foodtruck<br />
|
||||
Ob. Gartenstr. 6<br />
|
||||
92237 Sulzbach-Rosenberg<br />
|
||||
E-Mail: hallo@graffen-foodtruck.de
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Kontakt:</strong><br />
|
||||
E-Mail: hallo@graffen-foodtruck.de
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
BIN
static/graffen-hero.png
Normal file
BIN
static/graffen-hero.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
20
svelte.config.js
Normal file
20
svelte.config.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import adapter from '@sveltejs/adapter-static';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
kit: {
|
||||
adapter: adapter({
|
||||
pages: 'build',
|
||||
assets: 'build',
|
||||
fallback: undefined,
|
||||
precompress: false,
|
||||
strict: true
|
||||
})
|
||||
},
|
||||
vitePlugin: {
|
||||
dynamicCompileOptions: ({ filename }) =>
|
||||
filename.includes('node_modules') ? undefined : { runes: true }
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
20
tsconfig.json
Normal file
20
tsconfig.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rewriteRelativeImportExtensions": true,
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler"
|
||||
}
|
||||
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
||||
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
||||
//
|
||||
// To make changes to top-level options such as include and exclude, we recommend extending
|
||||
// the generated config; see https://svelte.dev/docs/kit/configuration#typescript
|
||||
}
|
||||
13
vite.config.ts
Normal file
13
vite.config.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
import pkg from './package.json';
|
||||
|
||||
export default defineConfig({
|
||||
server: {
|
||||
host: true,
|
||||
port: 31337,
|
||||
open: `http://${pkg.name}.localhost:31337`
|
||||
},
|
||||
plugins: [sveltekit(), tailwindcss()]
|
||||
});
|
||||
Loading…
Reference in a new issue