Skip to content

Commit 3954f4b

Browse files
authored
Persist print-list display sort and export format (#61)
The print-list display sort (added/name/qty) and decklist export format (grouped/plain) reset on every reload while the render options already persisted. Add both to the validated RenderOptions shape (per-field enum fallback) and wire them into the existing load/save effects, so the whole print page remembers its preferences across visits.
1 parent a647390 commit 3954f4b

4 files changed

Lines changed: 39 additions & 8 deletions

File tree

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,9 +118,10 @@ by added order / name / quantity and per-row -/+ quantity steppers),
118118
and a one-click render to home PDF or MakePlayingCards ZIP (with paper, DPI,
119119
gutter, and bleed options, plus an optional deck name that slugifies into the
120120
download filename, e.g. `my-lugia-deck.pdf`). The render options (target,
121-
paper, DPI, gutter, bleed, deck name) persist in localStorage and are restored
122-
on the next visit, with each stored field validated/clamped on load so a stale
123-
entry can never put the form into an invalid state. Search is served by
121+
paper, DPI, gutter, bleed, deck name, plus the print-list display sort and the
122+
export format) persist in localStorage and are restored on the next visit, with
123+
each stored field validated/clamped on load so a stale entry can never put the
124+
form into an invalid state. Search is served by
124125
Meilisearch with the Postgres fallback described above.
125126

126127
**Decklist import.** Paste a Pokémon TCG Live / Limitless decklist

apps/web/app/print/page.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,19 +57,21 @@ export default function PrintPage() {
5757
setBleed(o.bleed);
5858
setGutter(o.gutter);
5959
setDeckName(o.deckName);
60+
setPrintSort(o.printSort);
61+
setExportFormat(o.exportFormat);
6062
hydrated.current = true;
6163
}, []);
6264
useEffect(() => {
6365
if (!hydrated.current) return;
6466
try {
6567
window.localStorage.setItem(
6668
RENDER_OPTS_KEY,
67-
serializeRenderOptions({ target: target as 'pdf' | 'mpc', paper: paper as 'A4' | 'letter', dpi: dpi as '300' | '600', bleed, gutter, deckName }),
69+
serializeRenderOptions({ target: target as 'pdf' | 'mpc', paper: paper as 'A4' | 'letter', dpi: dpi as '300' | '600', bleed, gutter, deckName, printSort, exportFormat }),
6870
);
6971
} catch {
7072
/* storage full/unavailable: a non-persisted option is not worth crashing */
7173
}
72-
}, [target, paper, dpi, bleed, gutter, deckName]);
74+
}, [target, paper, dpi, bleed, gutter, deckName, printSort, exportFormat]);
7375

7476
const total = items.reduce((n, x) => n + x.qty, 0);
7577
const sheets = Math.ceil(total / 9); // 3x3 N-up

apps/web/lib/renderOptions.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,18 @@
66
* lives in the component.
77
*/
88

9+
import type { PrintSort } from './printsort';
10+
import type { ExportFormat } from './printlist';
11+
912
export interface RenderOptions {
1013
target: 'pdf' | 'mpc';
1114
paper: 'A4' | 'letter';
1215
dpi: '300' | '600';
1316
bleed: boolean;
1417
gutter: string; // millimetres, '0'..'20'
1518
deckName: string;
19+
printSort: PrintSort; // print-list display order
20+
exportFormat: ExportFormat; // decklist export shape
1621
}
1722

1823
export const DEFAULT_RENDER_OPTIONS: RenderOptions = {
@@ -22,11 +27,15 @@ export const DEFAULT_RENDER_OPTIONS: RenderOptions = {
2227
bleed: false,
2328
gutter: '4',
2429
deckName: '',
30+
printSort: 'added',
31+
exportFormat: 'grouped',
2532
};
2633

2734
const TARGETS = ['pdf', 'mpc'] as const;
2835
const PAPERS = ['A4', 'letter'] as const;
2936
const DPIS = ['300', '600'] as const;
37+
const PRINT_SORTS = ['added', 'name', 'qty'] as const;
38+
const EXPORT_FORMATS = ['grouped', 'plain'] as const;
3039

3140
function pick<T extends string>(allowed: readonly T[], v: unknown, fallback: T): T {
3241
return typeof v === 'string' && (allowed as readonly string[]).includes(v) ? (v as T) : fallback;
@@ -48,6 +57,8 @@ export function parseRenderOptions(raw: unknown): RenderOptions {
4857
bleed: typeof o.bleed === 'boolean' ? o.bleed : DEFAULT_RENDER_OPTIONS.bleed,
4958
gutter: clampGutter(o.gutter),
5059
deckName: typeof o.deckName === 'string' ? o.deckName.slice(0, 80) : DEFAULT_RENDER_OPTIONS.deckName,
60+
printSort: pick(PRINT_SORTS, o.printSort, DEFAULT_RENDER_OPTIONS.printSort),
61+
exportFormat: pick(EXPORT_FORMATS, o.exportFormat, DEFAULT_RENDER_OPTIONS.exportFormat),
5162
};
5263
}
5364

apps/web/test/renderOptions.test.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,14 @@ test('parseRenderOptions returns defaults for non-object input', () => {
1515

1616
test('parseRenderOptions keeps valid fields and falls back per-field', () => {
1717
assert.deepEqual(
18-
parseRenderOptions({ target: 'mpc', paper: 'letter', dpi: '600', bleed: true, gutter: '8', deckName: 'My Deck' }),
19-
{ target: 'mpc', paper: 'letter', dpi: '600', bleed: true, gutter: '8', deckName: 'My Deck' },
18+
parseRenderOptions({
19+
target: 'mpc', paper: 'letter', dpi: '600', bleed: true, gutter: '8', deckName: 'My Deck',
20+
printSort: 'qty', exportFormat: 'plain',
21+
}),
22+
{
23+
target: 'mpc', paper: 'letter', dpi: '600', bleed: true, gutter: '8', deckName: 'My Deck',
24+
printSort: 'qty', exportFormat: 'plain',
25+
},
2026
);
2127
// invalid enum values fall back, valid ones survive
2228
const r = parseRenderOptions({ target: 'xyz', paper: 'A4', dpi: 999, bleed: 'yes' });
@@ -26,6 +32,14 @@ test('parseRenderOptions keeps valid fields and falls back per-field', () => {
2632
assert.equal(r.bleed, false);
2733
});
2834

35+
test('validates the print-list display sort and export format', () => {
36+
assert.equal(parseRenderOptions({ printSort: 'name' }).printSort, 'name');
37+
assert.equal(parseRenderOptions({ printSort: 'bogus' }).printSort, 'added');
38+
assert.equal(parseRenderOptions({ printSort: 42 }).printSort, 'added');
39+
assert.equal(parseRenderOptions({ exportFormat: 'plain' }).exportFormat, 'plain');
40+
assert.equal(parseRenderOptions({ exportFormat: 'xml' }).exportFormat, 'grouped');
41+
});
42+
2943
test('clamps gutter to 0..20 integer millimetres', () => {
3044
assert.equal(parseRenderOptions({ gutter: '100' }).gutter, '20');
3145
assert.equal(parseRenderOptions({ gutter: -5 }).gutter, '0');
@@ -44,6 +58,9 @@ test('loadRenderOptions tolerates null and malformed JSON', () => {
4458
});
4559

4660
test('serialize then load round-trips a valid options object', () => {
47-
const o = { target: 'mpc', paper: 'letter', dpi: '600', bleed: true, gutter: '12', deckName: 'Lugia' } as const;
61+
const o = {
62+
target: 'mpc', paper: 'letter', dpi: '600', bleed: true, gutter: '12', deckName: 'Lugia',
63+
printSort: 'qty', exportFormat: 'plain',
64+
} as const;
4865
assert.deepEqual(loadRenderOptions(serializeRenderOptions(o)), o);
4966
});

0 commit comments

Comments
 (0)