From 4d12573a7209f6e6fbd7716780faab647ab45aef Mon Sep 17 00:00:00 2001 From: Shiv Shankar Tiwari Date: Sun, 28 Jun 2026 04:19:53 +0530 Subject: [PATCH 1/3] feat(form): Height is a type-or-select combo box MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Render the Height field as a text input backed by a of common heights (4'6"–6'6" with cm), so users can pick a value or type their own. Adds a 'combo' field type to the data model. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/components/BiodataForm.tsx | 18 ++++++++++++++++++ src/data/biodata.ts | 15 ++++++++++++--- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/components/BiodataForm.tsx b/src/components/BiodataForm.tsx index cdcc6da..29da635 100644 --- a/src/components/BiodataForm.tsx +++ b/src/components/BiodataForm.tsx @@ -232,6 +232,24 @@ function Field({ rows={2} className={base} /> + ) : field.type === "combo" ? ( + <> + {/* Type-or-select: free text with suggestions from a datalist. */} + onChange(e.target.value)} + placeholder={field.placeholder} + className={base} + autoComplete="off" + /> + + {field.options?.map((opt) => ( + + ) : ( { + const totalInches = 54 + i; // 4'6" … 6'6" + const ft = Math.floor(totalInches / 12); + const inch = totalInches % 12; + const cm = Math.round(totalInches * 2.54); + return `${ft}'${inch}" (${cm} cm)`; +}); + export interface SectionDef { id: string; title: string; @@ -31,7 +40,7 @@ export const biodataSections: SectionDef[] = [ { key: "dob", label: "Date of Birth", type: "date" }, { key: "tob", label: "Time of Birth", type: "time" }, { key: "pob", label: "Place of Birth", placeholder: "City, State" }, - { key: "height", label: "Height", placeholder: `e.g. 5'10" (178 cm)` }, + { key: "height", label: "Height", type: "combo", options: heightOptions, placeholder: `Select or type, e.g. 5'10" (178 cm)` }, { key: "complexion", label: "Complexion", From 75c26c0204d86cc10a6281f67bdeae25b7dfe963 Mon Sep 17 00:00:00 2001 From: Shiv Shankar Tiwari Date: Sun, 28 Jun 2026 04:31:47 +0530 Subject: [PATCH 2/3] fix(pdf): device-consistent Royal pagination + cleaner filename MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Royal used a viewport-responsive grid (grid-cols-1 sm:grid-cols-2) inside the document, so on a narrow phone it collapsed to one column, measured taller, and paginated to 2 pages while desktop showed 1 — a different PDF per device. The A4 document must never use viewport breakpoints; make it always 2 columns. Now identical on desktop and mobile (verified: same unit heights, 1 page). - Add a regression test asserting no template uses sm:/md:/lg:/xl: classes. - PDF filename: a non-Latin (e.g. Hindi) name slugifies to empty; fall back to "biodata.pdf" instead of "biodata-biodata.pdf". Verified via full E2E (5 templates × desktop/mobile): all download identical PDFs per template, no crashes clearing/retyping fields. 43 unit tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/app/create/page.tsx | 7 ++++-- src/components/templates/RoyalTemplate.tsx | 4 ++- .../templates/no-responsive.test.ts | 25 +++++++++++++++++++ 3 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 src/components/templates/no-responsive.test.ts diff --git a/src/app/create/page.tsx b/src/app/create/page.tsx index acfb507..cfa2370 100644 --- a/src/app/create/page.tsx +++ b/src/app/create/page.tsx @@ -37,9 +37,12 @@ export default function CreatePage() { await new Promise((r) => window.setTimeout(r, 300)); const area = document.getElementById("print-area"); if (!area) throw new Error("preview not ready"); - const name = slugify(data.values.fullName || "") || "biodata"; + // Name-based filename; non-Latin names (e.g. Hindi) slugify to empty, so + // fall back to a plain "biodata.pdf" rather than "biodata-biodata.pdf". + const slug = slugify(data.values.fullName || ""); + const filename = slug ? `${slug}-biodata.pdf` : "biodata.pdf"; const { downloadBiodataPdf } = await import("@/lib/pdf"); - await downloadBiodataPdf(area, `${name}-biodata.pdf`); + await downloadBiodataPdf(area, filename); } catch (err) { console.error(err); window.alert("Sorry, the PDF couldn't be generated. Please try again."); diff --git a/src/components/templates/RoyalTemplate.tsx b/src/components/templates/RoyalTemplate.tsx index 4213516..4381add 100644 --- a/src/components/templates/RoyalTemplate.tsx +++ b/src/components/templates/RoyalTemplate.tsx @@ -59,7 +59,9 @@ export default function RoyalTemplate({ data }: TemplateProps) { > {section.title} -
+ {/* Always 2 columns — the document is A4-width regardless of device, so + it must not use viewport breakpoints (that made mobile paginate to 2). */} +
{section.rows.map((row) => (
{row.label}
diff --git a/src/components/templates/no-responsive.test.ts b/src/components/templates/no-responsive.test.ts new file mode 100644 index 0000000..226691a --- /dev/null +++ b/src/components/templates/no-responsive.test.ts @@ -0,0 +1,25 @@ +import { describe, it, expect } from "vitest"; +import { readFileSync, readdirSync } from "node:fs"; +import { join } from "node:path"; + +/** + * Templates render a fixed A4-width document, so they must NOT use viewport + * breakpoints (sm:/md:/lg:/xl:). A responsive class collapses the layout on a + * narrow phone, making the document measure taller there and paginate + * differently than on desktop — i.e. a different PDF per device. Guard against + * reintroducing that (it was the Royal "1 page desktop / 2 pages mobile" bug). + */ +const dir = join(process.cwd(), "src/components/templates"); +const files = readdirSync(dir).filter((f) => f.endsWith("Template.tsx")); + +describe("templates use no viewport-responsive classes", () => { + it("has template files to check", () => { + expect(files.length).toBeGreaterThan(0); + }); + + it.each(files)("%s has no sm:/md:/lg:/xl: classes", (file) => { + const src = readFileSync(join(dir, file), "utf8"); + const matches = src.match(/\b(?:sm|md|lg|xl|2xl):/g) ?? []; + expect(matches).toEqual([]); + }); +}); From a8be57e0f9ac80610d15c3859af935f27fb1a962 Mon Sep 17 00:00:00 2001 From: Shiv Shankar Tiwari Date: Sun, 28 Jun 2026 07:55:50 +0530 Subject: [PATCH 3/3] fix(form): allow re-selecting the same photo after Remove MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reset the file input's value after a successful read, so picking the same file again (e.g. after removing it) still fires change. Found via rapid-interaction stress testing (add → remove → add). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/components/BiodataForm.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/BiodataForm.tsx b/src/components/BiodataForm.tsx index 07d312f..99c80f7 100644 --- a/src/components/BiodataForm.tsx +++ b/src/components/BiodataForm.tsx @@ -52,6 +52,8 @@ export default function BiodataForm({ data, onChange }: Props) { reader.onerror = () => setPhotoError("Sorry, that image couldn't be loaded. Please try another."); reader.onload = () => onChange({ ...data, photo: String(reader.result) }); reader.readAsDataURL(file); + // Reset the input so re-selecting the same file (e.g. after Remove) still fires. + e.target.value = ""; }; return (