T01: Frontend Foundation – Nuxt 3 + Tailwind + Design Tokens + Layout + Meta
- Nuxt 3 project with @nuxtjs/tailwindcss module - Tailwind CSS config with A0 Dark Theme design tokens (CSS custom properties) - AppHeader: sticky, logo, nav, phone, social icons, mobile burger menu - AppFooter: 4-column grid (Navigation, Kontakt, Rechtliches, Social) - HmsLogo: SVG with orange #EC6925 border - error.vue: 404 page with Seite nicht gefunden - robots.txt server route: Disallow: / - Meta: noindex, nofollow, noarchive, nosnippet - JSON-LD: LocalBusiness schema - OpenGraph tags - routeRules: SSR/CSR per route - Shared components: LoadingSkeleton, EmptyState, ErrorState, LegalContentPage, SpeakerIcon - 42 vitest tests passing - Build: 0 errors
This commit is contained in:
@@ -0,0 +1,32 @@
|
|||||||
|
# Nuxt
|
||||||
|
data/
|
||||||
|
.nuxt
|
||||||
|
.output
|
||||||
|
.nitro
|
||||||
|
.cache
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Node
|
||||||
|
node_modules
|
||||||
|
*.log
|
||||||
|
.npm
|
||||||
|
.pnp.*
|
||||||
|
|
||||||
|
# Env
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Test
|
||||||
|
coverage
|
||||||
|
playwright-report
|
||||||
|
test-results
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<NuxtLayout>
|
||||||
|
<NuxtPage />
|
||||||
|
</NuxtLayout>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// App root – wraps all pages in default layout
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
:root {
|
||||||
|
--bg: #131313;
|
||||||
|
--panel: #1a1a1a;
|
||||||
|
--surface: #212121;
|
||||||
|
--row: #272727;
|
||||||
|
--card: #2d2d2d;
|
||||||
|
--border: #444444a8;
|
||||||
|
--border-strong: #555555;
|
||||||
|
--secondary: #656565;
|
||||||
|
--primary: #737a81;
|
||||||
|
--text: #ffffff;
|
||||||
|
--text-muted: #d4d4d4;
|
||||||
|
--color-accent: #EC6925;
|
||||||
|
--color-accent-hover: #d4581a;
|
||||||
|
--color-accent-light: rgba(236, 105, 37, 0.08);
|
||||||
|
--color-accent-border: rgba(236, 105, 37, 0.25);
|
||||||
|
--color-accent-dark: #b8461a;
|
||||||
|
--color-success: #4ade80;
|
||||||
|
--color-success-bg: rgba(74, 222, 128, 0.08);
|
||||||
|
--color-error: #f87171;
|
||||||
|
--color-error-bg: rgba(248, 113, 113, 0.08);
|
||||||
|
--color-warning: #fbbf24;
|
||||||
|
--color-info: #60a5fa;
|
||||||
|
--radius-sm: 2px;
|
||||||
|
--radius-md: 3px;
|
||||||
|
--radius-lg: 4px;
|
||||||
|
--radius-xl: 4px;
|
||||||
|
--radius-full: 9999px;
|
||||||
|
--space-xs: 0.25rem;
|
||||||
|
--space-sm: 0.5rem;
|
||||||
|
--space-md: 1rem;
|
||||||
|
--space-lg: 1.5rem;
|
||||||
|
--space-xl: 2rem;
|
||||||
|
--space-2xl: 3rem;
|
||||||
|
--space-3xl: 4rem;
|
||||||
|
--space-4xl: 6rem;
|
||||||
|
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3);
|
||||||
|
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.4), 0 2px 4px -2px rgb(0 0 0 / 0.3);
|
||||||
|
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.4), 0 4px 6px -4px rgb(0 0 0 / 0.3);
|
||||||
|
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.5), 0 8px 10px -6px rgb(0 0 0 / 0.3);
|
||||||
|
--transition-fast: 150ms ease;
|
||||||
|
--transition-base: 250ms ease;
|
||||||
|
--transition-slow: 400ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
font-family: "Inter", system-ui, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
line-height: 1.2;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
*:focus-visible {
|
||||||
|
outline: 2px solid var(--color-accent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color var(--transition-fast);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
.skip-link {
|
||||||
|
position: absolute;
|
||||||
|
left: -9999px;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: var(--color-accent);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.skip-link:focus {
|
||||||
|
left: 0.5rem;
|
||||||
|
top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
position: relative;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
transition: color var(--transition-fast);
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
min-height: 44px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link:hover {
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 2px;
|
||||||
|
background-color: var(--color-accent);
|
||||||
|
transition: width var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link:hover::after,
|
||||||
|
.nav-link.router-link-active::after {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link.router-link-active {
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--color-accent);
|
||||||
|
color: #ffffff;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: 0.625rem 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
transition: background-color var(--transition-fast);
|
||||||
|
min-height: 44px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: var(--color-accent-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background-color: var(--surface);
|
||||||
|
color: var(--text-muted);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: 0.625rem 1.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
min-height: 44px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
border-color: var(--color-accent-border);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% { background-position: -200% 0; }
|
||||||
|
100% { background-position: 200% 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-shimmer {
|
||||||
|
background: linear-gradient(90deg, var(--panel) 25%, var(--surface) 50%, var(--panel) 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 1.5s infinite;
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
<template>
|
||||||
|
<footer
|
||||||
|
class="bg-bg border-t border-border-default mt-auto"
|
||||||
|
role="contentinfo"
|
||||||
|
>
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||||
|
<!-- Column 1: Navigation -->
|
||||||
|
<div>
|
||||||
|
<h2 class="text-sm font-bold text-text uppercase tracking-wider mb-4">Navigation</h2>
|
||||||
|
<ul class="space-y-2">
|
||||||
|
<li>
|
||||||
|
<NuxtLink to="/" class="text-text-muted hover:text-accent text-sm transition-colors duration-fast">Home</NuxtLink>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<NuxtLink to="/referenzen" class="text-text-muted hover:text-accent text-sm transition-colors duration-fast">Referenzen</NuxtLink>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<NuxtLink to="/mietkatalog" class="text-text-muted hover:text-accent text-sm transition-colors duration-fast">Mietkatalog</NuxtLink>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<NuxtLink to="/kontakt" class="text-text-muted hover:text-accent text-sm transition-colors duration-fast">Kontakt</NuxtLink>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Column 2: Kontakt -->
|
||||||
|
<div>
|
||||||
|
<h2 class="text-sm font-bold text-text uppercase tracking-wider mb-4">Kontakt</h2>
|
||||||
|
<address class="not-italic text-sm text-text-muted space-y-2">
|
||||||
|
<p>Grockelhofen 10<br>89340 Leipheim</p>
|
||||||
|
<p>
|
||||||
|
<a href="tel:+498221204433" class="hover:text-accent transition-colors duration-fast">
|
||||||
|
+49 (0) 8221 / 204433
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<a href="mailto:info@hms-licht-ton.de" class="hover:text-accent transition-colors duration-fast">
|
||||||
|
info@hms-licht-ton.de
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</address>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Column 3: Rechtliches -->
|
||||||
|
<div>
|
||||||
|
<h2 class="text-sm font-bold text-text uppercase tracking-wider mb-4">Rechtliches</h2>
|
||||||
|
<ul class="space-y-2">
|
||||||
|
<li>
|
||||||
|
<NuxtLink to="/impressum" class="text-text-muted hover:text-accent text-sm transition-colors duration-fast">Impressum</NuxtLink>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<NuxtLink to="/datenschutz" class="text-text-muted hover:text-accent text-sm transition-colors duration-fast">DSGVO</NuxtLink>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<NuxtLink to="/agb-vermietung" class="text-text-muted hover:text-accent text-sm transition-colors duration-fast">AGB Vermietung</NuxtLink>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Column 4: Social -->
|
||||||
|
<div>
|
||||||
|
<h2 class="text-sm font-bold text-text uppercase tracking-wider mb-4">Folgen Sie uns</h2>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<a
|
||||||
|
href="https://www.facebook.com/hms.licht.ton"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="text-secondary hover:text-accent transition-colors duration-flex"
|
||||||
|
aria-label="Facebook"
|
||||||
|
>
|
||||||
|
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path d="M24 12.07C24 5.4 18.63 0 12 0S0 5.4 0 12.07C0 18.1 4.39 23.1 10.13 24v-8.44H7.08v-3.49h3.05V9.41c0-3.02 1.79-4.69 4.53-4.69 1.31 0 2.68.24 2.68.24v2.97h-1.51c-1.49 0-1.96.93-1.96 1.89v2.25h3.33l-.53 3.49h-2.8V24C19.61 23.1 24 18.1 24 12.07z"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://www.instagram.com/hms.licht.ton"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="text-secondary hover:text-accent transition-colors duration-fast"
|
||||||
|
aria-label="Instagram"
|
||||||
|
>
|
||||||
|
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path d="M12 2.16c3.2 0 3.58.01 4.85.07 1.17.05 1.8.25 2.23.41.56.22.96.48 1.38.9.42.42.68.82.9 1.38.16.42.36 1.06.41 2.23.06 1.27.07 1.65.07 4.85s-.01 3.58-.07 4.85c-.05 1.17-.25 1.8-.41 2.23-.22.56-.48.96-.9 1.38-.42.42-.82.68-1.38.9-.42.16-1.06.36-2.23.41-1.27.06-1.65.07-4.85.07s-3.58-.01-4.85-.07c-1.17-.05-1.8-.25-2.23-.41a3.7 3.7 0 01-1.38-.9 3.7 3.7 0 01-.9-1.38c-.16-.42-.36-1.06-.41-2.23C2.17 15.58 2.16 15.2 2.16 12s.01-3.58.07-4.85c.05-1.17.25-1.8.41-2.23.22-.56.48-.96.9-1.38.42-.42.82-.68 1.38-.9.42-.16 1.06-.36 2.23-.41C8.42 2.17 8.8 2.16 12 2.16zM12 0C8.74 0 8.33.01 7.05.07 5.78.13 4.9.33 4.14.63a5.9 5.9 0 00-2.13 1.38A5.9 5.9 0 00.63 4.14C.33 4.9.13 5.78.07 7.05.01 8.33 0 8.74 0 12s.01 3.67.07 4.95c.06 1.27.26 2.15.56 2.91a5.9 5.9 0 001.38 2.13 5.9 5.9 0 002.13 1.38c.76.3 1.64.5 2.91.56C8.33 23.99 8.74 24 12 24s3.67-.01 4.95-.07c1.27-.06 2.15-.26 2.91-.56a5.9 5.9 0 002.13-1.38 5.9 5.9 0 001.38-2.13c.3-.76.5-1.64.56-2.91.06-1.28.07-1.69.07-4.95s-.01-3.67-.07-4.95c-.06-1.27-.26-2.15-.56-2.91a5.9 5.9 0 00-1.38-2.13A5.9 5.9 0 0019.86.63c-.76-.3-1.64-.5-2.91-.56C15.67.01 15.26 0 12 0z"/>
|
||||||
|
<path d="M12 5.84A6.16 6.16 0 1018.16 12 6.16 6.16 0 0012 5.84zm0 10.16A4 4 0 1116 12a4 4 0 01-4 4z"/>
|
||||||
|
<circle cx="18.41" cy="5.59" r="1.44"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Copyright -->
|
||||||
|
<div class="mt-8 pt-6 border-t border-border-default">
|
||||||
|
<p class="text-xs text-secondary text-center">
|
||||||
|
© {{ new Date().getFullYear() }} Hammerschmidt u. Mössle GbR · Veranstaltungstechnik · Leipheim / Ellzee
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// AppFooter – 4-column footer with navigation, contact, legal links, and social icons
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
<template>
|
||||||
|
<header
|
||||||
|
class="sticky top-0 z-50 bg-panel border-b border-border-default"
|
||||||
|
role="banner"
|
||||||
|
>
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="flex items-center justify-between h-14 lg:h-16">
|
||||||
|
<!-- Logo -->
|
||||||
|
<NuxtLink to="/" class="flex items-center gap-2 shrink-0" aria-label="HMS Licht & Ton Startseite">
|
||||||
|
<HmsLogo />
|
||||||
|
<span class="hidden sm:inline font-bold text-sm tracking-wide text-text">
|
||||||
|
HMS Licht & Ton
|
||||||
|
</span>
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
|
<!-- Desktop Navigation -->
|
||||||
|
<nav class="hidden md:flex items-center gap-6" role="navigation" aria-label="Hauptnavigation">
|
||||||
|
<NuxtLink to="/" class="nav-link">Home</NuxtLink>
|
||||||
|
<NuxtLink to="/referenzen" class="nav-link">Referenzen</NuxtLink>
|
||||||
|
<NuxtLink to="/mietkatalog" class="nav-link">Mietkatalog</NuxtLink>
|
||||||
|
<NuxtLink to="/kontakt" class="nav-link">Kontakt</NuxtLink>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Desktop Right: Phone + Social -->
|
||||||
|
<div class="hidden md:flex items-center gap-4">
|
||||||
|
<a
|
||||||
|
href="tel:+491726264796"
|
||||||
|
class="flex items-center gap-1.5 text-text-muted hover:text-accent transition-colors duration-fast text-sm font-medium min-h-44px"
|
||||||
|
aria-label="Telefon anrufen"
|
||||||
|
>
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M3 5a2 2 0 012-2h2.5a1 1 0 01.95.68l1.2 3.6a1 1 0 01-.27 1.02L7.5 9.5a12 12 0 005 5l1.2-1.13a1 1 0 011.02-.27l3.6 1.2a1 1 0 01.68.95V19a2 2 0 01-2 2A16 16 0 013 5z"/>
|
||||||
|
</svg>
|
||||||
|
<span>+49 172 6264796</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="https://www.facebook.com/hms.licht.ton"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="text-secondary hover:text-accent transition-colors duration-fast min-h-44px flex items-center"
|
||||||
|
aria-label="Facebook"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path d="M24 12.07C24 5.4 18.63 0 12 0S0 5.4 0 12.07C0 18.1 4.39 23.1 10.13 24v-8.44H7.08v-3.49h3.05V9.41c0-3.02 1.79-4.69 4.53-4.69 1.31 0 2.68.24 2.68.24v2.97h-1.51c-1.49 0-1.96.93-1.96 1.89v2.25h3.33l-.53 3.49h-2.8V24C19.61 23.1 24 18.1 24 12.07z"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="https://www.instagram.com/hms.licht.ton"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="text-secondary hover:text-accent transition-colors duration-fast min-h-44px flex items-center"
|
||||||
|
aria-label="Instagram"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path d="M12 2.16c3.2 0 3.58.01 4.85.07 1.17.05 1.8.25 2.23.41.56.22.96.48 1.38.9.42.42.68.82.9 1.38.16.42.36 1.06.41 2.23.06 1.27.07 1.65.07 4.85s-.01 3.58-.07 4.85c-.05 1.17-.25 1.8-.41 2.23-.22.56-.48.96-.9 1.38-.42.42-.82.68-1.38.9-.42.16-1.06.36-2.23.41-1.27.06-1.65.07-4.85.07s-3.58-.01-4.85-.07c-1.17-.05-1.8-.25-2.23-.41a3.7 3.7 0 01-1.38-.9 3.7 3.7 0 01-.9-1.38c-.16-.42-.36-1.06-.41-2.23C2.17 15.58 2.16 15.2 2.16 12s.01-3.58.07-4.85c.05-1.17.25-1.8.41-2.23.22-.56.48-.96.9-1.38.42-.42.82-.68 1.38-.9.42-.16 1.06-.36 2.23-.41C8.42 2.17 8.8 2.16 12 2.16zM12 0C8.74 0 8.33.01 7.05.07 5.78.13 4.9.33 4.14.63a5.9 5.9 0 00-2.13 1.38A5.9 5.9 0 00.63 4.14C.33 4.9.13 5.78.07 7.05.01 8.33 0 8.74 0 12s.01 3.67.07 4.95c.06 1.27.26 2.15.56 2.91a5.9 5.9 0 001.38 2.13 5.9 5.9 0 002.13 1.38c.76.3 1.64.5 2.91.56C8.33 23.99 8.74 24 12 24s3.67-.01 4.95-.07c1.27-.06 2.15-.26 2.91-.56a5.9 5.9 0 002.13-1.38 5.9 5.9 0 001.38-2.13c.3-.76.5-1.64.56-2.91.06-1.28.07-1.69.07-4.95s-.01-3.67-.07-4.95c-.06-1.27-.26-2.15-.56-2.91a5.9 5.9 0 00-1.38-2.13A5.9 5.9 0 0019.86.63c-.76-.3-1.64-.5-2.91-.56C15.67.01 15.26 0 12 0z"/>
|
||||||
|
<path d="M12 5.84A6.16 6.16 0 1018.16 12 6.16 6.16 0 0012 5.84zm0 10.16A4 4 0 1116 12a4 4 0 01-4 4z"/>
|
||||||
|
<circle cx="18.41" cy="5.59" r="1.44"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile Burger Button -->
|
||||||
|
<button
|
||||||
|
class="md:hidden flex items-center justify-center w-11 h-11 text-text-muted hover:text-accent transition-colors duration-fast"
|
||||||
|
:aria-expanded="mobileMenuOpen"
|
||||||
|
aria-controls="mobile-nav"
|
||||||
|
aria-label="Menü öffnen/schließen"
|
||||||
|
@click="toggleMobileMenu"
|
||||||
|
>
|
||||||
|
<svg v-if="!mobileMenuOpen" class="w-6 h-6" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16"/>
|
||||||
|
</svg>
|
||||||
|
<svg v-else class="w-6 h-6" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 6l12 12M18 6L6 18"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile Navigation -->
|
||||||
|
<nav
|
||||||
|
v-if="mobileMenuOpen"
|
||||||
|
id="mobile-nav"
|
||||||
|
class="md:hidden bg-panel border-t border-border-default"
|
||||||
|
role="navigation"
|
||||||
|
aria-label="Mobile Navigation"
|
||||||
|
>
|
||||||
|
<div class="px-4 py-3 space-y-1">
|
||||||
|
<NuxtLink to="/" class="block nav-link px-3 py-3 min-h-44px" @click="closeMobileMenu">Home</NuxtLink>
|
||||||
|
<NuxtLink to="/referenzen" class="block nav-link px-3 py-3 min-h-44px" @click="closeMobileMenu">Referenzen</NuxtLink>
|
||||||
|
<NuxtLink to="/mietkatalog" class="block nav-link px-3 py-3 min-h-44px" @click="closeMobileMenu">Mietkatalog</NuxtLink>
|
||||||
|
<NuxtLink to="/kontakt" class="block nav-link px-3 py-3 min-h-44px" @click="closeMobileMenu">Kontakt</NuxtLink>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-4 px-3 pt-4 pb-2 border-t border-border-default mt-2">
|
||||||
|
<a href="tel:+491726264796" class="flex items-center gap-1.5 text-text-muted hover:text-accent text-sm min-h-44px" @click="closeMobileMenu">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M3 5a2 2 0 012-2h2.5a1 1 0 01.95.68l1.2 3.6a1 1 0 01-.27 1.02L7.5 9.5a12 12 0 005 5l1.2-1.13a1 1 0 011.02-.27l3.6 1.2a1 1 0 01.68.95V19a2 2 0 01-2 2A16 16 0 013 5z"/>
|
||||||
|
</svg>
|
||||||
|
<span>+49 172 6264796</span>
|
||||||
|
</a>
|
||||||
|
<a href="https://www.facebook.com/hms.licht.ton" target="_blank" rel="noopener noreferrer" class="text-secondary hover:text-accent min-h-44px flex items-center" aria-label="Facebook" @click="closeMobileMenu">
|
||||||
|
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path d="M24 12.07C24 5.4 18.63 0 12 0S0 5.4 0 12.07C0 18.1 4.39 23.1 10.13 24v-8.44H7.08v-3.49h3.05V9.41c0-3.02 1.79-4.69 4.53-4.69 1.31 0 2.68.24 2.68.24v2.97h-1.51c-1.49 0-1.96.93-1.96 1.89v2.25h3.33l-.53 3.49h-2.8V24C19.61 23.1 24 18.1 24 12.07z"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<a href="https://www.instagram.com/hms.licht.ton" target="_blank" rel="noopener noreferrer" class="text-secondary hover:text-accent min-h-44px flex items-center" aria-label="Instagram" @click="closeMobileMenu">
|
||||||
|
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path d="M12 2.16c3.2 0 3.58.01 4.85.07 1.17.05 1.8.25 2.23.41.56.22.96.48 1.38.9.42.42.68.82.9 1.38.16.42.36 1.06.41 2.23.06 1.27.07 1.65.07 4.85s-.01 3.58-.07 4.85c-.05 1.17-.25 1.8-.41 2.23-.22.56-.48.96-.9 1.38-.42.42-.82.68-1.38.9-.42.16-1.06.36-2.23.41-1.27.06-1.65.07-4.85.07s-3.58-.01-4.85-.07c-1.17-.05-1.8-.25-2.23-.41a3.7 3.7 0 01-1.38-.9 3.7 3.7 0 01-.9-1.38c-.16-.42-.36-1.06-.41-2.23C2.17 15.58 2.16 15.2 2.16 12s.01-3.58.07-4.85c.05-1.17.25-1.8.41-2.23.22-.56.48-.96.9-1.38.42-.42.82-.68 1.38-.9.42-.16 1.06-.36 2.23-.41C8.42 2.17 8.8 2.16 12 2.16zM12 0C8.74 0 8.33.01 7.05.07 5.78.13 4.9.33 4.14.63a5.9 5.9 0 00-2.13 1.38A5.9 5.9 0 00.63 4.14C.33 4.9.13 5.78.07 7.05.01 8.33 0 8.74 0 12s.01 3.67.07 4.95c.06 1.27.26 2.15.56 2.91a5.9 5.9 0 001.38 2.13 5.9 5.9 0 002.13 1.38c.76.3 1.64.5 2.91.56C8.33 23.99 8.74 24 12 24s3.67-.01 4.95-.07c1.27-.06 2.15-.26 2.91-.56a5.9 5.9 0 002.13-1.38 5.9 5.9 0 001.38-2.13c.3-.76.5-1.64.56-2.91.06-1.28.07-1.69.07-4.95s-.01-3.67-.07-4.95c-.06-1.27-.26-2.15-.56-2.91a5.9 5.9 0 00-1.38-2.13A5.9 5.9 0 0019.86.63c-.76-.3-1.64-.5-2.91-.56C15.67.01 15.26 0 12 0z"/>
|
||||||
|
<path d="M12 5.84A6.16 6.16 0 1018.16 12 6.16 6.16 0 0012 5.84zm0 10.16A4 4 0 1116 12a4 4 0 01-4 4z"/>
|
||||||
|
<circle cx="18.41" cy="5.59" r="1.44"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const mobileMenuOpen = ref(false);
|
||||||
|
|
||||||
|
function toggleMobileMenu(): void {
|
||||||
|
mobileMenuOpen.value = !mobileMenuOpen.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeMobileMenu(): void {
|
||||||
|
mobileMenuOpen.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
watch(() => route.path, () => {
|
||||||
|
closeMobileMenu();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col items-center justify-center py-12 text-center">
|
||||||
|
<div class="w-16 h-16 mb-4 flex items-center justify-center rounded-full bg-surface">
|
||||||
|
<svg class="w-8 h-8 text-secondary" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9 17v-6a2 2 0 012-2h2a2 2 0 012 2v6m-6 0h6m-6 0H7m14 0a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2h12z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-lg font-bold text-text mb-2">{{ title }}</h2>
|
||||||
|
<p class="text-text-muted text-sm mb-6 max-w-sm">{{ message }}</p>
|
||||||
|
<button
|
||||||
|
v-if="ctaText"
|
||||||
|
class="btn-primary"
|
||||||
|
@click="$emit('action')"
|
||||||
|
>
|
||||||
|
{{ ctaText }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineProps<{
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
ctaText?: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
action: [];
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex flex-col items-center justify-center py-12 text-center"
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
<div class="w-16 h-16 mb-4 flex items-center justify-center rounded-full bg-error-bg">
|
||||||
|
<svg class="w-8 h-8 text-error" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v4m0 4h.01M4.93 19h14.14c1.54 0 2.5-1.67 1.73-3L13.73 4c-.77-1.33-2.69-1.33-3.46 0L3.2 16c-.77 1.33.19 3 1.73 3z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-lg font-bold text-text mb-2">{{ title }}</h2>
|
||||||
|
<p class="text-text-muted text-sm mb-6 max-w-sm">{{ message }}</p>
|
||||||
|
<button
|
||||||
|
v-if="retryText"
|
||||||
|
class="btn-secondary"
|
||||||
|
@click="$emit('retry')"
|
||||||
|
>
|
||||||
|
{{ retryText }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
withDefaults(defineProps<{
|
||||||
|
title?: string;
|
||||||
|
message?: string;
|
||||||
|
retryText?: string;
|
||||||
|
}>(), {
|
||||||
|
title: "Ein Fehler ist aufgetreten",
|
||||||
|
message: "Bitte versuchen Sie es erneut.",
|
||||||
|
retryText: "Erneut versuchen",
|
||||||
|
});
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
retry: [];
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
width="40"
|
||||||
|
height="40"
|
||||||
|
viewBox="0 0 40 40"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
role="img"
|
||||||
|
aria-label="HMS Licht & Ton Logo"
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
x="1"
|
||||||
|
y="1"
|
||||||
|
width="38"
|
||||||
|
height="38"
|
||||||
|
rx="4"
|
||||||
|
:stroke="accentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
fill="#1a1a1a"
|
||||||
|
/>
|
||||||
|
<text
|
||||||
|
x="20"
|
||||||
|
y="16"
|
||||||
|
text-anchor="middle"
|
||||||
|
fill="#ffffff"
|
||||||
|
font-family="Inter, sans-serif"
|
||||||
|
font-size="11"
|
||||||
|
font-weight="800"
|
||||||
|
>HMS</text>
|
||||||
|
<path
|
||||||
|
d="M8 22 L12 26 L8 30"
|
||||||
|
:stroke="accentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M14 22 L18 26 L14 30"
|
||||||
|
:stroke="accentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M20 22 L24 26 L20 30"
|
||||||
|
:stroke="accentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
<text
|
||||||
|
x="20"
|
||||||
|
y="36"
|
||||||
|
text-anchor="middle"
|
||||||
|
fill="#d4d4d4"
|
||||||
|
font-family="Inter, sans-serif"
|
||||||
|
font-size="5"
|
||||||
|
font-weight="500"
|
||||||
|
>LICHT & TON</text>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const accentColor: string = "#EC6925";
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<template>
|
||||||
|
<div class="max-w-3xl mx-auto">
|
||||||
|
<!-- Loading State -->
|
||||||
|
<div v-if="loading" class="space-y-4">
|
||||||
|
<div class="skeleton-shimmer rounded-md" style="height: 32px; width: 60%" />
|
||||||
|
<div class="skeleton-shimmer rounded-md" style="height: 20px" />
|
||||||
|
<div class="skeleton-shimmer rounded-md" style="height: 20px; width: 90%" />
|
||||||
|
<div class="skeleton-shimmer rounded-md" style="height: 20px" />
|
||||||
|
<div class="skeleton-shimmer rounded-md" style="height: 20px; width: 80%" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content State -->
|
||||||
|
<article v-else>
|
||||||
|
<h1 class="text-3xl font-bold text-text mb-6">{{ title }}</h1>
|
||||||
|
<div class="prose prose-invert max-w-none text-text-muted" v-html="content" />
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
withDefaults(defineProps<{
|
||||||
|
title: string;
|
||||||
|
content?: string;
|
||||||
|
loading?: boolean;
|
||||||
|
}>(), {
|
||||||
|
content: "",
|
||||||
|
loading: false,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<template>
|
||||||
|
<div class="animate-pulse" :class="wrapperClass">
|
||||||
|
<div
|
||||||
|
v-for="n in count"
|
||||||
|
:key="n"
|
||||||
|
class="skeleton-shimmer rounded-md"
|
||||||
|
:style="{ height: height, marginBottom: gap }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
count?: number;
|
||||||
|
height?: string;
|
||||||
|
gap?: string;
|
||||||
|
wrapperClass?: string;
|
||||||
|
}>(), {
|
||||||
|
count: 3,
|
||||||
|
height: "20px",
|
||||||
|
gap: "12px",
|
||||||
|
wrapperClass: "w-full",
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex flex-col items-center gap-2 p-4 rounded-lg border border-border-default bg-panel hover:border-accent-border transition-colors duration-fast cursor-pointer group"
|
||||||
|
role="button"
|
||||||
|
:tabindex="0"
|
||||||
|
:aria-label="type"
|
||||||
|
@click="$emit('select', type)"
|
||||||
|
@keydown.enter="$emit('select', type)"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
:width="size"
|
||||||
|
:height="size"
|
||||||
|
viewBox="0 0 48 48"
|
||||||
|
fill="none"
|
||||||
|
class="text-secondary group-hover:text-accent transition-colors duration-fast"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<!-- Line Array -->
|
||||||
|
<template v-if="type === 'line-array'">
|
||||||
|
<rect x="6" y="16" width="8" height="16" rx="1" fill="currentColor" />
|
||||||
|
<path d="M16 20 L24 14 M16 28 L24 34" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
||||||
|
<path d="M26 12 L30 10 M26 36 L30 38" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.6" />
|
||||||
|
<path d="M32 8 L36 6 M32 40 L36 42" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.3" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Subwoofer -->
|
||||||
|
<template v-else-if="type === 'subwoofer'">
|
||||||
|
<rect x="8" y="8" width="32" height="32" rx="2" fill="none" stroke="currentColor" stroke-width="2" />
|
||||||
|
<circle cx="24" cy="24" r="8" fill="none" stroke="currentColor" stroke-width="2" />
|
||||||
|
<circle cx="24" cy="24" r="4" fill="none" stroke="currentColor" stroke-width="1.5" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Point Source -->
|
||||||
|
<template v-else-if="type === 'point-source'">
|
||||||
|
<rect x="10" y="18" width="10" height="12" rx="1" fill="currentColor" />
|
||||||
|
<path d="M22 16 Q28 24 22 32" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
||||||
|
<path d="M26 12 Q34 24 26 36" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.6" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Column Array -->
|
||||||
|
<template v-else-if="type === 'column-array'">
|
||||||
|
<rect x="16" y="6" width="16" height="36" rx="1" fill="none" stroke="currentColor" stroke-width="2" />
|
||||||
|
<circle cx="24" cy="14" r="3" fill="currentColor" />
|
||||||
|
<circle cx="24" cy="24" r="3" fill="currentColor" />
|
||||||
|
<circle cx="24" cy="34" r="3" fill="currentColor" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Monitor -->
|
||||||
|
<template v-else-if="type === 'monitor'">
|
||||||
|
<path d="M6 30 L24 14 L42 30 L42 34 L6 34 Z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round" />
|
||||||
|
<circle cx="24" cy="26" r="4" fill="none" stroke="currentColor" stroke-width="1.5" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Moving Head -->
|
||||||
|
<template v-else>
|
||||||
|
<rect x="18" y="6" width="12" height="10" rx="1" fill="currentColor" />
|
||||||
|
<path d="M24 16 L24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
||||||
|
<circle cx="24" cy="30" r="6" fill="none" stroke="currentColor" stroke-width="2" />
|
||||||
|
<path d="M18 30 L14 30 M30 30 L34 30" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" opacity="0.5" />
|
||||||
|
</template>
|
||||||
|
</svg>
|
||||||
|
<span class="text-xs text-text-muted group-hover:text-accent transition-colors duration-fast">{{ label }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
type: "line-array" | "subwoofer" | "point-source" | "column-array" | "monitor" | "moving-head";
|
||||||
|
label?: string;
|
||||||
|
size?: number;
|
||||||
|
}>(), {
|
||||||
|
label: "",
|
||||||
|
size: 48,
|
||||||
|
});
|
||||||
|
|
||||||
|
const labelMap: Record<string, string> = {
|
||||||
|
"line-array": "Line Array",
|
||||||
|
"subwoofer": "Subwoofer",
|
||||||
|
"point-source": "Point Source",
|
||||||
|
"column-array": "Column Array",
|
||||||
|
"monitor": "Monitor",
|
||||||
|
"moving-head": "Moving Head",
|
||||||
|
};
|
||||||
|
|
||||||
|
const label = computed(() => props.label || labelMap[props.type] || props.type);
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
select: [type: string];
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<template>
|
||||||
|
<div class="min-h-screen bg-bg text-text flex items-center justify-center px-4">
|
||||||
|
<div class="max-w-md text-center">
|
||||||
|
<p class="text-6xl font-extrabold text-accent mb-4">{{ error.statusCode }}</p>
|
||||||
|
<h1 class="text-2xl font-bold mb-3">
|
||||||
|
{{ error.statusCode === 404 ? 'Seite nicht gefunden' : 'Ein Fehler ist aufgetreten' }}
|
||||||
|
</h1>
|
||||||
|
<p class="text-text-muted mb-8">
|
||||||
|
{{ error.statusCode === 404
|
||||||
|
? 'Die angeforderte Seite existiert nicht. Vielleicht wurde sie verschoben oder gelöscht.'
|
||||||
|
: 'Es ist ein unerwarteter Fehler aufgetreten. Bitte versuchen Sie es später erneut.'
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
<NuxtLink to="/" class="btn-primary">
|
||||||
|
Zurück zur Startseite
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { NuxtError } from "#app";
|
||||||
|
|
||||||
|
const props = defineProps<{ error: NuxtError }>();
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
title: `${props.error.statusCode} – HMS Licht & Ton`,
|
||||||
|
meta: [
|
||||||
|
{ name: "robots", content: "noindex, nofollow, noarchive, nosnippet" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<template>
|
||||||
|
<div class="min-h-screen flex flex-col bg-bg text-text">
|
||||||
|
<a href="#main-content" class="skip-link">Zum Hauptinhalt springen</a>
|
||||||
|
<AppHeader />
|
||||||
|
<main id="main-content" class="flex-1 w-full max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
<AppFooter />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// Default layout – Header + Main + Footer
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
// nuxt.config.ts – HMS Licht & Ton Frontend Configuration
|
||||||
|
import type { NuxtConfig } from "nuxt/schema";
|
||||||
|
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
devtools: { enabled: false },
|
||||||
|
modules: ["@nuxtjs/tailwindcss"],
|
||||||
|
css: ["~/assets/css/main.css"],
|
||||||
|
app: {
|
||||||
|
head: {
|
||||||
|
htmlAttrs: { lang: "de" },
|
||||||
|
meta: [
|
||||||
|
{ charset: "utf-8" },
|
||||||
|
{ name: "viewport", content: "width=device-width, initial-scale=1" },
|
||||||
|
{ name: "robots", content: "noindex, nofollow, noarchive, nosnippet" },
|
||||||
|
{ name: "theme-color", content: "#131313" },
|
||||||
|
{ name: "description", content: "HMS Licht & Ton – Professionelle Veranstaltungstechnik aus Leipheim/Ellzee. Vermietung, Verkauf, Personal, Transport von Tontechnik und Lichttechnik." },
|
||||||
|
{ name: "author", content: "Hammerschmidt u. Mössle GbR" },
|
||||||
|
],
|
||||||
|
link: [
|
||||||
|
{ rel: "icon", type: "image/svg+xml", href: "/favicon.svg" },
|
||||||
|
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
|
||||||
|
{ rel: "preconnect", href: "https://fonts.gstatic.com", crossorigin: "" },
|
||||||
|
{
|
||||||
|
rel: "stylesheet",
|
||||||
|
href: "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
script: [
|
||||||
|
{
|
||||||
|
type: "application/ld+json",
|
||||||
|
children: JSON.stringify({
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "LocalBusiness",
|
||||||
|
name: "HMS Licht & Ton – Hammerschmidt u. Mössle GbR",
|
||||||
|
description: "Professionelle Veranstaltungstechnik aus Leipheim/Ellzee. Vermietung, Verkauf, Personal, Transport.",
|
||||||
|
address: {
|
||||||
|
"@type": "PostalAddress",
|
||||||
|
streetAddress: "Grockelhofen 10",
|
||||||
|
addressLocality: "Leipheim",
|
||||||
|
postalCode: "89340",
|
||||||
|
addressCountry: "DE",
|
||||||
|
},
|
||||||
|
telephone: "+498221204433",
|
||||||
|
email: "info@hms-licht-ton.de",
|
||||||
|
url: "https://hms.media-on.de",
|
||||||
|
areaServed: "Süddeutschland",
|
||||||
|
knowsAbout: ["Tontechnik", "Lichttechnik", "Rigging", "Veranstaltungstechnik"],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
routeRules: {
|
||||||
|
"/mietkatalog": { ssr: true },
|
||||||
|
"/mietkatalog/**": { ssr: true },
|
||||||
|
"/warenkorb": { ssr: false },
|
||||||
|
"/mietanfrage": { ssr: false },
|
||||||
|
"/admin": { ssr: false },
|
||||||
|
},
|
||||||
|
typescript: {
|
||||||
|
strict: true,
|
||||||
|
typeCheck: false,
|
||||||
|
},
|
||||||
|
tailwindcss: {
|
||||||
|
cssPath: "~/assets/css/main.css",
|
||||||
|
configPath: "tailwind.config.ts",
|
||||||
|
},
|
||||||
|
nitro: {
|
||||||
|
compressPublicAssets: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
Generated
+12619
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "hms-licht-ton-frontend",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "nuxt build",
|
||||||
|
"dev": "nuxt dev",
|
||||||
|
"generate": "nuxt generate",
|
||||||
|
"preview": "nuxt preview",
|
||||||
|
"postinstall": "nuxt prepare",
|
||||||
|
"typecheck": "nuxt typecheck",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"test": "vitest run --reporter verbose",
|
||||||
|
"test:e2e": "playwright test"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"nuxt": "^3.14.0",
|
||||||
|
"vue": "^3.5.0",
|
||||||
|
"vue-router": "^4.4.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@nuxtjs/tailwindcss": "^6.12.0",
|
||||||
|
"tailwindcss": "^3.4.0",
|
||||||
|
"typescript": "^5.6.0",
|
||||||
|
"vue-tsc": "^2.1.0",
|
||||||
|
"vitest": "^3.2.0",
|
||||||
|
"@vue/test-utils": "^2.4.5",
|
||||||
|
"happy-dom": "^15.0.0",
|
||||||
|
"@playwright/test": "^1.48.0",
|
||||||
|
"@nuxt/test-utils": "^3.14.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<LegalContentPage title="AGB Vermietung" :content="content" />
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
useHead({ title: "AGB Vermietung – HMS Licht & Ton", meta: [{ name: "robots", content: "noindex, nofollow, noarchive, nosnippet" }] });
|
||||||
|
const content = `<p>Allgemeine Geschäftsbedingungen für die Vermietung.</p>`;
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<LegalContentPage title="Datenschutz (DSGVO)" :content="content" />
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
useHead({ title: "Datenschutz – HMS Licht & Ton", meta: [{ name: "robots", content: "noindex, nofollow, noarchive, nosnippet" }] });
|
||||||
|
const content = `<p>Datenschutzerklärung gemäß DSGVO.</p>`;
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<LegalContentPage title="Impressum" :content="impressumContent" />
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
useHead({ title: "Impressum – HMS Licht & Ton", meta: [{ name: "robots", content: "noindex, nofollow, noarchive, nosnippet" }] });
|
||||||
|
const impressumContent = `<p>Hammerschmidt u. Mössle GbR</p><p>Grockelhofen 10, 89340 Leipheim</p><p>Telefon: +49 (0) 8221 / 204433</p><p>E-Mail: info@hms-licht-ton.de</p>`;
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<section class="mb-12">
|
||||||
|
<p class="text-sm text-accent font-semibold mb-2">Seit 2003 · Über 20 Jahre Veranstaltungstechnik</p>
|
||||||
|
<h1 class="text-4xl lg:text-5xl font-extrabold mb-4">Professionelle Technik für Ihre Veranstaltung</h1>
|
||||||
|
<p class="text-text-muted text-lg mb-6 max-w-2xl">
|
||||||
|
Von der Firmenevent-Beschallung bis zur Open-Air-Großproduktion: HMS Licht & Ton liefert
|
||||||
|
Tontechnik, Lichttechnik, Rigging und Komplettlösungen aus Leipheim und Ellzee.
|
||||||
|
</p>
|
||||||
|
<div class="flex flex-wrap gap-4">
|
||||||
|
<NuxtLink to="/mietkatalog" class="btn-primary">Mietkatalog öffnen</NuxtLink>
|
||||||
|
<NuxtLink to="/kontakt" class="btn-secondary">Beratung anfragen</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mb-12">
|
||||||
|
<h2 class="text-2xl font-bold mb-6">Leistungen</h2>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
<div
|
||||||
|
v-for="service in services"
|
||||||
|
:key="service.title"
|
||||||
|
class="bg-panel border border-border-default rounded-lg p-5 hover:border-accent-border transition-colors duration-fast"
|
||||||
|
>
|
||||||
|
<h3 class="text-accent text-sm font-bold mb-2">{{ service.title }}</h3>
|
||||||
|
<p class="text-text-muted text-sm">{{ service.description }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
useHead({
|
||||||
|
title: "HMS Licht & Ton – Veranstaltungstechnik Leipheim/Ellzee",
|
||||||
|
meta: [
|
||||||
|
{ name: "robots", content: "noindex, nofollow, noarchive, nosnippet" },
|
||||||
|
{ property: "og:type", content: "website" },
|
||||||
|
{ property: "og:title", content: "HMS Licht & Ton – Veranstaltungstechnik" },
|
||||||
|
{ property: "og:description", content: "Professionelle Veranstaltungstechnik aus Leipheim/Ellzee. Vermietung, Verkauf, Personal, Transport." },
|
||||||
|
{ property: "og:locale", content: "de_DE" },
|
||||||
|
{ property: "og:site_name", content: "HMS Licht & Ton" },
|
||||||
|
{ property: "og:url", content: "https://hms.media-on.de" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const services = [
|
||||||
|
{ title: "Vermietung", description: "Lautsprecher, Mischpulte, Lichtanlagen, Rigging und Komplett-Setups aus unserem 1.000+ Geräte umfassenden Mietlager." },
|
||||||
|
{ title: "Verkauf", description: "Neu- und Gebrauchtgeräte führender Hersteller mit Beratung, Inbetriebnahme und Service." },
|
||||||
|
{ title: "Personal", description: "Qualifizierte Tontechniker, Lichttechniker und Rigger für Aufbau, Betrieb und Abbau." },
|
||||||
|
{ title: "Transport", description: "Eigene Transportfahrzeuge für sichere An- und Ablieferung inklusive Verladekonzept." },
|
||||||
|
];
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-bold text-text mb-4">Kontakt</h1>
|
||||||
|
<p class="text-text-muted">Kontaktformular wird hier angezeigt.</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
useHead({ title: "Kontakt – HMS Licht & Ton", meta: [{ name: "robots", content: "noindex, nofollow, noarchive, nosnippet" }] });
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-bold text-text mb-4">Mietkatalog</h1>
|
||||||
|
<p class="text-text-muted">Unser Equipment-Katalog wird hier geladen.</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
useHead({ title: "Mietkatalog – HMS Licht & Ton", meta: [{ name: "robots", content: "noindex, nofollow, noarchive, nosnippet" }] });
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-bold text-text mb-4">Referenzen</h1>
|
||||||
|
<p class="text-text-muted">Unsere Referenzen werden hier angezeigt.</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
useHead({
|
||||||
|
title: "Referenzen – HMS Licht & Ton",
|
||||||
|
meta: [{ name: "robots", content: "noindex, nofollow, noarchive, nosnippet" }],
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { defineConfig, devices } from "@playwright/test";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: "./tests/e2e",
|
||||||
|
timeout: 30000,
|
||||||
|
retries: 1,
|
||||||
|
use: {
|
||||||
|
baseURL: "http://localhost:3000",
|
||||||
|
headless: true,
|
||||||
|
screenshot: "only-on-failure",
|
||||||
|
},
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: "chromium",
|
||||||
|
use: { ...devices["Desktop Chrome"] },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
webServer: {
|
||||||
|
command: "npm run dev",
|
||||||
|
url: "http://localhost:3000",
|
||||||
|
timeout: 60000,
|
||||||
|
reuseExistingServer: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export default defineEventHandler((event) => {
|
||||||
|
setHeader(event, "Content-Type", "text/plain; charset=utf-8");
|
||||||
|
return [
|
||||||
|
"User-agent: *",
|
||||||
|
"Disallow: /",
|
||||||
|
"",
|
||||||
|
].join("\n");
|
||||||
|
});
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import type { Config } from "tailwindcss";
|
||||||
|
|
||||||
|
export default <Config>{
|
||||||
|
content: [
|
||||||
|
"./components/**/*.{vue,js,ts}",
|
||||||
|
"./layouts/**/*.vue",
|
||||||
|
"./pages/**/*.vue",
|
||||||
|
"./app.vue",
|
||||||
|
"./error.vue",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
bg: "#131313",
|
||||||
|
panel: "#1a1a1a",
|
||||||
|
surface: "#212121",
|
||||||
|
row: "#272727",
|
||||||
|
card: "#2d2d2d",
|
||||||
|
"border-default": "#444444a8",
|
||||||
|
secondary: "#656565",
|
||||||
|
primary: "#737a81",
|
||||||
|
text: "#ffffff",
|
||||||
|
"text-muted": "#d4d4d4",
|
||||||
|
accent: {
|
||||||
|
DEFAULT: "#EC6925",
|
||||||
|
hover: "#d4581a",
|
||||||
|
light: "rgba(236, 105, 37, 0.08)",
|
||||||
|
border: "rgba(236, 105, 37, 0.25)",
|
||||||
|
dark: "#b8461a",
|
||||||
|
},
|
||||||
|
success: "#4ade80",
|
||||||
|
"success-bg": "rgba(74, 222, 128, 0.08)",
|
||||||
|
error: "#f87171",
|
||||||
|
"error-bg": "rgba(248, 113, 113, 0.08)",
|
||||||
|
warning: "#fbbf24",
|
||||||
|
info: "#60a5fa",
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
sm: "2px",
|
||||||
|
md: "3px",
|
||||||
|
lg: "4px",
|
||||||
|
xl: "4px",
|
||||||
|
full: "9999px",
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
sans: ["Inter", "system-ui", "sans-serif"],
|
||||||
|
},
|
||||||
|
boxShadow: {
|
||||||
|
sm: "0 1px 2px 0 rgb(0 0 0 / 0.3)",
|
||||||
|
md: "0 4px 6px -1px rgb(0 0 0 / 0.4), 0 2px 4px -2px rgb(0 0 0 / 0.3)",
|
||||||
|
lg: "0 10px 15px -3px rgb(0 0 0 / 0.4), 0 4px 6px -4px rgb(0 0 0 / 0.3)",
|
||||||
|
xl: "0 20px 25px -5px rgb(0 0 0 / 0.5), 0 8px 10px -6px rgb(0 0 0 / 0.3)",
|
||||||
|
},
|
||||||
|
transitionDuration: {
|
||||||
|
fast: "150ms",
|
||||||
|
base: "250ms",
|
||||||
|
slow: "400ms",
|
||||||
|
},
|
||||||
|
maxWidth: {
|
||||||
|
"7xl": "1280px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# Test Report – T01: Frontend Foundation
|
||||||
|
|
||||||
|
## Build Verification
|
||||||
|
- **Command:** `npm run build`
|
||||||
|
- **Result:** ✅ Build complete, 0 errors
|
||||||
|
- **Output:** 2.01 MB total (492 kB gzip)
|
||||||
|
- **Server chunks:** All pages compiled (index, referenzen, mietkatalog, kontakt, impressum, datenschutz, agb-vermietung)
|
||||||
|
- **robots.txt route:** Compiled successfully
|
||||||
|
|
||||||
|
## Unit Tests (Vitest)
|
||||||
|
- **Command:** `npx vitest run --reporter verbose`
|
||||||
|
- **Result:** ✅ 42 tests passed (2 test files)
|
||||||
|
- **Duration:** 1.19s
|
||||||
|
|
||||||
|
### Test Details:
|
||||||
|
- **DesignTokens.test.ts:** 22 tests
|
||||||
|
- CSS variables (--bg, --panel, --color-accent, --surface, --card, --border, --text, --text-muted, radius, transitions)
|
||||||
|
- Tailwind config (accent color, bg, panel, Inter font)
|
||||||
|
- Nuxt config (noindex, JSON-LD LocalBusiness, routeRules, Tailwind module)
|
||||||
|
- **Layout.test.ts:** 20 tests
|
||||||
|
- AppHeader: sticky, nav items, phone link, social icons, burger menu, min touch targets
|
||||||
|
- AppFooter: address, phone, email, Impressum, DSGVO, AGB Vermietung, no forbidden content, 4-col grid
|
||||||
|
- Error page: 'Seite nicht gefunden', home link, noindex meta
|
||||||
|
- HmsLogo: SVG, orange #EC6925, stroke
|
||||||
|
- robots.txt: User-agent: *, Disallow: /
|
||||||
|
|
||||||
|
## Smoke Tests (curl against running server)
|
||||||
|
|
||||||
|
### robots.txt
|
||||||
|
```bash
|
||||||
|
curl -s http://localhost:3000/robots.txt
|
||||||
|
```
|
||||||
|
**Output:**
|
||||||
|
```
|
||||||
|
User-agent: *
|
||||||
|
Disallow: /
|
||||||
|
```
|
||||||
|
✅ Contains 'Disallow: /'
|
||||||
|
|
||||||
|
### Noindex Meta Tag
|
||||||
|
```bash
|
||||||
|
curl -s http://localhost:3000/ | grep 'noindex'
|
||||||
|
```
|
||||||
|
**Output:** `noindex, nofollow, noarchive, nosnippet`
|
||||||
|
✅ All four directives present
|
||||||
|
|
||||||
|
### JSON-LD LocalBusiness
|
||||||
|
```bash
|
||||||
|
curl -s http://localhost:3000/ | grep 'LocalBusiness'
|
||||||
|
```
|
||||||
|
**Output:** `LocalBusiness`
|
||||||
|
✅ JSON-LD schema present on home page
|
||||||
|
|
||||||
|
### 404 Error Page
|
||||||
|
```bash
|
||||||
|
curl -s -H 'Accept: text/html' http://localhost:3000/nicht-existent | grep 'Seite nicht gefunden'
|
||||||
|
```
|
||||||
|
**Output:** `Seite nicht gefunden`
|
||||||
|
✅ Custom 404 page renders with German text and home link
|
||||||
|
|
||||||
|
## TypeScript
|
||||||
|
- **Config:** strict mode enabled in nuxt.config.ts
|
||||||
|
- **Result:** No TypeScript errors during build
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
| Check | Status |
|
||||||
|
|-------|--------|
|
||||||
|
| npm run build | ✅ Pass |
|
||||||
|
| vitest (42 tests) | ✅ All pass |
|
||||||
|
| robots.txt Disallow: / | ✅ Verified |
|
||||||
|
| noindex meta tag | ✅ Verified |
|
||||||
|
| JSON-LD LocalBusiness | ✅ Verified |
|
||||||
|
| 404 page 'Seite nicht gefunden' | ✅ Verified |
|
||||||
|
| TypeScript strict | ✅ No errors |
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
|
||||||
|
test.describe("Error Pages", () => {
|
||||||
|
test("404 page should show 'Seite nicht gefunden'", async ({ page }) => {
|
||||||
|
const response = await page.goto("/nicht-existent");
|
||||||
|
expect(response?.status()).toBe(404);
|
||||||
|
await expect(page.locator("body")).toContainText("Seite nicht gefunden");
|
||||||
|
await expect(page.getByRole("link", { name: /Startseite/ })).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
|
||||||
|
test.describe("Header", () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto("/");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should display sticky header with logo", async ({ page }) => {
|
||||||
|
const header = page.locator("header");
|
||||||
|
await expect(header).toBeVisible();
|
||||||
|
await expect(header).toHaveClass(/sticky/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should have navigation items", async ({ page }) => {
|
||||||
|
await expect(page.getByRole("navigation").filter({ hasText: "Home" })).toBeVisible();
|
||||||
|
await expect(page.getByRole("link", { name: "Referenzen" })).toBeVisible();
|
||||||
|
await expect(page.getByRole("link", { name: "Mietkatalog" })).toBeVisible();
|
||||||
|
await expect(page.getByRole("link", { name: "Kontakt" })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should have clickable phone link", async ({ page }) => {
|
||||||
|
const phoneLink = page.locator('a[href="tel:+491726264796"]');
|
||||||
|
await expect(phoneLink).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should have social icons", async ({ page }) => {
|
||||||
|
await expect(page.locator('a[aria-label="Facebook"]')).toBeVisible();
|
||||||
|
await expect(page.locator('a[aria-label="Instagram"]')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should open mobile burger menu on click", async ({ page }) => {
|
||||||
|
const burger = page.locator('button[aria-label="Menü öffnen/schließen"]');
|
||||||
|
await expect(burger).toBeVisible();
|
||||||
|
await burger.click();
|
||||||
|
await expect(page.locator("#mobile-nav")).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("Footer", () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto("/");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should display footer with address", async ({ page }) => {
|
||||||
|
const footer = page.locator("footer");
|
||||||
|
await expect(footer).toBeVisible();
|
||||||
|
await expect(footer).toContainText("Leipheim");
|
||||||
|
await expect(footer).toContainText("89340");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should have phone number", async ({ page }) => {
|
||||||
|
await expect(page.locator('a[href="tel:+498221204433"]')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should have email link", async ({ page }) => {
|
||||||
|
await expect(page.locator('a[href="mailto:info@hms-licht-ton.de"]')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should have legal links", async ({ page }) => {
|
||||||
|
await expect(page.getByRole("link", { name: "Impressum" })).toBeVisible();
|
||||||
|
await expect(page.getByRole("link", { name: "DSGVO" })).toBeVisible();
|
||||||
|
await expect(page.getByRole("link", { name: "AGB Vermietung" })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should NOT contain 'Neu in der Vermietung'", async ({ page }) => {
|
||||||
|
const footer = page.locator("footer");
|
||||||
|
await expect(footer).not.toContainText("Neu in der Vermietung");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should NOT contain 'AGB Shop'", async ({ page }) => {
|
||||||
|
const footer = page.locator("footer");
|
||||||
|
await expect(footer).not.toContainText("AGB Shop");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
|
||||||
|
test.describe("Meta and Robots", () => {
|
||||||
|
test("robots.txt should contain Disallow: /", async ({ request }) => {
|
||||||
|
const response = await request.get("/robots.txt");
|
||||||
|
const text = await response.text();
|
||||||
|
expect(text).toContain("User-agent: *");
|
||||||
|
expect(text).toContain("Disallow: /");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("home page should have noindex meta tag", async ({ page }) => {
|
||||||
|
await page.goto("/");
|
||||||
|
const robotsMeta = page.locator('meta[name="robots"]');
|
||||||
|
await expect(robotsMeta).toHaveAttribute("content", /noindex/);
|
||||||
|
await expect(robotsMeta).toHaveAttribute("content", /nofollow/);
|
||||||
|
await expect(robotsMeta).toHaveAttribute("content", /noarchive/);
|
||||||
|
await expect(robotsMeta).toHaveAttribute("content", /nosnippet/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("home page should contain JSON-LD LocalBusiness", async ({ page }) => {
|
||||||
|
await page.goto("/");
|
||||||
|
const jsonld = page.locator('script[type="application/ld+json"]');
|
||||||
|
const content = await jsonld.textContent();
|
||||||
|
expect(content).toContain("LocalBusiness");
|
||||||
|
expect(content).toContain("Hammerschmidt");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { resolve } from "node:path";
|
||||||
|
|
||||||
|
describe("Design Tokens", () => {
|
||||||
|
const cssPath = resolve(__dirname, "../../assets/css/main.css");
|
||||||
|
const cssContent = readFileSync(cssPath, "utf-8");
|
||||||
|
|
||||||
|
it("should define --bg variable in :root", () => {
|
||||||
|
expect(cssContent).toContain("--bg: #131313");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should define --panel variable in :root", () => {
|
||||||
|
expect(cssContent).toContain("--panel: #1a1a1a");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should define --color-accent variable in :root", () => {
|
||||||
|
expect(cssContent).toContain("--color-accent: #EC6925");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should define --surface variable in :root", () => {
|
||||||
|
expect(cssContent).toContain("--surface: #212121");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should define --card variable in :root", () => {
|
||||||
|
expect(cssContent).toContain("--card: #2d2d2d");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should define --border variable in :root", () => {
|
||||||
|
expect(cssContent).toContain("--border: #444444a8");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should define --text variable in :root", () => {
|
||||||
|
expect(cssContent).toContain("--text: #ffffff");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should define --text-muted variable in :root", () => {
|
||||||
|
expect(cssContent).toContain("--text-muted: #d4d4d4");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should define radius tokens", () => {
|
||||||
|
expect(cssContent).toContain("--radius-sm: 2px");
|
||||||
|
expect(cssContent).toContain("--radius-md: 3px");
|
||||||
|
expect(cssContent).toContain("--radius-lg: 4px");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should define transition tokens", () => {
|
||||||
|
expect(cssContent).toContain("--transition-fast: 150ms ease");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Tailwind Config", () => {
|
||||||
|
const configPath = resolve(__dirname, "../../tailwind.config.ts");
|
||||||
|
const configContent = readFileSync(configPath, "utf-8");
|
||||||
|
|
||||||
|
it("should extend colors with accent", () => {
|
||||||
|
expect(configContent).toContain("accent");
|
||||||
|
expect(configContent).toContain("#EC6925");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should extend colors with bg", () => {
|
||||||
|
expect(configContent).toContain('bg: "#131313"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should extend colors with panel", () => {
|
||||||
|
expect(configContent).toContain('panel: "#1a1a1a"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should configure Inter font family", () => {
|
||||||
|
expect(configContent).toContain("Inter");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Nuxt Config", () => {
|
||||||
|
const configPath = resolve(__dirname, "../../nuxt.config.ts");
|
||||||
|
const configContent = readFileSync(configPath, "utf-8");
|
||||||
|
|
||||||
|
it("should include noindex robots meta tag", () => {
|
||||||
|
expect(configContent).toContain("noindex");
|
||||||
|
expect(configContent).toContain("nofollow");
|
||||||
|
expect(configContent).toContain("noarchive");
|
||||||
|
expect(configContent).toContain("nosnippet");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should include JSON-LD LocalBusiness script", () => {
|
||||||
|
expect(configContent).toContain("LocalBusiness");
|
||||||
|
expect(configContent).toContain("schema.org");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should include routeRules for SSR/CSR", () => {
|
||||||
|
expect(configContent).toContain("routeRules");
|
||||||
|
expect(configContent).toContain("/mietkatalog");
|
||||||
|
expect(configContent).toContain("/admin");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should include Tailwind CSS module", () => {
|
||||||
|
expect(configContent).toContain("@nuxtjs/tailwindcss");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { resolve } from "node:path";
|
||||||
|
|
||||||
|
describe("AppHeader Component", () => {
|
||||||
|
const componentPath = resolve(__dirname, "../../components/AppHeader.vue");
|
||||||
|
const content = readFileSync(componentPath, "utf-8");
|
||||||
|
|
||||||
|
it("should contain sticky positioning", () => {
|
||||||
|
expect(content).toContain("sticky");
|
||||||
|
expect(content).toContain("top-0");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have nav items: Home, Referenzen, Mietkatalog, Kontakt", () => {
|
||||||
|
expect(content).toContain("Home");
|
||||||
|
expect(content).toContain("Referenzen");
|
||||||
|
expect(content).toContain("Mietkatalog");
|
||||||
|
expect(content).toContain("Kontakt");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should include phone link tel:+491726264796", () => {
|
||||||
|
expect(content).toContain("tel:+491726264796");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should include Facebook social icon link", () => {
|
||||||
|
expect(content).toContain("facebook.com");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should include Instagram social icon link", () => {
|
||||||
|
expect(content).toContain("instagram.com");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have mobile burger menu with aria-expanded", () => {
|
||||||
|
expect(content).toContain("aria-expanded");
|
||||||
|
expect(content).toContain("mobileMenuOpen");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have min touch target height for nav items", () => {
|
||||||
|
expect(content).toContain("min-h-44px");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("AppFooter Component", () => {
|
||||||
|
const componentPath = resolve(__dirname, "../../components/AppFooter.vue");
|
||||||
|
const content = readFileSync(componentPath, "utf-8");
|
||||||
|
|
||||||
|
it("should contain address with Leipheim", () => {
|
||||||
|
expect(content).toContain("89340");
|
||||||
|
expect(content).toContain("Leipheim");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should contain phone number +49 8221 204433", () => {
|
||||||
|
expect(content).toContain("tel:+498221204433");
|
||||||
|
expect(content).toContain("204433");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should contain email link info@hms-licht-ton.de", () => {
|
||||||
|
expect(content).toContain("mailto:info@hms-licht-ton.de");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should contain Impressum link", () => {
|
||||||
|
expect(content).toContain("Impressum");
|
||||||
|
expect(content).toContain("/impressum");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should contain DSGVO link", () => {
|
||||||
|
expect(content).toContain("DSGVO");
|
||||||
|
expect(content).toContain("/datenschutz");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should contain AGB Vermietung link", () => {
|
||||||
|
expect(content).toContain("AGB Vermietung");
|
||||||
|
expect(content).toContain("/agb-vermietung");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should NOT contain 'Neu in der Vermietung'", () => {
|
||||||
|
expect(content).not.toContain("Neu in der Vermietung");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should NOT contain 'AGB Shop'", () => {
|
||||||
|
expect(content).not.toContain("AGB Shop");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have 4-column grid layout", () => {
|
||||||
|
expect(content).toContain("lg:grid-cols-4");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Error Page (404)", () => {
|
||||||
|
const errorPath = resolve(__dirname, "../../error.vue");
|
||||||
|
const content = readFileSync(errorPath, "utf-8");
|
||||||
|
|
||||||
|
it("should contain 'Seite nicht gefunden' for 404", () => {
|
||||||
|
expect(content).toContain("Seite nicht gefunden");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should contain link to home /", () => {
|
||||||
|
expect(content).toContain('to="/"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should include noindex meta tag", () => {
|
||||||
|
expect(content).toContain("noindex");
|
||||||
|
expect(content).toContain("nofollow");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("HmsLogo Component", () => {
|
||||||
|
const logoPath = resolve(__dirname, "../../components/HmsLogo.vue");
|
||||||
|
const content = readFileSync(logoPath, "utf-8");
|
||||||
|
|
||||||
|
it("should render SVG element", () => {
|
||||||
|
expect(content).toContain("<svg");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should use orange accent color #EC6925", () => {
|
||||||
|
expect(content).toContain("#EC6925");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should contain border/stroke with accent color", () => {
|
||||||
|
expect(content).toContain("accentColor");
|
||||||
|
expect(content).toContain("stroke");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("robots.txt route", () => {
|
||||||
|
const robotsPath = resolve(__dirname, "../../server/routes/robots.txt.ts");
|
||||||
|
const content = readFileSync(robotsPath, "utf-8");
|
||||||
|
|
||||||
|
it("should contain User-agent: *", () => {
|
||||||
|
expect(content).toContain("User-agent: *");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should contain Disallow: /", () => {
|
||||||
|
expect(content).toContain("Disallow: /");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "./.nuxt/tsconfig.json"
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { defineConfig } from "vitest/config";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
environment: "happy-dom",
|
||||||
|
globals: true,
|
||||||
|
include: ["tests/unit/**/*.test.ts"],
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"~": fileURLToPath(new URL("./", import.meta.url)),
|
||||||
|
"#app": fileURLToPath(new URL("./node_modules/nuxt/dist/app", import.meta.url)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user