feat: Refine Header UI, inline Firestore rules, and fix mobile layout bugs

This commit is contained in:
Moritz Graf 2026-01-06 22:03:20 +01:00
parent edbb90e5e2
commit 36c3bcc98b
9 changed files with 141 additions and 33 deletions

3
.gitignore vendored
View File

@ -32,3 +32,6 @@ dist-ssr
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
# Deprecated (Inlined in Terraform)
firestore.rules

View File

@ -1,5 +1,8 @@
# GEMINI.md - Haumdaucher Project Handbook # GEMINI.md - Haumdaucher Project Handbook
## 🚨 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.
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

View File

@ -16,8 +16,8 @@ kubectl create namespace $NAMESPACE --dry-run=client -o yaml | kubectl apply -f
echo "📦 Building Docker image..." echo "📦 Building Docker image..."
# Try to fetch Firebase config from Terraform # Try to fetch Firebase config from Terraform
if [ -d "terraform" ] && [ -f "terraform/terraform.tfstate" ]; then if [ -d "terraform" ]; then
echo "🔍 Detected Terraform state. Fetching Firebase config..." echo "🔍 Detected Terraform directory. Fetching Firebase config..."
cd terraform cd terraform
TF_OUT=$(terraform output -json firebase_config 2>/dev/null) TF_OUT=$(terraform output -json firebase_config 2>/dev/null)
cd .. cd ..

View File

@ -6,7 +6,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<title>Haumdaucher Regensburg</title> <title>Haumdaucher Regensburg</title>
<meta name="theme-color" content="#ffffff" /> <meta name="theme-color" content="#ffffff" />
<meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" /> <meta name="apple-mobile-web-app-status-bar-style" content="default" />
<link rel="apple-touch-icon" href="/icon-192.png" /> <link rel="apple-touch-icon" href="/icon-192.png" />
</head> </head>

View File

@ -100,9 +100,6 @@ const t = (key: string) => {
<main @click="triggerBSOD"> <main @click="triggerBSOD">
<!-- Member Banner --> <!-- Member Banner -->
<div v-if="isAllowed" class="member-banner">
Do bist a haumdaucher 🫵 🍻
</div>
<Hero :theme="theme" :t="t" /> <Hero :theme="theme" :t="t" />
<About :t="t" /> <About :t="t" />
@ -174,20 +171,4 @@ const t = (key: string) => {
opacity: 0; opacity: 0;
} }
.member-banner {
background: linear-gradient(90deg, #ff9a9e 0%, #fad0c4 99%, #fad0c4 100%);
color: #555;
text-align: center;
padding: 10px;
font-weight: bold;
font-size: 1.2rem;
animation: slide-down 0.5s ease-out;
margin-top: 60px; /* Offset for fixed header */
border-bottom: 2px solid rgba(255,255,255,0.5);
}
@keyframes slide-down {
from { transform: translateY(-20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
</style> </style>

View File

@ -11,8 +11,9 @@ const emit = defineEmits(['update:theme', 'update:lang', 'open:game'])
const themes = ['classic', 'unicorn', 'luxury', 'win95', 'nat'] const themes = ['classic', 'unicorn', 'luxury', 'win95', 'nat']
import { useAuth } from '../../composables/useAuth' import { useAuth } from '../../composables/useAuth'
import { watch } from 'vue' import { watch, ref } from 'vue'
const { user, login, logout, error } = useAuth() const { user, login, logout, error } = useAuth()
const showSettings = ref(false)
watch(user, (u) => { watch(user, (u) => {
console.log("👤 Header: User changed:", u ? u.email : "null") console.log("👤 Header: User changed:", u ? u.email : "null")
@ -23,22 +24,37 @@ watch(user, (u) => {
<template> <template>
<header class="fancy-glass header-top"> <header class="fancy-glass header-top">
<div class="container top-content"> <div class="container top-content">
<!-- Logo & Status Area -->
<div class="branding-area">
<div class="logo-text">HAUMDAUCHER</div> <div class="logo-text">HAUMDAUCHER</div>
<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="mobile-msg">{{ isNatUnlocked || isAllowed ? 'Haumdaucher! 🫵' : 'Vielleicht... 🤔' }}</span>
</div>
</div>
<div class="controls"> <div class="controls">
<!-- Auth Control -->
<!-- Mobile Settings Toggle -->
<button class="settings-toggle" @click="showSettings = !showSettings">
</button>
<!-- Auth Control (Always Visible) -->
<div class="auth-control"> <div class="auth-control">
<div v-if="error" class="auth-error" :title="error"></div> <div v-if="error" class="auth-error" :title="error"></div>
<button v-if="!user" @click="login" class="login-btn"> <button v-if="!user" @click="login" class="login-btn">
Login Login
</button> </button>
<div v-else class="user-menu"> <div v-else class="user-menu">
<img :src="user.photoURL || ''" class="avatar" :title="user.displayName || ''" />
<button @click="logout" class="logout-btn">Exit</button> <button @click="logout" class="logout-btn">Exit</button>
<img :src="user.photoURL || ''" class="avatar" :title="user.displayName || ''" />
</div> </div>
</div> </div>
<!-- Combined switch for better mobile spacing --> <!-- Combined switch for better mobile spacing -->
<div class="control-wrapper"> <div class="control-wrapper" :class="{ 'show-mobile': showSettings }">
<div class="switch-group"> <div class="switch-group">
<button <button
@ -117,6 +133,12 @@ watch(user, (u) => {
white-space: nowrap; white-space: nowrap;
} }
/* Base Layout */
.branding-area {
display: flex;
align-items: center;
}
.controls { .controls {
display: flex; display: flex;
align-items: center; align-items: center;
@ -132,6 +154,58 @@ watch(user, (u) => {
justify-content: flex-end; justify-content: flex-end;
} }
.status-message {
font-size: 0.8rem;
font-weight: bold;
margin-left: 15px;
color: #555;
white-space: nowrap;
}
.mobile-msg { display: none; }
.settings-toggle { display: none; } /* Hidden on desktop */
/* Mobile adjustments */
@media (max-width: 480px) {
.branding-area {
flex-direction: column;
align-items: flex-start;
}
.status-message {
margin-left: 0;
margin-top: 2px;
}
.desktop-msg { display: none; }
.mobile-msg { display: inline; }
/* Settings Toggle Logic */
.control-wrapper {
display: none; /* Hidden by default on mobile */
position: absolute;
top: 60px;
right: 10px;
background: rgba(255, 255, 255, 0.95);
padding: 10px;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
flex-direction: column;
align-items: flex-end;
}
.control-wrapper.show-mobile {
display: flex;
}
.settings-toggle {
display: block;
font-size: 1.2rem;
padding: 5px;
margin-left: 5px;
}
}
.switch-group { .switch-group {
display: flex; display: flex;
background: rgba(0,0,0,0.05); background: rgba(0,0,0,0.05);

View File

@ -5,7 +5,7 @@ provider "registry.terraform.io/hashicorp/google" {
version = "5.45.2" version = "5.45.2"
constraints = "~> 5.0" constraints = "~> 5.0"
hashes = [ hashes = [
"h1:iy2Q9VcnMu4z/bH3v/NmI/nEpgYY7bXgJmT/hVTAUS4=", "h1:y2hf6zus1eKA5vpAfoyYkNBKDBQTVqmx4OVh3iRBaRo=",
"zh:0d09c8f20b556305192cdbe0efa6d333ceebba963a8ba91f9f1714b5a20c4b7a", "zh:0d09c8f20b556305192cdbe0efa6d333ceebba963a8ba91f9f1714b5a20c4b7a",
"zh:117143fc91be407874568df416b938a6896f94cb873f26bba279cedab646a804", "zh:117143fc91be407874568df416b938a6896f94cb873f26bba279cedab646a804",
"zh:16ccf77d18dd2c5ef9c0625f9cf546ebdf3213c0a452f432204c69feed55081e", "zh:16ccf77d18dd2c5ef9c0625f9cf546ebdf3213c0a452f432204c69feed55081e",
@ -25,7 +25,7 @@ provider "registry.terraform.io/hashicorp/google-beta" {
version = "5.45.2" version = "5.45.2"
constraints = "~> 5.0" constraints = "~> 5.0"
hashes = [ hashes = [
"h1:ME/cVZGNln4h166gyo9r7CuunzZ3FEqlIaNyQ0e9yjE=", "h1:r9Tpv9w6j6hTI7MR7zeaUveGsyt/yNXjCmuO80asz98=",
"zh:16b77bac5d1555b7f066ba8014f4fc8a6d0de64e252a1988d3fbb400984a4b19", "zh:16b77bac5d1555b7f066ba8014f4fc8a6d0de64e252a1988d3fbb400984a4b19",
"zh:1b13f515c4809343840aed8265915cc4191f138bdab5a8c5e1f542fdfc69989f", "zh:1b13f515c4809343840aed8265915cc4191f138bdab5a8c5e1f542fdfc69989f",
"zh:1dcce4309aeab7c88fd36aea664d57e620d8a413b967ce513a5a866e8de901f2", "zh:1dcce4309aeab7c88fd36aea664d57e620d8a413b967ce513a5a866e8de901f2",

View File

@ -40,6 +40,13 @@ resource "google_project_service" "firestore" {
disable_on_destroy = false disable_on_destroy = false
} }
resource "google_project_service" "firebaserules" {
provider = google-beta
project = var.project_id
service = "firebaserules.googleapis.com"
disable_on_destroy = false
}
# Firebase Project # Firebase Project
resource "google_firebase_project" "default" { resource "google_firebase_project" "default" {
provider = google-beta provider = google-beta
@ -75,6 +82,7 @@ resource "google_identity_platform_config" "default" {
"localhost", "localhost",
"${var.project_id}.firebaseapp.com", "${var.project_id}.firebaseapp.com",
"${var.project_id}.web.app", "${var.project_id}.web.app",
"haumdaucher.de",
] ]
# Enable Google Sign-In (and others if needed, but keeping it simple) # Enable Google Sign-In (and others if needed, but keeping it simple)
@ -143,3 +151,38 @@ resource "google_firestore_document" "allowlist" {
} }
}) })
} }
# Firestore Security Rules
resource "google_firebaserules_ruleset" "firestore" {
provider = google
source {
files {
name = "firestore.rules"
content = <<-EOT
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /config/allowlist {
allow read: if request.auth != null;
}
}
}
EOT
}
}
depends_on = [
google_project_service.firestore,
google_project_service.firebaserules
]
}
resource "google_firebaserules_release" "firestore" {
provider = google
project = var.project_id
name = "cloud.firestore" # This specific name targets the default Firestore database
ruleset_name = google_firebaserules_ruleset.firestore.name
depends_on = [google_firebaserules_ruleset.firestore]
}

View File

@ -19,10 +19,13 @@ terraform {
provider "google" { provider "google" {
project = var.project_id project = var.project_id
region = var.region region = var.region
billing_project = var.project_id
user_project_override = true
} }
provider "google-beta" { provider "google-beta" {
project = var.project_id project = var.project_id
region = var.region region = var.region
billing_project = var.project_id
user_project_override = true user_project_override = true
} }