refactor: convert to minimal club website for Haumdaucher n.e.V.

This commit is contained in:
Moritz Graf
2026-04-19 09:32:03 +02:00
parent 36c3bcc98b
commit fdc6a6df23
14 changed files with 362 additions and 194 deletions
+19 -1
View File
@@ -1,7 +1,25 @@
import { describe, it, expect } from 'vitest'
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
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', () => {
it('mounts properly', () => {
const wrapper = mount(App)
+11 -8
View File
@@ -2,9 +2,9 @@
import { ref, onMounted } from 'vue'
import Header from './components/layout/Header.vue'
import Hero from './components/sections/Hero.vue'
import About from './components/sections/About.vue'
import History from './components/sections/History.vue'
import Beer from './components/sections/Beer.vue'
import ClubSpirit from './components/sections/ClubSpirit.vue'
import Impressum from './components/sections/Legal/Impressum.vue'
import Datenschutz from './components/sections/Legal/Datenschutz.vue'
import HaumdaucherGame from './components/layout/HaumdaucherGame.vue'
import { messages } from './locales/i18n'
import { useAuth } from './composables/useAuth'
@@ -102,9 +102,11 @@ const t = (key: string) => {
<!-- Member Banner -->
<Hero :theme="theme" :t="t" />
<About :t="t" />
<History :t="t" />
<Beer :t="t" />
<ClubSpirit :t="t" />
<!-- Legal Pages Section -->
<Impressum :t="t" />
<Datenschutz :t="t" />
<!-- Hidden Backdoor Pixel -->
<div class="backdoor" @click.stop="handleBackdoor"></div>
@@ -126,11 +128,12 @@ const t = (key: string) => {
</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;">
© {{ new Date().getFullYear() }} Haumdaucher Regensburg. Alles für den Vogel.
<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>
</template>
<style>
@import './assets/styles/global.css';
+5 -6
View File
@@ -92,18 +92,17 @@ watch(user, (u) => {
<!-- Mobile Bottom Nav -->
<nav class="fancy-glass mobile-nav">
<a href="#home" class="nav-item">🏠<span>{{ t('nav.home') }}</span></a>
<a href="#about" class="nav-item">👥<span>{{ t('nav.about') }}</span></a>
<a href="#spirit" class="nav-item">✨<span>{{ t('nav.spirit') }}</span></a>
<button class="nav-item game-btn" @click="emit('open:game')">🕹️<span>{{ t('nav.game') }}</span></button>
<a href="#history" class="nav-item">📜<span>{{ t('nav.history') }}</span></a>
<a href="#beer" class="nav-item">🍺<span>{{ t('nav.beer') }}</span></a>
<a href="#impressum" class="nav-item">⚖️<span>Impressum</span></a>
</nav>
<!-- Desktop Side Nav / Links -->
<nav class="desktop-links">
<div class="container">
<a href="#about">{{ t('nav.about') }}</a>
<a href="#history">{{ t('nav.history') }}</a>
<a href="#beer">{{ t('nav.beer') }}</a>
<a href="#spirit">{{ t('nav.spirit') }}</a>
<a href="#impressum">Impressum</a>
<a href="#datenschutz">Datenschutz</a>
<button class="game-nav-btn" @click="emit('open:game')">{{ t('nav.game') }}</button>
</div>
</nav>
-52
View File
@@ -1,52 +0,0 @@
<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>
-38
View File
@@ -1,38 +0,0 @@
<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>
+55
View File
@@ -0,0 +1,55 @@
<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>
-47
View File
@@ -1,47 +0,0 @@
<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>
@@ -0,0 +1,39 @@
<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>
@@ -0,0 +1,40 @@
<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>
+18 -9
View File
@@ -10,18 +10,22 @@ const isLoading = ref(true)
const error = ref<string | null>(null)
// Watch auth state
auth.onAuthStateChanged((u) => {
console.log("🔥 Auth State Changed:", u ? u.email : "Logged Out")
user.value = u
if (!u) {
isAllowed.value = false
isLoading.value = false
}
})
if (auth) {
auth.onAuthStateChanged((u) => {
console.log("🔥 Auth State Changed:", u ? u.email : "Logged Out")
user.value = u
if (!u) {
isAllowed.value = false
isLoading.value = false
}
})
} else {
isLoading.value = false
}
// Check allowlist
watchEffect((onCleanup) => {
if (!user.value) return
if (!user.value || !db) return
// Subscribe to the config/allowlist document
const allowlistRef = doc(db, 'config', 'allowlist')
@@ -46,6 +50,10 @@ watchEffect((onCleanup) => {
export function useAuth() {
const login = async () => {
error.value = null
if (!auth) {
error.value = "Firebase is not configured!"
return
}
try {
const provider = new GoogleAuthProvider()
await signInWithPopup(auth, provider)
@@ -58,6 +66,7 @@ export function useAuth() {
const logout = async () => {
error.value = null
if (!auth) return
try {
await signOut(auth)
user.value = null
+10 -5
View File
@@ -13,9 +13,14 @@ const firebaseConfig = {
appId: import.meta.env.VITE_FIREBASE_APP_ID
}
// Initialize Firebase
const app = initializeApp(firebaseConfig)
// Initialize Firebase safely
export let auth: ReturnType<typeof getAuth> | null = null;
export let db: ReturnType<typeof getFirestore> | null = null;
// Initialize services
export const auth = getAuth(app)
export const db = getFirestore(app)
if (firebaseConfig.apiKey) {
const app = initializeApp(firebaseConfig)
auth = getAuth(app)
db = getFirestore(app)
} else {
console.warn("⚠️ Firebase API Key missing. Firebase features are disabled.")
}
+23 -27
View File
@@ -2,26 +2,24 @@ export const messages = {
de: {
nav: {
home: 'Start',
about: 'Über uns',
history: 'Geschichte',
beer: 'Bier & Gaudi',
spirit: 'Spirit',
game: 'Haumdaucher Game'
},
hero: {
title: 'D\'Haumdaucher',
subtitle: 'Eine Gemeinschaft aus Regensburg – offen für alle.'
title: 'Haumdaucher',
subtitle: 'Wurst und Spezialitäten n.e.V. – in Gründung'
},
about: {
title: 'Wer wir sind',
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.'
spirit: {
title: 'Unser Spirit',
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.'
},
history: {
title: 'Die Geschichte der Haumdaucher',
content: 'Der "Haubentaucher" ist ein edler Vogel. Wir haben daraus den "Haumdaucher" gemacht – bayerisch, bodenständig, aber mit dem Blick über den Tellerrand.'
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'
},
beer: {
title: 'Bier & Gaudi',
content: 'Geselligkeit steht bei uns an erster Stelle. Ob beim bayerischen Hellen oder beim Austausch mit neuen Gesichtern – bei uns ist jeder willkommen.'
datenschutz: {
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.'
},
themes: {
classic: 'Klassisch',
@@ -41,26 +39,24 @@ export const messages = {
bar: {
nav: {
home: 'Dahoam',
about: 'Wer mir san',
history: 'Wia ois o’gfanga hod',
beer: 'Bia & Gaudi',
spirit: 'Unsara Spirit',
game: 'Haumdaucher Spui'
},
hero: {
title: 'D\'Haumdaucher',
subtitle: 'A echte Rengschbuaga G’schicht – offn fia olle.'
subtitle: 'Wuarscht und Spezialitätn n.e.V. – owei no am werkeln'
},
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.'
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.'
},
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.'
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'
},
beer: {
title: 'Bia & Gaudi',
content: 'Gmiatlickheit is des Wichtigste. Egal ob mid am gscheidn Hellen oda mid neie Leit – bei uns ghead jeda dazua.'
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',