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/BiodataForm.tsx b/src/components/BiodataForm.tsx index cb36773..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 ( @@ -232,6 +234,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) => ( + + ) : ( {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([]); + }); +}); diff --git a/src/data/biodata.ts b/src/data/biodata.ts index 58a69bb..f149769 100644 --- a/src/data/biodata.ts +++ b/src/data/biodata.ts @@ -5,17 +5,26 @@ * Field `key`s are unique across all sections so a flat `values` map is enough. */ -export type FieldType = "text" | "date" | "time" | "tel" | "email" | "select" | "textarea"; +export type FieldType = "text" | "date" | "time" | "tel" | "email" | "select" | "textarea" | "combo"; export interface FieldDef { key: string; label: string; type?: FieldType; placeholder?: string; - /** Options for a `select` field. */ + /** Choices for a `select` (fixed) or `combo` (suggestions you can also type past) field. */ options?: string[]; } +/** Common heights (4'6"–6'6") for the Height field; users can also type their own. */ +export const heightOptions: string[] = Array.from({ length: 25 }, (_, i) => { + 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",