From bf8652cf4fa5f40f14b47d6743a2b5b4d8a94281 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 25 Aug 2026 19:04:48 +0000 Subject: [PATCH 1/2] feat: five distinct widgets per category The catalog held one product per category, so 'Standard' the department and 'Standard Widget' the product were the same single thing and the category tiles led to a shelf of one. Clicking Mega could not pull Mega widgets because only one existed. The demo catalog now seeds 25 widgets - five per department. Within a department the five differ by form factor rather than by size: Block (the classic housing), Rotary (dial and pointer), Valve (inline, handwheel, ports), Hub (four ports) and Turbine (impeller in a housing). Kits, being containers, vary by case instead: pro, starter, builder, travel and master. Each carries its department in its name, so the existing keyword refinement in lib/catalog.ts routes it without new endpoints - Mega now returns exactly its five, Mini its five, Kits its five. Twenty-five illustrations follow, one per SKU, drawn from the same parametric machine in each department's finish so the shelf reads as one product line. productImage() matches the WW-001..WW-025 range rather than a set literal, so a new SKU outside it still falls back to the generic widget rather than a missing file. 243 frontend tests pass; coverage 100% lines and functions, 98.4% branches. Category routing verified against the running SPA: 25 across all departments, 5 under each of mega, mini and kit. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01EA4mmpcb1rcvNntHR1iG6j --- .../Seeding/DbSeeder.cs | 30 +++++++++++++++---- web/public/products/widget.svg | 2 +- web/public/products/ww-001.svg | 2 +- web/public/products/ww-002.svg | 2 +- web/public/products/ww-003.svg | 2 +- web/public/products/ww-004.svg | 2 +- web/public/products/ww-005.svg | 2 +- web/public/products/ww-006.svg | 1 + web/public/products/ww-007.svg | 1 + web/public/products/ww-008.svg | 1 + web/public/products/ww-009.svg | 1 + web/public/products/ww-010.svg | 1 + web/public/products/ww-011.svg | 1 + web/public/products/ww-012.svg | 1 + web/public/products/ww-013.svg | 1 + web/public/products/ww-014.svg | 1 + web/public/products/ww-015.svg | 1 + web/public/products/ww-016.svg | 1 + web/public/products/ww-017.svg | 1 + web/public/products/ww-018.svg | 1 + web/public/products/ww-019.svg | 1 + web/public/products/ww-020.svg | 1 + web/public/products/ww-021.svg | 1 + web/public/products/ww-022.svg | 1 + web/public/products/ww-023.svg | 1 + web/public/products/ww-024.svg | 1 + web/public/products/ww-025.svg | 1 + web/src/lib/img.ts | 7 +++-- 28 files changed, 56 insertions(+), 13 deletions(-) create mode 100644 web/public/products/ww-006.svg create mode 100644 web/public/products/ww-007.svg create mode 100644 web/public/products/ww-008.svg create mode 100644 web/public/products/ww-009.svg create mode 100644 web/public/products/ww-010.svg create mode 100644 web/public/products/ww-011.svg create mode 100644 web/public/products/ww-012.svg create mode 100644 web/public/products/ww-013.svg create mode 100644 web/public/products/ww-014.svg create mode 100644 web/public/products/ww-015.svg create mode 100644 web/public/products/ww-016.svg create mode 100644 web/public/products/ww-017.svg create mode 100644 web/public/products/ww-018.svg create mode 100644 web/public/products/ww-019.svg create mode 100644 web/public/products/ww-020.svg create mode 100644 web/public/products/ww-021.svg create mode 100644 web/public/products/ww-022.svg create mode 100644 web/public/products/ww-023.svg create mode 100644 web/public/products/ww-024.svg create mode 100644 web/public/products/ww-025.svg diff --git a/src/WidgetWorks.Infrastructure/Seeding/DbSeeder.cs b/src/WidgetWorks.Infrastructure/Seeding/DbSeeder.cs index 2d4f022..a0c527a 100644 --- a/src/WidgetWorks.Infrastructure/Seeding/DbSeeder.cs +++ b/src/WidgetWorks.Infrastructure/Seeding/DbSeeder.cs @@ -29,11 +29,31 @@ public sealed class DbSeeder(IDbConnectionFactory factory, IPasswordHasher hashe { private static readonly (string Sku, string Name, string Description, decimal Price, int OnHand)[] DemoWidgets = [ - ("WW-001", "Standard Widget", "The dependable everyday widget.", 9.99m, 250), - ("WW-002", "Deluxe Widget", "Premium finish with a reinforced housing.", 24.99m, 120), - ("WW-003", "Mega Widget", "Oversized widget for heavy-duty jobs.", 49.99m, 60), - ("WW-004", "Mini Widget", "Compact widget for tight spaces.", 4.99m, 500), - ("WW-005", "Widget Pro Kit", "Bundle of assorted widgets and accessories.", 79.99m, 40), + ("WW-001", "Standard Widget Block", "The dependable everyday widget.", 9.99m, 250), + ("WW-002", "Standard Widget Rotary", "Dial-adjustable everyday widget.", 11.49m, 220), + ("WW-003", "Standard Widget Valve", "Inline everyday widget with twin ports.", 12.99m, 180), + ("WW-004", "Standard Widget Hub", "Four-port everyday widget.", 13.99m, 160), + ("WW-005", "Standard Widget Turbine", "Ventilated everyday widget.", 14.99m, 140), + ("WW-006", "Deluxe Widget Block", "Premium finish with a reinforced housing.", 24.99m, 120), + ("WW-007", "Deluxe Widget Rotary", "Premium dial with a gold-plated pointer.", 27.49m, 110), + ("WW-008", "Deluxe Widget Valve", "Premium inline widget, machined ports.", 29.99m, 90), + ("WW-009", "Deluxe Widget Hub", "Premium four-port widget, gold contacts.", 32.99m, 80), + ("WW-010", "Deluxe Widget Turbine", "Premium ventilated widget, balanced rotor.", 34.99m, 70), + ("WW-011", "Mega Widget Block", "Oversized widget for heavy-duty jobs.", 49.99m, 60), + ("WW-012", "Mega Widget Rotary", "Oversized dial widget for heavy-duty jobs.", 54.49m, 55), + ("WW-013", "Mega Widget Valve", "Oversized inline widget, high-flow ports.", 59.99m, 45), + ("WW-014", "Mega Widget Hub", "Oversized four-port widget for busy lines.", 64.99m, 40), + ("WW-015", "Mega Widget Turbine", "Oversized ventilated widget, high throughput.", 69.99m, 35), + ("WW-016", "Mini Widget Block", "Compact widget for tight spaces.", 4.99m, 500), + ("WW-017", "Mini Widget Rotary", "Compact dial widget for tight spaces.", 5.99m, 460), + ("WW-018", "Mini Widget Valve", "Compact inline widget, low-flow ports.", 6.49m, 420), + ("WW-019", "Mini Widget Hub", "Compact four-port widget for tight spaces.", 7.49m, 380), + ("WW-020", "Mini Widget Turbine", "Compact ventilated widget, quiet running.", 8.49m, 340), + ("WW-021", "Widget Pro Kit", "Bundle of assorted widgets and accessories.", 79.99m, 40), + ("WW-022", "Widget Starter Kit", "Open-tray bundle for a first build.", 39.99m, 90), + ("WW-023", "Widget Builder Kit", "Three-drawer cabinet of sorted widgets.", 99.99m, 30), + ("WW-024", "Widget Travel Kit", "Soft-sided bundle for work away from the bench.", 49.99m, 60), + ("WW-025", "Widget Master Kit", "Two-case bundle covering the full range.", 149.99m, 20), ]; public async Task SeedAsync(SeedOptions options, CancellationToken ct) diff --git a/web/public/products/widget.svg b/web/public/products/widget.svg index 83aed71..dfaf001 100644 --- a/web/public/products/widget.svg +++ b/web/public/products/widget.svg @@ -1 +1 @@ - + diff --git a/web/public/products/ww-001.svg b/web/public/products/ww-001.svg index fb98253..d20f347 100644 --- a/web/public/products/ww-001.svg +++ b/web/public/products/ww-001.svg @@ -1 +1 @@ - + diff --git a/web/public/products/ww-002.svg b/web/public/products/ww-002.svg index 8e4c29d..de7640a 100644 --- a/web/public/products/ww-002.svg +++ b/web/public/products/ww-002.svg @@ -1 +1 @@ - + diff --git a/web/public/products/ww-003.svg b/web/public/products/ww-003.svg index 1fc1560..14ed72c 100644 --- a/web/public/products/ww-003.svg +++ b/web/public/products/ww-003.svg @@ -1 +1 @@ - + diff --git a/web/public/products/ww-004.svg b/web/public/products/ww-004.svg index 10693cb..880a1d9 100644 --- a/web/public/products/ww-004.svg +++ b/web/public/products/ww-004.svg @@ -1 +1 @@ - + diff --git a/web/public/products/ww-005.svg b/web/public/products/ww-005.svg index acd6690..1b87fa5 100644 --- a/web/public/products/ww-005.svg +++ b/web/public/products/ww-005.svg @@ -1 +1 @@ - + diff --git a/web/public/products/ww-006.svg b/web/public/products/ww-006.svg new file mode 100644 index 0000000..c661575 --- /dev/null +++ b/web/public/products/ww-006.svg @@ -0,0 +1 @@ + diff --git a/web/public/products/ww-007.svg b/web/public/products/ww-007.svg new file mode 100644 index 0000000..e27ef45 --- /dev/null +++ b/web/public/products/ww-007.svg @@ -0,0 +1 @@ + diff --git a/web/public/products/ww-008.svg b/web/public/products/ww-008.svg new file mode 100644 index 0000000..5b42ef6 --- /dev/null +++ b/web/public/products/ww-008.svg @@ -0,0 +1 @@ + diff --git a/web/public/products/ww-009.svg b/web/public/products/ww-009.svg new file mode 100644 index 0000000..c58cb7c --- /dev/null +++ b/web/public/products/ww-009.svg @@ -0,0 +1 @@ + diff --git a/web/public/products/ww-010.svg b/web/public/products/ww-010.svg new file mode 100644 index 0000000..0ff15d2 --- /dev/null +++ b/web/public/products/ww-010.svg @@ -0,0 +1 @@ + diff --git a/web/public/products/ww-011.svg b/web/public/products/ww-011.svg new file mode 100644 index 0000000..4ab3ade --- /dev/null +++ b/web/public/products/ww-011.svg @@ -0,0 +1 @@ + diff --git a/web/public/products/ww-012.svg b/web/public/products/ww-012.svg new file mode 100644 index 0000000..ce49a0c --- /dev/null +++ b/web/public/products/ww-012.svg @@ -0,0 +1 @@ + diff --git a/web/public/products/ww-013.svg b/web/public/products/ww-013.svg new file mode 100644 index 0000000..fdad27b --- /dev/null +++ b/web/public/products/ww-013.svg @@ -0,0 +1 @@ + diff --git a/web/public/products/ww-014.svg b/web/public/products/ww-014.svg new file mode 100644 index 0000000..bd9d188 --- /dev/null +++ b/web/public/products/ww-014.svg @@ -0,0 +1 @@ + diff --git a/web/public/products/ww-015.svg b/web/public/products/ww-015.svg new file mode 100644 index 0000000..37283b9 --- /dev/null +++ b/web/public/products/ww-015.svg @@ -0,0 +1 @@ + diff --git a/web/public/products/ww-016.svg b/web/public/products/ww-016.svg new file mode 100644 index 0000000..7f6758c --- /dev/null +++ b/web/public/products/ww-016.svg @@ -0,0 +1 @@ + diff --git a/web/public/products/ww-017.svg b/web/public/products/ww-017.svg new file mode 100644 index 0000000..078bde6 --- /dev/null +++ b/web/public/products/ww-017.svg @@ -0,0 +1 @@ + diff --git a/web/public/products/ww-018.svg b/web/public/products/ww-018.svg new file mode 100644 index 0000000..0691471 --- /dev/null +++ b/web/public/products/ww-018.svg @@ -0,0 +1 @@ + diff --git a/web/public/products/ww-019.svg b/web/public/products/ww-019.svg new file mode 100644 index 0000000..9cfc4e7 --- /dev/null +++ b/web/public/products/ww-019.svg @@ -0,0 +1 @@ + diff --git a/web/public/products/ww-020.svg b/web/public/products/ww-020.svg new file mode 100644 index 0000000..d03752b --- /dev/null +++ b/web/public/products/ww-020.svg @@ -0,0 +1 @@ + diff --git a/web/public/products/ww-021.svg b/web/public/products/ww-021.svg new file mode 100644 index 0000000..bc1f3f6 --- /dev/null +++ b/web/public/products/ww-021.svg @@ -0,0 +1 @@ + diff --git a/web/public/products/ww-022.svg b/web/public/products/ww-022.svg new file mode 100644 index 0000000..dd27f3d --- /dev/null +++ b/web/public/products/ww-022.svg @@ -0,0 +1 @@ + diff --git a/web/public/products/ww-023.svg b/web/public/products/ww-023.svg new file mode 100644 index 0000000..6e44611 --- /dev/null +++ b/web/public/products/ww-023.svg @@ -0,0 +1 @@ + diff --git a/web/public/products/ww-024.svg b/web/public/products/ww-024.svg new file mode 100644 index 0000000..5d1d4ba --- /dev/null +++ b/web/public/products/ww-024.svg @@ -0,0 +1 @@ + diff --git a/web/public/products/ww-025.svg b/web/public/products/ww-025.svg new file mode 100644 index 0000000..b5e1b45 --- /dev/null +++ b/web/public/products/ww-025.svg @@ -0,0 +1 @@ + diff --git a/web/src/lib/img.ts b/web/src/lib/img.ts index e934a79..e896ce8 100644 --- a/web/src/lib/img.ts +++ b/web/src/lib/img.ts @@ -3,12 +3,15 @@ // SKU (web/public/products) so the grid needs no external image service and // renders offline. Real product images set on the widget (admin) take // precedence; SKUs without bespoke art share a generic widget illustration. -const ILLUSTRATED_SKUS = new Set(['ww-001', 'ww-002', 'ww-003', 'ww-004', 'ww-005']) +const ILLUSTRATED = /^ww-(\d{3})$/ +const ILLUSTRATED_COUNT = 25 export function productImage(w: { sku: string; imageUrl?: string | null }): string { if (w.imageUrl && w.imageUrl.trim()) return w.imageUrl const sku = (w.sku || '').toLowerCase() - return ILLUSTRATED_SKUS.has(sku) ? `/products/${sku}.svg` : '/products/widget.svg' + const match = ILLUSTRATED.exec(sku) + const n = match ? Number(match[1]) : 0 + return n >= 1 && n <= ILLUSTRATED_COUNT ? `/products/${sku}.svg` : '/products/widget.svg' } // Deterministic, cosmetic star rating derived from the SKU. Placeholder visuals From 9d6215d89b45cf523c14641b99b483c1246074d9 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 25 Aug 2026 19:09:53 +0000 Subject: [PATCH 2/2] test: point the seeder assertion at a whole product name Seeding_stocks_the_demo_catalog_and_repeats_safely pinned WW-001 to "Standard Widget", which is now "Standard Widget Block", and counted rows matching "Standard Widget" to prove a second seed adds no duplicates. Both assertions move to the whole product name. The count one has to: the repository filters on name ilike '%search%', so with five widgets per department "Standard Widget" matches all five and the check would pass for the wrong reason. "Standard Widget Block" is unique to WW-001, so the test keeps meaning exactly what it meant - one row per SKU after a repeat run. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01EA4mmpcb1rcvNntHR1iG6j --- .../SeederAndMigrationTests.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/WidgetWorks.IntegrationTests/SeederAndMigrationTests.cs b/tests/WidgetWorks.IntegrationTests/SeederAndMigrationTests.cs index 81d5846..c22b744 100644 --- a/tests/WidgetWorks.IntegrationTests/SeederAndMigrationTests.cs +++ b/tests/WidgetWorks.IntegrationTests/SeederAndMigrationTests.cs @@ -107,13 +107,15 @@ public async Task Seeding_stocks_the_demo_catalog_and_repeats_safely() await Seeder.SeedAsync(Options(Guid.NewGuid().ToString("N")[..8]), CancellationToken.None); var standard = await widgets.GetBySkuAsync("WW-001", CancellationToken.None); - Assert.Equal("Standard Widget", standard!.Name); + Assert.Equal("Standard Widget Block", standard!.Name); Assert.Equal(9.99m, standard.Price); await Seeder.SeedAsync(Options(Guid.NewGuid().ToString("N")[..8]), CancellationToken.None); - // Still one row per SKU after a second run. - Assert.Equal(1, await widgets.CountAsync(new WidgetQuery("Standard Widget", true, 1, 50), CancellationToken.None)); + // Still one row per SKU after a second run. The search has to name a whole + // product: each department seeds five widgets, so "Standard Widget" on its own + // now matches all five of them. + Assert.Equal(1, await widgets.CountAsync(new WidgetQuery("Standard Widget Block", true, 1, 50), CancellationToken.None)); } [Fact]