From b47314fcab940f5a040eea4d15219b08a103fe9e Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Tue, 2 Jun 2026 23:23:52 +0330 Subject: [PATCH] feat(admin): image thumbnails in lists + template image/demo fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AdminThumb: reusable thumbnail (raster + SVG via , dashed fallback on empty/broken) - AdminResource: ColumnDef gains type:"image" → renders thumbnails in tables - image thumbnail columns for categories, slides, home-events, internal routes; categories icon field now multiline (accepts raw SVG markup) - TemplatesAdmin: cover image / mini-demo / demo / full-demo upload fields (backed by existing container image/demo fields) + thumbnail column in the list Co-Authored-By: Claude Opus 4.8 --- src/components/admin/AdminResource.tsx | 4 ++- src/components/admin/AdminThumb.tsx | 46 ++++++++++++++++++++++++ src/components/admin/TemplatesAdmin.tsx | 26 +++++++++++--- src/components/admin/admin-resources.tsx | 6 +++- 4 files changed, 76 insertions(+), 6 deletions(-) create mode 100644 src/components/admin/AdminThumb.tsx diff --git a/src/components/admin/AdminResource.tsx b/src/components/admin/AdminResource.tsx index c97b9ff..f5e06c9 100644 --- a/src/components/admin/AdminResource.tsx +++ b/src/components/admin/AdminResource.tsx @@ -3,6 +3,7 @@ import { useCallback, useEffect, useState, type ReactNode } from "react"; import { FileUploadField } from "@/components/admin/FileUploadField"; +import { AdminThumb } from "@/components/admin/AdminThumb"; export interface FieldDef { key: string; @@ -17,6 +18,7 @@ export interface FieldDef { export interface ColumnDef { key: string; label: string; + type?: "text" | "image"; render?: (row: Record) => ReactNode; } @@ -162,7 +164,7 @@ export function AdminResource({ config }: { config: ResourceConfig }) { {config.columns.map((c) => ( - {c.render ? c.render(row) : formatCell(row[c.key])} + {c.render ? c.render(row) : c.type === "image" ? : formatCell(row[c.key])} ))} {(config.canEdit || config.canDelete || config.rowActions) && ( diff --git a/src/components/admin/AdminThumb.tsx b/src/components/admin/AdminThumb.tsx new file mode 100644 index 0000000..011ca55 --- /dev/null +++ b/src/components/admin/AdminThumb.tsx @@ -0,0 +1,46 @@ +"use client"; + +import { useState } from "react"; + +/** + * Small image thumbnail for admin tables/forms. Renders raster + SVG via . + * Falls back to a dashed placeholder when empty or the URL fails to load. + */ +export function AdminThumb({ + src, + alt = "", + size = 40, + rounded = "rounded-md", +}: { + src?: unknown; + alt?: string; + size?: number; + rounded?: string; +}) { + const url = typeof src === "string" ? src.trim() : ""; + const [failed, setFailed] = useState(false); + + if (!url || failed) { + return ( +
+ {failed ? "✕" : "—"} +
+ ); + } + return ( + // eslint-disable-next-line @next/next/no-img-element + {alt} setFailed(true)} + style={{ width: size, height: size }} + className={`border border-[#262b40] bg-[#0c0e1a] object-cover ${rounded}`} + /> + ); +} diff --git a/src/components/admin/TemplatesAdmin.tsx b/src/components/admin/TemplatesAdmin.tsx index 7611671..fa67c6e 100644 --- a/src/components/admin/TemplatesAdmin.tsx +++ b/src/components/admin/TemplatesAdmin.tsx @@ -2,11 +2,17 @@ import { useCallback, useEffect, useState } from "react"; +import { FileUploadField } from "@/components/admin/FileUploadField"; +import { AdminThumb } from "@/components/admin/AdminThumb"; + interface Container { id: string; slug: string; name: string; description?: string | null; + image?: string | null; + demo?: string | null; + mini_demo?: string | null; is_published?: boolean; is_premium?: boolean; is_mockup?: boolean; @@ -18,6 +24,7 @@ interface Ref { id: string; name: string; slug?: string } interface Detail extends Container { keywords?: string | null; news_text?: string | null; + full_demo?: string | null; categories?: Ref[]; tags?: Ref[]; projects?: { id: string; name: string; aspect?: string | null; resolution?: string }[]; @@ -34,11 +41,13 @@ const lbl = "mb-1 block text-xs font-medium text-gray-400"; interface FormState { slug: string; name: string; description: string; keywords: string; news_text: string; + image: string; demo: string; full_demo: string; mini_demo: string; is_published: boolean; is_premium: boolean; is_mockup: boolean; primary_mode: string; sort: number; category_ids: string[]; tag_ids: string[]; } const emptyForm: FormState = { slug: "", name: "", description: "", keywords: "", news_text: "", + image: "", demo: "", full_demo: "", mini_demo: "", is_published: false, is_premium: false, is_mockup: false, primary_mode: "FLEXIBLE", sort: 0, category_ids: [], tag_ids: [], }; @@ -105,7 +114,9 @@ export function TemplatesAdmin() { setProjects(d.projects ?? []); setForm({ slug: d.slug, name: d.name, description: d.description ?? "", keywords: d.keywords ?? "", - news_text: d.news_text ?? "", is_published: !!d.is_published, is_premium: !!d.is_premium, + news_text: d.news_text ?? "", image: d.image ?? "", demo: d.demo ?? "", + full_demo: d.full_demo ?? "", mini_demo: d.mini_demo ?? "", + is_published: !!d.is_published, is_premium: !!d.is_premium, is_mockup: !!d.is_mockup, primary_mode: d.primary_mode ?? "FLEXIBLE", sort: d.sort ?? 0, category_ids: (d.categories ?? []).map((c) => c.id), tag_ids: (d.tags ?? []).map((t) => t.id), }); @@ -151,18 +162,19 @@ export function TemplatesAdmin() { - + {loading ? ( - + ) : rows.length === 0 ? ( - + ) : rows.map((r) => ( +
NameSlugImageNameSlug StatusMode SortActions
Loading…
Loading…
No templates.
No templates.
{r.name} {r.slug} @@ -195,6 +207,12 @@ export function TemplatesAdmin() {
setForm({ ...form, slug: e.target.value })} />