Reorganize layouts with dedicated admin sidebar (dark theme)
- Create admin layout with dark sidebar (zinc-900 background, light text) - Move dashboard components to layout/dashboard/ subfolder - Move admin components to layout/admin/ subfolder - Admin sidebar has: Dashboard, Organizations, Users nav items - Admin header shows "Admin" badge and "Exit Admin" link - Update all route imports to use new barrel exports - Add macOS sed syntax reference to CLAUDE.md Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,3 +6,11 @@ Before starting the dev server, check if it's already running:
|
|||||||
- Use `lsof -i :6827` or check for existing background tasks
|
- Use `lsof -i :6827` or check for existing background tasks
|
||||||
- The dev server runs on port 6827 (may fall back to 6828 if port is in use)
|
- The dev server runs on port 6827 (may fall back to 6828 if port is in use)
|
||||||
- Start with `bun run --cwd apps/publisher-dashboard dev` or `devenv up`
|
- Start with `bun run --cwd apps/publisher-dashboard dev` or `devenv up`
|
||||||
|
|
||||||
|
## macOS sed Syntax
|
||||||
|
|
||||||
|
macOS uses BSD sed which differs from GNU sed:
|
||||||
|
- In-place edit requires empty string for backup: `sed -i '' 's/old/new/g' file`
|
||||||
|
- GNU sed (Linux): `sed -i 's/old/new/g' file`
|
||||||
|
- Use `|` as delimiter when patterns contain `/`: `sed -i '' 's|old/path|new/path|g' file`
|
||||||
|
- For multiple files: `for f in *.txt; do sed -i '' 's/old/new/g' "$f"; done`
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Badge } from "$lib/components/ui/badge";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
import AdminMobileNav from "./admin-mobile-nav.svelte";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
class?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { title, class: className }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<header
|
||||||
|
class={cn(
|
||||||
|
"flex h-14 items-center justify-between border-b border-border bg-card px-4 lg:px-6",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<!-- Mobile menu button -->
|
||||||
|
<AdminMobileNav class="lg:hidden" />
|
||||||
|
|
||||||
|
<h1 class="text-base font-semibold tracking-tight text-foreground lg:text-lg">{title}</h1>
|
||||||
|
<Badge variant="destructive" class="hidden sm:inline-flex">Admin</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<a
|
||||||
|
href="/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">
|
||||||
|
<path d="M19 12H5M12 19l-7-7 7-7" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
<span class="hidden sm:inline">Exit Admin</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Snippet } from "svelte";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
import AdminHeader from "./admin-header.svelte";
|
||||||
|
import AdminMobileNav from "./admin-mobile-nav.svelte";
|
||||||
|
import AdminSidebar from "./admin-sidebar.svelte";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
children: Snippet;
|
||||||
|
class?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { title, children, class: className }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex h-screen overflow-hidden bg-background">
|
||||||
|
<!-- Desktop sidebar - hidden on mobile -->
|
||||||
|
<div class="hidden lg:block">
|
||||||
|
<AdminSidebar />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-1 flex-col overflow-hidden">
|
||||||
|
<AdminHeader {title} />
|
||||||
|
|
||||||
|
<main class="flex-1 overflow-auto p-4 lg:p-6">
|
||||||
|
<div class={cn("mx-auto max-w-7xl", className)}>
|
||||||
|
{@render children()}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { createQuery, useQueryClient } from "@tanstack/svelte-query";
|
||||||
|
import { goto } from "$app/navigation";
|
||||||
|
import { page } from "$app/stores";
|
||||||
|
import { api } from "$lib/api/client";
|
||||||
|
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";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
class?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { class: className }: Props = $props();
|
||||||
|
|
||||||
|
let open = $state(false);
|
||||||
|
|
||||||
|
// Fetch current user
|
||||||
|
const userQuery = createQuery(() => ({
|
||||||
|
queryKey: ["me"],
|
||||||
|
queryFn: () => api.me.get(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const user = $derived(userQuery.data);
|
||||||
|
|
||||||
|
// Generate initials from display name or email
|
||||||
|
const initials = $derived.by(() => {
|
||||||
|
if (!user) return "??";
|
||||||
|
if (user.displayName) {
|
||||||
|
const parts = user.displayName.split(" ");
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
return (parts[0].charAt(0) + parts[parts.length - 1].charAt(0)).toUpperCase();
|
||||||
|
}
|
||||||
|
return user.displayName.slice(0, 2).toUpperCase();
|
||||||
|
}
|
||||||
|
return user.email.slice(0, 2).toUpperCase();
|
||||||
|
});
|
||||||
|
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
function handleNavClick() {
|
||||||
|
open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSignOut() {
|
||||||
|
try {
|
||||||
|
await api.auth.logout();
|
||||||
|
queryClient.clear();
|
||||||
|
open = false;
|
||||||
|
goto("/login");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to sign out:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin nav items
|
||||||
|
const navItems = [
|
||||||
|
{ icon: "dashboard", href: "/admin", label: "Dashboard" },
|
||||||
|
{ icon: "building", href: "/admin/orgs", label: "Organizations" },
|
||||||
|
{ icon: "users", href: "/admin/users", label: "Users" },
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Sheet.Root bind:open>
|
||||||
|
<Sheet.Trigger>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<Button variant="ghost" size="icon" class={cn("h-9 w-9 lg:hidden", className)} {...props}>
|
||||||
|
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
|
||||||
|
<path d="M3 12h18M3 6h18M3 18h18" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
<span class="sr-only">Open menu</span>
|
||||||
|
</Button>
|
||||||
|
{/snippet}
|
||||||
|
</Sheet.Trigger>
|
||||||
|
|
||||||
|
<Sheet.Content side="left" class="w-72 border-zinc-800 bg-zinc-900 p-0">
|
||||||
|
<Sheet.Header class="border-b border-zinc-800 px-6 py-4">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="flex h-9 w-9 items-center justify-center rounded-lg bg-red-600">
|
||||||
|
<svg class="h-5 w-5 text-white" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||||||
|
<path d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<Sheet.Title class="text-lg font-semibold text-white">Admin Panel</Sheet.Title>
|
||||||
|
</div>
|
||||||
|
</Sheet.Header>
|
||||||
|
|
||||||
|
<nav class="flex flex-1 flex-col p-4">
|
||||||
|
<div class="space-y-1">
|
||||||
|
{#each navItems as item}
|
||||||
|
{@const isActive =
|
||||||
|
item.href === "/admin"
|
||||||
|
? $page.url.pathname === "/admin"
|
||||||
|
: $page.url.pathname.startsWith(item.href)}
|
||||||
|
<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",
|
||||||
|
isActive
|
||||||
|
? "bg-zinc-800 text-white"
|
||||||
|
: "text-zinc-400 hover:bg-zinc-800/50 hover:text-zinc-200",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{#if item.icon === "dashboard"}
|
||||||
|
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
|
||||||
|
<rect x="3" y="3" width="7" height="9" rx="1" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
<rect x="14" y="3" width="7" height="5" rx="1" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
<rect x="14" y="12" width="7" height="9" rx="1" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
<rect x="3" y="16" width="7" height="5" rx="1" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
{:else if item.icon === "building"}
|
||||||
|
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
|
||||||
|
<path d="M3 21h18M5 21V5a2 2 0 012-2h10a2 2 0 012 2v16" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
<path d="M9 6.5h1.5M9 10h1.5M9 13.5h1.5M13.5 6.5H15M13.5 10H15M13.5 13.5H15M9 21v-4h6v4" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
{:else if item.icon === "users"}
|
||||||
|
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
|
||||||
|
<path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
<circle cx="9" cy="7" r="4" />
|
||||||
|
<path d="M23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
{item.label}
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Separator and back to dashboard -->
|
||||||
|
<div class="mt-6">
|
||||||
|
<Separator class="bg-zinc-800" />
|
||||||
|
<a
|
||||||
|
href="/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"
|
||||||
|
>
|
||||||
|
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
|
||||||
|
<path d="M19 12H5M12 19l-7-7 7-7" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
Back to Dashboard
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- User section at bottom -->
|
||||||
|
<div class="mt-auto pt-4">
|
||||||
|
<Separator class="mb-4 bg-zinc-800" />
|
||||||
|
<div class="flex items-center gap-3 rounded-lg px-3 py-2">
|
||||||
|
{#if user?.avatarUrl}
|
||||||
|
<img src={user.avatarUrl} alt="" class="h-9 w-9 rounded-full object-cover" />
|
||||||
|
{:else}
|
||||||
|
<div class="flex h-9 w-9 items-center justify-center rounded-full bg-gradient-to-br from-red-500 to-red-700 text-xs font-semibold text-white">
|
||||||
|
{initials}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="flex-1">
|
||||||
|
<p class="text-sm font-medium text-white">{user?.displayName ?? user?.email ?? "Loading..."}</p>
|
||||||
|
<p class="text-xs text-zinc-400">Admin</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-2 space-y-1">
|
||||||
|
<a
|
||||||
|
href="/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"
|
||||||
|
>
|
||||||
|
<svg class="h-5 w-5" 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" />
|
||||||
|
</svg>
|
||||||
|
Account Settings
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
onclick={handleSignOut}
|
||||||
|
class="flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium text-red-400 transition-colors hover:bg-red-500/10"
|
||||||
|
>
|
||||||
|
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
|
||||||
|
<path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
<polyline points="16,17 21,12 16,7" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
<line x1="21" y1="12" x2="9" y2="12" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
Sign out
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</Sheet.Content>
|
||||||
|
</Sheet.Root>
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { createQuery, useQueryClient } from "@tanstack/svelte-query";
|
||||||
|
import { goto } from "$app/navigation";
|
||||||
|
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";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
class?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { class: className }: Props = $props();
|
||||||
|
|
||||||
|
// Fetch current user
|
||||||
|
const userQuery = createQuery(() => ({
|
||||||
|
queryKey: ["me"],
|
||||||
|
queryFn: () => api.me.get(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const user = $derived(userQuery.data);
|
||||||
|
|
||||||
|
// Generate initials from display name or email
|
||||||
|
const initials = $derived.by(() => {
|
||||||
|
if (!user) return "??";
|
||||||
|
if (user.displayName) {
|
||||||
|
const parts = user.displayName.split(" ");
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
return (parts[0].charAt(0) + parts[parts.length - 1].charAt(0)).toUpperCase();
|
||||||
|
}
|
||||||
|
return user.displayName.slice(0, 2).toUpperCase();
|
||||||
|
}
|
||||||
|
return user.email.slice(0, 2).toUpperCase();
|
||||||
|
});
|
||||||
|
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
async function handleSignOut() {
|
||||||
|
try {
|
||||||
|
await api.auth.logout();
|
||||||
|
queryClient.clear();
|
||||||
|
goto("/login");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to sign out:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin nav items
|
||||||
|
const navItems = [
|
||||||
|
{ icon: "dashboard", href: "/admin", label: "Dashboard" },
|
||||||
|
{ icon: "building", href: "/admin/orgs", label: "Organizations" },
|
||||||
|
{ icon: "users", href: "/admin/users", label: "Users" },
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<aside
|
||||||
|
class={cn(
|
||||||
|
"flex h-screen w-[80px] flex-col items-center bg-zinc-900",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<!-- Admin Logo -->
|
||||||
|
<div class="flex h-[94px] items-center justify-center">
|
||||||
|
<a
|
||||||
|
href="/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"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="h-4 w-4 text-white transition-transform duration-200 group-hover:scale-110"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2.5"
|
||||||
|
>
|
||||||
|
<path d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Navigation -->
|
||||||
|
<nav class="flex flex-1 flex-col items-center gap-3">
|
||||||
|
{#each navItems as item}
|
||||||
|
{@const isActive =
|
||||||
|
item.href === "/admin"
|
||||||
|
? $page.url.pathname === "/admin"
|
||||||
|
: $page.url.pathname.startsWith(item.href)}
|
||||||
|
<a
|
||||||
|
href={item.href}
|
||||||
|
class={cn(
|
||||||
|
"group relative flex h-8 w-8 items-center justify-center rounded-lg transition-all duration-150",
|
||||||
|
isActive
|
||||||
|
? "bg-zinc-700 text-white"
|
||||||
|
: "text-zinc-400 hover:bg-zinc-800 hover:text-zinc-200",
|
||||||
|
)}
|
||||||
|
aria-label={item.label}
|
||||||
|
aria-current={isActive ? "page" : undefined}
|
||||||
|
>
|
||||||
|
{#if item.icon === "dashboard"}
|
||||||
|
{#if isActive}
|
||||||
|
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z" />
|
||||||
|
</svg>
|
||||||
|
{:else}
|
||||||
|
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
|
||||||
|
<rect x="3" y="3" width="7" height="9" rx="1" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
<rect x="14" y="3" width="7" height="5" rx="1" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
<rect x="14" y="12" width="7" height="9" rx="1" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
<rect x="3" y="16" width="7" height="5" rx="1" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
{:else if item.icon === "building"}
|
||||||
|
{#if isActive}
|
||||||
|
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M4.5 2.25a.75.75 0 000 1.5v16.5h-.75a.75.75 0 000 1.5h16.5a.75.75 0 000-1.5h-.75V3.75a.75.75 0 000-1.5h-15zM9 6a.75.75 0 000 1.5h1.5a.75.75 0 000-1.5H9zm-.75 3.75A.75.75 0 019 9h1.5a.75.75 0 010 1.5H9a.75.75 0 01-.75-.75zM9 12a.75.75 0 000 1.5h1.5a.75.75 0 000-1.5H9zm3.75-5.25A.75.75 0 0113.5 6H15a.75.75 0 010 1.5h-1.5a.75.75 0 01-.75-.75zM13.5 9a.75.75 0 000 1.5H15A.75.75 0 0015 9h-1.5zm-.75 3.75a.75.75 0 01.75-.75H15a.75.75 0 010 1.5h-1.5a.75.75 0 01-.75-.75zM9 19.5v-2.25a.75.75 0 01.75-.75h4.5a.75.75 0 01.75.75v2.25H9z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{:else}
|
||||||
|
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
|
||||||
|
<path d="M3 21h18M5 21V5a2 2 0 012-2h10a2 2 0 012 2v16" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
<path d="M9 6.5h1.5M9 10h1.5M9 13.5h1.5M13.5 6.5H15M13.5 10H15M13.5 13.5H15M9 21v-4h6v4" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
{:else if item.icon === "users"}
|
||||||
|
{#if isActive}
|
||||||
|
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M8.25 6.75a3.75 3.75 0 117.5 0 3.75 3.75 0 01-7.5 0zM15.75 9.75a3 3 0 116 0 3 3 0 01-6 0zM2.25 9.75a3 3 0 116 0 3 3 0 01-6 0zM6.31 15.117A6.745 6.745 0 0112 12a6.745 6.745 0 016.709 7.498.75.75 0 01-.372.568A12.696 12.696 0 0112 21.75c-2.305 0-4.47-.612-6.337-1.684a.75.75 0 01-.372-.568 6.787 6.787 0 011.019-4.38z" />
|
||||||
|
<path d="M5.082 14.254a8.287 8.287 0 00-1.308 5.135 9.687 9.687 0 01-1.764-.44l-.115-.04a.563.563 0 01-.373-.487l-.01-.121a3.75 3.75 0 013.57-4.047zM20.226 19.389a8.287 8.287 0 00-1.308-5.135 3.75 3.75 0 013.57 4.047l-.01.121a.563.563 0 01-.373.486l-.115.04c-.567.2-1.156.349-1.764.441z" />
|
||||||
|
</svg>
|
||||||
|
{:else}
|
||||||
|
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
|
||||||
|
<path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
<circle cx="9" cy="7" r="4" />
|
||||||
|
<path d="M23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Tooltip -->
|
||||||
|
<span
|
||||||
|
class="pointer-events-none absolute left-full ml-3 whitespace-nowrap rounded-md bg-zinc-700 px-2.5 py-1.5 text-xs font-medium text-white opacity-0 shadow-lg transition-all duration-150 group-hover:opacity-100"
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Bottom section -->
|
||||||
|
<div class="flex flex-col items-center gap-3 pb-6">
|
||||||
|
<!-- Back to Dashboard link -->
|
||||||
|
<a
|
||||||
|
href="/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"
|
||||||
|
>
|
||||||
|
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
|
||||||
|
<path d="M19 12H5M12 19l-7-7 7-7" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
<span
|
||||||
|
class="pointer-events-none absolute left-full ml-3 whitespace-nowrap rounded-md bg-zinc-700 px-2.5 py-1.5 text-xs font-medium text-white opacity-0 shadow-lg transition-all duration-150 group-hover:opacity-100"
|
||||||
|
>
|
||||||
|
Back to Dashboard
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- User Menu -->
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<button
|
||||||
|
{...props}
|
||||||
|
class="relative h-6 w-6 overflow-hidden rounded-full ring-1 ring-zinc-700 transition-transform duration-150 hover:scale-110"
|
||||||
|
aria-label="User menu"
|
||||||
|
>
|
||||||
|
{#if user?.avatarUrl}
|
||||||
|
<img src={user.avatarUrl} alt="" class="h-full w-full object-cover" />
|
||||||
|
{:else}
|
||||||
|
<div
|
||||||
|
class="flex h-full w-full items-center justify-center bg-gradient-to-br from-red-500 to-red-700 text-[10px] font-semibold text-white"
|
||||||
|
>
|
||||||
|
{initials}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
{/snippet}
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content class="w-64" side="right" align="end" sideOffset={8}>
|
||||||
|
<!-- User info header -->
|
||||||
|
<div class="flex items-center gap-3 p-2">
|
||||||
|
{#if user?.avatarUrl}
|
||||||
|
<img src={user.avatarUrl} alt="" class="h-10 w-10 rounded-full object-cover" />
|
||||||
|
{:else}
|
||||||
|
<div
|
||||||
|
class="flex h-10 w-10 items-center justify-center rounded-full bg-gradient-to-br from-red-500 to-red-700 text-sm font-semibold text-white"
|
||||||
|
>
|
||||||
|
{initials}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-sm font-medium">{user?.displayName ?? user?.email ?? "Loading..."}</span>
|
||||||
|
<span class="text-xs text-muted-foreground">Admin</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<DropdownMenu.Item onSelect={() => goto("/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" />
|
||||||
|
</svg>
|
||||||
|
Account Settings
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<DropdownMenu.Item onSelect={handleSignOut} variant="destructive">
|
||||||
|
<svg class="mr-2 h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
|
||||||
|
<path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
<polyline points="16,17 21,12 16,7" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
<line x1="21" y1="12" x2="9" y2="12" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
Sign out
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export { default as AdminHeader } from "./admin-header.svelte";
|
||||||
|
export { default as AdminLayout } from "./admin-layout.svelte";
|
||||||
|
export { default as AdminMobileNav } from "./admin-mobile-nav.svelte";
|
||||||
|
export { default as AdminSidebar } from "./admin-sidebar.svelte";
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export { default as AppHeader } from "./app-header.svelte";
|
||||||
|
export { default as AppSidebar } from "./app-sidebar.svelte";
|
||||||
|
export { default as DashboardLayout } from "./dashboard-layout.svelte";
|
||||||
|
export { default as EmailVerificationBanner } from "./email-verification-banner.svelte";
|
||||||
|
export { default as MobileNav } from "./mobile-nav.svelte";
|
||||||
|
export { default as OrgSwitcher } from "./org-switcher.svelte";
|
||||||
|
export { default as UserMenu } from "./user-menu.svelte";
|
||||||
@@ -1,5 +1,18 @@
|
|||||||
export { default as AppHeader } from "./app-header.svelte";
|
// Dashboard layout components
|
||||||
export { default as AppSidebar } from "./app-sidebar.svelte";
|
export {
|
||||||
export { default as DashboardLayout } from "./dashboard-layout.svelte";
|
AppHeader,
|
||||||
export { default as EmailVerificationBanner } from "./email-verification-banner.svelte";
|
AppSidebar,
|
||||||
export { default as MobileNav } from "./mobile-nav.svelte";
|
DashboardLayout,
|
||||||
|
EmailVerificationBanner,
|
||||||
|
MobileNav,
|
||||||
|
OrgSwitcher,
|
||||||
|
UserMenu,
|
||||||
|
} from "./dashboard/index.js";
|
||||||
|
|
||||||
|
// Admin layout components
|
||||||
|
export {
|
||||||
|
AdminHeader,
|
||||||
|
AdminLayout,
|
||||||
|
AdminMobileNav,
|
||||||
|
AdminSidebar,
|
||||||
|
} from "./admin/index.js";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Snippet } from "svelte";
|
import type { Snippet } from "svelte";
|
||||||
import { AccountNav } from "$lib/components/account";
|
import { AccountNav } from "$lib/components/account";
|
||||||
import DashboardLayout from "$lib/components/layout/dashboard-layout.svelte";
|
import { DashboardLayout } from "$lib/components/layout";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: Snippet;
|
children: Snippet;
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
import { AlertCircle, Building, Loader2, Plus, Users } from "@lucide/svelte";
|
import { AlertCircle, Building, Loader2, Plus, Users } from "@lucide/svelte";
|
||||||
import { createQuery } from "@tanstack/svelte-query";
|
import { createQuery } from "@tanstack/svelte-query";
|
||||||
import { api } from "$lib/api/client.js";
|
import { api } from "$lib/api/client.js";
|
||||||
import DashboardLayout from "$lib/components/layout/dashboard-layout.svelte";
|
import { AdminLayout } from "$lib/components/layout";
|
||||||
import { Badge } from "$lib/components/ui/badge/index.js";
|
|
||||||
import { Button } from "$lib/components/ui/button/index.js";
|
import { Button } from "$lib/components/ui/button/index.js";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -36,13 +35,8 @@ const hasError = $derived(orgsQuery.error || usersQuery.error);
|
|||||||
<title>Admin Dashboard | Publisher Dashboard</title>
|
<title>Admin Dashboard | Publisher Dashboard</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<DashboardLayout title="Admin Dashboard">
|
<AdminLayout title="Dashboard">
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<!-- Admin badge -->
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<Badge variant="destructive">Admin</Badge>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if isLoading}
|
{#if isLoading}
|
||||||
<!-- Loading state -->
|
<!-- Loading state -->
|
||||||
<div class="flex flex-col items-center justify-center py-16">
|
<div class="flex flex-col items-center justify-center py-16">
|
||||||
@@ -109,4 +103,4 @@ const hasError = $derived(orgsQuery.error || usersQuery.error);
|
|||||||
</Card>
|
</Card>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</DashboardLayout>
|
</AdminLayout>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { AlertCircle, Building, Eye, Plus, Trash2 } from "@lucide/svelte";
|
|||||||
import { createQuery, useQueryClient } from "@tanstack/svelte-query";
|
import { createQuery, useQueryClient } from "@tanstack/svelte-query";
|
||||||
import { toast } from "svelte-sonner";
|
import { toast } from "svelte-sonner";
|
||||||
import { api } from "$lib/api/client.js";
|
import { api } from "$lib/api/client.js";
|
||||||
import DashboardLayout from "$lib/components/layout/dashboard-layout.svelte";
|
import { AdminLayout } from "$lib/components/layout";
|
||||||
import ConfirmDialog from "$lib/components/org/confirm-dialog.svelte";
|
import ConfirmDialog from "$lib/components/org/confirm-dialog.svelte";
|
||||||
import { Button } from "$lib/components/ui/button/index.js";
|
import { Button } from "$lib/components/ui/button/index.js";
|
||||||
import {
|
import {
|
||||||
@@ -80,7 +80,7 @@ async function executeConfirmAction() {
|
|||||||
<title>Organizations | Admin | Publisher Dashboard</title>
|
<title>Organizations | Admin | Publisher Dashboard</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<DashboardLayout title="Organizations">
|
<AdminLayout title="Organizations">
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
{#if orgsQuery.isPending}
|
{#if orgsQuery.isPending}
|
||||||
<!-- Loading skeleton -->
|
<!-- Loading skeleton -->
|
||||||
@@ -229,7 +229,7 @@ async function executeConfirmAction() {
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</DashboardLayout>
|
</AdminLayout>
|
||||||
|
|
||||||
<!-- Confirmation dialog -->
|
<!-- Confirmation dialog -->
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { toast } from "svelte-sonner";
|
|||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import { page } from "$app/state";
|
import { page } from "$app/state";
|
||||||
import { api } from "$lib/api/client";
|
import { api } from "$lib/api/client";
|
||||||
import DashboardLayout from "$lib/components/layout/dashboard-layout.svelte";
|
import { AdminLayout } from "$lib/components/layout";
|
||||||
import { ConfirmDialog } from "$lib/components/org";
|
import { ConfirmDialog } from "$lib/components/org";
|
||||||
import { Alert, AlertDescription } from "$lib/components/ui/alert";
|
import { Alert, AlertDescription } from "$lib/components/ui/alert";
|
||||||
import { Button } from "$lib/components/ui/button";
|
import { Button } from "$lib/components/ui/button";
|
||||||
@@ -220,7 +220,7 @@ async function executeConfirmAction() {
|
|||||||
</title>
|
</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<DashboardLayout title="Organization Details">
|
<AdminLayout title="Organization Details">
|
||||||
{#if orgQuery.isPending}
|
{#if orgQuery.isPending}
|
||||||
<div class="flex flex-col items-center justify-center py-16">
|
<div class="flex flex-col items-center justify-center py-16">
|
||||||
<Loader2 class="h-8 w-8 animate-spin text-muted-foreground" />
|
<Loader2 class="h-8 w-8 animate-spin text-muted-foreground" />
|
||||||
@@ -456,7 +456,7 @@ async function executeConfirmAction() {
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</DashboardLayout>
|
</AdminLayout>
|
||||||
|
|
||||||
<!-- Confirmation dialog -->
|
<!-- Confirmation dialog -->
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { ArrowLeft, Loader2 } from "@lucide/svelte";
|
|||||||
import { toast } from "svelte-sonner";
|
import { toast } from "svelte-sonner";
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import { api } from "$lib/api/client.js";
|
import { api } from "$lib/api/client.js";
|
||||||
import DashboardLayout from "$lib/components/layout/dashboard-layout.svelte";
|
import { AdminLayout } from "$lib/components/layout";
|
||||||
import { Button } from "$lib/components/ui/button/index.js";
|
import { Button } from "$lib/components/ui/button/index.js";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -74,7 +74,7 @@ function handleSlugInput(event: Event) {
|
|||||||
<title>New Organization | Admin | Publisher Dashboard</title>
|
<title>New Organization | Admin | Publisher Dashboard</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<DashboardLayout title="New Organization">
|
<AdminLayout title="New Organization">
|
||||||
<div class="mx-auto max-w-2xl space-y-6">
|
<div class="mx-auto max-w-2xl space-y-6">
|
||||||
<!-- Back link -->
|
<!-- Back link -->
|
||||||
<a
|
<a
|
||||||
@@ -157,4 +157,4 @@ function handleSlugInput(event: Event) {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</DashboardLayout>
|
</AdminLayout>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { AlertCircle, Check, Eye, Users, X } from "@lucide/svelte";
|
|||||||
import { createQuery } from "@tanstack/svelte-query";
|
import { createQuery } from "@tanstack/svelte-query";
|
||||||
import { api } from "$lib/api/client.js";
|
import { api } from "$lib/api/client.js";
|
||||||
import { SuperuserBadge } from "$lib/components/admin/index.js";
|
import { SuperuserBadge } from "$lib/components/admin/index.js";
|
||||||
import DashboardLayout from "$lib/components/layout/dashboard-layout.svelte";
|
import { AdminLayout } from "$lib/components/layout";
|
||||||
import { Button } from "$lib/components/ui/button/index.js";
|
import { Button } from "$lib/components/ui/button/index.js";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -37,7 +37,7 @@ const usersQuery = createQuery(() => ({
|
|||||||
<title>Users | Admin | Publisher Dashboard</title>
|
<title>Users | Admin | Publisher Dashboard</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<DashboardLayout title="Users">
|
<AdminLayout title="Users">
|
||||||
{#if usersQuery.isPending}
|
{#if usersQuery.isPending}
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<Card>
|
<Card>
|
||||||
@@ -141,4 +141,4 @@ const usersQuery = createQuery(() => ({
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</DashboardLayout>
|
</AdminLayout>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { toast } from "svelte-sonner";
|
|||||||
import { page } from "$app/state";
|
import { page } from "$app/state";
|
||||||
import { api } from "$lib/api/client.js";
|
import { api } from "$lib/api/client.js";
|
||||||
import { SuperuserBadge } from "$lib/components/admin/index.js";
|
import { SuperuserBadge } from "$lib/components/admin/index.js";
|
||||||
import DashboardLayout from "$lib/components/layout/dashboard-layout.svelte";
|
import { AdminLayout } from "$lib/components/layout";
|
||||||
import { Alert, AlertDescription } from "$lib/components/ui/alert/index.js";
|
import { Alert, AlertDescription } from "$lib/components/ui/alert/index.js";
|
||||||
import { Button } from "$lib/components/ui/button/index.js";
|
import { Button } from "$lib/components/ui/button/index.js";
|
||||||
import {
|
import {
|
||||||
@@ -147,7 +147,7 @@ async function handleConfirmEmail() {
|
|||||||
<title>{userDetailsQuery.data?.displayName ?? email} | Users | Admin</title>
|
<title>{userDetailsQuery.data?.displayName ?? email} | Users | Admin</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<DashboardLayout title="User Details">
|
<AdminLayout title="User Details">
|
||||||
<!-- Back navigation -->
|
<!-- Back navigation -->
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<Button variant="ghost" size="sm" href="/admin/users" class="gap-1">
|
<Button variant="ghost" size="sm" href="/admin/users" class="gap-1">
|
||||||
@@ -345,4 +345,4 @@ async function handleConfirmEmail() {
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</DashboardLayout>
|
</AdminLayout>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
import { createQuery } from "@tanstack/svelte-query";
|
import { createQuery } from "@tanstack/svelte-query";
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import { api } from "$lib/api/client";
|
import { api } from "$lib/api/client";
|
||||||
import DashboardLayout from "$lib/components/layout/dashboard-layout.svelte";
|
import { DashboardLayout } from "$lib/components/layout";
|
||||||
import { Badge } from "$lib/components/ui/badge";
|
import { Badge } from "$lib/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
import { createQuery } from "@tanstack/svelte-query";
|
import { createQuery } from "@tanstack/svelte-query";
|
||||||
import { getContext } from "svelte";
|
import { getContext } from "svelte";
|
||||||
import { api } from "$lib/api/client";
|
import { api } from "$lib/api/client";
|
||||||
import DashboardLayout from "$lib/components/layout/dashboard-layout.svelte";
|
import { DashboardLayout } from "$lib/components/layout";
|
||||||
import { RoleBadge } from "$lib/components/org";
|
import { RoleBadge } from "$lib/components/org";
|
||||||
import { Button } from "$lib/components/ui/button";
|
import { Button } from "$lib/components/ui/button";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { createQuery, useQueryClient } from "@tanstack/svelte-query";
|
|||||||
import { getContext } from "svelte";
|
import { getContext } from "svelte";
|
||||||
import { toast } from "svelte-sonner";
|
import { toast } from "svelte-sonner";
|
||||||
import { api } from "$lib/api/client";
|
import { api } from "$lib/api/client";
|
||||||
import DashboardLayout from "$lib/components/layout/dashboard-layout.svelte";
|
import { DashboardLayout } from "$lib/components/layout";
|
||||||
import { ConfirmDialog, RoleBadge } from "$lib/components/org";
|
import { ConfirmDialog, RoleBadge } from "$lib/components/org";
|
||||||
import { Button } from "$lib/components/ui/button";
|
import { Button } from "$lib/components/ui/button";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import FrequentFilters from "$lib/components/dashboard/frequent-filters.svelte";
|
|||||||
import MetricCard from "$lib/components/dashboard/metric-card.svelte";
|
import MetricCard from "$lib/components/dashboard/metric-card.svelte";
|
||||||
import PeakTrafficChart from "$lib/components/dashboard/peak-traffic-chart.svelte";
|
import PeakTrafficChart from "$lib/components/dashboard/peak-traffic-chart.svelte";
|
||||||
import PerformanceTable from "$lib/components/dashboard/performance-table.svelte";
|
import PerformanceTable from "$lib/components/dashboard/performance-table.svelte";
|
||||||
import DashboardLayout from "$lib/components/layout/dashboard-layout.svelte";
|
import { DashboardLayout } from "$lib/components/layout";
|
||||||
|
|
||||||
// Get org context (for future filtering by org)
|
// Get org context (for future filtering by org)
|
||||||
const orgContext = getContext<{ slug: string }>("orgContext");
|
const orgContext = getContext<{ slug: string }>("orgContext");
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import DashboardLayout from "$lib/components/layout/dashboard-layout.svelte";
|
import { DashboardLayout } from "$lib/components/layout";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { getContext } from "svelte";
|
|||||||
import { toast } from "svelte-sonner";
|
import { toast } from "svelte-sonner";
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import { api } from "$lib/api/client";
|
import { api } from "$lib/api/client";
|
||||||
import DashboardLayout from "$lib/components/layout/dashboard-layout.svelte";
|
import { DashboardLayout } from "$lib/components/layout";
|
||||||
import { ConfirmDialog } from "$lib/components/org";
|
import { ConfirmDialog } from "$lib/components/org";
|
||||||
import { Alert, AlertDescription } from "$lib/components/ui/alert";
|
import { Alert, AlertDescription } from "$lib/components/ui/alert";
|
||||||
import { Button } from "$lib/components/ui/button";
|
import { Button } from "$lib/components/ui/button";
|
||||||
|
|||||||
Reference in New Issue
Block a user