Add eslint-plugin-svelte and fix all Svelte linting errors
- Configure eslint-plugin-svelte with TypeScript parser support
- Add keys to all {#each} blocks for proper reactivity
- Wrap navigation paths with resolve() from $app/paths
- Remove unnecessary children snippets and useless mustaches
- Add @typescript-eslint/parser and svelte-eslint-parser dependencies
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,24 @@
|
||||
import { configs } from "@macalinao/eslint-config";
|
||||
import tsParser from "@typescript-eslint/parser";
|
||||
import svelte from "eslint-plugin-svelte";
|
||||
import svelteParser from "svelte-eslint-parser";
|
||||
|
||||
export default [
|
||||
{
|
||||
ignores: [".svelte-kit/**", "build/**"],
|
||||
},
|
||||
...configs.fast,
|
||||
...svelte.configs["flat/recommended"],
|
||||
{
|
||||
files: ["**/*.svelte", "**/*.svelte.ts"],
|
||||
languageOptions: {
|
||||
parser: svelteParser,
|
||||
parserOptions: {
|
||||
parser: tsParser,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
|
||||
@@ -39,9 +39,12 @@
|
||||
"@tailwindcss/vite": "^4.1.4",
|
||||
"@types/ua-parser-js": "^0.7.39",
|
||||
"@types/zxcvbn": "^4.4.5",
|
||||
"@typescript-eslint/parser": "^8.52.0",
|
||||
"eslint": "catalog:",
|
||||
"eslint-plugin-svelte": "^3.14.0",
|
||||
"svelte": "^5.28.2",
|
||||
"svelte-check": "^4.2.1",
|
||||
"svelte-eslint-parser": "^1.4.1",
|
||||
"tailwindcss": "^4.1.4",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "catalog:",
|
||||
|
||||
@@ -3,6 +3,7 @@ import ClockIcon from "@lucide/svelte/icons/clock";
|
||||
import MonitorIcon from "@lucide/svelte/icons/monitor";
|
||||
import ShieldCheckIcon from "@lucide/svelte/icons/shield-check";
|
||||
import UserIcon from "@lucide/svelte/icons/user";
|
||||
import { resolve } from "$app/paths";
|
||||
import { page } from "$app/stores";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
@@ -33,10 +34,10 @@ function isActive(href: string, pathname: string): boolean {
|
||||
className
|
||||
)}
|
||||
>
|
||||
{#each navItems as item}
|
||||
{#each navItems as item (item.href)}
|
||||
{@const active = isActive(item.href, $page.url.pathname)}
|
||||
<a
|
||||
href={item.href}
|
||||
href={resolve(item.href)}
|
||||
class={cn(
|
||||
"inline-flex h-[calc(100%-1px)] items-center justify-center gap-1.5 rounded-md border border-transparent px-3 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow]",
|
||||
active
|
||||
|
||||
@@ -3,6 +3,7 @@ import { AlertTriangle } from "@lucide/svelte";
|
||||
import { useQueryClient } from "@tanstack/svelte-query";
|
||||
import { toast } from "svelte-sonner";
|
||||
import { goto } from "$app/navigation";
|
||||
import { resolve } from "$app/paths";
|
||||
import { api } from "$lib/api/client";
|
||||
import { ErrorAlert } from "$lib/components/auth";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
@@ -51,7 +52,7 @@ async function handleDelete(e: Event) {
|
||||
open = false;
|
||||
|
||||
// Redirect to login
|
||||
goto("/auth/login");
|
||||
goto(resolve("/auth/login"));
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : "Failed to delete account";
|
||||
isDeleting = false;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import type { Snippet } from "svelte";
|
||||
import { createQuery } from "@tanstack/svelte-query";
|
||||
import { goto } from "$app/navigation";
|
||||
import { resolve } from "$app/paths";
|
||||
import { page } from "$app/state";
|
||||
import { api } from "$lib/api/client";
|
||||
|
||||
@@ -25,7 +26,9 @@ const userQuery = createQuery(() => ({
|
||||
// Redirect to login if not authenticated on non-auth pages
|
||||
$effect(() => {
|
||||
if (!isAuthPage && userQuery.error) {
|
||||
goto(`/auth/login?redirect=${encodeURIComponent(page.url.pathname)}`);
|
||||
goto(
|
||||
resolve(`/auth/login?redirect=${encodeURIComponent(page.url.pathname)}`),
|
||||
);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -32,7 +32,7 @@ const config = $derived(strengthConfig[score]);
|
||||
<div class="space-y-2">
|
||||
<!-- Strength bars -->
|
||||
<div class="flex gap-1">
|
||||
{#each Array(4) as _, i}
|
||||
{#each Array(4) as _, i (i)}
|
||||
<div
|
||||
class="h-1 flex-1 rounded-full transition-colors {i < score
|
||||
? config.color
|
||||
@@ -52,7 +52,7 @@ const config = $derived(strengthConfig[score]);
|
||||
{#if result.feedback.warning}
|
||||
<p class="text-destructive">{result.feedback.warning}</p>
|
||||
{/if}
|
||||
{#each result.feedback.suggestions as suggestion}
|
||||
{#each result.feedback.suggestions as suggestion, i (i)}
|
||||
<p>{suggestion}</p>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from "$app/paths";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
interface Props {
|
||||
@@ -25,9 +26,9 @@ const filters = [
|
||||
</div>
|
||||
|
||||
<div class="divide-y divide-border/50">
|
||||
{#each filters as filter}
|
||||
{#each filters as filter (filter.label)}
|
||||
<a
|
||||
href={filter.href}
|
||||
href={resolve(filter.href)}
|
||||
class="group flex items-center gap-3 px-5 py-3 transition-colors hover:bg-muted/30"
|
||||
>
|
||||
<div class="flex h-7 w-7 items-center justify-center rounded-md bg-muted text-muted-foreground transition-colors group-hover:bg-foreground/10 group-hover:text-foreground">
|
||||
|
||||
@@ -46,7 +46,7 @@ function hourToPercent(hour: number): number {
|
||||
<div class="flex">
|
||||
<!-- Y-axis labels -->
|
||||
<div class="flex w-10 flex-col justify-between pr-2" style="height: 210px">
|
||||
{#each hours as hour}
|
||||
{#each hours as hour (hour)}
|
||||
<span class="text-[11px] tabular-nums text-muted-foreground">{hour}</span>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -55,14 +55,14 @@ function hourToPercent(hour: number): number {
|
||||
<div class="relative flex-1">
|
||||
<!-- Grid lines -->
|
||||
<div class="absolute inset-0 flex flex-col justify-between" style="height: 210px">
|
||||
{#each hours as _}
|
||||
{#each hours as hour (hour)}
|
||||
<div class="h-px w-full bg-border"></div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Bars container -->
|
||||
<div class="relative grid grid-cols-7 gap-4 px-2" style="height: 210px">
|
||||
{#each days as _, dayIndex}
|
||||
{#each days as day, dayIndex (day)}
|
||||
{@const thisMonth = thisMonthData[dayIndex]}
|
||||
{@const lastMonth = lastMonthData[dayIndex]}
|
||||
<div class="relative flex justify-center">
|
||||
@@ -104,7 +104,7 @@ function hourToPercent(hour: number): number {
|
||||
|
||||
<!-- X-axis labels -->
|
||||
<div class="mt-2 grid grid-cols-7 gap-4 px-2">
|
||||
{#each days as day}
|
||||
{#each days as day (day)}
|
||||
<div class="text-center text-[11px] text-muted-foreground">{day}</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { resolve } from "$app/paths";
|
||||
import { page } from "$app/stores";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import {
|
||||
@@ -39,7 +40,7 @@ function handleTabChange(tabId: string) {
|
||||
} else {
|
||||
url.searchParams.set("tab", tabId);
|
||||
}
|
||||
goto(url.toString(), { replaceState: true, noScroll: true });
|
||||
goto(resolve(url.toString()), { replaceState: true, noScroll: true });
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -60,7 +61,7 @@ function handleTabChange(tabId: string) {
|
||||
|
||||
<!-- Tab navigation -->
|
||||
<div class="flex items-center gap-0.5" role="tablist">
|
||||
{#each tabs as tab}
|
||||
{#each tabs as tab (tab.id)}
|
||||
{@const isActive = activeTab === tab.id}
|
||||
<button
|
||||
role="tab"
|
||||
|
||||
@@ -78,7 +78,7 @@ const maxRevPercent = Math.max(...tableData.map((d) => d.revPercent));
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#each tableData as row, i}
|
||||
{#each tableData as row, i (row.id)}
|
||||
<Table.Row class="group border-b border-border/50 transition-colors last:border-0 hover:bg-muted/30">
|
||||
<Table.Cell class="w-10 py-3 pl-5">
|
||||
<div class="flex h-4 w-4 items-center justify-center rounded border border-border bg-muted/50 text-[10px] font-medium text-muted-foreground">
|
||||
|
||||
@@ -77,7 +77,7 @@ const maxRevPercent = Math.max(...tableData.map((d) => d.revPercent));
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#each tableData as row, i}
|
||||
{#each tableData as row, i (row.id)}
|
||||
<Table.Row class="group border-b border-border/50 transition-colors last:border-0 hover:bg-muted/30">
|
||||
<Table.Cell class="w-10 py-3 pl-5">
|
||||
<div class="flex h-4 w-4 items-center justify-center rounded border border-border bg-muted/50 text-[10px] font-medium text-muted-foreground">
|
||||
|
||||
@@ -47,7 +47,7 @@ const maxRevPercent = Math.max(...tableData.map((d) => d.revPercent));
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#each tableData as row, i}
|
||||
{#each tableData as row, i (row.id)}
|
||||
<Table.Row class="group border-b border-border/50 transition-colors last:border-0 hover:bg-muted/30">
|
||||
<Table.Cell class="w-10 py-3 pl-5">
|
||||
<div class="flex h-4 w-4 items-center justify-center rounded border border-border bg-muted/50 text-[10px] font-medium text-muted-foreground">
|
||||
|
||||
@@ -69,7 +69,7 @@ const maxRevPercent = Math.max(...tableData.map((d) => d.revPercent));
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#each tableData as row, i}
|
||||
{#each tableData as row, i (row.id)}
|
||||
<Table.Row class="group border-b border-border/50 transition-colors last:border-0 hover:bg-muted/30">
|
||||
<Table.Cell class="w-10 py-3 pl-5">
|
||||
<div class="flex h-4 w-4 items-center justify-center rounded border border-border bg-muted/50 text-[10px] font-medium text-muted-foreground">
|
||||
|
||||
@@ -63,7 +63,7 @@ const maxRevPercent = Math.max(...tableData.map((d) => d.revPercent));
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#each tableData as row, i}
|
||||
{#each tableData as row, i (row.id)}
|
||||
<Table.Row class="group border-b border-border/50 transition-colors last:border-0 hover:bg-muted/30">
|
||||
<Table.Cell class="w-10 py-3 pl-5">
|
||||
<div class="flex h-4 w-4 items-center justify-center rounded border border-border bg-muted/50 text-[10px] font-medium text-muted-foreground">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from "$app/paths";
|
||||
import { Badge } from "$lib/components/ui/badge";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import AdminMobileNav from "./admin-mobile-nav.svelte";
|
||||
@@ -27,7 +28,7 @@ let { title, class: className }: Props = $props();
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<a
|
||||
href="/dashboard"
|
||||
href={resolve("/dashboard")}
|
||||
class="flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
||||
>
|
||||
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { createQuery, useQueryClient } from "@tanstack/svelte-query";
|
||||
import { goto } from "$app/navigation";
|
||||
import { resolve } from "$app/paths";
|
||||
import { page } from "$app/stores";
|
||||
import { api } from "$lib/api/client";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
@@ -52,7 +53,7 @@ async function handleSignOut() {
|
||||
await api.auth.logout();
|
||||
queryClient.clear();
|
||||
open = false;
|
||||
goto("/login");
|
||||
goto(resolve("/login"));
|
||||
} catch (error) {
|
||||
console.error("Failed to sign out:", error);
|
||||
}
|
||||
@@ -92,13 +93,13 @@ const navItems = [
|
||||
|
||||
<nav class="flex flex-1 flex-col p-4">
|
||||
<div class="space-y-1">
|
||||
{#each navItems as item}
|
||||
{#each navItems as item (item.href)}
|
||||
{@const isActive =
|
||||
item.href === "/admin"
|
||||
? $page.url.pathname === "/admin"
|
||||
: $page.url.pathname.startsWith(item.href)}
|
||||
<a
|
||||
href={item.href}
|
||||
href={resolve(item.href)}
|
||||
onclick={handleNavClick}
|
||||
class={cn(
|
||||
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors",
|
||||
@@ -135,7 +136,7 @@ const navItems = [
|
||||
<div class="mt-6">
|
||||
<Separator class="bg-zinc-800" />
|
||||
<a
|
||||
href="/dashboard"
|
||||
href={resolve("/dashboard")}
|
||||
onclick={handleNavClick}
|
||||
class="mt-4 flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium text-zinc-400 transition-colors hover:bg-zinc-800/50 hover:text-zinc-200"
|
||||
>
|
||||
@@ -165,7 +166,7 @@ const navItems = [
|
||||
|
||||
<div class="mt-2 space-y-1">
|
||||
<a
|
||||
href="/account"
|
||||
href={resolve("/account")}
|
||||
onclick={handleNavClick}
|
||||
class="flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium text-zinc-400 transition-colors hover:bg-zinc-800/50 hover:text-zinc-200"
|
||||
>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { createQuery, useQueryClient } from "@tanstack/svelte-query";
|
||||
import { goto } from "$app/navigation";
|
||||
import { resolve } from "$app/paths";
|
||||
import { page } from "$app/stores";
|
||||
import { api } from "$lib/api/client";
|
||||
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
|
||||
@@ -43,7 +44,7 @@ async function handleSignOut() {
|
||||
try {
|
||||
await api.auth.logout();
|
||||
queryClient.clear();
|
||||
goto("/login");
|
||||
goto(resolve("/login"));
|
||||
} catch (error) {
|
||||
console.error("Failed to sign out:", error);
|
||||
}
|
||||
@@ -66,7 +67,7 @@ const navItems = [
|
||||
<!-- Admin Logo -->
|
||||
<div class="flex h-[94px] items-center justify-center">
|
||||
<a
|
||||
href="/admin"
|
||||
href={resolve("/admin")}
|
||||
class="group flex h-8 w-8 items-center justify-center rounded-lg bg-red-600 shadow-sm transition-transform duration-200 hover:scale-105"
|
||||
aria-label="Admin Home"
|
||||
>
|
||||
@@ -84,13 +85,13 @@ const navItems = [
|
||||
|
||||
<!-- Main Navigation -->
|
||||
<nav class="flex flex-1 flex-col items-center gap-3">
|
||||
{#each navItems as item}
|
||||
{#each navItems as item (item.href)}
|
||||
{@const isActive =
|
||||
item.href === "/admin"
|
||||
? $page.url.pathname === "/admin"
|
||||
: $page.url.pathname.startsWith(item.href)}
|
||||
<a
|
||||
href={item.href}
|
||||
href={resolve(item.href)}
|
||||
class={cn(
|
||||
"group relative flex h-8 w-8 items-center justify-center rounded-lg transition-all duration-150",
|
||||
isActive
|
||||
@@ -157,7 +158,7 @@ const navItems = [
|
||||
<div class="flex flex-col items-center gap-3 pb-6">
|
||||
<!-- Back to Dashboard link -->
|
||||
<a
|
||||
href="/dashboard"
|
||||
href={resolve("/dashboard")}
|
||||
class="group relative flex h-8 w-8 items-center justify-center rounded-lg text-zinc-400 transition-all duration-150 hover:bg-zinc-800 hover:text-zinc-200"
|
||||
aria-label="Back to Dashboard"
|
||||
>
|
||||
@@ -210,7 +211,7 @@ const navItems = [
|
||||
</div>
|
||||
</div>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item onSelect={() => goto("/account")}>
|
||||
<DropdownMenu.Item onSelect={() => goto(resolve("/account"))}>
|
||||
<svg class="mr-2 h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
|
||||
<path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<circle cx="12" cy="7" r="4" />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Settings } from "@lucide/svelte";
|
||||
import { getContext } from "svelte";
|
||||
import { resolve } from "$app/paths";
|
||||
import { page } from "$app/stores";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import OrgSwitcher from "./org-switcher.svelte";
|
||||
@@ -67,14 +68,14 @@ const navItems = $derived.by(() => {
|
||||
|
||||
<!-- Main Navigation -->
|
||||
<nav class="flex flex-1 flex-col items-center gap-3">
|
||||
{#each navItems as item}
|
||||
{#each navItems as item (item.href)}
|
||||
{@const isActive =
|
||||
item.icon === "home"
|
||||
? $page.url.pathname === item.href
|
||||
: $page.url.pathname === item.href ||
|
||||
$page.url.pathname.startsWith(item.href + "/")}
|
||||
<a
|
||||
href={item.href}
|
||||
href={resolve(item.href)}
|
||||
class={cn(
|
||||
"group relative flex h-8 w-8 items-center justify-center rounded-lg transition-all duration-150",
|
||||
isActive
|
||||
@@ -162,7 +163,7 @@ const navItems = $derived.by(() => {
|
||||
{#if currentSlug}
|
||||
{@const isSettingsActive = $page.url.pathname.startsWith(`/dashboard/${currentSlug}/settings`)}
|
||||
<a
|
||||
href="/dashboard/{currentSlug}/settings"
|
||||
href={resolve(`/dashboard/${currentSlug}/settings`)}
|
||||
class={cn(
|
||||
"group relative flex h-8 w-8 items-center justify-center rounded-lg transition-all duration-150",
|
||||
isSettingsActive
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { createQuery, useQueryClient } from "@tanstack/svelte-query";
|
||||
import { getContext } from "svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
import { resolve } from "$app/paths";
|
||||
import { page } from "$app/stores";
|
||||
import { api } from "$lib/api/client";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
@@ -85,7 +86,7 @@ async function handleSignOut() {
|
||||
await api.auth.logout();
|
||||
queryClient.clear();
|
||||
open = false;
|
||||
goto("/login");
|
||||
goto(resolve("/login"));
|
||||
} catch (error) {
|
||||
console.error("Failed to sign out:", error);
|
||||
}
|
||||
@@ -118,12 +119,12 @@ async function handleSignOut() {
|
||||
|
||||
<nav class="flex flex-1 flex-col p-4">
|
||||
<div class="space-y-1">
|
||||
{#each navItems as item}
|
||||
{#each navItems as item (item.href)}
|
||||
{@const isActive =
|
||||
$page.url.pathname === item.href ||
|
||||
(item.href !== "/" && $page.url.pathname.startsWith(item.href))}
|
||||
<a
|
||||
href={item.href}
|
||||
href={resolve(item.href)}
|
||||
onclick={handleNavClick}
|
||||
class={cn(
|
||||
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors",
|
||||
@@ -184,7 +185,7 @@ async function handleSignOut() {
|
||||
|
||||
<div class="mt-2 space-y-1">
|
||||
<a
|
||||
href="/account"
|
||||
href={resolve("/account")}
|
||||
onclick={handleNavClick}
|
||||
class="flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium text-muted-foreground transition-colors hover:bg-accent/50 hover:text-foreground"
|
||||
>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { createQuery } from "@tanstack/svelte-query";
|
||||
import { getContext } from "svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
import { resolve } from "$app/paths";
|
||||
import { api } from "$lib/api/client";
|
||||
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
|
||||
import { cn } from "$lib/utils.js";
|
||||
@@ -19,7 +20,7 @@ const orgsQuery = createQuery(() => ({
|
||||
const orgs = $derived(orgsQuery.data ?? []);
|
||||
|
||||
function handleOrgSelect(slug: string) {
|
||||
goto(`/dashboard/${slug}`);
|
||||
goto(resolve(`/dashboard/${slug}`));
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -51,7 +52,7 @@ function handleOrgSelect(slug: string) {
|
||||
{:else if orgs.length === 0}
|
||||
<DropdownMenu.Item disabled>No organizations</DropdownMenu.Item>
|
||||
{:else}
|
||||
{#each orgs as org}
|
||||
{#each orgs as org (org.slug)}
|
||||
{@const isActive = currentSlug === org.slug}
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => handleOrgSelect(org.slug)}
|
||||
@@ -76,7 +77,7 @@ function handleOrgSelect(slug: string) {
|
||||
{/each}
|
||||
{/if}
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item onSelect={() => goto("/dashboard/new")}>
|
||||
<DropdownMenu.Item onSelect={() => goto(resolve("/dashboard/new"))}>
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="12" y1="5" x2="12" y2="19" stroke-linecap="round" />
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { createQuery, useQueryClient } from "@tanstack/svelte-query";
|
||||
import { getContext } from "svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
import { resolve } from "$app/paths";
|
||||
import { api } from "$lib/api/client";
|
||||
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
|
||||
|
||||
@@ -43,7 +44,7 @@ async function handleSignOut() {
|
||||
await api.auth.logout();
|
||||
// Clear all cached queries
|
||||
queryClient.clear();
|
||||
goto("/login");
|
||||
goto(resolve("/login"));
|
||||
} catch (error) {
|
||||
console.error("Failed to sign out:", error);
|
||||
}
|
||||
@@ -92,7 +93,7 @@ async function handleSignOut() {
|
||||
</div>
|
||||
</div>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item onSelect={() => goto("/account")}>
|
||||
<DropdownMenu.Item onSelect={() => goto(resolve("/account"))}>
|
||||
<svg class="mr-2 h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
|
||||
<path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<circle cx="12" cy="7" r="4" />
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import type { Snippet } from "svelte";
|
||||
import { Building2, Globe, Settings, Users } from "@lucide/svelte";
|
||||
import { getContext } from "svelte";
|
||||
import { resolve } from "$app/paths";
|
||||
import { page } from "$app/stores";
|
||||
import { DashboardLayout } from "$lib/components/layout";
|
||||
import { cn } from "$lib/utils.js";
|
||||
@@ -58,10 +59,10 @@ function isActive(href: string): boolean {
|
||||
<nav class="w-full shrink-0 lg:w-64">
|
||||
<!-- Mobile: horizontal scroll -->
|
||||
<div class="flex gap-2 overflow-x-auto pb-2 lg:hidden">
|
||||
{#each navItems as item}
|
||||
{#each navItems as item (item.href)}
|
||||
{@const active = isActive(item.href)}
|
||||
<a
|
||||
href={item.href}
|
||||
href={resolve(item.href)}
|
||||
class={cn(
|
||||
"flex shrink-0 items-center gap-2 rounded-lg border px-3 py-2 text-sm font-medium transition-colors",
|
||||
active
|
||||
@@ -77,10 +78,10 @@ function isActive(href: string): boolean {
|
||||
|
||||
<!-- Desktop: vertical list -->
|
||||
<div class="hidden space-y-1 lg:block">
|
||||
{#each navItems as item}
|
||||
{#each navItems as item (item.href)}
|
||||
{@const active = isActive(item.href)}
|
||||
<a
|
||||
href={item.href}
|
||||
href={resolve(item.href)}
|
||||
class={cn(
|
||||
"group flex items-start gap-3 rounded-lg px-3 py-2.5 transition-colors",
|
||||
active
|
||||
|
||||
@@ -48,6 +48,7 @@ export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
/* eslint-disable svelte/no-navigation-without-resolve -- Button receives href as prop, callers must use resolve() */
|
||||
let {
|
||||
class: className,
|
||||
variant = "default",
|
||||
|
||||
@@ -24,9 +24,7 @@ const queryClient = new QueryClient({
|
||||
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthGuard>
|
||||
{#snippet children()}
|
||||
{@render children()}
|
||||
{/snippet}
|
||||
{@render children()}
|
||||
</AuthGuard>
|
||||
<SvelteQueryDevtools />
|
||||
</QueryClientProvider>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { Loader2 } from "@lucide/svelte";
|
||||
import { createQuery } from "@tanstack/svelte-query";
|
||||
import { goto } from "$app/navigation";
|
||||
import { resolve } from "$app/paths";
|
||||
import { api } from "$lib/api/client";
|
||||
|
||||
/**
|
||||
@@ -16,14 +17,16 @@ const orgsQuery = createQuery(() => ({
|
||||
$effect(() => {
|
||||
if (orgsQuery.error) {
|
||||
// Not authenticated, redirect to login
|
||||
goto(`/auth/login?redirect=${encodeURIComponent("/")}`);
|
||||
goto(resolve(`/auth/login?redirect=${encodeURIComponent("/")}`));
|
||||
} else if (orgsQuery.data) {
|
||||
if (orgsQuery.data.length > 0) {
|
||||
// Redirect to first org's dashboard
|
||||
goto(`/dashboard/${orgsQuery.data[0].slug}`, { replaceState: true });
|
||||
goto(resolve(`/dashboard/${orgsQuery.data[0].slug}`), {
|
||||
replaceState: true,
|
||||
});
|
||||
} else {
|
||||
// No orgs, show org list (empty state)
|
||||
goto("/dashboard", { replaceState: true });
|
||||
goto(resolve("/dashboard"), { replaceState: true });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from "@tanstack/svelte-query";
|
||||
import { toast } from "svelte-sonner";
|
||||
import { goto } from "$app/navigation";
|
||||
import { resolve } from "$app/paths";
|
||||
import { page } from "$app/state";
|
||||
import { api } from "$lib/api/client";
|
||||
import { Alert, AlertDescription } from "$lib/components/ui/alert";
|
||||
@@ -52,9 +53,9 @@ const acceptMutation = createMutation(() => ({
|
||||
queryClient.invalidateQueries({ queryKey: ["orgs"] });
|
||||
// Redirect to the org dashboard
|
||||
if (inviteQuery.data) {
|
||||
goto(`/dashboard/${inviteQuery.data.org.slug}`);
|
||||
goto(resolve(`/dashboard/${inviteQuery.data.org.slug}`));
|
||||
} else {
|
||||
goto("/dashboard");
|
||||
goto(resolve("/dashboard"));
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
@@ -71,7 +72,7 @@ const declineMutation = createMutation(() => ({
|
||||
toast.success("Invitation declined");
|
||||
// Invalidate queries
|
||||
queryClient.invalidateQueries({ queryKey: ["me", "invites"] });
|
||||
goto("/dashboard");
|
||||
goto(resolve("/dashboard"));
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(
|
||||
@@ -102,6 +103,7 @@ function formatDate(date: Date): string {
|
||||
* Check if invite is expiring soon (within 3 days)
|
||||
*/
|
||||
function isExpiringSoon(expiresAt: Date): boolean {
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- pure function, no reactivity needed
|
||||
const threeDaysFromNow = new Date();
|
||||
threeDaysFromNow.setDate(threeDaysFromNow.getDate() + 3);
|
||||
return expiresAt < threeDaysFromNow;
|
||||
@@ -114,7 +116,7 @@ function isExpiringSoon(expiresAt: Date): boolean {
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Back link -->
|
||||
<Button variant="ghost" size="sm" href="/dashboard" class="-ml-2">
|
||||
<Button variant="ghost" size="sm" href={resolve("/dashboard")} class="-ml-2">
|
||||
<ArrowLeft class="mr-2 h-4 w-4" />
|
||||
Back to Dashboard
|
||||
</Button>
|
||||
@@ -131,7 +133,7 @@ function isExpiringSoon(expiresAt: Date): boolean {
|
||||
{inviteQuery.error instanceof Error ? inviteQuery.error.message : "Failed to load invitation"}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<Button variant="outline" href="/dashboard">
|
||||
<Button variant="outline" href={resolve("/dashboard")}>
|
||||
Go to Dashboard
|
||||
</Button>
|
||||
{:else if inviteQuery.data}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { createQuery } from "@tanstack/svelte-query";
|
||||
import { setContext } from "svelte";
|
||||
import { toast } from "svelte-sonner";
|
||||
import { goto } from "$app/navigation";
|
||||
import { resolve } from "$app/paths";
|
||||
import { api } from "$lib/api/client.js";
|
||||
|
||||
interface Props {
|
||||
@@ -22,11 +23,13 @@ const userQuery = createQuery(() => ({
|
||||
$effect(() => {
|
||||
if (userQuery.data && !userQuery.data.isSuperuser) {
|
||||
toast.error("Access denied. Superuser privileges required.");
|
||||
goto("/dashboard");
|
||||
goto(resolve("/dashboard"));
|
||||
}
|
||||
if (userQuery.error) {
|
||||
goto(
|
||||
`/auth/login?redirect=${encodeURIComponent(window.location.pathname)}`,
|
||||
resolve(
|
||||
`/auth/login?redirect=${encodeURIComponent(window.location.pathname)}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { AlertCircle, Building, Loader2, Plus, Users } from "@lucide/svelte";
|
||||
import { createQuery } from "@tanstack/svelte-query";
|
||||
import { resolve } from "$app/paths";
|
||||
import { api } from "$lib/api/client.js";
|
||||
import { AdminLayout } from "$lib/components/layout";
|
||||
import { Button } from "$lib/components/ui/button/index.js";
|
||||
@@ -55,7 +56,7 @@ const hasError = $derived(orgsQuery.error || usersQuery.error);
|
||||
<!-- Summary cards -->
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<!-- Organizations card -->
|
||||
<a href="/admin/orgs" class="group block transition-transform hover:scale-[1.02]">
|
||||
<a href={resolve("/admin/orgs")} class="group block transition-transform hover:scale-[1.02]">
|
||||
<Card class="h-full transition-colors group-hover:border-primary/50">
|
||||
<CardHeader class="pb-2">
|
||||
<CardTitle class="flex items-center gap-2 text-base">
|
||||
@@ -71,7 +72,7 @@ const hasError = $derived(orgsQuery.error || usersQuery.error);
|
||||
</a>
|
||||
|
||||
<!-- Users card -->
|
||||
<a href="/admin/users" class="group block transition-transform hover:scale-[1.02]">
|
||||
<a href={resolve("/admin/users")} class="group block transition-transform hover:scale-[1.02]">
|
||||
<Card class="h-full transition-colors group-hover:border-primary/50">
|
||||
<CardHeader class="pb-2">
|
||||
<CardTitle class="flex items-center gap-2 text-base">
|
||||
@@ -94,7 +95,7 @@ const hasError = $derived(orgsQuery.error || usersQuery.error);
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Button href="/admin/orgs/new">
|
||||
<Button href={resolve("/admin/orgs/new")}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
New Organization
|
||||
</Button>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { AlertCircle, Building, Eye, Plus, Trash2 } from "@lucide/svelte";
|
||||
import { createQuery, useQueryClient } from "@tanstack/svelte-query";
|
||||
import { toast } from "svelte-sonner";
|
||||
import { resolve } from "$app/paths";
|
||||
import { api } from "$lib/api/client.js";
|
||||
import { AdminLayout } from "$lib/components/layout";
|
||||
import ConfirmDialog from "$lib/components/org/confirm-dialog.svelte";
|
||||
@@ -106,7 +107,7 @@ async function executeConfirmAction() {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each Array(5) as _}
|
||||
{#each Array(5) as _, i (i)}
|
||||
<TableRow>
|
||||
<TableCell><Skeleton class="h-4 w-24" /></TableCell>
|
||||
<TableCell><Skeleton class="h-4 w-32" /></TableCell>
|
||||
@@ -137,7 +138,7 @@ async function executeConfirmAction() {
|
||||
<h2 class="text-lg font-semibold">
|
||||
Organizations ({orgsQuery.data.length})
|
||||
</h2>
|
||||
<Button href="/admin/orgs/new">
|
||||
<Button href={resolve("/admin/orgs/new")}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
New Organization
|
||||
</Button>
|
||||
@@ -154,7 +155,7 @@ async function executeConfirmAction() {
|
||||
<p class="mt-2 text-center text-sm text-muted-foreground">
|
||||
Create your first organization to get started.
|
||||
</p>
|
||||
<Button href="/admin/orgs/new" class="mt-4">
|
||||
<Button href={resolve("/admin/orgs/new")} class="mt-4">
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
New Organization
|
||||
</Button>
|
||||
@@ -192,7 +193,7 @@ async function executeConfirmAction() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
href="/dashboard/{org.slug}"
|
||||
href={resolve(`/dashboard/${org.slug}`)}
|
||||
title="View organization"
|
||||
>
|
||||
<Eye class="h-4 w-4" />
|
||||
@@ -221,7 +222,7 @@ async function executeConfirmAction() {
|
||||
<!-- Back link -->
|
||||
<div class="pt-4">
|
||||
<a
|
||||
href="/admin"
|
||||
href={resolve("/admin")}
|
||||
class="text-sm text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
← Back to admin dashboard
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { createQuery, useQueryClient } from "@tanstack/svelte-query";
|
||||
import { toast } from "svelte-sonner";
|
||||
import { goto } from "$app/navigation";
|
||||
import { resolve } from "$app/paths";
|
||||
import { page } from "$app/state";
|
||||
import { api } from "$lib/api/client";
|
||||
import { AdminLayout } from "$lib/components/layout";
|
||||
@@ -186,7 +187,7 @@ function handleDelete() {
|
||||
await api.admin.orgs.delete({ slug: slug ?? "" });
|
||||
toast.success("Organization deleted");
|
||||
await queryClient.invalidateQueries({ queryKey: ["admin", "orgs"] });
|
||||
goto("/admin/orgs");
|
||||
goto(resolve("/admin/orgs"));
|
||||
} catch (e) {
|
||||
toast.error(
|
||||
e instanceof Error ? e.message : "Failed to delete organization",
|
||||
@@ -235,7 +236,7 @@ async function executeConfirmAction() {
|
||||
: "Failed to load organization"}
|
||||
</p>
|
||||
<a
|
||||
href="/admin/orgs"
|
||||
href={resolve("/admin/orgs")}
|
||||
class="mt-4 text-sm text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<ArrowLeft class="mr-1 inline h-4 w-4" />
|
||||
@@ -247,7 +248,7 @@ async function executeConfirmAction() {
|
||||
<div class="mx-auto max-w-2xl space-y-6">
|
||||
<!-- Back link -->
|
||||
<a
|
||||
href="/admin/orgs"
|
||||
href={resolve("/admin/orgs")}
|
||||
class="inline-flex items-center text-sm text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<ArrowLeft class="mr-1 h-4 w-4" />
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { ArrowLeft, Loader2 } from "@lucide/svelte";
|
||||
import { toast } from "svelte-sonner";
|
||||
import { goto } from "$app/navigation";
|
||||
import { resolve } from "$app/paths";
|
||||
import { api } from "$lib/api/client.js";
|
||||
import { AdminLayout } from "$lib/components/layout";
|
||||
import { Button } from "$lib/components/ui/button/index.js";
|
||||
@@ -49,7 +50,7 @@ async function handleSubmit() {
|
||||
ownerEmail: ownerEmail.trim(),
|
||||
});
|
||||
toast.success("Organization created successfully");
|
||||
goto("/admin/orgs");
|
||||
goto(resolve("/admin/orgs"));
|
||||
} catch (e) {
|
||||
toast.error(
|
||||
e instanceof Error ? e.message : "Failed to create organization",
|
||||
@@ -78,7 +79,7 @@ function handleSlugInput(event: Event) {
|
||||
<div class="mx-auto max-w-2xl space-y-6">
|
||||
<!-- Back link -->
|
||||
<a
|
||||
href="/admin/orgs"
|
||||
href={resolve("/admin/orgs")}
|
||||
class="inline-flex items-center text-sm text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<ArrowLeft class="mr-1 h-4 w-4" />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { AlertCircle, Check, Eye, Users, X } from "@lucide/svelte";
|
||||
import { createQuery } from "@tanstack/svelte-query";
|
||||
import { resolve } from "$app/paths";
|
||||
import { api } from "$lib/api/client.js";
|
||||
import { SuperuserBadge } from "$lib/components/admin/index.js";
|
||||
import { AdminLayout } from "$lib/components/layout";
|
||||
@@ -59,7 +60,7 @@ const usersQuery = createQuery(() => ({
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each Array(5) as _}
|
||||
{#each Array(5) as _, i (i)}
|
||||
<TableRow>
|
||||
<TableCell><Skeleton class="h-4 w-40" /></TableCell>
|
||||
<TableCell><Skeleton class="h-4 w-24" /></TableCell>
|
||||
@@ -124,7 +125,7 @@ const usersQuery = createQuery(() => ({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
href="/admin/users/{encodeURIComponent(user.email)}"
|
||||
href={resolve(`/admin/users/${encodeURIComponent(user.email)}`)}
|
||||
>
|
||||
<Eye class="mr-1 h-4 w-4" />
|
||||
View
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from "@lucide/svelte";
|
||||
import { createQuery, useQueryClient } from "@tanstack/svelte-query";
|
||||
import { toast } from "svelte-sonner";
|
||||
import { resolve } from "$app/paths";
|
||||
import { page } from "$app/state";
|
||||
import { api } from "$lib/api/client.js";
|
||||
import { SuperuserBadge } from "$lib/components/admin/index.js";
|
||||
@@ -150,7 +151,7 @@ async function handleConfirmEmail() {
|
||||
<AdminLayout title="User Details">
|
||||
<!-- Back navigation -->
|
||||
<div class="mb-6">
|
||||
<Button variant="ghost" size="sm" href="/admin/users" class="gap-1">
|
||||
<Button variant="ghost" size="sm" href={resolve("/admin/users")} class="gap-1">
|
||||
<ArrowLeft class="h-4 w-4" />
|
||||
Back to users
|
||||
</Button>
|
||||
@@ -179,7 +180,7 @@ async function handleConfirmEmail() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
{#each Array(5) as _}
|
||||
{#each Array(5) as _, i (i)}
|
||||
<div class="space-y-1">
|
||||
<Skeleton class="h-4 w-20" />
|
||||
<Skeleton class="h-5 w-32" />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from "svelte";
|
||||
import { resolve } from "$app/paths";
|
||||
|
||||
interface Props {
|
||||
children: Snippet;
|
||||
@@ -80,9 +81,9 @@ let { children }: Props = $props();
|
||||
<!-- Footer -->
|
||||
<p class="text-center text-xs text-muted-foreground">
|
||||
By continuing, you agree to our
|
||||
<a href="/terms" class="underline underline-offset-4 hover:text-foreground">Terms of Service</a>
|
||||
<a href={resolve("/terms")} class="underline underline-offset-4 hover:text-foreground">Terms of Service</a>
|
||||
and
|
||||
<a href="/privacy" class="underline underline-offset-4 hover:text-foreground">Privacy Policy</a>
|
||||
<a href={resolve("/privacy")} class="underline underline-offset-4 hover:text-foreground">Privacy Policy</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from "@lucide/svelte";
|
||||
import { createQuery } from "@tanstack/svelte-query";
|
||||
import { goto } from "$app/navigation";
|
||||
import { resolve } from "$app/paths";
|
||||
import { api } from "$lib/api/client";
|
||||
import { ErrorAlert } from "$lib/components/auth";
|
||||
import { Alert, AlertDescription } from "$lib/components/ui/alert";
|
||||
@@ -40,7 +41,7 @@ async function copyToClipboard() {
|
||||
// Guard: redirect to /auth/login if no active login flow
|
||||
$effect(() => {
|
||||
if (!loginFlowState.email) {
|
||||
goto("/auth/login");
|
||||
goto(resolve("/auth/login"));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -58,7 +59,7 @@ const statusQuery = createQuery(() => ({
|
||||
$effect(() => {
|
||||
if (statusQuery.data?.status === "completed") {
|
||||
clearLoginFlowState();
|
||||
goto(statusQuery.data.redirectTo || "/");
|
||||
goto(resolve(statusQuery.data.redirectTo || "/"));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -88,7 +89,7 @@ async function handleResendEmail() {
|
||||
|
||||
function handleDifferentEmail() {
|
||||
clearLoginFlowState();
|
||||
goto("/auth/login");
|
||||
goto(resolve("/auth/login"));
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { CheckCircle2 } from "@lucide/svelte";
|
||||
import { resolve } from "$app/paths";
|
||||
import { api } from "$lib/api/client";
|
||||
import { ErrorAlert } from "$lib/components/auth";
|
||||
import { Alert, AlertDescription } from "$lib/components/ui/alert";
|
||||
@@ -119,8 +120,8 @@ async function handleSubmit(e: Event) {
|
||||
|
||||
<!-- Back to login link -->
|
||||
<div class="text-center text-sm text-muted-foreground">
|
||||
Remember your password?{" "}
|
||||
<a href="/auth/login" class="text-foreground underline underline-offset-4 hover:text-primary">
|
||||
Remember your password?
|
||||
<a href={resolve("/auth/login")} class="text-foreground underline underline-offset-4 hover:text-primary">
|
||||
Sign in
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { resolve } from "$app/paths";
|
||||
import { api } from "$lib/api/client";
|
||||
import { ErrorAlert } from "$lib/components/auth";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
@@ -21,12 +22,12 @@ async function handleSubmit(e: SubmitEvent) {
|
||||
setLoginFlowState(response);
|
||||
|
||||
if (response.hasPasskey) {
|
||||
goto("/auth/login/passkey");
|
||||
goto(resolve("/auth/login/passkey"));
|
||||
} else if (response.hasPassword) {
|
||||
goto("/auth/login/password");
|
||||
goto(resolve("/auth/login/password"));
|
||||
} else {
|
||||
// Anti-enumeration: always redirect to confirm even if user doesn't exist
|
||||
goto("/auth/confirm");
|
||||
goto(resolve("/auth/confirm"));
|
||||
}
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : "An unexpected error occurred";
|
||||
@@ -75,7 +76,7 @@ async function handleSubmit(e: SubmitEvent) {
|
||||
<div class="text-center">
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Don't have an account?
|
||||
<a href="/auth/signup" class="text-sm text-muted-foreground underline underline-offset-4 hover:text-foreground">
|
||||
<a href={resolve("/auth/signup")} class="text-sm text-muted-foreground underline underline-offset-4 hover:text-foreground">
|
||||
Sign up
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { Fingerprint, KeyRound, Loader2 } from "@lucide/svelte";
|
||||
import { startAuthentication } from "@simplewebauthn/browser";
|
||||
import { goto } from "$app/navigation";
|
||||
import { resolve } from "$app/paths";
|
||||
import { api } from "$lib/api/client";
|
||||
import { ErrorAlert } from "$lib/components/auth";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
@@ -43,7 +44,7 @@ async function authenticate(): Promise<void> {
|
||||
});
|
||||
|
||||
// Success - redirect to confirm for session creation
|
||||
goto("/auth/confirm");
|
||||
goto(resolve("/auth/confirm"));
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : "Authentication failed";
|
||||
hasAttempted = true;
|
||||
@@ -55,7 +56,7 @@ async function authenticate(): Promise<void> {
|
||||
// Guard: redirect to /auth/login if no active login flow
|
||||
$effect(() => {
|
||||
if (!loginFlowState.email) {
|
||||
goto("/auth/login");
|
||||
goto(resolve("/auth/login"));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -134,7 +135,7 @@ $effect(() => {
|
||||
|
||||
<!-- Fallback links -->
|
||||
{#if loginFlowState.hasPassword}
|
||||
<Button variant="outline" class="h-10 w-full" href="/auth/login/password">
|
||||
<Button variant="outline" class="h-10 w-full" href={resolve("/auth/login/password")}>
|
||||
Use password instead
|
||||
</Button>
|
||||
{/if}
|
||||
@@ -142,7 +143,7 @@ $effect(() => {
|
||||
<div class="text-center">
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => goto("/auth/login")}
|
||||
onclick={() => goto(resolve("/auth/login"))}
|
||||
class="text-sm text-muted-foreground underline underline-offset-4 hover:text-foreground"
|
||||
>
|
||||
Use a different email
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { resolve } from "$app/paths";
|
||||
import { api } from "$lib/api/client";
|
||||
import { ErrorAlert, PasswordInput } from "$lib/components/auth";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
@@ -14,7 +15,7 @@ let error = $state<string | null>(null);
|
||||
// Guard: redirect to /auth/login if no active login flow
|
||||
$effect(() => {
|
||||
if (!loginFlowState.email) {
|
||||
goto("/auth/login");
|
||||
goto(resolve("/auth/login"));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -26,7 +27,7 @@ async function handleSubmit(e: SubmitEvent) {
|
||||
try {
|
||||
await api.auth.loginPassword({ password });
|
||||
// On success, redirect to confirm page for email verification
|
||||
goto("/auth/confirm");
|
||||
goto(resolve("/auth/confirm"));
|
||||
} catch (err) {
|
||||
error =
|
||||
err instanceof Error
|
||||
@@ -38,7 +39,7 @@ async function handleSubmit(e: SubmitEvent) {
|
||||
|
||||
function handleDifferentEmail() {
|
||||
clearLoginFlowState();
|
||||
goto("/auth/login");
|
||||
goto(resolve("/auth/login"));
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -82,7 +83,7 @@ function handleDifferentEmail() {
|
||||
<!-- Secondary Links -->
|
||||
<div class="space-y-3 text-center">
|
||||
<a
|
||||
href="/auth/forgot-password"
|
||||
href={resolve("/auth/forgot-password")}
|
||||
class="text-sm text-muted-foreground underline underline-offset-4 hover:text-foreground"
|
||||
>
|
||||
Forgot password?
|
||||
@@ -91,7 +92,7 @@ function handleDifferentEmail() {
|
||||
{#if loginFlowState.hasPasskey}
|
||||
<div>
|
||||
<a
|
||||
href="/auth/login/passkey"
|
||||
href={resolve("/auth/login/passkey")}
|
||||
class="text-sm text-muted-foreground underline underline-offset-4 hover:text-foreground"
|
||||
>
|
||||
Use passkey instead
|
||||
|
||||
@@ -3,6 +3,7 @@ import { AlertCircle } from "@lucide/svelte";
|
||||
import { toast } from "svelte-sonner";
|
||||
import zxcvbn from "zxcvbn";
|
||||
import { goto } from "$app/navigation";
|
||||
import { resolve } from "$app/paths";
|
||||
import { page } from "$app/stores";
|
||||
import { api } from "$lib/api/client";
|
||||
import {
|
||||
@@ -56,7 +57,7 @@ async function handleSubmit(e: Event) {
|
||||
toast.success("Password reset successfully", {
|
||||
description: "You can now sign in with your new password.",
|
||||
});
|
||||
await goto("/auth/login");
|
||||
await goto(resolve("/auth/login"));
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
// Handle specific error cases
|
||||
@@ -97,7 +98,7 @@ async function handleSubmit(e: Event) {
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<Button variant="outline" class="h-10 w-full" onclick={() => goto("/auth/forgot-password")}>
|
||||
<Button variant="outline" class="h-10 w-full" onclick={() => goto(resolve("/auth/forgot-password"))}>
|
||||
Request new reset link
|
||||
</Button>
|
||||
{:else}
|
||||
@@ -147,8 +148,8 @@ async function handleSubmit(e: Event) {
|
||||
|
||||
<!-- Back to login link -->
|
||||
<div class="text-center text-sm text-muted-foreground">
|
||||
Remember your password?{" "}
|
||||
<a href="/auth/login" class="text-foreground underline underline-offset-4 hover:text-primary">
|
||||
Remember your password?
|
||||
<a href={resolve("/auth/login")} class="text-foreground underline underline-offset-4 hover:text-primary">
|
||||
Sign in
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { AlertCircle, Loader2 } from "@lucide/svelte";
|
||||
import { createQuery } from "@tanstack/svelte-query";
|
||||
import { toast } from "svelte-sonner";
|
||||
import { goto } from "$app/navigation";
|
||||
import { resolve } from "$app/paths";
|
||||
import { api } from "$lib/api/client";
|
||||
import { ErrorAlert } from "$lib/components/auth";
|
||||
import { Alert, AlertDescription } from "$lib/components/ui/alert";
|
||||
@@ -22,7 +23,7 @@ const userQuery = createQuery(() => ({
|
||||
// Redirect if user doesn't need setup
|
||||
$effect(() => {
|
||||
if (userQuery.data && !userQuery.data.needsSetup) {
|
||||
goto("/");
|
||||
goto(resolve("/"));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -68,7 +69,7 @@ async function handleSubmit(e: Event) {
|
||||
});
|
||||
|
||||
toast.success("Profile setup complete!");
|
||||
goto("/");
|
||||
goto(resolve("/"));
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : "Failed to save profile";
|
||||
} finally {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
} from "@simplewebauthn/browser";
|
||||
import zxcvbn from "zxcvbn";
|
||||
import { goto } from "$app/navigation";
|
||||
import { resolve } from "$app/paths";
|
||||
import { api } from "$lib/api/client";
|
||||
import {
|
||||
ErrorAlert,
|
||||
@@ -75,7 +76,7 @@ async function handlePasskeySignup() {
|
||||
});
|
||||
|
||||
// Redirect to user setup
|
||||
await goto("/auth/setup/user");
|
||||
await goto(resolve("/auth/setup/user"));
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
// Handle WebAuthn cancellation
|
||||
@@ -103,7 +104,7 @@ async function handlePasswordSignup() {
|
||||
});
|
||||
|
||||
// Redirect to user setup
|
||||
await goto("/auth/setup/user");
|
||||
await goto(resolve("/auth/setup/user"));
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
error = err.message;
|
||||
@@ -249,8 +250,8 @@ function switchToPasskey() {
|
||||
|
||||
<!-- Sign in link -->
|
||||
<div class="text-center text-sm text-muted-foreground">
|
||||
Already have an account?{" "}
|
||||
<a href="/auth/login" class="text-foreground underline underline-offset-4 hover:text-primary">
|
||||
Already have an account?
|
||||
<a href={resolve("/auth/login")} class="text-foreground underline underline-offset-4 hover:text-primary">
|
||||
Sign in
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { createQuery } from "@tanstack/svelte-query";
|
||||
import { toast } from "svelte-sonner";
|
||||
import { UAParser } from "ua-parser-js";
|
||||
import { goto } from "$app/navigation";
|
||||
import { resolve } from "$app/paths";
|
||||
import { api } from "$lib/api/client";
|
||||
import { ErrorAlert } from "$lib/components/auth";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
@@ -52,7 +53,7 @@ async function handleTrust() {
|
||||
try {
|
||||
await api.me.devices.trust({ name: deviceName.trim() });
|
||||
toast.success("Device trusted successfully!");
|
||||
goto("/");
|
||||
goto(resolve("/"));
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : "Failed to trust device";
|
||||
} finally {
|
||||
@@ -61,7 +62,7 @@ async function handleTrust() {
|
||||
}
|
||||
|
||||
async function handleSkip() {
|
||||
goto("/performance");
|
||||
goto(resolve("/performance"));
|
||||
}
|
||||
|
||||
// Get device icon based on type
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { CheckCircle2, Loader2, Mail, XCircle } from "@lucide/svelte";
|
||||
import { toast } from "svelte-sonner";
|
||||
import { goto } from "$app/navigation";
|
||||
import { resolve } from "$app/paths";
|
||||
import { page } from "$app/state";
|
||||
import { api } from "$lib/api/client";
|
||||
import { ErrorAlert } from "$lib/components/auth";
|
||||
@@ -31,7 +32,7 @@ async function verifyEmail(): Promise<void> {
|
||||
try {
|
||||
await api.auth.verifyEmail({ token });
|
||||
toast.success("Email verified successfully!");
|
||||
goto("/");
|
||||
goto(resolve("/"));
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : "Verification failed";
|
||||
} finally {
|
||||
@@ -132,7 +133,7 @@ async function resendVerification(): Promise<void> {
|
||||
|
||||
<div class="text-center">
|
||||
<a
|
||||
href="/auth/login"
|
||||
href={resolve("/auth/login")}
|
||||
class="text-sm text-muted-foreground underline underline-offset-4 hover:text-foreground"
|
||||
>
|
||||
Back to login
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from "@lucide/svelte";
|
||||
import { createQuery } from "@tanstack/svelte-query";
|
||||
import { goto } from "$app/navigation";
|
||||
import { resolve } from "$app/paths";
|
||||
import { api } from "$lib/api/client";
|
||||
import { DashboardLayout } from "$lib/components/layout";
|
||||
import { Badge } from "$lib/components/ui/badge";
|
||||
@@ -40,7 +41,9 @@ const invitesQuery = createQuery(() => ({
|
||||
$effect(() => {
|
||||
if (orgsQuery.error) {
|
||||
goto(
|
||||
`/auth/login?redirect=${encodeURIComponent(window.location.pathname)}`,
|
||||
resolve(
|
||||
`/auth/login?redirect=${encodeURIComponent(window.location.pathname)}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -98,7 +101,7 @@ function formatRole(role: string): string {
|
||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{#each invitesQuery.data as invite (invite.id)}
|
||||
<a
|
||||
href="/account/org-invites/{invite.id}"
|
||||
href={resolve(`/account/org-invites/${invite.id}`)}
|
||||
class="group block"
|
||||
>
|
||||
<Card class="h-full border-primary/30 bg-primary/5 transition-colors group-hover:border-primary/50">
|
||||
@@ -183,7 +186,7 @@ function formatRole(role: string): string {
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{#each orgsQuery.data as org (org.id)}
|
||||
<a
|
||||
href="/dashboard/{org.slug}"
|
||||
href={resolve(`/dashboard/${org.slug}`)}
|
||||
class="group block transition-transform hover:scale-[1.02]"
|
||||
>
|
||||
<Card class="h-full transition-colors group-hover:border-primary/50">
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from "@lucide/svelte";
|
||||
import { createQuery } from "@tanstack/svelte-query";
|
||||
import { getContext } from "svelte";
|
||||
import { resolve } from "$app/paths";
|
||||
import { api } from "$lib/api/client";
|
||||
import { DashboardLayout } from "$lib/components/layout";
|
||||
import { RoleBadge } from "$lib/components/org";
|
||||
@@ -86,7 +87,7 @@ const orgName = $derived(orgQuery.data?.displayName ?? slug);
|
||||
: "Failed to load organization"}
|
||||
</p>
|
||||
<a
|
||||
href="/dashboard"
|
||||
href={resolve("/dashboard")}
|
||||
class="mt-4 text-sm text-primary underline underline-offset-4 hover:text-primary/80"
|
||||
>
|
||||
Back to organizations
|
||||
@@ -117,7 +118,7 @@ const orgName = $derived(orgQuery.data?.displayName ?? slug);
|
||||
</div>
|
||||
</div>
|
||||
{#if canManageOrg}
|
||||
<Button variant="outline" href="/dashboard/{slug}/settings">
|
||||
<Button variant="outline" href={resolve(`/dashboard/${slug}/settings`)}>
|
||||
<Settings class="mr-2 h-4 w-4" />
|
||||
Settings
|
||||
</Button>
|
||||
@@ -126,7 +127,7 @@ const orgName = $derived(orgQuery.data?.displayName ?? slug);
|
||||
|
||||
<!-- Stats cards -->
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<a href="/dashboard/{slug}/members" class="group">
|
||||
<a href={resolve(`/dashboard/${slug}/members`)} class="group">
|
||||
<Card class="transition-colors group-hover:border-primary/50">
|
||||
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle class="text-sm font-medium">Members</CardTitle>
|
||||
@@ -163,7 +164,7 @@ const orgName = $derived(orgQuery.data?.displayName ?? slug);
|
||||
<div class="flex items-center justify-between">
|
||||
<CardTitle class="text-base">Team Members</CardTitle>
|
||||
<a
|
||||
href="/dashboard/{slug}/members"
|
||||
href={resolve(`/dashboard/${slug}/members`)}
|
||||
class="flex items-center text-sm text-primary hover:underline"
|
||||
>
|
||||
View all
|
||||
|
||||
@@ -300,7 +300,7 @@ const availableInviteRoles = $derived.by(() => {
|
||||
{inviteRole.charAt(0).toUpperCase() + inviteRole.slice(1)}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{#each availableInviteRoles as role}
|
||||
{#each availableInviteRoles as role (role)}
|
||||
<SelectItem value={role} label={role.charAt(0).toUpperCase() + role.slice(1)} />
|
||||
{/each}
|
||||
</SelectContent>
|
||||
|
||||
@@ -47,7 +47,7 @@ const metrics = [
|
||||
<!-- Metric Cards -->
|
||||
<section>
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
||||
{#each metrics as metric}
|
||||
{#each metrics as metric (metric.label)}
|
||||
<MetricCard
|
||||
label={metric.label}
|
||||
value={metric.value}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { createQuery, useQueryClient } from "@tanstack/svelte-query";
|
||||
import { getContext } from "svelte";
|
||||
import { toast } from "svelte-sonner";
|
||||
import { goto } from "$app/navigation";
|
||||
import { resolve } from "$app/paths";
|
||||
import { api } from "$lib/api/client";
|
||||
import { SettingsLayout } from "$lib/components/layout";
|
||||
import { ConfirmDialog } from "$lib/components/org";
|
||||
@@ -124,7 +125,7 @@ function handleLeave() {
|
||||
await api.orgs.leave({ slug });
|
||||
toast.success("You have left the organization");
|
||||
await queryClient.invalidateQueries({ queryKey: ["orgs"] });
|
||||
goto("/dashboard");
|
||||
goto(resolve("/dashboard"));
|
||||
} catch (e) {
|
||||
toast.error(
|
||||
e instanceof Error ? e.message : "Failed to leave organization",
|
||||
@@ -147,7 +148,7 @@ function handleDelete() {
|
||||
await api.orgs.delete({ slug });
|
||||
toast.success("Organization deleted");
|
||||
await queryClient.invalidateQueries({ queryKey: ["orgs"] });
|
||||
goto("/dashboard");
|
||||
goto(resolve("/dashboard"));
|
||||
} catch (e) {
|
||||
toast.error(
|
||||
e instanceof Error ? e.message : "Failed to delete organization",
|
||||
|
||||
@@ -300,7 +300,7 @@ const availableInviteRoles = $derived.by(() => {
|
||||
{inviteRole.charAt(0).toUpperCase() + inviteRole.slice(1)}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{#each availableInviteRoles as role}
|
||||
{#each availableInviteRoles as role (role)}
|
||||
<SelectItem value={role} label={role.charAt(0).toUpperCase() + role.slice(1)} />
|
||||
{/each}
|
||||
</SelectContent>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { CheckCircle2, Loader2, UserPlus, XCircle } from "@lucide/svelte";
|
||||
import { toast } from "svelte-sonner";
|
||||
import { goto } from "$app/navigation";
|
||||
import { resolve } from "$app/paths";
|
||||
import { page } from "$app/state";
|
||||
import { api } from "$lib/api/client";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
@@ -45,7 +46,7 @@ async function acceptInvite(): Promise<void> {
|
||||
if (!isAuthenticated) {
|
||||
// Redirect to login with return URL
|
||||
const returnUrl = `/invite/accept?token=${encodeURIComponent(token)}`;
|
||||
goto(`/auth/login?redirect=${encodeURIComponent(returnUrl)}`);
|
||||
goto(resolve(`/auth/login?redirect=${encodeURIComponent(returnUrl)}`));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -55,7 +56,7 @@ async function acceptInvite(): Promise<void> {
|
||||
toast.success("You've joined the organization!");
|
||||
// Redirect to dashboard after a short delay
|
||||
setTimeout(() => {
|
||||
goto("/dashboard");
|
||||
goto(resolve("/dashboard"));
|
||||
}, 1500);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
@@ -168,13 +169,13 @@ $effect(() => {
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
<Button variant="outline" class="h-10 w-full" href="/dashboard">
|
||||
<Button variant="outline" class="h-10 w-full" href={resolve("/dashboard")}>
|
||||
Go to Dashboard
|
||||
</Button>
|
||||
|
||||
<div class="text-center">
|
||||
<a
|
||||
href="/auth/login"
|
||||
href={resolve("/auth/login")}
|
||||
class="text-sm text-muted-foreground underline underline-offset-4 hover:text-foreground"
|
||||
>
|
||||
Sign in with a different account
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { resolve } from "$app/paths";
|
||||
|
||||
// Redirect old /login route to new /auth/login
|
||||
$effect(() => {
|
||||
goto("/auth/login", { replaceState: true });
|
||||
goto(resolve("/auth/login"), { replaceState: true });
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user