Compare commits

..

No commits in common. "1d8563474068c40323e8b0d9aaa962c23799ea82" and "36c3bcc98b548eab8a2e26438b27c75a0fd1ffa7" have entirely different histories.

16 changed files with 370 additions and 487 deletions

View File

@ -3,58 +3,59 @@
## 🚨 Rules ## 🚨 Rules
**1. Infrastructure as Code is rule #1.** Manual creation of resources (e.g., via `gcloud` or Console) is forbidden. The use of Terraform/Tofu is mandatory. **1. Infrastructure as Code is rule #1.** Manual creation of resources (e.g., via `gcloud` or Console) is forbidden. The use of Terraform/Tofu is mandatory.
**2. Agental Protocol (Git & Deployment):**
- Never perform `git add` or `./deploy.sh` before the user has formally accepted the **Walkthrough** artifact.
- Once accepted, the agent is responsible for committing changes and triggering the deployment script.
This document serves as the "Source of Truth" for the Haumdaucher website. It outlines the design principles, technical architecture, and specialized features to ensure consistent future development. This document serves as the "Source of Truth" for the Haumdaucher website. It outlines the design principles, technical architecture, and specialized features to ensure consistent future development.
## 🦢 Project Essence ## 🦢 Project Essence
**Haumdaucher** is a community project from Regensburg, Germany. The website represents the "HAUMDAUCHER Wurst und Spezialitäten n.e.V." (nicht eingetragener Verein). It is designed to be humorous, culturally rich, and technically "surprising." **Haumdaucher** is a community project from Regensburg, Germany. The website is designed to be humorous, culturally rich (Bavarian), and technically "surprising."
## 🎨 Design Principles ## 🎨 Design Principles
- **Vibrant Aesthetics**: Each theme must feel like a completely different app. - **Vibrant Aesthetics**: Each theme must feel like a completely different app.
- **Glassmorphism**: Use `backdrop-filter` and semi-transparent backgrounds for a premium feel. - **Glassmorphism**: Use `backdrop-filter` and semi-transparent backgrounds for a premium feel.
- **Micro-interactions**: Subtle entrance animations and consistent hover states. - **Micro-interactions**: Subtle entrance animations (logo spin, hero text slide) and consistent hover states.
- **Accessibility**: Mobile-first design with safe-area support for PWA usage on iOS/Android. - **Accessibility**: Mobile-first design with safe-area support for PWA usage on iOS/Android.
## ⚖️ Legal & Compliance (n.e.V.)
Maintaining the legal section is critical for German compliance (§ 5 DDG).
- **Entity Type**: The club is a **nicht eingetragener Verein (n.e.V.)**. Do NOT add Registry data (Registergericht/Nummer).
- **Mandatory Impressum Fields**:
- Full Name: `HAUMDAUCHER Wurst und Spezialitäten n.e.V.`
- Vertretungsberechtigter: `Moritz Graf (1. Vorstand)`
- Ladungsfähige Anschrift: `Grabengasse 7, 93059 Regensburg`
- Kontakt: `Telefon: 094183065717, E-Mail: info@haumdaucher.de`
- **Privacy (DSGVO)**: The website uses **Google Firebase Authentication**. The privacy policy must disclose the data processing by Google Ireland Limited and the US data transfer details.
## 🛠 Technical Specifications ## 🛠 Technical Specifications
- **Framework**: Vue 3 (Composition API) + Vite + TypeScript. - **Framework**: Vue 3 (Composition API) + Vite + TypeScript.
- **State Management**: Centralized in `App.vue` using standard `ref` hooks. - **State Management**: Centralized in `App.vue` using standard `ref` hooks. Persisted in `localStorage`.
- **Theming System**: - **Theming System**:
- Driven by `data-theme` attribute on `:root`. - Driven by `data-theme` attribute on `:root`.
- Defined in `src/assets/styles/global.css`. - Defined in `src/assets/styles/global.css`.
- Themes: `Classic` (Light), `Dark` (Premium Charcoal/Gold), `NAT` (Boar Easter Egg). - Themes: `Classic`, `Unicorn`, `Luxury`, `Win95`, `NAT`.
- **Localization**: - **Localization**:
- Centralized in `src/locales/i18n.ts`. - Centralized in `src/locales/i18n.ts`.
- Language: `de` (Standard German) only. - Supports `de` (Standard German) and `bar` (Bavarian Dialect).
- **PWA**:
- Managed via `vite-plugin-pwa`.
- Custom icons and standalone manifest for "Add to Home Screen" support.
## 🚢 Deployment & DevOps
Deployment is automated via the `./deploy.sh` script.
**Workflow Details:** ## 🧪 Testing
1. **Cloud Sync**: Script ensures the `haumdaucher` namespace exists in Kubernetes. - **Framework**: Vitest + HappyDOM.
2. **Config Extraction**: Fetches Firebase credentials directly from Terraform outputs (`terraform output -json firebase_config`). - **Scope**: Lightweight sanity checks (e.g., verifying App mount).
3. **Build Pipeline**: Docker build passes Firebase credentials as `--build-arg` (VITE_FIREBASE_*). - **Commands**:
4. **Distribution**: Pushes the image to `registry.haumdaucher.de/haumdaucher-website:latest`. - `npm test`: Run tests in watch mode.
5. **K8s Update**: Applies `k8s-manifests.yaml` and triggers a `kubectl rollout restart` to fetch the new image. - `npm test -- --run`: Run tests once (CI mode).
## 🕹 The Haumdaucher Game ## 🕹 The Haumdaucher Game
- **Engine**: HTML5 Canvas rendering. Game style changes dynamically based on the site's active theme. - **Engine**: HTML5 Canvas rendering.
- **Unlocking NAT Mode**: - **Controls**: Touch-responsive (horizontal drag) and Keyboard (Arrow Keys).
- **Natural**: Reach Level 10. - **Thematization**: The game visual style (backgrounds, player, obstacles) changes dynamically based on the site's active theme.
- **Backdoor**: Single 1x1 pixel in the bottom-left corner. Type `nat mode` into the prompt. - **Difficulty**: Balanced (10 levels). Level 10 triggers a "Boar Rain" supermode.
## 🔐 Progression & Gating
- **NAT Mode**: This theme is locked by default to maintain the "collectible" feel.
- **Unlocking**:
- **Natural**: Reach Level 10 in the game.
- **Backdoor**: Single 1x1 pixel in the bottom-left corner of the site. Clicking it triggers a prompt. Type `nat mode` to unlock.
## 🚢 Deployment & DevOps
- **Docker**: Dual-stage build (Node build -> Nginx serving).
- **Registry**: `registry.haumdaucher.de`.
- **Kubernetes**:
- Managed via `k8s-manifests.yaml`.
- Features `cert-manager` for SSL and `registry-haumdaucher-de` pull secret.
- **CI/CD Logic**: The `deploy.sh` script handles builds, pushes, and triggers a `kubectl rollout restart` to force deployment updates when utilizing the `latest` image tag.
## 📝 Ongoing Maintenance ## 📝 Ongoing Maintenance
- **Assets**: Static images should be placed in `public/images/`. - **Assets**: Static images should be placed in `public/images/` to avoid bundling issues in production containers.
- **Style Overrides**: Mobile-first approach is mandatory. Always test with `max-width: 375px`. - **Style Overrides**: Mobile-first approach is mandatory. Always test with `max-width: 375px`.

View File

@ -1,127 +0,0 @@
# **Satzung des „HAUMDAUCHER Wurst und Spezialitäten n.e.V.“**
**§ 1 Name, Sitz und Online-Präsenz**
(1) Der Verein führt den Namen „HAUMDAUCHER Wurst und Spezialitäten n.e.V.“. Er ist ein nicht eingetragener Verein.
(2) Der Sitz des Vereins ist Regensburg.
(3) Der Verein präsentiert sich online unter www.haumdaucher.de.
**§ 2 Vereinszweck und „Spirit“**
(1) Der Verein pflegt die Kultur der Kulinarik. Es liegt in der DNA der Gemeinschaft, andere Menschen mit Spezialitäten zu bewirten und sich gegenseitig mit Köstlichkeiten zu verwöhnen. Der Fokus liegt auf, aber nicht exklusiv bei, Wurstspezialitäten.
(2) Der Verein ist idealistisch und nicht auf wirtschaftlichen Gewinn ausgerichtet.
**§ 3 Der „Wursttober“**
Das Hauptevent des Vereinsjahres ist der „Wursttober“. Diese Veranstaltung findet jährlich, vorzugsweise im Oktober, statt. Fester Bestandteil und Tradition dieses Events ist es, gemeinschaftlich selbst frische Wurst herzustellen. Die Veranstaltung dient darüber hinaus der Präsentation der handwerklichen Ergebnisse und der Stärkung des Gemeinschaftsgeistes.
**§ 4 Der Vorstand und Vertretung**
(1) Der **geschäftsführende Vorstand** im Sinne des § 26 BGB besteht aus:
a) dem 1\. Vorsitzenden
b) dem stellvertretenden (2.) Vorsitzenden
c) dem Finanzvorstand (Kassenwart)
(2) Der **erweiterte Vorstand** besteht aus dem geschäftsführenden Vorstand sowie den jeweiligen Abteilungsvorsitzenden. Der erweiterte Vorstand berät über die strategische und kulinarische Ausrichtung des Vereins.
(3) **Vertretungsbefugnis:** Der Verein wird gerichtlich und außergerichtlich durch den 1\. Vorsitzenden oder den 2\. Vorsitzenden vertreten (Einzelvertretungsbefugnis).
(4) **Verfügungsbeschränkung:** Für Rechtsgeschäfte und Verträge, die einen Gegenwert von 500,00 Euro überschreiten, ist die gemeinsame Unterschrift von zwei Mitgliedern des geschäftsführenden Vorstands zwingend erforderlich.
**§ 5 Abteilungen des Vereins**
(1) Zur Erfüllung der vielfältigen kulinarischen Aufgaben können innerhalb des Vereins Abteilungen gebildet werden.
(2) Die Gründung einer Abteilung erfolgt durch Beschluss des geschäftsführenden Vorstands.
(3) Jede Abteilung wird durch einen Abteilungsvorsitzenden geleitet. Dieser wird durch seine Ernennung automatisch Mitglied des erweiterten Vorstands.
(4) Die Einzelheiten der Abteilungsführung werden in einem separaten Abteilungsgründungsdokument festgelegt.
**§ 6 Mitgliedschaft, Beendigung und Ausschluss**
(1) Mitglied kann jeder werden, der den „HAUMDAUCHER-Spirit“ teilt. Über die Aufnahme entscheidet der Vorstand.
(2) Die Mitgliedschaft endet durch Austritt, Tod oder Ausschluss.
(3) **Ausschluss:** Mitglieder, die gegen den Vereinszweck oder den „HAUMDAUCHER-Spirit“ verstoßen, können durch einen mehrheitlichen Beschluss des geschäftsführenden Vorstands mit sofortiger Wirkung aus dem Verein ausgeschlossen werden.
**§ 7 Die Mitgliederversammlung und Satzungsänderungen**
(1) Die Mitgliederversammlung ist das oberste Organ des Vereins. Sie ist insbesondere zuständig für die Wahl des Vorstands, die Entlastung des Vorstands sowie für Satzungsänderungen.
(2) Eine ordentliche Mitgliederversammlung findet mindestens einmal jährlich statt (vorzugsweise im Rahmen des „Wursttobers“).
(3) Die Einberufung erfolgt formlos, in der Regel per E-Mail oder über die offizielle Vereins-WhatsApp-Gruppe, durch den Vorstand mit einer Frist von mindestens 7 Tagen unter Angabe der Tagesordnung.
(4) **Satzungsänderungen:** Änderungen der Satzung bedürfen einer Mehrheit von zwei Dritteln (2/3) der anwesenden Mitglieder in der Mitgliederversammlung.
**§ 8 Kassenprüfung**
(1) Die Mitgliederversammlung wählt für die Dauer von zwei Jahren einen Kassenprüfer, der nicht dem Vorstand angehören darf.
(2) Der Kassenprüfer prüft mindestens einmal jährlich die Vereinskasse und erstattet der Mitgliederversammlung Bericht, um die Entlastung des Finanzvorstands zu empfehlen.
(3) Haben im abgelaufenen Geschäftsjahr keine finanziellen Transaktionen (Einnahmen oder Ausgaben) über die Vereinskasse stattgefunden, kann durch Beschluss des Vorstands auf die jährliche Kassenprüfung verzichtet werden.
**§ 9 Datenschutz**
(1) Zur Erfüllung der Zwecke und Aufgaben des Vereins werden unter Beachtung der Vorgaben der EU-Datenschutz-Grundverordnung (DSGVO) und des Bundesdatenschutzgesetzes (BDSG) personenbezogene Daten der Mitglieder (Name, Mobilfunknummer, E-Mail-Adresse) verarbeitet und vereinsintern gespeichert.
(2) Die Kommunikation im Verein erfolgt maßgeblich über E-Mail und Messenger-Dienste (z. B. WhatsApp). Jedes Mitglied stimmt der Aufnahme in entsprechende Verteiler und Gruppen mit seinem Beitritt zu.
**§ 10 Haftungsbeschränkung**
(1) Die Haftung der Mitglieder des Vorstands (geschäftsführend und erweitert) gegenüber dem Verein und seinen Mitgliedern ist auf Vorsatz und grobe Fahrlässigkeit beschränkt.
(2) Werden Vorstandsmitglieder durch Dritte auf Ersatz eines Schadens in Anspruch genommen, der bei der Wahrnehmung der Vorstandspflichten entstanden ist, so stellt der Verein sie im Innenverhältnis von der Haftung frei, sofern sie nicht vorsätzlich oder grob fahrlässig gehandelt haben.
**§ 11 Auflösung**
Bei Auflösung des Vereins, welche nur mit einer 3/4-Mehrheit der Mitgliederversammlung beschlossen werden kann, fällt das verbleibende Vermögen (sowie Ausrüstung und Vorräte) zu gleichen Teilen an die zum Zeitpunkt der Auflösung bestehenden Mitglieder.
*Beschlossen und unterschrieben in der Gründungsversammlung am \_\_\_\_\_\_\_\_\_ in Etterzhausen.*
*Liste der Gründungsmitglieder*
*Name Unterschrift*
\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_ \_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_
\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_ \_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_
\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_ \_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_
\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_ \_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_
\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_ \_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_
\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_ \_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_
\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_ \_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_
\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_ \_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_
\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_ \_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_
\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_ \_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_
\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_ \_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_
\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_ \_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_
\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_ \_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_
\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_ \_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_
\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_ \_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_
\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_ \_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_
\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_ \_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_

16
package-lock.json generated
View File

@ -74,7 +74,6 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.27.1", "@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5", "@babel/generator": "^7.28.5",
@ -2119,7 +2118,6 @@
"resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.6.tgz", "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.6.tgz",
"integrity": "sha512-4uyt8BOrBsSq6i4yiOV/gG6BnnrvTeyymlNcaN/dKvyU1GoolxAafvIvaNP1RCGPlNab3OuE4MKUQuv2lH+PLQ==", "integrity": "sha512-4uyt8BOrBsSq6i4yiOV/gG6BnnrvTeyymlNcaN/dKvyU1GoolxAafvIvaNP1RCGPlNab3OuE4MKUQuv2lH+PLQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"@firebase/component": "0.7.0", "@firebase/component": "0.7.0",
"@firebase/logger": "0.5.0", "@firebase/logger": "0.5.0",
@ -2186,7 +2184,6 @@
"resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.6.tgz", "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.6.tgz",
"integrity": "sha512-YYGARbutghQY4zZUWMYia0ib0Y/rb52y72/N0z3vglRHL7ii/AaK9SA7S/dzScVOlCdnbHXz+sc5Dq+r8fwFAg==", "integrity": "sha512-YYGARbutghQY4zZUWMYia0ib0Y/rb52y72/N0z3vglRHL7ii/AaK9SA7S/dzScVOlCdnbHXz+sc5Dq+r8fwFAg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"@firebase/app": "0.14.6", "@firebase/app": "0.14.6",
"@firebase/component": "0.7.0", "@firebase/component": "0.7.0",
@ -2202,8 +2199,7 @@
"version": "0.9.3", "version": "0.9.3",
"resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz", "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz",
"integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==", "integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==",
"license": "Apache-2.0", "license": "Apache-2.0"
"peer": true
}, },
"node_modules/@firebase/auth": { "node_modules/@firebase/auth": {
"version": "1.12.0", "version": "1.12.0",
@ -2654,7 +2650,6 @@
"integrity": "sha512-0AZUyYUfpMNcztR5l09izHwXkZpghLgCUaAGjtMwXnCg3bj4ml5VgiwqOMOxJ+Nw4qN/zJAaOQBcJ7KGkWStqQ==", "integrity": "sha512-0AZUyYUfpMNcztR5l09izHwXkZpghLgCUaAGjtMwXnCg3bj4ml5VgiwqOMOxJ+Nw4qN/zJAaOQBcJ7KGkWStqQ==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"tslib": "^2.1.0" "tslib": "^2.1.0"
}, },
@ -3685,7 +3680,6 @@
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1", "fast-uri": "^3.0.1",
@ -3914,7 +3908,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.9.0", "baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759", "caniuse-lite": "^1.0.30001759",
@ -4748,7 +4741,6 @@
"resolved": "https://registry.npmjs.org/firebase/-/firebase-12.7.0.tgz", "resolved": "https://registry.npmjs.org/firebase/-/firebase-12.7.0.tgz",
"integrity": "sha512-ZBZg9jFo8uH4Emd7caOqtalKJfDGHnHQSrCPiqRAdTFQd0wL3ERilUBfhnhBLnlernugkN/o7nJa0p+sE71Izg==", "integrity": "sha512-ZBZg9jFo8uH4Emd7caOqtalKJfDGHnHQSrCPiqRAdTFQd0wL3ERilUBfhnhBLnlernugkN/o7nJa0p+sE71Izg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"@firebase/ai": "2.6.1", "@firebase/ai": "2.6.1",
"@firebase/analytics": "0.10.19", "@firebase/analytics": "0.10.19",
@ -5061,7 +5053,6 @@
"integrity": "sha512-QsCdAUHAmiDeKeaNojb1OHOPF7NjcWPBR7obdu3NwH2a/oyQaLg5d0aaCy/9My6CdPChYF07dvz5chaXBGaD4g==", "integrity": "sha512-QsCdAUHAmiDeKeaNojb1OHOPF7NjcWPBR7obdu3NwH2a/oyQaLg5d0aaCy/9My6CdPChYF07dvz5chaXBGaD4g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/node": "^20.0.0", "@types/node": "^20.0.0",
"@types/whatwg-mimetype": "^3.0.2", "@types/whatwg-mimetype": "^3.0.2",
@ -6378,7 +6369,6 @@
"integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"rollup": "dist/bin/rollup" "rollup": "dist/bin/rollup"
}, },
@ -7177,7 +7167,6 @@
"integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
"devOptional": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@ -7326,7 +7315,6 @@
"integrity": "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==", "integrity": "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.18.10", "esbuild": "^0.18.10",
"postcss": "^8.4.27", "postcss": "^8.4.27",
@ -8024,7 +8012,6 @@
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.26.tgz", "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.26.tgz",
"integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==", "integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@vue/compiler-dom": "3.5.26", "@vue/compiler-dom": "3.5.26",
"@vue/compiler-sfc": "3.5.26", "@vue/compiler-sfc": "3.5.26",
@ -8496,7 +8483,6 @@
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"rollup": "dist/bin/rollup" "rollup": "dist/bin/rollup"
}, },

View File

@ -1,25 +1,7 @@
import { describe, it, expect, vi } from 'vitest' import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import App from './App.vue' import App from './App.vue'
vi.mock('./firebase', () => ({
auth: {},
db: {}
}))
vi.mock('./composables/useAuth', () => {
const { ref } = require('vue')
return {
useAuth: () => ({
isAllowed: true,
user: ref(null),
login: vi.fn(),
logout: vi.fn(),
error: null
})
}
})
describe('Smoke Test', () => { describe('Smoke Test', () => {
it('mounts properly', () => { it('mounts properly', () => {
const wrapper = mount(App) const wrapper = mount(App)

View File

@ -2,9 +2,9 @@
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import Header from './components/layout/Header.vue' import Header from './components/layout/Header.vue'
import Hero from './components/sections/Hero.vue' import Hero from './components/sections/Hero.vue'
import ClubSpirit from './components/sections/ClubSpirit.vue' import About from './components/sections/About.vue'
import Impressum from './components/sections/Legal/Impressum.vue' import History from './components/sections/History.vue'
import Datenschutz from './components/sections/Legal/Datenschutz.vue' import Beer from './components/sections/Beer.vue'
import HaumdaucherGame from './components/layout/HaumdaucherGame.vue' import HaumdaucherGame from './components/layout/HaumdaucherGame.vue'
import { messages } from './locales/i18n' import { messages } from './locales/i18n'
import { useAuth } from './composables/useAuth' import { useAuth } from './composables/useAuth'
@ -12,7 +12,9 @@ import { useAuth } from './composables/useAuth'
const { isAllowed } = useAuth() const { isAllowed } = useAuth()
const theme = ref('classic') const theme = ref('classic')
const lang = ref<'de' | 'bar'>('de')
const showGame = ref(false) const showGame = ref(false)
const showBSOD = ref(false)
const isNatUnlocked = ref(false) const isNatUnlocked = ref(false)
const boars = ref<{id: number, top: number}[]>([]) const boars = ref<{id: number, top: number}[]>([])
@ -32,7 +34,7 @@ const unlockNat = () => {
} }
const handleBackdoor = () => { const handleBackdoor = () => {
const secret = prompt('Bitte Geheimcode eingeben (Unlock NAT):') const secret = prompt('Bitte Geheimcode eigem (Unlock NAT):')
if (secret?.toLowerCase() === 'nat mode') { if (secret?.toLowerCase() === 'nat mode') {
unlockNat() unlockNat()
alert('🐗 NAT-Modus freigschaltet! Wiedaschaun, reinghaun!') alert('🐗 NAT-Modus freigschaltet! Wiedaschaun, reinghaun!')
@ -51,18 +53,32 @@ const startBoarRun = () => {
}, 8000) }, 8000)
} }
const triggerBSOD = () => {
if (theme.value === 'win95') {
showBSOD.value = true
setTimeout(() => showBSOD.value = false, 3000)
}
}
const toggleLang = (newLang: 'de' | 'bar') => {
lang.value = newLang
localStorage.setItem('haumdaucher-lang', newLang)
}
onMounted(() => { onMounted(() => {
const savedTheme = localStorage.getItem('haumdaucher-theme') const savedTheme = localStorage.getItem('haumdaucher-theme')
if (savedTheme) toggleTheme(savedTheme) if (savedTheme) toggleTheme(savedTheme)
const savedLang = localStorage.getItem('haumdaucher-lang') as 'de' | 'bar'
if (savedLang) lang.value = savedLang
const savedNat = localStorage.getItem('haumdaucher-nat-unlocked') const savedNat = localStorage.getItem('haumdaucher-nat-unlocked')
if (savedNat === 'true') isNatUnlocked.value = true if (savedNat === 'true') isNatUnlocked.value = true
}) })
// Simplified translation: German only
const t = (key: string) => { const t = (key: string) => {
const keys = key.split('.') const keys = key.split('.')
let result: any = messages.de let result: any = messages[lang.value]
for (const k of keys) { for (const k of keys) {
if (result[k]) result = result[k] if (result[k]) result = result[k]
else return key else return key
@ -74,24 +90,23 @@ const t = (key: string) => {
<template> <template>
<Header <Header
:currentTheme="theme" :currentTheme="theme"
:currentLang="lang"
:isNatUnlocked="isNatUnlocked" :isNatUnlocked="isNatUnlocked"
@update:theme="toggleTheme" @update:theme="toggleTheme"
@update:lang="toggleLang"
@open:game="showGame = true" @open:game="showGame = true"
:t="t" :t="t"
/> />
<main> <main @click="triggerBSOD">
<!-- Member Banner -->
<Hero :theme="theme" :t="t" /> <Hero :theme="theme" :t="t" />
<ClubSpirit :t="t" id="spirit" /> <About :t="t" />
<History :t="t" />
<div class="legal-separator"></div> <Beer :t="t" />
<section id="impressum" class="legal-section">
<Impressum :t="t" />
</section>
<section id="datenschutz" class="legal-section">
<Datenschutz :t="t" />
</section>
<!-- Hidden Backdoor Pixel -->
<div class="backdoor" @click.stop="handleBackdoor"></div> <div class="backdoor" @click.stop="handleBackdoor"></div>
</main> </main>
@ -103,26 +118,35 @@ const t = (key: string) => {
@unlock-nat="unlockNat" @unlock-nat="unlockNat"
/> />
<div v-if="showBSOD" class="bsod">
<h1>:(</h1>
<p>A problem has been detected and Windows has been shut down to prevent damage to your Haumdaucher.</p>
<p>DRIVER_IRQL_NOT_LESS_OR_EQUAL</p>
<p>Press any key to continue... just kidding, it's a website.</p>
</div>
<div v-for="boar in boars" :key="boar.id" class="running-boar" :style="{ top: boar.top + '%' }">🐗💨</div> <div v-for="boar in boars" :key="boar.id" class="running-boar" :style="{ top: boar.top + '%' }">🐗💨</div>
<footer class="container" style="padding: 40px 0; opacity: 0.6; font-size: 0.9em;">
<footer class="site-footer"> © {{ new Date().getFullYear() }} Haumdaucher Regensburg. Alles für den Vogel.
<div class="container footer-content">
<div class="footer-info">
© {{ new Date().getFullYear() }} HAUMDAUCHER Wurst und Spezialitäten n.e.V.
</div>
<nav class="footer-links">
<a href="#impressum">Impressum</a>
<span class="sep"></span>
<a href="#datenschutz">Datenschutz</a>
</nav>
</div>
</footer> </footer>
</template> </template>
<style> <style>
@import './assets/styles/global.css'; @import './assets/styles/global.css';
.bsod {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #0000aa;
color: white;
z-index: 3000;
padding: 50px;
font-family: 'Courier New', monospace;
}
.running-boar { .running-boar {
position: fixed; position: fixed;
left: -100px; left: -100px;
@ -147,63 +171,4 @@ const t = (key: string) => {
opacity: 0; opacity: 0;
} }
.legal-section {
min-height: auto !important;
padding: 60px 20px !important;
opacity: 0.8;
}
.legal-separator {
height: 1px;
background: var(--glass-border);
margin: 40px 10% 0;
opacity: 0.3;
}
.site-footer {
padding: 60px 0 100px; /* Extra bottom padding for mobile drawer */
border-top: 1px solid var(--glass-border);
background: var(--bg-color);
opacity: 0.8;
font-size: 0.85rem;
}
.footer-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
}
.footer-links {
display: flex;
gap: 15px;
align-items: center;
}
.footer-links a {
color: inherit;
text-decoration: none;
font-weight: 500;
transition: opacity 0.2s;
}
.footer-links a:hover {
opacity: 1;
text-decoration: underline;
}
.sep {
opacity: 0.3;
}
@media (min-width: 768px) {
.site-footer {
padding: 60px 0;
}
.footer-content {
flex-direction: row;
justify-content: space-between;
}
}
</style> </style>

View File

@ -42,9 +42,10 @@ body {
.container { .container {
width: 100%; width: 100%;
max-width: 1000px; /* Centered content on desktop */ max-width: 100%;
margin: 0 auto; margin: 0;
padding: 0 20px; padding: 0 10px;
/* Reduced from 15px to save space */
box-sizing: border-box; box-sizing: border-box;
} }
@ -93,14 +94,43 @@ p {
max-width: 100%; max-width: 100%;
} }
/* Theme Overrides */ /* Theme Overrides (Remain same but refined) */
[data-theme='dark'] { [data-theme='unicorn'] {
--bg-color: #0f0f0f; --bg-color: #fff0fb;
--text-color: #f0f0f0; --text-color: #4a148c;
--primary-color: #f06292;
--accent-color: #ba68c8;
--header-bg: rgba(255, 240, 251, 0.85);
--glass-border: rgba(240, 98, 146, 0.2);
}
[data-theme='luxury'] {
--bg-color: #0a0a0a;
--text-color: #e0e0e0;
--primary-color: #d4af37; --primary-color: #d4af37;
--accent-color: #ffffff; --accent-color: #e5e4e2;
--header-bg: rgba(15, 15, 15, 0.85); --header-bg: rgba(10, 10, 10, 0.85);
--glass-border: rgba(212, 175, 55, 0.2); --glass-border: rgba(212, 175, 55, 0.3);
}
[data-theme='win95'] {
--bg-color: #008080;
--text-color: #000000;
--primary-color: #c0c0c0;
--accent-color: #808080;
--header-bg: #c0c0c0;
--glass-border: #ffffff;
--font-family: 'MS Sans Serif', Tahoma, sans-serif;
cursor: url('https://cur.cursors-4u.net/games/gam-4/gam373.cur'), auto;
}
[data-theme='win95'] * {
border-radius: 0 !important;
}
[data-theme='win95'] .fancy-glass {
border: 2px solid;
border-color: #ffffff #808080 #808080 #ffffff;
} }
[data-theme='nat'] { [data-theme='nat'] {

View File

@ -1,17 +1,17 @@
<script setup lang="ts"> <script setup lang="ts">
import { useAuth } from '../../composables/useAuth'
import { watch, ref } from 'vue'
const props = defineProps<{ const props = defineProps<{
currentTheme: string currentTheme: string
currentLang: string
isNatUnlocked: boolean isNatUnlocked: boolean
t: (key: string) => string t: (key: string) => string
}>() }>()
const emit = defineEmits(['update:theme', 'open:game']) const emit = defineEmits(['update:theme', 'update:lang', 'open:game'])
const themes = ['classic', 'dark', 'nat'] const themes = ['classic', 'unicorn', 'luxury', 'win95', 'nat']
import { useAuth } from '../../composables/useAuth'
import { watch, ref } from 'vue'
const { user, login, logout, error } = useAuth() const { user, login, logout, error } = useAuth()
const showSettings = ref(false) const showSettings = ref(false)
@ -29,8 +29,8 @@ watch(user, (u) => {
<div class="branding-area"> <div class="branding-area">
<div class="logo-text">HAUMDAUCHER</div> <div class="logo-text">HAUMDAUCHER</div>
<div v-if="user" class="status-message"> <div v-if="user" class="status-message">
<span class="desktop-msg">{{ isNatUnlocked ? 'Du bist a Haumdaucher 🫵 🍻' : 'Vielleicht... bist du a Haumdaucher' }}</span> <span class="desktop-msg">{{ isNatUnlocked || isAllowed ? 'Du bist a Haumdaucher 🫵 🍻' : 'Vielleicht... bist du a Haumdaucher' }}</span>
<span class="mobile-msg">{{ isNatUnlocked ? 'Haumdaucher! 🫵' : 'Vielleicht... 🤔' }}</span> <span class="mobile-msg">{{ isNatUnlocked || isAllowed ? 'Haumdaucher! 🫵' : 'Vielleicht... 🤔' }}</span>
</div> </div>
</div> </div>
@ -53,8 +53,19 @@ watch(user, (u) => {
</div> </div>
</div> </div>
<!-- Theme switch --> <!-- Combined switch for better mobile spacing -->
<div class="control-wrapper" :class="{ 'show-mobile': showSettings }"> <div class="control-wrapper" :class="{ 'show-mobile': showSettings }">
<div class="switch-group">
<button
v-for="l in ['de', 'bar']"
:key="l"
:class="{ active: currentLang === l }"
@click="emit('update:lang', l)"
>
{{ l.toUpperCase() }}
</button>
</div>
<div class="switch-group"> <div class="switch-group">
<template v-for="th in themes"> <template v-for="th in themes">
<button <button
@ -63,10 +74,12 @@ watch(user, (u) => {
:class="{ active: currentTheme === th }" :class="{ active: currentTheme === th }"
@click="emit('update:theme', th)" @click="emit('update:theme', th)"
class="theme-btn" class="theme-btn"
:title="t('themes.' + th)" :title="th"
> >
<span v-if="th === 'classic'"></span> <span v-if="th === 'classic'"></span>
<span v-if="th === 'dark'">🌙</span> <span v-if="th === 'unicorn'">🦄</span>
<span v-if="th === 'luxury'">👑</span>
<span v-if="th === 'win95'">💾</span>
<span v-if="th === 'nat'">🐗</span> <span v-if="th === 'nat'">🐗</span>
</button> </button>
</template> </template>
@ -79,17 +92,18 @@ watch(user, (u) => {
<!-- Mobile Bottom Nav --> <!-- Mobile Bottom Nav -->
<nav class="fancy-glass mobile-nav"> <nav class="fancy-glass mobile-nav">
<a href="#home" class="nav-item">🏠<span>{{ t('nav.home') }}</span></a> <a href="#home" class="nav-item">🏠<span>{{ t('nav.home') }}</span></a>
<a href="#spirit" class="nav-item"><span>{{ t('nav.spirit') }}</span></a> <a href="#about" class="nav-item">👥<span>{{ t('nav.about') }}</span></a>
<button class="nav-item game-btn" @click="emit('open:game')">🕹<span>{{ t('nav.game') }}</span></button> <button class="nav-item game-btn" @click="emit('open:game')">🕹<span>{{ t('nav.game') }}</span></button>
<a href="#impressum" class="nav-item"><span>Impressum</span></a> <a href="#history" class="nav-item">📜<span>{{ t('nav.history') }}</span></a>
<a href="#beer" class="nav-item">🍺<span>{{ t('nav.beer') }}</span></a>
</nav> </nav>
<!-- Desktop Side Nav / Links --> <!-- Desktop Side Nav / Links -->
<nav class="desktop-links"> <nav class="desktop-links">
<div class="container"> <div class="container">
<a href="#spirit">{{ t('nav.spirit') }}</a> <a href="#about">{{ t('nav.about') }}</a>
<a href="#impressum">Impressum</a> <a href="#history">{{ t('nav.history') }}</a>
<a href="#datenschutz">Datenschutz</a> <a href="#beer">{{ t('nav.beer') }}</a>
<button class="game-nav-btn" @click="emit('open:game')">{{ t('nav.game') }}</button> <button class="game-nav-btn" @click="emit('open:game')">{{ t('nav.game') }}</button>
</div> </div>
</nav> </nav>
@ -136,6 +150,8 @@ watch(user, (u) => {
display: flex; display: flex;
gap: 8px; gap: 8px;
align-items: center; align-items: center;
flex-wrap: wrap;
justify-content: flex-end;
} }
.status-message { .status-message {
@ -170,14 +186,12 @@ watch(user, (u) => {
position: absolute; position: absolute;
top: 60px; top: 60px;
right: 10px; right: 10px;
background: var(--header-bg); background: rgba(255, 255, 255, 0.95);
padding: 10px; padding: 10px;
border-radius: 12px; border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1); box-shadow: 0 4px 12px rgba(0,0,0,0.1);
flex-direction: column; flex-direction: column;
align-items: flex-end; align-items: flex-end;
border: 1px solid var(--glass-border);
backdrop-filter: blur(10px);
} }
.control-wrapper.show-mobile { .control-wrapper.show-mobile {
@ -214,7 +228,7 @@ button {
button.active { button.active {
background: var(--primary-color); background: var(--primary-color);
color: #fff; color: white;
opacity: 1; opacity: 1;
} }

View File

@ -0,0 +1,52 @@
<script setup lang="ts">
defineProps<{
t: (key: string) => string
}>()
</script>
<template>
<section id="about" class="about">
<div class="container fancy-glass alternate-bg">
<h2 class="section-title">{{ t('about.title') }}</h2>
<p class="section-text">{{ t('about.content') }}</p>
<div class="friends-grid">
<div v-for="i in 5" :key="i" class="friend-avatar">🍻</div>
</div>
</div>
</section>
</template>
<style scoped>
.about {
background: linear-gradient(180deg, transparent 0%, rgba(0,0,0,0.02) 100%);
}
.alternate-bg {
padding: 60px;
border-radius: 40px;
}
.section-title {
font-size: clamp(1.8rem, 8vw, 3rem);
margin-bottom: 20px;
}
.section-text {
font-size: clamp(1rem, 4vw, 1.3rem);
max-width: 800px;
margin: 0 auto 30px;
}
.friends-grid {
display: flex;
justify-content: center;
gap: 20px;
font-size: 2rem;
}
.friend-avatar {
background: rgba(var(--primary-color), 0.1);
padding: 20px;
border-radius: 50%;
}
</style>

View File

@ -0,0 +1,38 @@
<script setup lang="ts">
defineProps<{
t: (key: string) => string
}>()
</script>
<template>
<section id="beer" class="beer">
<div class="container fancy-glass alternate-bg">
<h2 class="section-title">{{ t('beer.title') }}</h2>
<p class="section-text">{{ t('beer.content') }}</p>
<div class="beer-animation">🍺 🍺 🍺</div>
</div>
</section>
</template>
<style scoped>
.history {
background-attachment: fixed;
}
.section-title {
font-size: clamp(1.8rem, 8vw, 3rem);
margin-bottom: 20px;
}
.section-text {
font-size: clamp(1rem, 4vw, 1.3rem);
max-width: 800px;
margin: 0 auto 30px;
}
.bird-accent {
font-size: 5rem;
margin-top: 40px;
opacity: 0.3;
}
</style>

View File

@ -1,55 +0,0 @@
<script setup lang="ts">
const props = defineProps<{
t: (key: string) => string
}>()
</script>
<template>
<section id="spirit" class="glass-section container">
<h2>{{ t('spirit.title') }}</h2>
<div class="content-text">
<p>{{ t('spirit.content') }}</p>
</div>
</section>
</template>
<style scoped>
.glass-section {
background: var(--glass-bg);
border: var(--glass-border);
box-shadow: var(--glass-shadow);
backdrop-filter: var(--glass-blur);
-webkit-backdrop-filter: var(--glass-blur);
border-radius: 20px;
padding: 40px;
margin-bottom: 40px;
color: var(--text-color);
transition: all 0.3s ease;
}
h2 {
font-size: 2.5rem;
margin-bottom: 20px;
color: var(--primary-color);
text-transform: uppercase;
letter-spacing: 2px;
}
.content-text {
font-size: 1.2rem;
line-height: 1.8;
white-space: pre-line;
}
@media (max-width: 480px) {
.glass-section {
padding: 20px;
}
h2 {
font-size: 2rem;
}
.content-text {
font-size: 1.1rem;
}
}
</style>

View File

@ -0,0 +1,47 @@
<script setup lang="ts">
defineProps<{
t: (key: string) => string
}>()
</script>
<template>
<section id="history" class="history">
<div class="container fancy-glass alternate-bg">
<h2 class="section-title">{{ t('history.title') }}</h2>
<p class="section-text">{{ t('history.content') }}</p>
<div class="bird-accent">🦢</div>
</div>
</section>
</template>
<style scoped>
.beer {
padding-bottom: 150px;
}
.alternate-bg {
padding: 60px;
border-radius: 40px;
}
.section-title {
font-size: clamp(1.8rem, 8vw, 3rem);
margin-bottom: 20px;
}
.section-text {
font-size: clamp(1rem, 4vw, 1.3rem);
max-width: 800px;
margin: 0 auto 30px;
}
.beer-animation {
font-size: 4rem;
animation: clink 2s infinite ease-in-out;
}
@keyframes clink {
0%, 100% { transform: rotate(0); }
50% { transform: rotate(10deg) scale(1.1); }
}
</style>

View File

@ -1,39 +0,0 @@
<script setup lang="ts">
const props = defineProps<{
t: (key: string) => string
}>()
</script>
<template>
<section id="datenschutz" class="glass-section container legal-section">
<h2>{{ t('datenschutz.title') }}</h2>
<div class="content-text">
<p>{{ t('datenschutz.content') }}</p>
</div>
</section>
</template>
<style scoped>
.legal-section {
background: rgba(0, 0, 0, 0.02);
border: var(--glass-border);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-radius: 12px;
padding: 30px;
margin-bottom: 20px;
}
h2 {
font-size: 1.5rem;
margin-bottom: 15px;
text-transform: uppercase;
}
.content-text {
font-size: 1rem;
line-height: 1.6;
white-space: pre-line;
opacity: 0.8;
}
</style>

View File

@ -1,40 +0,0 @@
<script setup lang="ts">
const props = defineProps<{
t: (key: string) => string
}>()
</script>
<template>
<section id="impressum" class="glass-section container legal-section">
<h2>{{ t('impressum.title') }}</h2>
<div class="content-text">
<p>{{ t('impressum.content') }}</p>
</div>
</section>
</template>
<style scoped>
.legal-section {
margin-top: 60px;
background: rgba(0, 0, 0, 0.02);
border: var(--glass-border);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-radius: 12px;
padding: 30px;
margin-bottom: 20px;
}
h2 {
font-size: 1.5rem;
margin-bottom: 15px;
text-transform: uppercase;
}
.content-text {
font-size: 1rem;
line-height: 1.6;
white-space: pre-line;
opacity: 0.8;
}
</style>

View File

@ -10,22 +10,18 @@ const isLoading = ref(true)
const error = ref<string | null>(null) const error = ref<string | null>(null)
// Watch auth state // Watch auth state
if (auth) { auth.onAuthStateChanged((u) => {
auth.onAuthStateChanged((u) => { console.log("🔥 Auth State Changed:", u ? u.email : "Logged Out")
console.log("🔥 Auth State Changed:", u ? u.email : "Logged Out") user.value = u
user.value = u if (!u) {
if (!u) { isAllowed.value = false
isAllowed.value = false isLoading.value = false
isLoading.value = false }
} })
})
} else {
isLoading.value = false
}
// Check allowlist // Check allowlist
watchEffect((onCleanup) => { watchEffect((onCleanup) => {
if (!user.value || !db) return if (!user.value) return
// Subscribe to the config/allowlist document // Subscribe to the config/allowlist document
const allowlistRef = doc(db, 'config', 'allowlist') const allowlistRef = doc(db, 'config', 'allowlist')
@ -50,10 +46,6 @@ watchEffect((onCleanup) => {
export function useAuth() { export function useAuth() {
const login = async () => { const login = async () => {
error.value = null error.value = null
if (!auth) {
error.value = "Firebase is not configured!"
return
}
try { try {
const provider = new GoogleAuthProvider() const provider = new GoogleAuthProvider()
await signInWithPopup(auth, provider) await signInWithPopup(auth, provider)
@ -66,7 +58,6 @@ export function useAuth() {
const logout = async () => { const logout = async () => {
error.value = null error.value = null
if (!auth) return
try { try {
await signOut(auth) await signOut(auth)
user.value = null user.value = null

View File

@ -13,14 +13,9 @@ const firebaseConfig = {
appId: import.meta.env.VITE_FIREBASE_APP_ID appId: import.meta.env.VITE_FIREBASE_APP_ID
} }
// Initialize Firebase safely // Initialize Firebase
export let auth: ReturnType<typeof getAuth> | null = null; const app = initializeApp(firebaseConfig)
export let db: ReturnType<typeof getFirestore> | null = null;
if (firebaseConfig.apiKey) { // Initialize services
const app = initializeApp(firebaseConfig) export const auth = getAuth(app)
auth = getAuth(app) export const db = getFirestore(app)
db = getFirestore(app)
} else {
console.warn("⚠️ Firebase API Key missing. Firebase features are disabled.")
}

View File

@ -2,28 +2,32 @@ export const messages = {
de: { de: {
nav: { nav: {
home: 'Start', home: 'Start',
spirit: 'Spirit', about: 'Über uns',
history: 'Geschichte',
beer: 'Bier & Gaudi',
game: 'Haumdaucher Game' game: 'Haumdaucher Game'
}, },
hero: { hero: {
title: 'Haumdaucher', title: 'D\'Haumdaucher',
subtitle: 'Wurst und Spezialitäten n.e.V. in Gründung' subtitle: 'Eine Gemeinschaft aus Regensburg offen für alle.'
}, },
spirit: { about: {
title: 'Unser Spirit', title: 'Wer wir sind',
content: 'Wir pflegen die Kultur der Kulinarik. Es liegt in unserer DNA, andere Menschen mit Spezialitäten zu bewirten und uns gegenseitig mit Köstlichkeiten zu verwöhnen. Der Fokus liegt bei uns auf aber nicht exklusiv bei echtem Wurst-Handwerk.' content: 'Wir sind eine Truppe von etwa 15 Freunden aus Regensburg, die sich schon ewig kennen. Doch wir sind kein geschlossener Kreis: Wir freuen uns über jede neue Bekanntschaft und sind offen für die Welt.'
}, },
impressum: { history: {
title: 'Impressum', title: 'Die Geschichte der Haumdaucher',
content: 'HAUMDAUCHER Wurst und Spezialitäten n.e.V. (in Gründung)\n\nVertreten durch:\nMoritz Graf (1. Vorstand)\nGrabengasse 7\n93059 Regensburg\n\nKontakt:\nTelefon: 094183065717\nE-Mail: info@haumdaucher.de' content: 'Der "Haubentaucher" ist ein edler Vogel. Wir haben daraus den "Haumdaucher" gemacht bayerisch, bodenständig, aber mit dem Blick über den Tellerrand.'
}, },
datenschutz: { beer: {
title: 'Datenschutzerklärung', title: 'Bier & Gaudi',
content: 'Wir nehmen den Schutz Ihrer persönlichen Daten sehr ernst. Als reine Repräsentationsseite erheben wir keine personenbezogenen Daten von Besuchern, es sei denn, dies ist technisch notwendig.\n\n**Google Firebase Authentication**\nZur Bereitstellung unseres Mitglieder-Logins nutzen wir Firebase Authentication (Google Ireland Limited). Dabei werden Daten (z.B. E-Mail) verarbeitet. Dies erfolgt auf Grundlage von Art. 6 Abs. 1 lit. b DSGVO. Daten können in die USA übertragen werden, abgesichert durch das EU-U.S. Data Privacy Framework und Standardvertragsklauseln.' content: 'Geselligkeit steht bei uns an erster Stelle. Ob beim bayerischen Hellen oder beim Austausch mit neuen Gesichtern bei uns ist jeder willkommen.'
}, },
themes: { themes: {
classic: 'Klassisch', classic: 'Klassisch',
dark: 'Dunkel', unicorn: 'Einhorn',
luxury: 'Luxus',
win95: 'Windows 95',
nat: 'NAT-Mode' nat: 'NAT-Mode'
}, },
game: { game: {
@ -33,5 +37,44 @@ export const messages = {
level: 'Level', level: 'Level',
win: 'NATinator bezwungen! Du bist a echter Haumdaucher.' win: 'NATinator bezwungen! Du bist a echter Haumdaucher.'
} }
},
bar: {
nav: {
home: 'Dahoam',
about: 'Wer mir san',
history: 'Wia ois ogfanga hod',
beer: 'Bia & Gaudi',
game: 'Haumdaucher Spui'
},
hero: {
title: 'D\'Haumdaucher',
subtitle: 'A echte Rengschbuaga Gschicht offn fia olle.'
},
about: {
title: 'Wer mir san',
content: 'Mir san a Hauffa vo ca. 15 Spezln aus Rengschbuag. Mia hoidn zam, oba mir san fia jedn offn, dea a bisserl a Gmiatlichkeit midbringt.'
},
history: {
title: 'Vom Haubndaucha zum Haumdaucha',
content: 'Da "Haubentaucher" is a scheena Vogl. Mia hom an "Haumdaucher" draus gmocht boarisch, ehrlich und offn fia de ganze Welt.'
},
beer: {
title: 'Bia & Gaudi',
content: 'Gmiatlickheit is des Wichtigste. Egal ob mid am gscheidn Hellen oda mid neie Leit bei uns ghead jeda dazua.'
},
themes: {
classic: 'Klassisch',
unicorn: 'Einhorn',
luxury: 'Luxus',
win95: 'Windows 95',
nat: 'NAT-Mode'
},
game: {
title: 'Haumdaucher Spui',
start: 'Af geht\'s!',
gameOver: 'Schluss is! Game Over.',
level: 'Level',
win: 'NATinator gschlong! Du bist a Pfundskerl.'
}
} }
} }