264 lines
7.5 KiB
Svelte
264 lines
7.5 KiB
Svelte
<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 {
|
||
|
|
if (href === "/") return i18n.t("site.nav.home");
|
||
|
|
if (href === "/pricing") return i18n.t("site.nav.pricing");
|
||
|
|
if (href === "/docs") return i18n.t("site.nav.apiDocs");
|
||
|
|
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}
|
||
|
|
<div class="absolute top-full right-0 left-0 bg-background/95 shadow-md backdrop-blur-md lg:hidden">
|
||
|
|
<div class="border-t border-border/80 px-4 pt-2 pb-6">
|
||
|
|
<nav class="mt-2 mb-4 flex flex-col space-y-1" aria-label={i18n.t("site.nav.mobile")}>
|
||
|
|
{#each NAV_ITEMS as item}
|
||
|
|
<a
|
||
|
|
href={item.href}
|
||
|
|
class="block py-2.5 text-base font-medium text-text transition-colors hover:text-link"
|
||
|
|
onclick={(e) => onNavClick(e, item.href)}
|
||
|
|
>
|
||
|
|
{navTitle(item.href)}
|
||
|
|
</a>
|
||
|
|
{/each}
|
||
|
|
</nav>
|
||
|
|
<div class="mt-4 space-y-3">
|
||
|
|
{#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(
|
||
|
|
"ghost",
|
||
|
|
"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>
|