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:
@@ -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 1–3 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">
|
||||
|
||||
Reference in New Issue
Block a user