742 lines
27 KiB
TypeScript
742 lines
27 KiB
TypeScript
"use client";
|
|
|
|
import type { MediaKind, PortfolioAssetKind, PortfolioSectionType } from "@prisma/client";
|
|
import {
|
|
ArrowDown,
|
|
ArrowUp,
|
|
BriefcaseBusiness,
|
|
CalendarRange,
|
|
FolderTree,
|
|
Hash,
|
|
Link2,
|
|
Plus,
|
|
Sparkles,
|
|
Trash2,
|
|
} from "lucide-react";
|
|
import { useState } from "react";
|
|
|
|
import { MediaFieldPicker, type MediaFieldState } from "@/components/root/media-field-picker";
|
|
import { AppCard } from "@/components/ui/app-card";
|
|
import { Button } from "@/components/ui/button";
|
|
import { CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { moveArrayItem } from "@/lib/array";
|
|
import type { MediaOption } from "@/lib/media";
|
|
import type { PortfolioCategoryView, PortfolioProjectView } from "@/lib/portfolio";
|
|
|
|
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 PortfolioProjectFormProps = {
|
|
action: (formData: FormData) => void | Promise<void>;
|
|
categories: PortfolioCategoryView[];
|
|
mediaOptions: MediaOption[];
|
|
project?: PortfolioProjectView | null;
|
|
formId: string;
|
|
redirectPath: string;
|
|
submitLabel: string;
|
|
};
|
|
|
|
const sectionTypeOptions: PortfolioSectionType[] = [
|
|
"RICH_TEXT",
|
|
"GALLERY",
|
|
"STATS",
|
|
"DELIVERABLES",
|
|
"LINK",
|
|
];
|
|
|
|
const assetKindOptions: PortfolioAssetKind[] = ["IMAGE", "DOCUMENT"];
|
|
const localeFieldConfig = [
|
|
{ key: "ar" as const, suffix: "Ar" as const, label: "AR" },
|
|
{ key: "en" as const, suffix: "En" as const, label: "EN" },
|
|
{ key: "de" as const, suffix: "De" as const, label: "DE" },
|
|
];
|
|
|
|
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 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,
|
|
};
|
|
}
|
|
|
|
export function PortfolioProjectForm({
|
|
action,
|
|
categories,
|
|
mediaOptions,
|
|
project,
|
|
formId,
|
|
redirectPath,
|
|
submitLabel,
|
|
}: PortfolioProjectFormProps) {
|
|
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,
|
|
}))
|
|
: [],
|
|
);
|
|
const [isFeatured, setIsFeatured] = useState(project?.isFeatured ?? false);
|
|
const [isPublished, setIsPublished] = useState(project?.isPublished ?? false);
|
|
|
|
const sectionsPayload = JSON.stringify(
|
|
sections.map((section, index) => ({
|
|
...section,
|
|
sortOrder: index,
|
|
})),
|
|
);
|
|
|
|
const assetsPayload = JSON.stringify(
|
|
assets.map((asset, index) => ({
|
|
...asset,
|
|
sortOrder: index,
|
|
})),
|
|
);
|
|
|
|
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 type="hidden" name="sections" value={sectionsPayload} />
|
|
<input type="hidden" name="assets" value={assetsPayload} />
|
|
|
|
<AppCard>
|
|
<CardHeader>
|
|
<CardTitle>Projekt Basisdaten</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="grid gap-4 md:grid-cols-2">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="categoryId" className="sr-only">Kategorie</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"
|
|
defaultValue={project?.category.id ?? categories[0]?.id ?? ""}
|
|
required
|
|
>
|
|
<SelectTrigger id="categoryId" className="pl-9">
|
|
<SelectValue placeholder="Kategorie" />
|
|
</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" className="sr-only">Slug</Label>
|
|
<div className="relative">
|
|
<Hash className="pointer-events-none absolute left-3 top-1/2 z-10 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
<Input
|
|
id="slug"
|
|
name="slug"
|
|
defaultValue={project?.slug ?? ""}
|
|
placeholder="Slug"
|
|
className="pl-9"
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="clientName" className="sr-only">Kundenname</Label>
|
|
<div className="relative">
|
|
<BriefcaseBusiness className="pointer-events-none absolute left-3 top-1/2 z-10 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
<Input
|
|
id="clientName"
|
|
name="clientName"
|
|
defaultValue={project?.clientName ?? ""}
|
|
placeholder="Kundenname"
|
|
className="pl-9"
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="projectYear" className="sr-only">Projektjahr</Label>
|
|
<div className="relative">
|
|
<CalendarRange className="pointer-events-none absolute left-3 top-1/2 z-10 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
<Input
|
|
id="projectYear"
|
|
name="projectYear"
|
|
type="number"
|
|
min="2000"
|
|
max="2100"
|
|
defaultValue={project?.projectYear ?? new Date().getFullYear()}
|
|
placeholder="Projektjahr"
|
|
className="pl-9"
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="previewUrl" className="sr-only">Preview URL</Label>
|
|
<div className="relative">
|
|
<Link2 className="pointer-events-none absolute left-3 top-1/2 z-10 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
<Input
|
|
id="previewUrl"
|
|
name="previewUrl"
|
|
type="url"
|
|
defaultValue={project?.previewUrl ?? ""}
|
|
placeholder="https://example.com"
|
|
className="pl-9"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="sortOrder" className="sr-only">Sortierung</Label>
|
|
<div className="relative">
|
|
<Hash className="pointer-events-none absolute left-3 top-1/2 z-10 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
<Input
|
|
id="sortOrder"
|
|
name="sortOrder"
|
|
type="number"
|
|
min="0"
|
|
defaultValue={project?.sortOrder ?? 0}
|
|
placeholder="Sortierung"
|
|
className="pl-9"
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2 md:col-span-2">
|
|
<MediaFieldPicker
|
|
title="Cover Bild"
|
|
value={coverMedia}
|
|
onChange={setCoverMedia}
|
|
options={mediaOptions}
|
|
inputName="coverMedia"
|
|
fileFieldName="coverFile"
|
|
accept="image/*,.svg"
|
|
/>
|
|
</div>
|
|
|
|
<label className="flex items-center gap-3 rounded-md border border-border px-4 py-3 text-sm">
|
|
<Checkbox
|
|
name="isFeatured"
|
|
checked={isFeatured}
|
|
onCheckedChange={(checked) => setIsFeatured(checked === true)}
|
|
/>
|
|
<Sparkles className="h-4 w-4 text-muted-foreground" />
|
|
Hervorgehoben
|
|
</label>
|
|
|
|
<label className="flex items-center gap-3 rounded-md border border-border px-4 py-3 text-sm">
|
|
<Checkbox
|
|
name="isPublished"
|
|
checked={isPublished}
|
|
onCheckedChange={(checked) => setIsPublished(checked === true)}
|
|
/>
|
|
<Sparkles className="h-4 w-4 text-muted-foreground" />
|
|
Veroeffentlicht
|
|
</label>
|
|
</CardContent>
|
|
</AppCard>
|
|
|
|
<AppCard>
|
|
<CardHeader>
|
|
<CardTitle>Lokalisierte Inhalte</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Tabs defaultValue="de">
|
|
<TabsList>
|
|
{localeFieldConfig.map((locale) => (
|
|
<TabsTrigger key={locale.key} value={locale.key}>
|
|
{locale.label}
|
|
</TabsTrigger>
|
|
))}
|
|
</TabsList>
|
|
{localeFieldConfig.map((locale) => (
|
|
<TabsContent key={locale.key} value={locale.key}>
|
|
<div className="grid gap-4 rounded-surface border border-border p-4 md:grid-cols-2">
|
|
<div className="md:col-span-2 text-sm text-muted-foreground">
|
|
{locale.key === "ar"
|
|
? "Arabische Inhalte fuer die arabische Website."
|
|
: locale.key === "en"
|
|
? "Englische Inhalte fuer die englische Website."
|
|
: "Deutsche Inhalte fuer die deutsche Website."}
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor={`title${locale.suffix}`}>{`Titel ${locale.label}`}</Label>
|
|
<Input
|
|
id={`title${locale.suffix}`}
|
|
name={`title${locale.suffix}`}
|
|
defaultValue={project?.title[locale.key] ?? ""}
|
|
required
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor={`serviceLabel${locale.suffix}`}>{`Leistungslabel ${locale.label}`}</Label>
|
|
<Input
|
|
id={`serviceLabel${locale.suffix}`}
|
|
name={`serviceLabel${locale.suffix}`}
|
|
defaultValue={project?.serviceLabel[locale.key] ?? ""}
|
|
required
|
|
/>
|
|
</div>
|
|
<div className="space-y-2 md:col-span-2">
|
|
<Label htmlFor={`summary${locale.suffix}`}>{`Kurzbeschreibung ${locale.label}`}</Label>
|
|
<Textarea
|
|
id={`summary${locale.suffix}`}
|
|
name={`summary${locale.suffix}`}
|
|
defaultValue={project?.summary[locale.key] ?? ""}
|
|
rows={4}
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
</TabsContent>
|
|
))}
|
|
</Tabs>
|
|
</CardContent>
|
|
</AppCard>
|
|
|
|
<AppCard>
|
|
<CardHeader className="flex flex-row items-center justify-between">
|
|
<CardTitle>Abschnitte</CardTitle>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={() => setSections((current) => [...current, createEmptySection(current.length)])}
|
|
>
|
|
<Plus className="h-4 w-4" />
|
|
Abschnitt hinzufuegen
|
|
</Button>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{sections.map((section, index) => (
|
|
<AppCard key={section.id ?? `${section.type}-${index}`} level={2}>
|
|
<CardContent className="space-y-4 p-4">
|
|
<div className="flex items-center justify-between gap-4">
|
|
<p className="text-sm font-medium text-foreground">{`Abschnitt #${index + 1}`}</p>
|
|
<div className="flex gap-2">
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={() =>
|
|
setSections((current) => moveArrayItem(current, index, index - 1))
|
|
}
|
|
disabled={index === 0}
|
|
>
|
|
<ArrowUp className="h-4 w-4" />
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={() =>
|
|
setSections((current) => moveArrayItem(current, index, index + 1))
|
|
}
|
|
disabled={index === sections.length - 1}
|
|
>
|
|
<ArrowDown className="h-4 w-4" />
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
className="text-destructive"
|
|
onClick={() =>
|
|
setSections((current) => current.filter((_, currentIndex) => currentIndex !== index))
|
|
}
|
|
disabled={sections.length === 1}
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
Entfernen
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
<div className="space-y-2">
|
|
<Label>Typ</Label>
|
|
<Select
|
|
value={section.type}
|
|
onValueChange={(value) =>
|
|
setSections((current) =>
|
|
current.map((item, currentIndex) =>
|
|
currentIndex === index
|
|
? { ...item, type: value as PortfolioSectionType }
|
|
: item,
|
|
),
|
|
)
|
|
}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Typ" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{sectionTypeOptions.map((type) => (
|
|
<SelectItem key={type} value={type}>
|
|
{type}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-2 md:col-span-2">
|
|
<MediaFieldPicker
|
|
title="Abschnitt Bild"
|
|
value={section.media}
|
|
onChange={(media) =>
|
|
setSections((current) =>
|
|
current.map((item, currentIndex) =>
|
|
currentIndex === index
|
|
? {
|
|
...item,
|
|
media,
|
|
imagePath: media.url,
|
|
}
|
|
: item,
|
|
),
|
|
)
|
|
}
|
|
options={mediaOptions}
|
|
inputName={`section-media-${index}`}
|
|
fileFieldName={`section-image-upload-${index}`}
|
|
accept="image/*,.svg"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2 md:col-span-2">
|
|
<Label className="sr-only">Link URL</Label>
|
|
<div className="relative">
|
|
<Link2 className="pointer-events-none absolute left-3 top-1/2 z-10 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
<Input
|
|
value={section.linkUrl}
|
|
onChange={(event) =>
|
|
setSections((current) =>
|
|
current.map((item, currentIndex) =>
|
|
currentIndex === index ? { ...item, linkUrl: event.target.value } : item,
|
|
),
|
|
)
|
|
}
|
|
placeholder="Link URL"
|
|
className="pl-9"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{localeFieldConfig.map((locale) => (
|
|
<div key={`${locale.key}-title-${index}`} className="space-y-2">
|
|
<Label>{`Titel ${locale.label}`}</Label>
|
|
<Input
|
|
value={section[`title${locale.suffix}` as keyof SectionFormValue] as string}
|
|
onChange={(event) =>
|
|
setSections((current) =>
|
|
current.map((item, currentIndex) =>
|
|
currentIndex === index
|
|
? {
|
|
...item,
|
|
[`title${locale.suffix}`]: event.target.value,
|
|
}
|
|
: item,
|
|
),
|
|
)
|
|
}
|
|
/>
|
|
</div>
|
|
))}
|
|
|
|
{localeFieldConfig.map((locale) => (
|
|
<div key={`${locale.key}-body-${index}`} className="space-y-2 md:col-span-2">
|
|
<Label>{`Text ${locale.label}`}</Label>
|
|
<Textarea
|
|
rows={4}
|
|
value={section[`body${locale.suffix}` as keyof SectionFormValue] as string}
|
|
onChange={(event) =>
|
|
setSections((current) =>
|
|
current.map((item, currentIndex) =>
|
|
currentIndex === index
|
|
? {
|
|
...item,
|
|
[`body${locale.suffix}`]: event.target.value,
|
|
}
|
|
: item,
|
|
),
|
|
)
|
|
}
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</AppCard>
|
|
))}
|
|
</CardContent>
|
|
</AppCard>
|
|
|
|
<AppCard>
|
|
<CardHeader className="flex flex-row items-center justify-between">
|
|
<CardTitle>Dateien</CardTitle>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={() => setAssets((current) => [...current, createEmptyAsset(current.length)])}
|
|
>
|
|
<Plus className="h-4 w-4" />
|
|
Datei hinzufuegen
|
|
</Button>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{assets.length === 0 ? (
|
|
<p className="text-sm text-muted-foreground">Noch keine Dateien hinzugefuegt.</p>
|
|
) : null}
|
|
|
|
{assets.map((asset, index) => (
|
|
<AppCard key={asset.id ?? `${asset.kind}-${index}`} level={2}>
|
|
<CardContent className="space-y-4 p-4">
|
|
<div className="flex items-center justify-between gap-4">
|
|
<p className="text-sm font-medium text-foreground">{`Datei #${index + 1}`}</p>
|
|
<div className="flex gap-2">
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={() => setAssets((current) => moveArrayItem(current, index, index - 1))}
|
|
disabled={index === 0}
|
|
>
|
|
<ArrowUp className="h-4 w-4" />
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={() => setAssets((current) => moveArrayItem(current, index, index + 1))}
|
|
disabled={index === assets.length - 1}
|
|
>
|
|
<ArrowDown className="h-4 w-4" />
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
className="text-destructive"
|
|
onClick={() =>
|
|
setAssets((current) => current.filter((_, currentIndex) => currentIndex !== index))
|
|
}
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
Entfernen
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
<div className="space-y-2">
|
|
<Label>Typ</Label>
|
|
<Select
|
|
value={asset.kind}
|
|
onValueChange={(value) =>
|
|
setAssets((current) =>
|
|
current.map((item, currentIndex) =>
|
|
currentIndex === index
|
|
? {
|
|
...item,
|
|
kind: value as PortfolioAssetKind,
|
|
media: {
|
|
...item.media,
|
|
kind: value as PortfolioAssetKind,
|
|
},
|
|
}
|
|
: item,
|
|
),
|
|
)
|
|
}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Typ" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{assetKindOptions.map((kind) => (
|
|
<SelectItem key={kind} value={kind}>
|
|
{kind}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-2 md:col-span-2">
|
|
<MediaFieldPicker
|
|
title="Datei"
|
|
value={asset.media}
|
|
onChange={(media) =>
|
|
setAssets((current) =>
|
|
current.map((item, currentIndex) =>
|
|
currentIndex === index
|
|
? {
|
|
...item,
|
|
media,
|
|
filePath: media.url,
|
|
}
|
|
: item,
|
|
),
|
|
)
|
|
}
|
|
options={mediaOptions}
|
|
inputName={`asset-media-${index}`}
|
|
fileFieldName={asset.fileFieldName}
|
|
accept="image/*,.svg,.pdf"
|
|
/>
|
|
</div>
|
|
|
|
{localeFieldConfig.map((locale) => (
|
|
<div key={`${locale.key}-asset-${index}`} className="space-y-2">
|
|
<Label>{`Alt ${locale.label}`}</Label>
|
|
<Input
|
|
value={asset[`alt${locale.suffix}` as keyof AssetFormValue] as string}
|
|
onChange={(event) =>
|
|
setAssets((current) =>
|
|
current.map((item, currentIndex) =>
|
|
currentIndex === index
|
|
? {
|
|
...item,
|
|
[`alt${locale.suffix}`]: event.target.value,
|
|
}
|
|
: item,
|
|
),
|
|
)
|
|
}
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</AppCard>
|
|
))}
|
|
</CardContent>
|
|
</AppCard>
|
|
|
|
<div className="flex justify-end">
|
|
<Button type="submit">{submitLabel}</Button>
|
|
</div>
|
|
</form>
|
|
);
|
|
}
|