From 79ce49532f54f365b68ef7104de43fa30b2aa17d Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Sat, 22 Nov 2025 23:36:29 +0100 Subject: [PATCH 1/6] try cdn fallback --- web/src/app/icons/[icon]/opengraph-image.tsx | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/web/src/app/icons/[icon]/opengraph-image.tsx b/web/src/app/icons/[icon]/opengraph-image.tsx index 52874434..0b4c2402 100644 --- a/web/src/app/icons/[icon]/opengraph-image.tsx +++ b/web/src/app/icons/[icon]/opengraph-image.tsx @@ -2,6 +2,7 @@ import { readFile } from "node:fs/promises"; import { join } from "node:path"; import { ImageResponse } from "next/og"; import { getAllIcons } from "@/lib/api"; +import { BASE_URL } from "@/constants"; export const revalidate = false; export async function generateStaticParams() { @@ -66,21 +67,19 @@ export default async function Image({ .join(" "); // Read the icon file from local filesystem - let iconData: Buffer | null = null; + let iconData: ArrayBuffer | null = null; try { - const iconPath = join(process.cwd(), `../png/${icon}.png`); - console.log( - `Generating opengraph image for ${icon} (${index + 1} / ${totalIcons}) from path ${iconPath}`, - ); - iconData = await readFile(iconPath); + const fetchedIcon = await fetch(`${BASE_URL}/png/${icon}.png`); + if (!fetchedIcon.ok) { + throw new Error(`Failed to fetch icon ${icon} from CDN`); + } + iconData = await fetchedIcon.arrayBuffer(); } catch (_error) { - console.error(`Icon ${icon} was not found locally`); + console.error(`Icon ${icon} was not found. Using placeholder instead.`); } // Convert the image data to a data URL or use placeholder - const iconUrl = iconData - ? `data:image/png;base64,${iconData.toString("base64")}` - : null; + const iconUrl = iconData ? `data:image/png;base64,${Buffer.from(iconData).toString("base64")}` : null; return new ImageResponse(
Date: Sat, 22 Nov 2025 23:40:04 +0100 Subject: [PATCH 2/6] try cdn fallback --- web/src/app/icons/[icon]/opengraph-image.tsx | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/web/src/app/icons/[icon]/opengraph-image.tsx b/web/src/app/icons/[icon]/opengraph-image.tsx index 0b4c2402..52874434 100644 --- a/web/src/app/icons/[icon]/opengraph-image.tsx +++ b/web/src/app/icons/[icon]/opengraph-image.tsx @@ -2,7 +2,6 @@ import { readFile } from "node:fs/promises"; import { join } from "node:path"; import { ImageResponse } from "next/og"; import { getAllIcons } from "@/lib/api"; -import { BASE_URL } from "@/constants"; export const revalidate = false; export async function generateStaticParams() { @@ -67,19 +66,21 @@ export default async function Image({ .join(" "); // Read the icon file from local filesystem - let iconData: ArrayBuffer | null = null; + let iconData: Buffer | null = null; try { - const fetchedIcon = await fetch(`${BASE_URL}/png/${icon}.png`); - if (!fetchedIcon.ok) { - throw new Error(`Failed to fetch icon ${icon} from CDN`); - } - iconData = await fetchedIcon.arrayBuffer(); + const iconPath = join(process.cwd(), `../png/${icon}.png`); + console.log( + `Generating opengraph image for ${icon} (${index + 1} / ${totalIcons}) from path ${iconPath}`, + ); + iconData = await readFile(iconPath); } catch (_error) { - console.error(`Icon ${icon} was not found. Using placeholder instead.`); + console.error(`Icon ${icon} was not found locally`); } // Convert the image data to a data URL or use placeholder - const iconUrl = iconData ? `data:image/png;base64,${Buffer.from(iconData).toString("base64")}` : null; + const iconUrl = iconData + ? `data:image/png;base64,${iconData.toString("base64")}` + : null; return new ImageResponse(
Date: Sun, 23 Nov 2025 08:22:43 +0100 Subject: [PATCH 3/6] update opengraph --- web/src/app/community/[icon]/page.tsx | 11 + web/src/app/icons/[icon]/opengraph-image.tsx | 320 ------------------- web/src/app/icons/[icon]/page.tsx | 1 + web/src/app/icons/opengraph-image.tsx | 283 ---------------- 4 files changed, 12 insertions(+), 603 deletions(-) delete mode 100644 web/src/app/icons/[icon]/opengraph-image.tsx delete mode 100644 web/src/app/icons/opengraph-image.tsx diff --git a/web/src/app/community/[icon]/page.tsx b/web/src/app/community/[icon]/page.tsx index 0b388171..f1d1ab56 100644 --- a/web/src/app/community/[icon]/page.tsx +++ b/web/src/app/community/[icon]/page.tsx @@ -79,11 +79,22 @@ export async function generateMetadata({ params }: Props, _parent: ResolvingMeta type: "website", url: pageUrl, siteName: "Dashboard Icons", + locale: "en_US", + images: [ + { + url: mainIconUrl, + width: 512, + height: 512, + alt: `${formattedIconName} icon`, + type: mainIconUrl.endsWith(".svg") ? "image/svg+xml" : mainIconUrl.endsWith(".webp") ? "image/webp" : "image/png", + }, + ], }, twitter: { card: "summary_large_image", title: `${formattedIconName} Icon (Community) | Dashboard Icons`, description: `Download the ${formattedIconName} community-submitted icon. Part of a collection of ${totalIcons} community icons awaiting review and addition to the Dashboard Icons collection.`, + images: [mainIconUrl], }, alternates: { canonical: `${WEB_URL}/community/${icon}`, diff --git a/web/src/app/icons/[icon]/opengraph-image.tsx b/web/src/app/icons/[icon]/opengraph-image.tsx deleted file mode 100644 index 52874434..00000000 --- a/web/src/app/icons/[icon]/opengraph-image.tsx +++ /dev/null @@ -1,320 +0,0 @@ -import { readFile } from "node:fs/promises"; -import { join } from "node:path"; -import { ImageResponse } from "next/og"; -import { getAllIcons } from "@/lib/api"; - -export const revalidate = false; -export async function generateStaticParams() { - const iconsData = await getAllIcons(); - if (process.env.CI_MODE === "false") { - // This is meant to speed up the build process in local development - return Object.keys(iconsData) - .slice(0, 5) - .map((icon) => ({ - icon, - })); - } - return Object.keys(iconsData).map((icon) => ({ - icon, - })); -} - -export const size = { - width: 1200, - height: 630, -}; - -export const alt = "Icon Open Graph Image"; -export const contentType = "image/png"; -export default async function Image({ - params, -}: { - params: Promise<{ icon: string }>; -}) { - const { icon } = await params; - - if (!icon) { - console.error(`[Opengraph Image] Icon not found for ${icon}`); - return new ImageResponse( -
- Icon not found -
, - { ...size }, - ); - } - - const iconsData = await getAllIcons(); - const totalIcons = Object.keys(iconsData).length; - const index = Object.keys(iconsData).indexOf(icon); - - // Format the icon name for display - const formattedIconName = icon - .split("-") - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(" "); - - // Read the icon file from local filesystem - let iconData: Buffer | null = null; - try { - const iconPath = join(process.cwd(), `../png/${icon}.png`); - console.log( - `Generating opengraph image for ${icon} (${index + 1} / ${totalIcons}) from path ${iconPath}`, - ); - iconData = await readFile(iconPath); - } catch (_error) { - console.error(`Icon ${icon} was not found locally`); - } - - // Convert the image data to a data URL or use placeholder - const iconUrl = iconData - ? `data:image/png;base64,${iconData.toString("base64")}` - : null; - - return new ImageResponse( -
- {/* Background blur blobs */} -
-
- - {/* Main content layout */} -
- {/* Icon container */} -
-
- {iconUrl && ( - // biome-ignore lint/performance/noImgElement: Not using nextjs here - {formattedIconName} - )} - {!iconUrl && ( -
- {formattedIconName} -
- )} -
- - {/* Text content */} -
-
- Download {formattedIconName} icon for free -
- -
- Amongst {totalIcons} other high-quality dashboard icons -
- -
- {["SVG", "PNG", "WEBP"].map((format) => ( -
- {format} -
- ))} -
-
-
- - {/* Footer */} -
-
-
- dashboardicons.com -
-
-
, - { - ...size, - }, - ); -} diff --git a/web/src/app/icons/[icon]/page.tsx b/web/src/app/icons/[icon]/page.tsx index d69c49f2..52fc58b7 100644 --- a/web/src/app/icons/[icon]/page.tsx +++ b/web/src/app/icons/[icon]/page.tsx @@ -77,6 +77,7 @@ export async function generateMetadata({ params, searchParams }: Props, _parent: type: "website", url: pageUrl, siteName: "Dashboard Icons", + locale: "en_US", images: [ { url: `${BASE_URL}/png/${icon}.png`, diff --git a/web/src/app/icons/opengraph-image.tsx b/web/src/app/icons/opengraph-image.tsx deleted file mode 100644 index e29bb7e3..00000000 --- a/web/src/app/icons/opengraph-image.tsx +++ /dev/null @@ -1,283 +0,0 @@ -import { ImageResponse } from "next/og" -import { getAllIcons } from "@/lib/api" - -export const dynamic = "force-static" - -export const size = { - width: 1200, - height: 630, -} - -// Define a fixed list of representative icons -const representativeIcons = [ - "homarr", - "sonarr", - "radarr", - "lidarr", - "readarr", - "prowlarr", - "qbittorrent", - "home-assistant", - "cloudflare", - "github", - "traefik", - "portainer-alt", - "plex", - "jellyfin", - "overseerr", -] - -export default async function Image() { - const iconsData = await getAllIcons() - const totalIcons = Object.keys(iconsData).length - // Round down to the nearest 100 - const roundedTotalIcons = Math.round(totalIcons / 100) * 100 - - const iconImages = representativeIcons.map((icon) => ({ - name: icon - .split("-") - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(" "), - url: `https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/${icon}.png`, - })) - - return new ImageResponse( -
-
-
- -
-
-
- Dashboard Icons -
-
- A curated collection of {roundedTotalIcons}+ free icons for dashboards and app directories -
-
- -
- {iconImages.map((icon, index) => ( -
-
- {icon.name} -
- ))} -
-
-
- +{totalIcons - representativeIcons.length} -
-
-
- -
-
- -
-
-
- dashboardicons.com -
-
-
, - { - ...size, - }, - ) -} From 289432cf673f419efc9a360f7f890a3d3c79cf08 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Sun, 23 Nov 2025 08:49:55 +0100 Subject: [PATCH 4/6] update dynamic params for community pages --- web/src/app/community/[icon]/page.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/web/src/app/community/[icon]/page.tsx b/web/src/app/community/[icon]/page.tsx index f1d1ab56..4f0f7121 100644 --- a/web/src/app/community/[icon]/page.tsx +++ b/web/src/app/community/[icon]/page.tsx @@ -5,8 +5,16 @@ import { BASE_URL, WEB_URL } from "@/constants" import { getAllIcons } from "@/lib/api" import { getCommunityGalleryRecord, getCommunitySubmissionByName, getCommunitySubmissions } from "@/lib/community" -export const dynamicParams = false +export const dynamicParams = true; export const revalidate = 21600 // 6 hours +export const dynamic = "force-static"; + +export async function generateStaticParams() { + const icons = await getCommunitySubmissions(); + return icons.map((icon) => ({ + icon: icon.name, + })); +} type Props = { params: Promise<{ icon: string }> From cb7114eaf2fdbc4ded33befdf448086deaca3eb4 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Sun, 23 Nov 2025 08:56:00 +0100 Subject: [PATCH 5/6] wording changes and small ui --- ...advanced-icon-submission-form-tanstack.tsx | 471 +++++++++++------- 1 file changed, 296 insertions(+), 175 deletions(-) diff --git a/web/src/components/advanced-icon-submission-form-tanstack.tsx b/web/src/components/advanced-icon-submission-form-tanstack.tsx index 222c1261..95ecb8cc 100644 --- a/web/src/components/advanced-icon-submission-form-tanstack.tsx +++ b/web/src/components/advanced-icon-submission-form-tanstack.tsx @@ -1,12 +1,12 @@ -"use client" +"use client"; -import { useForm } from "@tanstack/react-form" -import { Check, FileImage, FileType, Plus, X } from "lucide-react" -import { useState } from "react" -import { toast } from "sonner" -import { revalidateAllSubmissions } from "@/app/actions/submissions" -import { ExperimentalWarning } from "@/components/experimental-warning" -import { IconNameCombobox } from "@/components/icon-name-combobox" +import { useForm } from "@tanstack/react-form"; +import { Check, FileImage, FileType, Link, Plus, X } from "lucide-react"; +import { useState } from "react"; +import { toast } from "sonner"; +import { revalidateAllSubmissions } from "@/app/actions/submissions"; +import { ExperimentalWarning } from "@/components/experimental-warning"; +import { IconNameCombobox } from "@/components/icon-name-combobox"; import { AlertDialog, AlertDialogAction, @@ -16,23 +16,37 @@ import { AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, -} from "@/components/ui/alert-dialog" -import { Badge } from "@/components/ui/badge" -import { Button } from "@/components/ui/button" -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" -import { MultiSelect, type MultiSelectOption } from "@/components/ui/multi-select" -import { Dropzone, DropzoneContent, DropzoneEmptyState } from "@/components/ui/shadcn-io/dropzone" -import { Textarea } from "@/components/ui/textarea" -import { useExistingIconNames } from "@/hooks/use-submissions" -import { pb } from "@/lib/pb" +} from "@/components/ui/alert-dialog"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + MultiSelect, + type MultiSelectOption, +} from "@/components/ui/multi-select"; +import { + Dropzone, + DropzoneContent, + DropzoneEmptyState, +} from "@/components/ui/shadcn-io/dropzone"; +import { Textarea } from "@/components/ui/textarea"; +import { REPO_PATH } from "@/constants"; +import { useExistingIconNames } from "@/hooks/use-submissions"; +import { pb } from "@/lib/pb"; interface VariantConfig { - id: string - label: string - description: string - field: "base" | "dark" | "light" | "wordmark" | "wordmark_dark" + id: string; + label: string; + description: string; + field: "base" | "dark" | "light" | "wordmark" | "wordmark_dark"; } const VARIANTS: VariantConfig[] = [ @@ -66,7 +80,7 @@ const VARIANTS: VariantConfig[] = [ description: "Wordmark optimized for dark backgrounds", field: "wordmark_dark", }, -] +]; // Convert VARIANTS to MultiSelect options const VARIANT_OPTIONS: MultiSelectOption[] = VARIANTS.map((variant) => ({ @@ -74,7 +88,7 @@ const VARIANT_OPTIONS: MultiSelectOption[] = VARIANTS.map((variant) => ({ value: variant.id, icon: variant.id === "base" ? FileImage : FileType, disabled: variant.id === "base", // Base is always required -})) +})); const AVAILABLE_CATEGORIES = [ "automation", @@ -94,23 +108,23 @@ const AVAILABLE_CATEGORIES = [ "tools", "utility", "other", -] +]; interface FormData { - iconName: string - selectedVariants: string[] - files: Record - filePreviews: Record - aliases: string[] - aliasInput: string - categories: string[] - description: string + iconName: string; + selectedVariants: string[]; + files: Record; + filePreviews: Record; + aliases: string[]; + aliasInput: string; + categories: string[]; + description: string; } export function AdvancedIconSubmissionFormTanStack() { - const [filePreviews, setFilePreviews] = useState>({}) - const [showConfirmDialog, setShowConfirmDialog] = useState(false) - const { data: existingIcons = [] } = useExistingIconNames() + const [filePreviews, setFilePreviews] = useState>({}); + const [showConfirmDialog, setShowConfirmDialog] = useState(false); + const { data: existingIcons = [] } = useExistingIconNames(); const form = useForm({ defaultValues: { @@ -125,24 +139,24 @@ export function AdvancedIconSubmissionFormTanStack() { } as FormData, onSubmit: async ({ value }) => { if (!pb.authStore.isValid) { - toast.error("You must be logged in to submit an icon") - return + toast.error("You must be logged in to submit an icon"); + return; } - setShowConfirmDialog(true) + setShowConfirmDialog(true); }, - }) + }); const handleConfirmedSubmit = async () => { - const value = form.state.values - setShowConfirmDialog(false) + const value = form.state.values; + setShowConfirmDialog(false); try { - const assetFiles: File[] = [] + const assetFiles: File[] = []; - // Add base file if (value.files.base?.[0]) { - assetFiles.push(value.files.base[0]) + // Add base file + assetFiles.push(value.files.base[0]); } // Build extras object @@ -150,31 +164,31 @@ export function AdvancedIconSubmissionFormTanStack() { aliases: value.aliases, categories: value.categories, base: value.files.base[0]?.name.split(".").pop() || "svg", - } + }; - // Add color variants if present if (value.files.dark?.[0] || value.files.light?.[0]) { - extras.colors = {} + // Add color variants if present + extras.colors = {}; if (value.files.dark?.[0]) { - extras.colors.dark = value.files.dark[0].name - assetFiles.push(value.files.dark[0]) + extras.colors.dark = value.files.dark[0].name; + assetFiles.push(value.files.dark[0]); } if (value.files.light?.[0]) { - extras.colors.light = value.files.light[0].name - assetFiles.push(value.files.light[0]) + extras.colors.light = value.files.light[0].name; + assetFiles.push(value.files.light[0]); } } - // Add wordmark variants if present if (value.files.wordmark?.[0] || value.files.wordmark_dark?.[0]) { - extras.wordmark = {} + // Add wordmark variants if present + extras.wordmark = {}; if (value.files.wordmark?.[0]) { - extras.wordmark.light = value.files.wordmark[0].name - assetFiles.push(value.files.wordmark[0]) + extras.wordmark.light = value.files.wordmark[0].name; + assetFiles.push(value.files.wordmark[0]); } if (value.files.wordmark_dark?.[0]) { - extras.wordmark.dark = value.files.wordmark_dark[0].name - assetFiles.push(value.files.wordmark_dark[0]) + extras.wordmark.dark = value.files.wordmark_dark[0].name; + assetFiles.push(value.files.wordmark_dark[0]); } } @@ -185,155 +199,166 @@ export function AdvancedIconSubmissionFormTanStack() { created_by: pb.authStore.model?.id, status: "pending", extras: extras, - } + }; - const record = await pb.collection("submissions").create(submissionData) + const record = await pb.collection("submissions").create(submissionData); - // Update extras with real filenames from PocketBase response - // PocketBase sanitizes and renames files, so we need to update our references if (record.assets && record.assets.length > 0) { - const updatedExtras = JSON.parse(JSON.stringify(extras)) - let assetIndex = 0 + // Update extras with real filenames from PocketBase response + // PocketBase sanitizes and renames files, so we need to update our references + const updatedExtras = JSON.parse(JSON.stringify(extras)); + let assetIndex = 0; // Skip base icon (first asset) as we track it by 'base' format string only - assetIndex++ + assetIndex++; if (value.files.dark?.[0] && updatedExtras.colors) { - updatedExtras.colors.dark = record.assets[assetIndex] - assetIndex++ + updatedExtras.colors.dark = record.assets[assetIndex]; + assetIndex++; } if (value.files.light?.[0] && updatedExtras.colors) { - updatedExtras.colors.light = record.assets[assetIndex] - assetIndex++ + updatedExtras.colors.light = record.assets[assetIndex]; + assetIndex++; } if (value.files.wordmark?.[0] && updatedExtras.wordmark) { - updatedExtras.wordmark.light = record.assets[assetIndex] - assetIndex++ + updatedExtras.wordmark.light = record.assets[assetIndex]; + assetIndex++; } if (value.files.wordmark_dark?.[0] && updatedExtras.wordmark) { - updatedExtras.wordmark.dark = record.assets[assetIndex] - assetIndex++ + updatedExtras.wordmark.dark = record.assets[assetIndex]; + assetIndex++; } await pb.collection("submissions").update(record.id, { extras: updatedExtras, - }) + }); } // Revalidate Next.js cache for community pages - await revalidateAllSubmissions() + await revalidateAllSubmissions(); toast.success("Icon submitted!", { description: `Your icon "${value.iconName}" has been submitted for review`, - }) + }); // Reset form - form.reset() - setFilePreviews({}) + form.reset(); + setFilePreviews({}); } catch (error: any) { - console.error("Submission error:", error) + console.error("Submission error:", error); toast.error("Failed to submit icon", { description: error?.message || "Please try again later", - }) + }); } - } + }; const handleRemoveVariant = (variantId: string) => { if (variantId !== "base") { // Remove from selected variants - const currentVariants = form.getFieldValue("selectedVariants") + const currentVariants = form.getFieldValue("selectedVariants"); form.setFieldValue( "selectedVariants", currentVariants.filter((v) => v !== variantId), - ) + ); // Remove files - const currentFiles = form.getFieldValue("files") - const newFiles = { ...currentFiles } - delete newFiles[variantId] - form.setFieldValue("files", newFiles) + const currentFiles = form.getFieldValue("files"); + const newFiles = { ...currentFiles }; + delete newFiles[variantId]; + form.setFieldValue("files", newFiles); // Remove previews - const newPreviews = { ...filePreviews } - delete newPreviews[variantId] - setFilePreviews(newPreviews) + const newPreviews = { ...filePreviews }; + delete newPreviews[variantId]; + setFilePreviews(newPreviews); } - } + }; const handleVariantSelectionChange = (selectedValues: string[]) => { // Ensure base is always included - const finalValues = selectedValues.includes("base") ? selectedValues : ["base", ...selectedValues] - return finalValues - } + const finalValues = selectedValues.includes("base") + ? selectedValues + : ["base", ...selectedValues]; + return finalValues; + }; const handleFileDrop = (variantId: string, droppedFiles: File[]) => { - const currentFiles = form.getFieldValue("files") + const currentFiles = form.getFieldValue("files"); form.setFieldValue("files", { ...currentFiles, [variantId]: droppedFiles, - }) + }); // Generate preview for the first file if (droppedFiles.length > 0) { - const reader = new FileReader() + const reader = new FileReader(); reader.onload = (e) => { if (typeof e.target?.result === "string") { setFilePreviews({ ...filePreviews, [variantId]: e.target.result, - }) + }); } - } - reader.readAsDataURL(droppedFiles[0]) + }; + reader.readAsDataURL(droppedFiles[0]); } - } + }; const handleAddAlias = () => { - const aliasInput = form.getFieldValue("aliasInput") - const trimmedAlias = aliasInput.trim() + const aliasInput = form.getFieldValue("aliasInput"); + const trimmedAlias = aliasInput.trim(); if (trimmedAlias) { - const currentAliases = form.getFieldValue("aliases") + const currentAliases = form.getFieldValue("aliases"); if (!currentAliases.includes(trimmedAlias)) { - form.setFieldValue("aliases", [...currentAliases, trimmedAlias]) + form.setFieldValue("aliases", [...currentAliases, trimmedAlias]); } - form.setFieldValue("aliasInput", "") + form.setFieldValue("aliasInput", ""); } - } + }; const handleRemoveAlias = (alias: string) => { - const currentAliases = form.getFieldValue("aliases") + const currentAliases = form.getFieldValue("aliases"); form.setFieldValue( "aliases", currentAliases.filter((a) => a !== alias), - ) - } + ); + }; const toggleCategory = (category: string) => { - const currentCategories = form.getFieldValue("categories") + const currentCategories = form.getFieldValue("categories"); if (currentCategories.includes(category)) { form.setFieldValue( "categories", currentCategories.filter((c) => c !== category), - ) + ); } else { - form.setFieldValue("categories", [...currentCategories, category]) + form.setFieldValue("categories", [...currentCategories, category]); } - } + }; return (
- + Confirm Submission - This icon submission form is a work-in-progress and is currently in an experimentation phase. Your submission will not be - reviewed or processed at this time. We're using this to gather feedback and improve the experience. + This icon submission form is a work-in-progress and is currently + in an experimentation phase. If you want a faster review, please + submit your icon to the{" "} + + github repository + {" "} + instead.

Do you still want to proceed with submitting your icon? @@ -341,45 +366,55 @@ export function AdvancedIconSubmissionFormTanStack() {
Cancel - Yes, Submit Anyway + + Yes, Submit Anyway +
{ - e.preventDefault() - e.stopPropagation() - form.handleSubmit() + e.preventDefault(); + e.stopPropagation(); + form.handleSubmit(); }} > Submit an Icon - Fill in the details below to submit your icon for review + + Fill in the details below to submit your icon for review + {/* Icon Name Section */}
-

Icon Identification

-

Choose a unique identifier for your icon

+

+ Icon Identification +

+

+ Choose a unique identifier for your icon +

{ - if (!value) return "Icon name is required" + if (!value) return "Icon name is required"; if (!/^[a-z0-9-]+$/.test(value)) { - return "Icon name must contain only lowercase letters, numbers, and hyphens" + return "Icon name must contain only lowercase letters, numbers, and hyphens"; } // Check if icon already exists - const iconExists = existingIcons.some((icon) => icon.value === value) + const iconExists = existingIcons.some( + (icon) => icon.value === value, + ); if (iconExists) { - return "This icon already exists. Icon updates are not yet supported. Please choose a different name." + return "This icon already exists. Icon updates are not yet supported. Please choose a different name."; } - return undefined + return undefined; }, }} > @@ -390,9 +425,13 @@ export function AdvancedIconSubmissionFormTanStack() { value={field.state.value} onValueChange={field.handleChange} error={field.state.meta.errors.join(", ")} - isInvalid={!field.state.meta.isValid && field.state.meta.isTouched} + isInvalid={ + !field.state.meta.isValid && field.state.meta.isTouched + } /> -

Use lowercase letters, numbers, and hyphens only

+

+ Use lowercase letters, numbers, and hyphens only +

)} @@ -400,25 +439,44 @@ export function AdvancedIconSubmissionFormTanStack() { {/* Icon Preview Section */} {Object.keys(filePreviews).length > 0 && ( - ({ iconName: state.values.iconName, categories: state.values.categories })}> + ({ + iconName: state.values.iconName, + categories: state.values.categories, + })} + > {(state) => (
-

Icon Preview

-

How your icon will appear

+

+ Icon Preview +

+

+ How your icon will appear +

- {Object.entries(filePreviews).map(([variantId, preview]) => ( -
-
- {`${variantId} + {Object.entries(filePreviews).map( + ([variantId, preview]) => ( +
+
+ {`${variantId} +
+
+

+ {state.iconName || "preview"} +

+

+ {variantId} +

+
-
-

{state.iconName || "preview"}

-

{variantId}

-
-
- ))} + ), + )}
)} @@ -429,7 +487,28 @@ export function AdvancedIconSubmissionFormTanStack() {

Icon Variants

-

Select which variants you want to upload

+

+ Select which variants you want to upload +

+
+ +
+

+ File Format Requirements +

+
    +
  • + SVG format is strongly preferred over PNG + or WebP for better scalability and quality +
  • +
  • + + All icons must have a transparent background + {" "} + - submissions with opaque or colored backgrounds will be + rejected +
  • +
@@ -441,8 +520,9 @@ export function AdvancedIconSubmissionFormTanStack() { options={VARIANT_OPTIONS} defaultValue={field.state.value} onValueChange={(values) => { - const finalValues = handleVariantSelectionChange(values) - field.handleChange(finalValues) + const finalValues = + handleVariantSelectionChange(values); + field.handleChange(finalValues); }} placeholder="Select icon variants..." maxCount={5} @@ -451,18 +531,22 @@ export function AdvancedIconSubmissionFormTanStack() { resetOnDefaultValueChange={true} />

- Base icon is required and cannot be removed. Select additional variants as needed. + Base icon is required and cannot be removed. Select + additional variants as needed.

{/* Upload zones for selected variants - using field.state.value for reactivity */}
{field.state.value.map((variantId) => { - const variant = VARIANTS.find((v) => v.id === variantId) - if (!variant) return null + const variant = VARIANTS.find( + (v) => v.id === variantId, + ); + if (!variant) return null; - const hasFile = form.getFieldValue("files")[variant.id]?.length > 0 - const isBase = variant.id === "base" + const hasFile = + form.getFieldValue("files")[variant.id]?.length > 0; + const isBase = variant.id === "base"; return (
-

{variant.label}

+

+ {variant.label} +

{isBase && ( - + Required )} {hasFile && ( - + Uploaded )}
-

{variant.description}

+

+ {variant.description} +

handleFileDrop(variant.id, droppedFiles)} - onError={(error) => toast.error(error.message)} + onDrop={(droppedFiles) => + handleFileDrop(variant.id, droppedFiles) + } + onError={(error) => + toast.error(error.message) + } src={form.getFieldValue("files")[variant.id]} > @@ -529,7 +627,7 @@ export function AdvancedIconSubmissionFormTanStack() {
- ) + ); })}
@@ -541,7 +639,9 @@ export function AdvancedIconSubmissionFormTanStack() {

Icon Metadata

-

Provide additional information about your icon

+

+ Provide additional information about your icon +

{/* Categories */} @@ -553,7 +653,11 @@ export function AdvancedIconSubmissionFormTanStack() { {AVAILABLE_CATEGORIES.map((category) => ( toggleCategory(category)} > @@ -561,10 +665,15 @@ export function AdvancedIconSubmissionFormTanStack() { ))}
-

Select all categories that apply to your icon

- {!field.state.meta.isValid && field.state.meta.isTouched && ( -

{field.state.meta.errors.join(", ")}

- )} +

+ Select all categories that apply to your icon +

+ {!field.state.meta.isValid && + field.state.meta.isTouched && ( +

+ {field.state.meta.errors.join(", ")} +

+ )}
)} @@ -582,8 +691,8 @@ export function AdvancedIconSubmissionFormTanStack() { onChange={(e) => field.handleChange(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { - e.preventDefault() - handleAddAlias() + e.preventDefault(); + handleAddAlias(); } }} /> @@ -601,7 +710,11 @@ export function AdvancedIconSubmissionFormTanStack() { {field.state.value.length > 0 && (
{field.state.value.map((alias) => ( - + {alias}
{/* Description */} @@ -634,7 +749,9 @@ export function AdvancedIconSubmissionFormTanStack() { onChange={(e) => field.handleChange(e.target.value)} rows={3} /> -

This helps reviewers understand your submission

+

+ This helps reviewers understand your submission +

)} @@ -646,8 +763,8 @@ export function AdvancedIconSubmissionFormTanStack() { type="button" variant="outline" onClick={() => { - form.reset() - setFilePreviews({}) + form.reset(); + setFilePreviews({}); }} > Clear Form @@ -659,7 +776,11 @@ export function AdvancedIconSubmissionFormTanStack() { })} > {(state) => ( - )} @@ -669,5 +790,5 @@ export function AdvancedIconSubmissionFormTanStack() {
- ) + ); } From 24edae8dd6cffe16215ee48b5283636696770b5e Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Sun, 23 Nov 2025 08:57:19 +0100 Subject: [PATCH 6/6] wording changes