mirror of
https://github.com/Xevion/xevion.dev.git
synced 2026-01-31 10:26:52 -06:00
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:
+3
-1
@@ -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
@@ -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>
|
||||
|
||||
@@ -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
@@ -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) => ({
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ===
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user