107 lines
3.8 KiB
Vue
107 lines
3.8 KiB
Vue
<template>
|
|
<header :class="themeClass">
|
|
<div class="max-w-[960px] mx-auto px-6 py-5 flex justify-between items-center flex-wrap gap-3">
|
|
<router-link to="/" class="text-olive font-pixel text-xs tracking-tight hover:text-orange transition-colors">
|
|
LEUSCHKE<span class="text-orange">.</span>DIGITAL
|
|
</router-link>
|
|
|
|
<!-- Desktop Nav -->
|
|
<nav class="hidden md:flex items-center gap-3">
|
|
<router-link v-for="item in navItems" :key="item.path" :to="item.path"
|
|
class="font-pixel text-[8px] text-olive hover:text-orange transition-colors"
|
|
:class="{ 'text-orange border-b-2 border-orange': route.path === item.path }">
|
|
{{ t(item.label) }}
|
|
</router-link>
|
|
</nav>
|
|
|
|
<!-- Controls -->
|
|
<div class="flex items-center gap-2">
|
|
<!-- Mobile menu button -->
|
|
<button @click="mobileOpen = !mobileOpen" class="md:hidden pixel-btn text-[8px] px-3 py-2">
|
|
☰
|
|
</button>
|
|
<button @click="toggleTheme" class="hidden md:inline-flex pixel-btn text-[8px] px-3 py-2">
|
|
{{ isDark ? '☀️' : '🌙' }} {{ isDark ? t('nav.light') : t('nav.dark') }}
|
|
</button>
|
|
<button @click="toggleLang" class="hidden md:inline-flex pixel-btn text-[8px] px-3 py-2">
|
|
{{ lang === 'de' ? '🇩🇪 DE' : '🇬🇧 EN' }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Mobile menu -->
|
|
<div v-if="mobileOpen" class="md:hidden pixel-border mx-3 mb-3 p-3 flex flex-col gap-3">
|
|
<router-link v-for="item in navItems" :key="item.path" :to="item.path"
|
|
class="font-pixel text-[8px] text-olive hover:text-orange block px-2 py-1"
|
|
@click="mobileOpen = false">
|
|
{{ t(item.label) }}
|
|
</router-link>
|
|
<div class="flex gap-2 pt-2">
|
|
<button @click="toggleTheme" class="pixel-btn text-[8px] px-3 py-2">
|
|
{{ isDark ? '☀️' : '🌙' }} {{ isDark ? t('nav.light') : t('nav.dark') }}
|
|
</button>
|
|
<button @click="toggleLang" class="pixel-btn text-[8px] px-3 py-2">
|
|
{{ lang === 'de' ? '🇩🇪 DE' : '🇬🇧 EN' }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed, watch } from 'vue'
|
|
import { useRoute } from 'vue-router'
|
|
import { t, currentLang, type Lang } from '../i18n'
|
|
|
|
const route = useRoute()
|
|
const isDark = ref<boolean>(false)
|
|
const lang = ref<Lang>('de')
|
|
const mobileOpen = ref(false)
|
|
|
|
const navItems = [
|
|
{ path: '/', label: 'nav.home' },
|
|
{ path: '/about', label: 'nav.about' },
|
|
{ path: '/projects', label: 'nav.projects' },
|
|
{ path: '/blog', label: 'nav.blog' },
|
|
{ path: '/contact', label: 'nav.contact' },
|
|
{ path: '/impressum', label: 'nav.impressum' },
|
|
]
|
|
|
|
const toggleTheme = () => { isDark.value = !isDark.value; applyTheme() }
|
|
const toggleLang = () => {
|
|
lang.value = lang.value === 'de' ? 'en' : 'de'
|
|
currentLang.value = lang.value
|
|
}
|
|
const applyTheme = () => {
|
|
document.documentElement.setAttribute('data-theme', isDark.value ? 'dark' : 'light')
|
|
document.documentElement.setAttribute('data-lang', lang.value)
|
|
}
|
|
|
|
const themeClass = computed(() => ({
|
|
'bg-beige-light border-b-4 border-olive': true,
|
|
'[&[data-theme=dark]]:bg-beige-dark [&[data-theme=dark]]:text-[#d4ccb8] [&[data-theme=dark]]:border-olive-dark': isDark.value,
|
|
}))
|
|
|
|
// Init from localStorage or defaults
|
|
if (typeof localStorage !== 'undefined') {
|
|
isDark.value = localStorage.getItem('theme') === 'dark'
|
|
}
|
|
lang.value = (localStorage.getItem('lang') as Lang) || 'de'
|
|
currentLang.value = lang.value
|
|
applyTheme()
|
|
|
|
watch(isDark, (v) => {
|
|
localStorage.setItem('theme', v ? 'dark' : 'light')
|
|
applyTheme()
|
|
})
|
|
watch(lang, (v) => {
|
|
localStorage.setItem('lang', v)
|
|
currentLang.value = v
|
|
applyTheme()
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
header { position: fixed; top: 0; left: 0; right: 0; z-index: 50; }
|
|
</style>
|