Skip to content

Comments

[FEAT] Frontend Pivot: 2D Viewer and PZ UX#621

Merged
shayancoin merged 3 commits intomainfrom
feat/configurator-pzds-pivot
Nov 8, 2025
Merged

[FEAT] Frontend Pivot: 2D Viewer and PZ UX#621
shayancoin merged 3 commits intomainfrom
feat/configurator-pzds-pivot

Conversation

@shayancoin
Copy link
Owner

Summary

  • replace legacy 3D viewer with the new Parviz 2D sprite compositor and PZ design system tokens
  • wire the configurator panel, pricing footer, and Playwright checks to the updated layout and metrics
  • drop unused three.js tooling and ship initial kitchen hero sprite assets + OTEL viewer spans

Testing

  • npm install --package-lock-only --ignore-scripts

@vercel
Copy link

vercel bot commented Nov 3, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
paform Ready Ready Preview Comment Nov 3, 2025 6:09am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 3, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Summary by CodeRabbit

  • New Features

    • Introduced 2D layered sprite viewer for kitchen visualization
    • Added pricing footer displaying total price, monthly financing estimates (3.99% APR, 72-month term), and order actions
  • Refactor

    • Redesigned configurator layout with improved visual hierarchy and spacing
    • Migrated from 3D model rendering to 2D sprite-based composition
    • Updated theme colors and styling system
  • Chores

    • Added OpenTelemetry instrumentation for performance monitoring
    • Updated dependencies and build configuration

Walkthrough

Removed 3D viewer, related pages and tests; added a 2D layered sprite viewer and browser OpenTelemetry instrumentation; moved styling from KDS/CSS to Tailwind and updated global colors; adjusted pages, imports, and package.json scripts/dependencies.

Changes

Cohort / File(s) Summary
3D Viewer Removal
frontend/src/app/components/Viewer3D.tsx, frontend/src/app/components/__tests__/viewer3d.test.tsx
Deleted the Viewer3D component and its full test suite (Three.js/GLTF logic, controls, helpers, and interaction tests).
2D Viewer & Compose Instrumentation
frontend/src/components/Viewer2D.tsx, frontend/src/otel/viewer2d-instrumentation.ts
Added/rewrote Viewer2D to render sorted sprite layers as stacked Images with load-tracking; added beginViewerCompose/ComposeHandle OTEL API to instrument compose spans.
Sprite Manifest & Assets
frontend/public/sprites/kitchen/hero/layers.json, frontend/src/lib/sprite-manifest.ts
New hero sprite manifest (1600x900); manifest baseUrl and filenames updated; lighting layer removed and revision/paths adjusted.
Removed Preloader
frontend/src/lib/sprite-preloader.ts
Removed sprite preloader module and its cached fetch logic.
Pages & Routing Simplification
frontend/src/app/configurator/ClientPage.tsx, frontend/src/app/configurator/page.tsx, frontend/src/app/kitchen-configurator/[sku]/page.tsx, frontend/src/app/partcad/[model]/page.tsx, frontend/src/app/(shop)/product/[handle]/page.tsx
Replaced 3D pages with ClientPage/Viewer2D, removed partcad page, simplified dynamic imports/types, and applied dynamic='force-dynamic' where added/kept.
Layout Shell & KDS CSS Removal
frontend/src/components/KitchenLayoutShell.tsx, frontend/src/styles/kds.css, frontend/src/components/GalleryAffordances.css
Deleted KitchenLayoutShell and the KDS and gallery affordances stylesheets (tokens, component styles, gallery interactions).
Theme, Globals & Providers
frontend/src/app/globals.css, frontend/src/app/layout.tsx, frontend/src/app/providers.tsx
Updated CSS root vars (foreground/background), removed vector pink/purple, removed kds.css import, changed body bg to light, and updated Chakra theme colors to blue/black with explicit hex globals.
Configurator & Summary UI
frontend/src/components/ConfiguratorPanel.tsx, frontend/src/components/OrderSummaryCard.tsx, frontend/src/components/PricingFooter.tsx
Migrated to Tailwind-based styling, redesigned OptionRow and OrderSummaryCard layout/content, and added a new PricingFooter component with financing calculation and CTA.
Instrumentation & package.json
frontend/src/instrumentation.ts, frontend/package.json, frontend/next.config.mjs
Removed Node OTEL registration bits, introduced browser-only init/initBrowserTracing, added browser OpenTelemetry deps, removed threepipe and assets:* scripts, and removed experimental.instrumentationHook.
Tests & Playwright
frontend/tests/e2e/layout.spec.ts, frontend/tests/e2e/perf/perf-reporter.ts, frontend/playwright.config.ts
Updated e2e tests to assert Viewer2D and PricingFooter; adjusted Playwright perf reporter types; normalized reporter entries in config.
Async helpers & Misc
frontend/src/app/dealer/actions.ts, frontend/src/app/server-actions/dealerQuotes.ts, frontend/src/workers/compositor.worker.ts, frontend/next-env.d.ts, frontend/src/app/components/app-ready.tsx
Made cookie/token/client helpers async and awaited; renamed dynamic import variable for clarity; simplified worker iteration; added TS reference path; minor iteration style change in worker.

Sequence Diagram(s)

sequenceDiagram
    participant U as User
    participant Page as ClientPage
    participant V2 as Viewer2D
    participant L as LayerImage
    participant OT as OTEL

    U->>Page: navigate to configurator
    activate Page

    Page->>OT: beginViewerCompose(context)
    activate OT

    Page->>V2: render (Suspense -> Spinner fallback)
    activate V2

    V2->>V2: load manifest & sort layers
    par load layers
        V2->>L: request base layer image
        L-->>V2: onLoadingComplete
        V2->>L: request cabinets layer
        L-->>V2: onLoadingComplete
        V2->>L: request countertop layer
        L-->>V2: onLoadingComplete
        V2->>L: request hardware layer
        L-->>V2: onLoadingComplete
    end

    V2->>OT: composeHandle.end(success, durationMs)
    deactivate V2

    OT->>OT: record duration, set status, end span
    deactivate OT

    deactivate Page
    U->>U: view composed sprite
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Pay extra attention to:
    • Viewer2D composition lifecycle, layer ordering, responsive sizing and load states.
    • OTEL browser changes: tracer setup, span attributes, and dependency alignment.
    • Deleted 3D files/pages: ensure no dangling imports elsewhere.
    • Theme and CSS removals: visual regressions across key pages and components.

Possibly related PRs

Poem

🐰
I chewed the meshes, stacked pixels with care,
Layers align neatly, light dances there.
Pink swapped to blue, the background grows bright,
A rabbit applauds the simpler sight. ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Description Check ❓ Inconclusive The pull request description provides substantive information about the changes with a clear summary of the main objectives and a testing instruction section; however, it does not follow the required template structure with explicit "# PR Type", "# Short Description", and "# Tests Added" sections. While the content covers the what and why of the changes, it lacks specific detail about which tests were added or modified to support the feature changes, particularly given that the raw summary indicates test file removals (viewer3d.test.tsx) and rewrites (layout.spec.ts). The description is more aligned with a general change summary than a complete adherence to the specified template format. To improve this PR description, reformat it to follow the template exactly with labeled sections for PR Type (Feature), a concise Short Description of the key changes, and a detailed Tests Added section that describes which tests were added, modified, or removed to support the 2D viewer transition and updated UX components. This will ensure clarity about test coverage for the significant architectural changes being introduced.
✅ Passed checks (1 passed)
Check name Status Explanation
Title Check ✅ Passed The pull request title "[FEAT] Frontend Pivot: 2D Viewer and PZ UX" directly and clearly summarizes the primary change across the changeset: replacing the legacy 3D viewer component with a new 2D sprite-based compositor while adopting the Parviz design system (PZ) tokens and visual system. The title is concise, specific, and accurately reflects the main objectives documented in the PR description and the substantial refactoring evident across the file summaries. It avoids vague language and provides a teammate sufficient context to understand the core change without requiring review of all modified files.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/configurator-pzds-pivot

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 21a4592 and 30672d8.

📒 Files selected for processing (1)
  • frontend/playwright.config.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/playwright.config.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build-and-test-perf

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai bot added the codex label Nov 3, 2025
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +61 to +65
requestId,
optionsHash: computeOptionsHash({ elevation, finish, hardware, countertop, rev: manifest.rev }),
layerCount: layers.length,
})
setLoadedMap({})

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Clearing layer load state hides unchanged sprites after option changes

The effect that runs on every configurator change resets loadedMap to an empty object but the Image elements reuse the same layer.key when a layer’s asset doesn’t change (e.g. the base structure when only hardware is toggled). Because those images are not remounted, their onLoadingComplete handler never fires again, so isLoaded stays false and they render with opacity-0 indefinitely after the first configuration change. The result is that any layer whose key remains constant disappears from the viewer after adjusting other options. Consider preserving load state for persistent keys or force those layers to remount when you wipe the map.

Useful? React with 👍 / 👎.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
frontend/src/components/PricingFooter.tsx (1)

36-36: Hardcoded "Due Today" amount should be configurable.

The "$250" deposit amount is hardcoded. Consider making this configurable via the store or props for flexibility.

frontend/src/lib/sprite-manifest.ts (1)

34-39: Consider removing unused 'lighting' from SpriteLayer union.

The lighting layer has been removed from the manifest (line 7 in the type definition still includes 'lighting' in the SpriteLayer union), but it's no longer used in the manifestFor function.

If the lighting layer is permanently removed, apply this diff to clean up the type:

 export type SpriteLayer =
   | 'base'
   | 'cabinets'
   | 'hardware'
   | 'countertop'
   | 'backsplash'
-  | 'lighting'
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 8f46916 and d1b7fd6.

⛔ Files ignored due to path filters (2)
  • frontend/package-lock.json is excluded by !**/package-lock.json
  • frontend/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (20)
  • frontend/package.json (0 hunks)
  • frontend/public/sprites/kitchen/hero/layers.json (1 hunks)
  • frontend/src/app/components/Viewer3D.tsx (0 hunks)
  • frontend/src/app/components/__tests__/viewer3d.test.tsx (0 hunks)
  • frontend/src/app/configurator/ClientPage.tsx (1 hunks)
  • frontend/src/app/configurator/page.tsx (1 hunks)
  • frontend/src/app/globals.css (1 hunks)
  • frontend/src/app/layout.tsx (1 hunks)
  • frontend/src/app/providers.tsx (1 hunks)
  • frontend/src/components/ConfiguratorPanel.tsx (3 hunks)
  • frontend/src/components/GalleryAffordances.css (0 hunks)
  • frontend/src/components/KitchenLayoutShell.tsx (0 hunks)
  • frontend/src/components/OrderSummaryCard.tsx (1 hunks)
  • frontend/src/components/PricingFooter.tsx (1 hunks)
  • frontend/src/components/Viewer2D.tsx (2 hunks)
  • frontend/src/lib/sprite-manifest.ts (1 hunks)
  • frontend/src/lib/sprite-preloader.ts (0 hunks)
  • frontend/src/otel/viewer2d-instrumentation.ts (1 hunks)
  • frontend/src/styles/kds.css (0 hunks)
  • frontend/tests/e2e/layout.spec.ts (1 hunks)
💤 Files with no reviewable changes (7)
  • frontend/src/components/GalleryAffordances.css
  • frontend/src/components/KitchenLayoutShell.tsx
  • frontend/src/lib/sprite-preloader.ts
  • frontend/src/styles/kds.css
  • frontend/package.json
  • frontend/src/app/components/Viewer3D.tsx
  • frontend/src/app/components/tests/viewer3d.test.tsx
🧰 Additional context used
🧬 Code graph analysis (4)
frontend/src/components/Viewer2D.tsx (3)
frontend/src/lib/sprite-manifest.ts (1)
  • manifestFor (24-41)
frontend/src/otel/viewer2d-instrumentation.ts (1)
  • beginViewerCompose (20-49)
frontend/src/lib/telemetry.ts (1)
  • computeOptionsHash (45-45)
frontend/src/app/configurator/ClientPage.tsx (3)
frontend/src/lib/otel-route.ts (1)
  • initOtelRoute (20-38)
frontend/src/components/Viewer2D.tsx (1)
  • Viewer2D (21-118)
frontend/src/components/PricingFooter.tsx (1)
  • PricingFooter (10-51)
frontend/src/components/ConfiguratorPanel.tsx (1)
frontend/src/components/OrderSummaryCard.tsx (1)
  • OrderSummaryCard (10-38)
frontend/src/app/configurator/page.tsx (1)
frontend/src/app/configurator/ClientPage.tsx (1)
  • ClientPage (19-47)
🔇 Additional comments (10)
frontend/src/app/layout.tsx (1)

51-51: LGTM!

The background color #f4f4f4 correctly matches the --background-rgb: 244, 244, 244 value set in globals.css, ensuring consistency across the light theme update.

frontend/src/app/providers.tsx (2)

11-12: LGTM!

The brand color update from pink/purple to blue/black aligns with the broader design system pivot documented in the PR objectives.


18-19: LGTM!

The explicit hex values match the CSS custom properties in globals.css: #f4f4f4 corresponds to --background-rgb: 244, 244, 244 and #171a20 to --foreground-rgb: 23, 26, 32.

frontend/tests/e2e/layout.spec.ts (2)

6-16: LGTM!

The test correctly validates the configurator panel width of 448px, which matches the md:w-[448px] class in ClientPage.tsx, and verifies the pricing footer's static positioning.


18-24: LGTM!

The sprite rendering test provides basic coverage by verifying that at least one image is present in the viewer section, validating the 2D sprite compositor is functioning.

frontend/src/components/PricingFooter.tsx (1)

7-8: LGTM!

The financing constants are clearly defined and align with the displayed financing terms.

frontend/src/lib/sprite-manifest.ts (2)

28-29: LGTM!

The sprite size and base URL updates align with the new hero-based asset structure defined in layers.json.


32-32: LGTM!

Including the elevation parameter in the revision string provides better cache invalidation when elevation changes.

frontend/src/app/globals.css (1)

6-7: Legacy color variables successfully removed with no remaining references.

Verification confirms that --vector-pink and --vector-purple are not referenced anywhere in the codebase. Comprehensive searches across all file types (CSS, JavaScript, TypeScript, JSX, TSX) returned no matches for either direct usage or string-based references.

frontend/src/app/configurator/page.tsx (1)

1-1: No SSR issues found—the changes are safe and follow modern Next.js patterns.

The verification reveals no SSR incompatibilities:

  • All components (ClientPage, Viewer2D, PricingFooter, ConfiguratorPanel) are properly marked with 'use client'
  • initOtelRoute() has a proper guard: if (typeof window === 'undefined') return
  • No unguarded browser API usage detected
  • The store uses 'use client' and zustand with no direct browser APIs

The current approach using static imports with 'use client' directives is the modern, idiomatic Next.js 13+ pattern and is architecturally superior to the previous next/dynamic with ssr: false. The 'use client' directive explicitly marks components as client-only, eliminating SSR concerns without relying on code-splitting tricks.

Likely an incorrect or invalid review comment.

Comment on lines +1 to +10
{
"rev": "hero-r1",
"size": { "w": 1600, "h": 900 },
"layers": [
{ "key": "base:structure", "src": "/sprites/kitchen/hero/base.webp", "z": 0 },
{ "key": "cabinets:{finish}", "src": "/sprites/kitchen/hero/cabinets_{finish}.webp", "z": 10 },
{ "key": "countertop:{countertop}", "src": "/sprites/kitchen/hero/countertop_{countertop}.webp", "z": 20 },
{ "key": "hardware:{hardware}", "src": "/sprites/kitchen/hero/hardware_{hardware}.webp", "z": 30 }
]
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify usage of layers.json manifest.

This static JSON manifest defines the sprite layer structure, but it's unclear if and how it's consumed by the application. The sprite-manifest.ts file programmatically generates manifests with the same structure, suggesting this file might be unused or serve as documentation/reference.

Run the following script to check for references to this layers.json file:


🏁 Script executed:

#!/bin/bash
# Description: Check for imports or fetch calls to layers.json

# Search for imports or references to layers.json
rg -n --type=ts --type=tsx --type=js 'layers\.json'

# Search for fetch/require of the file
rg -n --type=ts --type=tsx --type=js '/sprites/kitchen/hero/layers'

Length of output: 55


I'll run improved shell scripts to search for references to this file more broadly:


🏁 Script executed:

#!/bin/bash
# Search for layers.json references without file type restrictions
echo "=== Searching for 'layers.json' ===" 
rg -n 'layers\.json' 

echo -e "\n=== Searching for sprite path pattern ===" 
rg -n '/sprites/kitchen/hero/layers'

echo -e "\n=== Looking for sprite-manifest.ts ===" 
fd -type f -name 'sprite-manifest.ts'

Length of output: 289


🏁 Script executed:

#!/bin/bash
# Check for any fetch or import patterns that might load JSON manifests
echo "=== Searching for dynamic imports/fetch patterns ===" 
rg -n 'fetch.*json|import.*json|require.*json' --type=js --type=ts 

echo -e "\n=== Searching for sprite references in general ===" 
rg -n 'sprites.*kitchen.*hero' --type=js --type=ts

Length of output: 533


🏁 Script executed:

#!/bin/bash
# If sprite-manifest.ts exists, examine its content
echo "=== Checking sprite-manifest.ts location ===" 
fd -type f -name '*manifest*' | head -20

Length of output: 215



🏁 Script executed:

#!/bin/bash
# Examine sprite-manifest.ts to understand manifest generation
echo "=== Content of sprite-manifest.ts ===" 
cat frontend/src/lib/sprite-manifest.ts

echo -e "\n=== Searching for any build scripts that might reference layers.json ===" 
find . -type f \( -name '*.json' -o -name '*.js' -o -name '*.ts' \) -path '*/build/*' -o -path '*/scripts/*' | head -10

echo -e "\n=== Check if layers.json is in any build or config files ===" 
rg -n 'layers.json' . 2>/dev/null || echo "No results"

Length of output: 1548


🏁 Script executed:

#!/bin/bash
# Find all manifest files to understand the structure
echo "=== All files with 'manifest' in name ===" 
find . -type f -name '*manifest*' 2>/dev/null | grep -v node_modules | head -20

echo -e "\n=== Check models/manifest.json ===" 
cat frontend/public/models/manifest.json 2>/dev/null | head -30 || echo "File not found or not accessible"

Length of output: 916


Remove unused layers.json—it's dead code.

Verification confirms that frontend/public/sprites/kitchen/hero/layers.json is not referenced anywhere in the codebase. The application generates sprite manifests dynamically via sprite-manifest.ts:manifestFor(), which produces the identical structure programmatically with parameterized values. This static file serves no purpose and can be safely deleted.

🤖 Prompt for AI Agents
frontend/public/sprites/kitchen/hero/layers.json (lines 1-10): this static
layers.json is dead code and duplicates the programmatic manifest produced by
sprite-manifest.ts:manifestFor(); delete the file from the repo (remove it from
git), ensure no imports or references remain (search for any remaining usages)
and, if your build/static asset manifest lists this file, remove it from that
list so it is not packaged or deployed.

Comment on lines 33 to 41
<button
key={option}
type="button"
className="kds-cta"
style={{
background: isActive ? '#fff' : 'var(--kds-pill-bg)',
color: isActive ? '#000' : 'var(--kds-fg)',
}}
className={`rounded-full border px-pzds-2 py-[10px] text-sm font-medium transition-colors ${
isActive
? 'border-pzds-black bg-pzds-black text-pzds-white shadow-[0_18px_36px_rgba(0,0,0,0.18)]'
: 'border-pzds-grey-50 bg-transparent text-pzds-grey-20 hover:border-pzds-grey-30 hover:text-pzds-black'
}`}
onClick={() => onChange(option)}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Expose the active state via ARIA for the option pills.

These buttons act as toggle choices but never announce which one is selected. Screen readers need the state exposed (e.g., aria-pressed={isActive} or converting this block into a proper radiogroup) to let users understand and change the selection. Please add the ARIA state (and, if you go the radiogroup route, the corresponding group semantics) so the control meets WCAG 4.1.2.

Apply this diff, or an equivalent radiogroup refactor, to expose the state:

-            <button
+            <button
               key={option}
               type="button"
               className={`rounded-full border px-pzds-2 py-[10px] text-sm font-medium transition-colors ${
                 isActive
                   ? 'border-pzds-black bg-pzds-black text-pzds-white shadow-[0_18px_36px_rgba(0,0,0,0.18)]'
                   : 'border-pzds-grey-50 bg-transparent text-pzds-grey-20 hover:border-pzds-grey-30 hover:text-pzds-black'
               }`}
+              aria-pressed={isActive}
               onClick={() => onChange(option)}
             >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button
key={option}
type="button"
className="kds-cta"
style={{
background: isActive ? '#fff' : 'var(--kds-pill-bg)',
color: isActive ? '#000' : 'var(--kds-fg)',
}}
className={`rounded-full border px-pzds-2 py-[10px] text-sm font-medium transition-colors ${
isActive
? 'border-pzds-black bg-pzds-black text-pzds-white shadow-[0_18px_36px_rgba(0,0,0,0.18)]'
: 'border-pzds-grey-50 bg-transparent text-pzds-grey-20 hover:border-pzds-grey-30 hover:text-pzds-black'
}`}
onClick={() => onChange(option)}
<button
key={option}
type="button"
className={`rounded-full border px-pzds-2 py-[10px] text-sm font-medium transition-colors ${
isActive
? 'border-pzds-black bg-pzds-black text-pzds-white shadow-[0_18px_36px_rgba(0,0,0,0.18)]'
: 'border-pzds-grey-50 bg-transparent text-pzds-grey-20 hover:border-pzds-grey-30 hover:text-pzds-black'
}`}
aria-pressed={isActive}
onClick={() => onChange(option)}
>
🤖 Prompt for AI Agents
frontend/src/components/ConfiguratorPanel.tsx around lines 33 to 41: the option
buttons do not expose selection state to assistive tech; add ARIA state by
setting aria-pressed={isActive} on each button (or, if you prefer a radiogroup
pattern, wrap the options in an element with role="radiogroup" and give each
option role="radio" with aria-checked={isActive} and a keyboard/space/arrow
handling consistent with radio semantics). Ensure the active visual classes
remain unchanged and update any onKey handlers as needed so the ARIA state
always reflects the current selection.

Comment on lines +13 to +26
const { totalLabel, monthlyLabel } = useMemo(() => {
if (priceCents == null) {
return {
totalLabel: '—',
monthlyLabel: '—',
}
}
const total = priceCents / 100
const monthly = total / FINANCE_TERM_MONTHS
return {
totalLabel: `$${total.toLocaleString(undefined, { minimumFractionDigits: 0 })}`,
monthlyLabel: `$${monthly.toLocaleString(undefined, { minimumFractionDigits: 0 })}/mo`,
}
}, [priceCents])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix incorrect monthly payment calculation.

The monthly payment calculation at line 21 incorrectly divides the total by the number of months without accounting for interest. This produces a significantly lower payment estimate than a proper amortized loan calculation would show.

For a loan with principal P, monthly rate r, and n payments, the correct formula is:

monthly = P × [r(1+r)^n] / [(1+r)^n - 1]

With 3.99% APR (r = 0.0399/12 ≈ 0.003325), the true monthly payment will be noticeably higher than total/72.

Apply this diff to fix the calculation:

  const { totalLabel, monthlyLabel } = useMemo(() => {
    if (priceCents == null) {
      return {
        totalLabel: '—',
        monthlyLabel: '—',
      }
    }
    const total = priceCents / 100
-   const monthly = total / FINANCE_TERM_MONTHS
+   const monthlyRate = APR / 100 / 12
+   const n = FINANCE_TERM_MONTHS
+   const monthly = total * (monthlyRate * Math.pow(1 + monthlyRate, n)) / (Math.pow(1 + monthlyRate, n) - 1)
    return {
      totalLabel: `$${total.toLocaleString(undefined, { minimumFractionDigits: 0 })}`,
      monthlyLabel: `$${monthly.toLocaleString(undefined, { minimumFractionDigits: 0 })}/mo`,
    }
  }, [priceCents])

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
frontend/src/components/PricingFooter.tsx around lines 13 to 26: the current
monthly payment uses total / FINANCE_TERM_MONTHS which ignores interest; replace
it with the amortized loan formula: set principal = priceCents / 100,
monthlyRate = FINANCE_APR / 12 (use the APR constant used elsewhere or add
FINANCE_APR = 0.0399), n = FINANCE_TERM_MONTHS, then compute monthly = principal
* (r * (1 + r) ** n) / ((1 + r) ** n - 1) (handle r === 0 fallback to principal
/ n), format monthlyLabel with toLocaleString and “/mo” as before, and keep the
null price handling unchanged.

Comment on lines +102 to +111
<Image
key={layer.key}
src={layer.url}
alt={layer.key}
fill
priority={index === 0}
className={`${layerBaseClass} ${isLoaded ? 'opacity-100' : 'opacity-0'}`}
style={{ zIndex: layer.z }}
onLoadingComplete={() => handleLayerComplete(layer.key)}
/>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

End the compositor span when a layer fails to load.

If any sprite layer errors, onLoadingComplete never fires, so composeHandleRef.current stays open forever and the compositor.compose span never ends. That breaks telemetry and makes subsequent traces misleading. Please add an error handler that records the failure and closes the span.

One way to address it:

-        {layers.map((layer, index) => {
+        {layers.map((layer, index) => {
           const isLoaded = Boolean(loadedMap[layer.key])
           return (
             <Image
               key={layer.key}
               src={layer.url}
               alt={layer.key}
               fill
               priority={index === 0}
               className={`${layerBaseClass} ${isLoaded ? 'opacity-100' : 'opacity-0'}`}
               style={{ zIndex: layer.z }}
               onLoadingComplete={() => handleLayerComplete(layer.key)}
+              onError={() => {
+                composeHandleRef.current?.end({ error: new Error(`failed to load layer ${layer.key}`) })
+                composeHandleRef.current = null
+              }}
             />
           )
         })}

If you prefer to keep the handler reusable, factor it into a dedicated handleLayerError helper next to handleLayerComplete.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Image
key={layer.key}
src={layer.url}
alt={layer.key}
fill
priority={index === 0}
className={`${layerBaseClass} ${isLoaded ? 'opacity-100' : 'opacity-0'}`}
style={{ zIndex: layer.z }}
onLoadingComplete={() => handleLayerComplete(layer.key)}
/>
<Image
key={layer.key}
src={layer.url}
alt={layer.key}
fill
priority={index === 0}
className={`${layerBaseClass} ${isLoaded ? 'opacity-100' : 'opacity-0'}`}
style={{ zIndex: layer.z }}
onLoadingComplete={() => handleLayerComplete(layer.key)}
onError={() => {
composeHandleRef.current?.end({ error: new Error(`failed to load layer ${layer.key}`) })
composeHandleRef.current = null
}}
/>
🤖 Prompt for AI Agents
In frontend/src/components/Viewer2D.tsx around lines 102 to 111, the Image lacks
an error handler so if a layer fails to load onLoadingComplete never fires and
composeHandleRef.current remains open; add an onError prop that calls a new
helper (e.g. handleLayerError) next to handleLayerComplete which records the
failure (log/telemetry) and ensures composeHandleRef.current is ended/closed and
any per-layer state is updated so the compositor span is always finished even on
load errors.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 3, 2025

Note

Docstrings generation - SUCCESS
Generated docstrings for this pull request at #622

coderabbitai bot added a commit that referenced this pull request Nov 3, 2025
Docstrings generation was requested by @shayancoin.

* #621 (comment)

The following files were modified:

* `frontend/src/app/configurator/ClientPage.tsx`
* `frontend/src/app/configurator/page.tsx`
* `frontend/src/app/layout.tsx`
* `frontend/src/components/OrderSummaryCard.tsx`
* `frontend/src/components/PricingFooter.tsx`
* `frontend/src/components/Viewer2D.tsx`
* `frontend/src/otel/viewer2d-instrumentation.ts`
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
frontend/src/instrumentation.ts (2)

127-132: Consider simplifying the request URL extraction.

The nested ternary for extracting requestUrl is verbose. Consider a more readable approach:

-              const requestUrl =
-                typeof request === 'string'
-                  ? request
-                  : request && typeof (request as { url?: string }).url === 'string'
-                    ? ((request as { url: string }).url)
-                    : '';
+              const requestUrl = typeof request === 'string' 
+                ? request 
+                : (request as { url?: string })?.url ?? '';

183-192: Consider consolidating duplicate initialization logic.

Both register() and initBrowserTracing() perform identical operations by calling registerBrowserInstrumentation(). This duplication could lead to confusion about which function to use.

If both functions are required for different entry points, consider adding documentation clarifying their intended usage contexts. Otherwise, one could delegate to the other or be marked as deprecated:

export async function register(): Promise<void> {
  await initBrowserTracing();
}

/**
 * Initialize browser-side OpenTelemetry tracing.
 * Call this from browser contexts to set up instrumentation.
 */
export async function initBrowserTracing(): Promise<void> {
  if (typeof window === 'undefined') {
    return;
  }
  await registerBrowserInstrumentation();
}
frontend/src/app/kitchens/vvd/view/[file]/page.tsx (1)

1-9: Consider whether 'use client' directive is necessary here.

The file uses dynamic() with ssr: false (line 6) which already ensures client-side-only rendering. The 'use client' directive on line 1 may be redundant in this case, as the dynamic import boundary handles the client/server split.

If the intent is to make this entire page a client component, the 'use client' directive is appropriate. However, if the goal is only to lazy-load the viewer client-side, you could remove the directive and keep just the dynamic import:

-'use client'
-
 import dynamic from 'next/dynamic'

 const ViewerPage = dynamic(() => import('./viewer-client'), {
   ssr: false,
 })

 export default ViewerPage

This would allow the page wrapper to remain a Server Component while only the viewer loads client-side.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d1b7fd6 and 21a4592.

⛔ Files ignored due to path filters (1)
  • frontend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (16)
  • frontend/next-env.d.ts (1 hunks)
  • frontend/next.config.mjs (0 hunks)
  • frontend/package.json (1 hunks)
  • frontend/playwright.config.ts (1 hunks)
  • frontend/src/app/(shop)/product/[handle]/page.tsx (1 hunks)
  • frontend/src/app/components/app-ready.tsx (1 hunks)
  • frontend/src/app/dealer/actions.ts (5 hunks)
  • frontend/src/app/kitchen-configurator/[sku]/page.tsx (1 hunks)
  • frontend/src/app/kitchens/vvd/page.tsx (2 hunks)
  • frontend/src/app/kitchens/vvd/view/[file]/page.tsx (1 hunks)
  • frontend/src/app/kitchens/vvd/view/[file]/viewer-client.tsx (1 hunks)
  • frontend/src/app/partcad/[model]/page.tsx (0 hunks)
  • frontend/src/app/server-actions/dealerQuotes.ts (2 hunks)
  • frontend/src/instrumentation.ts (5 hunks)
  • frontend/src/workers/compositor.worker.ts (1 hunks)
  • frontend/tests/e2e/perf/perf-reporter.ts (2 hunks)
💤 Files with no reviewable changes (2)
  • frontend/next.config.mjs
  • frontend/src/app/partcad/[model]/page.tsx
✅ Files skipped from review due to trivial changes (2)
  • frontend/src/workers/compositor.worker.ts
  • frontend/next-env.d.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/package.json
🧰 Additional context used
🧬 Code graph analysis (3)
frontend/src/app/kitchens/vvd/view/[file]/page.tsx (1)
frontend/src/app/kitchens/vvd/view/[file]/viewer-client.tsx (1)
  • ViewerPage (7-28)
frontend/src/app/kitchens/vvd/view/[file]/viewer-client.tsx (1)
frontend/src/components/Viewer2D.tsx (1)
  • Viewer2D (21-118)
frontend/src/app/kitchen-configurator/[sku]/page.tsx (4)
frontend/src/app/(shop)/product/[handle]/page.tsx (1)
  • dynamic (4-4)
frontend/src/app/kitchens/vvd/page.tsx (1)
  • dynamic (5-5)
frontend/src/app/configurator/quote/complete/page.tsx (1)
  • dynamic (9-9)
frontend/src/app/configurator/ClientPage.tsx (1)
  • ClientPage (19-47)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build-and-test-perf
🔇 Additional comments (14)
frontend/tests/e2e/perf/perf-reporter.ts (1)

3-3: LGTM! Correct type fix for Playwright Reporter API.

The change correctly updates the onBegin signature to use Suite instead of TestCase, aligning with Playwright's Reporter API specification where onBegin receives the root test suite.

Also applies to: 23-23

frontend/src/app/kitchen-configurator/[sku]/page.tsx (1)

1-6: Clean handoff to the new configurator surface.

Importing ClientPage here and marking the route force-dynamic mirrors the other updated configurator pages, so this page will opt out of caching and defer all UI to the new 2D viewer layout cleanly. Nice tidy removal of the old 3D plumbing.

frontend/src/app/server-actions/dealerQuotes.ts (2)

25-27: LGTM! Correct Next.js 15 async cookies() migration.

The function signature and cookies() call have been properly updated to handle the asynchronous API introduced in Next.js 15.


53-53: LGTM! Properly awaits the async function.

The call to createDealerServerClient() correctly awaits the now-asynchronous function.

frontend/src/app/dealer/actions.ts (4)

56-57: LGTM! Correct Next.js 15 async cookies() migration.

The function properly handles the asynchronous cookies() API with the appropriate async signature and await keyword.


106-107: LGTM! Properly propagates async changes.

The function correctly awaits the now-asynchronous extractAccessTokenFromCookie() call.


158-158: LGTM! Call site correctly updated.

The call to requireDealerToken() properly awaits the async function.


200-200: LGTM! All call sites correctly updated.

Both approveDealerQuote and getQuoteDownloadUrl properly await the async requireDealerToken() function.

Also applies to: 238-238

frontend/playwright.config.ts (1)

25-37: LGTM! Cleaner reporter processing logic.

The refactored approach using normalizeReporter makes the reporter processing more consistent and maintainable. The non-null checks ensure only valid normalized reporters are added to the array.

frontend/src/app/components/app-ready.tsx (1)

45-47: LGTM! Improved variable naming.

The renaming from module to instrumentationModule improves code clarity and avoids potential confusion with the reserved module identifier in Node.js contexts.

frontend/src/app/kitchens/vvd/page.tsx (1)

3-3: LGTM! Upgraded to Next.js Image component.

The migration from standard img to Next.js Image component enables automatic image optimization, lazy loading, and better performance. The addition of the required height prop completes the API requirements.

Also applies to: 75-75

frontend/src/instrumentation.ts (1)

82-82: LGTM! Simplified exporter URL resolution.

The refactored resolveExporterUrl is more concise and directly returns the environment variable without unnecessary complexity.

frontend/src/app/kitchens/vvd/view/[file]/viewer-client.tsx (2)

7-28: LGTM! Clean 2D viewer implementation.

The migration from 3D Canvas to the Viewer2D component is well-structured. The file parameter extraction properly handles both single values and arrays, and the conditional footer messaging provides clear user feedback.


18-21: Verify footer messaging aligns with product roadmap.

The footer states "Detailed 3D model ... is not yet available" (line 20), which implies 3D models may be added in the future. Ensure this messaging aligns with the product roadmap and doesn't create misleading expectations if 3D support is not planned.

export const dynamic = 'force-dynamic';

export default async function Page({params}: {params: {handle: string}}) {
export default async function Page({params}: any) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Replace any with proper typing and handle async params.

Using any removes type safety and masks a critical issue: in Next.js 15, params is asynchronous and must be awaited. The current synchronous access on line 11 will fail.

Apply this diff to use proper typing and await params:

-export default async function Page({params}: any) {
+export default async function Page({params}: {params: Promise<{handle: string}>}) {
+  const {handle} = await params;
+
   if (!process.env.NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN || !process.env.NEXT_PUBLIC_SHOPIFY_STOREFRONT_API_TOKEN) {
     return <p>Connect Shopify</p>;
   }
 
-  const data = await shopify.storefront.query(ProductQueryDocument, {variables: {handle: params.handle}});
+  const data = await shopify.storefront.query(ProductQueryDocument, {variables: {handle}});

Alternatively, use Next.js 15.5's PageProps helper type if available in your project.

Based on library documentation.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In frontend/src/app/(shop)/product/[handle]/page.tsx around line 6, the function
parameter uses `any` and accesses `params` synchronously even though Next.js 15
makes `params` async; replace `any` with a proper Page props type (e.g., Next.js
PageProps or an explicit interface with params.handle as string) and await the
params before using them (for example, accept the props object, await
props.params to extract handle) so type safety is restored and the runtime error
is avoided.

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@shayancoin shayancoin merged commit 2a618e9 into main Nov 8, 2025
7 of 12 checks passed
@shayancoin shayancoin deleted the feat/configurator-pzds-pivot branch November 8, 2025 23:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant