Files
sass-mohfarawati/components/root/portfolio-project-form.tsx
T
2026-03-10 05:24:50 +01:00

868 lines
35 KiB
TypeScript

"use client";
import type { MediaKind, PortfolioAssetKind, PortfolioSectionType } from "@prisma/client";
import { ArrowDown, ArrowUp, CalendarDays, FolderTree, Link2, Plus, Text, Trash2, UserRound } from "lucide-react";
import { useEffect, useMemo, useRef, useState } from "react";
import { MediaFieldPicker, type MediaFieldState } from "@/components/root/media-field-picker";
import { AppCard } from "@/components/ui/app-card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { moveArrayItem } from "@/lib/array";
import type { MediaOption } from "@/lib/media";
import type { PortfolioCategoryView, PortfolioProjectView } from "@/lib/portfolio";
import { cn } from "@/lib/utils";
type PanelKey = "basic" | "localized" | "sections" | "assets";
type SectionFormValue = {
id?: string;
type: PortfolioSectionType;
titleAr: string;
titleEn: string;
titleDe: string;
bodyAr: string;
bodyEn: string;
bodyDe: string;
imagePath: string;
media: MediaFieldState;
linkUrl: string;
sortOrder: number;
};
type AssetFormValue = {
id?: string;
kind: PortfolioAssetKind;
filePath: string;
fileFieldName: string;
media: MediaFieldState;
altAr: string;
altEn: string;
altDe: string;
sortOrder: number;
};
type ProjectFormState = {
categoryId: string;
slug: string;
clientName: string;
projectYear: string;
previewUrl: string;
sortOrder: string;
isFeatured: boolean;
isPublished: boolean;
titleAr: string;
titleEn: string;
titleDe: string;
serviceLabelAr: string;
serviceLabelEn: string;
serviceLabelDe: string;
summaryAr: string;
summaryEn: string;
summaryDe: string;
};
type PortfolioProjectFormProps = {
action: (formData: FormData) => void | Promise<void>;
categories: PortfolioCategoryView[];
mediaOptions: MediaOption[];
project?: PortfolioProjectView | null;
formId: string;
redirectPath: string;
};
const localeFieldConfig = [
{ key: "ar" as const, suffix: "Ar" as const, label: "Arabic" },
{ key: "en" as const, suffix: "En" as const, label: "English" },
{ key: "de" as const, suffix: "De" as const, label: "German" },
] as const;
const sectionTypeOptions: PortfolioSectionType[] = [
"RICH_TEXT",
"GALLERY",
"STATS",
"DELIVERABLES",
"LINK",
];
const assetKindOptions: PortfolioAssetKind[] = ["IMAGE", "DOCUMENT"];
function getSectionTypeLabel(type: PortfolioSectionType) {
switch (type) {
case "RICH_TEXT":
return "Rich Text";
case "GALLERY":
return "Single Image";
case "STATS":
return "Stats";
case "DELIVERABLES":
return "Deliverables";
case "LINK":
return "Link";
default:
return type;
}
}
function createMediaFieldState(params: {
kind: MediaKind;
assetId?: string | null;
url?: string | null;
label?: string | null;
}): MediaFieldState {
return {
mode: params.assetId ? "library" : params.url ? "external" : "upload",
assetId: params.assetId ?? "",
url: params.url ?? "",
label: params.label ?? "",
kind: params.kind,
};
}
function createInitialProjectState(
project: PortfolioProjectView | null | undefined,
categories: PortfolioCategoryView[],
): ProjectFormState {
return {
categoryId: project?.category.id ?? categories[0]?.id ?? "",
slug: project?.slug ?? "",
clientName: project?.clientName ?? "",
projectYear: String(project?.projectYear ?? new Date().getFullYear()),
previewUrl: project?.previewUrl ?? "",
sortOrder: String(project?.sortOrder ?? 0),
isFeatured: project?.isFeatured ?? false,
isPublished: project?.isPublished ?? false,
titleAr: project?.title.ar ?? "",
titleEn: project?.title.en ?? "",
titleDe: project?.title.de ?? "",
serviceLabelAr: project?.serviceLabel.ar ?? "",
serviceLabelEn: project?.serviceLabel.en ?? "",
serviceLabelDe: project?.serviceLabel.de ?? "",
summaryAr: project?.summary.ar ?? "",
summaryEn: project?.summary.en ?? "",
summaryDe: project?.summary.de ?? "",
};
}
function createEmptySection(index: number): SectionFormValue {
return {
type: "RICH_TEXT",
titleAr: "",
titleEn: "",
titleDe: "",
bodyAr: "",
bodyEn: "",
bodyDe: "",
imagePath: "",
media: createMediaFieldState({ kind: "IMAGE" }),
linkUrl: "",
sortOrder: index,
};
}
function createEmptyAsset(index: number): AssetFormValue {
return {
kind: "IMAGE",
filePath: "",
fileFieldName: `asset-upload-${index}`,
media: createMediaFieldState({ kind: "IMAGE" }),
altAr: "",
altEn: "",
altDe: "",
sortOrder: index,
};
}
function getAssetKindLabel(kind: PortfolioAssetKind) {
return kind === "IMAGE" ? "Image" : "Document";
}
function hasText(value: string) {
return value.trim().length > 0;
}
function isSectionComplete(section: SectionFormValue) {
const hasLocalizedTitle =
hasText(section.titleAr) &&
hasText(section.titleEn) &&
hasText(section.titleDe);
if (!hasLocalizedTitle) {
return false;
}
if (section.type === "GALLERY") {
return hasText(section.media.assetId) || hasText(section.imagePath);
}
if (section.type === "LINK") {
return hasText(section.linkUrl);
}
return (
hasText(section.bodyAr) &&
hasText(section.bodyEn) &&
hasText(section.bodyDe)
);
}
function isAssetComplete(asset: AssetFormValue) {
return (
(hasText(asset.media.assetId) || hasText(asset.media.url)) &&
hasText(asset.altAr) &&
hasText(asset.altEn) &&
hasText(asset.altDe)
);
}
function PanelButton({
active,
title,
done,
onClick,
}: {
active: boolean;
title: string;
done: boolean;
onClick: () => void;
}) {
return (
<button
type="button"
onClick={onClick}
className={cn(
"flex w-full items-center justify-between rounded-nested border px-4 py-3 text-left transition-colors",
active
? "border-input bg-accent/40"
: "border-input bg-background hover:bg-accent/20",
)}
>
<span className="text-sm font-medium text-foreground">{title}</span>
<Badge variant={done ? "success" : "outline"}>
{done ? "Ready" : "Open"}
</Badge>
</button>
);
}
function LocaleBlock({
title,
renderField,
}: {
title: string;
renderField: (locale: (typeof localeFieldConfig)[number]) => React.ReactNode;
}) {
return (
<AppCard level={2} padding="sm" className="space-y-3">
<p className="text-sm font-medium text-foreground">{title}</p>
<div className="grid gap-4 xl:grid-cols-3">
{localeFieldConfig.map((locale) => (
<AppCard key={`${title}-${locale.key}`} padding="sm" className="space-y-2 rounded-nested">
<p className="text-sm font-medium text-foreground">{locale.label}</p>
{renderField(locale)}
</AppCard>
))}
</div>
</AppCard>
);
}
export function PortfolioProjectForm({
action,
categories,
mediaOptions,
project,
formId,
redirectPath,
}: PortfolioProjectFormProps) {
const [activePanel, setActivePanel] = useState<PanelKey>("basic");
const [projectState, setProjectState] = useState<ProjectFormState>(
createInitialProjectState(project, categories),
);
const [coverMedia, setCoverMedia] = useState<MediaFieldState>(
createMediaFieldState({
kind: "IMAGE",
assetId: project?.coverMediaAssetId,
url: project?.coverImagePath,
label: project?.title.de ?? project?.title.en ?? project?.title.ar ?? "",
}),
);
const [sections, setSections] = useState<SectionFormValue[]>(
project?.sections.length
? project.sections.map((section, index) => ({
id: section.id,
type: section.type,
titleAr: section.title.ar,
titleEn: section.title.en,
titleDe: section.title.de,
bodyAr: section.body.ar,
bodyEn: section.body.en,
bodyDe: section.body.de,
imagePath: section.imagePath ?? "",
media: createMediaFieldState({
kind: "IMAGE",
assetId: section.mediaAssetId,
url: section.imagePath,
label: section.title.de || section.title.en || section.title.ar,
}),
linkUrl: section.linkUrl ?? "",
sortOrder: index,
}))
: [createEmptySection(0)],
);
const [assets, setAssets] = useState<AssetFormValue[]>(
project?.assets.length
? project.assets.map((asset, index) => ({
id: asset.id,
kind: asset.kind,
filePath: asset.filePath,
fileFieldName: `asset-upload-${index}`,
media: createMediaFieldState({
kind: asset.kind,
assetId: asset.mediaAssetId,
url: asset.filePath,
label: asset.alt.de || asset.alt.en || asset.alt.ar,
}),
altAr: asset.alt.ar,
altEn: asset.alt.en,
altDe: asset.alt.de,
sortOrder: index,
}))
: [createEmptyAsset(0)],
);
const [selectedSectionIndex, setSelectedSectionIndex] = useState(0);
const [selectedAssetIndex, setSelectedAssetIndex] = useState(0);
const sectionsInputRef = useRef<HTMLInputElement | null>(null);
const assetsInputRef = useRef<HTMLInputElement | null>(null);
const selectedSection = sections[selectedSectionIndex];
const selectedAsset = assets[selectedAssetIndex];
const sectionsPayload = JSON.stringify(
sections.map((section, index) => ({
...section,
sortOrder: index,
})),
);
const assetsPayload = JSON.stringify(
assets.map((asset, index) => ({
...asset,
sortOrder: index,
})),
);
useEffect(() => {
const hiddenInput = sectionsInputRef.current;
if (!hiddenInput) {
return;
}
hiddenInput.dispatchEvent(new Event("input", { bubbles: true }));
hiddenInput.dispatchEvent(new Event("change", { bubbles: true }));
}, [sectionsPayload]);
useEffect(() => {
const hiddenInput = assetsInputRef.current;
if (!hiddenInput) {
return;
}
hiddenInput.dispatchEvent(new Event("input", { bubbles: true }));
hiddenInput.dispatchEvent(new Event("change", { bubbles: true }));
}, [assetsPayload]);
useEffect(() => {
if (selectedSectionIndex > sections.length - 1) {
setSelectedSectionIndex(Math.max(0, sections.length - 1));
}
}, [sections.length, selectedSectionIndex]);
useEffect(() => {
if (selectedAssetIndex > assets.length - 1) {
setSelectedAssetIndex(Math.max(0, assets.length - 1));
}
}, [assets.length, selectedAssetIndex]);
const validation = useMemo(() => {
const basicDone =
hasText(projectState.categoryId) &&
hasText(projectState.slug) &&
hasText(projectState.clientName) &&
hasText(projectState.projectYear) &&
hasText(projectState.sortOrder);
const localizedDone = localeFieldConfig.every((locale) =>
hasText(projectState[`title${locale.suffix}`]) &&
hasText(projectState[`serviceLabel${locale.suffix}`]) &&
hasText(projectState[`summary${locale.suffix}`]),
);
const sectionsDone = sections.length > 0 && sections.every(isSectionComplete);
const assetsDone = assets.length > 0 && assets.every(isAssetComplete);
return {
basicDone,
localizedDone,
sectionsDone,
assetsDone,
allDone: basicDone && localizedDone && sectionsDone && assetsDone,
};
}, [assets, projectState, sections]);
const setProjectField = <K extends keyof ProjectFormState>(key: K, value: ProjectFormState[K]) => {
setProjectState((current) => ({
...current,
[key]: value,
}));
};
const updateSection = (index: number, nextValue: SectionFormValue) => {
setSections((current) =>
current.map((item, currentIndex) => (currentIndex === index ? nextValue : item)),
);
};
const updateAsset = (index: number, nextValue: AssetFormValue) => {
setAssets((current) =>
current.map((item, currentIndex) => (currentIndex === index ? nextValue : item)),
);
};
return (
<form id={formId} action={action} className="space-y-6">
<input type="hidden" name="id" value={project?.id ?? ""} />
<input type="hidden" name="redirectPath" value={redirectPath} />
<input type="hidden" name="currentCoverImagePath" value={project?.coverImagePath ?? ""} />
<input ref={sectionsInputRef} type="hidden" name="sections" value={sectionsPayload} />
<input ref={assetsInputRef} type="hidden" name="assets" value={assetsPayload} />
<div className="grid gap-6 xl:grid-cols-[280px_minmax(0,1fr)]">
<AppCard level={2}>
<CardContent className="space-y-3 p-4">
<PanelButton active={activePanel === "basic"} title="Basic Info" done={validation.basicDone} onClick={() => setActivePanel("basic")} />
<PanelButton active={activePanel === "localized"} title="Localized Content" done={validation.localizedDone} onClick={() => setActivePanel("localized")} />
<PanelButton active={activePanel === "sections"} title="Sections" done={validation.sectionsDone} onClick={() => setActivePanel("sections")} />
<PanelButton active={activePanel === "assets"} title="Assets" done={validation.assetsDone} onClick={() => setActivePanel("assets")} />
</CardContent>
</AppCard>
<div className="space-y-6">
<section className={cn(activePanel !== "basic" && "hidden")}>
<AppCard>
<CardHeader>
<CardTitle>Basic Info</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="categoryId">Category</Label>
<div className="relative">
<FolderTree className="pointer-events-none absolute left-3 top-1/2 z-10 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Select name="categoryId" value={projectState.categoryId} onValueChange={(value) => setProjectField("categoryId", value)} required>
<SelectTrigger id="categoryId" className="pl-9">
<SelectValue placeholder="Category" />
</SelectTrigger>
<SelectContent>
{categories.map((category) => (
<SelectItem key={category.id} value={category.id}>
{category.name.de} / {category.name.en}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="slug">Slug</Label>
<div className="relative">
<Text className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input id="slug" name="slug" value={projectState.slug} onChange={(event) => setProjectField("slug", event.target.value)} className="pl-9" required />
</div>
</div>
<div className="space-y-2">
<Label htmlFor="clientName">Client</Label>
<div className="relative">
<UserRound className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input id="clientName" name="clientName" value={projectState.clientName} onChange={(event) => setProjectField("clientName", event.target.value)} className="pl-9" required />
</div>
</div>
<div className="space-y-2">
<Label htmlFor="projectYear">Project Year</Label>
<div className="relative">
<CalendarDays className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input id="projectYear" name="projectYear" type="number" min="2000" max="2100" value={projectState.projectYear} onChange={(event) => setProjectField("projectYear", event.target.value)} className="pl-9" required />
</div>
</div>
<div className="space-y-2">
<Label htmlFor="previewUrl">Preview URL</Label>
<div className="relative">
<Link2 className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input id="previewUrl" name="previewUrl" type="url" value={projectState.previewUrl} onChange={(event) => setProjectField("previewUrl", event.target.value)} className="pl-9" />
</div>
</div>
<div className="space-y-2">
<Label htmlFor="sortOrder">Sort Order</Label>
<div className="relative">
<Text className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input id="sortOrder" name="sortOrder" type="number" min="0" value={projectState.sortOrder} onChange={(event) => setProjectField("sortOrder", event.target.value)} className="pl-9" required />
</div>
</div>
</div>
<MediaFieldPicker
title="Cover Image"
value={coverMedia}
onChange={setCoverMedia}
options={mediaOptions}
inputName="coverMedia"
fileFieldName="coverFile"
accept="image/*,.svg"
allowExternal={false}
/>
<div className="grid gap-4 md:grid-cols-2">
<label className="flex items-center gap-3 rounded-nested border border-input bg-card px-4 py-3 text-sm">
<input type="checkbox" name="isFeatured" checked={projectState.isFeatured} onChange={(event) => setProjectField("isFeatured", event.target.checked)} />
Featured
</label>
<label className="flex items-center gap-3 rounded-nested border border-input bg-card px-4 py-3 text-sm">
<input type="checkbox" name="isPublished" checked={projectState.isPublished} onChange={(event) => setProjectField("isPublished", event.target.checked)} />
Published
</label>
</div>
</CardContent>
</AppCard>
</section>
<section className={cn(activePanel !== "localized" && "hidden")}>
<AppCard>
<CardHeader>
<CardTitle>Localized Content</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<LocaleBlock
title="Project Title"
renderField={(locale) => (
<Input
id={`title${locale.suffix}`}
name={`title${locale.suffix}`}
value={projectState[`title${locale.suffix}`]}
onChange={(event) => setProjectField(`title${locale.suffix}`, event.target.value)}
/>
)}
/>
<LocaleBlock
title="Service Label"
renderField={(locale) => (
<Input
id={`serviceLabel${locale.suffix}`}
name={`serviceLabel${locale.suffix}`}
value={projectState[`serviceLabel${locale.suffix}`]}
onChange={(event) => setProjectField(`serviceLabel${locale.suffix}`, event.target.value)}
/>
)}
/>
<LocaleBlock
title="Summary"
renderField={(locale) => (
<Textarea
id={`summary${locale.suffix}`}
name={`summary${locale.suffix}`}
rows={5}
value={projectState[`summary${locale.suffix}`]}
onChange={(event) => setProjectField(`summary${locale.suffix}`, event.target.value)}
/>
)}
/>
</CardContent>
</AppCard>
</section>
<section className={cn(activePanel !== "sections" && "hidden")}>
<AppCard>
<CardHeader className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<CardTitle>Sections</CardTitle>
<Button
type="button"
variant="outline"
onClick={() => {
setSections((current) => [...current, createEmptySection(current.length)]);
setSelectedSectionIndex(sections.length);
}}
>
<Plus className="h-4 w-4" />
Add Section
</Button>
</CardHeader>
<CardContent className="grid gap-4 xl:grid-cols-3">
<div className="space-y-3">
{sections.map((section, index) => (
<button
key={section.id ?? `section-${index}`}
type="button"
onClick={() => setSelectedSectionIndex(index)}
className={cn(
"w-full rounded-nested border p-4 text-left transition-colors",
selectedSectionIndex === index
? "border-input bg-accent/40"
: "border-input bg-background hover:bg-accent/20",
)}
>
<div className="flex items-center justify-between gap-3">
<div>
<p className="text-sm font-medium text-foreground">
{section.titleDe || section.titleEn || section.titleAr || `Section ${index + 1}`}
</p>
<p className="mt-1 text-xs text-muted-foreground">{getSectionTypeLabel(section.type)}</p>
</div>
<Badge variant={isSectionComplete(section) ? "success" : "outline"}>
{isSectionComplete(section) ? "Ready" : "Open"}
</Badge>
</div>
</button>
))}
</div>
<div className="xl:col-span-2">
{selectedSection ? (
<AppCard level={2}>
<CardContent className="space-y-4 p-5">
<div className="flex flex-wrap items-center justify-between gap-3">
<p className="text-sm font-medium text-foreground">{`Section ${selectedSectionIndex + 1}`}</p>
<div className="flex flex-wrap gap-2">
<Button type="button" variant="outline" onClick={() => {
setSections((current) => moveArrayItem(current, selectedSectionIndex, selectedSectionIndex - 1));
setSelectedSectionIndex((current) => Math.max(0, current - 1));
}} disabled={selectedSectionIndex === 0}>
<ArrowUp className="h-4 w-4" />
</Button>
<Button type="button" variant="outline" onClick={() => {
setSections((current) => moveArrayItem(current, selectedSectionIndex, selectedSectionIndex + 1));
setSelectedSectionIndex((current) => Math.min(sections.length - 1, current + 1));
}} disabled={selectedSectionIndex === sections.length - 1}>
<ArrowDown className="h-4 w-4" />
</Button>
<Button type="button" variant="ghost" className="text-destructive" onClick={() => {
if (sections.length === 1) {
return;
}
setSections((current) => current.filter((_, index) => index !== selectedSectionIndex));
setSelectedSectionIndex((current) => Math.max(0, current - 1));
}}>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label>Type</Label>
<Select value={selectedSection.type} onValueChange={(value) => updateSection(selectedSectionIndex, { ...selectedSection, type: value as PortfolioSectionType })}>
<SelectTrigger>
<SelectValue placeholder="Type" />
</SelectTrigger>
<SelectContent>
{sectionTypeOptions.map((type) => (
<SelectItem key={type} value={type}>{getSectionTypeLabel(type)}</SelectItem>
))}
</SelectContent>
</Select>
</div>
{selectedSection.type === "LINK" ? (
<div className="space-y-2">
<Label>Link URL</Label>
<div className="relative">
<Link2 className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input value={selectedSection.linkUrl} onChange={(event) => updateSection(selectedSectionIndex, { ...selectedSection, linkUrl: event.target.value })} className="pl-9" />
</div>
</div>
) : null}
</div>
{selectedSection.type === "GALLERY" ? (
<MediaFieldPicker
title="Single Image"
value={selectedSection.media}
onChange={(media) => updateSection(selectedSectionIndex, { ...selectedSection, media, imagePath: media.url })}
options={mediaOptions}
inputName={`section-media-${selectedSectionIndex}`}
fileFieldName={`section-image-upload-${selectedSectionIndex}`}
accept="image/*,.svg"
allowExternal={false}
/>
) : null}
<LocaleBlock
title="Section Title"
renderField={(locale) => (
<Input
value={selectedSection[`title${locale.suffix}`]}
onChange={(event) => updateSection(selectedSectionIndex, { ...selectedSection, [`title${locale.suffix}`]: event.target.value })}
/>
)}
/>
{selectedSection.type !== "GALLERY" && selectedSection.type !== "LINK" ? (
<LocaleBlock
title="Section Body"
renderField={(locale) => (
<Textarea
rows={5}
value={selectedSection[`body${locale.suffix}`]}
onChange={(event) => updateSection(selectedSectionIndex, { ...selectedSection, [`body${locale.suffix}`]: event.target.value })}
/>
)}
/>
) : null}
</CardContent>
</AppCard>
) : null}
</div>
</CardContent>
</AppCard>
</section>
<section className={cn(activePanel !== "assets" && "hidden")}>
<AppCard>
<CardHeader className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<CardTitle>Assets</CardTitle>
<Button
type="button"
variant="outline"
onClick={() => {
setAssets((current) => [...current, createEmptyAsset(current.length)]);
setSelectedAssetIndex(assets.length);
}}
>
<Plus className="h-4 w-4" />
Add Asset
</Button>
</CardHeader>
<CardContent className="grid gap-4 xl:grid-cols-3">
<div className="space-y-3">
{assets.map((asset, index) => (
<button
key={asset.id ?? `asset-${index}`}
type="button"
onClick={() => setSelectedAssetIndex(index)}
className={cn(
"w-full rounded-nested border p-4 text-left transition-colors",
selectedAssetIndex === index
? "border-input bg-accent/40"
: "border-input bg-background hover:bg-accent/20",
)}
>
<div className="flex items-center justify-between gap-3">
<div>
<p className="text-sm font-medium text-foreground">
{asset.altDe || asset.altEn || asset.altAr || `Asset ${index + 1}`}
</p>
<p className="mt-1 text-xs text-muted-foreground">{getAssetKindLabel(asset.kind)}</p>
</div>
<Badge variant={isAssetComplete(asset) ? "success" : "outline"}>
{isAssetComplete(asset) ? "Ready" : "Open"}
</Badge>
</div>
</button>
))}
</div>
<div className="xl:col-span-2">
{selectedAsset ? (
<AppCard level={2}>
<CardContent className="space-y-4 p-5">
<div className="flex flex-wrap items-center justify-between gap-3">
<p className="text-sm font-medium text-foreground">{`Asset ${selectedAssetIndex + 1}`}</p>
<div className="flex flex-wrap gap-2">
<Button type="button" variant="outline" onClick={() => {
setAssets((current) => moveArrayItem(current, selectedAssetIndex, selectedAssetIndex - 1));
setSelectedAssetIndex((current) => Math.max(0, current - 1));
}} disabled={selectedAssetIndex === 0}>
<ArrowUp className="h-4 w-4" />
</Button>
<Button type="button" variant="outline" onClick={() => {
setAssets((current) => moveArrayItem(current, selectedAssetIndex, selectedAssetIndex + 1));
setSelectedAssetIndex((current) => Math.min(assets.length - 1, current + 1));
}} disabled={selectedAssetIndex === assets.length - 1}>
<ArrowDown className="h-4 w-4" />
</Button>
<Button type="button" variant="ghost" className="text-destructive" onClick={() => {
if (assets.length === 1) {
return;
}
setAssets((current) => current.filter((_, index) => index !== selectedAssetIndex));
setSelectedAssetIndex((current) => Math.max(0, current - 1));
}}>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
<div className="space-y-2">
<Label>Type</Label>
<Select value={selectedAsset.kind} onValueChange={(value) => updateAsset(selectedAssetIndex, { ...selectedAsset, kind: value as PortfolioAssetKind, media: { ...selectedAsset.media, kind: value as MediaKind } })}>
<SelectTrigger>
<SelectValue placeholder="Type" />
</SelectTrigger>
<SelectContent>
{assetKindOptions.map((kind) => (
<SelectItem key={kind} value={kind}>{getAssetKindLabel(kind)}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<MediaFieldPicker
title="Asset"
value={selectedAsset.media}
onChange={(media) => updateAsset(selectedAssetIndex, { ...selectedAsset, media, filePath: media.url })}
options={mediaOptions}
inputName={`asset-media-${selectedAssetIndex}`}
fileFieldName={selectedAsset.fileFieldName}
accept={selectedAsset.kind === "IMAGE" ? "image/*,.svg" : ".pdf,.doc,.docx,.ppt,.pptx"}
allowExternal={false}
/>
<LocaleBlock
title={selectedAsset.kind === "IMAGE" ? "Alt Text" : "Document Label"}
renderField={(locale) => (
<Input
value={selectedAsset[`alt${locale.suffix}`]}
onChange={(event) => updateAsset(selectedAssetIndex, { ...selectedAsset, [`alt${locale.suffix}`]: event.target.value })}
/>
)}
/>
</CardContent>
</AppCard>
) : null}
</div>
</CardContent>
</AppCard>
</section>
</div>
</div>
</form>
);
}