|
1 | 1 | <script lang="ts"> |
2 | | - import "../../app.css"; |
3 | | - import { |
4 | | - BarChart3, |
5 | | - DatabaseBackup, |
6 | | - FileText, |
7 | | - Folder, |
8 | | - LayoutGrid, |
9 | | - LogOut, |
10 | | - Menu, |
11 | | - Search, |
12 | | - Settings, |
13 | | - Sparkles, |
14 | | - Users, |
15 | | - } from "lucide-svelte"; |
16 | | - import { page } from "$app/state"; |
17 | | - import logo from "$lib/assets/logo.svg"; |
18 | | - import AdminSearchModal, { openSearch } from "$lib/components/admin-search-modal.svelte"; |
19 | | - import ConfirmModal from "$lib/components/confirm-modal.svelte"; |
20 | | - import { setupToast } from "$lib/components/toast/controls.svelte"; |
21 | | - import Toast from "$lib/components/toast/toast.svelte"; |
| 2 | + import "../../app.css"; |
| 3 | + import { |
| 4 | + BarChart3, |
| 5 | + DatabaseBackup, |
| 6 | + FileText, |
| 7 | + Folder, |
| 8 | + LayoutGrid, |
| 9 | + LogOut, |
| 10 | + Menu, |
| 11 | + Search, |
| 12 | + Sparkles, |
| 13 | + Users, |
| 14 | + } from "lucide-svelte"; |
| 15 | + import { page } from "$app/state"; |
| 16 | + import logo from "$lib/assets/logo.svg"; |
| 17 | + import AdminSearchModal, { openSearch } from "$lib/components/admin-search-modal.svelte"; |
| 18 | + import ConfirmModal from "$lib/components/confirm-modal.svelte"; |
| 19 | + import { setupToast } from "$lib/components/toast/controls.svelte"; |
| 20 | + import Toast from "$lib/components/toast/toast.svelte"; |
22 | 21 |
|
23 | | - let { children, data } = $props(); |
| 22 | + let { children, data } = $props(); |
24 | 23 |
|
25 | | - // Instantiate toast context for all admin pages (must be called before any await) |
26 | | - const toastControls = setupToast(); |
| 24 | + // Instantiate toast context for all admin pages (must be called before any await) |
| 25 | + const toastControls = setupToast(); |
27 | 26 |
|
28 | | - const session = $derived(data.session); |
| 27 | + const session = $derived(data.session); |
29 | 28 |
|
30 | | - const navItems = [ |
31 | | - { href: "/admin", label: "Dashboard", icon: LayoutGrid, exact: true }, |
32 | | - { href: "/admin/members", label: "Members", icon: Users, exact: false }, |
33 | | - { href: "/admin/articles", label: "Articles", icon: FileText, exact: false }, |
34 | | - { href: "/admin/projects", label: "Projects", icon: Folder, exact: false }, |
35 | | - { href: "/admin/analytics", label: "Analytics", icon: BarChart3, exact: false }, |
36 | | - { href: "/admin/settings", label: "Settings", icon: Settings, exact: false }, |
37 | | - { href: "/admin/migrate", label: "Migration", icon: DatabaseBackup, exact: false }, |
38 | | - ] as const; |
| 29 | + const navItems = [ |
| 30 | + { href: "/admin", label: "Dashboard", icon: LayoutGrid, exact: true }, |
| 31 | + { href: "/admin/members", label: "Members", icon: Users, exact: false }, |
| 32 | + { href: "/admin/articles", label: "Articles", icon: FileText, exact: false }, |
| 33 | + { href: "/admin/projects", label: "Projects", icon: Folder, exact: false }, |
| 34 | + { href: "/admin/analytics", label: "Analytics", icon: BarChart3, exact: false }, |
| 35 | + { href: "/admin/migrate", label: "Migration", icon: DatabaseBackup, exact: false }, |
| 36 | + ] as const; |
39 | 37 |
|
40 | | - function isActive(path: string, exact: boolean) { |
41 | | - if (exact) return page.url.pathname === path; |
42 | | - return page.url.pathname.startsWith(path); |
43 | | - } |
| 38 | + function isActive(path: string, exact: boolean) { |
| 39 | + if (exact) return page.url.pathname === path; |
| 40 | + return page.url.pathname.startsWith(path); |
| 41 | + } |
44 | 42 | </script> |
45 | 43 |
|
46 | 44 | <div class="drawer lg:drawer-open"> |
47 | | - <input id="admin-drawer" type="checkbox" class="drawer-toggle" /> |
| 45 | + <input id="admin-drawer" type="checkbox" class="drawer-toggle" /> |
48 | 46 |
|
49 | | - <!-- Main content --> |
50 | | - <div class="gradient-mesh relative drawer-content"> |
51 | | - <!-- Mobile header --> |
52 | | - <header class="sticky top-0 z-30 flex h-14 items-center gap-4 glass px-4 lg:hidden"> |
53 | | - <label for="admin-drawer" class="btn btn-square btn-ghost btn-sm"> |
54 | | - <Menu class="h-5 w-5" /> |
55 | | - </label> |
56 | | - <a href="/" class="flex flex-1 items-center transition-opacity hover:opacity-70"> |
57 | | - <img src={logo} alt="ut.code();" class="h-7" /> |
58 | | - </a> |
59 | | - <button type="button" onclick={openSearch} class="btn btn-square btn-ghost btn-sm"> |
60 | | - <Search class="h-5 w-5" /> |
61 | | - </button> |
62 | | - </header> |
| 47 | + <!-- Main content --> |
| 48 | + <div class="gradient-mesh relative drawer-content"> |
| 49 | + <!-- Mobile header --> |
| 50 | + <header class="sticky top-0 z-30 flex h-14 items-center gap-4 glass px-4 lg:hidden"> |
| 51 | + <label for="admin-drawer" class="btn btn-square btn-ghost btn-sm"> |
| 52 | + <Menu class="h-5 w-5" /> |
| 53 | + </label> |
| 54 | + <a href="/" class="flex flex-1 items-center transition-opacity hover:opacity-70"> |
| 55 | + <img src={logo} alt="ut.code();" class="h-7" /> |
| 56 | + </a> |
| 57 | + <button type="button" onclick={openSearch} class="btn btn-square btn-ghost btn-sm"> |
| 58 | + <Search class="h-5 w-5" /> |
| 59 | + </button> |
| 60 | + </header> |
63 | 61 |
|
64 | | - <main class="min-h-screen"> |
65 | | - <div class="mx-auto max-w-5xl px-4 py-4 sm:px-6 sm:py-6 lg:py-8"> |
66 | | - {@render children()} |
67 | | - </div> |
68 | | - </main> |
69 | | - </div> |
| 62 | + <main class="min-h-screen"> |
| 63 | + <div class="mx-auto max-w-5xl px-4 py-4 sm:px-6 sm:py-6 lg:py-8"> |
| 64 | + {@render children()} |
| 65 | + </div> |
| 66 | + </main> |
| 67 | + </div> |
70 | 68 |
|
71 | | - <!-- Sidebar --> |
72 | | - <aside class="drawer-side z-40"> |
73 | | - <label for="admin-drawer" aria-label="close sidebar" class="drawer-overlay"></label> |
74 | | - <nav class="gradient-dark relative flex h-full min-h-screen w-64 flex-col overflow-hidden"> |
75 | | - <!-- Decorative gradient orbs --> |
76 | | - <div class="absolute -top-20 -right-20 h-40 w-40 rounded-full bg-primary/20 blur-3xl"></div> |
77 | | - <div class="absolute top-1/3 -left-10 h-32 w-32 rounded-full bg-info/10 blur-3xl"></div> |
| 69 | + <!-- Sidebar --> |
| 70 | + <aside class="drawer-side z-40"> |
| 71 | + <label for="admin-drawer" aria-label="close sidebar" class="drawer-overlay"></label> |
| 72 | + <nav class="gradient-dark relative flex h-full min-h-screen w-64 flex-col overflow-hidden"> |
| 73 | + <!-- Decorative gradient orbs --> |
| 74 | + <div class="absolute -top-20 -right-20 h-40 w-40 rounded-full bg-primary/20 blur-3xl"></div> |
| 75 | + <div class="absolute top-1/3 -left-10 h-32 w-32 rounded-full bg-info/10 blur-3xl"></div> |
78 | 76 |
|
79 | | - <!-- Logo --> |
80 | | - <div class="relative flex h-16 shrink-0 items-center gap-3 px-5"> |
81 | | - <a href="/" class="flex items-center gap-2 transition-all hover:opacity-80"> |
82 | | - <img src={logo} alt="ut.code();" class="h-8" /> |
83 | | - </a> |
84 | | - <span class="gradient-primary badge badge-xs text-[10px] font-bold text-white"> CMS </span> |
85 | | - </div> |
| 77 | + <!-- Logo --> |
| 78 | + <div class="relative flex h-16 shrink-0 items-center gap-3 px-5"> |
| 79 | + <a href="/" class="flex items-center gap-2 transition-all hover:opacity-80"> |
| 80 | + <img src={logo} alt="ut.code();" class="h-8" /> |
| 81 | + </a> |
| 82 | + <span class="gradient-primary badge badge-xs text-[10px] font-bold text-white"> CMS </span> |
| 83 | + </div> |
86 | 84 |
|
87 | | - <!-- Search --> |
88 | | - <div class="relative px-4 pt-2"> |
89 | | - <button |
90 | | - type="button" |
91 | | - onclick={openSearch} |
92 | | - class="flex w-full items-center gap-3 rounded-xl bg-white/5 px-4 py-2.5 text-left text-sm text-white/50 transition-all hover:bg-white/10 hover:text-white/70" |
93 | | - > |
94 | | - <Search class="h-4 w-4" /> |
95 | | - <span class="flex-1">Search...</span> |
96 | | - <kbd |
97 | | - class="hidden rounded bg-white/10 px-1.5 py-0.5 font-mono text-[10px] text-white/40 lg:inline" |
98 | | - > |
99 | | - ⌘K |
100 | | - </kbd> |
101 | | - </button> |
102 | | - </div> |
| 85 | + <!-- Search --> |
| 86 | + <div class="relative px-4 pt-2"> |
| 87 | + <button |
| 88 | + type="button" |
| 89 | + onclick={openSearch} |
| 90 | + class="flex w-full items-center gap-3 rounded-xl bg-white/5 px-4 py-2.5 text-left text-sm text-white/50 transition-all hover:bg-white/10 hover:text-white/70" |
| 91 | + > |
| 92 | + <Search class="h-4 w-4" /> |
| 93 | + <span class="flex-1">Search...</span> |
| 94 | + <kbd |
| 95 | + class="hidden rounded bg-white/10 px-1.5 py-0.5 font-mono text-[10px] text-white/40 lg:inline" |
| 96 | + > |
| 97 | + ⌘K |
| 98 | + </kbd> |
| 99 | + </button> |
| 100 | + </div> |
103 | 101 |
|
104 | | - <!-- Navigation --> |
105 | | - <div class="relative flex-1 overflow-y-auto px-3 py-6"> |
106 | | - <div class="mb-3 px-3"> |
107 | | - <span class="font-mono text-[10px] font-semibold tracking-widest text-white/30 uppercase"> |
108 | | - Menu |
109 | | - </span> |
110 | | - </div> |
111 | | - <ul class="space-y-1"> |
112 | | - {#each navItems as item (item.href)} |
113 | | - {@const active = isActive(item.href, item.exact)} |
114 | | - <li> |
115 | | - <a |
116 | | - href={item.href} |
117 | | - class="group flex items-center gap-3 rounded-xl px-4 py-3 font-medium transition-all duration-200 |
| 102 | + <!-- Navigation --> |
| 103 | + <div class="relative flex-1 overflow-y-auto px-3 py-6"> |
| 104 | + <div class="mb-3 px-3"> |
| 105 | + <span class="font-mono text-[10px] font-semibold tracking-widest text-white/30 uppercase"> |
| 106 | + Menu |
| 107 | + </span> |
| 108 | + </div> |
| 109 | + <ul class="space-y-1"> |
| 110 | + {#each navItems as item (item.href)} |
| 111 | + {@const active = isActive(item.href, item.exact)} |
| 112 | + <li> |
| 113 | + <a |
| 114 | + href={item.href} |
| 115 | + class="group flex items-center gap-3 rounded-xl px-4 py-3 font-medium transition-all duration-200 |
118 | 116 | {active |
119 | | - ? 'nav-active bg-white/10 text-white' |
120 | | - : 'text-white/60 hover:bg-white/5 hover:text-white/90'}" |
121 | | - > |
122 | | - <div |
123 | | - class="flex h-8 w-8 items-center justify-center rounded-lg transition-all duration-200 |
| 117 | + ? 'nav-active bg-white/10 text-white' |
| 118 | + : 'text-white/60 hover:bg-white/5 hover:text-white/90'}" |
| 119 | + > |
| 120 | + <div |
| 121 | + class="flex h-8 w-8 items-center justify-center rounded-lg transition-all duration-200 |
124 | 122 | {active |
125 | | - ? 'gradient-primary glow-primary' |
126 | | - : 'bg-white/5 group-hover:bg-white/10'}" |
127 | | - > |
128 | | - <item.icon class="h-4 w-4 {active ? 'text-white' : 'text-white/70'}" /> |
129 | | - </div> |
130 | | - {item.label} |
131 | | - {#if active} |
132 | | - <Sparkles class="ml-auto h-3 w-3 animate-pulse text-primary" /> |
133 | | - {/if} |
134 | | - </a> |
135 | | - </li> |
136 | | - {/each} |
137 | | - </ul> |
138 | | - </div> |
| 123 | + ? 'gradient-primary glow-primary' |
| 124 | + : 'bg-white/5 group-hover:bg-white/10'}" |
| 125 | + > |
| 126 | + <item.icon class="h-4 w-4 {active ? 'text-white' : 'text-white/70'}" /> |
| 127 | + </div> |
| 128 | + {item.label} |
| 129 | + {#if active} |
| 130 | + <Sparkles class="ml-auto h-3 w-3 animate-pulse text-primary" /> |
| 131 | + {/if} |
| 132 | + </a> |
| 133 | + </li> |
| 134 | + {/each} |
| 135 | + </ul> |
| 136 | + </div> |
139 | 137 |
|
140 | | - <!-- User section --> |
141 | | - <div class="relative shrink-0 border-t border-white/10 p-4"> |
142 | | - <div class="flex items-center gap-3 rounded-xl bg-white/5 p-3 transition-colors duration-150"> |
143 | | - <a |
144 | | - href="/admin/settings" |
145 | | - class="group flex min-w-0 flex-1 items-center gap-3 transition-opacity hover:opacity-80" |
146 | | - title="Personal Settings" |
147 | | - > |
148 | | - {#if session.user.image} |
149 | | - <div class="avatar"> |
150 | | - <div |
151 | | - class="aspect-square w-10 rounded-full ring-2 ring-primary/50 ring-offset-2 ring-offset-neutral" |
152 | | - > |
153 | | - <img src={session.user.image} alt={session.user.name ?? "User"} class="h-full w-full object-cover" /> |
154 | | - </div> |
155 | | - </div> |
156 | | - {:else} |
157 | | - <div class="placeholder avatar"> |
158 | | - <div class="gradient-primary aspect-square w-10 rounded-full text-white"> |
159 | | - <span class="text-sm font-bold">{session.user.name?.charAt(0) ?? "?"}</span> |
160 | | - </div> |
161 | | - </div> |
162 | | - {/if} |
163 | | - <div class="min-w-0 flex-1"> |
164 | | - <p class="truncate text-sm font-semibold text-white">{session.user.name}</p> |
165 | | - <p class="truncate text-xs text-white/40">{session.user.email}</p> |
166 | | - </div> |
167 | | - </a> |
168 | | - <a |
169 | | - href="/api/auth/sign-out" |
170 | | - class="btn btn-circle text-white/40 btn-ghost btn-sm hover:bg-white/10 hover:text-white" |
171 | | - title="Sign out" |
172 | | - > |
173 | | - <LogOut class="h-4 w-4" /> |
174 | | - </a> |
175 | | - </div> |
176 | | - </div> |
177 | | - </nav> |
178 | | - </aside> |
| 138 | + <!-- User section --> |
| 139 | + <div class="relative shrink-0 border-t border-white/10 p-4"> |
| 140 | + <div |
| 141 | + class="flex items-center gap-3 rounded-xl bg-white/5 p-3 transition-colors duration-150" |
| 142 | + > |
| 143 | + <a |
| 144 | + href="/admin/settings" |
| 145 | + class="group flex min-w-0 flex-1 items-center gap-3 transition-opacity hover:opacity-80" |
| 146 | + title="Personal Settings" |
| 147 | + > |
| 148 | + {#if session.user.image} |
| 149 | + <div class="avatar"> |
| 150 | + <div |
| 151 | + class="aspect-square w-10 rounded-full ring-2 ring-primary/50 ring-offset-2 ring-offset-neutral" |
| 152 | + > |
| 153 | + <img |
| 154 | + src={session.user.image} |
| 155 | + alt={session.user.name ?? "User"} |
| 156 | + class="h-full w-full object-cover" |
| 157 | + /> |
| 158 | + </div> |
| 159 | + </div> |
| 160 | + {:else} |
| 161 | + <div class="placeholder avatar"> |
| 162 | + <div class="gradient-primary aspect-square w-10 rounded-full text-white"> |
| 163 | + <span class="text-sm font-bold">{session.user.name?.charAt(0) ?? "?"}</span> |
| 164 | + </div> |
| 165 | + </div> |
| 166 | + {/if} |
| 167 | + <div class="min-w-0 flex-1"> |
| 168 | + <p class="truncate text-sm font-semibold text-white">{session.user.name}</p> |
| 169 | + <p class="truncate text-xs text-white/40">{session.user.email}</p> |
| 170 | + </div> |
| 171 | + </a> |
| 172 | + <a |
| 173 | + href="/api/auth/sign-out" |
| 174 | + class="btn btn-circle text-white/40 btn-ghost btn-sm hover:bg-white/10 hover:text-white" |
| 175 | + title="Sign out" |
| 176 | + > |
| 177 | + <LogOut class="h-4 w-4" /> |
| 178 | + </a> |
| 179 | + </div> |
| 180 | + </div> |
| 181 | + </nav> |
| 182 | + </aside> |
179 | 183 | </div> |
180 | 184 |
|
181 | 185 | <ConfirmModal /> |
|
0 commit comments