"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; 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 ( ); } function LocaleBlock({ title, renderField, }: { title: string; renderField: (locale: (typeof localeFieldConfig)[number]) => React.ReactNode; }) { return (

{title}

{localeFieldConfig.map((locale) => (

{locale.label}

{renderField(locale)}
))}
); } export function PortfolioProjectForm({ action, categories, mediaOptions, project, formId, redirectPath, }: PortfolioProjectFormProps) { const [activePanel, setActivePanel] = useState("basic"); const [projectState, setProjectState] = useState( createInitialProjectState(project, categories), ); const [coverMedia, setCoverMedia] = useState( createMediaFieldState({ kind: "IMAGE", assetId: project?.coverMediaAssetId, url: project?.coverImagePath, label: project?.title.de ?? project?.title.en ?? project?.title.ar ?? "", }), ); const [sections, setSections] = useState( 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( 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(null); const assetsInputRef = useRef(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 = (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 (
setActivePanel("basic")} /> setActivePanel("localized")} /> setActivePanel("sections")} /> setActivePanel("assets")} />
Basic Info
setProjectField("slug", event.target.value)} className="pl-9" required />
setProjectField("clientName", event.target.value)} className="pl-9" required />
setProjectField("projectYear", event.target.value)} className="pl-9" required />
setProjectField("previewUrl", event.target.value)} className="pl-9" />
setProjectField("sortOrder", event.target.value)} className="pl-9" required />
Localized Content ( setProjectField(`title${locale.suffix}`, event.target.value)} /> )} /> ( setProjectField(`serviceLabel${locale.suffix}`, event.target.value)} /> )} /> (