mirror of
https://github.com/Xevion/xevion.dev.git
synced 2026-01-31 04:26:43 -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:
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"db_name": "PostgreSQL",
|
||||||
"query": "\n SELECT \n id, \n slug, \n name,\n short_description,\n description, \n status as \"status: ProjectStatus\", \n github_repo, \n demo_url, \n\n last_github_activity, \n created_at, \n updated_at\n FROM projects\n WHERE id = $1\n ",
|
"query": "\n SELECT\n id,\n slug,\n name,\n short_description,\n description,\n status as \"status: ProjectStatus\",\n github_repo,\n demo_url,\n\n last_github_activity,\n created_at,\n updated_at\n FROM projects\n WHERE id = $1\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -90,5 +90,5 @@
|
|||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "990fae3e6568e19f18784fb562b0f712f5bd2373a5be11cad1714daf357c0f34"
|
"hash": "170d08a3b1effa554fe831e43a8d7b640fbddaa348289360fb520057c0ef3272"
|
||||||
}
|
}
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"db_name": "PostgreSQL",
|
||||||
"query": "\n SELECT \n status as \"status!: ProjectStatus\",\n COUNT(*)::int as \"count!\"\n FROM projects\n GROUP BY status\n ",
|
"query": "\n SELECT\n status as \"status!: ProjectStatus\",\n COUNT(*)::int as \"count!\"\n FROM projects\n GROUP BY status\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -34,5 +34,5 @@
|
|||||||
null
|
null
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "1dce5dfc93dcb882e6bdeacbe4156ad56c0e14144b2e0a6d8b1fcc29196dcca0"
|
"hash": "2bf2a98d854d1c467f31edc4b73fa0e6cc8610aec8e07f3de50d76b4900bef0b"
|
||||||
}
|
}
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"db_name": "PostgreSQL",
|
||||||
"query": "\n INSERT INTO projects (slug, name, short_description, description, status, github_repo, demo_url)\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n RETURNING id, slug, name, short_description, description, status as \"status: ProjectStatus\", \n github_repo, demo_url, last_github_activity, created_at, updated_at\n ",
|
"query": "\n INSERT INTO projects (slug, name, short_description, description, status, github_repo, demo_url)\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n RETURNING id, slug, name, short_description, description, status as \"status: ProjectStatus\",\n github_repo, demo_url, last_github_activity, created_at, updated_at\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -108,5 +108,5 @@
|
|||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "ad86ca2613973d89e49b3bea1b3f0c8cf0885e431aea0cd349998de717c5e776"
|
"hash": "538842bf07a2ac3fca612413e5a8e10769b2fc4d90d463040911f416314a42ac"
|
||||||
}
|
}
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"db_name": "PostgreSQL",
|
||||||
"query": "\n SELECT \n id, \n slug, \n name,\n short_description,\n description, \n status as \"status: ProjectStatus\", \n github_repo, \n demo_url, \n last_github_activity, \n created_at, \n updated_at\n FROM projects\n ORDER BY updated_at DESC\n ",
|
"query": "\n SELECT\n id,\n slug,\n name,\n short_description,\n description,\n status as \"status: ProjectStatus\",\n github_repo,\n demo_url,\n last_github_activity,\n created_at,\n updated_at\n FROM projects\n ORDER BY updated_at DESC\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -88,5 +88,5 @@
|
|||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "5514e52a1311c6e10f84d60f906c90178894997da3b13be0323c6172ecb419db"
|
"hash": "7210d993016792490832230c09ed3c4dfc1a6292fcc9c16ded0b182b9952b7ce"
|
||||||
}
|
}
|
||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"db_name": "PostgreSQL",
|
||||||
"query": "\n SELECT \n p.id, \n p.slug, \n p.name,\n p.short_description,\n p.description, \n p.status as \"status: ProjectStatus\", \n p.github_repo, \n p.demo_url, \n p.last_github_activity, \n p.created_at, \n p.updated_at\n FROM projects p\n JOIN project_tags pt ON p.id = pt.project_id\n WHERE pt.tag_id = $1\n ORDER BY p.updated_at DESC\n ",
|
"query": "\n SELECT \n p.id, \n p.slug, \n p.name,\n p.short_description,\n p.description, \n p.status as \"status: super::ProjectStatus\", \n p.github_repo, \n p.demo_url, \n p.last_github_activity, \n p.created_at, \n p.updated_at\n FROM projects p\n JOIN project_tags pt ON p.id = pt.project_id\n WHERE pt.tag_id = $1\n ORDER BY p.updated_at DESC\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 5,
|
"ordinal": 5,
|
||||||
"name": "status: ProjectStatus",
|
"name": "status: super::ProjectStatus",
|
||||||
"type_info": {
|
"type_info": {
|
||||||
"Custom": {
|
"Custom": {
|
||||||
"name": "project_status",
|
"name": "project_status",
|
||||||
@@ -90,5 +90,5 @@
|
|||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "42723e40f3adff9eb24a701347c5b9c331ac7914b527ca6a98865fe4fb8a793c"
|
"hash": "73404037b3f04ace5f775906ed25d4c572647889dc0185aed652038447ef9642"
|
||||||
}
|
}
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"db_name": "PostgreSQL",
|
||||||
"query": "\n SELECT \n id, \n slug, \n name,\n short_description,\n description, \n status as \"status: ProjectStatus\", \n github_repo, \n demo_url, \n last_github_activity, \n created_at, \n updated_at\n FROM projects\n WHERE status != 'hidden'\n ORDER BY updated_at DESC\n ",
|
"query": "\n SELECT\n id,\n slug,\n name,\n short_description,\n description,\n status as \"status: ProjectStatus\",\n github_repo,\n demo_url,\n last_github_activity,\n created_at,\n updated_at\n FROM projects\n WHERE status != 'hidden'\n ORDER BY updated_at DESC\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -88,5 +88,5 @@
|
|||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "960a24b5174e421d57f21944632a8356f6ef20fa5aae9c6126e4944dffbd5ee0"
|
"hash": "92641a97d3f5329df38e2ecb456b1f43392b34bba0b286ab9bd75a8207354fea"
|
||||||
}
|
}
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"db_name": "PostgreSQL",
|
||||||
"query": "\n SELECT \n id, \n slug, \n name,\n short_description,\n description, \n status as \"status: ProjectStatus\", \n github_repo, \n demo_url, \n\n last_github_activity, \n created_at, \n updated_at\n FROM projects\n WHERE slug = $1\n ",
|
"query": "\n SELECT\n id,\n slug,\n name,\n short_description,\n description,\n status as \"status: ProjectStatus\",\n github_repo,\n demo_url,\n\n last_github_activity,\n created_at,\n updated_at\n FROM projects\n WHERE slug = $1\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -90,5 +90,5 @@
|
|||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "e3c4842a1151f51f3871212c3e591103d04a7ff27b27d7d197fbd53593c06b74"
|
"hash": "f97c44ba8b156a2f97cdbc240d1c760ae1efc041d1acd5c3462b96a9236dd3dc"
|
||||||
}
|
}
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"db_name": "PostgreSQL",
|
||||||
"query": "\n UPDATE projects\n SET slug = $2, name = $3, short_description = $4, description = $5, \n status = $6, github_repo = $7, demo_url = $8\n WHERE id = $1\n RETURNING id, slug, name, short_description, description, status as \"status: ProjectStatus\", \n github_repo, demo_url, last_github_activity, created_at, updated_at\n ",
|
"query": "\n UPDATE projects\n SET slug = $2, name = $3, short_description = $4, description = $5,\n status = $6, github_repo = $7, demo_url = $8\n WHERE id = $1\n RETURNING id, slug, name, short_description, description, status as \"status: ProjectStatus\",\n github_repo, demo_url, last_github_activity, created_at, updated_at\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -109,5 +109,5 @@
|
|||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "39f5640a8e812c05d62a0c5ab696a60c70a942d8d29af1439c30a5b04ad4fc6b"
|
"hash": "fdd7f3743ad2e6d1d571ee2d3dbd8b4d49074ef2cbe74dc79c6f4f638bdd3a88"
|
||||||
}
|
}
|
||||||
+7
-1
@@ -63,7 +63,8 @@ COPY migrations/ ./migrations/
|
|||||||
COPY --from=frontend /build/build/client ./web/build/client
|
COPY --from=frontend /build/build/client ./web/build/client
|
||||||
COPY --from=frontend /build/build/prerendered ./web/build/prerendered
|
COPY --from=frontend /build/build/prerendered ./web/build/prerendered
|
||||||
|
|
||||||
# Build with real assets
|
# Build with real assets (use sqlx offline mode)
|
||||||
|
ENV SQLX_OFFLINE=true
|
||||||
RUN cargo build --release && \
|
RUN cargo build --release && \
|
||||||
strip target/release/api
|
strip target/release/api
|
||||||
|
|
||||||
@@ -83,6 +84,11 @@ COPY --from=frontend /build/build/client ./web/build/client
|
|||||||
COPY --from=frontend /build/build/*.js ./web/build/
|
COPY --from=frontend /build/build/*.js ./web/build/
|
||||||
COPY web/console-logger.js ./web/
|
COPY web/console-logger.js ./web/
|
||||||
|
|
||||||
|
# Install production dependencies for SSR runtime
|
||||||
|
COPY web/package.json web/bun.lock ./web/
|
||||||
|
RUN cd web && bun install --frozen-lockfile --production && \
|
||||||
|
ln -s /app/web/node_modules /app/web/build/node_modules
|
||||||
|
|
||||||
# Create inline entrypoint script
|
# Create inline entrypoint script
|
||||||
RUN cat > /entrypoint.sh << 'EOF'
|
RUN cat > /entrypoint.sh << 'EOF'
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ pub use tags::*;
|
|||||||
pub struct CreateTagRequest {
|
pub struct CreateTagRequest {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub slug: Option<String>,
|
pub slug: Option<String>,
|
||||||
|
pub icon: Option<String>,
|
||||||
pub color: Option<String>,
|
pub color: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,6 +27,7 @@ pub struct CreateTagRequest {
|
|||||||
pub struct UpdateTagRequest {
|
pub struct UpdateTagRequest {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub slug: Option<String>,
|
pub slug: Option<String>,
|
||||||
|
pub icon: Option<String>,
|
||||||
pub color: Option<String>,
|
pub color: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ pub async fn create_tag_handler(
|
|||||||
&state.pool,
|
&state.pool,
|
||||||
&payload.name,
|
&payload.name,
|
||||||
payload.slug.as_deref(),
|
payload.slug.as_deref(),
|
||||||
None, // icon - not yet supported in admin UI
|
payload.icon.as_deref(),
|
||||||
payload.color.as_deref(),
|
payload.color.as_deref(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -214,7 +214,7 @@ pub async fn update_tag_handler(
|
|||||||
tag.id,
|
tag.id,
|
||||||
&payload.name,
|
&payload.name,
|
||||||
payload.slug.as_deref(),
|
payload.slug.as_deref(),
|
||||||
None, // icon - not yet supported in admin UI
|
payload.icon.as_deref(),
|
||||||
payload.color.as_deref(),
|
payload.color.as_deref(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
|||||||
+4
-2
@@ -8,6 +8,9 @@
|
|||||||
"@fontsource-variable/schibsted-grotesk": "^5.2.8",
|
"@fontsource-variable/schibsted-grotesk": "^5.2.8",
|
||||||
"@fontsource/hanken-grotesk": "^5.1.0",
|
"@fontsource/hanken-grotesk": "^5.1.0",
|
||||||
"@fontsource/schibsted-grotesk": "^5.2.8",
|
"@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",
|
"@logtape/logtape": "^1.3.5",
|
||||||
"@resvg/resvg-js": "^2.6.2",
|
"@resvg/resvg-js": "^2.6.2",
|
||||||
"@xevion/satori-html": "^0.4.1",
|
"@xevion/satori-html": "^0.4.1",
|
||||||
@@ -20,7 +23,6 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.2",
|
"@eslint/js": "^9.39.2",
|
||||||
"@fontsource/inter": "^5.2.8",
|
"@fontsource/inter": "^5.2.8",
|
||||||
"@iconify/json": "^2.2.424",
|
|
||||||
"@sveltejs/kit": "^2.21.0",
|
"@sveltejs/kit": "^2.21.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
@@ -140,7 +142,7 @@
|
|||||||
|
|
||||||
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
|
"@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=="],
|
"@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-variable/schibsted-grotesk": "^5.2.8",
|
||||||
"@fontsource/hanken-grotesk": "^5.1.0",
|
"@fontsource/hanken-grotesk": "^5.1.0",
|
||||||
"@fontsource/schibsted-grotesk": "^5.2.8",
|
"@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",
|
"@logtape/logtape": "^1.3.5",
|
||||||
"@resvg/resvg-js": "^2.6.2",
|
"@resvg/resvg-js": "^2.6.2",
|
||||||
"@xevion/satori-html": "^0.4.1",
|
"@xevion/satori-html": "^0.4.1",
|
||||||
@@ -32,7 +35,6 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.2",
|
"@eslint/js": "^9.39.2",
|
||||||
"@fontsource/inter": "^5.2.8",
|
"@fontsource/inter": "^5.2.8",
|
||||||
"@iconify/json": "^2.2.424",
|
|
||||||
"@sveltejs/kit": "^2.21.0",
|
"@sveltejs/kit": "^2.21.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export interface UpdateProjectData extends CreateProjectData {
|
|||||||
export interface CreateTagData {
|
export interface CreateTagData {
|
||||||
name: string;
|
name: string;
|
||||||
slug?: string;
|
slug?: string;
|
||||||
|
icon?: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,13 +15,7 @@ interface BunFetchOptions extends RequestInit {
|
|||||||
* Create a socket-aware fetch function
|
* Create a socket-aware fetch function
|
||||||
* Automatically handles Unix socket vs TCP based on UPSTREAM_URL
|
* Automatically handles Unix socket vs TCP based on UPSTREAM_URL
|
||||||
*/
|
*/
|
||||||
function createSmartFetch(upstreamUrl: string | undefined) {
|
function createSmartFetch(upstreamUrl: string) {
|
||||||
if (!upstreamUrl) {
|
|
||||||
const error = "UPSTREAM_URL environment variable not set";
|
|
||||||
logger.error(error);
|
|
||||||
throw new Error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isUnixSocket =
|
const isUnixSocket =
|
||||||
upstreamUrl.startsWith("/") || upstreamUrl.startsWith("./");
|
upstreamUrl.startsWith("/") || upstreamUrl.startsWith("./");
|
||||||
const baseUrl = isUnixSocket ? "http://localhost" : upstreamUrl;
|
const baseUrl = isUnixSocket ? "http://localhost" : upstreamUrl;
|
||||||
@@ -84,5 +78,20 @@ function createSmartFetch(upstreamUrl: string | undefined) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export the configured smart fetch function
|
// Lazy-initialized fetch function (only throws if UPSTREAM_URL is missing when actually used)
|
||||||
export const apiFetch = createSmartFetch(env.UPSTREAM_URL);
|
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:
|
primary:
|
||||||
"bg-admin-accent text-white hover:bg-admin-accent-hover focus-visible:ring-admin-accent shadow-sm hover:shadow",
|
"bg-admin-accent text-white hover:bg-admin-accent-hover focus-visible:ring-admin-accent shadow-sm hover:shadow",
|
||||||
secondary:
|
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:
|
danger:
|
||||||
"bg-red-600 text-white hover:bg-red-500 focus-visible:ring-red-500 shadow-sm hover:shadow",
|
"bg-red-600 text-white hover:bg-red-500 focus-visible:ring-red-500 shadow-sm hover:shadow",
|
||||||
ghost:
|
ghost:
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<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">
|
<table class="w-full text-sm">
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
|
|||||||
@@ -2,16 +2,34 @@ import type { LayoutServerLoad } from "./$types";
|
|||||||
import { getOGImageUrl } from "$lib/og-types";
|
import { getOGImageUrl } from "$lib/og-types";
|
||||||
import { apiFetch } from "$lib/api.server";
|
import { apiFetch } from "$lib/api.server";
|
||||||
import type { SiteSettings } from "$lib/admin-types";
|
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 }) => {
|
export const load: LayoutServerLoad = async ({ url, fetch }) => {
|
||||||
// Fetch site settings for all pages
|
let settings: SiteSettings;
|
||||||
const settings = await apiFetch<SiteSettings>("/api/settings", { fetch });
|
|
||||||
|
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 {
|
return {
|
||||||
settings,
|
settings,
|
||||||
metadata: {
|
metadata: {
|
||||||
title: settings.identity.siteTitle,
|
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" }),
|
ogImage: getOGImageUrl({ type: "index" }),
|
||||||
url: url.toString(),
|
url: url.toString(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -58,10 +58,10 @@
|
|||||||
|
|
||||||
<!-- Recent Events -->
|
<!-- Recent Events -->
|
||||||
<div
|
<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
|
<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">
|
<h2 class="text-sm font-medium text-admin-text-secondary">
|
||||||
Recent Events
|
Recent Events
|
||||||
|
|||||||
@@ -108,7 +108,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<Table>
|
<Table>
|
||||||
<thead class="bg-admin-surface/50">
|
<thead class="bg-admin-surface-hover">
|
||||||
<tr>
|
<tr>
|
||||||
<th
|
<th
|
||||||
class="px-4 py-3 text-left text-xs font-medium text-admin-text-muted"
|
class="px-4 py-3 text-left text-xs font-medium text-admin-text-muted"
|
||||||
@@ -137,9 +137,9 @@
|
|||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-admin-border/50">
|
<tbody class="divide-y divide-admin-border">
|
||||||
{#each projects as project (project.id)}
|
{#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">
|
<td class="px-4 py-3">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import Table from "$lib/components/admin/Table.svelte";
|
import Table from "$lib/components/admin/Table.svelte";
|
||||||
import Modal from "$lib/components/admin/Modal.svelte";
|
import Modal from "$lib/components/admin/Modal.svelte";
|
||||||
import ColorPicker from "$lib/components/admin/ColorPicker.svelte";
|
import ColorPicker from "$lib/components/admin/ColorPicker.svelte";
|
||||||
|
import IconPicker from "$lib/components/admin/IconPicker.svelte";
|
||||||
import {
|
import {
|
||||||
getAdminTags,
|
getAdminTags,
|
||||||
createAdminTag,
|
createAdminTag,
|
||||||
@@ -25,6 +26,7 @@
|
|||||||
let showCreateForm = $state(false);
|
let showCreateForm = $state(false);
|
||||||
let createName = $state("");
|
let createName = $state("");
|
||||||
let createSlug = $state("");
|
let createSlug = $state("");
|
||||||
|
let createIcon = $state<string>("");
|
||||||
let createColor = $state<string | undefined>(undefined);
|
let createColor = $state<string | undefined>(undefined);
|
||||||
let creating = $state(false);
|
let creating = $state(false);
|
||||||
|
|
||||||
@@ -32,6 +34,7 @@
|
|||||||
let editingId = $state<string | null>(null);
|
let editingId = $state<string | null>(null);
|
||||||
let editName = $state("");
|
let editName = $state("");
|
||||||
let editSlug = $state("");
|
let editSlug = $state("");
|
||||||
|
let editIcon = $state<string>("");
|
||||||
let editColor = $state<string | undefined>(undefined);
|
let editColor = $state<string | undefined>(undefined);
|
||||||
let updating = $state(false);
|
let updating = $state(false);
|
||||||
|
|
||||||
@@ -63,12 +66,14 @@
|
|||||||
const data: CreateTagData = {
|
const data: CreateTagData = {
|
||||||
name: createName,
|
name: createName,
|
||||||
slug: createSlug || undefined,
|
slug: createSlug || undefined,
|
||||||
|
icon: createIcon || undefined,
|
||||||
color: createColor,
|
color: createColor,
|
||||||
};
|
};
|
||||||
await createAdminTag(data);
|
await createAdminTag(data);
|
||||||
await loadTags();
|
await loadTags();
|
||||||
createName = "";
|
createName = "";
|
||||||
createSlug = "";
|
createSlug = "";
|
||||||
|
createIcon = "";
|
||||||
createColor = undefined;
|
createColor = undefined;
|
||||||
showCreateForm = false;
|
showCreateForm = false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -83,6 +88,7 @@
|
|||||||
editingId = tag.id;
|
editingId = tag.id;
|
||||||
editName = tag.name;
|
editName = tag.name;
|
||||||
editSlug = tag.slug;
|
editSlug = tag.slug;
|
||||||
|
editIcon = tag.icon || "";
|
||||||
editColor = tag.color;
|
editColor = tag.color;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,6 +96,7 @@
|
|||||||
editingId = null;
|
editingId = null;
|
||||||
editName = "";
|
editName = "";
|
||||||
editSlug = "";
|
editSlug = "";
|
||||||
|
editIcon = "";
|
||||||
editColor = undefined;
|
editColor = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,6 +110,7 @@
|
|||||||
name: editName,
|
name: editName,
|
||||||
slug: editSlug || undefined,
|
slug: editSlug || undefined,
|
||||||
color: editColor,
|
color: editColor,
|
||||||
|
icon: editIcon || undefined,
|
||||||
};
|
};
|
||||||
await updateAdminTag(data);
|
await updateAdminTag(data);
|
||||||
await loadTags();
|
await loadTags();
|
||||||
@@ -199,6 +207,9 @@
|
|||||||
placeholder="Leave empty to auto-generate"
|
placeholder="Leave empty to auto-generate"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mt-4">
|
||||||
|
<IconPicker bind:selectedIcon={createIcon} label="Icon (optional)" />
|
||||||
|
</div>
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<ColorPicker bind:selectedColor={createColor} />
|
<ColorPicker bind:selectedColor={createColor} />
|
||||||
</div>
|
</div>
|
||||||
@@ -229,7 +240,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<Table>
|
<Table>
|
||||||
<thead class="bg-admin-surface/50">
|
<thead class="bg-admin-surface-hover">
|
||||||
<tr>
|
<tr>
|
||||||
<th
|
<th
|
||||||
class="px-4 py-3 text-left text-xs font-medium text-admin-text-muted"
|
class="px-4 py-3 text-left text-xs font-medium text-admin-text-muted"
|
||||||
@@ -241,6 +252,11 @@
|
|||||||
>
|
>
|
||||||
Slug
|
Slug
|
||||||
</th>
|
</th>
|
||||||
|
<th
|
||||||
|
class="px-4 py-3 text-left text-xs font-medium text-admin-text-muted"
|
||||||
|
>
|
||||||
|
Icon
|
||||||
|
</th>
|
||||||
<th
|
<th
|
||||||
class="px-4 py-3 text-left text-xs font-medium text-admin-text-muted"
|
class="px-4 py-3 text-left text-xs font-medium text-admin-text-muted"
|
||||||
>
|
>
|
||||||
@@ -258,9 +274,9 @@
|
|||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-admin-border/50">
|
<tbody class="divide-y divide-admin-border">
|
||||||
{#each tags as tag (tag.id)}
|
{#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}
|
{#if editingId === tag.id}
|
||||||
<!-- Edit mode -->
|
<!-- Edit mode -->
|
||||||
<td class="px-4 py-3">
|
<td class="px-4 py-3">
|
||||||
@@ -277,6 +293,17 @@
|
|||||||
placeholder="tag-slug"
|
placeholder="tag-slug"
|
||||||
/>
|
/>
|
||||||
</td>
|
</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">
|
<td class="px-4 py-3">
|
||||||
{#if editColor}
|
{#if editColor}
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
@@ -323,6 +350,17 @@
|
|||||||
<td class="px-4 py-3 text-admin-text-secondary">
|
<td class="px-4 py-3 text-admin-text-secondary">
|
||||||
{tag.slug}
|
{tag.slug}
|
||||||
</td>
|
</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">
|
<td class="px-4 py-3">
|
||||||
{#if tag.color}
|
{#if tag.color}
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
|
|||||||
Reference in New Issue
Block a user