554cb99f2c
Add the Umami tracker (cookieless, no consent banner) in the document head, limited to the production hostnames via data-domains so dev traffic doesn't pollute the stats. Pageviews are auto-tracked per page and locale. Custom events on the key funnel: - demo-request (demo form submit, with teamSize) - partner-application (partner form submit, with type) - book-demo (every "Book a demo" CTA click) via data-umami-event - login (clicks through to the app) Also fix the mobile nav menu links, which weren't localized (would drop Danish visitors back to English).
206 lines
7.8 KiB
Vue
206 lines
7.8 KiB
Vue
<script setup lang="ts">
|
|
// Sticky top nav with logo, anchor links, language toggle, login + demo CTA.
|
|
// Ported from landing-sections.jsx Nav (light mode, production subset).
|
|
import { computed, ref } from 'vue'
|
|
import { useRoute } from 'vue-router'
|
|
import { APP_URL } from '~/utils/landingTokens'
|
|
import { useTheme, useCopy, useLang, useLocalizeHref, scrollToAnchor } from '~/composables/useLanding'
|
|
|
|
const t = useTheme()
|
|
const copy = useCopy()
|
|
const lang = useLang()
|
|
const route = useRoute()
|
|
const loc = useLocalizeHref()
|
|
const localePath = useLocalePath()
|
|
const switchLocalePath = useSwitchLocalePath()
|
|
|
|
const mobileOpen = ref(false)
|
|
|
|
const items = computed(() => [
|
|
{ label: copy.value.nav.product, href: '/#suite' },
|
|
{ label: copy.value.nav.security, href: '/#sovereignty' },
|
|
{ label: copy.value.nav.whitelabel, href: '/#whitelabel' },
|
|
{ label: copy.value.nav.pricing, href: '/#pricing' },
|
|
{ label: copy.value.nav.docs, href: '/docs' },
|
|
])
|
|
|
|
function onLogo(e: MouseEvent) {
|
|
// On the homepage (current locale), scroll to top in place; from a sub-page
|
|
// let NuxtLink route home.
|
|
if (route.path === localePath('/')) {
|
|
e.preventDefault()
|
|
window.scrollTo({ top: 0, behavior: 'smooth' })
|
|
}
|
|
}
|
|
|
|
function onNav(e: MouseEvent, href: string) {
|
|
if (href.includes('#') && route.path === localePath('/')) {
|
|
e.preventDefault()
|
|
scrollToAnchor(href.slice(href.indexOf('#')))
|
|
}
|
|
mobileOpen.value = false
|
|
}
|
|
|
|
function onMobileLink(e: MouseEvent, href: string) {
|
|
onNav(e, href)
|
|
mobileOpen.value = false
|
|
}
|
|
|
|
// Segmented language switcher: pick a locale (no-op if already active) and
|
|
// close the mobile menu. The active segment is filled so it's obvious which
|
|
// language you're on.
|
|
function switchTo(code: 'en' | 'da') {
|
|
navigateTo(switchLocalePath(code))
|
|
mobileOpen.value = false
|
|
}
|
|
function segStyle(code: 'en' | 'da') {
|
|
const active = lang.value === code
|
|
return {
|
|
padding: '5px 9px', border: 'none', cursor: 'pointer', borderRadius: '3px',
|
|
fontFamily: '\'JetBrains Mono\', monospace', fontSize: '11px', letterSpacing: '0.08em',
|
|
background: active ? t.value.fg : 'transparent',
|
|
color: active ? t.value.bg : t.value.fgMuted,
|
|
fontWeight: active ? 700 : 500,
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<header :style="{
|
|
position: 'sticky', top: '0', zIndex: 100,
|
|
background: 'rgba(250,250,247,0.84)',
|
|
backdropFilter: 'blur(14px)', WebkitBackdropFilter: 'blur(14px)',
|
|
borderBottom: `1px solid ${t.border}`,
|
|
}">
|
|
<div :style="{
|
|
maxWidth: '1280px', margin: '0 auto',
|
|
padding: `18px clamp(20px, 5vw, 64px)`,
|
|
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
}">
|
|
<NuxtLink :to="loc('/')" :style="{ display: 'flex', alignItems: 'center', gap: '12px', cursor: 'pointer' }" @click="onLogo">
|
|
<BrandNodeMark :size="32" :fg="t.fg" :accent="t.signal" />
|
|
<div :style="{ fontFamily: '\'JetBrains Mono\', monospace', fontWeight: 600, fontSize: '16px', letterSpacing: '-0.02em', color: t.fg }">dezky</div>
|
|
</NuxtLink>
|
|
|
|
<!-- Desktop nav cluster — hidden on mobile via scoped CSS -->
|
|
<nav class="nav-desktop" :style="{ display: 'flex', alignItems: 'center', gap: '36px' }">
|
|
<NuxtLink
|
|
v-for="(it, i) in items" :key="i" :to="loc(it.href)"
|
|
:style="{ fontFamily: '\'Inter\', sans-serif', fontSize: '14px', color: t.fgMuted, letterSpacing: '-0.005em' }"
|
|
@click="onNav($event, it.href)"
|
|
>{{ it.label }}</NuxtLink>
|
|
</nav>
|
|
|
|
<!-- Desktop CTA cluster — hidden on mobile via scoped CSS -->
|
|
<div class="nav-desktop" :style="{ display: 'flex', alignItems: 'center', gap: '14px' }">
|
|
<div :style="{ display: 'inline-flex', alignItems: 'center', gap: '2px', border: `1px solid ${t.border}`, borderRadius: '5px', padding: '2px' }">
|
|
<button :style="segStyle('en')" @click="switchTo('en')">EN</button>
|
|
<button :style="segStyle('da')" @click="switchTo('da')">DA</button>
|
|
</div>
|
|
<a :href="APP_URL" data-umami-event="login" :style="{ fontFamily: '\'Inter\', sans-serif', fontSize: '14px', color: t.fgMuted }">{{ copy.nav.login }}</a>
|
|
<LandingBtn variant="primary" data-umami-event="book-demo" @click="navigateTo(localePath('/demo'))">{{ copy.nav.cta }} →</LandingBtn>
|
|
</div>
|
|
|
|
<!-- Hamburger — visible only on mobile via scoped CSS -->
|
|
<button
|
|
class="nav-hamburger"
|
|
:aria-label="mobileOpen ? 'Close menu' : 'Open menu'"
|
|
:aria-expanded="mobileOpen"
|
|
:style="{
|
|
flexDirection: 'column', justifyContent: 'center', alignItems: 'center',
|
|
gap: '5px', background: 'transparent', border: 'none', cursor: 'pointer',
|
|
padding: '6px', borderRadius: '4px',
|
|
}"
|
|
@click="mobileOpen = !mobileOpen"
|
|
>
|
|
<!-- Three bar lines; top/bottom rotate to X when open -->
|
|
<span :style="{
|
|
display: 'block', width: '22px', height: '2px',
|
|
background: t.fg, borderRadius: '2px',
|
|
transition: 'transform 0.2s, opacity 0.2s',
|
|
transform: mobileOpen ? 'translateY(7px) rotate(45deg)' : 'none',
|
|
}" />
|
|
<span :style="{
|
|
display: 'block', width: '22px', height: '2px',
|
|
background: t.fg, borderRadius: '2px',
|
|
transition: 'opacity 0.2s',
|
|
opacity: mobileOpen ? 0 : 1,
|
|
}" />
|
|
<span :style="{
|
|
display: 'block', width: '22px', height: '2px',
|
|
background: t.fg, borderRadius: '2px',
|
|
transition: 'transform 0.2s, opacity 0.2s',
|
|
transform: mobileOpen ? 'translateY(-7px) rotate(-45deg)' : 'none',
|
|
}" />
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Mobile dropdown panel — JS-toggled, SSR-safe (visibility is CSS-controlled above 768px) -->
|
|
<div
|
|
v-if="mobileOpen"
|
|
class="nav-mobile-panel"
|
|
:style="{
|
|
background: 'rgba(250,250,247,0.97)',
|
|
borderTop: `1px solid ${t.border}`,
|
|
padding: '24px clamp(20px, 5vw, 64px) 32px',
|
|
display: 'flex', flexDirection: 'column', gap: '0',
|
|
}"
|
|
>
|
|
<NuxtLink
|
|
v-for="(it, i) in items" :key="i" :to="loc(it.href)"
|
|
:style="{
|
|
fontFamily: '\'Inter\', sans-serif', fontSize: '16px', color: t.fgMuted,
|
|
letterSpacing: '-0.005em', padding: '14px 0',
|
|
borderBottom: `1px solid ${t.border}`,
|
|
display: 'block',
|
|
}"
|
|
@click="onMobileLink($event, it.href)"
|
|
>{{ it.label }}</NuxtLink>
|
|
|
|
<div :style="{ display: 'flex', alignItems: 'center', gap: '14px', paddingTop: '24px', flexWrap: 'wrap' }">
|
|
<div :style="{ display: 'inline-flex', alignItems: 'center', gap: '2px', border: `1px solid ${t.border}`, borderRadius: '5px', padding: '2px' }">
|
|
<button :style="segStyle('en')" @click="switchTo('en')">EN</button>
|
|
<button :style="segStyle('da')" @click="switchTo('da')">DA</button>
|
|
</div>
|
|
<a
|
|
:href="APP_URL"
|
|
data-umami-event="login"
|
|
:style="{ fontFamily: '\'Inter\', sans-serif', fontSize: '14px', color: t.fgMuted }"
|
|
@click="mobileOpen = false"
|
|
>{{ copy.nav.login }}</a>
|
|
<LandingBtn variant="primary" data-umami-event="book-demo" @click="() => { navigateTo(localePath('/demo')); mobileOpen = false }">{{ copy.nav.cta }} →</LandingBtn>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
</template>
|
|
|
|
<style scoped>
|
|
/* Desktop clusters: visible by default, hidden on mobile */
|
|
.nav-desktop {
|
|
display: flex;
|
|
}
|
|
|
|
/* Hamburger: hidden by default, shown on mobile */
|
|
.nav-hamburger {
|
|
display: none;
|
|
}
|
|
|
|
/* Mobile panel: always rendered when v-if is true, but constrained to mobile */
|
|
@media (max-width: 768px) {
|
|
.nav-desktop {
|
|
display: none !important;
|
|
}
|
|
|
|
.nav-hamburger {
|
|
display: flex;
|
|
}
|
|
}
|
|
|
|
/* Ensure the mobile panel is not shown on desktop even if somehow open */
|
|
@media (min-width: 769px) {
|
|
.nav-mobile-panel {
|
|
display: none !important;
|
|
}
|
|
}
|
|
</style>
|