From 7f6155d661463be76382ae9af9bd74e5b59fe4bd Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Thu, 20 Nov 2025 09:47:39 +0100 Subject: [PATCH] fix(community): robust asset filename matching for existing submissions Implements a normalization strategy to map stored original filenames (e.g. 'icon (2).png') to PocketBase sanitized filenames (e.g. 'icon_2_....png') to ensure variants display correctly for existing records. --- web/src/lib/community.ts | 61 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/web/src/lib/community.ts b/web/src/lib/community.ts index 447616a2..8b75aebd 100644 --- a/web/src/lib/community.ts +++ b/web/src/lib/community.ts @@ -16,6 +16,42 @@ function createServerPB() { return new PocketBase(process.env.NEXT_PUBLIC_POCKETBASE_URL || "http://127.0.0.1:8090") } +/** + * Helper to find the best matching asset filename for a given original filename + * PocketBase sanitizes filenames and appends a random suffix. + * This function attempts to map the stored original filename (in extras) to the actual sanitized filename (in assets). + */ +function findBestMatchingAsset(originalName: string, assets: string[]): string { + if (!originalName || !assets || assets.length === 0) return originalName + + // 1. Exact match + if (assets.includes(originalName)) return originalName + + // 2. Normalized match + // Normalize: remove non-alphanumeric, lowercase + const normalize = (s: string) => s.replace(/[^a-z0-9]/gi, "").toLowerCase() + + // Remove extension for comparison + const originalBase = originalName.substring(0, originalName.lastIndexOf(".")) || originalName + const normalizedOriginal = normalize(originalBase) + + // Check against assets + for (const asset of assets) { + const assetBase = asset.substring(0, asset.lastIndexOf(".")) || asset + const normalizedAsset = normalize(assetBase) + + // Check if normalized asset STARTS with normalized original + // PocketBase usually appends `_` + random chars, which normalize removes or appends to end + // "langsmith (2)" -> "langsmith2" + // "langsmith_2_8tf..." -> "langsmith28tf..." + if (normalizedAsset.startsWith(normalizedOriginal)) { + return asset + } + } + + return originalName // Fallback to original if no match found +} + /** * Transform a CommunityGallery item to IconWithName format for use with IconSearch * For community icons, base is the full HTTP URL to the main icon asset @@ -29,6 +65,27 @@ function transformGalleryToIcon(item: CommunityGallery): any { const mainAssetExt = item.assets?.[0]?.split(".").pop()?.toLowerCase() || "svg" const baseFormat = mainAssetExt === "svg" ? "svg" : mainAssetExt === "png" ? "png" : "webp" + // Process and fix file mappings in extras + const colors = item.extras?.colors ? { ...item.extras.colors } : undefined + if (colors && item.assets) { + Object.keys(colors).forEach((key) => { + const k = key as keyof typeof colors + if (colors[k]) { + colors[k] = findBestMatchingAsset(colors[k]!, item.assets || []) + } + }) + } + + const wordmark = item.extras?.wordmark ? { ...item.extras.wordmark } : undefined + if (wordmark && item.assets) { + Object.keys(wordmark).forEach((key) => { + const k = key as keyof typeof wordmark + if (wordmark[k]) { + wordmark[k] = findBestMatchingAsset(wordmark[k]!, item.assets || []) + } + }) + } + const transformed = { name: item.name, status: item.status, @@ -46,8 +103,8 @@ function transformGalleryToIcon(item: CommunityGallery): any { name: item.created_by || "Community", }, }, - colors: item.extras?.colors, - wordmark: item.extras?.wordmark, + colors: colors, + wordmark: wordmark, }, }