feat: implement comprehensive item management system with admin UI, API, and asset handling utilities.

This commit is contained in:
syntaxbullet
2026-02-06 12:19:14 +01:00
parent 109b36ffe2
commit 34958aa220
22 changed files with 3718 additions and 15 deletions

102
shared/lib/assets.ts Normal file
View File

@@ -0,0 +1,102 @@
/**
* Asset URL Resolution Utilities
* Provides helpers for constructing full asset URLs from item IDs or relative paths.
* Works for both local development and production environments.
*/
import { env } from "./env";
/**
* Get the base URL for assets.
* In production, this should be the public dashboard URL.
* In development, it defaults to localhost with the configured port.
*/
function getAssetsBaseUrl(): string {
// Check for explicitly configured asset URL first
if (process.env.ASSETS_BASE_URL) {
return process.env.ASSETS_BASE_URL.replace(/\/$/, ""); // Remove trailing slash
}
// In production, construct from HOST/PORT or use a default
if (process.env.NODE_ENV === "production") {
// If WEB_URL is set, use it (future-proofing)
if (process.env.WEB_URL) {
return process.env.WEB_URL.replace(/\/$/, "");
}
// Fallback: use the configured host and port
const host = env.HOST === "0.0.0.0" ? "localhost" : env.HOST;
return `http://${host}:${env.PORT}`;
}
// Development: use localhost with the configured port
return `http://localhost:${env.PORT}`;
}
/**
* Get the full URL for an item's icon image.
* @param itemId - The item's database ID
* @returns Full URL to the item's icon image
*
* @example
* getItemIconUrl(42) // => "http://localhost:3000/assets/items/42.png"
*/
export function getItemIconUrl(itemId: number): string {
return `${getAssetsBaseUrl()}/assets/items/${itemId}.png`;
}
/**
* Get the full URL for any asset given its relative path.
* @param relativePath - Path relative to the assets root (e.g., "items/42.png")
* @returns Full URL to the asset
*
* @example
* getAssetUrl("items/42.png") // => "http://localhost:3000/assets/items/42.png"
*/
export function getAssetUrl(relativePath: string): string {
const cleanPath = relativePath.replace(/^\/+/, ""); // Remove leading slashes
return `${getAssetsBaseUrl()}/assets/${cleanPath}`;
}
/**
* Check if a URL is a local asset URL (relative path starting with /assets/).
* @param url - The URL to check
* @returns True if it's a local asset reference
*/
export function isLocalAssetUrl(url: string | null | undefined): boolean {
if (!url) return false;
return url.startsWith("/assets/") || url.startsWith("assets/");
}
/**
* Convert a relative asset path to a full URL.
* If the URL is already absolute (http/https), returns it unchanged.
* If it's a relative asset path, constructs the full URL.
*
* @param url - The URL to resolve (relative or absolute)
* @returns The resolved full URL, or null if input was null/undefined
*/
export function resolveAssetUrl(url: string | null | undefined): string | null {
if (!url) return null;
// Already absolute
if (url.startsWith("http://") || url.startsWith("https://")) {
return url;
}
// Relative asset path
if (isLocalAssetUrl(url)) {
const cleanPath = url.replace(/^\/+assets\//, "").replace(/^assets\//, "");
return getAssetUrl(cleanPath);
}
// Unknown format, return as-is
return url;
}
/**
* Get the placeholder image URL for items without an uploaded image.
* @returns Full URL to the placeholder image
*/
export function getPlaceholderIconUrl(): string {
return `${getAssetsBaseUrl()}/assets/items/placeholder.png`;
}