From b3dd1954d3d68d040731c0239719c383f6e765c7 Mon Sep 17 00:00:00 2001 From: Xevion Date: Tue, 6 Jan 2026 14:39:21 -0600 Subject: [PATCH] refactor: formatting and accessibility improvements across admin components - Enforce consistent code formatting via ESLint rules - Add accessibility enhancements (input IDs, labels, ARIA attributes) - Replace Svelte store initialization patterns with reactive $effect - Use $derived for reactive computed values - Update error-codes and og-types indentation to spaces - Add TypeScript definitions for html-minifier-terser - Use resolve() for internal links in error and login pages --- web/eslint.config.js | 10 ++ web/src/html-minifier-terser.d.ts | 22 +++ web/src/lib/admin-types.ts | 7 +- web/src/lib/api.ts | 135 ++++++++++++++---- web/src/lib/components/admin/Button.svelte | 11 +- web/src/lib/components/admin/EventLog.svelte | 22 ++- web/src/lib/components/admin/Input.svelte | 34 +++-- web/src/lib/components/admin/Modal.svelte | 8 +- .../lib/components/admin/ProjectForm.svelte | 70 ++++----- web/src/lib/components/admin/Sidebar.svelte | 22 ++- web/src/lib/components/admin/Table.svelte | 4 +- web/src/lib/components/admin/TagPicker.svelte | 16 ++- web/src/lib/error-codes.ts | 50 +++---- web/src/lib/og-types.ts | 26 ++-- web/src/routes/+error.svelte | 5 +- web/src/routes/admin/+layout.server.ts | 9 +- web/src/routes/admin/+layout.svelte | 14 +- web/src/routes/admin/+page.svelte | 23 ++- web/src/routes/admin/events/+page.svelte | 24 ++-- web/src/routes/admin/login/+page.svelte | 9 +- web/src/routes/admin/projects/+page.svelte | 20 ++- .../routes/admin/projects/[id]/+page.svelte | 36 +++-- .../routes/admin/projects/new/+page.svelte | 27 ++-- .../admin/settings/[[tab]]/+page.svelte | 31 ++-- web/src/routes/admin/tags/+page.svelte | 39 +++-- web/src/routes/errors/[code]/+page.server.ts | 12 +- web/src/routes/errors/[code]/+page.svelte | 1 - web/src/routes/internal/ogp/+page.svelte | 3 +- web/vite-plugin-json-logger.ts | 2 +- 29 files changed, 446 insertions(+), 246 deletions(-) create mode 100644 web/src/html-minifier-terser.d.ts diff --git a/web/eslint.config.js b/web/eslint.config.js index 7d709cd..562fb7a 100644 --- a/web/eslint.config.js +++ b/web/eslint.config.js @@ -25,6 +25,16 @@ export default ts.config( parser: ts.parser, }, }, + rules: { + // Disable resolve() requirement for dynamic hrefs in components + "svelte/no-navigation-without-resolve": "off", + }, + }, + { + files: ["**/*.svelte.ts"], + languageOptions: { + parser: ts.parser, + }, }, { ignores: ["build/", ".svelte-kit/", "dist/"], diff --git a/web/src/html-minifier-terser.d.ts b/web/src/html-minifier-terser.d.ts new file mode 100644 index 0000000..00a769e --- /dev/null +++ b/web/src/html-minifier-terser.d.ts @@ -0,0 +1,22 @@ +declare module "html-minifier-terser" { + export interface Options { + collapseBooleanAttributes?: boolean; + collapseWhitespace?: boolean; + conservativeCollapse?: boolean; + decodeEntities?: boolean; + html5?: boolean; + ignoreCustomComments?: RegExp[]; + minifyCSS?: boolean; + minifyJS?: boolean; + removeAttributeQuotes?: boolean; + removeComments?: boolean; + removeOptionalTags?: boolean; + removeRedundantAttributes?: boolean; + removeScriptTypeAttributes?: boolean; + removeStyleLinkTypeAttributes?: boolean; + sortAttributes?: boolean; + sortClassName?: boolean; + } + + export function minify(html: string, options?: Options): string; +} diff --git a/web/src/lib/admin-types.ts b/web/src/lib/admin-types.ts index 9aab3eb..914c9ac 100644 --- a/web/src/lib/admin-types.ts +++ b/web/src/lib/admin-types.ts @@ -78,7 +78,12 @@ export interface AuthSession { expiresAt: string; // ISO 8601 } -export type SocialPlatform = "github" | "linkedin" | "discord" | "email" | "pgp"; +export type SocialPlatform = + | "github" + | "linkedin" + | "discord" + | "email" + | "pgp"; export interface SocialLink { id: string; diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index 0b423d3..6443095 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -9,9 +9,6 @@ import type { CreateTagData, UpdateTagData, SiteSettings, - SiteIdentity, - SocialLink, - AdminPreferences, } from "./admin-types"; // ============================================================================ @@ -19,30 +16,110 @@ import type { // ============================================================================ // Mock data storage (in-memory for now) -let MOCK_TAGS: AdminTag[] = [ - { id: "tag-1", slug: "rust", name: "Rust", createdAt: "2024-01-15T10:00:00Z" }, - { id: "tag-2", slug: "typescript", name: "TypeScript", createdAt: "2024-01-16T10:00:00Z" }, +const MOCK_TAGS: AdminTag[] = [ + { + id: "tag-1", + slug: "rust", + name: "Rust", + createdAt: "2024-01-15T10:00:00Z", + }, + { + id: "tag-2", + slug: "typescript", + name: "TypeScript", + createdAt: "2024-01-16T10:00:00Z", + }, { id: "tag-3", slug: "web", name: "Web", createdAt: "2024-01-17T10:00:00Z" }, { id: "tag-4", slug: "cli", name: "CLI", createdAt: "2024-01-18T10:00:00Z" }, { id: "tag-5", slug: "api", name: "API", createdAt: "2024-01-19T10:00:00Z" }, - { id: "tag-6", slug: "database", name: "Database", createdAt: "2024-01-20T10:00:00Z" }, - { id: "tag-7", slug: "svelte", name: "Svelte", createdAt: "2024-01-21T10:00:00Z" }, - { id: "tag-8", slug: "python", name: "Python", createdAt: "2024-01-22T10:00:00Z" }, - { id: "tag-9", slug: "machine-learning", name: "Machine Learning", createdAt: "2024-01-23T10:00:00Z" }, - { id: "tag-10", slug: "docker", name: "Docker", createdAt: "2024-01-24T10:00:00Z" }, - { id: "tag-11", slug: "kubernetes", name: "Kubernetes", createdAt: "2024-01-25T10:00:00Z" }, - { id: "tag-12", slug: "react", name: "React", createdAt: "2024-01-26T10:00:00Z" }, - { id: "tag-13", slug: "nextjs", name: "Next.js", createdAt: "2024-01-27T10:00:00Z" }, - { id: "tag-14", slug: "tailwind", name: "Tailwind CSS", createdAt: "2024-01-28T10:00:00Z" }, - { id: "tag-15", slug: "graphql", name: "GraphQL", createdAt: "2024-01-29T10:00:00Z" }, - { id: "tag-16", slug: "postgres", name: "PostgreSQL", createdAt: "2024-01-30T10:00:00Z" }, - { id: "tag-17", slug: "redis", name: "Redis", createdAt: "2024-01-31T10:00:00Z" }, + { + id: "tag-6", + slug: "database", + name: "Database", + createdAt: "2024-01-20T10:00:00Z", + }, + { + id: "tag-7", + slug: "svelte", + name: "Svelte", + createdAt: "2024-01-21T10:00:00Z", + }, + { + id: "tag-8", + slug: "python", + name: "Python", + createdAt: "2024-01-22T10:00:00Z", + }, + { + id: "tag-9", + slug: "machine-learning", + name: "Machine Learning", + createdAt: "2024-01-23T10:00:00Z", + }, + { + id: "tag-10", + slug: "docker", + name: "Docker", + createdAt: "2024-01-24T10:00:00Z", + }, + { + id: "tag-11", + slug: "kubernetes", + name: "Kubernetes", + createdAt: "2024-01-25T10:00:00Z", + }, + { + id: "tag-12", + slug: "react", + name: "React", + createdAt: "2024-01-26T10:00:00Z", + }, + { + id: "tag-13", + slug: "nextjs", + name: "Next.js", + createdAt: "2024-01-27T10:00:00Z", + }, + { + id: "tag-14", + slug: "tailwind", + name: "Tailwind CSS", + createdAt: "2024-01-28T10:00:00Z", + }, + { + id: "tag-15", + slug: "graphql", + name: "GraphQL", + createdAt: "2024-01-29T10:00:00Z", + }, + { + id: "tag-16", + slug: "postgres", + name: "PostgreSQL", + createdAt: "2024-01-30T10:00:00Z", + }, + { + id: "tag-17", + slug: "redis", + name: "Redis", + createdAt: "2024-01-31T10:00:00Z", + }, { id: "tag-18", slug: "aws", name: "AWS", createdAt: "2024-02-01T10:00:00Z" }, - { id: "tag-19", slug: "devops", name: "DevOps", createdAt: "2024-02-02T10:00:00Z" }, - { id: "tag-20", slug: "security", name: "Security", createdAt: "2024-02-03T10:00:00Z" }, + { + id: "tag-19", + slug: "devops", + name: "DevOps", + createdAt: "2024-02-02T10:00:00Z", + }, + { + id: "tag-20", + slug: "security", + name: "Security", + createdAt: "2024-02-03T10:00:00Z", + }, ]; -let MOCK_PROJECTS: AdminProject[] = [ +const MOCK_PROJECTS: AdminProject[] = [ { id: "proj-1", slug: "portfolio-site", @@ -167,7 +244,8 @@ let MOCK_PROJECTS: AdminProject[] = [ id: "proj-9", slug: "security-scanner", title: "Security Scanner", - description: "Automated security vulnerability scanner for web applications", + description: + "Automated security vulnerability scanner for web applications", status: "active", githubRepo: "xevion/sec-scanner", demoUrl: null, @@ -197,7 +275,8 @@ let MOCK_PROJECTS: AdminProject[] = [ id: "proj-11", slug: "deployment-tools", title: "Deployment Automation Tools", - description: "CLI tools for automated deployments to multiple cloud providers", + description: + "CLI tools for automated deployments to multiple cloud providers", status: "active", githubRepo: "xevion/deploy-tools", demoUrl: null, @@ -270,7 +349,7 @@ let MOCK_PROJECTS: AdminProject[] = [ }, ]; -let MOCK_EVENTS: AdminEvent[] = [ +const MOCK_EVENTS: AdminEvent[] = [ { id: "evt-1", timestamp: "2025-01-06T10:30:00Z", @@ -455,7 +534,9 @@ export async function getAdminProjects(): Promise { return [...MOCK_PROJECTS].sort((a, b) => b.priority - a.priority); } -export async function getAdminProject(id: string): Promise { +export async function getAdminProject( + id: string, +): Promise { // TODO: Replace with apiFetch(`/admin/api/projects/${id}`) when backend ready await new Promise((resolve) => setTimeout(resolve, 50)); return MOCK_PROJECTS.find((p) => p.id === id) || null; @@ -743,7 +824,9 @@ export async function getSettings(): Promise { return structuredClone(MOCK_SETTINGS); } -export async function updateSettings(settings: SiteSettings): Promise { +export async function updateSettings( + settings: SiteSettings, +): Promise { // TODO: Replace with apiFetch('/admin/api/settings', { method: 'PUT', body: JSON.stringify(settings) }) await new Promise((resolve) => setTimeout(resolve, 200)); diff --git a/web/src/lib/components/admin/Button.svelte b/web/src/lib/components/admin/Button.svelte index eedfdff..27ac5eb 100644 --- a/web/src/lib/components/admin/Button.svelte +++ b/web/src/lib/components/admin/Button.svelte @@ -33,8 +33,7 @@ "bg-transparent text-admin-text border border-zinc-700 hover:border-zinc-600 hover:bg-zinc-800/50 focus-visible:ring-zinc-500", 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-zinc-800/50 focus-visible:ring-zinc-500", + ghost: "text-admin-text hover:bg-zinc-800/50 focus-visible:ring-zinc-500", }; const sizeStyles = { @@ -47,7 +46,13 @@ {#if href} {@render children?.()} diff --git a/web/src/lib/components/admin/EventLog.svelte b/web/src/lib/components/admin/EventLog.svelte index 7a860a0..74a781b 100644 --- a/web/src/lib/components/admin/EventLog.svelte +++ b/web/src/lib/components/admin/EventLog.svelte @@ -34,28 +34,30 @@
- {#each events as event} + {#each events as event (event.id)} {@const levelColors = { info: "text-cyan-500/60", warning: "text-amber-500/70", - error: "text-rose-500/70" + error: "text-rose-500/70", }} {@const levelLabels = { info: "INFO", warning: "WARN", - error: "ERR" + error: "ERR", }}
- + {levelLabels[event.level]} @@ -82,9 +84,15 @@
{#if showMetadata && expandedEventId === event.id && event.metadata}
-
+

Metadata:

-
{JSON.stringify(event.metadata, null, 2)}
+
{JSON.stringify(
+                  event.metadata,
+                  null,
+                  2,
+                )}
{/if} diff --git a/web/src/lib/components/admin/Input.svelte b/web/src/lib/components/admin/Input.svelte index 4b9b33d..8b308d7 100644 --- a/web/src/lib/components/admin/Input.svelte +++ b/web/src/lib/components/admin/Input.svelte @@ -3,7 +3,14 @@ interface Props { label?: string; - type?: "text" | "number" | "email" | "password" | "url" | "textarea" | "select"; + type?: + | "text" + | "number" + | "email" + | "password" + | "url" + | "textarea" + | "select"; value: string | number; placeholder?: string; disabled?: boolean; @@ -17,9 +24,9 @@ } let { - label, type = "text", - value = $bindable(""), + value = $bindable(), + label, placeholder, disabled = false, required = false, @@ -31,15 +38,21 @@ oninput, }: Props = $props(); + // Generate unique ID for accessibility + const inputId = `input-${Math.random().toString(36).substring(2, 11)}`; + const inputStyles = "block w-full rounded-md border border-zinc-700 bg-zinc-900 px-3 py-2 text-sm text-zinc-200 placeholder:text-zinc-500 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 disabled:cursor-not-allowed disabled:opacity-50 transition-colors"; - const errorStyles = error - ? "border-red-500 focus:border-red-500 focus:ring-red-500" - : ""; + const errorStyles = $derived( + error ? "border-red-500 focus:border-red-500 focus:ring-red-500" : "", + ); function handleInput(e: Event) { - const target = e.target as HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement; + const target = e.target as + | HTMLInputElement + | HTMLTextAreaElement + | HTMLSelectElement; const newValue = type === "number" ? Number(target.value) : target.value; value = newValue; oninput?.(newValue); @@ -48,7 +61,7 @@
{#if label} -