ADDED - Save an unfinished project as a draft
- Add a Save as draft button that bypasses the completeness gate: the user can save partial work and finish later - projectDraftInputSchema makes the user-facing copy (title, summary, service label, client) optional; the action auto-generates a slug from the title (or draft-<timestamp>), defaults the year to the current year, and forces the project unpublished - Drafts drop not-yet-valid sections/assets instead of failing the save - Pass intent via a hidden field set on click so the server reliably sees it - Tests: draft schema accepts empty copy a strict save rejects, and still requires a slug and a valid year
This commit is contained in:
@@ -30,10 +30,21 @@ import { getSiteSettings } from "@/lib/app-config";
|
||||
import {
|
||||
assetInputSchema,
|
||||
categoryInputSchema,
|
||||
projectDraftInputSchema,
|
||||
projectInputSchema,
|
||||
sectionInputSchema,
|
||||
} from "@/lib/portfolio-validation";
|
||||
|
||||
/** Build a URL-safe slug from a title, falling back to a unique draft slug. */
|
||||
function slugifyForDraft(input: string): string {
|
||||
const base = input
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
|
||||
return base || `draft-${Date.now()}`;
|
||||
}
|
||||
|
||||
async function ensureAdmin() {
|
||||
if (!(await isAdminAuthenticated())) {
|
||||
await clearAdminSessionCookie();
|
||||
@@ -204,28 +215,67 @@ export async function saveProjectAction(formData: FormData) {
|
||||
const createdMediaAssetIds: string[] = [];
|
||||
|
||||
try {
|
||||
const sections = parseJsonArray(formData.get("sections"), "sections").map((section, index) =>
|
||||
const intent = String(formData.get("intent") ?? "save");
|
||||
const isDraft = intent === "draft";
|
||||
|
||||
const parseSection = (section: Record<string, unknown>, index: number) =>
|
||||
sectionInputSchema.parse({
|
||||
...section,
|
||||
media: section.media ? mediaFieldInputSchema.parse(section.media) : undefined,
|
||||
sortOrder: section.sortOrder ?? index,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
const assets = parseJsonArray(formData.get("assets"), "assets").map((asset, index) =>
|
||||
const parseAsset = (asset: Record<string, unknown>, index: number) =>
|
||||
assetInputSchema.parse({
|
||||
...asset,
|
||||
media: asset.media ? mediaFieldInputSchema.parse(asset.media) : undefined,
|
||||
sortOrder: asset.sortOrder ?? index,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
// A draft keeps only the entries that are already valid; a full save
|
||||
// validates every entry strictly.
|
||||
const sections = parseJsonArray(formData.get("sections"), "sections").flatMap((section, index) => {
|
||||
if (!isDraft) {
|
||||
return [parseSection(section, index)];
|
||||
}
|
||||
|
||||
try {
|
||||
return [parseSection(section, index)];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
const assets = parseJsonArray(formData.get("assets"), "assets").flatMap((asset, index) => {
|
||||
if (!isDraft) {
|
||||
return [parseAsset(asset, index)];
|
||||
}
|
||||
|
||||
try {
|
||||
return [parseAsset(asset, index)];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
const coverMedia = parseJsonObject(formData.get("coverMedia"), "coverMedia");
|
||||
|
||||
const parsed = projectInputSchema.parse({
|
||||
const rawSlug = String(formData.get("slug") ?? "").trim();
|
||||
const slug =
|
||||
isDraft && !rawSlug
|
||||
? slugifyForDraft(
|
||||
String(formData.get("titleDe") ?? "") ||
|
||||
String(formData.get("titleEn") ?? "") ||
|
||||
String(formData.get("titleAr") ?? ""),
|
||||
)
|
||||
: rawSlug;
|
||||
const rawYear = String(formData.get("projectYear") ?? "").trim();
|
||||
const projectYear = isDraft && !rawYear ? String(new Date().getFullYear()) : rawYear;
|
||||
|
||||
const parsed = (isDraft ? projectDraftInputSchema : projectInputSchema).parse({
|
||||
id: String(formData.get("id") ?? "").trim() || undefined,
|
||||
categoryId: String(formData.get("categoryId") ?? ""),
|
||||
slug: String(formData.get("slug") ?? ""),
|
||||
slug,
|
||||
viewMode: String(formData.get("viewMode") ?? "GRID"),
|
||||
titleAr: String(formData.get("titleAr") ?? ""),
|
||||
titleEn: String(formData.get("titleEn") ?? ""),
|
||||
@@ -234,7 +284,7 @@ export async function saveProjectAction(formData: FormData) {
|
||||
summaryEn: String(formData.get("summaryEn") ?? ""),
|
||||
summaryDe: String(formData.get("summaryDe") ?? ""),
|
||||
clientName: String(formData.get("clientName") ?? ""),
|
||||
projectYear: String(formData.get("projectYear") ?? ""),
|
||||
projectYear,
|
||||
serviceLabelAr: String(formData.get("serviceLabelAr") ?? ""),
|
||||
serviceLabelEn: String(formData.get("serviceLabelEn") ?? ""),
|
||||
serviceLabelDe: String(formData.get("serviceLabelDe") ?? ""),
|
||||
@@ -243,7 +293,7 @@ export async function saveProjectAction(formData: FormData) {
|
||||
coverMedia: coverMedia ? mediaFieldInputSchema.parse(coverMedia) : undefined,
|
||||
sortOrder: String(formData.get("sortOrder") ?? "0"),
|
||||
isFeatured: normalizeCheckboxValue(formData, "isFeatured"),
|
||||
isPublished: normalizeCheckboxValue(formData, "isPublished"),
|
||||
isPublished: isDraft ? false : normalizeCheckboxValue(formData, "isPublished"),
|
||||
sections,
|
||||
assets,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user