diff --git a/components/admin/portfolio-project-form.tsx b/components/admin/portfolio-project-form.tsx index 4fcc06b..1e38d6c 100644 --- a/components/admin/portfolio-project-form.tsx +++ b/components/admin/portfolio-project-form.tsx @@ -3,10 +3,13 @@ import type { PortfolioProjectViewMode, PortfolioSectionType } from "@/lib/db/enums"; import { ArrowDown, + ArrowLeft, + ArrowRight, ArrowUp, BookOpenText, BriefcaseBusiness, CalendarDays, + Check, CircleAlert, FolderTree, Grid2x2, @@ -19,7 +22,7 @@ import { UserRound, } from "lucide-react"; import Link from "next/link"; -import { type ReactNode, useEffect, useRef, useState } from "react"; +import { Fragment, type ReactNode, useEffect, useRef, useState } from "react"; import { useFormStatus } from "react-dom"; import { MediaFieldPicker, type MediaFieldState } from "@/components/admin/media-field-picker"; @@ -50,6 +53,8 @@ import { getPortfolioWizardProgress, isPortfolioAssetReady, isPortfolioSectionReady, + type PortfolioWizardStep, + type PortfolioWizardStepState, } from "@/lib/portfolio-form-progress"; import type { MediaOption } from "@/lib/media"; import type { PortfolioCategoryView, PortfolioProjectView } from "@/lib/portfolio"; @@ -145,6 +150,65 @@ const viewModeOptions: Array<{ { value: "CASE_STUDY", label: "Case Study", description: "Structured challenge/solution/outcome view. Sections 1–3 become the lead panels.", icon: BriefcaseBusiness }, ]; +const WIZARD_STEPS: Array<{ key: PortfolioWizardStep; label: string; description: string }> = [ + { key: "basics", label: "Grundlagen", description: "Kategorie & Metadaten" }, + { key: "content", label: "Inhalte", description: "Lokalisierte Texte" }, + { key: "sections", label: "Abschnitte", description: "Projektgeschichte" }, + { key: "assets", label: "Medien", description: "Cover & Galerie" }, +]; + +function WizardStepper({ + activeKey, + progress, + onSelect, +}: { + activeKey: PortfolioWizardStep; + progress: PortfolioWizardStepState[]; + onSelect: (key: PortfolioWizardStep) => void; +}) { + return ( +
+ {WIZARD_STEPS.map((step, index) => { + const complete = progress.find((item) => item.key === step.key)?.complete ?? false; + const active = step.key === activeKey; + + return ( + + + {index < WIZARD_STEPS.length - 1 ? ( +
+ ) : null} + + ); + })} +
+ ); +} + function createMediaFieldState(params: { kind: "IMAGE"; assetId?: string | null; @@ -499,6 +563,7 @@ export function PortfolioProjectForm({ const [sections, setSections] = useState(initialSections); const [assets, setAssets] = useState(initialAssets); const [showValidation, setShowValidation] = useState(false); + const [activeStep, setActiveStep] = useState("basics"); const sectionsInputRef = useRef(null); const assetsInputRef = useRef(null); const intentRef = useRef(null); @@ -532,6 +597,12 @@ export function PortfolioProjectForm({ })), }); const firstIncompleteStep = getFirstIncompleteWizardStep(progress); + const activeStepIndex = WIZARD_STEPS.findIndex((step) => step.key === activeStep); + const isLastStep = activeStepIndex === WIZARD_STEPS.length - 1; + const goToStep = (index: number) => { + const target = WIZARD_STEPS[Math.min(Math.max(index, 0), WIZARD_STEPS.length - 1)]; + setActiveStep(target.key); + }; useEffect(() => { sectionsInputRef.current?.dispatchEvent(new Event("input", { bubbles: true })); @@ -594,7 +665,10 @@ export function PortfolioProjectForm({ -
+ + +
+
+
+
+
+
)}
+
+
+
-
+
{showValidation && firstIncompleteStep ? (
@@ -1171,19 +1252,27 @@ export function PortfolioProjectForm({
) : null} -
-
- {firstIncompleteStep ? ( -

- Fill Basics and Localized Content to publish — or save as a draft anytime. -

- ) : ( -

- Ready to save. Sections and assets are optional. -

- )} +
+ + +
setIntent("draft")} /> - setIntent("save")} /> + {isLastStep ? ( + setIntent("save")} /> + ) : ( + + )}