diff --git a/.claude/settings.local.json b/.claude/settings.local.json index aacf7b5..3c19668 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -7,7 +7,12 @@ "Bash(go run:*)", "Bash(go build:*)", "Bash(go doc:*)", - "Bash(go test:*)" + "Bash(go test:*)", + "WebFetch(domain:lucia-auth.com)", + "WebFetch(domain:v3.lucia-auth.com)", + "Bash(bun install:*)", + "Bash(bunx svelte-kit sync:*)", + "Bash(bun run check:*)" ] } } diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 113e845..2e3c66b 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -700,6 +700,12 @@ A web dashboard (`dashboard/` directory) for browsing, triaging, and downloading - Individual file download and bulk ZIP download (all files + report metadata) - Report deletion with confirmation dialog - Dark mode UI matching EMLy's aesthetic +- **Authentication**: Session-based auth with Lucia v3 + Drizzle ORM adapter + - Default admin account: username `admin`, password `admin` (seeded on first migration) + - Password hashing with argon2 via `@node-rs/argon2` + - Session cookies with automatic refresh + - Role-based access: `admin` and `user` roles +- **User Management**: Admin-only `/users` page for creating/deleting dashboard users - **Development**: `cd dashboard && bun install && bun dev` (localhost:3001) #### Configuration (config.ini) diff --git a/config.ini b/config.ini index 839e7a9..2d8dec6 100644 --- a/config.ini +++ b/config.ini @@ -1,11 +1,11 @@ [EMLy] -SDK_DECODER_SEMVER = 1.3.2 -SDK_DECODER_RELEASE_CHANNEL = stable -GUI_SEMVER = 1.5.4 +SDK_DECODER_SEMVER = 1.4.1 +SDK_DECODER_RELEASE_CHANNEL = beta +GUI_SEMVER = 1.5.5 GUI_RELEASE_CHANNEL = beta LANGUAGE = it UPDATE_CHECK_ENABLED = false UPDATE_PATH = -UPDATE_AUTO_CHECK = true +UPDATE_AUTO_CHECK = false BUGREPORT_API_URL = "https://api.whiskr.it" BUGREPORT_API_KEY = "emly_1BaQdBknsMGcY5DynSby71JnWOKXtJvnuUprkgWT0pujpLFxj5HaTXP9vtJAMk63" \ No newline at end of file diff --git a/server/dashboard/components.json b/server/dashboard/components.json new file mode 100644 index 0000000..e44a4eb --- /dev/null +++ b/server/dashboard/components.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://shadcn-svelte.com/schema.json", + "tailwind": { + "css": "src\\app.css", + "baseColor": "neutral" + }, + "aliases": { + "components": "$lib/components", + "utils": "$lib/utils", + "ui": "$lib/components/ui", + "hooks": "$lib/hooks", + "lib": "$lib" + }, + "typescript": true, + "registry": "https://shadcn-svelte.com/registry" +} diff --git a/server/dashboard/package.json b/server/dashboard/package.json index 1b67641..eafdd10 100644 --- a/server/dashboard/package.json +++ b/server/dashboard/package.json @@ -9,24 +9,31 @@ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json" }, "devDependencies": { + "@internationalized/date": "^3.10.0", + "@lucide/svelte": "^0.561.0", "@sveltejs/adapter-node": "^5.5.3", "@sveltejs/kit": "^2.51.0", "@sveltejs/vite-plugin-svelte": "^5.1.1", "@tailwindcss/vite": "^4.1.18", "@types/node": "^25.2.3", + "drizzle-kit": "^0.31.9", "svelte": "^5.51.1", "svelte-check": "^4.4.0", "tailwindcss": "^4.1.18", + "tw-animate-css": "^1.4.0", "typescript": "^5.9.3", "vite": "^6.4.1" }, "dependencies": { + "@lucia-auth/adapter-drizzle": "^1.1.0", + "@node-rs/argon2": "^2.0.2", "drizzle-orm": "^0.38.4", + "lucia": "^3.2.2", "mysql2": "^3.17.1", - "bits-ui": "^1.8.0", + "bits-ui": "^2.14.4", "clsx": "^2.1.1", "tailwind-merge": "^3.4.0", - "tailwind-variants": "^0.3.1", + "tailwind-variants": "^3.2.2", "jszip": "^3.10.1", "lucide-svelte": "^0.469.0" }, diff --git a/server/dashboard/src/app.css b/server/dashboard/src/app.css index 2ac6589..ac256d3 100644 --- a/server/dashboard/src/app.css +++ b/server/dashboard/src/app.css @@ -1,42 +1,121 @@ @import "tailwindcss"; +@import "tw-animate-css"; + @custom-variant dark (&:is(.dark *)); -@theme { - --color-background: hsl(222.2 84% 4.9%); - --color-foreground: hsl(210 40% 98%); - --color-card: hsl(222.2 84% 4.9%); - --color-card-foreground: hsl(210 40% 98%); - --color-popover: hsl(222.2 84% 4.9%); - --color-popover-foreground: hsl(210 40% 98%); - --color-primary: hsl(217.2 91.2% 59.8%); - --color-primary-foreground: hsl(222.2 47.4% 11.2%); - --color-secondary: hsl(217.2 32.6% 17.5%); - --color-secondary-foreground: hsl(210 40% 98%); - --color-muted: hsl(217.2 32.6% 17.5%); - --color-muted-foreground: hsl(215 20.2% 65.1%); - --color-accent: hsl(217.2 32.6% 17.5%); - --color-accent-foreground: hsl(210 40% 98%); - --color-destructive: hsl(0 62.8% 30.6%); - --color-destructive-foreground: hsl(210 40% 98%); - --color-border: hsl(217.2 32.6% 17.5%); - --color-input: hsl(217.2 32.6% 17.5%); - --color-ring: hsl(224.3 76.3% 48%); - --radius: 0.5rem; - - --color-sidebar: hsl(222.2 84% 3.5%); - --color-sidebar-foreground: hsl(210 40% 98%); - --color-sidebar-border: hsl(217.2 32.6% 12%); - - --font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); } -* { - border-color: var(--color-border); +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); } -body { - background-color: var(--color-background); - color: var(--color-foreground); - font-family: var(--font-sans); +@theme inline { + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); } + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} \ No newline at end of file diff --git a/server/dashboard/src/app.d.ts b/server/dashboard/src/app.d.ts new file mode 100644 index 0000000..c0087e0 --- /dev/null +++ b/server/dashboard/src/app.d.ts @@ -0,0 +1,10 @@ +declare global { + namespace App { + interface Locals { + user: import('lucia').User | null; + session: import('lucia').Session | null; + } + } +} + +export {}; diff --git a/server/dashboard/src/hooks.server.ts b/server/dashboard/src/hooks.server.ts new file mode 100644 index 0000000..474ba4a --- /dev/null +++ b/server/dashboard/src/hooks.server.ts @@ -0,0 +1,34 @@ +import type { Handle } from '@sveltejs/kit'; +import { lucia } from '$lib/server/auth'; + +export const handle: Handle = async ({ event, resolve }) => { + const sessionId = event.cookies.get(lucia.sessionCookieName); + + if (!sessionId) { + event.locals.user = null; + event.locals.session = null; + return resolve(event); + } + + const { session, user } = await lucia.validateSession(sessionId); + + if (session && session.fresh) { + const sessionCookie = lucia.createSessionCookie(session.id); + event.cookies.set(sessionCookie.name, sessionCookie.value, { + path: '.', + ...sessionCookie.attributes + }); + } + + if (!session) { + const sessionCookie = lucia.createBlankSessionCookie(); + event.cookies.set(sessionCookie.name, sessionCookie.value, { + path: '.', + ...sessionCookie.attributes + }); + } + + event.locals.user = user; + event.locals.session = session; + return resolve(event); +}; diff --git a/server/dashboard/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte b/server/dashboard/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte new file mode 100644 index 0000000..a005691 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte @@ -0,0 +1,18 @@ + + + diff --git a/server/dashboard/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte b/server/dashboard/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte new file mode 100644 index 0000000..a7b0cf7 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte @@ -0,0 +1,18 @@ + + + diff --git a/server/dashboard/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte b/server/dashboard/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte new file mode 100644 index 0000000..236bcad --- /dev/null +++ b/server/dashboard/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte @@ -0,0 +1,29 @@ + + + + + + diff --git a/server/dashboard/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte b/server/dashboard/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte new file mode 100644 index 0000000..2ec67dc --- /dev/null +++ b/server/dashboard/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte @@ -0,0 +1,17 @@ + + + diff --git a/server/dashboard/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte b/server/dashboard/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte new file mode 100644 index 0000000..f78b97a --- /dev/null +++ b/server/dashboard/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/server/dashboard/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte b/server/dashboard/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte new file mode 100644 index 0000000..1835d91 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/server/dashboard/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte b/server/dashboard/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte new file mode 100644 index 0000000..a64ee76 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte @@ -0,0 +1,20 @@ + + + diff --git a/server/dashboard/src/lib/components/ui/alert-dialog/alert-dialog-portal.svelte b/server/dashboard/src/lib/components/ui/alert-dialog/alert-dialog-portal.svelte new file mode 100644 index 0000000..f0a19a8 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/alert-dialog/alert-dialog-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/server/dashboard/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte b/server/dashboard/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte new file mode 100644 index 0000000..7ef2b5f --- /dev/null +++ b/server/dashboard/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte @@ -0,0 +1,17 @@ + + + diff --git a/server/dashboard/src/lib/components/ui/alert-dialog/alert-dialog-trigger.svelte b/server/dashboard/src/lib/components/ui/alert-dialog/alert-dialog-trigger.svelte new file mode 100644 index 0000000..b22d1d5 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/alert-dialog/alert-dialog-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/server/dashboard/src/lib/components/ui/alert-dialog/alert-dialog.svelte b/server/dashboard/src/lib/components/ui/alert-dialog/alert-dialog.svelte new file mode 100644 index 0000000..7ea78bb --- /dev/null +++ b/server/dashboard/src/lib/components/ui/alert-dialog/alert-dialog.svelte @@ -0,0 +1,7 @@ + + + diff --git a/server/dashboard/src/lib/components/ui/alert-dialog/index.ts b/server/dashboard/src/lib/components/ui/alert-dialog/index.ts new file mode 100644 index 0000000..269538e --- /dev/null +++ b/server/dashboard/src/lib/components/ui/alert-dialog/index.ts @@ -0,0 +1,37 @@ +import Root from "./alert-dialog.svelte"; +import Portal from "./alert-dialog-portal.svelte"; +import Trigger from "./alert-dialog-trigger.svelte"; +import Title from "./alert-dialog-title.svelte"; +import Action from "./alert-dialog-action.svelte"; +import Cancel from "./alert-dialog-cancel.svelte"; +import Footer from "./alert-dialog-footer.svelte"; +import Header from "./alert-dialog-header.svelte"; +import Overlay from "./alert-dialog-overlay.svelte"; +import Content from "./alert-dialog-content.svelte"; +import Description from "./alert-dialog-description.svelte"; + +export { + Root, + Title, + Action, + Cancel, + Portal, + Footer, + Header, + Trigger, + Overlay, + Content, + Description, + // + Root as AlertDialog, + Title as AlertDialogTitle, + Action as AlertDialogAction, + Cancel as AlertDialogCancel, + Portal as AlertDialogPortal, + Footer as AlertDialogFooter, + Header as AlertDialogHeader, + Trigger as AlertDialogTrigger, + Overlay as AlertDialogOverlay, + Content as AlertDialogContent, + Description as AlertDialogDescription, +}; diff --git a/server/dashboard/src/lib/components/ui/button/button.svelte b/server/dashboard/src/lib/components/ui/button/button.svelte new file mode 100644 index 0000000..a8296ae --- /dev/null +++ b/server/dashboard/src/lib/components/ui/button/button.svelte @@ -0,0 +1,82 @@ + + + + +{#if href} + + {@render children?.()} + +{:else} + +{/if} diff --git a/server/dashboard/src/lib/components/ui/button/index.ts b/server/dashboard/src/lib/components/ui/button/index.ts new file mode 100644 index 0000000..fb585d7 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/button/index.ts @@ -0,0 +1,17 @@ +import Root, { + type ButtonProps, + type ButtonSize, + type ButtonVariant, + buttonVariants, +} from "./button.svelte"; + +export { + Root, + type ButtonProps as Props, + // + Root as Button, + buttonVariants, + type ButtonProps, + type ButtonSize, + type ButtonVariant, +}; diff --git a/server/dashboard/src/lib/components/ui/card/card-action.svelte b/server/dashboard/src/lib/components/ui/card/card-action.svelte new file mode 100644 index 0000000..cc36c56 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/card/card-action.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/server/dashboard/src/lib/components/ui/card/card-content.svelte b/server/dashboard/src/lib/components/ui/card/card-content.svelte new file mode 100644 index 0000000..bc90b83 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/card/card-content.svelte @@ -0,0 +1,15 @@ + + +
+ {@render children?.()} +
diff --git a/server/dashboard/src/lib/components/ui/card/card-description.svelte b/server/dashboard/src/lib/components/ui/card/card-description.svelte new file mode 100644 index 0000000..9b20ac7 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/card/card-description.svelte @@ -0,0 +1,20 @@ + + +

+ {@render children?.()} +

diff --git a/server/dashboard/src/lib/components/ui/card/card-footer.svelte b/server/dashboard/src/lib/components/ui/card/card-footer.svelte new file mode 100644 index 0000000..2d4d0f2 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/card/card-footer.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/server/dashboard/src/lib/components/ui/card/card-header.svelte b/server/dashboard/src/lib/components/ui/card/card-header.svelte new file mode 100644 index 0000000..2501788 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/card/card-header.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/server/dashboard/src/lib/components/ui/card/card-title.svelte b/server/dashboard/src/lib/components/ui/card/card-title.svelte new file mode 100644 index 0000000..7447231 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/card/card-title.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/server/dashboard/src/lib/components/ui/card/card.svelte b/server/dashboard/src/lib/components/ui/card/card.svelte new file mode 100644 index 0000000..99448cc --- /dev/null +++ b/server/dashboard/src/lib/components/ui/card/card.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/server/dashboard/src/lib/components/ui/card/index.ts b/server/dashboard/src/lib/components/ui/card/index.ts new file mode 100644 index 0000000..4d3fce4 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/card/index.ts @@ -0,0 +1,25 @@ +import Root from "./card.svelte"; +import Content from "./card-content.svelte"; +import Description from "./card-description.svelte"; +import Footer from "./card-footer.svelte"; +import Header from "./card-header.svelte"; +import Title from "./card-title.svelte"; +import Action from "./card-action.svelte"; + +export { + Root, + Content, + Description, + Footer, + Header, + Title, + Action, + // + Root as Card, + Content as CardContent, + Description as CardDescription, + Footer as CardFooter, + Header as CardHeader, + Title as CardTitle, + Action as CardAction, +}; diff --git a/server/dashboard/src/lib/components/ui/dialog/dialog-close.svelte b/server/dashboard/src/lib/components/ui/dialog/dialog-close.svelte new file mode 100644 index 0000000..840b2f6 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/dialog/dialog-close.svelte @@ -0,0 +1,7 @@ + + + diff --git a/server/dashboard/src/lib/components/ui/dialog/dialog-content.svelte b/server/dashboard/src/lib/components/ui/dialog/dialog-content.svelte new file mode 100644 index 0000000..5c6ee6d --- /dev/null +++ b/server/dashboard/src/lib/components/ui/dialog/dialog-content.svelte @@ -0,0 +1,45 @@ + + + + + + {@render children?.()} + {#if showCloseButton} + + + Close + + {/if} + + diff --git a/server/dashboard/src/lib/components/ui/dialog/dialog-description.svelte b/server/dashboard/src/lib/components/ui/dialog/dialog-description.svelte new file mode 100644 index 0000000..3845023 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/dialog/dialog-description.svelte @@ -0,0 +1,17 @@ + + + diff --git a/server/dashboard/src/lib/components/ui/dialog/dialog-footer.svelte b/server/dashboard/src/lib/components/ui/dialog/dialog-footer.svelte new file mode 100644 index 0000000..e7ff446 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/dialog/dialog-footer.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/server/dashboard/src/lib/components/ui/dialog/dialog-header.svelte b/server/dashboard/src/lib/components/ui/dialog/dialog-header.svelte new file mode 100644 index 0000000..4e5c447 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/dialog/dialog-header.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/server/dashboard/src/lib/components/ui/dialog/dialog-overlay.svelte b/server/dashboard/src/lib/components/ui/dialog/dialog-overlay.svelte new file mode 100644 index 0000000..f81ad83 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/dialog/dialog-overlay.svelte @@ -0,0 +1,20 @@ + + + diff --git a/server/dashboard/src/lib/components/ui/dialog/dialog-portal.svelte b/server/dashboard/src/lib/components/ui/dialog/dialog-portal.svelte new file mode 100644 index 0000000..ccfa79c --- /dev/null +++ b/server/dashboard/src/lib/components/ui/dialog/dialog-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/server/dashboard/src/lib/components/ui/dialog/dialog-title.svelte b/server/dashboard/src/lib/components/ui/dialog/dialog-title.svelte new file mode 100644 index 0000000..e4d4b34 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/dialog/dialog-title.svelte @@ -0,0 +1,17 @@ + + + diff --git a/server/dashboard/src/lib/components/ui/dialog/dialog-trigger.svelte b/server/dashboard/src/lib/components/ui/dialog/dialog-trigger.svelte new file mode 100644 index 0000000..9d1e801 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/dialog/dialog-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/server/dashboard/src/lib/components/ui/dialog/dialog.svelte b/server/dashboard/src/lib/components/ui/dialog/dialog.svelte new file mode 100644 index 0000000..211672c --- /dev/null +++ b/server/dashboard/src/lib/components/ui/dialog/dialog.svelte @@ -0,0 +1,7 @@ + + + diff --git a/server/dashboard/src/lib/components/ui/dialog/index.ts b/server/dashboard/src/lib/components/ui/dialog/index.ts new file mode 100644 index 0000000..076cef5 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/dialog/index.ts @@ -0,0 +1,34 @@ +import Root from "./dialog.svelte"; +import Portal from "./dialog-portal.svelte"; +import Title from "./dialog-title.svelte"; +import Footer from "./dialog-footer.svelte"; +import Header from "./dialog-header.svelte"; +import Overlay from "./dialog-overlay.svelte"; +import Content from "./dialog-content.svelte"; +import Description from "./dialog-description.svelte"; +import Trigger from "./dialog-trigger.svelte"; +import Close from "./dialog-close.svelte"; + +export { + Root, + Title, + Portal, + Footer, + Header, + Trigger, + Overlay, + Content, + Description, + Close, + // + Root as Dialog, + Title as DialogTitle, + Portal as DialogPortal, + Footer as DialogFooter, + Header as DialogHeader, + Trigger as DialogTrigger, + Overlay as DialogOverlay, + Content as DialogContent, + Description as DialogDescription, + Close as DialogClose, +}; diff --git a/server/dashboard/src/lib/components/ui/empty/empty-content.svelte b/server/dashboard/src/lib/components/ui/empty/empty-content.svelte new file mode 100644 index 0000000..f5a9c68 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/empty/empty-content.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/server/dashboard/src/lib/components/ui/empty/empty-description.svelte b/server/dashboard/src/lib/components/ui/empty/empty-description.svelte new file mode 100644 index 0000000..85a866c --- /dev/null +++ b/server/dashboard/src/lib/components/ui/empty/empty-description.svelte @@ -0,0 +1,23 @@ + + +
a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4", + className + )} + {...restProps} +> + {@render children?.()} +
diff --git a/server/dashboard/src/lib/components/ui/empty/empty-header.svelte b/server/dashboard/src/lib/components/ui/empty/empty-header.svelte new file mode 100644 index 0000000..296eaf8 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/empty/empty-header.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/server/dashboard/src/lib/components/ui/empty/empty-media.svelte b/server/dashboard/src/lib/components/ui/empty/empty-media.svelte new file mode 100644 index 0000000..0b4e45d --- /dev/null +++ b/server/dashboard/src/lib/components/ui/empty/empty-media.svelte @@ -0,0 +1,41 @@ + + + + +
+ {@render children?.()} +
diff --git a/server/dashboard/src/lib/components/ui/empty/empty-title.svelte b/server/dashboard/src/lib/components/ui/empty/empty-title.svelte new file mode 100644 index 0000000..8c237aa --- /dev/null +++ b/server/dashboard/src/lib/components/ui/empty/empty-title.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/server/dashboard/src/lib/components/ui/empty/empty.svelte b/server/dashboard/src/lib/components/ui/empty/empty.svelte new file mode 100644 index 0000000..4ccf060 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/empty/empty.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/server/dashboard/src/lib/components/ui/empty/index.ts b/server/dashboard/src/lib/components/ui/empty/index.ts new file mode 100644 index 0000000..ae4c106 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/empty/index.ts @@ -0,0 +1,22 @@ +import Root from "./empty.svelte"; +import Header from "./empty-header.svelte"; +import Media from "./empty-media.svelte"; +import Title from "./empty-title.svelte"; +import Description from "./empty-description.svelte"; +import Content from "./empty-content.svelte"; + +export { + Root, + Header, + Media, + Title, + Description, + Content, + // + Root as Empty, + Header as EmptyHeader, + Media as EmptyMedia, + Title as EmptyTitle, + Description as EmptyDescription, + Content as EmptyContent, +}; diff --git a/server/dashboard/src/lib/components/ui/input/index.ts b/server/dashboard/src/lib/components/ui/input/index.ts new file mode 100644 index 0000000..f47b6d3 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/input/index.ts @@ -0,0 +1,7 @@ +import Root from "./input.svelte"; + +export { + Root, + // + Root as Input, +}; diff --git a/server/dashboard/src/lib/components/ui/input/input.svelte b/server/dashboard/src/lib/components/ui/input/input.svelte new file mode 100644 index 0000000..ff1a4c8 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/input/input.svelte @@ -0,0 +1,52 @@ + + +{#if type === "file"} + +{:else} + +{/if} diff --git a/server/dashboard/src/lib/components/ui/label/index.ts b/server/dashboard/src/lib/components/ui/label/index.ts new file mode 100644 index 0000000..8bfca0b --- /dev/null +++ b/server/dashboard/src/lib/components/ui/label/index.ts @@ -0,0 +1,7 @@ +import Root from "./label.svelte"; + +export { + Root, + // + Root as Label, +}; diff --git a/server/dashboard/src/lib/components/ui/label/label.svelte b/server/dashboard/src/lib/components/ui/label/label.svelte new file mode 100644 index 0000000..d71afbc --- /dev/null +++ b/server/dashboard/src/lib/components/ui/label/label.svelte @@ -0,0 +1,20 @@ + + + diff --git a/server/dashboard/src/lib/components/ui/select/index.ts b/server/dashboard/src/lib/components/ui/select/index.ts new file mode 100644 index 0000000..4dec358 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/select/index.ts @@ -0,0 +1,37 @@ +import Root from "./select.svelte"; +import Group from "./select-group.svelte"; +import Label from "./select-label.svelte"; +import Item from "./select-item.svelte"; +import Content from "./select-content.svelte"; +import Trigger from "./select-trigger.svelte"; +import Separator from "./select-separator.svelte"; +import ScrollDownButton from "./select-scroll-down-button.svelte"; +import ScrollUpButton from "./select-scroll-up-button.svelte"; +import GroupHeading from "./select-group-heading.svelte"; +import Portal from "./select-portal.svelte"; + +export { + Root, + Group, + Label, + Item, + Content, + Trigger, + Separator, + ScrollDownButton, + ScrollUpButton, + GroupHeading, + Portal, + // + Root as Select, + Group as SelectGroup, + Label as SelectLabel, + Item as SelectItem, + Content as SelectContent, + Trigger as SelectTrigger, + Separator as SelectSeparator, + ScrollDownButton as SelectScrollDownButton, + ScrollUpButton as SelectScrollUpButton, + GroupHeading as SelectGroupHeading, + Portal as SelectPortal, +}; diff --git a/server/dashboard/src/lib/components/ui/select/select-content.svelte b/server/dashboard/src/lib/components/ui/select/select-content.svelte new file mode 100644 index 0000000..4b9ca43 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/select/select-content.svelte @@ -0,0 +1,45 @@ + + + + + + + {@render children?.()} + + + + diff --git a/server/dashboard/src/lib/components/ui/select/select-group-heading.svelte b/server/dashboard/src/lib/components/ui/select/select-group-heading.svelte new file mode 100644 index 0000000..1fab5f0 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/select/select-group-heading.svelte @@ -0,0 +1,21 @@ + + + + {@render children?.()} + diff --git a/server/dashboard/src/lib/components/ui/select/select-group.svelte b/server/dashboard/src/lib/components/ui/select/select-group.svelte new file mode 100644 index 0000000..a1f43bf --- /dev/null +++ b/server/dashboard/src/lib/components/ui/select/select-group.svelte @@ -0,0 +1,7 @@ + + + diff --git a/server/dashboard/src/lib/components/ui/select/select-item.svelte b/server/dashboard/src/lib/components/ui/select/select-item.svelte new file mode 100644 index 0000000..b85eef6 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/select/select-item.svelte @@ -0,0 +1,38 @@ + + + + {#snippet children({ selected, highlighted })} + + {#if selected} + + {/if} + + {#if childrenProp} + {@render childrenProp({ selected, highlighted })} + {:else} + {label || value} + {/if} + {/snippet} + diff --git a/server/dashboard/src/lib/components/ui/select/select-label.svelte b/server/dashboard/src/lib/components/ui/select/select-label.svelte new file mode 100644 index 0000000..4696025 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/select/select-label.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/server/dashboard/src/lib/components/ui/select/select-portal.svelte b/server/dashboard/src/lib/components/ui/select/select-portal.svelte new file mode 100644 index 0000000..424bcdd --- /dev/null +++ b/server/dashboard/src/lib/components/ui/select/select-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/server/dashboard/src/lib/components/ui/select/select-scroll-down-button.svelte b/server/dashboard/src/lib/components/ui/select/select-scroll-down-button.svelte new file mode 100644 index 0000000..3629205 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/select/select-scroll-down-button.svelte @@ -0,0 +1,20 @@ + + + + + diff --git a/server/dashboard/src/lib/components/ui/select/select-scroll-up-button.svelte b/server/dashboard/src/lib/components/ui/select/select-scroll-up-button.svelte new file mode 100644 index 0000000..1aa2300 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/select/select-scroll-up-button.svelte @@ -0,0 +1,20 @@ + + + + + diff --git a/server/dashboard/src/lib/components/ui/select/select-separator.svelte b/server/dashboard/src/lib/components/ui/select/select-separator.svelte new file mode 100644 index 0000000..0eac3eb --- /dev/null +++ b/server/dashboard/src/lib/components/ui/select/select-separator.svelte @@ -0,0 +1,18 @@ + + + diff --git a/server/dashboard/src/lib/components/ui/select/select-trigger.svelte b/server/dashboard/src/lib/components/ui/select/select-trigger.svelte new file mode 100644 index 0000000..dbb81df --- /dev/null +++ b/server/dashboard/src/lib/components/ui/select/select-trigger.svelte @@ -0,0 +1,29 @@ + + + + {@render children?.()} + + diff --git a/server/dashboard/src/lib/components/ui/select/select.svelte b/server/dashboard/src/lib/components/ui/select/select.svelte new file mode 100644 index 0000000..05eb663 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/select/select.svelte @@ -0,0 +1,11 @@ + + + diff --git a/server/dashboard/src/lib/components/ui/separator/index.ts b/server/dashboard/src/lib/components/ui/separator/index.ts new file mode 100644 index 0000000..82442d2 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/separator/index.ts @@ -0,0 +1,7 @@ +import Root from "./separator.svelte"; + +export { + Root, + // + Root as Separator, +}; diff --git a/server/dashboard/src/lib/components/ui/separator/separator.svelte b/server/dashboard/src/lib/components/ui/separator/separator.svelte new file mode 100644 index 0000000..f40999f --- /dev/null +++ b/server/dashboard/src/lib/components/ui/separator/separator.svelte @@ -0,0 +1,21 @@ + + + diff --git a/server/dashboard/src/lib/components/ui/sheet/index.ts b/server/dashboard/src/lib/components/ui/sheet/index.ts new file mode 100644 index 0000000..28d7da1 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/sheet/index.ts @@ -0,0 +1,34 @@ +import Root from "./sheet.svelte"; +import Portal from "./sheet-portal.svelte"; +import Trigger from "./sheet-trigger.svelte"; +import Close from "./sheet-close.svelte"; +import Overlay from "./sheet-overlay.svelte"; +import Content from "./sheet-content.svelte"; +import Header from "./sheet-header.svelte"; +import Footer from "./sheet-footer.svelte"; +import Title from "./sheet-title.svelte"; +import Description from "./sheet-description.svelte"; + +export { + Root, + Close, + Trigger, + Portal, + Overlay, + Content, + Header, + Footer, + Title, + Description, + // + Root as Sheet, + Close as SheetClose, + Trigger as SheetTrigger, + Portal as SheetPortal, + Overlay as SheetOverlay, + Content as SheetContent, + Header as SheetHeader, + Footer as SheetFooter, + Title as SheetTitle, + Description as SheetDescription, +}; diff --git a/server/dashboard/src/lib/components/ui/sheet/sheet-close.svelte b/server/dashboard/src/lib/components/ui/sheet/sheet-close.svelte new file mode 100644 index 0000000..ae382c1 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/sheet/sheet-close.svelte @@ -0,0 +1,7 @@ + + + diff --git a/server/dashboard/src/lib/components/ui/sheet/sheet-content.svelte b/server/dashboard/src/lib/components/ui/sheet/sheet-content.svelte new file mode 100644 index 0000000..065fe04 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/sheet/sheet-content.svelte @@ -0,0 +1,60 @@ + + + + + + + + {@render children?.()} + + + Close + + + diff --git a/server/dashboard/src/lib/components/ui/sheet/sheet-description.svelte b/server/dashboard/src/lib/components/ui/sheet/sheet-description.svelte new file mode 100644 index 0000000..333b17a --- /dev/null +++ b/server/dashboard/src/lib/components/ui/sheet/sheet-description.svelte @@ -0,0 +1,17 @@ + + + diff --git a/server/dashboard/src/lib/components/ui/sheet/sheet-footer.svelte b/server/dashboard/src/lib/components/ui/sheet/sheet-footer.svelte new file mode 100644 index 0000000..dd9ed84 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/sheet/sheet-footer.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/server/dashboard/src/lib/components/ui/sheet/sheet-header.svelte b/server/dashboard/src/lib/components/ui/sheet/sheet-header.svelte new file mode 100644 index 0000000..757a6a5 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/sheet/sheet-header.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/server/dashboard/src/lib/components/ui/sheet/sheet-overlay.svelte b/server/dashboard/src/lib/components/ui/sheet/sheet-overlay.svelte new file mode 100644 index 0000000..345e197 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/sheet/sheet-overlay.svelte @@ -0,0 +1,20 @@ + + + diff --git a/server/dashboard/src/lib/components/ui/sheet/sheet-portal.svelte b/server/dashboard/src/lib/components/ui/sheet/sheet-portal.svelte new file mode 100644 index 0000000..f3085a3 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/sheet/sheet-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/server/dashboard/src/lib/components/ui/sheet/sheet-title.svelte b/server/dashboard/src/lib/components/ui/sheet/sheet-title.svelte new file mode 100644 index 0000000..9fda327 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/sheet/sheet-title.svelte @@ -0,0 +1,17 @@ + + + diff --git a/server/dashboard/src/lib/components/ui/sheet/sheet-trigger.svelte b/server/dashboard/src/lib/components/ui/sheet/sheet-trigger.svelte new file mode 100644 index 0000000..e266975 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/sheet/sheet-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/server/dashboard/src/lib/components/ui/sheet/sheet.svelte b/server/dashboard/src/lib/components/ui/sheet/sheet.svelte new file mode 100644 index 0000000..5bf9783 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/sheet/sheet.svelte @@ -0,0 +1,7 @@ + + + diff --git a/server/dashboard/src/lib/components/ui/sidebar/constants.ts b/server/dashboard/src/lib/components/ui/sidebar/constants.ts new file mode 100644 index 0000000..4de4435 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/sidebar/constants.ts @@ -0,0 +1,6 @@ +export const SIDEBAR_COOKIE_NAME = "sidebar:state"; +export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; +export const SIDEBAR_WIDTH = "16rem"; +export const SIDEBAR_WIDTH_MOBILE = "18rem"; +export const SIDEBAR_WIDTH_ICON = "3rem"; +export const SIDEBAR_KEYBOARD_SHORTCUT = "b"; diff --git a/server/dashboard/src/lib/components/ui/sidebar/context.svelte.ts b/server/dashboard/src/lib/components/ui/sidebar/context.svelte.ts new file mode 100644 index 0000000..15248ad --- /dev/null +++ b/server/dashboard/src/lib/components/ui/sidebar/context.svelte.ts @@ -0,0 +1,81 @@ +import { IsMobile } from "$lib/hooks/is-mobile.svelte.js"; +import { getContext, setContext } from "svelte"; +import { SIDEBAR_KEYBOARD_SHORTCUT } from "./constants.js"; + +type Getter = () => T; + +export type SidebarStateProps = { + /** + * A getter function that returns the current open state of the sidebar. + * We use a getter function here to support `bind:open` on the `Sidebar.Provider` + * component. + */ + open: Getter; + + /** + * A function that sets the open state of the sidebar. To support `bind:open`, we need + * a source of truth for changing the open state to ensure it will be synced throughout + * the sub-components and any `bind:` references. + */ + setOpen: (open: boolean) => void; +}; + +class SidebarState { + readonly props: SidebarStateProps; + open = $derived.by(() => this.props.open()); + openMobile = $state(false); + setOpen: SidebarStateProps["setOpen"]; + #isMobile: IsMobile; + state = $derived.by(() => (this.open ? "expanded" : "collapsed")); + + constructor(props: SidebarStateProps) { + this.setOpen = props.setOpen; + this.#isMobile = new IsMobile(); + this.props = props; + } + + // Convenience getter for checking if the sidebar is mobile + // without this, we would need to use `sidebar.isMobile.current` everywhere + get isMobile() { + return this.#isMobile.current; + } + + // Event handler to apply to the `` + handleShortcutKeydown = (e: KeyboardEvent) => { + if (e.key === SIDEBAR_KEYBOARD_SHORTCUT && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + this.toggle(); + } + }; + + setOpenMobile = (value: boolean) => { + this.openMobile = value; + }; + + toggle = () => { + return this.#isMobile.current + ? (this.openMobile = !this.openMobile) + : this.setOpen(!this.open); + }; +} + +const SYMBOL_KEY = "scn-sidebar"; + +/** + * Instantiates a new `SidebarState` instance and sets it in the context. + * + * @param props The constructor props for the `SidebarState` class. + * @returns The `SidebarState` instance. + */ +export function setSidebar(props: SidebarStateProps): SidebarState { + return setContext(Symbol.for(SYMBOL_KEY), new SidebarState(props)); +} + +/** + * Retrieves the `SidebarState` instance from the context. This is a class instance, + * so you cannot destructure it. + * @returns The `SidebarState` instance. + */ +export function useSidebar(): SidebarState { + return getContext(Symbol.for(SYMBOL_KEY)); +} diff --git a/server/dashboard/src/lib/components/ui/sidebar/index.ts b/server/dashboard/src/lib/components/ui/sidebar/index.ts new file mode 100644 index 0000000..318a341 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/sidebar/index.ts @@ -0,0 +1,75 @@ +import { useSidebar } from "./context.svelte.js"; +import Content from "./sidebar-content.svelte"; +import Footer from "./sidebar-footer.svelte"; +import GroupAction from "./sidebar-group-action.svelte"; +import GroupContent from "./sidebar-group-content.svelte"; +import GroupLabel from "./sidebar-group-label.svelte"; +import Group from "./sidebar-group.svelte"; +import Header from "./sidebar-header.svelte"; +import Input from "./sidebar-input.svelte"; +import Inset from "./sidebar-inset.svelte"; +import MenuAction from "./sidebar-menu-action.svelte"; +import MenuBadge from "./sidebar-menu-badge.svelte"; +import MenuButton from "./sidebar-menu-button.svelte"; +import MenuItem from "./sidebar-menu-item.svelte"; +import MenuSkeleton from "./sidebar-menu-skeleton.svelte"; +import MenuSubButton from "./sidebar-menu-sub-button.svelte"; +import MenuSubItem from "./sidebar-menu-sub-item.svelte"; +import MenuSub from "./sidebar-menu-sub.svelte"; +import Menu from "./sidebar-menu.svelte"; +import Provider from "./sidebar-provider.svelte"; +import Rail from "./sidebar-rail.svelte"; +import Separator from "./sidebar-separator.svelte"; +import Trigger from "./sidebar-trigger.svelte"; +import Root from "./sidebar.svelte"; + +export { + Content, + Footer, + Group, + GroupAction, + GroupContent, + GroupLabel, + Header, + Input, + Inset, + Menu, + MenuAction, + MenuBadge, + MenuButton, + MenuItem, + MenuSkeleton, + MenuSub, + MenuSubButton, + MenuSubItem, + Provider, + Rail, + Root, + Separator, + // + Root as Sidebar, + Content as SidebarContent, + Footer as SidebarFooter, + Group as SidebarGroup, + GroupAction as SidebarGroupAction, + GroupContent as SidebarGroupContent, + GroupLabel as SidebarGroupLabel, + Header as SidebarHeader, + Input as SidebarInput, + Inset as SidebarInset, + Menu as SidebarMenu, + MenuAction as SidebarMenuAction, + MenuBadge as SidebarMenuBadge, + MenuButton as SidebarMenuButton, + MenuItem as SidebarMenuItem, + MenuSkeleton as SidebarMenuSkeleton, + MenuSub as SidebarMenuSub, + MenuSubButton as SidebarMenuSubButton, + MenuSubItem as SidebarMenuSubItem, + Provider as SidebarProvider, + Rail as SidebarRail, + Separator as SidebarSeparator, + Trigger as SidebarTrigger, + Trigger, + useSidebar, +}; diff --git a/server/dashboard/src/lib/components/ui/sidebar/sidebar-content.svelte b/server/dashboard/src/lib/components/ui/sidebar/sidebar-content.svelte new file mode 100644 index 0000000..f121800 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/sidebar/sidebar-content.svelte @@ -0,0 +1,24 @@ + + +
+ {@render children?.()} +
diff --git a/server/dashboard/src/lib/components/ui/sidebar/sidebar-footer.svelte b/server/dashboard/src/lib/components/ui/sidebar/sidebar-footer.svelte new file mode 100644 index 0000000..6259cb9 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/sidebar/sidebar-footer.svelte @@ -0,0 +1,21 @@ + + +
+ {@render children?.()} +
diff --git a/server/dashboard/src/lib/components/ui/sidebar/sidebar-group-action.svelte b/server/dashboard/src/lib/components/ui/sidebar/sidebar-group-action.svelte new file mode 100644 index 0000000..a76dfe1 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/sidebar/sidebar-group-action.svelte @@ -0,0 +1,36 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} + +{/if} diff --git a/server/dashboard/src/lib/components/ui/sidebar/sidebar-group-content.svelte b/server/dashboard/src/lib/components/ui/sidebar/sidebar-group-content.svelte new file mode 100644 index 0000000..415255f --- /dev/null +++ b/server/dashboard/src/lib/components/ui/sidebar/sidebar-group-content.svelte @@ -0,0 +1,21 @@ + + +
+ {@render children?.()} +
diff --git a/server/dashboard/src/lib/components/ui/sidebar/sidebar-group-label.svelte b/server/dashboard/src/lib/components/ui/sidebar/sidebar-group-label.svelte new file mode 100644 index 0000000..b2e72b6 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/sidebar/sidebar-group-label.svelte @@ -0,0 +1,34 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} +
+ {@render children?.()} +
+{/if} diff --git a/server/dashboard/src/lib/components/ui/sidebar/sidebar-group.svelte b/server/dashboard/src/lib/components/ui/sidebar/sidebar-group.svelte new file mode 100644 index 0000000..ec18a69 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/sidebar/sidebar-group.svelte @@ -0,0 +1,21 @@ + + +
+ {@render children?.()} +
diff --git a/server/dashboard/src/lib/components/ui/sidebar/sidebar-header.svelte b/server/dashboard/src/lib/components/ui/sidebar/sidebar-header.svelte new file mode 100644 index 0000000..a1b2db1 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/sidebar/sidebar-header.svelte @@ -0,0 +1,21 @@ + + +
+ {@render children?.()} +
diff --git a/server/dashboard/src/lib/components/ui/sidebar/sidebar-input.svelte b/server/dashboard/src/lib/components/ui/sidebar/sidebar-input.svelte new file mode 100644 index 0000000..19b3666 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/sidebar/sidebar-input.svelte @@ -0,0 +1,21 @@ + + + diff --git a/server/dashboard/src/lib/components/ui/sidebar/sidebar-inset.svelte b/server/dashboard/src/lib/components/ui/sidebar/sidebar-inset.svelte new file mode 100644 index 0000000..7d6d459 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/sidebar/sidebar-inset.svelte @@ -0,0 +1,24 @@ + + +
+ {@render children?.()} +
diff --git a/server/dashboard/src/lib/components/ui/sidebar/sidebar-menu-action.svelte b/server/dashboard/src/lib/components/ui/sidebar/sidebar-menu-action.svelte new file mode 100644 index 0000000..d3fe295 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/sidebar/sidebar-menu-action.svelte @@ -0,0 +1,43 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} + +{/if} diff --git a/server/dashboard/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte b/server/dashboard/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte new file mode 100644 index 0000000..e8ecdb4 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte @@ -0,0 +1,29 @@ + + +
+ {@render children?.()} +
diff --git a/server/dashboard/src/lib/components/ui/sidebar/sidebar-menu-button.svelte b/server/dashboard/src/lib/components/ui/sidebar/sidebar-menu-button.svelte new file mode 100644 index 0000000..0acd1ec --- /dev/null +++ b/server/dashboard/src/lib/components/ui/sidebar/sidebar-menu-button.svelte @@ -0,0 +1,103 @@ + + + + +{#snippet Button({ props }: { props?: Record })} + {@const mergedProps = mergeProps(buttonProps, props)} + {#if child} + {@render child({ props: mergedProps })} + {:else} + + {/if} +{/snippet} + +{#if !tooltipContent} + {@render Button({})} +{:else} + + + {#snippet child({ props })} + {@render Button({ props })} + {/snippet} + + + +{/if} diff --git a/server/dashboard/src/lib/components/ui/sidebar/sidebar-menu-item.svelte b/server/dashboard/src/lib/components/ui/sidebar/sidebar-menu-item.svelte new file mode 100644 index 0000000..4db4453 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/sidebar/sidebar-menu-item.svelte @@ -0,0 +1,21 @@ + + +
  • + {@render children?.()} +
  • diff --git a/server/dashboard/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte b/server/dashboard/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte new file mode 100644 index 0000000..68604e2 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte @@ -0,0 +1,36 @@ + + +
    + {#if showIcon} + + {/if} + + {@render children?.()} +
    diff --git a/server/dashboard/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte b/server/dashboard/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte new file mode 100644 index 0000000..c8cd4ff --- /dev/null +++ b/server/dashboard/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte @@ -0,0 +1,43 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} + + {@render children?.()} + +{/if} diff --git a/server/dashboard/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte b/server/dashboard/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte new file mode 100644 index 0000000..681d0f1 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte @@ -0,0 +1,21 @@ + + +
  • + {@render children?.()} +
  • diff --git a/server/dashboard/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte b/server/dashboard/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte new file mode 100644 index 0000000..76bd1d9 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte @@ -0,0 +1,25 @@ + + +
      + {@render children?.()} +
    diff --git a/server/dashboard/src/lib/components/ui/sidebar/sidebar-menu.svelte b/server/dashboard/src/lib/components/ui/sidebar/sidebar-menu.svelte new file mode 100644 index 0000000..946ccce --- /dev/null +++ b/server/dashboard/src/lib/components/ui/sidebar/sidebar-menu.svelte @@ -0,0 +1,21 @@ + + +
      + {@render children?.()} +
    diff --git a/server/dashboard/src/lib/components/ui/sidebar/sidebar-provider.svelte b/server/dashboard/src/lib/components/ui/sidebar/sidebar-provider.svelte new file mode 100644 index 0000000..5b0d0aa --- /dev/null +++ b/server/dashboard/src/lib/components/ui/sidebar/sidebar-provider.svelte @@ -0,0 +1,53 @@ + + + + + +
    + {@render children?.()} +
    +
    diff --git a/server/dashboard/src/lib/components/ui/sidebar/sidebar-rail.svelte b/server/dashboard/src/lib/components/ui/sidebar/sidebar-rail.svelte new file mode 100644 index 0000000..704d54f --- /dev/null +++ b/server/dashboard/src/lib/components/ui/sidebar/sidebar-rail.svelte @@ -0,0 +1,36 @@ + + + diff --git a/server/dashboard/src/lib/components/ui/sidebar/sidebar-separator.svelte b/server/dashboard/src/lib/components/ui/sidebar/sidebar-separator.svelte new file mode 100644 index 0000000..5a7deda --- /dev/null +++ b/server/dashboard/src/lib/components/ui/sidebar/sidebar-separator.svelte @@ -0,0 +1,19 @@ + + + diff --git a/server/dashboard/src/lib/components/ui/sidebar/sidebar-trigger.svelte b/server/dashboard/src/lib/components/ui/sidebar/sidebar-trigger.svelte new file mode 100644 index 0000000..1825182 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/sidebar/sidebar-trigger.svelte @@ -0,0 +1,35 @@ + + + diff --git a/server/dashboard/src/lib/components/ui/sidebar/sidebar.svelte b/server/dashboard/src/lib/components/ui/sidebar/sidebar.svelte new file mode 100644 index 0000000..bac55d8 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/sidebar/sidebar.svelte @@ -0,0 +1,104 @@ + + +{#if collapsible === "none"} +
    + {@render children?.()} +
    +{:else if sidebar.isMobile} + sidebar.openMobile, (v) => sidebar.setOpenMobile(v)} + {...restProps} + > + + + Sidebar + Displays the mobile sidebar. + +
    + {@render children?.()} +
    +
    +
    +{:else} + +{/if} diff --git a/server/dashboard/src/lib/components/ui/skeleton/index.ts b/server/dashboard/src/lib/components/ui/skeleton/index.ts new file mode 100644 index 0000000..186db21 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/skeleton/index.ts @@ -0,0 +1,7 @@ +import Root from "./skeleton.svelte"; + +export { + Root, + // + Root as Skeleton, +}; diff --git a/server/dashboard/src/lib/components/ui/skeleton/skeleton.svelte b/server/dashboard/src/lib/components/ui/skeleton/skeleton.svelte new file mode 100644 index 0000000..c7e3d26 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/skeleton/skeleton.svelte @@ -0,0 +1,17 @@ + + +
    diff --git a/server/dashboard/src/lib/components/ui/table/index.ts b/server/dashboard/src/lib/components/ui/table/index.ts new file mode 100644 index 0000000..14695c8 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/table/index.ts @@ -0,0 +1,28 @@ +import Root from "./table.svelte"; +import Body from "./table-body.svelte"; +import Caption from "./table-caption.svelte"; +import Cell from "./table-cell.svelte"; +import Footer from "./table-footer.svelte"; +import Head from "./table-head.svelte"; +import Header from "./table-header.svelte"; +import Row from "./table-row.svelte"; + +export { + Root, + Body, + Caption, + Cell, + Footer, + Head, + Header, + Row, + // + Root as Table, + Body as TableBody, + Caption as TableCaption, + Cell as TableCell, + Footer as TableFooter, + Head as TableHead, + Header as TableHeader, + Row as TableRow, +}; diff --git a/server/dashboard/src/lib/components/ui/table/table-body.svelte b/server/dashboard/src/lib/components/ui/table/table-body.svelte new file mode 100644 index 0000000..29e9687 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/table/table-body.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/server/dashboard/src/lib/components/ui/table/table-caption.svelte b/server/dashboard/src/lib/components/ui/table/table-caption.svelte new file mode 100644 index 0000000..4696cff --- /dev/null +++ b/server/dashboard/src/lib/components/ui/table/table-caption.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/server/dashboard/src/lib/components/ui/table/table-cell.svelte b/server/dashboard/src/lib/components/ui/table/table-cell.svelte new file mode 100644 index 0000000..2c0c26a --- /dev/null +++ b/server/dashboard/src/lib/components/ui/table/table-cell.svelte @@ -0,0 +1,23 @@ + + + + {@render children?.()} + diff --git a/server/dashboard/src/lib/components/ui/table/table-footer.svelte b/server/dashboard/src/lib/components/ui/table/table-footer.svelte new file mode 100644 index 0000000..b9b14eb --- /dev/null +++ b/server/dashboard/src/lib/components/ui/table/table-footer.svelte @@ -0,0 +1,20 @@ + + +tr]:last:border-b-0", className)} + {...restProps} +> + {@render children?.()} + diff --git a/server/dashboard/src/lib/components/ui/table/table-head.svelte b/server/dashboard/src/lib/components/ui/table/table-head.svelte new file mode 100644 index 0000000..b67a6f9 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/table/table-head.svelte @@ -0,0 +1,23 @@ + + + + {@render children?.()} + diff --git a/server/dashboard/src/lib/components/ui/table/table-header.svelte b/server/dashboard/src/lib/components/ui/table/table-header.svelte new file mode 100644 index 0000000..f47d259 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/table/table-header.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/server/dashboard/src/lib/components/ui/table/table-row.svelte b/server/dashboard/src/lib/components/ui/table/table-row.svelte new file mode 100644 index 0000000..0df769e --- /dev/null +++ b/server/dashboard/src/lib/components/ui/table/table-row.svelte @@ -0,0 +1,23 @@ + + +svelte-css-wrapper]:[&>th,td]:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors", + className + )} + {...restProps} +> + {@render children?.()} + diff --git a/server/dashboard/src/lib/components/ui/table/table.svelte b/server/dashboard/src/lib/components/ui/table/table.svelte new file mode 100644 index 0000000..a334956 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/table/table.svelte @@ -0,0 +1,22 @@ + + +
    + + {@render children?.()} +
    +
    diff --git a/server/dashboard/src/lib/components/ui/textarea/index.ts b/server/dashboard/src/lib/components/ui/textarea/index.ts new file mode 100644 index 0000000..ace797a --- /dev/null +++ b/server/dashboard/src/lib/components/ui/textarea/index.ts @@ -0,0 +1,7 @@ +import Root from "./textarea.svelte"; + +export { + Root, + // + Root as Textarea, +}; diff --git a/server/dashboard/src/lib/components/ui/textarea/textarea.svelte b/server/dashboard/src/lib/components/ui/textarea/textarea.svelte new file mode 100644 index 0000000..124e9d0 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/textarea/textarea.svelte @@ -0,0 +1,23 @@ + + + diff --git a/server/dashboard/src/lib/components/ui/tooltip/index.ts b/server/dashboard/src/lib/components/ui/tooltip/index.ts new file mode 100644 index 0000000..1718604 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/tooltip/index.ts @@ -0,0 +1,19 @@ +import Root from "./tooltip.svelte"; +import Trigger from "./tooltip-trigger.svelte"; +import Content from "./tooltip-content.svelte"; +import Provider from "./tooltip-provider.svelte"; +import Portal from "./tooltip-portal.svelte"; + +export { + Root, + Trigger, + Content, + Provider, + Portal, + // + Root as Tooltip, + Content as TooltipContent, + Trigger as TooltipTrigger, + Provider as TooltipProvider, + Portal as TooltipPortal, +}; diff --git a/server/dashboard/src/lib/components/ui/tooltip/tooltip-content.svelte b/server/dashboard/src/lib/components/ui/tooltip/tooltip-content.svelte new file mode 100644 index 0000000..2662522 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/tooltip/tooltip-content.svelte @@ -0,0 +1,52 @@ + + + + + {@render children?.()} + + {#snippet child({ props })} +
    + {/snippet} +
    +
    +
    diff --git a/server/dashboard/src/lib/components/ui/tooltip/tooltip-portal.svelte b/server/dashboard/src/lib/components/ui/tooltip/tooltip-portal.svelte new file mode 100644 index 0000000..d234f7d --- /dev/null +++ b/server/dashboard/src/lib/components/ui/tooltip/tooltip-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/server/dashboard/src/lib/components/ui/tooltip/tooltip-provider.svelte b/server/dashboard/src/lib/components/ui/tooltip/tooltip-provider.svelte new file mode 100644 index 0000000..8150bef --- /dev/null +++ b/server/dashboard/src/lib/components/ui/tooltip/tooltip-provider.svelte @@ -0,0 +1,7 @@ + + + diff --git a/server/dashboard/src/lib/components/ui/tooltip/tooltip-trigger.svelte b/server/dashboard/src/lib/components/ui/tooltip/tooltip-trigger.svelte new file mode 100644 index 0000000..1acdaa4 --- /dev/null +++ b/server/dashboard/src/lib/components/ui/tooltip/tooltip-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/server/dashboard/src/lib/components/ui/tooltip/tooltip.svelte b/server/dashboard/src/lib/components/ui/tooltip/tooltip.svelte new file mode 100644 index 0000000..0b0f9ce --- /dev/null +++ b/server/dashboard/src/lib/components/ui/tooltip/tooltip.svelte @@ -0,0 +1,7 @@ + + + diff --git a/server/dashboard/src/lib/hooks/is-mobile.svelte.ts b/server/dashboard/src/lib/hooks/is-mobile.svelte.ts new file mode 100644 index 0000000..4829c00 --- /dev/null +++ b/server/dashboard/src/lib/hooks/is-mobile.svelte.ts @@ -0,0 +1,9 @@ +import { MediaQuery } from "svelte/reactivity"; + +const DEFAULT_MOBILE_BREAKPOINT = 768; + +export class IsMobile extends MediaQuery { + constructor(breakpoint: number = DEFAULT_MOBILE_BREAKPOINT) { + super(`max-width: ${breakpoint - 1}px`); + } +} diff --git a/server/dashboard/src/lib/schema.ts b/server/dashboard/src/lib/schema.ts index c6557b5..9d946eb 100644 --- a/server/dashboard/src/lib/schema.ts +++ b/server/dashboard/src/lib/schema.ts @@ -6,6 +6,7 @@ import { json, mysqlEnum, timestamp, + datetime, customType } from 'drizzle-orm/mysql-core'; @@ -49,6 +50,23 @@ export const bugReportFiles = mysqlTable('bug_report_files', { created_at: timestamp('created_at').notNull().defaultNow() }); +export const userTable = mysqlTable('user', { + id: varchar('id', { length: 255 }).primaryKey(), + username: varchar('username', { length: 255 }).notNull().unique(), + displayname: varchar('displayname', { length: 255 }).notNull().default(''), + passwordHash: varchar('password_hash', { length: 255 }).notNull(), + role: mysqlEnum('role', ['admin', 'user']).notNull().default('user'), + createdAt: timestamp('created_at').notNull().defaultNow() +}); + +export const sessionTable = mysqlTable('session', { + id: varchar('id', { length: 255 }).primaryKey(), + userId: varchar('user_id', { length: 255 }) + .notNull() + .references(() => userTable.id), + expiresAt: datetime('expires_at').notNull() +}); + export type BugReport = typeof bugReports.$inferSelect; export type BugReportFile = typeof bugReportFiles.$inferSelect; export type BugReportStatus = BugReport['status']; diff --git a/server/dashboard/src/lib/server/auth.ts b/server/dashboard/src/lib/server/auth.ts new file mode 100644 index 0000000..590e6b0 --- /dev/null +++ b/server/dashboard/src/lib/server/auth.ts @@ -0,0 +1,36 @@ +import { Lucia } from 'lucia'; +import { DrizzleMySQLAdapter } from '@lucia-auth/adapter-drizzle'; +import { db } from './db'; +import { sessionTable, userTable } from '$lib/schema'; +import { dev } from '$app/environment'; + +const adapter = new DrizzleMySQLAdapter(db, sessionTable, userTable); + +export const lucia = new Lucia(adapter, { + sessionCookie: { + attributes: { + secure: !dev + } + }, + getUserAttributes: (attributes) => { + return { + username: attributes.username, + role: attributes.role, + displayname: attributes.displayname + }; + } +}); + +declare module 'lucia' { + interface Register { + Lucia: typeof lucia; + DatabaseUserAttributes: DatabaseUserAttributes; + } +} + +interface DatabaseUserAttributes { + username: string; + role: 'admin' | 'user'; + displayname: string; +} +// End of file diff --git a/server/dashboard/src/lib/utils.ts b/server/dashboard/src/lib/utils.ts index 10803a7..3cebf9a 100644 --- a/server/dashboard/src/lib/utils.ts +++ b/server/dashboard/src/lib/utils.ts @@ -1,5 +1,5 @@ -import { type ClassValue, clsx } from 'clsx'; -import { twMerge } from 'tailwind-merge'; +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); @@ -37,3 +37,10 @@ export const statusLabels: Record = { resolved: 'Resolved', closed: 'Closed' }; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type WithoutChild = T extends { child?: any } ? Omit : T; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type WithoutChildren = T extends { children?: any } ? Omit : T; +export type WithoutChildrenOrChild = WithoutChildren>; +export type WithElementRef = T & { ref?: U | null }; diff --git a/server/dashboard/src/routes/+layout.server.ts b/server/dashboard/src/routes/+layout.server.ts index c3760d9..d95b760 100644 --- a/server/dashboard/src/routes/+layout.server.ts +++ b/server/dashboard/src/routes/+layout.server.ts @@ -1,15 +1,25 @@ import type { LayoutServerLoad } from './$types'; +import { redirect } from '@sveltejs/kit'; import { db } from '$lib/server/db'; import { bugReports } from '$lib/schema'; import { eq, count } from 'drizzle-orm'; -export const load: LayoutServerLoad = async () => { +export const load: LayoutServerLoad = async ({ locals, url }) => { + if (url.pathname === '/login') { + return { newCount: 0, user: null }; + } + + if (!locals.user) { + redirect(302, '/login'); + } + const [result] = await db .select({ count: count() }) .from(bugReports) .where(eq(bugReports.status, 'new')); return { - newCount: result.count + newCount: result.count, + user: locals.user }; }; diff --git a/server/dashboard/src/routes/+layout.svelte b/server/dashboard/src/routes/+layout.svelte index a97862d..56ada1d 100644 --- a/server/dashboard/src/routes/+layout.svelte +++ b/server/dashboard/src/routes/+layout.svelte @@ -1,61 +1,121 @@ -
    - - - - -
    - -
    -

    - {#if $page.url.pathname === '/'} - Bug Reports - {:else if $page.url.pathname.startsWith('/reports/')} - Report Detail - {:else} - Dashboard - {/if} -

    - {#if data.newCount > 0} -
    - - {data.newCount} new {data.newCount === 1 ? 'report' : 'reports'} +{#if !data.user} + {@render children()} +{:else} + + + +
    + + EMLy Dashboard
    - {/if} -
    - - -
    - {@render children()} -
    -
    -
    + + + + Menu + + + + + {#snippet child({ props })} + + + Reports + + {/snippet} + + + {#if data.user.role === 'admin'} + + + {#snippet child({ props })} + + + Users + + {/snippet} + + + {/if} + + + + + + + + + {#snippet child({ props })} +
    +
    + {data.user.displayname || data.user.username} + + {data.user.role} + +
    +
    + +
    +
    + {/snippet} +
    +
    +
    +
    + + +
    +
    + + +

    + {#if $page.url.pathname === '/'} + Bug Reports + {:else if $page.url.pathname.startsWith('/reports/')} + Report Detail + {:else if $page.url.pathname === '/users'} + User Management + {:else} + Dashboard + {/if} +

    +
    +
    + {#if data.newCount > 0} +
    + + {data.newCount} new {data.newCount === 1 ? 'report' : 'reports'} +
    + {/if} +
    +
    +
    + {@render children()} +
    +
    + +{/if} diff --git a/server/dashboard/src/routes/+page.svelte b/server/dashboard/src/routes/+page.svelte index 44d914b..84b8900 100644 --- a/server/dashboard/src/routes/+page.svelte +++ b/server/dashboard/src/routes/+page.svelte @@ -2,13 +2,25 @@ import { goto, invalidateAll } from '$app/navigation'; import { page } from '$app/stores'; import { statusColors, statusLabels, formatDate } from '$lib/utils'; - import { Search, ChevronLeft, ChevronRight, Filter, Paperclip, RefreshCcw } from 'lucide-svelte'; + import { Search, ChevronLeft, ChevronRight, Filter, Paperclip, RefreshCcw, Inbox } from 'lucide-svelte'; + import { Button } from '$lib/components/ui/button'; + import { Input } from '$lib/components/ui/input'; + import * as Table from '$lib/components/ui/table'; + import * as Select from '$lib/components/ui/select'; + import * as Empty from '$lib/components/ui/empty'; let { data } = $props(); let searchInput = $state(''); let statusFilter = $state(''); + const statusOptions = [ + { value: 'new', label: 'New' }, + { value: 'in_review', label: 'In Review' }, + { value: 'resolved', label: 'Resolved' }, + { value: 'closed', label: 'Closed' } + ]; + $effect(() => { searchInput = data.filters.search; statusFilter = data.filters.status; @@ -48,7 +60,7 @@
    -
    - - - + + {#if data.filters.search || data.filters.status} - + {/if}
    -
    - - - - - - - - - - - - - - {#each data.reports as report (report.id)} - goto(`/reports/${report.id}`)} - > - - - - - - - - + {#if data.reports.length === 0} +
    + + + + + + No reports found + + There are no bug reports matching your current filters. + + + {#if data.filters.search || data.filters.status} + + + {:else} -
    - - - {/each} - -
    IDHostnameUserReporterStatusFilesCreated
    #{report.id}{report.hostname || '—'}{report.os_user || '—'} -
    {report.name}
    -
    {report.email}
    -
    - - {statusLabels[report.status]} - - - {#if report.file_count > 0} - - - {report.file_count} - - {:else} - - {/if} - {formatDate(report.created_at)}
    - No reports found. -
    -
    + + + + {/if} + + + {:else} +
    + + + + ID + Hostname + User + Reporter + Status + Files + Created + + + + {#each data.reports as report (report.id)} + goto(`/reports/${report.id}`)} + > + #{report.id} + {report.hostname || '—'} + {report.os_user || '—'} + +
    {report.name}
    +
    {report.email}
    +
    + + + {statusLabels[report.status]} + + + + {#if report.file_count > 0} + + + {report.file_count} + + {:else} + + {/if} + + + {formatDate(report.created_at)} + +
    + {/each} +
    +
    +
    + {/if} {#if data.pagination.totalPages > 1} @@ -158,35 +189,35 @@ )} of {data.pagination.total} reports

    - + {#each Array.from({ length: data.pagination.totalPages }, (_, i) => i + 1) as p} {#if p === 1 || p === data.pagination.totalPages || (p >= data.pagination.page - 1 && p <= data.pagination.page + 1)} - + {:else if p === data.pagination.page - 2 || p === data.pagination.page + 2} ... {/if} {/each} - +
    {/if} diff --git a/server/dashboard/src/routes/login/+page.server.ts b/server/dashboard/src/routes/login/+page.server.ts new file mode 100644 index 0000000..fc52c9c --- /dev/null +++ b/server/dashboard/src/routes/login/+page.server.ts @@ -0,0 +1,59 @@ +import type { Actions, PageServerLoad } from './$types'; +import { fail, redirect } from '@sveltejs/kit'; +import { verify } from '@node-rs/argon2'; +import { lucia } from '$lib/server/auth'; +import { db } from '$lib/server/db'; +import { userTable } from '$lib/schema'; +import { eq } from 'drizzle-orm'; + +export const load: PageServerLoad = async ({ locals }) => { + if (locals.user) { + redirect(302, '/'); + } +}; + +export const actions: Actions = { + default: async ({ request, cookies }) => { + const formData = await request.formData(); + const username = formData.get('username'); + const password = formData.get('password'); + + if (typeof username !== 'string' || typeof password !== 'string') { + return fail(400, { message: 'Invalid input' }); + } + + if (!username || !password) { + return fail(400, { message: 'Username and password are required' }); + } + + const [user] = await db + .select() + .from(userTable) + .where(eq(userTable.username, username)) + .limit(1); + + if (!user) { + return fail(400, { message: 'Invalid username or password' }); + } + + const validPassword = await verify(user.passwordHash, password, { + memoryCost: 19456, + timeCost: 2, + outputLen: 32, + parallelism: 1 + }); + + if (!validPassword) { + return fail(400, { message: 'Invalid username or password' }); + } + + const session = await lucia.createSession(user.id, {}); + const sessionCookie = lucia.createSessionCookie(session.id); + cookies.set(sessionCookie.name, sessionCookie.value, { + path: '.', + ...sessionCookie.attributes + }); + + redirect(302, '/'); + } +}; diff --git a/server/dashboard/src/routes/login/+page.svelte b/server/dashboard/src/routes/login/+page.svelte new file mode 100644 index 0000000..2c74fee --- /dev/null +++ b/server/dashboard/src/routes/login/+page.svelte @@ -0,0 +1,61 @@ + + +
    +
    +
    + +

    EMLy Dashboard

    +

    Sign in to continue

    +
    + + {#if form?.message} +
    + {form.message} +
    + {/if} + +
    +
    + + +
    + +
    + + +
    + + +
    +
    +
    diff --git a/server/dashboard/src/routes/logout/+page.server.ts b/server/dashboard/src/routes/logout/+page.server.ts new file mode 100644 index 0000000..67b748e --- /dev/null +++ b/server/dashboard/src/routes/logout/+page.server.ts @@ -0,0 +1,24 @@ +import type { Actions, PageServerLoad } from './$types'; +import { redirect } from '@sveltejs/kit'; +import { lucia } from '$lib/server/auth'; + +export const load: PageServerLoad = async () => { + redirect(302, '/'); +}; + +export const actions: Actions = { + default: async ({ locals, cookies }) => { + if (!locals.session) { + redirect(302, '/login'); + } + + await lucia.invalidateSession(locals.session.id); + const sessionCookie = lucia.createBlankSessionCookie(); + cookies.set(sessionCookie.name, sessionCookie.value, { + path: '.', + ...sessionCookie.attributes + }); + + redirect(302, '/login'); + } +}; diff --git a/server/dashboard/src/routes/reports/[id]/+page.svelte b/server/dashboard/src/routes/reports/[id]/+page.svelte index eb43c0a..9a9e713 100644 --- a/server/dashboard/src/routes/reports/[id]/+page.svelte +++ b/server/dashboard/src/routes/reports/[id]/+page.svelte @@ -10,14 +10,17 @@ Monitor, Settings, Database, - ChevronDown, - ChevronRight, Mail } from 'lucide-svelte'; + import * as AlertDialog from '$lib/components/ui/alert-dialog'; + import * as Card from '$lib/components/ui/card'; + import * as Select from '$lib/components/ui/select'; + import * as Table from '$lib/components/ui/table'; + import { Button } from '$lib/components/ui/button'; + import { Textarea } from '$lib/components/ui/textarea'; let { data } = $props(); let showDeleteDialog = $state(false); - let showSystemInfo = $state(false); let statusUpdating = $state(false); let deleting = $state(false); @@ -64,216 +67,213 @@
    - + -
    -
    -
    -
    -

    Report #{data.report.id}

    - - {statusLabels[data.report.status]} - + + +
    +
    +
    + Report #{data.report.id} + + {statusLabels[data.report.status]} + +
    + + Submitted by {data.report.name} ({data.report.email}) +
    -

    - Submitted by {data.report.name} ({data.report.email}) -

    -
    -
    - - +
    + + updateStatus(val)} + > + + {statusLabels[data.report.status]} + + + + + + + + - - - - ZIP - + + - - + + +
    -
    - - -
    -
    -

    Hostname

    -

    {data.report.hostname || '—'}

    + + + +
    +
    +

    Hostname

    +

    {data.report.hostname || '—'}

    +
    +
    +

    OS User

    +

    {data.report.os_user || '—'}

    +
    +
    +

    HWID

    +

    {data.report.hwid || '—'}

    +
    +
    +

    IP Address

    +

    {data.report.submitter_ip || '—'}

    +
    +
    +

    Created

    +

    {formatDate(data.report.created_at)}

    +
    +
    +

    Updated

    +

    {formatDate(data.report.updated_at)}

    +
    -
    -

    OS User

    -

    {data.report.os_user || '—'}

    -
    -
    -

    HWID

    -

    {data.report.hwid || '—'}

    -
    -
    -

    IP Address

    -

    {data.report.submitter_ip || '—'}

    -
    -
    -

    Created

    -

    {formatDate(data.report.created_at)}

    -
    -
    -

    Updated

    -

    {formatDate(data.report.updated_at)}

    -
    -
    -
    + + -
    -

    Description

    -

    {data.report.description}

    -
    + + + Description + + +

    {data.report.description}

    +
    +
    {#if data.report.system_info} -
    - - {#if showSystemInfo} -
    -
    {data.report.system_info}
    + + + System Information + + +
    +