2026-08-09 22:47:43 +02:00
|
|
|
<script lang="ts">
|
|
|
|
|
import { onMount } from "svelte";
|
|
|
|
|
import { Button, buttonClasses } from "$lib/components/ui";
|
|
|
|
|
import { api } from "$lib/api";
|
|
|
|
|
import type { MeResponse } from "$lib/types";
|
|
|
|
|
import ThemeToggle from "$lib/components/ThemeToggle.svelte";
|
|
|
|
|
import { i18n } from "$lib/i18n";
|
|
|
|
|
import { NAV_ITEMS } from "./data";
|
|
|
|
|
|
|
|
|
|
let mobileMenuOpen = $state(false);
|
|
|
|
|
let me = $state<MeResponse | null>(null);
|
|
|
|
|
let authReady = $state(false);
|
|
|
|
|
|
|
|
|
|
const loggedIn = $derived(me != null);
|
|
|
|
|
const accountLabel = $derived.by(() => {
|
|
|
|
|
const company = me?.company?.name?.trim();
|
|
|
|
|
if (company) return company;
|
|
|
|
|
const name = me?.user?.name?.trim();
|
|
|
|
|
if (name) return name;
|
|
|
|
|
const email = me?.user?.email?.trim();
|
|
|
|
|
if (email) return email;
|
|
|
|
|
return i18n.t("site.account");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function navTitle(href: string): string {
|
2026-09-12 21:07:43 +02:00
|
|
|
if (href === "/#how-it-works") return i18n.t("site.nav.howItWorks");
|
|
|
|
|
if (href === "/#benefits") return i18n.t("site.nav.benefits");
|
2026-08-09 22:47:43 +02:00
|
|
|
if (href === "/pricing") return i18n.t("site.nav.pricing");
|
|
|
|
|
return href;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onMount(() => {
|
|
|
|
|
const ac = new AbortController();
|
|
|
|
|
const timer = window.setTimeout(() => ac.abort(), 8_000);
|
|
|
|
|
void api<MeResponse>("/api/auth/me", { signal: ac.signal })
|
|
|
|
|
.then((res) => {
|
|
|
|
|
me = res;
|
|
|
|
|
})
|
|
|
|
|
.catch(() => {
|
|
|
|
|
me = null;
|
|
|
|
|
})
|
|
|
|
|
.finally(() => {
|
|
|
|
|
window.clearTimeout(timer);
|
|
|
|
|
authReady = true;
|
|
|
|
|
});
|
|
|
|
|
return () => {
|
|
|
|
|
ac.abort();
|
|
|
|
|
window.clearTimeout(timer);
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function scrollToSection(href: string) {
|
|
|
|
|
if (!href.startsWith("#")) return;
|
|
|
|
|
const targetId = href.slice(1);
|
|
|
|
|
const element = document.getElementById(targetId);
|
|
|
|
|
if (!element) return;
|
|
|
|
|
const headerOffset = 100;
|
|
|
|
|
const elementPosition = element.getBoundingClientRect().top;
|
|
|
|
|
const offsetPosition = elementPosition + window.pageYOffset - headerOffset;
|
|
|
|
|
window.scrollTo({ top: offsetPosition, behavior: "smooth" });
|
|
|
|
|
mobileMenuOpen = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onNavClick(event: MouseEvent, href: string) {
|
|
|
|
|
if (href.startsWith("#")) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
scrollToSection(href);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const hashIdx = href.indexOf("#");
|
|
|
|
|
if (hashIdx >= 0) {
|
|
|
|
|
const path = href.slice(0, hashIdx) || "/";
|
|
|
|
|
const hash = href.slice(hashIdx);
|
|
|
|
|
const here = typeof window !== "undefined" ? window.location.pathname : "";
|
|
|
|
|
if (path === "/" || path === here) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
if (here !== "/" && path === "/") {
|
|
|
|
|
window.location.assign(href);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
scrollToSection(hash);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
mobileMenuOpen = false;
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<a
|
|
|
|
|
href="#main-content"
|
|
|
|
|
class="sr-only focus:not-sr-only focus:absolute focus:top-2 focus:left-2 focus:z-[60] focus:rounded-md focus:bg-surface focus:px-3 focus:py-2 focus:text-sm focus:font-medium focus:text-text focus:shadow-md focus:outline focus:outline-2 focus:outline-offset-2 focus:outline-ring"
|
|
|
|
|
>
|
|
|
|
|
{i18n.t("a11y.skipToContent")}
|
|
|
|
|
</a>
|
|
|
|
|
|
|
|
|
|
<header
|
|
|
|
|
class="fixed top-0 right-0 left-0 z-[110] border-b border-border/80 bg-background/90 backdrop-blur-md"
|
|
|
|
|
>
|
|
|
|
|
<div class="container mx-auto max-w-7xl">
|
|
|
|
|
<div class="flex h-16 items-center justify-between px-4 sm:px-0">
|
|
|
|
|
<a href="/" class="group flex items-center space-x-3" aria-label={i18n.t("app.home")}>
|
|
|
|
|
<div
|
|
|
|
|
class="relative flex h-9 w-9 items-center justify-center transition-transform duration-200 group-hover:scale-105"
|
|
|
|
|
>
|
|
|
|
|
<img
|
|
|
|
|
src="/descrybe_logo.png"
|
|
|
|
|
alt=""
|
|
|
|
|
width="36"
|
|
|
|
|
height="36"
|
|
|
|
|
class="h-auto w-auto"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<span class="hidden text-xl font-bold text-text sm:inline-block">{i18n.t("app.name")}</span>
|
|
|
|
|
</a>
|
|
|
|
|
|
|
|
|
|
<div class="absolute left-1/2 hidden -translate-x-1/2 transform lg:block">
|
|
|
|
|
<nav class="flex items-center space-x-10" aria-label={i18n.t("site.nav.primary")}>
|
|
|
|
|
{#each NAV_ITEMS as item}
|
|
|
|
|
<a
|
|
|
|
|
href={item.href}
|
|
|
|
|
class="text-sm font-medium text-text-muted transition-colors hover:text-link"
|
|
|
|
|
onclick={(e) => onNavClick(e, item.href)}
|
|
|
|
|
>
|
|
|
|
|
{navTitle(item.href)}
|
|
|
|
|
</a>
|
|
|
|
|
{/each}
|
|
|
|
|
</nav>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="flex items-center gap-1 sm:gap-2" data-auth-ready={authReady}>
|
|
|
|
|
<ThemeToggle />
|
|
|
|
|
<div class="hidden items-center gap-2 lg:flex">
|
|
|
|
|
{#if loggedIn}
|
|
|
|
|
<span
|
|
|
|
|
class="max-w-[10rem] truncate text-sm font-medium text-text-muted"
|
|
|
|
|
title={accountLabel}>{accountLabel}</span
|
|
|
|
|
>
|
|
|
|
|
<a
|
|
|
|
|
href="/dashboard"
|
|
|
|
|
class={buttonClasses(
|
|
|
|
|
"default",
|
|
|
|
|
"sm",
|
|
|
|
|
"box-border h-9 min-h-9 border border-transparent px-4 text-sm font-medium leading-none"
|
|
|
|
|
)}>{i18n.t("site.goToApp")}</a
|
|
|
|
|
>
|
|
|
|
|
{:else}
|
|
|
|
|
<a
|
|
|
|
|
href="/login"
|
|
|
|
|
class={buttonClasses(
|
|
|
|
|
"ghost",
|
|
|
|
|
"sm",
|
|
|
|
|
"box-border h-9 min-h-9 px-4 text-sm font-medium leading-none"
|
|
|
|
|
)}>{i18n.t("site.logIn")}</a
|
|
|
|
|
>
|
|
|
|
|
<a
|
|
|
|
|
href="/register"
|
|
|
|
|
class={buttonClasses(
|
|
|
|
|
"default",
|
|
|
|
|
"sm",
|
|
|
|
|
"box-border h-9 min-h-9 border border-transparent px-4 text-sm font-medium leading-none"
|
|
|
|
|
)}>{i18n.t("site.getStarted")}</a
|
|
|
|
|
>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
|
|
|
|
class="ml-0 lg:hidden"
|
|
|
|
|
aria-label={mobileMenuOpen ? i18n.t("nav.closeMenu") : i18n.t("nav.open")}
|
|
|
|
|
aria-expanded={mobileMenuOpen}
|
|
|
|
|
onclick={() => (mobileMenuOpen = !mobileMenuOpen)}
|
|
|
|
|
>
|
|
|
|
|
{#if mobileMenuOpen}
|
|
|
|
|
<svg
|
|
|
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
|
|
|
width="24"
|
|
|
|
|
height="24"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
stroke-width="2"
|
|
|
|
|
stroke-linecap="round"
|
|
|
|
|
stroke-linejoin="round"
|
|
|
|
|
class="h-6 w-6"
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
><line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" /></svg
|
|
|
|
|
>
|
|
|
|
|
{:else}
|
|
|
|
|
<svg
|
|
|
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
|
|
|
width="24"
|
|
|
|
|
height="24"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
stroke-width="2"
|
|
|
|
|
stroke-linecap="round"
|
|
|
|
|
stroke-linejoin="round"
|
|
|
|
|
class="h-6 w-6"
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
><line x1="3" x2="21" y1="6" y2="6" /><line x1="3" x2="21" y1="12" y2="12" /><line
|
|
|
|
|
x1="3"
|
|
|
|
|
x2="21"
|
|
|
|
|
y1="18"
|
|
|
|
|
y2="18"
|
|
|
|
|
/></svg
|
|
|
|
|
>
|
|
|
|
|
{/if}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{#if mobileMenuOpen}
|
2026-09-12 21:07:43 +02:00
|
|
|
<div
|
|
|
|
|
class="absolute top-full right-3 left-3 mt-2 overflow-hidden rounded-xl border border-border/80 bg-background/95 shadow-xl backdrop-blur-md sm:right-4 sm:left-auto sm:w-80 lg:hidden"
|
|
|
|
|
>
|
|
|
|
|
<div class="p-3">
|
|
|
|
|
<nav class="flex flex-col gap-1" aria-label={i18n.t("site.nav.mobile")}>
|
2026-08-09 22:47:43 +02:00
|
|
|
{#each NAV_ITEMS as item}
|
|
|
|
|
<a
|
|
|
|
|
href={item.href}
|
2026-09-12 21:07:43 +02:00
|
|
|
class="rounded-lg px-3 py-2.5 text-sm font-medium text-text transition-colors hover:bg-accent hover:text-link"
|
2026-08-09 22:47:43 +02:00
|
|
|
onclick={(e) => onNavClick(e, item.href)}
|
|
|
|
|
>
|
|
|
|
|
{navTitle(item.href)}
|
|
|
|
|
</a>
|
|
|
|
|
{/each}
|
|
|
|
|
</nav>
|
2026-09-12 21:07:43 +02:00
|
|
|
<div
|
|
|
|
|
class={`mt-3 border-t border-border/80 pt-3 ${loggedIn ? "space-y-3" : "grid grid-cols-2 gap-2"}`}
|
|
|
|
|
>
|
2026-08-09 22:47:43 +02:00
|
|
|
{#if loggedIn}
|
|
|
|
|
<p class="truncate text-center text-sm font-medium text-text-muted">
|
|
|
|
|
{accountLabel}
|
|
|
|
|
</p>
|
|
|
|
|
<a
|
|
|
|
|
href="/dashboard"
|
|
|
|
|
class={buttonClasses(
|
|
|
|
|
"default",
|
|
|
|
|
"default",
|
|
|
|
|
"box-border h-10 w-full min-h-10 text-sm font-medium"
|
|
|
|
|
)}>{i18n.t("site.goToApp")}</a
|
|
|
|
|
>
|
|
|
|
|
{:else}
|
|
|
|
|
<a
|
|
|
|
|
href="/login"
|
|
|
|
|
class={buttonClasses(
|
2026-09-12 21:07:43 +02:00
|
|
|
"outline",
|
2026-08-09 22:47:43 +02:00
|
|
|
"default",
|
|
|
|
|
"box-border h-10 w-full min-h-10 text-sm font-medium"
|
|
|
|
|
)}>{i18n.t("site.logIn")}</a
|
|
|
|
|
>
|
|
|
|
|
<a
|
|
|
|
|
href="/register"
|
|
|
|
|
class={buttonClasses(
|
|
|
|
|
"default",
|
|
|
|
|
"default",
|
|
|
|
|
"box-border h-10 w-full min-h-10 text-sm font-medium"
|
|
|
|
|
)}>{i18n.t("site.getStarted")}</a
|
|
|
|
|
>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
</header>
|