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} -
-
-

{selectedIcon}

-
- -
- {/if} + + {#if selectedIcon} +
+
+ {#if selectedIconSvg} + + {@html selectedIconSvg} + {:else} +
+ {/if} +
+
+

{selectedIcon}

+
+ +
+ {/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)} - - {/each} -
+ + + + {/each} +
- {#if isLoading} -
- Loading... -
- {/if} -
- {: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 `${body}`; + const attributeString = Object.entries(attributes) + .map(([key, value]) => `${key}="${value}"`) + .join(" "); + + return `${body}`; } /** * 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(" { - const collections: IconCollection[] = []; + const collections: IconCollection[] = []; - // Load common collections to get metadata - for (const collectionId of PRE_CACHE_COLLECTIONS) { - const iconSet = await loadCollection(collectionId); - if (iconSet && iconSet.info) { - collections.push({ - id: collectionId, - name: iconSet.info.name || collectionId, - total: iconSet.info.total || Object.keys(iconSet.icons).length, - category: iconSet.info.category, - prefix: iconSet.prefix, - }); - } - } + // Load common collections to get metadata + for (const collectionId of PRE_CACHE_COLLECTIONS) { + const iconSet = await loadCollection(collectionId); + if (iconSet && iconSet.info) { + collections.push({ + id: collectionId, + name: iconSet.info.name || collectionId, + total: iconSet.info.total || Object.keys(iconSet.icons).length, + category: iconSet.info.category, + prefix: iconSet.prefix, + }); + } + } - return collections; + return collections; } /** * Search icons across collections */ export async function searchIcons( - query: string, - limit: number = 50, + query: string, + limit: number = 50, ): Promise<{ identifier: string; collection: string; name: string }[]> { - const results: { identifier: string; collection: string; name: string }[] = []; + const results: { identifier: string; collection: string; name: string }[] = + []; - // Parse query for collection prefix (e.g., "lucide:home" or "lucide:") - const colonIndex = query.indexOf(":"); - let targetCollection: string | null = null; - let searchTerm = query.toLowerCase(); + // Parse query for collection prefix (e.g., "lucide:home" or "lucide:") + const colonIndex = query.indexOf(":"); + let targetCollection: string | null = null; + let searchTerm = query.toLowerCase(); - if (colonIndex !== -1) { - targetCollection = query.substring(0, colonIndex); - searchTerm = query.substring(colonIndex + 1).toLowerCase(); - } + if (colonIndex !== -1) { + targetCollection = query.substring(0, colonIndex); + searchTerm = query.substring(colonIndex + 1).toLowerCase(); + } - // Determine which collections to search - const collectionsToSearch = targetCollection - ? [targetCollection] - : PRE_CACHE_COLLECTIONS; + // Determine which collections to search + const collectionsToSearch = targetCollection + ? [targetCollection] + : PRE_CACHE_COLLECTIONS; - for (const collectionId of collectionsToSearch) { - if (results.length >= limit) break; + for (const collectionId of collectionsToSearch) { + if (results.length >= limit) break; - const iconSet = await loadCollection(collectionId); - if (!iconSet) continue; + const iconSet = await loadCollection(collectionId); + if (!iconSet) continue; - const iconNames = Object.keys(iconSet.icons); + const iconNames = Object.keys(iconSet.icons); - for (const iconName of iconNames) { - if (results.length >= limit) break; + for (const iconName of iconNames) { + if (results.length >= limit) break; - // Search in icon name - if (searchTerm === "" || iconName.toLowerCase().includes(searchTerm)) { - results.push({ - identifier: `${collectionId}:${iconName}`, - collection: collectionId, - name: iconName, - }); - } - } - } + // Search in icon name + if (searchTerm === "" || iconName.toLowerCase().includes(searchTerm)) { + results.push({ + identifier: `${collectionId}:${iconName}`, + collection: collectionId, + name: iconName, + }); + } + } + } - return results; + return results; } /** * Pre-cache common icon collections on server startup */ export async function preCacheCollections(): Promise { - logger.info("Pre-caching icon collections...", { - collections: PRE_CACHE_COLLECTIONS, - }); + logger.info("Pre-caching icon collections...", { + collections: PRE_CACHE_COLLECTIONS, + }); - const promises = PRE_CACHE_COLLECTIONS.map((collection) => loadCollection(collection)); - await Promise.all(promises); + const promises = PRE_CACHE_COLLECTIONS.map((collection) => + loadCollection(collection), + ); + await Promise.all(promises); - logger.info("Icon collections pre-cached", { - cached: collectionCache.size, - }); + logger.info("Icon collections pre-cached", { + cached: collectionCache.size, + }); } // TODO: Future enhancement - Support color customization in icon identifiers diff --git a/web/src/lib/types/icons.ts b/web/src/lib/types/icons.ts index f0076c9..6940696 100644 --- a/web/src/lib/types/icons.ts +++ b/web/src/lib/types/icons.ts @@ -8,38 +8,38 @@ export type IconIdentifier = `${string}:${string}`; * Icon metadata for search results and picker */ export interface IconMetadata { - identifier: IconIdentifier; - collection: string; - name: string; - keywords?: string[]; + identifier: IconIdentifier; + collection: string; + name: string; + keywords?: string[]; } /** * Icon collection information */ export interface IconCollection { - id: string; - name: string; - total: number; - category?: string; - prefix: string; + id: string; + name: string; + total: number; + category?: string; + prefix: string; } /** * Full icon data with SVG */ export interface IconData { - identifier: IconIdentifier; - collection: string; - name: string; - svg: string; + identifier: IconIdentifier; + collection: string; + name: string; + svg: string; } /** * Options for rendering icon SVG */ export interface IconRenderOptions { - class?: string; - size?: number; - color?: string; + class?: string; + size?: number; + color?: string; } diff --git a/web/src/routes/+page.server.ts b/web/src/routes/+page.server.ts new file mode 100644 index 0000000..cfe0a31 --- /dev/null +++ b/web/src/routes/+page.server.ts @@ -0,0 +1,36 @@ +import type { PageServerLoad } from "./$types"; +import { MOCK_PROJECTS } from "$lib/mock-data/projects"; +import { renderIconSVG } from "$lib/server/icons"; + +// import { apiFetch } from '$lib/api.server'; +// import type { ApiProjectWithTags } from '$lib/admin-types'; + +export const load: PageServerLoad = async () => { + // TODO: Replace with real API data + // const projects = await apiFetch('/api/projects', { fetch }); + + // Pre-render icon SVGs for tags (server-side only) + const projectsWithIcons = await Promise.all( + MOCK_PROJECTS.map(async (project) => { + const tagsWithIcons = await Promise.all( + project.tags.map(async (tag) => ({ + ...tag, + iconSvg: (await renderIconSVG(tag.icon, { size: 12 })) || "", + })), + ); + + const clockIconSvg = + (await renderIconSVG("lucide:clock", { size: 12 })) || ""; + + return { + ...project, + tags: tagsWithIcons, + clockIconSvg, + }; + }), + ); + + return { + projects: projectsWithIcons, + }; +}; diff --git a/web/src/routes/+page.svelte b/web/src/routes/+page.svelte index 0b8889e..f2f16ce 100644 --- a/web/src/routes/+page.svelte +++ b/web/src/routes/+page.svelte @@ -1,10 +1,15 @@ @@ -24,7 +29,7 @@
-

+

A fanatical software engineer with expertise and passion for sound, scalable and high-performance applications. I'm always working on something new.
@@ -74,5 +79,13 @@

+ +
+
+ {#each projects as project (project.id)} + + {/each} +
+
diff --git a/web/src/routes/admin/settings/[[tab]]/+page.svelte b/web/src/routes/admin/settings/[[tab]]/+page.svelte index 7b7a8bb..cd90680 100644 --- a/web/src/routes/admin/settings/[[tab]]/+page.svelte +++ b/web/src/routes/admin/settings/[[tab]]/+page.svelte @@ -100,7 +100,6 @@ } function navigateToTab(tab: Tab) { - // eslint-disable-next-line svelte/no-navigation-without-resolve goto(`/admin/settings/${tab}`, { replaceState: true }); } diff --git a/web/src/routes/api/icons/[collection]/[name]/+server.ts b/web/src/routes/api/icons/[collection]/[name]/+server.ts index 3961fec..124cd21 100644 --- a/web/src/routes/api/icons/[collection]/[name]/+server.ts +++ b/web/src/routes/api/icons/[collection]/[name]/+server.ts @@ -4,17 +4,17 @@ import { requireAuth } from "$lib/server/auth"; import { getIcon } from "$lib/server/icons"; export const GET: RequestHandler = async (event) => { - // Require authentication - requireAuth(event); + // Require authentication + requireAuth(event); - const { collection, name } = event.params; - const identifier = `${collection}:${name}`; + const { collection, name } = event.params; + const identifier = `${collection}:${name}`; - const iconData = await getIcon(identifier); + const iconData = await getIcon(identifier); - if (!iconData) { - throw error(404, `Icon not found: ${identifier}`); - } + if (!iconData) { + throw error(404, `Icon not found: ${identifier}`); + } - return json(iconData); + return json(iconData); }; diff --git a/web/src/routes/api/icons/collections/+server.ts b/web/src/routes/api/icons/collections/+server.ts index 6b3766f..0c52135 100644 --- a/web/src/routes/api/icons/collections/+server.ts +++ b/web/src/routes/api/icons/collections/+server.ts @@ -4,13 +4,13 @@ import { requireAuth } from "$lib/server/auth"; import { getCollections } from "$lib/server/icons"; export const GET: RequestHandler = async (event) => { - // Require authentication - requireAuth(event); + // Require authentication + requireAuth(event); - const collections = await getCollections(); + const collections = await getCollections(); - return json({ - collections, - count: collections.length, - }); + return json({ + collections, + count: collections.length, + }); }; diff --git a/web/src/routes/api/icons/search/+server.ts b/web/src/routes/api/icons/search/+server.ts index c7fd1fa..772dad2 100644 --- a/web/src/routes/api/icons/search/+server.ts +++ b/web/src/routes/api/icons/search/+server.ts @@ -4,18 +4,18 @@ import { requireAuth } from "$lib/server/auth"; import { searchIcons } from "$lib/server/icons"; export const GET: RequestHandler = async (event) => { - // Require authentication - requireAuth(event); + // Require authentication + requireAuth(event); - const query = event.url.searchParams.get("q") || ""; - const limitParam = event.url.searchParams.get("limit"); - const limit = limitParam ? parseInt(limitParam, 10) : 50; + const query = event.url.searchParams.get("q") || ""; + const limitParam = event.url.searchParams.get("limit"); + const limit = limitParam ? parseInt(limitParam, 10) : 50; - const results = await searchIcons(query, limit); + const results = await searchIcons(query, limit); - return json({ - icons: results, - query, - count: results.length, - }); + return json({ + icons: results, + query, + count: results.length, + }); }; diff --git a/web/src/routes/projects/+page.server.ts b/web/src/routes/projects/+page.server.ts index 0a25272..2987048 100644 --- a/web/src/routes/projects/+page.server.ts +++ b/web/src/routes/projects/+page.server.ts @@ -20,7 +20,7 @@ export interface Project { export const load: PageServerLoad = async ({ url }) => { const projects = await apiFetch("/api/projects"); - + // Render icon SVGs server-side const projectsWithIcons = await Promise.all( projects.map(async (project) => ({ @@ -28,9 +28,9 @@ export const load: PageServerLoad = async ({ url }) => { iconSvg: await renderIconSVG(project.icon ?? "lucide:heart", { class: "text-3xl opacity-80 saturate-0", }), - })) + })), ); - + return { projects: projectsWithIcons, metadata: { diff --git a/web/src/routes/projects/+page.svelte b/web/src/routes/projects/+page.svelte index a06b97b..0177992 100644 --- a/web/src/routes/projects/+page.svelte +++ b/web/src/routes/projects/+page.svelte @@ -24,6 +24,7 @@ {@const links = project.links} {@const useAnchor = links.length > 0} {@const href = useAnchor ? links[0].url : undefined} + {@const iconSvg = project.iconSvg}
- {@html (project as any).iconSvg} + + {@html iconSvg}