Files
Kordant/web/src/components/layout/Navbar.tsx

354 lines
11 KiB
TypeScript

import { createSignal, onMount, onCleanup, Show } from "solid-js";
import { A, useLocation } from "@solidjs/router";
import { cn } from "~/lib/utils";
import { Button } from "~/components/ui";
import { Typewriter } from "~/components/ui/Typewriter";
import { useTheme } from "~/lib/theme";
import { SignedIn, SignedOut, UserButton } from "clerk-solidjs";
import { useRealtimeAlerts } from "~/hooks/useRealtimeAlerts";
function ShieldLogo() {
return (
<svg
width="28"
height="32"
viewBox="0 0 28 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<linearGradient
id="shield-grad"
x1="0"
y1="0"
x2="28"
y2="32"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="var(--color-brand-primary)" />
<stop offset="1" stop-color="var(--color-brand-accent)" />
</linearGradient>
</defs>
<path
d="M14 0L26 6V16C26 24 14 32 14 32S2 24 2 16V6L14 0Z"
fill="url(#shield-grad)"
/>
<path
d="M10 16L13 19L19 13"
stroke="white"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
);
}
function ThemeToggle() {
const { toggle, resolved } = useTheme();
const [mounted, setMounted] = createSignal(false);
onMount(() => {
setMounted(true);
});
return (
<button
type="button"
aria-label="Toggle theme"
class="flex items-center gap-1.5 p-2 rounded-lg text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-secondary)] transition-all duration-200 ease-in-out hover:scale-105"
onClick={toggle}
>
<Show
when={mounted()}
fallback={<div style={{ width: "20px", height: "20px" }} />}
>
<Show
when={resolved() === "dark"}
fallback={
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="5" />
<line x1="12" y1="1" x2="12" y2="3" />
<line x1="12" y1="21" x2="12" y2="23" />
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
<line x1="1" y1="12" x2="3" y2="12" />
<line x1="21" y1="12" x2="23" y2="12" />
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
</svg>
}
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z" />
</svg>
</Show>
</Show>
<Show
when={mounted()}
fallback={<span style={{ visibility: "hidden" }}>Light</span>}
>
<span class="text-sm font-medium hidden sm:inline">
<Show
when={resolved() === "dark"}
fallback={<Typewriter keepAlive={false}>Light</Typewriter>}
>
<Typewriter keepAlive={false}>Dark</Typewriter>
</Show>
</span>
</Show>
</button>
);
}
const marketingLinks = [
{ label: "Features", href: "/features" },
{ label: "Pricing", href: "/pricing" },
{ label: "Blog", href: "/blog" },
];
const productLinks = [
{ label: "Dashboard", href: "/dashboard" },
{ label: "DarkWatch", href: "/darkwatch" },
{ label: "VoicePrint", href: "/voiceprint" },
{ label: "SpamShield", href: "/spamshield" },
{ label: "HomeTitle", href: "/hometitle" },
{ label: "RemoveBrokers", href: "/removebrokers" },
];
function RealtimeIndicator() {
const { connectionStatus, unreadCount, clearUnread } = useRealtimeAlerts();
return (
<div class="flex items-center gap-2">
<Show when={unreadCount() > 0}>
<button
type="button"
onClick={clearUnread}
class="relative flex items-center justify-center w-6 h-6 rounded-full bg-(--color-error) text-white text-[10px] font-bold leading-none transition-transform hover:scale-110"
aria-label={`${unreadCount()} unread alerts`}
>
{unreadCount() > 99 ? "99+" : unreadCount()}
</button>
</Show>
<Show
when={connectionStatus() === "connected"}
fallback={
connectionStatus() === "reconnecting" ||
connectionStatus() === "connecting" ? (
<div
class="flex items-center gap-1 text-[10px] text-(--color-warning)"
aria-label="Reconnecting"
>
<span class="relative flex h-2 w-2">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-(--color-warning) opacity-75" />
<span class="relative inline-flex rounded-full h-2 w-2 bg-(--color-warning)" />
</span>
<span class="hidden sm:inline">Reconnecting</span>
</div>
) : (
<div
class="flex items-center gap-1 text-[10px] text-(--color-text-muted)"
aria-label="Offline"
>
<span class="inline-flex rounded-full h-2 w-2 bg-(--color-text-muted)" />
<span class="hidden sm:inline">Offline</span>
</div>
)
}
>
<div class="flex items-center gap-1" aria-label="Connected">
<span class="relative flex h-2 w-2">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-(--color-success) opacity-75" />
<span class="relative inline-flex rounded-full h-2 w-2 bg-(--color-success)" />
</span>
</div>
</Show>
</div>
);
}
export default function Navbar() {
const [mobileOpen, setMobileOpen] = createSignal(false);
const [scrolled, setScrolled] = createSignal(false);
const location = useLocation();
onMount(() => {
const onScroll = () => {
setScrolled(window.scrollY > 8);
};
window.addEventListener("scroll", onScroll, { passive: true });
onCleanup(() => window.removeEventListener("scroll", onScroll));
});
const isActive = (href: string) => {
if (href === "/dashboard") return location.pathname === "/dashboard";
return location.pathname.startsWith(href);
};
const NavLink = (props: {
href: string;
label: string;
mobile?: boolean;
}) => (
<A
href={props.href}
class={cn(
props.mobile
? "block px-3 py-2 rounded-lg text-base font-medium transition-colors"
: "text-sm font-medium transition-colors",
isActive(props.href)
? "text-text-primary"
: "text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]",
props.mobile &&
!isActive(props.href) &&
"hover:bg-[var(--color-bg-secondary)]",
)}
onClick={() => props.mobile && setMobileOpen(false)}
>
{props.label}
</A>
);
return (
<nav
class={cn(
"fixed top-0 left-0 right-0 z-50 h-16 transition-all duration-300",
scrolled()
? "glass border-b border-[var(--color-border)] shadow-sm"
: "bg-transparent",
)}
>
<div class="max-w-7xl mx-auto px-4 md:px-6 lg:px-8 h-16 flex items-center justify-between">
<A href="/" class="flex items-center gap-2">
<ShieldLogo />
<span class="text-lg font-bold text-[var(--color-text-primary)]">
Kordant
</span>
</A>
<div class="hidden md:flex items-center gap-6">
<SignedOut>
{marketingLinks.map((link) => (
<NavLink href={link.href} label={link.label} />
))}
</SignedOut>
<SignedIn>
{productLinks.map((link) => (
<NavLink href={link.href} label={link.label} />
))}
</SignedIn>
</div>
<div class="hidden md:flex items-center gap-3">
<ThemeToggle />
<SignedIn>
<UserButton showName />
<RealtimeIndicator />
</SignedIn>
<SignedOut>
<Button variant="secondary" size="sm">
<A href="/login">Sign In</A>
</Button>
<Button variant="primary" size="sm">
<A href="/signup">Get Started</A>
</Button>
</SignedOut>
</div>
<div class="flex md:hidden items-center gap-2">
<ThemeToggle />
<button
type="button"
aria-label="Toggle menu"
class="p-2 rounded-lg text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]"
onClick={() => setMobileOpen((v) => !v)}
>
<Show
when={mobileOpen()}
fallback={
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
>
<line x1="3" y1="6" x2="21" y2="6" />
<line x1="3" y1="12" x2="21" y2="12" />
<line x1="3" y1="18" x2="21" y2="18" />
</svg>
}
>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</Show>
</button>
</div>
</div>
<Show when={mobileOpen()}>
<div class="md:hidden glass border-t border-[var(--color-border)]">
<div class="px-4 py-4 space-y-1">
<SignedOut>
{marketingLinks.map((link) => (
<NavLink href={link.href} label={link.label} mobile />
))}
</SignedOut>
<SignedIn>
{productLinks.map((link) => (
<NavLink href={link.href} label={link.label} mobile />
))}
</SignedIn>
<div class="pt-3 flex flex-col gap-2">
<SignedIn>
<Button variant="secondary" class="w-full">
<A href="/dashboard">Go to Dashboard</A>
</Button>
</SignedIn>
<SignedOut>
<Button variant="secondary" class="w-full">
<A href="/login">Sign In</A>
</Button>
<Button variant="primary" class="w-full">
<A href="/signup">Get Started</A>
</Button>
</SignedOut>
</div>
</div>
</div>
</Show>
</nav>
);
}