STYLED - Convert the add-project form from a 4-tab wizard to one page

- Remove the tab navigation, per-step sidebar, and Previous/Next buttons;
  render Basics, Localized Content, Sections and Assets stacked on a single
  scrollable page
- Drop the now-unused wizard step machinery (currentStep state, wizardSteps,
  StepStateBadge, step stat card) and related imports
- Keep the same fields, validation, and save gating (Basics + Localized
  Content required; sections/assets optional)
This commit is contained in:
moh
2026-09-20 16:39:59 +02:00
parent 22cd3f8645
commit 3c2421fb8b
+10 -169
View File
@@ -8,10 +8,7 @@ import {
BriefcaseBusiness,
CalendarDays,
CheckCircle2,
ChevronLeft,
ChevronRight,
CircleAlert,
CircleDashed,
Files,
FolderTree,
Grid2x2,
@@ -43,7 +40,6 @@ import {
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 { getAdminAppPath } from "@/lib/admin-routing";
@@ -52,7 +48,6 @@ import {
getPortfolioWizardProgress,
isPortfolioAssetReady,
isPortfolioSectionReady,
type PortfolioWizardStep,
} from "@/lib/portfolio-form-progress";
import type { MediaOption } from "@/lib/media";
import type { PortfolioCategoryView, PortfolioProjectView } from "@/lib/portfolio";
@@ -148,33 +143,6 @@ const viewModeOptions: Array<{
{ value: "CASE_STUDY", label: "Case Study", description: "Structured challenge/solution/outcome view. Sections 13 become the lead panels.", icon: BriefcaseBusiness },
];
const wizardSteps: Array<{
key: PortfolioWizardStep;
label: string;
description: string;
}> = [
{
key: "basics",
label: "Basics",
description: "Category, slug, client, year, and status.",
},
{
key: "content",
label: "Localized Content",
description: "Title, service label, and summary in all locales.",
},
{
key: "sections",
label: "Sections (optional)",
description: "Optional — build the project story with ordered content blocks.",
},
{
key: "assets",
label: "Assets & Media (optional)",
description: "Optional — cover and gallery assets from the media library.",
},
];
function createMediaFieldState(params: {
kind: "IMAGE";
assetId?: string | null;
@@ -379,30 +347,6 @@ function SectionHeader({
);
}
function StepStateBadge({
complete,
active,
showValidation,
}: {
complete: boolean;
active: boolean;
showValidation: boolean;
}) {
if (complete) {
return <Badge variant="success">Complete</Badge>;
}
if (showValidation) {
return <Badge variant="warning">Needs Attention</Badge>;
}
if (active) {
return <Badge variant="outline">Current</Badge>;
}
return <Badge variant="outline">Incomplete</Badge>;
}
function SubmitButton() {
const { pending } = useFormStatus();
@@ -508,27 +452,6 @@ export function PortfolioProjectForm({
const initialProjectState = createInitialState(project, availableCategories);
const initialSections = createInitialSections(project);
const initialAssets = createInitialAssets(project);
const initialProgress = getPortfolioWizardProgress({
basics: initialProjectState,
content: initialProjectState,
sections: initialSections.map((section) => ({
type: section.type,
titleAr: section.titleAr,
titleEn: section.titleEn,
titleDe: section.titleDe,
bodyAr: section.bodyAr,
bodyEn: section.bodyEn,
bodyDe: section.bodyDe,
linkUrl: section.linkUrl,
mediaAssetId: section.media.assetId,
})),
assets: initialAssets.map((asset) => ({
mediaAssetId: asset.media.assetId,
altAr: asset.altAr,
altEn: asset.altEn,
altDe: asset.altDe,
})),
});
const [projectState, setProjectState] = useState<ProjectFormState>(initialProjectState);
const [coverMedia, setCoverMedia] = useState<MediaFieldState>(
createMediaFieldState({
@@ -540,9 +463,6 @@ export function PortfolioProjectForm({
);
const [sections, setSections] = useState<SectionFormValue[]>(initialSections);
const [assets, setAssets] = useState<AssetFormValue[]>(initialAssets);
const [currentStep, setCurrentStep] = useState<PortfolioWizardStep>(
getFirstIncompleteWizardStep(initialProgress) ?? "basics",
);
const [showValidation, setShowValidation] = useState(false);
const sectionsInputRef = useRef<HTMLInputElement | null>(null);
const assetsInputRef = useRef<HTMLInputElement | null>(null);
@@ -571,7 +491,6 @@ export function PortfolioProjectForm({
})),
});
const firstIncompleteStep = getFirstIncompleteWizardStep(progress);
const currentStepIndex = wizardSteps.findIndex((step) => step.key === currentStep);
const selectedCategory =
availableCategories.find((category) => category.id === projectState.categoryId) ?? null;
const projectLabel =
@@ -627,7 +546,6 @@ export function PortfolioProjectForm({
event.preventDefault();
setShowValidation(true);
setCurrentStep(firstIncompleteStep);
}}
>
<input type="hidden" name="id" value={project?.id ?? ""} />
@@ -657,11 +575,6 @@ export function PortfolioProjectForm({
</div>
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
<StatsCard
title="Step"
value={wizardSteps[currentStepIndex]?.label ?? "Basics"}
icon={Layers3}
/>
<StatsCard title="Sections" value={String(sections.length)} icon={Files} />
<StatsCard title="Assets" value={String(assets.length)} icon={ImagePlus} />
<StatsCard
@@ -677,9 +590,9 @@ export function PortfolioProjectForm({
<div className="flex items-start gap-3">
<CircleAlert className="mt-0.5 h-4 w-4 text-status-warning" />
<div className="space-y-1">
<p className="text-sm font-medium text-foreground">Complete the highlighted step before saving.</p>
<p className="text-sm font-medium text-foreground">Some required fields are still missing.</p>
<p className="text-sm text-muted-foreground">
The wizard moved to the first incomplete step so the missing fields are easier to find.
Fill the Basics and Localized Content fields below, then save.
</p>
</div>
</div>
@@ -688,53 +601,8 @@ export function PortfolioProjectForm({
</div>
</AppCard>
<Tabs value={currentStep} onValueChange={(value) => setCurrentStep(value as PortfolioWizardStep)}>
<div className="grid gap-6 xl:grid-cols-[300px_minmax(0,1fr)] xl:items-start">
<AppCard level={2} layer="single" padding="sm" className="xl:sticky xl:top-4">
<TabsList className="grid h-auto w-full gap-3 rounded-none bg-transparent p-0">
{wizardSteps.map((step, index) => {
const stepProgress = progress.find((entry) => entry.key === step.key);
const isActive = currentStep === step.key;
return (
<TabsTrigger
key={step.key}
value={step.key}
className={cn(
"w-full rounded-nested border border-border/80 bg-background p-0 text-left hover:bg-accent/30",
isActive && "border-input bg-accent/20 text-foreground shadow-xs",
)}
>
<div className="flex w-full flex-col gap-3 p-4">
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-3">
<span className="inline-flex h-8 w-8 items-center justify-center rounded-full border border-border/80 bg-surface-2 text-sm font-semibold text-foreground">
{index + 1}
</span>
<div>
<p className="text-sm font-semibold text-foreground">{step.label}</p>
</div>
</div>
{stepProgress?.complete ? (
<CheckCircle2 className="h-4 w-4 text-status-success" />
) : (
<CircleDashed className="h-4 w-4 text-muted-foreground" />
)}
</div>
<StepStateBadge
complete={Boolean(stepProgress?.complete)}
active={isActive}
showValidation={showValidation && !stepProgress?.complete}
/>
</div>
</TabsTrigger>
);
})}
</TabsList>
</AppCard>
<div className="space-y-6">
<TabsContent value="basics" className="mt-0" forceMount>
<div>
<AppCard level={3} layer="single" padding="lg">
<section className="space-y-6">
<SectionHeader
@@ -894,9 +762,9 @@ export function PortfolioProjectForm({
</div>
</section>
</AppCard>
</TabsContent>
</div>
<TabsContent value="content" className="mt-0" forceMount>
<div>
<AppCard level={3} layer="single" padding="lg">
<section className="space-y-6">
<SectionHeader
@@ -945,9 +813,9 @@ export function PortfolioProjectForm({
/>
</section>
</AppCard>
</TabsContent>
</div>
<TabsContent value="sections" className="mt-0" forceMount>
<div>
<AppCard level={3} layer="single" padding="lg">
<section className="space-y-6">
<SectionHeader
@@ -1135,9 +1003,9 @@ export function PortfolioProjectForm({
</div>
</section>
</AppCard>
</TabsContent>
</div>
<TabsContent value="assets" className="mt-0" forceMount>
<div>
<AppCard level={3} layer="single" padding="lg">
<div className="space-y-8">
<div className="grid gap-4 lg:grid-cols-[minmax(0,1fr)_280px]">
@@ -1310,38 +1178,11 @@ export function PortfolioProjectForm({
</div>
</div>
</AppCard>
</TabsContent>
</div>
</div>
</Tabs>
<AppCard level={3} layer="single" padding="lg">
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div className="flex flex-wrap gap-2">
<Button
type="button"
variant="outline"
onClick={() => setCurrentStep(wizardSteps[Math.max(currentStepIndex - 1, 0)]!.key)}
disabled={currentStepIndex === 0}
>
<ChevronLeft className="h-4 w-4" />
Previous
</Button>
<Button
type="button"
variant="outline"
onClick={() =>
setCurrentStep(
wizardSteps[Math.min(currentStepIndex + 1, wizardSteps.length - 1)]!.key,
)
}
disabled={currentStepIndex === wizardSteps.length - 1}
>
Next
<ChevronRight className="h-4 w-4" />
</Button>
</div>
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-end">
<div className="flex flex-wrap items-center gap-3">
{firstIncompleteStep ? (
<p className="text-sm text-muted-foreground">