@@ -8,15 +8,14 @@ import {
|
||||
Hash,
|
||||
Layers3,
|
||||
Pencil,
|
||||
Sparkles,
|
||||
ShieldCheck,
|
||||
ShieldOff,
|
||||
Text,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
|
||||
import type { deleteCategoryAction, upsertCategoryAction } from "@/app/_admin/portfolio/actions";
|
||||
import { StatsCard } from "@/components/dashboard/stats-card";
|
||||
import { AppCard } from "@/components/ui/app-card";
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CardContent } from "@/components/ui/card";
|
||||
@@ -57,6 +56,10 @@ const copy = {
|
||||
empty: "No categories yet.",
|
||||
deleteBlocked: "Delete becomes available only when no projects are assigned.",
|
||||
editCategory: "Edit Category",
|
||||
visibilityActive: "Visible on public portfolio filters.",
|
||||
visibilityInactive: "Hidden from public portfolio filters.",
|
||||
statusSection: "Visibility & Status",
|
||||
statusHint: "Control whether the category can be assigned and shown publicly.",
|
||||
};
|
||||
|
||||
type CategoryAction = typeof upsertCategoryAction;
|
||||
@@ -145,6 +148,56 @@ function CategoryLocaleFields({
|
||||
);
|
||||
}
|
||||
|
||||
function CategoryStatusFields({
|
||||
formId,
|
||||
values,
|
||||
}: {
|
||||
formId: string;
|
||||
values?: CategoryFormValues;
|
||||
}) {
|
||||
const isActive = values?.isActive ?? true;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-semibold text-foreground">{copy.statusSection}</p>
|
||||
<p className="text-sm text-muted-foreground">{copy.statusHint}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-[minmax(0,1fr)_220px]">
|
||||
<AppCard level={2} padding="sm" className="rounded-nested">
|
||||
<div className="flex items-start gap-3">
|
||||
{isActive ? (
|
||||
<ShieldCheck className="mt-0.5 h-4 w-4 text-status-success" />
|
||||
) : (
|
||||
<ShieldOff className="mt-0.5 h-4 w-4 text-status-warning" />
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<p className="text-sm font-medium text-foreground">{copy.active}</p>
|
||||
<Badge variant={isActive ? "success" : "warning"}>
|
||||
{isActive ? "Public" : "Hidden"}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{isActive ? copy.visibilityActive : copy.visibilityInactive}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</AppCard>
|
||||
|
||||
<label
|
||||
htmlFor={`${formId}-isActive`}
|
||||
className="flex items-center gap-3 rounded-nested border border-input bg-card px-4 py-3 text-sm"
|
||||
>
|
||||
<Checkbox id={`${formId}-isActive`} name="isActive" defaultChecked={isActive} />
|
||||
{copy.active}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CategoryForm({
|
||||
formId,
|
||||
action,
|
||||
@@ -200,17 +253,16 @@ function CategoryForm({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label className="flex items-center gap-3 rounded-nested border border-input bg-card px-4 py-3 text-sm">
|
||||
<Checkbox name="isActive" defaultChecked={values?.isActive ?? true} />
|
||||
{copy.active}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<CategoryLocaleFields idPrefix={formId} values={values} />
|
||||
|
||||
<Separator />
|
||||
|
||||
<CategoryStatusFields formId={formId} values={values} />
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -232,7 +284,7 @@ function EditCategoryDialog({
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-h-[90vh] overflow-y-auto">
|
||||
<DialogContent className="max-h-[90vh] max-w-5xl overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{copy.editCategory}</DialogTitle>
|
||||
<DialogDescription>{copy.editDescription}</DialogDescription>
|
||||
@@ -247,7 +299,7 @@ function EditCategoryDialog({
|
||||
<p className="text-xs uppercase tracking-[0.18em] text-muted-foreground">Status</p>
|
||||
<div className="mt-2">
|
||||
<Badge variant={category.isActive ? "success" : "warning"}>
|
||||
{category.isActive ? "Active" : "Inactive"}
|
||||
{category.isActive ? "Public" : "Hidden"}
|
||||
</Badge>
|
||||
</div>
|
||||
</AppCard>
|
||||
@@ -276,7 +328,9 @@ function EditCategoryDialog({
|
||||
|
||||
<DialogFooter className="items-center justify-between sm:flex-row">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{category.projectCount > 0 ? copy.deleteBlocked : "Category can be deleted."}
|
||||
{category.projectCount > 0
|
||||
? `Delete blocked. ${copy.deleteBlocked}`
|
||||
: "Category can be deleted."}
|
||||
</p>
|
||||
<div className="flex w-full flex-col-reverse gap-2 sm:w-auto sm:flex-row">
|
||||
<form action={removeCategoryAction}>
|
||||
@@ -316,13 +370,24 @@ export function PortfolioCategoriesManager({
|
||||
}, [searchParams]);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<AppCard level={3}>
|
||||
<CardContent className="flex flex-col gap-6 p-6 lg:flex-row lg:items-end lg:justify-between lg:p-8">
|
||||
<div className="grid gap-4 sm:grid-cols-3">
|
||||
<StatsCard title="Total" value={String(categories.length)} />
|
||||
<StatsCard title="Active" value={String(activeCount)} />
|
||||
<StatsCard title="Assigned" value={String(assignedProjects)} />
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-2 text-sm text-muted-foreground">
|
||||
<span>
|
||||
Total:
|
||||
{" "}
|
||||
<span className="font-semibold text-foreground">{categories.length}</span>
|
||||
</span>
|
||||
<span>
|
||||
Active:
|
||||
{" "}
|
||||
<span className="font-semibold text-foreground">{activeCount}</span>
|
||||
</span>
|
||||
<span>
|
||||
Assigned:
|
||||
{" "}
|
||||
<span className="font-semibold text-foreground">{assignedProjects}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
|
||||
@@ -332,29 +397,28 @@ export function PortfolioCategoriesManager({
|
||||
{copy.addCategory}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-[90vh] overflow-y-auto">
|
||||
<DialogContent className="max-h-[90vh] max-w-5xl overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{copy.addCategory}</DialogTitle>
|
||||
<DialogDescription>{copy.modalDescription}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<AppCard level={2} padding="sm" className="rounded-nested">
|
||||
<div className="grid gap-3 sm:grid-cols-3">
|
||||
<AppCard level={2} padding="sm" className="rounded-nested">
|
||||
<Sparkles className="h-4 w-4 text-brand-primary" />
|
||||
<p className="mt-3 text-sm font-medium text-foreground">Start with basics</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">Slug and sort order first.</p>
|
||||
</AppCard>
|
||||
<AppCard level={2} padding="sm" className="rounded-nested">
|
||||
<Layers3 className="h-4 w-4 text-brand-primary" />
|
||||
<p className="mt-3 text-sm font-medium text-foreground">Fill all locales</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">Keep names and descriptions complete.</p>
|
||||
</AppCard>
|
||||
<AppCard level={2} padding="sm" className="rounded-nested">
|
||||
<Pencil className="h-4 w-4 text-brand-primary" />
|
||||
<p className="mt-3 text-sm font-medium text-foreground">Publish when ready</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">Categories stay manageable from day one.</p>
|
||||
</AppCard>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium text-foreground">1. Basics</p>
|
||||
<p className="text-sm text-muted-foreground">Slug and sort order first.</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium text-foreground">2. Localization</p>
|
||||
<p className="text-sm text-muted-foreground">Names and descriptions in all locales.</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium text-foreground">3. Visibility</p>
|
||||
<p className="text-sm text-muted-foreground">Decide if the category is public or hidden.</p>
|
||||
</div>
|
||||
</div>
|
||||
</AppCard>
|
||||
|
||||
<CategoryForm
|
||||
formId="portfolio-category-create-form"
|
||||
@@ -368,8 +432,7 @@ export function PortfolioCategoriesManager({
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</CardContent>
|
||||
</AppCard>
|
||||
</div>
|
||||
|
||||
<AppCard level={3}>
|
||||
<CardContent className="space-y-4 p-6">
|
||||
@@ -381,21 +444,25 @@ export function PortfolioCategoriesManager({
|
||||
{categories.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">{copy.empty}</p>
|
||||
) : (
|
||||
<Accordion type="single" collapsible className="space-y-3">
|
||||
<div className="grid gap-3">
|
||||
{categories.map((category) => (
|
||||
<AccordionItem key={category.id} value={category.id}>
|
||||
<AccordionTrigger className="bg-surface-2 hover:no-underline">
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-3 text-left lg:flex-row lg:items-center lg:justify-between">
|
||||
<div className="space-y-1">
|
||||
<AppCard
|
||||
key={category.id}
|
||||
level={2}
|
||||
padding="sm"
|
||||
className="rounded-nested"
|
||||
>
|
||||
<div className="flex min-w-0 flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div className="min-w-0 space-y-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-base font-semibold text-foreground">
|
||||
<span className="truncate text-base font-semibold text-foreground">
|
||||
{category.name.de || category.name.en || category.name.ar}
|
||||
</span>
|
||||
<Badge variant={category.isActive ? "success" : "warning"}>
|
||||
{category.isActive ? "Active" : "Inactive"}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 text-xs text-muted-foreground">
|
||||
<div className="flex flex-wrap gap-x-3 gap-y-1 text-xs text-muted-foreground">
|
||||
<span>{category.slug}</span>
|
||||
<span>{copy.projects}: {category.projectCount}</span>
|
||||
<span>{copy.sortOrder}: {category.sortOrder}</span>
|
||||
@@ -405,30 +472,12 @@ export function PortfolioCategoriesManager({
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
setEditingCategoryId(category.id);
|
||||
}}
|
||||
onClick={() => setEditingCategoryId(category.id)}
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="space-y-4">
|
||||
<div className="grid gap-4 xl:grid-cols-3">
|
||||
{locales.map((locale) => (
|
||||
<AppCard key={`${category.id}-${locale.key}`} level={2} padding="sm" className="space-y-3 rounded-nested">
|
||||
<p className="text-sm font-medium text-foreground">{locale.label}</p>
|
||||
<p className="text-sm font-semibold text-foreground">
|
||||
{category.name[locale.key.toLowerCase() as "ar" | "en" | "de"]}
|
||||
</p>
|
||||
<p className="text-sm leading-6 text-muted-foreground">
|
||||
{category.description[locale.key.toLowerCase() as "ar" | "en" | "de"]}
|
||||
</p>
|
||||
</AppCard>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<EditCategoryDialog
|
||||
category={category}
|
||||
@@ -437,10 +486,9 @@ export function PortfolioCategoriesManager({
|
||||
saveCategoryAction={saveCategoryAction}
|
||||
removeCategoryAction={removeCategoryAction}
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</AppCard>
|
||||
))}
|
||||
</Accordion>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</AppCard>
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { getAdminAppPath } from "@/lib/admin-routing";
|
||||
|
||||
const copy = {
|
||||
editProject: "Bearbeiten",
|
||||
@@ -40,7 +41,7 @@ export function PortfolioProjectActions({ projectId }: { projectId: string }) {
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href={`/portfolio/projects/${projectId}`}>
|
||||
<Link href={getAdminAppPath(`/portfolio/projects/${projectId}`)}>
|
||||
<FolderKanban className="mr-2 h-4 w-4" />
|
||||
{copy.editProject}
|
||||
</Link>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+18
-4
@@ -22,16 +22,30 @@ function useTabsContext() {
|
||||
}
|
||||
|
||||
type TabsProps = {
|
||||
defaultValue: string;
|
||||
defaultValue?: string;
|
||||
value?: string;
|
||||
onValueChange?: (value: string) => void;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function Tabs({ defaultValue, children, className }: TabsProps) {
|
||||
const [value, setValue] = React.useState(defaultValue);
|
||||
function Tabs({ defaultValue, value, onValueChange, children, className }: TabsProps) {
|
||||
const [internalValue, setInternalValue] = React.useState(defaultValue ?? "");
|
||||
const activeValue = value ?? internalValue;
|
||||
|
||||
const setValue = React.useCallback(
|
||||
(nextValue: string) => {
|
||||
if (value === undefined) {
|
||||
setInternalValue(nextValue);
|
||||
}
|
||||
|
||||
onValueChange?.(nextValue);
|
||||
},
|
||||
[onValueChange, value],
|
||||
);
|
||||
|
||||
return (
|
||||
<TabsContext.Provider value={{ value, setValue }}>
|
||||
<TabsContext.Provider value={{ value: activeValue, setValue }}>
|
||||
<div className={cn("w-full", className)}>{children}</div>
|
||||
</TabsContext.Provider>
|
||||
);
|
||||
|
||||
@@ -138,7 +138,7 @@ export function getAdminNavigation(
|
||||
active: portfolioChild === "new-project",
|
||||
},
|
||||
{
|
||||
label: "Add Category",
|
||||
label: "Categories",
|
||||
href: getAdminAppPath("/portfolio/categories"),
|
||||
icon: Tags,
|
||||
active: portfolioChild === "categories",
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import type { PortfolioProjectViewMode, PortfolioSectionType } from "@prisma/client";
|
||||
|
||||
export type PortfolioWizardStep = "basics" | "content" | "sections" | "assets";
|
||||
|
||||
export type PortfolioWizardStepState = {
|
||||
key: PortfolioWizardStep;
|
||||
complete: boolean;
|
||||
summary: string;
|
||||
};
|
||||
|
||||
type ProjectBasicsProgressInput = {
|
||||
categoryId: string;
|
||||
slug: string;
|
||||
clientName: string;
|
||||
projectYear: string;
|
||||
sortOrder: string;
|
||||
viewMode: PortfolioProjectViewMode;
|
||||
};
|
||||
|
||||
type ProjectContentProgressInput = {
|
||||
titleAr: string;
|
||||
titleEn: string;
|
||||
titleDe: string;
|
||||
serviceLabelAr: string;
|
||||
serviceLabelEn: string;
|
||||
serviceLabelDe: string;
|
||||
summaryAr: string;
|
||||
summaryEn: string;
|
||||
summaryDe: string;
|
||||
};
|
||||
|
||||
type ProjectSectionProgressInput = {
|
||||
type: PortfolioSectionType;
|
||||
titleAr: string;
|
||||
titleEn: string;
|
||||
titleDe: string;
|
||||
bodyAr: string;
|
||||
bodyEn: string;
|
||||
bodyDe: string;
|
||||
linkUrl: string;
|
||||
mediaAssetId: string;
|
||||
};
|
||||
|
||||
type ProjectAssetProgressInput = {
|
||||
mediaAssetId: string;
|
||||
altAr: string;
|
||||
altEn: string;
|
||||
altDe: string;
|
||||
};
|
||||
|
||||
type PortfolioWizardProgressInput = {
|
||||
basics: ProjectBasicsProgressInput;
|
||||
content: ProjectContentProgressInput;
|
||||
sections: ProjectSectionProgressInput[];
|
||||
assets: ProjectAssetProgressInput[];
|
||||
};
|
||||
|
||||
function hasText(value: string) {
|
||||
return value.trim().length > 0;
|
||||
}
|
||||
|
||||
function isValidSlug(value: string) {
|
||||
return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value.trim());
|
||||
}
|
||||
|
||||
function isValidYear(value: string) {
|
||||
const year = Number(value);
|
||||
|
||||
return Number.isInteger(year) && year >= 2000 && year <= 2100;
|
||||
}
|
||||
|
||||
function isValidSortOrder(value: string) {
|
||||
const sortOrder = Number(value);
|
||||
|
||||
return Number.isInteger(sortOrder) && sortOrder >= 0 && sortOrder <= 9999;
|
||||
}
|
||||
|
||||
export function isPortfolioSectionReady(section: ProjectSectionProgressInput) {
|
||||
const hasTitles = hasText(section.titleAr) && hasText(section.titleEn) && hasText(section.titleDe);
|
||||
|
||||
if (!hasTitles) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (section.type === "GALLERY") {
|
||||
return hasText(section.mediaAssetId);
|
||||
}
|
||||
|
||||
if (section.type === "LINK") {
|
||||
return hasText(section.linkUrl);
|
||||
}
|
||||
|
||||
return hasText(section.bodyAr) && hasText(section.bodyEn) && hasText(section.bodyDe);
|
||||
}
|
||||
|
||||
export function isPortfolioAssetReady(asset: ProjectAssetProgressInput) {
|
||||
return (
|
||||
hasText(asset.mediaAssetId) &&
|
||||
hasText(asset.altAr) &&
|
||||
hasText(asset.altEn) &&
|
||||
hasText(asset.altDe)
|
||||
);
|
||||
}
|
||||
|
||||
export function getPortfolioWizardProgress(
|
||||
input: PortfolioWizardProgressInput,
|
||||
): PortfolioWizardStepState[] {
|
||||
const completedSections = input.sections.filter(isPortfolioSectionReady).length;
|
||||
const completedAssets = input.assets.filter(isPortfolioAssetReady).length;
|
||||
|
||||
return [
|
||||
{
|
||||
key: "basics",
|
||||
complete:
|
||||
hasText(input.basics.categoryId) &&
|
||||
isValidSlug(input.basics.slug) &&
|
||||
hasText(input.basics.clientName) &&
|
||||
isValidYear(input.basics.projectYear) &&
|
||||
isValidSortOrder(input.basics.sortOrder) &&
|
||||
Boolean(input.basics.viewMode),
|
||||
summary: "Category, slug, client, year, and order are set.",
|
||||
},
|
||||
{
|
||||
key: "content",
|
||||
complete:
|
||||
hasText(input.content.titleAr) &&
|
||||
hasText(input.content.titleEn) &&
|
||||
hasText(input.content.titleDe) &&
|
||||
hasText(input.content.serviceLabelAr) &&
|
||||
hasText(input.content.serviceLabelEn) &&
|
||||
hasText(input.content.serviceLabelDe) &&
|
||||
hasText(input.content.summaryAr) &&
|
||||
hasText(input.content.summaryEn) &&
|
||||
hasText(input.content.summaryDe),
|
||||
summary: "Title, service label, and summary exist in all locales.",
|
||||
},
|
||||
{
|
||||
key: "sections",
|
||||
complete: input.sections.length > 0 && completedSections === input.sections.length,
|
||||
summary: `${completedSections}/${input.sections.length} sections ready.`,
|
||||
},
|
||||
{
|
||||
key: "assets",
|
||||
complete: input.assets.length > 0 && completedAssets === input.assets.length,
|
||||
summary: `${completedAssets}/${input.assets.length} assets ready.`,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function getFirstIncompleteWizardStep(
|
||||
steps: PortfolioWizardStepState[],
|
||||
): PortfolioWizardStep | null {
|
||||
return steps.find((step) => !step.complete)?.key ?? null;
|
||||
}
|
||||
@@ -1,5 +1,11 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
getFirstIncompleteWizardStep,
|
||||
getPortfolioWizardProgress,
|
||||
isPortfolioAssetReady,
|
||||
isPortfolioSectionReady,
|
||||
} from "../lib/portfolio-form-progress";
|
||||
import {
|
||||
assetInputSchema,
|
||||
categoryInputSchema,
|
||||
@@ -203,4 +209,87 @@ describe("portfolio validation", () => {
|
||||
}),
|
||||
).toThrow(/link/i);
|
||||
});
|
||||
|
||||
it("calculates wizard progress and points to the first incomplete step", () => {
|
||||
const progress = getPortfolioWizardProgress({
|
||||
basics: {
|
||||
categoryId: "cat_1",
|
||||
slug: "case-study-entry",
|
||||
clientName: "Client",
|
||||
projectYear: "2025",
|
||||
sortOrder: "1",
|
||||
viewMode: "GRID",
|
||||
},
|
||||
content: {
|
||||
titleAr: "عنوان",
|
||||
titleEn: "Title",
|
||||
titleDe: "Titel",
|
||||
serviceLabelAr: "خدمة",
|
||||
serviceLabelEn: "Service",
|
||||
serviceLabelDe: "Service",
|
||||
summaryAr: "",
|
||||
summaryEn: "",
|
||||
summaryDe: "",
|
||||
},
|
||||
sections: [
|
||||
{
|
||||
type: "RICH_TEXT",
|
||||
titleAr: "العنوان",
|
||||
titleEn: "Title",
|
||||
titleDe: "Titel",
|
||||
bodyAr: "النص",
|
||||
bodyEn: "Body",
|
||||
bodyDe: "Text",
|
||||
linkUrl: "",
|
||||
mediaAssetId: "",
|
||||
},
|
||||
],
|
||||
assets: [
|
||||
{
|
||||
mediaAssetId: "asset_1",
|
||||
altAr: "بديل",
|
||||
altEn: "Alt",
|
||||
altDe: "Alt",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(progress.find((step) => step.key === "basics")?.complete).toBe(true);
|
||||
expect(progress.find((step) => step.key === "content")?.complete).toBe(false);
|
||||
expect(getFirstIncompleteWizardStep(progress)).toBe("content");
|
||||
});
|
||||
|
||||
it("marks sections and assets ready only when required fields are present", () => {
|
||||
expect(
|
||||
isPortfolioSectionReady({
|
||||
type: "LINK",
|
||||
titleAr: "رابط",
|
||||
titleEn: "Link",
|
||||
titleDe: "Link",
|
||||
bodyAr: "",
|
||||
bodyEn: "",
|
||||
bodyDe: "",
|
||||
linkUrl: "https://example.com",
|
||||
mediaAssetId: "",
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
isPortfolioAssetReady({
|
||||
mediaAssetId: "asset_1",
|
||||
altAr: "بديل",
|
||||
altEn: "Alt",
|
||||
altDe: "Alt",
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
isPortfolioAssetReady({
|
||||
mediaAssetId: "",
|
||||
altAr: "بديل",
|
||||
altEn: "Alt",
|
||||
altDe: "Alt",
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user