diff --git a/web/package.json b/web/package.json
index 69e67fc..8ae08e1 100644
--- a/web/package.json
+++ b/web/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"type": "module",
- "packageManager": "bun@latest",
+ "packageManager": "bun@1.3.5",
"scripts": {
"preinstall": "npx only-allow bun",
"dev": "bunx --bun vite dev",
diff --git a/web/src/lib/components/Icon.svelte b/web/src/lib/components/Icon.svelte
index f3a2360..2175b6b 100644
--- a/web/src/lib/components/Icon.svelte
+++ b/web/src/lib/components/Icon.svelte
@@ -1,29 +1,36 @@
{#await renderIconSVG(icon, { class: cn("inline-block", className), size })}
-
+
{:then svg}
- {#if svg}
- {@html svg}
- {:else}
-
- {#await renderIconSVG(fallback, { class: cn("inline-block", className), size }) then fallbackSvg}
- {@html fallbackSvg}
- {/await}
- {/if}
+ {#if svg}
+
+ {@html svg}
+ {:else}
+
+ {#await renderIconSVG( fallback, { class: cn("inline-block", className), size }, ) then fallbackSvg}
+
+ {@html fallbackSvg}
+ {/await}
+ {/if}
{/await}
diff --git a/web/src/lib/components/ProjectCard.svelte b/web/src/lib/components/ProjectCard.svelte
new file mode 100644
index 0000000..cbe6067
--- /dev/null
+++ b/web/src/lib/components/ProjectCard.svelte
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+ {project.name}
+
+
+ {formatDate(project.updatedAt)}
+
+
+
+ {project.description}
+
+
+
+
+ {#each project.tags as tag (tag.name)}
+
+
+ {#if tag.iconSvg}
+
+
+ {@html tag.iconSvg}
+
+ {/if}
+ {tag.name}
+
+ {/each}
+
+
diff --git a/web/src/lib/components/admin/IconPicker.svelte b/web/src/lib/components/admin/IconPicker.svelte
index ad1b794..129f559 100644
--- a/web/src/lib/components/admin/IconPicker.svelte
+++ b/web/src/lib/components/admin/IconPicker.svelte
@@ -1,325 +1,334 @@
- {#if label}
-
- {/if}
+ {#if label}
+
+ {/if}
-
- {#if selectedIcon}
-
-
- {#if selectedIconSvg}
- {@html selectedIconSvg}
- {:else}
-
- {/if}
-
-
-
-
- {/if}
+
+ {#if selectedIcon}
+
+
+ {#if selectedIconSvg}
+
+ {@html selectedIconSvg}
+ {:else}
+
+ {/if}
+
+
+
+
+ {/if}
-
-
-
- {#each collections as collection (collection.id)}
-
- {/each}
-
+
+
+
+ {#each collections as collection (collection.id)}
+
+ {/each}
+
-
-
-
+
+
+
-
- {#if showDropdown && searchResults.length > 0}
-
-
-
- {#each searchResults as result (result.identifier)}
- {@const cachedSvg = iconSvgCache.get(result.identifier)}
-
- {:else if showDropdown && searchQuery && !isLoading}
-
- No icons found for "{searchQuery}"
-
- {/if}
-
+ {#if isLoading}
+
+ Loading...
+
+ {/if}
+
+ {:else if showDropdown && searchQuery && !isLoading}
+
+ No icons found for "{searchQuery}"
+
+ {/if}
+
-
- Tip: Use "collection:search" to filter (e.g., "lucide:home" or "simple-icons:react")
-
+
+ Tip: Use "collection:search" to filter (e.g., "lucide:home" or
+ "simple-icons:react")
+
@@ -327,14 +336,14 @@
diff --git a/web/src/lib/mock-data/projects.ts b/web/src/lib/mock-data/projects.ts
new file mode 100644
index 0000000..63532f1
--- /dev/null
+++ b/web/src/lib/mock-data/projects.ts
@@ -0,0 +1,97 @@
+export interface MockProjectTag {
+ name: string;
+ icon: string; // Icon identifier like "simple-icons:rust"
+ iconSvg?: string; // Pre-rendered SVG (populated server-side)
+}
+
+export interface MockProject {
+ id: string;
+ name: string;
+ description: string;
+ url: string;
+ tags: MockProjectTag[];
+ updatedAt: string;
+ clockIconSvg?: string; // Pre-rendered clock icon for "Updated" text
+}
+
+export const MOCK_PROJECTS: MockProject[] = [
+ {
+ id: "1",
+ name: "xevion.dev",
+ description:
+ "Personal portfolio showcasing projects and technical expertise. Built with Rust backend, SvelteKit frontend, and PostgreSQL.",
+ url: "https://github.com/Xevion/xevion.dev",
+ tags: [
+ { name: "Rust", icon: "simple-icons:rust" },
+ { name: "SvelteKit", icon: "simple-icons:svelte" },
+ { name: "PostgreSQL", icon: "cib:postgresql" },
+ ],
+ updatedAt: "2026-01-06T22:12:37Z",
+ },
+ {
+ id: "2",
+ name: "historee",
+ description:
+ "Powerful browser history analyzer for visualizing and understanding web browsing patterns across multiple browsers.",
+ url: "https://github.com/Xevion/historee",
+ tags: [
+ { name: "Rust", icon: "simple-icons:rust" },
+ { name: "CLI", icon: "lucide:terminal" },
+ { name: "Analytics", icon: "lucide:bar-chart-3" },
+ ],
+ updatedAt: "2026-01-06T06:01:27Z",
+ },
+ {
+ id: "3",
+ name: "satori-html",
+ description:
+ "HTML adapter for Vercel's Satori library, enabling generation of beautiful social card images from HTML markup.",
+ url: "https://github.com/Xevion/satori-html",
+ tags: [
+ { name: "TypeScript", icon: "simple-icons:typescript" },
+ { name: "NPM", icon: "simple-icons:npm" },
+ { name: "Graphics", icon: "lucide:image" },
+ ],
+ updatedAt: "2026-01-05T20:23:07Z",
+ },
+ {
+ id: "4",
+ name: "byte-me",
+ description:
+ "Cross-platform media bitrate visualizer with real-time analysis. Built with Tauri for native performance and modern UI.",
+ url: "https://github.com/Xevion/byte-me",
+ tags: [
+ { name: "Rust", icon: "simple-icons:rust" },
+ { name: "Tauri", icon: "simple-icons:tauri" },
+ { name: "Desktop", icon: "lucide:monitor" },
+ { name: "Media", icon: "lucide:video" },
+ ],
+ updatedAt: "2026-01-05T05:09:09Z",
+ },
+ {
+ id: "5",
+ name: "rdap",
+ description:
+ "Modern RDAP query client for domain registration data lookup. Clean interface built with static Next.js for instant loads.",
+ url: "https://github.com/Xevion/rdap",
+ tags: [
+ { name: "TypeScript", icon: "simple-icons:typescript" },
+ { name: "Next.js", icon: "simple-icons:nextdotjs" },
+ { name: "Networking", icon: "lucide:network" },
+ ],
+ updatedAt: "2026-01-05T10:36:55Z",
+ },
+ {
+ id: "6",
+ name: "rebinded",
+ description:
+ "Cross-platform key remapping daemon with per-application context awareness and intelligent stateful debouncing.",
+ url: "https://github.com/Xevion/rebinded",
+ tags: [
+ { name: "Rust", icon: "simple-icons:rust" },
+ { name: "System", icon: "lucide:settings-2" },
+ { name: "Cross-platform", icon: "lucide:globe" },
+ ],
+ updatedAt: "2026-01-01T00:34:09Z",
+ },
+];
diff --git a/web/src/lib/server/auth.ts b/web/src/lib/server/auth.ts
index 9634eaa..27d8ff4 100644
--- a/web/src/lib/server/auth.ts
+++ b/web/src/lib/server/auth.ts
@@ -6,13 +6,13 @@ import type { RequestEvent } from "@sveltejs/kit";
* Returns the username if authenticated, throws 401 error if not
*/
export function requireAuth(event: RequestEvent): string {
- const sessionUser = event.request.headers.get("x-session-user");
+ const sessionUser = event.request.headers.get("x-session-user");
- if (!sessionUser) {
- throw error(401, "Unauthorized");
- }
+ if (!sessionUser) {
+ throw error(401, "Unauthorized");
+ }
- return sessionUser;
+ return sessionUser;
}
/**
@@ -20,5 +20,5 @@ export function requireAuth(event: RequestEvent): string {
* Returns the username if authenticated, null if not
*/
export function getAuth(event: RequestEvent): string | null {
- return event.request.headers.get("x-session-user");
+ return event.request.headers.get("x-session-user");
}
diff --git a/web/src/lib/server/icons.ts b/web/src/lib/server/icons.ts
index a146226..b7b056e 100644
--- a/web/src/lib/server/icons.ts
+++ b/web/src/lib/server/icons.ts
@@ -3,7 +3,12 @@ import { join } from "path";
import type { IconifyJSON } from "@iconify/types";
import { getIconData, iconToSVG, replaceIDs } from "@iconify/utils";
import { getLogger } from "@logtape/logtape";
-import type { IconCollection, IconData, IconIdentifier, IconRenderOptions } from "$lib/types/icons";
+import type {
+ IconCollection,
+ IconData,
+ IconIdentifier,
+ IconRenderOptions,
+} from "$lib/types/icons";
const logger = getLogger(["server", "icons"]);
@@ -12,11 +17,11 @@ const collectionCache = new Map();
// Collections to pre-cache on server startup
const PRE_CACHE_COLLECTIONS = [
- "lucide",
- "simple-icons",
- "material-symbols",
- "heroicons",
- "feather",
+ "lucide",
+ "simple-icons",
+ "material-symbols",
+ "heroicons",
+ "feather",
];
// Default fallback icon
@@ -25,236 +30,245 @@ const DEFAULT_FALLBACK_ICON = "lucide:help-circle";
/**
* Parse icon identifier into collection and name
*/
-function parseIdentifier(identifier: string): { collection: string; name: string } | null {
- const parts = identifier.split(":");
- if (parts.length !== 2) {
- return null;
- }
- return { collection: parts[0], name: parts[1] };
+function parseIdentifier(
+ identifier: string,
+): { collection: string; name: string } | null {
+ const parts = identifier.split(":");
+ if (parts.length !== 2) {
+ return null;
+ }
+ return { collection: parts[0], name: parts[1] };
}
/**
* Load icon collection from @iconify/json
*/
async function loadCollection(collection: string): Promise {
- // Check cache first
- if (collectionCache.has(collection)) {
- return collectionCache.get(collection)!;
- }
+ // Check cache first
+ if (collectionCache.has(collection)) {
+ return collectionCache.get(collection)!;
+ }
- try {
- const iconifyJsonPath = join(
- process.cwd(),
- "node_modules",
- "@iconify",
- "json",
- "json",
- `${collection}.json`,
- );
+ try {
+ const iconifyJsonPath = join(
+ process.cwd(),
+ "node_modules",
+ "@iconify",
+ "json",
+ "json",
+ `${collection}.json`,
+ );
- const data = await readFile(iconifyJsonPath, "utf-8");
- const iconSet: IconifyJSON = JSON.parse(data);
+ const data = await readFile(iconifyJsonPath, "utf-8");
+ const iconSet: IconifyJSON = JSON.parse(data);
- // Cache the collection
- collectionCache.set(collection, iconSet);
+ // Cache the collection
+ collectionCache.set(collection, iconSet);
- logger.debug(`Loaded icon collection: ${collection}`, {
- total: iconSet.info?.total || Object.keys(iconSet.icons).length,
- });
+ logger.debug(`Loaded icon collection: ${collection}`, {
+ total: iconSet.info?.total || Object.keys(iconSet.icons).length,
+ });
- return iconSet;
- } catch (error) {
- logger.warn(`Failed to load icon collection: ${collection}`, {
- error: error instanceof Error ? error.message : String(error),
- });
- return null;
- }
+ return iconSet;
+ } catch (error) {
+ logger.warn(`Failed to load icon collection: ${collection}`, {
+ error: error instanceof Error ? error.message : String(error),
+ });
+ return null;
+ }
}
/**
* Get icon data by identifier
*/
export async function getIcon(identifier: string): Promise {
- const parsed = parseIdentifier(identifier);
- if (!parsed) {
- logger.warn(`Invalid icon identifier: ${identifier}`);
- return null;
- }
+ const parsed = parseIdentifier(identifier);
+ if (!parsed) {
+ logger.warn(`Invalid icon identifier: ${identifier}`);
+ return null;
+ }
- const { collection, name } = parsed;
- const iconSet = await loadCollection(collection);
+ const { collection, name } = parsed;
+ const iconSet = await loadCollection(collection);
- if (!iconSet) {
- return null;
- }
+ if (!iconSet) {
+ return null;
+ }
- // Get icon data from the set
- const iconData = getIconData(iconSet, name);
- if (!iconData) {
- logger.warn(`Icon not found: ${identifier}`);
- return null;
- }
+ // Get icon data from the set
+ const iconData = getIconData(iconSet, name);
+ if (!iconData) {
+ logger.warn(`Icon not found: ${identifier}`);
+ return null;
+ }
- // Build SVG
- const svg = renderIconData(iconData, iconSet);
+ // Build SVG
+ const svg = renderIconData(iconData);
- return {
- identifier: identifier as IconIdentifier,
- collection,
- name,
- svg,
- };
+ return {
+ identifier: identifier as IconIdentifier,
+ collection,
+ name,
+ svg,
+ };
}
/**
* Render icon data to SVG string
*/
-function renderIconData(iconData: any, iconSet: IconifyJSON): string {
- // Convert icon data to SVG attributes
- const renderData = iconToSVG(iconData);
+function renderIconData(iconData: ReturnType): string {
+ if (!iconData) {
+ throw new Error("Icon data is null");
+ }
- // Get SVG body
- const body = replaceIDs(iconData.body);
+ // Convert icon data to SVG attributes
+ const renderData = iconToSVG(iconData);
- // Build SVG element
- const attributes = {
- ...renderData.attributes,
- xmlns: "http://www.w3.org/2000/svg",
- "xmlns:xlink": "http://www.w3.org/1999/xlink",
- };
+ // Get SVG body
+ const body = replaceIDs(iconData.body);
- const attributeString = Object.entries(attributes)
- .map(([key, value]) => `${key}="${value}"`)
- .join(" ");
+ // Build SVG element
+ const attributes = {
+ ...renderData.attributes,
+ xmlns: "http://www.w3.org/2000/svg",
+ "xmlns:xlink": "http://www.w3.org/1999/xlink",
+ };
- return ``;
+ const attributeString = Object.entries(attributes)
+ .map(([key, value]) => `${key}="${value}"`)
+ .join(" ");
+
+ return ``;
}
/**
* Render icon SVG with custom options
*/
export async function renderIconSVG(
- identifier: string,
- options: IconRenderOptions = {},
+ identifier: string,
+ options: IconRenderOptions = {},
): Promise {
- const iconData = await getIcon(identifier);
+ const iconData = await getIcon(identifier);
- if (!iconData) {
- // Try fallback icon if provided, otherwise use default
- if (identifier !== DEFAULT_FALLBACK_ICON) {
- logger.warn(`Icon not found, using fallback: ${identifier}`);
- return renderIconSVG(DEFAULT_FALLBACK_ICON, options);
- }
- return null;
- }
+ if (!iconData) {
+ // Try fallback icon if provided, otherwise use default
+ if (identifier !== DEFAULT_FALLBACK_ICON) {
+ logger.warn(`Icon not found, using fallback: ${identifier}`);
+ return renderIconSVG(DEFAULT_FALLBACK_ICON, options);
+ }
+ return null;
+ }
- let svg = iconData.svg;
+ let svg = iconData.svg;
- // Apply custom class
- if (options.class) {
- svg = svg.replace("