Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/app/create/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
Expand Down
20 changes: 20 additions & 0 deletions src/components/BiodataForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -232,6 +234,24 @@ function Field({
rows={2}
className={base}
/>
) : field.type === "combo" ? (
<>
{/* Type-or-select: free text with suggestions from a datalist. */}
<input
type="text"
list={`${field.key}-options`}
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={field.placeholder}
className={base}
autoComplete="off"
/>
<datalist id={`${field.key}-options`}>
{field.options?.map((opt) => (
<option key={opt} value={opt} />
))}
</datalist>
</>
) : (
<input
type={field.type ?? "text"}
Expand Down
4 changes: 3 additions & 1 deletion src/components/templates/RoyalTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ export default function RoyalTemplate({ data }: TemplateProps) {
>
{section.title}
</h2>
<dl className="grid grid-cols-1 gap-x-8 gap-y-1.5 px-1 sm:grid-cols-2">
{/* Always 2 columns — the document is A4-width regardless of device, so
it must not use viewport breakpoints (that made mobile paginate to 2). */}
<dl className="grid grid-cols-2 gap-x-8 gap-y-1.5 px-1">
{section.rows.map((row) => (
<div key={row.label} className="flex gap-3 text-[12.5px] leading-relaxed">
<dt className="w-[42%] shrink-0 font-semibold text-[#55606f]">{row.label}</dt>
Expand Down
25 changes: 25 additions & 0 deletions src/components/templates/no-responsive.test.ts
Original file line number Diff line number Diff line change
@@ -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([]);
});
});
15 changes: 12 additions & 3 deletions src/data/biodata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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",
Expand Down
Loading