Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
This commit is contained in:
@@ -0,0 +1,263 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user