Refactor: standardize on n.e.V. legal requirements, add Dark Mode, and update handbook

This commit is contained in:
Moritz Graf 2026-04-19 12:01:35 +02:00
parent fdc6a6df23
commit 1d85634740
5 changed files with 146 additions and 197 deletions

View File

@ -3,59 +3,58 @@
## 🚨 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 is designed to be humorous, culturally rich (Bavarian), and technically "surprising." **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."
## 🎨 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 (logo spin, hero text slide) and consistent hover states. - **Micro-interactions**: Subtle entrance animations 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. Persisted in `localStorage`. - **State Management**: Centralized in `App.vue` using standard `ref` hooks.
- **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`, `Unicorn`, `Luxury`, `Win95`, `NAT`. - Themes: `Classic` (Light), `Dark` (Premium Charcoal/Gold), `NAT` (Boar Easter Egg).
- **Localization**: - **Localization**:
- Centralized in `src/locales/i18n.ts`. - Centralized in `src/locales/i18n.ts`.
- Supports `de` (Standard German) and `bar` (Bavarian Dialect). - Language: `de` (Standard German) only.
- **PWA**:
- Managed via `vite-plugin-pwa`.
- Custom icons and standalone manifest for "Add to Home Screen" support.
## 🧪 Testing
- **Framework**: Vitest + HappyDOM.
- **Scope**: Lightweight sanity checks (e.g., verifying App mount).
- **Commands**:
- `npm test`: Run tests in watch mode.
- `npm test -- --run`: Run tests once (CI mode).
## 🕹 The Haumdaucher Game
- **Engine**: HTML5 Canvas rendering.
- **Controls**: Touch-responsive (horizontal drag) and Keyboard (Arrow Keys).
- **Thematization**: The game visual style (backgrounds, player, obstacles) changes dynamically based on the site's active theme.
- **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 ## 🚢 Deployment & DevOps
- **Docker**: Dual-stage build (Node build -> Nginx serving). Deployment is automated via the `./deploy.sh` script.
- **Registry**: `registry.haumdaucher.de`.
- **Kubernetes**: **Workflow Details:**
- Managed via `k8s-manifests.yaml`. 1. **Cloud Sync**: Script ensures the `haumdaucher` namespace exists in Kubernetes.
- Features `cert-manager` for SSL and `registry-haumdaucher-de` pull secret. 2. **Config Extraction**: Fetches Firebase credentials directly from Terraform outputs (`terraform output -json firebase_config`).
- **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. 3. **Build Pipeline**: Docker build passes Firebase credentials as `--build-arg` (VITE_FIREBASE_*).
4. **Distribution**: Pushes the image to `registry.haumdaucher.de/haumdaucher-website:latest`.
5. **K8s Update**: Applies `k8s-manifests.yaml` and triggers a `kubectl rollout restart` to fetch the new image.
## 🕹 The Haumdaucher Game
- **Engine**: HTML5 Canvas rendering. Game style changes dynamically based on the site's active theme.
- **Unlocking NAT Mode**:
- **Natural**: Reach Level 10.
- **Backdoor**: Single 1x1 pixel in the bottom-left corner. Type `nat mode` into the prompt.
## 📝 Ongoing Maintenance ## 📝 Ongoing Maintenance
- **Assets**: Static images should be placed in `public/images/` to avoid bundling issues in production containers. - **Assets**: Static images should be placed in `public/images/`.
- **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

@ -12,9 +12,7 @@ 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}[]>([])
@ -34,7 +32,7 @@ const unlockNat = () => {
} }
const handleBackdoor = () => { const handleBackdoor = () => {
const secret = prompt('Bitte Geheimcode eigem (Unlock NAT):') const secret = prompt('Bitte Geheimcode eingeben (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!')
@ -53,32 +51,18 @@ 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[lang.value] let result: any = messages.de
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
@ -90,25 +74,24 @@ 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 @click="triggerBSOD"> <main>
<!-- Member Banner -->
<Hero :theme="theme" :t="t" /> <Hero :theme="theme" :t="t" />
<ClubSpirit :t="t" /> <ClubSpirit :t="t" id="spirit" />
<!-- Legal Pages Section --> <div class="legal-separator"></div>
<Impressum :t="t" />
<Datenschutz :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>
@ -120,16 +103,19 @@ 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; text-align: center;">
© {{ new Date().getFullYear() }} HAUMDAUCHER Wurst und Spezialitäten n.e.V. (in Gründung) <footer class="site-footer">
<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>
@ -137,19 +123,6 @@ const t = (key: string) => {
<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;
@ -174,4 +147,63 @@ 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,10 +42,9 @@ body {
.container { .container {
width: 100%; width: 100%;
max-width: 100%; max-width: 1000px; /* Centered content on desktop */
margin: 0; margin: 0 auto;
padding: 0 10px; padding: 0 20px;
/* Reduced from 15px to save space */
box-sizing: border-box; box-sizing: border-box;
} }
@ -94,43 +93,14 @@ p {
max-width: 100%; max-width: 100%;
} }
/* Theme Overrides (Remain same but refined) */ /* Theme Overrides */
[data-theme='unicorn'] { [data-theme='dark'] {
--bg-color: #fff0fb; --bg-color: #0f0f0f;
--text-color: #4a148c; --text-color: #f0f0f0;
--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: #e5e4e2; --accent-color: #ffffff;
--header-bg: rgba(10, 10, 10, 0.85); --header-bg: rgba(15, 15, 15, 0.85);
--glass-border: rgba(212, 175, 55, 0.3); --glass-border: rgba(212, 175, 55, 0.2);
}
[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', 'update:lang', 'open:game']) const emit = defineEmits(['update:theme', 'open:game'])
const themes = ['classic', 'unicorn', 'luxury', 'win95', 'nat'] const themes = ['classic', 'dark', '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 || isAllowed ? 'Du bist a Haumdaucher 🫵 🍻' : 'Vielleicht... bist du a Haumdaucher' }}</span> <span class="desktop-msg">{{ isNatUnlocked ? 'Du bist a Haumdaucher 🫵 🍻' : 'Vielleicht... bist du a Haumdaucher' }}</span>
<span class="mobile-msg">{{ isNatUnlocked || isAllowed ? 'Haumdaucher! 🫵' : 'Vielleicht... 🤔' }}</span> <span class="mobile-msg">{{ isNatUnlocked ? 'Haumdaucher! 🫵' : 'Vielleicht... 🤔' }}</span>
</div> </div>
</div> </div>
@ -53,19 +53,8 @@ watch(user, (u) => {
</div> </div>
</div> </div>
<!-- Combined switch for better mobile spacing --> <!-- Theme switch -->
<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
@ -74,12 +63,10 @@ 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="th" :title="t('themes.' + th)"
> >
<span v-if="th === 'classic'"></span> <span v-if="th === 'classic'"></span>
<span v-if="th === 'unicorn'">🦄</span> <span v-if="th === 'dark'">🌙</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>
@ -149,8 +136,6 @@ 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 {
@ -185,12 +170,14 @@ watch(user, (u) => {
position: absolute; position: absolute;
top: 60px; top: 60px;
right: 10px; right: 10px;
background: rgba(255, 255, 255, 0.95); background: var(--header-bg);
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 {
@ -227,7 +214,7 @@ button {
button.active { button.active {
background: var(--primary-color); background: var(--primary-color);
color: white; color: #fff;
opacity: 1; opacity: 1;
} }

View File

@ -15,17 +15,15 @@ export const messages = {
}, },
impressum: { impressum: {
title: 'Impressum', title: 'Impressum',
content: 'HAUMDAUCHER Wurst und Spezialitäten n.e.V. (in Gründung)\n\nVertreten durch:\n[Name des Vertreters]\n[Straße Hausnummer]\n[PLZ Ort]\n\nKontakt:\nE-Mail: info@haumdaucher.de' 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'
}, },
datenschutz: { datenschutz: {
title: 'Datenschutzerklärung', title: 'Datenschutzerklärung',
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 für die Bereitstellung der Website absolut notwendig (z.B. Server-Logfiles). Wir verwenden keine Cookies zur Nachverfolgung Ihres Surfverhaltens.' 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.'
}, },
themes: { themes: {
classic: 'Klassisch', classic: 'Klassisch',
unicorn: 'Einhorn', dark: 'Dunkel',
luxury: 'Luxus',
win95: 'Windows 95',
nat: 'NAT-Mode' nat: 'NAT-Mode'
}, },
game: { game: {
@ -35,42 +33,5 @@ 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',
spirit: 'Unsara Spirit',
game: 'Haumdaucher Spui'
},
hero: {
title: 'D\'Haumdaucher',
subtitle: 'Wuarscht und Spezialitätn n.e.V. owei no am werkeln'
},
spirit: {
title: 'Unsara Spirit',
content: 'Mir lem d\'Kultur vom guatn Essn. Bei uns in da DNA steht gschriem, dass ma andre Leid mid Spezialitätn gmiatlich bekocht und si a moi gegenseitig verwehnt. Da Fokus liegt hoid oba ned nua af echtem Wuarscht-Handwerk.'
},
impressum: {
title: 'Impressum (Rechtlichs Zoig)',
content: 'HAUMDAUCHER Wurst und Spezialitäten n.e.V. (in Gründung)\n\nVadredn durch:\n[Name des Vertreters]\n[Straße Hausnummer]\n[PLZ Ort]\n\nKontakt:\nE-Mail: info@haumdaucher.de'
},
datenschutz: {
title: 'Datenschutz',
content: 'Mir passn af dei Datn auf wia af a rohes Ei. Weil des nua a Schauseitn is, sammln mia a koane Datn vo dir eib, außa des wos da Server unbedingt braucht damits lafft. Mir schiabn dir a koane Cookies unda.'
},
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.'
}
} }
} }