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,
|
BriefcaseBusiness,
|
||||||
CalendarDays,
|
CalendarDays,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
ChevronLeft,
|
|
||||||
ChevronRight,
|
|
||||||
CircleAlert,
|
CircleAlert,
|
||||||
CircleDashed,
|
|
||||||
Files,
|
Files,
|
||||||
FolderTree,
|
FolderTree,
|
||||||
Grid2x2,
|
Grid2x2,
|
||||||
@@ -43,7 +40,6 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { moveArrayItem } from "@/lib/array";
|
import { moveArrayItem } from "@/lib/array";
|
||||||
import { getAdminAppPath } from "@/lib/admin-routing";
|
import { getAdminAppPath } from "@/lib/admin-routing";
|
||||||
@@ -52,7 +48,6 @@ import {
|
|||||||
getPortfolioWizardProgress,
|
getPortfolioWizardProgress,
|
||||||
isPortfolioAssetReady,
|
isPortfolioAssetReady,
|
||||||
isPortfolioSectionReady,
|
isPortfolioSectionReady,
|
||||||
type PortfolioWizardStep,
|
|
||||||
} from "@/lib/portfolio-form-progress";
|
} from "@/lib/portfolio-form-progress";
|
||||||
import type { MediaOption } from "@/lib/media";
|
import type { MediaOption } from "@/lib/media";
|
||||||
import type { PortfolioCategoryView, PortfolioProjectView } from "@/lib/portfolio";
|
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 },
|
{ 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: {
|
function createMediaFieldState(params: {
|
||||||
kind: "IMAGE";
|
kind: "IMAGE";
|
||||||
assetId?: string | null;
|
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() {
|
function SubmitButton() {
|
||||||
const { pending } = useFormStatus();
|
const { pending } = useFormStatus();
|
||||||
|
|
||||||
@@ -508,27 +452,6 @@ export function PortfolioProjectForm({
|
|||||||
const initialProjectState = createInitialState(project, availableCategories);
|
const initialProjectState = createInitialState(project, availableCategories);
|
||||||
const initialSections = createInitialSections(project);
|
const initialSections = createInitialSections(project);
|
||||||
const initialAssets = createInitialAssets(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 [projectState, setProjectState] = useState<ProjectFormState>(initialProjectState);
|
||||||
const [coverMedia, setCoverMedia] = useState<MediaFieldState>(
|
const [coverMedia, setCoverMedia] = useState<MediaFieldState>(
|
||||||
createMediaFieldState({
|
createMediaFieldState({
|
||||||
@@ -540,9 +463,6 @@ export function PortfolioProjectForm({
|
|||||||
);
|
);
|
||||||
const [sections, setSections] = useState<SectionFormValue[]>(initialSections);
|
const [sections, setSections] = useState<SectionFormValue[]>(initialSections);
|
||||||
const [assets, setAssets] = useState<AssetFormValue[]>(initialAssets);
|
const [assets, setAssets] = useState<AssetFormValue[]>(initialAssets);
|
||||||
const [currentStep, setCurrentStep] = useState<PortfolioWizardStep>(
|
|
||||||
getFirstIncompleteWizardStep(initialProgress) ?? "basics",
|
|
||||||
);
|
|
||||||
const [showValidation, setShowValidation] = useState(false);
|
const [showValidation, setShowValidation] = useState(false);
|
||||||
const sectionsInputRef = useRef<HTMLInputElement | null>(null);
|
const sectionsInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
const assetsInputRef = useRef<HTMLInputElement | null>(null);
|
const assetsInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
@@ -571,7 +491,6 @@ export function PortfolioProjectForm({
|
|||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
const firstIncompleteStep = getFirstIncompleteWizardStep(progress);
|
const firstIncompleteStep = getFirstIncompleteWizardStep(progress);
|
||||||
const currentStepIndex = wizardSteps.findIndex((step) => step.key === currentStep);
|
|
||||||
const selectedCategory =
|
const selectedCategory =
|
||||||
availableCategories.find((category) => category.id === projectState.categoryId) ?? null;
|
availableCategories.find((category) => category.id === projectState.categoryId) ?? null;
|
||||||
const projectLabel =
|
const projectLabel =
|
||||||
@@ -627,7 +546,6 @@ export function PortfolioProjectForm({
|
|||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setShowValidation(true);
|
setShowValidation(true);
|
||||||
setCurrentStep(firstIncompleteStep);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<input type="hidden" name="id" value={project?.id ?? ""} />
|
<input type="hidden" name="id" value={project?.id ?? ""} />
|
||||||
@@ -657,11 +575,6 @@ export function PortfolioProjectForm({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
|
<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="Sections" value={String(sections.length)} icon={Files} />
|
||||||
<StatsCard title="Assets" value={String(assets.length)} icon={ImagePlus} />
|
<StatsCard title="Assets" value={String(assets.length)} icon={ImagePlus} />
|
||||||
<StatsCard
|
<StatsCard
|
||||||
@@ -677,9 +590,9 @@ export function PortfolioProjectForm({
|
|||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<CircleAlert className="mt-0.5 h-4 w-4 text-status-warning" />
|
<CircleAlert className="mt-0.5 h-4 w-4 text-status-warning" />
|
||||||
<div className="space-y-1">
|
<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">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -688,53 +601,8 @@ export function PortfolioProjectForm({
|
|||||||
</div>
|
</div>
|
||||||
</AppCard>
|
</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">
|
<div className="space-y-6">
|
||||||
<TabsContent value="basics" className="mt-0" forceMount>
|
<div>
|
||||||
<AppCard level={3} layer="single" padding="lg">
|
<AppCard level={3} layer="single" padding="lg">
|
||||||
<section className="space-y-6">
|
<section className="space-y-6">
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
@@ -894,9 +762,9 @@ export function PortfolioProjectForm({
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</AppCard>
|
</AppCard>
|
||||||
</TabsContent>
|
</div>
|
||||||
|
|
||||||
<TabsContent value="content" className="mt-0" forceMount>
|
<div>
|
||||||
<AppCard level={3} layer="single" padding="lg">
|
<AppCard level={3} layer="single" padding="lg">
|
||||||
<section className="space-y-6">
|
<section className="space-y-6">
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
@@ -945,9 +813,9 @@ export function PortfolioProjectForm({
|
|||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</AppCard>
|
</AppCard>
|
||||||
</TabsContent>
|
</div>
|
||||||
|
|
||||||
<TabsContent value="sections" className="mt-0" forceMount>
|
<div>
|
||||||
<AppCard level={3} layer="single" padding="lg">
|
<AppCard level={3} layer="single" padding="lg">
|
||||||
<section className="space-y-6">
|
<section className="space-y-6">
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
@@ -1135,9 +1003,9 @@ export function PortfolioProjectForm({
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</AppCard>
|
</AppCard>
|
||||||
</TabsContent>
|
</div>
|
||||||
|
|
||||||
<TabsContent value="assets" className="mt-0" forceMount>
|
<div>
|
||||||
<AppCard level={3} layer="single" padding="lg">
|
<AppCard level={3} layer="single" padding="lg">
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<div className="grid gap-4 lg:grid-cols-[minmax(0,1fr)_280px]">
|
<div className="grid gap-4 lg:grid-cols-[minmax(0,1fr)_280px]">
|
||||||
@@ -1310,38 +1178,11 @@ export function PortfolioProjectForm({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AppCard>
|
</AppCard>
|
||||||
</TabsContent>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
<AppCard level={3} layer="single" padding="lg">
|
<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-col gap-4 md:flex-row md:items-center md:justify-end">
|
||||||
<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-wrap items-center gap-3">
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
{firstIncompleteStep ? (
|
{firstIncompleteStep ? (
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
|
|||||||
Reference in New Issue
Block a user