import type { CampaignInput, TierInput } from "./api"; import type { TierType } from "./types"; /** * Parsing helpers shared by the admin form and its server action. Kept apart * from the action file so they can be read without "use server" in the way. */ export const CATEGORIES = [ "Közösségi tér", "Oktatás", "Sport", "Kultúra & Művészet", "Környezet", "Egészség", ] as const; export const ART_KEYS = ["kert", "hang", "bicikli", "szinhaz"] as const; export const STATUSES = [ { value: "DRAFT", label: "Piszkozat" }, { value: "BEADVA", label: "Beadva" }, { value: "AKTIV", label: "Aktív" }, { value: "FINANSZIROZOTT", label: "Finanszírozott" }, { value: "LEZART", label: "Lezárt" }, { value: "ELUTASITVA", label: "Elutasítva" }, ] as const; export const TIER_TYPES: { value: Uppercase; label: string }[] = [ { value: "DONATION", label: "Adomány — azonnal terhelünk" }, { value: "DIGITAL", label: "Digitális — azonnal terhelünk" }, { value: "PHYSICAL", label: "Tárgyi — csak a cél elérésekor terhelünk" }, ]; export interface StoryDraft { heading: string; body: string[]; } /** * The story is edited as one textarea. A line starting with "## " opens a new * section; blank lines separate paragraphs. Minimal, and readable as plain text. */ export function parseStory(raw: string): StoryDraft[] { const sections: StoryDraft[] = []; let current: StoryDraft | null = null; let paragraph: string[] = []; const flushParagraph = () => { const text = paragraph.join(" ").trim(); if (text && current) current.body.push(text); paragraph = []; }; for (const line of raw.replace(/\r\n/g, "\n").split("\n")) { if (line.startsWith("## ")) { flushParagraph(); if (current) sections.push(current); current = { heading: line.slice(3).trim(), body: [] }; } else if (line.trim() === "") { flushParagraph(); } else { paragraph.push(line.trim()); } } flushParagraph(); if (current) sections.push(current); return sections; } export function serializeStory(sections: StoryDraft[]): string { return sections .map((s) => `## ${s.heading}\n\n${s.body.join("\n\n")}`) .join("\n\n"); } /** "1 200 000" / "1200000 Ft" -> 1200000. Empty or unparseable -> null. */ export function parseAmount(raw: FormDataEntryValue | null): number | null { if (typeof raw !== "string") return null; const digits = raw.replace(/[^\d]/g, ""); if (!digits) return null; return Number.parseInt(digits, 10); } export function slugify(input: string): string { const map: Record = { á: "a", é: "e", í: "i", ó: "o", ö: "o", ő: "o", ú: "u", ü: "u", ű: "u", }; return input .toLowerCase() .replace(/[áéíóöőúüű]/g, (c) => map[c] ?? c) .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") .slice(0, 80); } /** * Reads the tier editor back out of a submitted form. * * The reconciliation this feeds used to live in a server action: rows carrying * an id are updates, rows without one are new, and a row ticked for deletion * is simply left out — the API deletes whatever is missing, unless pledges * reference it. Empty rows are ignored so the always-present blank row does * not create a tier. */ export function readTierRows(form: FormData, rowCount: number): TierInput[] { const tiers: TierInput[] = []; for (let i = 0; i < rowCount; i++) { const field = (key: string) => form.get(`tier-${i}-${key}`); const id = String(field("id") ?? ""); if (field("delete") === "on") continue; const title = String(field("title") ?? "").trim(); const price = parseAmount(field("price")); if (!title || !price) continue; tiers.push({ id, title, price, listPrice: parseAmount(field("listPrice")), type: String(field("type") ?? "DONATION"), includes: String(field("includes") ?? "") .split("\n") .map((line) => line.trim()) .filter(Boolean), inventoryLimit: parseAmount(field("limit")), estimatedDelivery: String(field("delivery") ?? ""), featured: field("featured") === "on", }); } return tiers; } /** Reads the whole editor, ready to PUT. */ export function readCampaignForm(form: FormData, rowCount: number): CampaignInput { const title = String(form.get("title") ?? "").trim(); return { id: String(form.get("campaignId") ?? ""), title, slug: slugify(String(form.get("slug") ?? "") || title), category: String(form.get("category") ?? ""), summary: String(form.get("summary") ?? "").trim(), art: String(form.get("art") ?? "kert"), goalAmount: parseAmount(form.get("goalAmount")) ?? 0, endsAt: String(form.get("endsAt") ?? ""), status: String(form.get("status") ?? "DRAFT"), ownerId: String(form.get("ownerId") ?? ""), story: parseStory(String(form.get("story") ?? "")), tiers: readTierRows(form, rowCount), }; }