mirror of
https://github.com/Xevion/xevion.dev.git
synced 2026-01-31 10:26:52 -06:00
feat: add icon picker for tags with Iconify integration
- Add icon field to tag creation/update API and handlers - Install @iconify packages (json, types, utils) as production deps - Build IconPicker component for tag admin UI - Fix apiFetch lazy initialization for build-time safety - Update Docker to install production dependencies for SSR runtime
This commit is contained in:
+4
-2
@@ -8,6 +8,9 @@
|
||||
"@fontsource-variable/schibsted-grotesk": "^5.2.8",
|
||||
"@fontsource/hanken-grotesk": "^5.1.0",
|
||||
"@fontsource/schibsted-grotesk": "^5.2.8",
|
||||
"@iconify/json": "^2.2.425",
|
||||
"@iconify/types": "^2.0.0",
|
||||
"@iconify/utils": "^3.1.0",
|
||||
"@logtape/logtape": "^1.3.5",
|
||||
"@resvg/resvg-js": "^2.6.2",
|
||||
"@xevion/satori-html": "^0.4.1",
|
||||
@@ -20,7 +23,6 @@
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
"@iconify/json": "^2.2.424",
|
||||
"@sveltejs/kit": "^2.21.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
@@ -140,7 +142,7 @@
|
||||
|
||||
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
|
||||
|
||||
"@iconify/json": ["@iconify/json@2.2.424", "", { "dependencies": { "@iconify/types": "*", "pathe": "^2.0.3" } }, "sha512-Lxx8ad2DgyTGV88ec0mlaJ+OSjr0SyU0j8Awfbxl9JrxxHmBEFQJ+jywhztWAhLnaMUG3+G1htNJYzEsoAsNMQ=="],
|
||||
"@iconify/json": ["@iconify/json@2.2.425", "", { "dependencies": { "@iconify/types": "*", "pathe": "^2.0.3" } }, "sha512-RJcJeoLFAmKPr7e7bP7gw33ASBSONuFsiCg35cEvrf/s8DDG5+C9eSqOvIiggNwZwwjYeGNKIJ7RduJTWgN0IQ=="],
|
||||
|
||||
"@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="],
|
||||
|
||||
|
||||
+3
-1
@@ -20,6 +20,9 @@
|
||||
"@fontsource-variable/schibsted-grotesk": "^5.2.8",
|
||||
"@fontsource/hanken-grotesk": "^5.1.0",
|
||||
"@fontsource/schibsted-grotesk": "^5.2.8",
|
||||
"@iconify/json": "^2.2.425",
|
||||
"@iconify/types": "^2.0.0",
|
||||
"@iconify/utils": "^3.1.0",
|
||||
"@logtape/logtape": "^1.3.5",
|
||||
"@resvg/resvg-js": "^2.6.2",
|
||||
"@xevion/satori-html": "^0.4.1",
|
||||
@@ -32,7 +35,6 @@
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
"@iconify/json": "^2.2.424",
|
||||
"@sveltejs/kit": "^2.21.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
|
||||
@@ -49,6 +49,7 @@ export interface UpdateProjectData extends CreateProjectData {
|
||||
export interface CreateTagData {
|
||||
name: string;
|
||||
slug?: string;
|
||||
icon?: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,13 +15,7 @@ interface BunFetchOptions extends RequestInit {
|
||||
* Create a socket-aware fetch function
|
||||
* Automatically handles Unix socket vs TCP based on UPSTREAM_URL
|
||||
*/
|
||||
function createSmartFetch(upstreamUrl: string | undefined) {
|
||||
if (!upstreamUrl) {
|
||||
const error = "UPSTREAM_URL environment variable not set";
|
||||
logger.error(error);
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
function createSmartFetch(upstreamUrl: string) {
|
||||
const isUnixSocket =
|
||||
upstreamUrl.startsWith("/") || upstreamUrl.startsWith("./");
|
||||
const baseUrl = isUnixSocket ? "http://localhost" : upstreamUrl;
|
||||
@@ -84,5 +78,20 @@ function createSmartFetch(upstreamUrl: string | undefined) {
|
||||
};
|
||||
}
|
||||
|
||||
// Export the configured smart fetch function
|
||||
export const apiFetch = createSmartFetch(env.UPSTREAM_URL);
|
||||
// Lazy-initialized fetch function (only throws if UPSTREAM_URL is missing when actually used)
|
||||
let cachedFetch: ReturnType<typeof createSmartFetch> | null = null;
|
||||
|
||||
export async function apiFetch<T>(
|
||||
path: string,
|
||||
options?: FetchOptions,
|
||||
): Promise<T> {
|
||||
if (!cachedFetch) {
|
||||
if (!env.UPSTREAM_URL) {
|
||||
const error = "UPSTREAM_URL environment variable not set";
|
||||
logger.error(error);
|
||||
throw new Error(error);
|
||||
}
|
||||
cachedFetch = createSmartFetch(env.UPSTREAM_URL);
|
||||
}
|
||||
return cachedFetch(path, options);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
primary:
|
||||
"bg-admin-accent text-white hover:bg-admin-accent-hover focus-visible:ring-admin-accent shadow-sm hover:shadow",
|
||||
secondary:
|
||||
"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",
|
||||
"bg-zinc-200/60 dark:bg-zinc-600/50 text-admin-text border border-zinc-400/50 dark:border-zinc-700 hover:border-zinc-400 dark:hover:border-zinc-500 hover:bg-zinc-300/70 dark:hover:bg-zinc-500/60 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:
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={cn("overflow-x-auto rounded-lg border border-admin-border", className)}
|
||||
class={cn("overflow-x-auto rounded-lg border border-admin-border bg-admin-surface", className)}
|
||||
>
|
||||
<table class="w-full text-sm">
|
||||
{@render children?.()}
|
||||
|
||||
@@ -2,16 +2,34 @@ import type { LayoutServerLoad } from "./$types";
|
||||
import { getOGImageUrl } from "$lib/og-types";
|
||||
import { apiFetch } from "$lib/api.server";
|
||||
import type { SiteSettings } from "$lib/admin-types";
|
||||
import { building } from "$app/environment";
|
||||
|
||||
const DEFAULT_SETTINGS: SiteSettings = {
|
||||
identity: {
|
||||
siteTitle: "xevion.dev",
|
||||
displayName: "Ryan Walters",
|
||||
occupation: "Software Engineer",
|
||||
bio: "Software engineer and developer",
|
||||
},
|
||||
socialLinks: [],
|
||||
};
|
||||
|
||||
export const load: LayoutServerLoad = async ({ url, fetch }) => {
|
||||
// Fetch site settings for all pages
|
||||
const settings = await apiFetch<SiteSettings>("/api/settings", { fetch });
|
||||
let settings: SiteSettings;
|
||||
|
||||
if (building) {
|
||||
// During prerendering, use default settings (API isn't available)
|
||||
settings = DEFAULT_SETTINGS;
|
||||
} else {
|
||||
// At runtime, fetch from API
|
||||
settings = await apiFetch<SiteSettings>("/api/settings", { fetch });
|
||||
}
|
||||
|
||||
return {
|
||||
settings,
|
||||
metadata: {
|
||||
title: settings.identity.siteTitle,
|
||||
description: settings.identity.bio.split("\n")[0], // First line of bio
|
||||
description: settings.identity.bio.split("\n")[0],
|
||||
ogImage: getOGImageUrl({ type: "index" }),
|
||||
url: url.toString(),
|
||||
},
|
||||
|
||||
@@ -58,10 +58,10 @@
|
||||
|
||||
<!-- Recent Events -->
|
||||
<div
|
||||
class="rounded-xl border border-admin-border bg-admin-surface/50 overflow-hidden shadow-sm shadow-black/10 dark:shadow-black/20"
|
||||
class="rounded-xl border border-admin-border bg-admin-surface overflow-hidden shadow-sm shadow-black/10 dark:shadow-black/20"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-between px-6 py-3.5 bg-admin-surface-hover/30 border-b border-admin-border"
|
||||
class="flex items-center justify-between px-6 py-3.5 bg-admin-surface-hover border-b border-admin-border"
|
||||
>
|
||||
<h2 class="text-sm font-medium text-admin-text-secondary">
|
||||
Recent Events
|
||||
|
||||
@@ -108,7 +108,7 @@
|
||||
</div>
|
||||
{:else}
|
||||
<Table>
|
||||
<thead class="bg-admin-surface/50">
|
||||
<thead class="bg-admin-surface-hover">
|
||||
<tr>
|
||||
<th
|
||||
class="px-4 py-3 text-left text-xs font-medium text-admin-text-muted"
|
||||
@@ -137,9 +137,9 @@
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-admin-border/50">
|
||||
<tbody class="divide-y divide-admin-border">
|
||||
{#each projects as project (project.id)}
|
||||
<tr class="hover:bg-admin-surface-hover/30 transition-colors">
|
||||
<tr class="hover:bg-admin-surface-hover/50 transition-colors">
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<div>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import Table from "$lib/components/admin/Table.svelte";
|
||||
import Modal from "$lib/components/admin/Modal.svelte";
|
||||
import ColorPicker from "$lib/components/admin/ColorPicker.svelte";
|
||||
import IconPicker from "$lib/components/admin/IconPicker.svelte";
|
||||
import {
|
||||
getAdminTags,
|
||||
createAdminTag,
|
||||
@@ -25,6 +26,7 @@
|
||||
let showCreateForm = $state(false);
|
||||
let createName = $state("");
|
||||
let createSlug = $state("");
|
||||
let createIcon = $state<string>("");
|
||||
let createColor = $state<string | undefined>(undefined);
|
||||
let creating = $state(false);
|
||||
|
||||
@@ -32,6 +34,7 @@
|
||||
let editingId = $state<string | null>(null);
|
||||
let editName = $state("");
|
||||
let editSlug = $state("");
|
||||
let editIcon = $state<string>("");
|
||||
let editColor = $state<string | undefined>(undefined);
|
||||
let updating = $state(false);
|
||||
|
||||
@@ -63,12 +66,14 @@
|
||||
const data: CreateTagData = {
|
||||
name: createName,
|
||||
slug: createSlug || undefined,
|
||||
icon: createIcon || undefined,
|
||||
color: createColor,
|
||||
};
|
||||
await createAdminTag(data);
|
||||
await loadTags();
|
||||
createName = "";
|
||||
createSlug = "";
|
||||
createIcon = "";
|
||||
createColor = undefined;
|
||||
showCreateForm = false;
|
||||
} catch (error) {
|
||||
@@ -83,6 +88,7 @@
|
||||
editingId = tag.id;
|
||||
editName = tag.name;
|
||||
editSlug = tag.slug;
|
||||
editIcon = tag.icon || "";
|
||||
editColor = tag.color;
|
||||
}
|
||||
|
||||
@@ -90,6 +96,7 @@
|
||||
editingId = null;
|
||||
editName = "";
|
||||
editSlug = "";
|
||||
editIcon = "";
|
||||
editColor = undefined;
|
||||
}
|
||||
|
||||
@@ -103,6 +110,7 @@
|
||||
name: editName,
|
||||
slug: editSlug || undefined,
|
||||
color: editColor,
|
||||
icon: editIcon || undefined,
|
||||
};
|
||||
await updateAdminTag(data);
|
||||
await loadTags();
|
||||
@@ -199,6 +207,9 @@
|
||||
placeholder="Leave empty to auto-generate"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<IconPicker bind:selectedIcon={createIcon} label="Icon (optional)" />
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<ColorPicker bind:selectedColor={createColor} />
|
||||
</div>
|
||||
@@ -229,7 +240,7 @@
|
||||
</div>
|
||||
{:else}
|
||||
<Table>
|
||||
<thead class="bg-admin-surface/50">
|
||||
<thead class="bg-admin-surface-hover">
|
||||
<tr>
|
||||
<th
|
||||
class="px-4 py-3 text-left text-xs font-medium text-admin-text-muted"
|
||||
@@ -241,6 +252,11 @@
|
||||
>
|
||||
Slug
|
||||
</th>
|
||||
<th
|
||||
class="px-4 py-3 text-left text-xs font-medium text-admin-text-muted"
|
||||
>
|
||||
Icon
|
||||
</th>
|
||||
<th
|
||||
class="px-4 py-3 text-left text-xs font-medium text-admin-text-muted"
|
||||
>
|
||||
@@ -258,9 +274,9 @@
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-admin-border/50">
|
||||
<tbody class="divide-y divide-admin-border">
|
||||
{#each tags as tag (tag.id)}
|
||||
<tr class="hover:bg-admin-surface-hover/30 transition-colors">
|
||||
<tr class="hover:bg-admin-surface-hover/50 transition-colors">
|
||||
{#if editingId === tag.id}
|
||||
<!-- Edit mode -->
|
||||
<td class="px-4 py-3">
|
||||
@@ -277,6 +293,17 @@
|
||||
placeholder="tag-slug"
|
||||
/>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
{#if editIcon}
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="text-admin-text">
|
||||
<span class="text-xs text-admin-text-muted">{editIcon}</span>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<span class="text-xs text-admin-text-muted">No icon</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
{#if editColor}
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -323,6 +350,17 @@
|
||||
<td class="px-4 py-3 text-admin-text-secondary">
|
||||
{tag.slug}
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
{#if tag.icon}
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="text-admin-text">
|
||||
<span class="text-xs text-admin-text-muted">{tag.icon}</span>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<span class="text-xs text-admin-text-muted">No icon</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
{#if tag.color}
|
||||
<div class="flex items-center gap-2">
|
||||
|
||||
Reference in New Issue
Block a user