refactor: reorganize Rust codebase into modular handlers and database layers

- Split monolithic src/db.rs (1122 lines) into domain modules: projects, tags, settings
- Extract API handlers from main.rs into separate handler modules by domain
- Add proxy module for ISR/SSR coordination with Bun process
- Introduce AppState for shared application context
- Add utility functions for asset serving and request classification
- Remove obsolete middleware/auth.rs in favor of session checks in handlers
This commit is contained in:
2026-01-07 13:55:23 -06:00
parent 4663b00942
commit cf599d09d6
45 changed files with 3525 additions and 3326 deletions
+3 -1
View File
@@ -170,7 +170,9 @@ html,
body {
@apply font-inter overflow-x-hidden;
color: var(--color-text-primary);
transition: background-color 0.3s ease-in-out, color 0.3s ease-in-out;
transition:
background-color 0.3s ease-in-out,
color 0.3s ease-in-out;
}
body {
+7 -4
View File
@@ -4,11 +4,14 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function() {
const stored = localStorage.getItem('theme');
const isDark = stored === 'dark' || (stored !== 'light' && window.matchMedia('(prefers-color-scheme: dark)').matches);
(function () {
const stored = localStorage.getItem("theme");
const isDark =
stored === "dark" ||
(stored !== "light" &&
window.matchMedia("(prefers-color-scheme: dark)").matches);
if (isDark) {
document.documentElement.classList.add('dark');
document.documentElement.classList.add("dark");
}
})();
</script>
+1 -1
View File
@@ -19,7 +19,7 @@ export async function apiFetch<T>(
const url = `${baseUrl}${path}`;
const method = init?.method ?? "GET";
// Unix sockets require Bun's native fetch (SvelteKit's fetch doesn't support it)
const fetchFn = isUnixSocket ? fetch : (init?.fetch ?? fetch);
+5 -7
View File
@@ -16,10 +16,7 @@ import type {
// ============================================================================
// Client-side fetch wrapper for browser requests
async function clientApiFetch<T>(
path: string,
init?: RequestInit,
): Promise<T> {
async function clientApiFetch<T>(path: string, init?: RequestInit): Promise<T> {
const response = await fetch(path, {
...init,
credentials: "same-origin", // Include cookies for auth
@@ -83,9 +80,10 @@ export async function deleteAdminProject(id: string): Promise<AdminProject> {
// Admin Tags API
export async function getAdminTags(): Promise<AdminTagWithCount[]> {
const tags = await clientApiFetch<
Array<AdminTag & { project_count: number }>
>("/api/tags");
const tags =
await clientApiFetch<Array<AdminTag & { project_count: number }>>(
"/api/tags",
);
// Transform snake_case to camelCase
return tags.map((item) => ({
+12 -2
View File
@@ -19,9 +19,19 @@
} = $props();
</script>
<div class={cn("pointer-events-none fixed inset-0 -z-20 bg-white dark:bg-black transition-colors duration-300", bgColor)}></div>
<div
class={cn(
"pointer-events-none fixed inset-0 -z-20 bg-white dark:bg-black transition-colors duration-300",
bgColor,
)}
></div>
<Dots class={[backgroundClass]} />
<main class={cn("relative min-h-screen text-zinc-900 dark:text-zinc-50 transition-colors duration-300", className)}>
<main
class={cn(
"relative min-h-screen text-zinc-900 dark:text-zinc-50 transition-colors duration-300",
className,
)}
>
{#if showThemeToggle}
<div class="absolute top-5 right-6 z-50">
<ThemeToggle />
+16 -5
View File
@@ -83,7 +83,10 @@
transition:scale={{ duration: 200, start: 0.95 }}
>
<div class="flex items-start justify-between mb-4">
<h2 id="pgp-modal-title" class="text-xl font-semibold text-zinc-900 dark:text-white">
<h2
id="pgp-modal-title"
class="text-xl font-semibold text-zinc-900 dark:text-white"
>
PGP Public Key
</h2>
<button
@@ -109,17 +112,23 @@
</div>
<!-- Fingerprint -->
<div class="mb-4 p-3 bg-zinc-100 dark:bg-zinc-800 rounded border border-zinc-200 dark:border-zinc-700">
<div
class="mb-4 p-3 bg-zinc-100 dark:bg-zinc-800 rounded border border-zinc-200 dark:border-zinc-700"
>
<div class="text-xs font-medium text-zinc-600 dark:text-zinc-400 mb-1">
Fingerprint
</div>
<div class="font-mono text-sm text-zinc-900 dark:text-zinc-100 break-all">
<div
class="font-mono text-sm text-zinc-900 dark:text-zinc-100 break-all"
>
{PGP_KEY_METADATA.fingerprint}
</div>
</div>
<!-- Key Content -->
<div class="mb-4 border border-zinc-200 dark:border-zinc-700 rounded overflow-hidden">
<div
class="mb-4 border border-zinc-200 dark:border-zinc-700 rounded overflow-hidden"
>
{#if loading}
<div class="p-4 text-center text-zinc-600 dark:text-zinc-400">
Loading key...
@@ -156,7 +165,9 @@
class="flex items-center gap-2 px-4 py-2 rounded-sm bg-zinc-900 dark:bg-zinc-100 text-white dark:text-zinc-900 hover:bg-zinc-800 dark:hover:bg-zinc-200 transition-colors"
>
<IconCopy class="size-4" />
<span class="text-sm font-medium">{copySuccess ? "Copied!" : "Copy to Clipboard"}</span>
<span class="text-sm font-medium"
>{copySuccess ? "Copied!" : "Copy to Clipboard"}</span
>
</button>
</div>
</div>
+86 -76
View File
@@ -13,7 +13,9 @@
let { project, class: className }: Props = $props();
// Prefer demo URL, fallback to GitHub repo
const projectUrl = project.demoUrl || (project.githubRepo ? `https://github.com/${project.githubRepo}` : null);
const projectUrl =
project.demoUrl ||
(project.githubRepo ? `https://github.com/${project.githubRepo}` : null);
function formatDate(dateString: string): string {
const date = new Date(dateString);
@@ -34,87 +36,95 @@
</script>
{#if projectUrl}
<a
href={projectUrl}
target="_blank"
rel="noopener noreferrer"
class={cn(
"group flex h-44 flex-col gap-2.5 rounded-lg border border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/50 p-3 transition-all hover:border-zinc-300 dark:hover:border-zinc-700 hover:bg-zinc-100 dark:hover:bg-zinc-800/70",
className,
)}
>
<div class="flex flex-col gap-1">
<div class="flex items-start justify-between gap-2">
<h3
class="truncate font-medium text-lg sm:text-base text-zinc-900 dark:text-zinc-100 transition-colors group-hover:text-zinc-950 dark:group-hover:text-white"
<a
href={projectUrl}
target="_blank"
rel="noopener noreferrer"
class={cn(
"group flex h-44 flex-col gap-2.5 rounded-lg border border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/50 p-3 transition-all hover:border-zinc-300 dark:hover:border-zinc-700 hover:bg-zinc-100 dark:hover:bg-zinc-800/70",
className,
)}
>
<div class="flex flex-col gap-1">
<div class="flex items-start justify-between gap-2">
<h3
class="truncate font-medium text-lg sm:text-base text-zinc-900 dark:text-zinc-100 transition-colors group-hover:text-zinc-950 dark:group-hover:text-white"
>
{project.name}
</h3>
<span
class="shrink-0 sm:text-[0.83rem] text-zinc-600 dark:text-zinc-300"
>
{formatDate(project.updatedAt)}
</span>
</div>
<p
class="line-clamp-3 sm:text-sm leading-relaxed text-zinc-600 dark:text-zinc-400"
>
{project.name}
</h3>
<span class="shrink-0 sm:text-[0.83rem] text-zinc-600 dark:text-zinc-300">
{formatDate(project.updatedAt)}
</span>
{project.shortDescription}
</p>
</div>
<p class="line-clamp-3 sm:text-sm leading-relaxed text-zinc-600 dark:text-zinc-400">
{project.shortDescription}
</p>
</div>
<div class="mt-auto flex flex-wrap gap-1">
{#each project.tags as tag (tag.name)}
<!-- TODO: Add link to project search with tag filtering -->
<span
class="inline-flex items-center gap-1.25 rounded-r-sm rounded-l-xs bg-zinc-200/80 dark:bg-zinc-700/50 px-2 sm:px-1.5 py-1 sm:py-0.75 text-sm sm:text-xs text-zinc-700 dark:text-zinc-300 border-l-3"
style="border-left-color: #{tag.color || '06b6d4'}"
>
{#if tag.iconSvg}
<span class="size-4.25 sm:size-3.75 [&>svg]:w-full [&>svg]:h-full">
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html tag.iconSvg}
</span>
{/if}
<span>{tag.name}</span>
</span>
{/each}
</div>
</a>
<div class="mt-auto flex flex-wrap gap-1">
{#each project.tags as tag (tag.name)}
<!-- TODO: Add link to project search with tag filtering -->
<span
class="inline-flex items-center gap-1.25 rounded-r-sm rounded-l-xs bg-zinc-200/80 dark:bg-zinc-700/50 px-2 sm:px-1.5 py-1 sm:py-0.75 text-sm sm:text-xs text-zinc-700 dark:text-zinc-300 border-l-3"
style="border-left-color: #{tag.color || '06b6d4'}"
>
{#if tag.iconSvg}
<span class="size-4.25 sm:size-3.75 [&>svg]:w-full [&>svg]:h-full">
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html tag.iconSvg}
</span>
{/if}
<span>{tag.name}</span>
</span>
{/each}
</div>
</a>
{:else}
<div
class={cn(
"flex h-44 flex-col gap-2.5 rounded-lg border border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/50 p-3",
className,
)}
>
<div class="flex flex-col gap-1">
<div class="flex items-start justify-between gap-2">
<h3
class="truncate font-medium text-lg sm:text-base text-zinc-900 dark:text-zinc-100"
<div
class={cn(
"flex h-44 flex-col gap-2.5 rounded-lg border border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/50 p-3",
className,
)}
>
<div class="flex flex-col gap-1">
<div class="flex items-start justify-between gap-2">
<h3
class="truncate font-medium text-lg sm:text-base text-zinc-900 dark:text-zinc-100"
>
{project.name}
</h3>
<span
class="shrink-0 sm:text-[0.83rem] text-zinc-600 dark:text-zinc-300"
>
{formatDate(project.updatedAt)}
</span>
</div>
<p
class="line-clamp-3 sm:text-sm leading-relaxed text-zinc-600 dark:text-zinc-400"
>
{project.name}
</h3>
<span class="shrink-0 sm:text-[0.83rem] text-zinc-600 dark:text-zinc-300">
{formatDate(project.updatedAt)}
</span>
{project.shortDescription}
</p>
</div>
<p class="line-clamp-3 sm:text-sm leading-relaxed text-zinc-600 dark:text-zinc-400">
{project.shortDescription}
</p>
</div>
<div class="mt-auto flex flex-wrap gap-1">
{#each project.tags as tag (tag.name)}
<span
class="inline-flex items-center gap-1.25 rounded-r-sm rounded-l-xs bg-zinc-200/80 dark:bg-zinc-700/50 px-2 sm:px-1.5 py-1 sm:py-0.75 text-sm sm:text-xs text-zinc-700 dark:text-zinc-300 border-l-3"
style="border-left-color: #{tag.color || '06b6d4'}"
>
{#if tag.iconSvg}
<span class="size-4.25 sm:size-3.75 [&>svg]:w-full [&>svg]:h-full">
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html tag.iconSvg}
</span>
{/if}
<span>{tag.name}</span>
</span>
{/each}
<div class="mt-auto flex flex-wrap gap-1">
{#each project.tags as tag (tag.name)}
<span
class="inline-flex items-center gap-1.25 rounded-r-sm rounded-l-xs bg-zinc-200/80 dark:bg-zinc-700/50 px-2 sm:px-1.5 py-1 sm:py-0.75 text-sm sm:text-xs text-zinc-700 dark:text-zinc-300 border-l-3"
style="border-left-color: #{tag.color || '06b6d4'}"
>
{#if tag.iconSvg}
<span class="size-4.25 sm:size-3.75 [&>svg]:w-full [&>svg]:h-full">
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html tag.iconSvg}
</span>
{/if}
<span>{tag.name}</span>
</span>
{/each}
</div>
</div>
</div>
{/if}
+3 -1
View File
@@ -7,7 +7,9 @@
<button
type="button"
onclick={() => themeStore.toggle()}
aria-label={themeStore.isDark ? "Switch to light mode" : "Switch to dark mode"}
aria-label={themeStore.isDark
? "Switch to light mode"
: "Switch to dark mode"}
class="relative size-9 rounded-md border border-zinc-300 dark:border-zinc-700 bg-zinc-100 dark:bg-zinc-900/50 hover:bg-zinc-200 dark:hover:bg-zinc-800/70 transition-all duration-200"
>
<div class="absolute inset-0 flex items-center justify-center">
+2 -1
View File
@@ -33,7 +33,8 @@
"bg-transparent text-admin-text border border-admin-border hover:border-admin-border-hover hover:bg-admin-surface-hover/50 focus-visible:ring-admin-accent",
danger:
"bg-red-600 text-white hover:bg-red-500 focus-visible:ring-red-500 shadow-sm hover:shadow",
ghost: "text-admin-text hover:bg-admin-surface-hover focus-visible:ring-admin-accent",
ghost:
"text-admin-text hover:bg-admin-surface-hover focus-visible:ring-admin-accent",
};
const sizeStyles = {
@@ -117,7 +117,8 @@
<div class="flex items-start gap-2">
<div class="flex-1">
<div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-admin-text-muted"
<span
class="absolute left-3 top-1/2 -translate-y-1/2 text-admin-text-muted"
>#</span
>
<input
+5 -2
View File
@@ -64,7 +64,9 @@
{event.message}
</span>
<span class="text-admin-text-muted shrink-0">
target=<span class="text-admin-text-secondary">{event.target}</span>
target=<span class="text-admin-text-secondary"
>{event.target}</span
>
</span>
</div>
<div class="flex items-center gap-3 shrink-0">
@@ -88,7 +90,8 @@
class="bg-admin-surface border border-admin-border rounded p-3 text-[11px]"
>
<p class="text-admin-text-muted mb-2 font-medium">Metadata:</p>
<pre class="text-admin-text-secondary overflow-x-auto">{JSON.stringify(
<pre
class="text-admin-text-secondary overflow-x-auto">{JSON.stringify(
event.metadata,
null,
2,
@@ -211,7 +211,9 @@
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html selectedIconSvg}
{:else}
<div class="size-6 animate-pulse rounded bg-admin-surface-hover"></div>
<div
class="size-6 animate-pulse rounded bg-admin-surface-hover"
></div>
{/if}
</div>
<div class="flex-1">
+9 -3
View File
@@ -78,10 +78,14 @@
>
<div class="flex h-full flex-col">
<!-- Logo -->
<div class="border-b border-admin-border px-4 py-5 flex items-center justify-between">
<div
class="border-b border-admin-border px-4 py-5 flex items-center justify-between"
>
<h1 class="text-base font-semibold text-admin-text">
xevion.dev
<span class="text-xs font-normal text-admin-text-muted ml-1.5">Admin</span>
<span class="text-xs font-normal text-admin-text-muted ml-1.5"
>Admin</span
>
</h1>
<ThemeToggle />
</div>
@@ -110,7 +114,9 @@
</nav>
<!-- Bottom actions -->
<div class="space-y-0.5 border-t border-admin-border bg-admin-surface/50 p-3">
<div
class="space-y-0.5 border-t border-admin-border bg-admin-surface/50 p-3"
>
<a
href="/"
class="flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium text-admin-text-muted transition-all hover:text-admin-text hover:bg-admin-surface-hover/50"
+4 -4
View File
@@ -1,6 +1,6 @@
export const PGP_KEY_METADATA = {
fingerprint: '211D 7157 249B F07D 81C8 B9DE C217 005C F3C0 0672',
keyId: 'C217005CF3C00672',
email: 'xevion@xevion.dev',
name: 'Ryan Walters',
fingerprint: "211D 7157 249B F07D 81C8 B9DE C217 005C F3C0 0672",
keyId: "C217005CF3C00672",
email: "xevion@xevion.dev",
name: "Ryan Walters",
} as const;
+18 -7
View File
@@ -34,10 +34,13 @@
class="max-w-2xl mx-4 border-b border-zinc-200 dark:border-zinc-700 divide-y divide-zinc-200 dark:divide-zinc-700 sm:mx-6"
>
<div class="flex flex-col pb-4">
<span class="text-2xl font-bold text-zinc-900 dark:text-white sm:text-3xl"
<span
class="text-2xl font-bold text-zinc-900 dark:text-white sm:text-3xl"
>{settings.identity.displayName},</span
>
<span class="text-xl font-normal text-zinc-600 dark:text-zinc-400 sm:text-2xl">
<span
class="text-xl font-normal text-zinc-600 dark:text-zinc-400 sm:text-2xl"
>
{settings.identity.occupation}
</span>
</div>
@@ -61,7 +64,8 @@
<span class="size-4 text-zinc-600 dark:text-zinc-300">
{@html link.iconSvg}
</span>
<span class="whitespace-nowrap text-sm text-zinc-800 dark:text-zinc-100"
<span
class="whitespace-nowrap text-sm text-zinc-800 dark:text-zinc-100"
>{link.label}</span
>
</a>
@@ -75,7 +79,8 @@
<span class="size-4 text-zinc-600 dark:text-zinc-300">
{@html link.iconSvg}
</span>
<span class="whitespace-nowrap text-sm text-zinc-800 dark:text-zinc-100"
<span
class="whitespace-nowrap text-sm text-zinc-800 dark:text-zinc-100"
>{link.label}</span
>
</button>
@@ -88,7 +93,8 @@
<span class="size-4.5 text-zinc-600 dark:text-zinc-300">
{@html link.iconSvg}
</span>
<span class="whitespace-nowrap text-sm text-zinc-800 dark:text-zinc-100"
<span
class="whitespace-nowrap text-sm text-zinc-800 dark:text-zinc-100"
>{link.label}</span
>
</a>
@@ -100,8 +106,13 @@
class="flex items-center gap-x-1.5 px-1.5 py-1 rounded-sm bg-zinc-100 dark:bg-zinc-900 shadow-sm hover:bg-zinc-200 dark:hover:bg-zinc-800 transition-colors"
onclick={() => (pgpModalOpen = true)}
>
<MaterialSymbolsVpnKey class="size-4.5 text-zinc-600 dark:text-zinc-300" />
<span class="whitespace-nowrap text-sm text-zinc-800 dark:text-zinc-100">PGP Key</span>
<MaterialSymbolsVpnKey
class="size-4.5 text-zinc-600 dark:text-zinc-300"
/>
<span
class="whitespace-nowrap text-sm text-zinc-800 dark:text-zinc-100"
>PGP Key</span
>
</button>
</div>
</div>
+6 -2
View File
@@ -63,7 +63,9 @@
<div
class="flex items-center justify-between px-6 py-3.5 bg-admin-surface-hover/30 border-b border-admin-border"
>
<h2 class="text-sm font-medium text-admin-text-secondary">Recent Events</h2>
<h2 class="text-sm font-medium text-admin-text-secondary">
Recent Events
</h2>
<a
href={resolve("/admin/events")}
class="text-sm text-admin-accent hover:text-admin-accent-hover transition-colors"
@@ -73,7 +75,9 @@
</div>
{#if recentEvents.length === 0}
<p class="text-sm text-admin-text-muted text-center py-8">No events yet</p>
<p class="text-sm text-admin-text-muted text-center py-8">
No events yet
</p>
{:else}
<EventLog events={recentEvents} maxHeight="400px" />
{/if}
+3 -1
View File
@@ -84,7 +84,9 @@
<div
class="rounded-xl border border-admin-border bg-admin-surface/50 overflow-hidden shadow-sm shadow-black/10 dark:shadow-black/20"
>
<div class="px-6 py-3.5 bg-admin-surface-hover/30 border-b border-admin-border">
<div
class="px-6 py-3.5 bg-admin-surface-hover/30 border-b border-admin-border"
>
<h2 class="text-sm font-medium text-admin-text-secondary">
Event Log
<span class="text-admin-text-muted font-normal ml-2">
+3 -1
View File
@@ -43,7 +43,9 @@
<div class="flex min-h-screen items-center justify-center px-4">
<div class="w-full max-w-md space-y-4">
<!-- Login Form -->
<div class="rounded-lg bg-admin-surface border border-admin-border p-8 shadow-2xl shadow-black/10 dark:shadow-zinc-500/20">
<div
class="rounded-lg bg-admin-surface border border-admin-border p-8 shadow-2xl shadow-black/10 dark:shadow-zinc-500/20"
>
<form onsubmit={handleSubmit} class="space-y-6">
<Input
label="Username"
+24 -8
View File
@@ -84,7 +84,9 @@
<div class="flex items-center justify-between">
<div>
<h1 class="text-xl font-semibold text-admin-text">Projects</h1>
<p class="mt-1 text-sm text-admin-text-muted">Manage your project portfolio</p>
<p class="mt-1 text-sm text-admin-text-muted">
Manage your project portfolio
</p>
</div>
<Button variant="primary" href="/admin/projects/new">
<IconPlus class="w-4 h-4 mr-2" />
@@ -94,7 +96,9 @@
<!-- Projects Table -->
{#if loading}
<div class="text-center py-12 text-admin-text-muted">Loading projects...</div>
<div class="text-center py-12 text-admin-text-muted">
Loading projects...
</div>
{:else if projects.length === 0}
<div class="text-center py-12">
<p class="text-admin-text-muted mb-4">No projects yet</p>
@@ -106,19 +110,29 @@
<Table>
<thead class="bg-admin-surface/50">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium text-admin-text-muted">
<th
class="px-4 py-3 text-left text-xs font-medium text-admin-text-muted"
>
Name
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-admin-text-muted">
<th
class="px-4 py-3 text-left text-xs font-medium text-admin-text-muted"
>
Status
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-admin-text-muted">
<th
class="px-4 py-3 text-left text-xs font-medium text-admin-text-muted"
>
Tags
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-admin-text-muted">
<th
class="px-4 py-3 text-left text-xs font-medium text-admin-text-muted"
>
Updated
</th>
<th class="px-4 py-3 text-right text-xs font-medium text-admin-text-muted">
<th
class="px-4 py-3 text-right text-xs font-medium text-admin-text-muted"
>
Actions
</th>
</tr>
@@ -192,7 +206,9 @@
oncancel={cancelDelete}
>
{#if deleteTarget}
<div class="rounded-md bg-admin-surface-hover/50 border border-admin-border p-3">
<div
class="rounded-md bg-admin-surface-hover/50 border border-admin-border p-3"
>
<p class="font-medium text-admin-text">{deleteTarget.name}</p>
<p class="text-sm text-admin-text-secondary">{deleteTarget.slug}</p>
</div>
@@ -257,7 +257,11 @@
>
Cancel
</Button>
<Button variant="primary" onclick={handleSave} disabled={!hasChanges || saving}>
<Button
variant="primary"
onclick={handleSave}
disabled={!hasChanges || saving}
>
{saving ? "Saving..." : "Save Changes"}
</Button>
</div>
+24 -8
View File
@@ -231,19 +231,29 @@
<Table>
<thead class="bg-admin-surface/50">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium text-admin-text-muted">
<th
class="px-4 py-3 text-left text-xs font-medium text-admin-text-muted"
>
Name
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-admin-text-muted">
<th
class="px-4 py-3 text-left text-xs font-medium text-admin-text-muted"
>
Slug
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-admin-text-muted">
<th
class="px-4 py-3 text-left text-xs font-medium text-admin-text-muted"
>
Color
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-admin-text-muted">
<th
class="px-4 py-3 text-left text-xs font-medium text-admin-text-muted"
>
Projects
</th>
<th class="px-4 py-3 text-right text-xs font-medium text-admin-text-muted">
<th
class="px-4 py-3 text-right text-xs font-medium text-admin-text-muted"
>
Actions
</th>
</tr>
@@ -274,7 +284,9 @@
class="size-6 rounded border border-admin-border"
style="background-color: #{editColor}"
/>
<span class="text-xs text-admin-text-muted">#{editColor}</span>
<span class="text-xs text-admin-text-muted"
>#{editColor}</span
>
</div>
{:else}
<span class="text-xs text-admin-text-muted">No color</span>
@@ -318,7 +330,9 @@
class="size-6 rounded border border-admin-border"
style="background-color: #{tag.color}"
/>
<span class="text-xs text-admin-text-muted">#{tag.color}</span>
<span class="text-xs text-admin-text-muted"
>#{tag.color}</span
>
</div>
{:else}
<span class="text-xs text-admin-text-muted">No color</span>
@@ -364,7 +378,9 @@
oncancel={cancelDelete}
>
{#if deleteTarget}
<div class="rounded-md bg-admin-surface-hover/50 border border-admin-border p-3">
<div
class="rounded-md bg-admin-surface-hover/50 border border-admin-border p-3"
>
<p class="font-medium text-admin-text">{deleteTarget.name}</p>
<p class="text-sm text-admin-text-secondary">
Used in {deleteTarget.projectCount} project{deleteTarget.projectCount ===
+6 -6
View File
@@ -1,14 +1,14 @@
import { PGP_KEY_METADATA } from '$lib/pgp/key-info';
import { readFileSync } from 'fs';
import { join } from 'path';
import { PGP_KEY_METADATA } from "$lib/pgp/key-info";
import { readFileSync } from "fs";
import { join } from "path";
export const prerender = true;
export const load = () => {
// Read the PGP key from static directory at build time
const keyPath = join(process.cwd(), 'static', 'publickey.asc');
const content = readFileSync(keyPath, 'utf-8');
const keyPath = join(process.cwd(), "static", "publickey.asc");
const content = readFileSync(keyPath, "utf-8");
return {
key: {
...PGP_KEY_METADATA,
+53 -19
View File
@@ -26,7 +26,9 @@
async function copyCommand() {
try {
await navigator.clipboard.writeText("curl https://xevion.dev/pgp | gpg --import");
await navigator.clipboard.writeText(
"curl https://xevion.dev/pgp | gpg --import",
);
copyCommandSuccess = true;
setTimeout(() => {
copyCommandSuccess = false;
@@ -48,7 +50,10 @@
<svelte:head>
<title>PGP Public Key - Ryan Walters</title>
<meta name="description" content="Download or copy Ryan Walters' PGP public key" />
<meta
name="description"
content="Download or copy Ryan Walters' PGP public key"
/>
</svelte:head>
<AppWrapper class="overflow-x-hidden font-schibsted">
@@ -56,11 +61,14 @@
<div class="max-w-2xl w-full">
<!-- Header -->
<div class="mb-6">
<h1 class="text-2xl sm:text-3xl font-bold text-zinc-900 dark:text-white mb-2">
<h1
class="text-2xl sm:text-3xl font-bold text-zinc-900 dark:text-white mb-2"
>
PGP Public Key
</h1>
<p class="text-sm sm:text-base text-zinc-600 dark:text-zinc-400">
Use this key to send me encrypted messages or verify my signed content.
Use this key to send me encrypted messages or verify my signed
content.
</p>
</div>
@@ -68,13 +76,19 @@
<div
class="mb-6 p-3 sm:p-4 bg-zinc-100 dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700"
>
<div class="text-xs sm:text-sm font-semibold text-zinc-700 dark:text-zinc-300 mb-2">
<div
class="text-xs sm:text-sm font-semibold text-zinc-700 dark:text-zinc-300 mb-2"
>
Key Fingerprint
</div>
<div class="font-mono text-sm sm:text-base text-zinc-900 dark:text-zinc-100 break-all">
<div
class="font-mono text-sm sm:text-base text-zinc-900 dark:text-zinc-100 break-all"
>
{data.key.fingerprint}
</div>
<div class="mt-3 pt-3 border-t border-zinc-200 dark:border-zinc-700 space-y-1">
<div
class="mt-3 pt-3 border-t border-zinc-200 dark:border-zinc-700 space-y-1"
>
<div class="text-xs sm:text-sm text-zinc-600 dark:text-zinc-400">
<span class="font-medium">Key ID:</span>
<span class="font-mono ml-2">{data.key.keyId}</span>
@@ -90,8 +104,12 @@
<div
class="mb-6 border border-zinc-200 dark:border-zinc-700 rounded-lg overflow-hidden bg-white dark:bg-zinc-900"
>
<div class="px-3 sm:px-4 py-2 sm:py-3 bg-zinc-50 dark:bg-zinc-800 border-b border-zinc-200 dark:border-zinc-700">
<div class="text-xs sm:text-sm font-semibold text-zinc-700 dark:text-zinc-300">
<div
class="px-3 sm:px-4 py-2 sm:py-3 bg-zinc-50 dark:bg-zinc-800 border-b border-zinc-200 dark:border-zinc-700"
>
<div
class="text-xs sm:text-sm font-semibold text-zinc-700 dark:text-zinc-300"
>
Public Key
</div>
</div>
@@ -103,7 +121,8 @@
style="max-height: 400px"
>
<pre
class="p-3 sm:p-4 text-xs font-mono text-zinc-800 dark:text-zinc-200 bg-zinc-50 dark:bg-zinc-900/50 overflow-x-auto">{data.key.content}</pre>
class="p-3 sm:p-4 text-xs font-mono text-zinc-800 dark:text-zinc-200 bg-zinc-50 dark:bg-zinc-900/50 overflow-x-auto">{data
.key.content}</pre>
</OverlayScrollbarsComponent>
</div>
@@ -114,7 +133,9 @@
class="flex items-center justify-center gap-2 px-3 sm:px-4 py-2 sm:py-2.5 rounded-sm bg-zinc-900 dark:bg-zinc-100 text-white dark:text-zinc-900 hover:bg-zinc-800 dark:hover:bg-zinc-200 transition-colors shadow-sm"
>
<IconCopy class="size-4 sm:size-5" />
<span class="text-sm sm:text-base font-medium">{copySuccess ? "Copied!" : "Copy to Clipboard"}</span>
<span class="text-sm sm:text-base font-medium"
>{copySuccess ? "Copied!" : "Copy to Clipboard"}</span
>
</button>
<button
onclick={downloadKey}
@@ -126,31 +147,44 @@
</div>
<!-- Additional Info -->
<div class="mt-8 p-3 sm:p-4 bg-zinc-50 dark:bg-zinc-800/50 rounded-lg border border-zinc-200 dark:border-zinc-700">
<h2 class="text-xs sm:text-sm font-semibold text-zinc-700 dark:text-zinc-300 mb-2">
<div
class="mt-8 p-3 sm:p-4 bg-zinc-50 dark:bg-zinc-800/50 rounded-lg border border-zinc-200 dark:border-zinc-700"
>
<h2
class="text-xs sm:text-sm font-semibold text-zinc-700 dark:text-zinc-300 mb-2"
>
How to use this key
</h2>
<div class="text-xs sm:text-sm text-zinc-600 dark:text-zinc-400 space-y-2">
<div
class="text-xs sm:text-sm text-zinc-600 dark:text-zinc-400 space-y-2"
>
<p>
Import this key into your GPG keyring to encrypt messages for me or verify my signatures:
Import this key into your GPG keyring to encrypt messages for me or
verify my signatures:
</p>
<div class="relative">
<pre class="p-2 sm:p-3 pr-12 bg-white dark:bg-zinc-900 rounded border border-zinc-200 dark:border-zinc-700 font-mono text-xs overflow-x-auto">curl https://xevion.dev/pgp | gpg --import</pre>
<pre
class="p-2 sm:p-3 pr-12 bg-white dark:bg-zinc-900 rounded border border-zinc-200 dark:border-zinc-700 font-mono text-xs overflow-x-auto">curl https://xevion.dev/pgp | gpg --import</pre>
<button
onclick={copyCommand}
disabled={copyCommandSuccess}
class="absolute top-1/2 -translate-y-1/2 right-2 p-1 rounded border border-zinc-300 dark:border-zinc-600 bg-zinc-50 dark:bg-zinc-800 hover:bg-zinc-100 dark:hover:bg-zinc-700 hover:border-zinc-400 dark:hover:border-zinc-500 transition-all {copyCommandSuccess ? 'cursor-default' : 'cursor-pointer'}"
class="absolute top-1/2 -translate-y-1/2 right-2 p-1 rounded border border-zinc-300 dark:border-zinc-600 bg-zinc-50 dark:bg-zinc-800 hover:bg-zinc-100 dark:hover:bg-zinc-700 hover:border-zinc-400 dark:hover:border-zinc-500 transition-all {copyCommandSuccess
? 'cursor-default'
: 'cursor-pointer'}"
title={copyCommandSuccess ? "Copied!" : "Copy command"}
>
{#if copyCommandSuccess}
<IconCheck class="size-3.5 text-green-600 dark:text-green-500" />
<IconCheck
class="size-3.5 text-green-600 dark:text-green-500"
/>
{:else}
<IconCopy class="size-3.5 text-zinc-600 dark:text-zinc-400" />
{/if}
</button>
</div>
<p class="text-xs text-zinc-500 dark:text-zinc-500">
You can also find this key on public keyservers by searching for the fingerprint above.
You can also find this key on public keyservers by searching for the
fingerprint above.
</p>
</div>
</div>