- Notifications
You must be signed in to change notification settings - Fork474
Product page updates#985
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
base:dev
Are you sure you want to change the base?
Conversation
vercelbot commentedOct 28, 2025 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
The latest updates on your projects. Learn more aboutVercel for GitHub.
|
coderabbitaibot commentedOct 28, 2025 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
Note Other AI code review bot(s) detectedCodeRabbit 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. WalkthroughThis PR adds new UI components and utilities for dashboard payments products, including interval selection and price editing widgets. It refactors product/catalog and list views with editing workflows, adds a test-mode banner to the payments layout, introduces a URL query-state hook, and extends router and CodeBlock component functionality. Changes
Sequence Diagram(s)sequenceDiagram actor User participant Page as Payments Layout participant Config as Project Config participant Banner as Test Mode Banner User->>Page: Navigate to payments products Page->>Config: Read paymentsConfig.testMode alt Test Mode Enabled Config-->>Page: testMode = true Page->>Banner: Render test-mode banner User->>Banner: Click "Disable Test Mode" Banner->>Config: Update payments.testMode to false Config-->>Page: Config updated Page->>Page: Re-render without banner else Test Mode Disabled Config-->>Page: testMode = false Page->>Page: Show onboarding prompt if not submitted endsequenceDiagram participant Editor as Product Editor participant PriceRow as ProductPriceRow participant Popover as IntervalPopover participant Store as Update Handler Editor->>PriceRow: Render with price data PriceRow->>PriceRow: Initialize state from prop Editor->>PriceRow: Click edit PriceRow->>PriceRow: Enter edit mode PriceRow->>Popover: Render interval selector Editor->>Popover: Select interval preset Popover->>PriceRow: onChange(DayInterval) PriceRow->>PriceRow: Update internal state Editor->>PriceRow: Update amount input PriceRow->>PriceRow: Update amount state Editor->>PriceRow: Click save PriceRow->>Store: onSave(buildPriceUpdate(...)) Store->>Store: Update configEstimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes
Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (2 warnings, 1 inconclusive)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for usingCodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Pull Request Overview
This PR implements comprehensive updates to the product/payments page, including a new custom hook for URL query state management, enhanced router functionality, and a complete redesign of the product editing interface with improved UI components and better separation of concerns.
Key changes:
- Added
useQueryStatehook for persistent view state in URL parameters - Refactored product card editing interface with expanded form layout and better visual hierarchy
- Extracted shared UI components and utility functions into separate modules for better maintainability
Reviewed Changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/stack-shared/src/utils/react.tsx | AddsuseQueryState hook for URL query parameter state management |
| apps/dashboard/src/components/router.tsx | Enhances router methods to accept full parameter spread |
| apps/dashboard/src/components/code-block.tsx | Adds styling props for flexible CodeBlock rendering |
| apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.ts | New utility file with type definitions and helper functions for product/price operations |
| apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-price-row.tsx | New component for price editing with full form controls |
| apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client.tsx | Refactors page layout with query state and unified header |
| apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx | Removes duplicate code and integrates shared components |
| apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx | Major refactor with expanded editing UI and hash-based navigation |
| apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/layout.tsx | Adds test mode banner with disable functionality |
| apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/create-catalog-dialog.tsx | Adds ID sanitization for catalog creation |
| apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/components.tsx | New file with shared UI components (IntervalPopover, OrSeparator, SectionHeading) |
💡Add Copilot custom instructions for smarter, more guided reviews.Learn how to get started.
| exportfunctionuseQueryState(key:string,defaultValue?:string){ | ||
| constgetValue=()=>newURLSearchParams(window.location.search).get(key)??defaultValue??""; | ||
| const[value,setValue]=React.useState(getValue); | ||
| React.useEffect(()=>{ | ||
| constonPopState=()=>setValue(getValue()); | ||
| window.addEventListener("popstate",onPopState); | ||
| return()=>window.removeEventListener("popstate",onPopState); | ||
| },[]); |
CopilotAIOct 28, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
ThegetValue function is defined inside the component but used as a dependency in the effect (line 177). This will cause the effect to recreate the event listener on every render becausegetValue is a new function reference each time. MovegetValue outside the component or wrap it inuseCallback, or inline the logic in theonPopState handler.
| useEffect(()=>{ | ||
| constupdateFromHash=()=>{ | ||
| if(window.location.hash===currentHash)return; | ||
| setCurrentHash(window.location.hash); | ||
| }; | ||
| updateFromHash(); | ||
| constinterval=setInterval(()=>updateFromHash(),10); |
CopilotAIOct 28, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Polling window.location.hash every 10ms is inefficient. Use the 'hashchange' event listener instead:window.addEventListener('hashchange', updateFromHash). This provides the same functionality without continuous polling.
| setDraft(product); | ||
| setLocalProductId(id); | ||
| } | ||
| },[product,id,isEditing]); |
CopilotAIOct 28, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
The effect hasisEditing as a dependency, which means whenisEditing changes totrue, the effect runs and updates the draft, potentially overwriting any edits made before entering edit mode. The conditionif (!isEditing) should prevent this, but the dependency should be removed or the logic restructured to avoid this edge case.
| },[product,id,isEditing]); | |
| },[product,id]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Greptile Overview
Greptile Summary
This PR refactors the payments products page with improved code organization and adds test mode functionality.
Key Changes:
- Extracted reusable UI components (
IntervalPopover,OrSeparator,SectionHeading) intocomponents.tsx - Created
utils.tsfor shared types, constants, and helper functions - Moved
ProductPriceRowcomponent to its own file for better modularity - Added test mode banner and toggle functionality in the UI
- Improved catalog ID input sanitization in
create-catalog-dialog.tsx - Added
useQueryStatehook tostack-sharedfor URL query parameter management - Enhanced
CodeBlockcomponent with new display options
Issues Found:
- Missing error handling on two async button click handlers in
layout.tsx(lines 38-40 and 140) - The removal of
runAsynchronouslyWithAlertbreaks error handling for the "Continue setup" button - The new
handleDisableTestModefunction also lacks error handling
Confidence Score: 4/5
- Safe to merge with one critical error handling issue that needs fixing
- The refactoring is well-executed with clean code organization, but missing error handling on async operations violates project custom rules and could lead to silent failures that confuse users
- Pay close attention to
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/layout.tsx- needs error handling restored
Important Files Changed
File Analysis
| Filename | Score | Overview |
|---|---|---|
| apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/components.tsx | 5/5 | New file extracting reusable UI components (OrSeparator, SectionHeading, IntervalPopover) - clean refactoring with no issues |
| apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.ts | 5/5 | New utility file extracting type definitions, constants, and helper functions - good code organization |
| apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/layout.tsx | 4/5 | Added test mode banner and removed runAsynchronouslyWithAlert - missing error handling on button click |
| apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx | 5/5 | Major refactoring extracting components and utilities to separate files - improved code organization |
Sequence Diagram
sequenceDiagram participant User participant PageClient participant PageClientCatalogsView participant ProductDialog participant IntervalPopover participant Config User->>PageClient: Visit products page PageClient->>PageClient: Check if products/items exist alt No products/items PageClient->>User: Show WelcomeScreen User->>PageClient: Click "Create First Product" PageClient->>PageClient: Set welcomeDismissed=true end PageClient->>PageClientCatalogsView: Render catalogs view User->>PageClient: Toggle test mode switch PageClient->>Config: updateConfig(payments.testMode) Config-->>PageClient: Config updated PageClient->>User: Show toast notification User->>ProductDialog: Create/edit product ProductDialog->>IntervalPopover: Select billing frequency IntervalPopover->>IntervalPopover: User selects interval IntervalPopover->>ProductDialog: Return interval value ProductDialog->>Config: Save product configuration Config-->>ProductDialog: Product saved User->>PageClient: Toggle view (Pricing table ↔ List) PageClient->>PageClient: Update URL query param PageClient->>User: Switch view11 files reviewed, 2 comments
| <Button | ||
| size="lg" | ||
| onClick={()=>runAsynchronouslyWithAlert(setupPayments)} | ||
| onClick={()=>setupPayments()} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
logic: removedrunAsynchronouslyWithAlert which was handling errors - button click now has no error handling
| onClick={()=>setupPayments()} | |
| onClick={()=>runAsynchronouslyWithAlert(setupPayments)} |
Context Used: Rule fromdashboard - UserunAsynchronouslyWithAlert from@stackframe/stack-shared/dist/utils/promises for async butto... (source)
Prompt To Fix With AI
This is a comment left during a code review.Path: apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/layout.tsxLine: 140:140Comment:**logic:** removed`runAsynchronouslyWithAlert` which was handling errors - button click now has no error handling```suggestion onClick={() => runAsynchronouslyWithAlert(setupPayments)}```**Context Used:** Rule from`dashboard` - Use`runAsynchronouslyWithAlert` from`@stackframe/stack-shared/dist/utils/promises` for async butto... ([source](https://app.greptile.com/review/custom-context?memory=5e671275-7493-402a-93a8-969537ec4d63))How can I resolve this? If you propose a fix, please make it concise.
| consthandleDisableTestMode=async()=>{ | ||
| awaitproject.updateConfig({"payments.testMode":false}); | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
logic: missing error handling - ifupdateConfig fails, user won't see an error message
| consthandleDisableTestMode=async()=>{ | |
| awaitproject.updateConfig({"payments.testMode":false}); | |
| }; | |
| consthandleDisableTestMode=async()=>{ | |
| awaitrunAsynchronouslyWithAlert(async()=>{ | |
| awaitproject.updateConfig({"payments.testMode":false}); | |
| }); | |
| }; |
Context Used: Rule fromdashboard - UserunAsynchronouslyWithAlert from@stackframe/stack-shared/dist/utils/promises for async butto... (source)
Prompt To Fix With AI
This is a comment left during a code review.Path: apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/layout.tsxLine: 38:40Comment:**logic:** missing error handling - if`updateConfig` fails, user won't see an error message```suggestion const handleDisableTestMode = async () => { await runAsynchronouslyWithAlert(async () => { await project.updateConfig({ "payments.testMode": false }); }); };```**Context Used:** Rule from`dashboard` - Use`runAsynchronouslyWithAlert` from`@stackframe/stack-shared/dist/utils/promises` for async butto... ([source](https://app.greptile.com/review/custom-context?memory=5e671275-7493-402a-93a8-969537ec4d63))How can I resolve this? If you propose a fix, please make it concise.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx (1)
549-569:Blocking error uses toast; switch to an alert or inline error.“Price ID already exists” prevents save; per dashboard guideline, blocking errors shouldn’t use toast.
- if (Object.prototype.hasOwnProperty.call(nextPrices, newId)) {- toast({ title: "Price ID already exists" });+ if (Object.prototype.hasOwnProperty.call(nextPrices, newId)) {+ alert("Price ID already exists"); return prev; // Do not change state }As per coding guidelines
🧹 Nitpick comments (12)
apps/dashboard/src/components/code-block.tsx (1)
50-50:LGTM with optional readability suggestion.The conditional padding and border logic is correct and handles all prop combinations appropriately.
For improved readability, you could extract the padding logic into a helper function:
constgetHeaderPadding=()=>{if(props.noSeparator)return"pt-1 pb-0";returnprops.compact ?"py-1" :"py-2";};Then use it as:
-<div className={cn("text-muted-foreground font-medium pl-4 pr-2 text-sm flex justify-between items-center", props.compact && !props.noSeparator && "py-1", !props.compact && !props.noSeparator && "py-2", props.noSeparator && "pt-1 pb-0", !props.noSeparator && "border-b")}>+<div className={cn("text-muted-foreground font-medium pl-4 pr-2 text-sm flex justify-between items-center", getHeaderPadding(), !props.noSeparator && "border-b")}>apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/create-catalog-dialog.tsx (1)
74-76:Sanitization vs validation mismatch (underscores).You keep underscores in onChange but validation rejects them (regex only allows a–z, 0–9, hyphen). Align both; prefer the shared sanitizer for consistency.
Apply:
-import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Input, Label, SimpleTooltip, Typography } from "@stackframe/stack-ui";+import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Input, Label, SimpleTooltip, Typography } from "@stackframe/stack-ui";+import { sanitizeId } from "./utils";@@- onChange={(e) => {- const value = e.target.value.toLowerCase().replace(/[^a-z0-9_\-]/g, '-');- setCatalogId(value);- setErrors(prev => ({ ...prev, id: undefined }));- }}+ onChange={(e) => {+ setCatalogId(sanitizeId(e.target.value));+ setErrors(prev => ({ ...prev, id: undefined }));+ }}If underscores are intended, update the validation to
/^[a-z0-9_-]+$/and error text to mention underscores. Based on guidelines.apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/layout.tsx (1)
38-40:Add async state and error handling for test mode + onboarding.Disabling test mode and starting onboarding lack error handling and allow double clicks.
- Disable CTA while awaiting, and catch failures with inline messaging (not toast for blocking flows).
- Example:
-const handleDisableTestMode = async () => {- await project.updateConfig({ "payments.testMode": false });-};+const [disabling, setDisabling] = useState(false);+const handleDisableTestMode = async () => {+ try {+ setDisabling(true);+ await project.updateConfig({ "payments.testMode": false });+ } catch (e) {+ // TODO: surface inline alert/banner here+ } finally {+ setDisabling(false);+ }+};@@- <Button+ <Button size="lg" variant="outline"- onClick={() => handleDisableTestMode()}+ onClick={() => handleDisableTestMode()}+ disabled={disabling} >Similarly wrap setupPayments() with try/catch and disable its buttons during the request. As per coding guidelines.
Also applies to: 83-111, 140-141
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-price-row.tsx (4)
151-163:Avoid saving on every keystroke; debounce amount updates.Current onChange calls savePriceUpdate() on each character, which can thrash parent state or trigger extra work.
Apply a small debounce:
+ const amountSaveTimer = useRef<number | null>(null);@@- onChange={(e) => {+ onChange={(e) => { const v = e.target.value; if (v === '' || /^\d*(?:\.?\d{0,2})?$/.test(v)) setAmount(v);- savePriceUpdate();+ if (amountSaveTimer.current) window.clearTimeout(amountSaveTimer.current);+ amountSaveTimer.current = window.setTimeout(() => {+ savePriceUpdate();+ }, 250); }}
67-69:Trigger label can be stale during edits.intervalText is derived from price.interval; while editing, the trigger shows the old label until parent updates.
Compute from local state:
-const intervalText = intervalLabel(price.interval);+const intervalText = intervalSelection === 'one-time'+ ? null+ : intervalLabel([intervalSelection === 'custom' ? intervalCount : 1, priceInterval ?? 'month'] as DayInterval);Also applies to: 175-190
295-301:Consistent currency formatting in view mode.
$${+amount}may render 9.9 instead of 9.90, diverging from saved normalization.Format using the source price or toFixed:
- <div className="text-xl font-semibold tabular-nums">- {isFree ? 'Free' : `$${niceAmount}`}- </div>+ <div className="text-xl font-semibold tabular-nums">+ {isFree ? 'Free' : `$${(+price.USD || 0).toFixed(2).replace(/\.00$/, '')}`}+ </div>
20-30:Unused prop: existingPriceIds.Prop is not referenced. If not needed, drop it to reduce API surface.
Would you like me to remove it and update call sites?
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx (1)
180-221:Throttle scroll/resize path updates (perf).updatePath runs on every scroll/resize event; add rAF coalescing to avoid jank in large lists.
Apply:
useEffect(() => { if (!fromRef.current || !toRef.current || !containerRef.current) return;-- const updatePath = () => {+ let raf = 0;+ const updatePathNow = () => { const container = containerRef.current; const from = fromRef.current; const to = toRef.current;@@ setPath(pathStr); setMidpoint({ x: midX, y: midY }); };-- updatePath();- window.addEventListener('resize', updatePath);- window.addEventListener('scroll', updatePath, true);+ const schedule = () => {+ if (raf) cancelAnimationFrame(raf);+ raf = requestAnimationFrame(updatePathNow);+ };+ schedule();+ window.addEventListener('resize', schedule);+ window.addEventListener('scroll', schedule, true); return () => {- window.removeEventListener('resize', updatePath);- window.removeEventListener('scroll', updatePath, true);+ if (raf) cancelAnimationFrame(raf);+ window.removeEventListener('resize', schedule);+ window.removeEventListener('scroll', schedule, true); }; }, [fromRef, toRef, containerRef]);Keeps transitions snappy per animation guidance. As per coding guidelines.
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/components.tsx (1)
94-105:Prefer Map over Record for labels (project TS guideline).Use ES6 Map for label lookup and access with .get().
- const buttonLabels: Record<DayInterval[1], string> = useDurationLabels ? {- day: '1 day',- week: '1 week',- month: '1 month',- year: '1 year',- } : {- day: 'daily',- week: 'weekly',- month: 'monthly',- year: 'yearly',- };+ const buttonLabels = new Map<DayInterval[1], string>(+ useDurationLabels+ ? [['day','1 day'],['week','1 week'],['month','1 month'],['year','1 year']]+ : [['day','daily'],['week','weekly'],['month','monthly'],['year','yearly']]+ );@@- {buttonLabels[unitOption]}+ {buttonLabels.get(unitOption)}Also applies to: 173-174
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.ts (2)
39-45:Use Map for unit suffix lookup.Align with project guidance and avoid object key drift.
- const map: Record<DayInterval[1], string> = { day: 'd', week: 'wk', month: 'mo', year: 'yr' };- const suffix = map[unit];+ const map = new Map<DayInterval[1], string>([['day','d'],['week','wk'],['month','mo'],['year','yr']]);+ const suffix = map.get(unit)!;
128-130:Low-entropy IDs; add randomness to reduce collision risk.Two calls in the same ms can collide. Mix time and random.
-export function generateUniqueId(prefix: string): string {- return `${prefix}-${Date.now().toString(36).slice(2, 8)}`;-}+export function generateUniqueId(prefix: string): string {+ const t = Date.now().toString(36);+ const r = Math.random().toString(36).slice(2, 6);+ return `${prefix}-${t.slice(-6)}${r}`;+}apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx (1)
719-723:Animation duration is long; prefer snappy transitions.600ms on color transitions feels sluggish in UI. Reduce to ~150–250ms.
- <div className={cn("flex h-full flex-col rounded-3xl border border-border bg-background/95 shadow-lg transition-colors duration-600",+ <div className={cn("flex h-full flex-col rounded-3xl border border-border bg-background/95 shadow-lg transition-colors duration-200",As per coding guidelines
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (11)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/components.tsx(1 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/create-catalog-dialog.tsx(1 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/layout.tsx(4 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx(30 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx(6 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client.tsx(3 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-price-row.tsx(1 hunks)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.ts(1 hunks)apps/dashboard/src/components/code-block.tsx(4 hunks)apps/dashboard/src/components/router.tsx(1 hunks)packages/stack-shared/src/utils/react.tsx(1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use ES6 Maps instead of Records wherever possible in TypeScript code
Files:
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/create-catalog-dialog.tsxapps/dashboard/src/components/router.tsxpackages/stack-shared/src/utils/react.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsxapps/dashboard/src/components/code-block.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/layout.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/components.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.tsapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-price-row.tsx
apps/{dashboard,dev-launchpad}/**/*.{tsx,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
For blocking alerts and errors in the UI, never use toast; use alerts instead
Files:
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/create-catalog-dialog.tsxapps/dashboard/src/components/router.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsxapps/dashboard/src/components/code-block.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/layout.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/components.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-price-row.tsx
apps/{dashboard,dev-launchpad}/**/*.{css,tsx,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
Animations: keep hover/click transitions snappy; do not delay actions with pre-hover transitions; apply transitions after the action (e.g., fade-out on hover end)
Files:
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/create-catalog-dialog.tsxapps/dashboard/src/components/router.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsxapps/dashboard/src/components/code-block.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/layout.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/components.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-price-row.tsx
🧬 Code graph analysis (7)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx (3)
apps/dashboard/src/components/illustrated-info.tsx (1)
IllustratedInfo(4-31)docs/src/components/icons.tsx (1)
Plus(317-320)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client.tsx (1)
PageClient(72-149)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client.tsx (1)
packages/stack-shared/src/utils/react.tsx (1)
useQueryState(171-191)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/layout.tsx (1)
packages/stack-shared/src/utils/promises.tsx (1)
wait(260-268)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/components.tsx (2)
packages/stack-shared/src/utils/dates.tsx (1)
DayInterval(146-146)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.ts (1)
DEFAULT_INTERVAL_UNITS(16-16)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.ts (2)
packages/stack-shared/src/config/schema.ts (1)
CompleteConfig(1074-1074)packages/stack-shared/src/utils/dates.tsx (1)
DayInterval(146-146)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx (4)
packages/stack-ui/src/components/ui/input.tsx (1)
Input(10-41)apps/dashboard/src/components/link.tsx (1)
Link(21-42)packages/stack-shared/src/utils/react.tsx (1)
useQueryState(171-191)packages/stack-shared/src/utils/arrays.tsx (1)
typedIncludes(3-5)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-price-row.tsx (5)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.ts (5)
intervalLabel(26-34)buildPriceUpdate(64-95)PRICE_INTERVAL_UNITS(17-17)DEFAULT_INTERVAL_UNITS(16-16)freeTrialLabel(50-55)packages/stack-shared/src/utils/dates.tsx (1)
DayInterval(146-146)packages/stack-ui/src/components/ui/input.tsx (1)
Input(10-41)apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/components.tsx (1)
IntervalPopover(77-218)packages/stack-ui/src/components/simple-tooltip.tsx (1)
SimpleTooltip(5-46)
⏰ 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). (12)
- GitHub Check: Vercel Agent Review
- GitHub Check: lint_and_build (latest)
- GitHub Check: restart-dev-and-test-with-custom-base-port
- GitHub Check: setup-tests
- GitHub Check: docker
- GitHub Check: all-good
- GitHub Check: build (22.x)
- GitHub Check: check_prisma_migrations (22.x)
- GitHub Check: build (22.x)
- GitHub Check: restart-dev-and-test
- GitHub Check: build (22.x)
- GitHub Check: Security Check
🔇 Additional comments (5)
apps/dashboard/src/components/router.tsx (1)
20-26:LGTM! Enhanced API flexibility.The updated signatures correctly forward all parameters to the underlying Next.js router methods using spread operators and the
Parametersutility type. This allows callers to pass additional options (e.g.,{ scroll: false }) while preserving the confirmation logic and maintaining backward compatibility.apps/dashboard/src/components/code-block.tsx (4)
4-4:LGTM!The new imports are necessary for the enhanced styling logic and are correctly placed.
Also applies to: 7-7
28-30:LGTM!The new optional props provide clean API surface for controlling visual presentation. The naming is clear and consistent with existing patterns.
49-49:LGTM!The conditional styling logic correctly handles full-width and neutral background variants.
59-59:LGTM!The conditional variant selection ensures proper visual hierarchy and contrast between the button and its background.
| constselectOneTime=()=>{ | ||
| setIntervalSelection('one-time'); | ||
| setUnit(undefined); | ||
| setCount(1); | ||
| if(!readOnly)onChange(null); | ||
| setOpen(false); | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
readOnly still mutates parent state; also allow-open in read-only.
Handlers call setIntervalSelection/setUnit/setCount even when readOnly, and Popover can still open. That breaks read-only semantics and can cause unexpected state changes upstream. Also use a semantic button for the trigger.
Apply:
@@ export function IntervalPopover({ readOnly,@@ }: IntervalPopoverProps) {- const [open, setOpen] = useState(false);+ const [open, setOpen] = useState(false);+ const isInteractive = !readOnly;@@- const selectOneTime = () => {+ const selectOneTime = () => {+ if (!isInteractive) { setOpen(false); return; } setIntervalSelection('one-time'); setUnit(undefined); setCount(1); if (!readOnly) onChange(null); setOpen(false); };@@- const selectFixed = (unitOption: DayInterval[1]) => {+ const selectFixed = (unitOption: DayInterval[1]) => {+ if (!isInteractive) { setOpen(false); return; } if (!normalizedUnits.includes(unitOption)) return; setIntervalSelection(unitOption); setUnit(unitOption); setCount(1); if (!readOnly) onChange([1, unitOption]); setOpen(false); };@@- const applyCustom = (countValue: number, maybeUnit?: DayInterval[1]) => {+ const applyCustom = (countValue: number, maybeUnit?: DayInterval[1]) => {+ if (!isInteractive) return; const safeUnit = maybeUnit && normalizedUnits.includes(maybeUnit) ? maybeUnit : defaultUnit; setIntervalSelection('custom'); setUnit(safeUnit); setCount(countValue); if (!readOnly) onChange([countValue, safeUnit]); };@@- return (- <Popover open={open} onOpenChange={setOpen}>- <PopoverTrigger>- <div className={cn(triggerClasses, readOnly && "cursor-default")}>+ return (+ <Popover open={isInteractive ? open : false} onOpenChange={(v) => isInteractive && setOpen(v)}>+ <PopoverTrigger asChild>+ <button+ type="button"+ className={cn(triggerClasses, readOnly && "cursor-default")}+ aria-haspopup="menu"+ aria-expanded={isInteractive ? open : false}+ disabled={!isInteractive}+ > {triggerLabel} <ChevronsUpDown className="h-4 w-4" />- </div>+ </button> </PopoverTrigger>Also applies to: 124-131, 133-139, 145-151
| useEffect(()=>{ | ||
| constupdateFromHash=()=>{ | ||
| if(window.location.hash===currentHash)return; | ||
| setCurrentHash(window.location.hash); | ||
| }; | ||
| updateFromHash(); | ||
| constinterval=setInterval(()=>updateFromHash(),10); | ||
| constremoveHashTarget=()=>{ | ||
| if(isHashTarget&&window.location.hash===hashAnchor){ | ||
| window.history.replaceState(null,"",window.location.pathname+window.location.search); | ||
| } | ||
| }; | ||
| window.addEventListener("click",removeHashTarget,{capture:true}); | ||
| return()=>{ | ||
| clearInterval(interval); | ||
| window.removeEventListener("click",removeHashTarget,{capture:true}); | ||
| }; | ||
| },[currentHash,hashAnchor,isHashTarget]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
10ms polling loop for hash updates is expensive; use hashchange.
Replace setInterval(10ms) with a hashchange listener.
useEffect(() => {- const updateFromHash = () => {- if (window.location.hash === currentHash) return;- setCurrentHash(window.location.hash);- };- updateFromHash();- const interval = setInterval(() => updateFromHash(), 10);+ const updateFromHash = () => {+ const h = window.location.hash;+ if (h !== currentHash) setCurrentHash(h);+ };+ updateFromHash();+ window.addEventListener('hashchange', updateFromHash);@@- return () => {- clearInterval(interval);- window.removeEventListener("click", removeHashTarget, { capture: true });- };- }, [currentHash, hashAnchor, isHashTarget]);+ return () => {+ window.removeEventListener('hashchange', updateFromHash);+ window.removeEventListener("click", removeHashTarget, { capture: true });+ };+ }, [hashAnchor, isHashTarget, currentHash]);📝 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.
| useEffect(()=>{ | |
| constupdateFromHash=()=>{ | |
| if(window.location.hash===currentHash)return; | |
| setCurrentHash(window.location.hash); | |
| }; | |
| updateFromHash(); | |
| constinterval=setInterval(()=>updateFromHash(),10); | |
| constremoveHashTarget=()=>{ | |
| if(isHashTarget&&window.location.hash===hashAnchor){ | |
| window.history.replaceState(null,"",window.location.pathname+window.location.search); | |
| } | |
| }; | |
| window.addEventListener("click",removeHashTarget,{capture:true}); | |
| return()=>{ | |
| clearInterval(interval); | |
| window.removeEventListener("click",removeHashTarget,{capture:true}); | |
| }; | |
| },[currentHash,hashAnchor,isHashTarget]); | |
| useEffect(()=>{ | |
| constupdateFromHash=()=>{ | |
| consth=window.location.hash; | |
| if(h!==currentHash)setCurrentHash(h); | |
| }; | |
| updateFromHash(); | |
| window.addEventListener('hashchange',updateFromHash); | |
| constremoveHashTarget=()=>{ | |
| if(isHashTarget&&window.location.hash===hashAnchor){ | |
| window.history.replaceState(null,"",window.location.pathname+window.location.search); | |
| } | |
| }; | |
| window.addEventListener("click",removeHashTarget,{capture:true}); | |
| return()=>{ | |
| window.removeEventListener('hashchange',updateFromHash); | |
| window.removeEventListener("click",removeHashTarget,{capture:true}); | |
| }; | |
| },[hashAnchor,isHashTarget,currentHash]); |
🤖 Prompt for AI Agents
Inapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsxaround lines 456 to 475, there is a tight 10ms setInterval polling thewindow.location.hash; replace it with a window.addEventListener('hashchange',...) handler that updates currentHash (and still call it once initially tocapture current state), remove the interval creation/clearInterval logic, keepthe existing removeHashTarget click listener and cleanup by removing both eventlisteners in the effect return, and ensure the effect dependencies still includecurrentHash, hashAnchor, and isHashTarget.| consthandleSaveEdit=async()=>{ | ||
| consttrimmed=localProductId.trim(); | ||
| constvalidId=trimmed&&/^[a-z0-9-]+$/.test(trimmed) ?trimmed :id; | ||
| if(validId!==id){ | ||
| awaitonSave(validId,draft); | ||
| awaitonDelete(id); | ||
| }else{ | ||
| awaitonSave(id,draft); | ||
| } | ||
| setIsEditing(false); | ||
| setEditingPriceId(undefined); | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
🛠️ Refactor suggestion |🟠 Major
Unify ID validation/sanitization with utils.
Use sanitizeId/isValidId from utils to avoid regex drift and the “_” inconsistency.
@@- const trimmed = localProductId.trim();- const validId = trimmed && /^[a-z0-9-]+$/.test(trimmed) ? trimmed : id;+ const trimmed = localProductId.trim();+ const validId = trimmed && isValidId(trimmed) ? trimmed : id;@@- <Input+ <Input className="h-10 rounded-xl border border-border bg-background px-3 text-sm" value={localProductId} onChange={(event) => {- const value = event.target.value.toLowerCase().replace(/[^a-z0-9_\-]/g, '-');- setLocalProductId(value);+ setLocalProductId(sanitizeId(event.target.value)); }}
@@- const value = e.target.value.toLowerCase().replace(/[^a-z0-9_\-]/g, '-');- setNewCatalogId(value);+ setNewCatalogId(sanitizeId(e.target.value));
@@- if (!/^[a-z0-9-]+$/.test(id)) {+ if (!isValidId(id)) { alert("Catalog ID must be lowercase letters, numbers, and hyphens"); return; }
And import:
-import {+import { generateUniqueId, intervalLabel, shortIntervalLabel,+ sanitizeId,+ isValidId, type Price, type PricesObject, type Product } from "./utils";
Also applies to: 748-759, 1189-1191, 1254-1255
| consthandleToggleTestMode=async(enabled:boolean)=>{ | ||
| awaitproject.updateConfig({"payments.testMode":enabled}); | ||
| toast({title:enabled ?"Test mode enabled" :"Test mode disabled"}); | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
🧩 Analysis chain
Handle failure when toggling test mode.
If updateConfig fails, the Switch stays toggled and no feedback is shown.
Apply:
-const handleToggleTestMode = async (enabled: boolean) => {- await project.updateConfig({ "payments.testMode": enabled });- toast({ title: enabled ? "Test mode enabled" : "Test mode disabled" });-};+const handleToggleTestMode = async (enabled: boolean) => {+ try {+ await project.updateConfig({ "payments.testMode": enabled });+ toast({ title: enabled ? "Test mode enabled" : "Test mode disabled" });+ } catch (e) {+ // Optionally surface an inline error and revert+ await project.updateConfig({ "payments.testMode": !enabled });+ }+};
Also verify no other places rely on stale paymentsConfig after toggling.
Also applies to: 128-134
🏁 Script executed:
#!/bin/bashrg -n --hidden -C2'payments\.testMode' --type=ts --type=tsx
Length of output: 27
---🏁 Script executed:```shell#!/bin/bash# Search for payments.testMode without problematic type filterrg -n --hidden -C2 'payments\.testMode'Length of output: 2643
🏁 Script executed:
#!/bin/bash# Find and examine the specific filefd'page-client.tsx' apps/dashboard
Length of output: 3415
🏁 Script executed:
#!/bin/bash# Look for paymentsConfig references in dashboard payments contextrg -n'paymentsConfig' --hidden
Length of output: 7136
---🏁 Script executed:```shell#!/bin/bash# View page-client.tsx around lines 100-140 to see both handleToggleTestMode and the second locationcat apps/dashboard/src/app/\(main\)/\(protected\)/projects/\[projectId\]/payments/products/page-client.tsx | sed -n '95,140p' | cat -nLength of output: 1785
🏁 Script executed:
#!/bin/bash# View layout.tsx around line 38-42 to see handleDisableTestModecat apps/dashboard/src/app/\(main\)/\(protected\)/projects/\[projectId\]/payments/products/layout.tsx| sed -n'35,45p'| cat -n
Length of output: 361
🏁 Script executed:
#!/bin/bash# Check if useConfig refetches data or is a static hookrg -n'useConfig' apps/dashboard/src/app/\(main\)/\(protected\)/projects/\[projectId\]/payments/products/page-client.tsx -B2 -A2
Length of output: 193
Add error handling to test mode toggle and surface errors as alerts per design guidelines.
The Switch component is controlled bypaymentsConfig.testMode. IfupdateConfig fails, the Switch remains toggled while the backend is unchanged, creating UI/backend divergence with no user feedback. Additionally, blocking errors must use alerts, not toast (per design guidelines).
Apply error handling to both locations:
-const handleToggleTestMode = async (enabled: boolean) => {- await project.updateConfig({ "payments.testMode": enabled });- toast({ title: enabled ? "Test mode enabled" : "Test mode disabled" });-};+const handleToggleTestMode = async (enabled: boolean) => {+ try {+ await project.updateConfig({ "payments.testMode": enabled });+ toast({ title: enabled ? "Test mode enabled" : "Test mode disabled" });+ } catch (e) {+ alert("Failed to update test mode. Please try again.");+ }+};
Also inlayout.tsx line 38–40:
-const handleDisableTestMode = async () => {- await project.updateConfig({ "payments.testMode": false });-};+const handleDisableTestMode = async () => {+ try {+ await project.updateConfig({ "payments.testMode": false });+ } catch (e) {+ alert("Failed to disable test mode. Please try again.");+ }+};
📝 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.
| consthandleToggleTestMode=async(enabled:boolean)=>{ | |
| awaitproject.updateConfig({"payments.testMode":enabled}); | |
| toast({title:enabled ?"Test mode enabled" :"Test mode disabled"}); | |
| }; | |
| consthandleToggleTestMode=async(enabled:boolean)=>{ | |
| try{ | |
| awaitproject.updateConfig({"payments.testMode":enabled}); | |
| toast({title:enabled ?"Test mode enabled" :"Test mode disabled"}); | |
| }catch(e){ | |
| alert("Failed to update test mode. Please try again."); | |
| } | |
| }; |
🤖 Prompt for AI Agents
Inapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client.tsxaround lines 105 to 109 and inapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/layout.tsxaround lines 38 to 40, add proper error handling around calls toproject.updateConfig: wrap the async update call in try/catch, revert the localSwitch state if the update fails so UI and backend stay in sync, and surfacefailures using the alert mechanism (per design guidelines) instead of toast;ensure the catch extracts and displays the error message in the alert and thatoptimistic state changes are only applied after a successful update (or rolledback on error).| constID_PATTERN=/^[a-z0-9-]+$/; | ||
| /** | ||
| * Validates if an ID matches the required pattern | ||
| */ | ||
| exportfunctionisValidId(id:string):boolean{ | ||
| returnID_PATTERN.test(id); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Inconsistent ID rules: sanitizeId allows “_” but isValidId forbids it.
This will accept underscores in UI then reject on save. Unify to a single policy.
-const ID_PATTERN = /^[a-z0-9-]+$/;+const ID_PATTERN = /^[a-z0-9-]+$/; // single source of truth@@-export function sanitizeId(input: string): string {- return input.toLowerCase().replace(/[^a-z0-9_\-]/g, '-');-}+export function sanitizeId(input: string): string {+ // keep only chars allowed by ID_PATTERN+ return input.toLowerCase().replace(/[^a-z0-9-]/g, '-');+}
Also applies to: 135-137
🤖 Prompt for AI Agents
Inapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.tsaround lines 116-123 (and also update lines 135-137), the ID validation regexforbids underscores while sanitizeId currently allows them, causing UIacceptance but server-side rejection; pick one canonical policy and make bothfunctions consistent: either widen ID_PATTERN to allow underscores (e.g.,include "_" in the character class) or change sanitizeId to strip/replaceunderscores to match the existing pattern, then update any related uses/tests toreflect the chosen rule so IDs are validated/sanitized consistently across thecodebase.| exportfunctionuseQueryState(key:string,defaultValue?:string){ | ||
| constgetValue=()=>newURLSearchParams(window.location.search).get(key)??defaultValue??""; | ||
| const[value,setValue]=React.useState(getValue); | ||
| React.useEffect(()=>{ | ||
| constonPopState=()=>setValue(getValue()); | ||
| window.addEventListener("popstate",onPopState); | ||
| return()=>window.removeEventListener("popstate",onPopState); | ||
| },[]); | ||
| constupdate=(next:string)=>{ | ||
| constparams=newURLSearchParams(window.location.search); | ||
| params.set(key,next); | ||
| constnewUrl=`${window.location.pathname}?${params.toString()}`; | ||
| window.history.pushState(null,"",newUrl); | ||
| setValue(next); | ||
| }; | ||
| return[value,update]asconst; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
SSR-safety and history hygiene for useQueryState.
Initializer/effect read window unguarded (breaks on SSR). Also always pushState and never remove default/empty values.
Apply:
-export function useQueryState(key: string, defaultValue?: string) {- const getValue = () => new URLSearchParams(window.location.search).get(key) ?? defaultValue ?? "";-- const [value, setValue] = React.useState(getValue);-- React.useEffect(() => {- const onPopState = () => setValue(getValue());- window.addEventListener("popstate", onPopState);- return () => window.removeEventListener("popstate", onPopState);- }, []);-- const update = (next: string) => {- const params = new URLSearchParams(window.location.search);- params.set(key, next);- const newUrl = `${window.location.pathname}?${params.toString()}`;- window.history.pushState(null, "", newUrl);- setValue(next);- };-- return [value, update] as const;-}+export function useQueryState(key: string, defaultValue?: string) {+ const getValue = React.useCallback(() => {+ if (!isBrowserLike()) return defaultValue ?? "";+ const params = new URLSearchParams(window.location.search);+ return params.get(key) ?? defaultValue ?? "";+ }, [key, defaultValue]);++ const [value, setValue] = React.useState<string>(getValue);++ React.useEffect(() => {+ if (!isBrowserLike()) return;+ const onPopState = () => setValue(getValue());+ window.addEventListener("popstate", onPopState);+ return () => window.removeEventListener("popstate", onPopState);+ }, [getValue]);++ const update = React.useCallback((next: string) => {+ if (!isBrowserLike()) {+ setValue(next);+ return;+ }+ if (next === value) return; // avoid redundant entries+ const params = new URLSearchParams(window.location.search);+ const isDefault = next === "" || next === (defaultValue ?? "");+ if (isDefault) {+ params.delete(key);+ } else {+ params.set(key, next);+ }+ const qs = params.toString();+ const newUrl = qs ? `${window.location.pathname}?${qs}` : window.location.pathname;+ (isDefault ? window.history.replaceState : window.history.pushState).call(window.history, null, "", newUrl);+ setValue(next);+ }, [key, value, defaultValue]);++ return [value, update] as const;+}
Committable suggestion skipped: line range outside the PR's diff.
| <> | ||
| {/* Mobile tabs */} | ||
| <divclassName="lg:hidden mb-4"> | ||
| <divclassName="lg:hidden mb-4"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Multiple JSX syntax errors with spaces in opening/closing tags. The code has< div,</div >,</Card > with spaces before or after the angle brackets, which is invalid JSX syntax and will cause compilation failures.
View Details
📝 Patch Details
diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsxindex 2d77184e..2a85dc88 100644--- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx+++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx@@ -764,7 +764,7 @@ export default function PageClient() { const innerContent = ( <> {/* Mobile tabs */}- < div className="lg:hidden mb-4" >+ <div className="lg:hidden mb-4"> <div className="flex space-x-1 bg-muted p-1 rounded-md"> <button onClick={() => setActiveTab("products")}@@ -789,7 +789,7 @@ export default function PageClient() { Items </button> </div>- </div >+ </div> {/* Content */} <div className="flex gap-6 flex-1" style={{@@ -798,7 +798,7 @@ export default function PageClient() { } }> {/* Desktop two-column layout */}- <Card className="hidden lg:flex w-full relative" ref={containerRef} >+ <Card className="hidden lg:flex w-full relative" ref={containerRef}> <CardContent className="flex w-full"> <div className="flex-1"> <ProductsList@@ -856,10 +856,10 @@ export default function PageClient() { /> )) }- </Card >+ </Card> {/* Mobile single column with tabs */}- < div className="lg:hidden w-full" >+ <div className="lg:hidden w-full"> {activeTab === "products" ? ( <ProductsList groupedProducts={groupedProducts}@@ -884,8 +884,8 @@ export default function PageClient() { setShowItemDialog={setShowItemDialog} /> )}- </div >- </div >+ </div>+ </div> </> );
Analysis
Invalid JSX syntax with spaces in angle brackets prevents compilation
What fails: JSX parser in page-client-list-view.tsx cannot parse tags with spaces around angle brackets like< div>,</div >,</Card > at lines 767, 792, 801, 859, 862, and 887-888
How to reproduce:
cd apps/dashboard&& pnpmexec tsc --jsx react --noEmit src/app/\(main\)/\(protected\)/projects/\[projectId\]/payments/products/page-client-list-view.tsx
Result: TypeScript JSX parser throws syntax errors for malformed tags. JSX requires no spaces between angle brackets and tag names.
Expected: JSX tags should be<div>,</div>,<Card> without spaces perJSX specification - spaces between< and tag names violate JSX parsing rules.
| value={catalogId} | ||
| onChange={(e)=>{ | ||
| setCatalogId(e.target.value); | ||
| constvalue=e.target.value.toLowerCase().replace(/[^a-z0-9_\-]/g,'-'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
The input sanitization regex allows underscores but the validation regex doesn't, causing user input with underscores to pass sanitization but fail validation.
View Details
📝 Patch Details
diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/create-catalog-dialog.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/create-catalog-dialog.tsxindex 64b9f934..f5b91f1b 100644--- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/create-catalog-dialog.tsx+++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/create-catalog-dialog.tsx@@ -71,7 +71,7 @@ export function CreateCatalogDialog({ open, onOpenChange, onCreate }: CreateCata value={catalogId} onChange={(e) => {- const value = e.target.value.toLowerCase().replace(/[^a-z0-9_\-]/g, '-');+ const value = e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '-'); setCatalogId(value); setErrors(prev => ({ ...prev, id: undefined })); }}@@ -121,4 +121,3 @@ export function CreateCatalogDialog({ open, onOpenChange, onCreate }: CreateCata </Dialog> ); }-diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsxindex b3aa62b9..fc86dc3f 100644--- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx+++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx@@ -750,7 +750,7 @@ function ProductCard({ id, activeType, product, allProducts, existingItems, onSa className="h-10 rounded-xl border border-border bg-background px-3 text-sm" value={localProductId} onChange={(event) => {- const value = event.target.value.toLowerCase().replace(/[^a-z0-9_\-]/g, '-');+ const value = event.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '-'); setLocalProductId(value); }} placeholder="offer-id"@@ -1186,7 +1186,7 @@ function CatalogView({ groupedProducts, groups, existingItems, onSaveProduct, on ref={newGroupInputRef} value={newCatalogId} onChange={(e) => {- const value = e.target.value.toLowerCase().replace(/[^a-z0-9_\-]/g, '-');+ const value = e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '-'); setNewCatalogId(value); }} placeholder="catalog-id"diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.ts b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.tsindex cccdd714..4f948485 100644--- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.ts+++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.ts@@ -133,5 +133,5 @@ export function generateUniqueId(prefix: string): string { * Sanitizes user input into a valid ID format (lowercase, hyphenated) */ export function sanitizeId(input: string): string {- return input.toLowerCase().replace(/[^a-z0-9_\-]/g, '-');+ return input.toLowerCase().replace(/[^a-z0-9-]/g, '-'); }
Analysis
Regex inconsistency in catalog ID sanitization vs validation causes UX confusion
What fails: CreateCatalogDialog and related components use inconsistent regex patterns - sanitization preserves underscores but validation rejects them
How to reproduce:
- Open create catalog dialog in dashboard
- Type "product_one" in Catalog ID field
- Observe input shows "product_one" (sanitization preserves underscore)
- Click "Create Catalog"
- Get validation error: "Catalog ID must contain only lowercase letters, numbers, and hyphens"
Result: User sees "product_one" in input field but validation fails because sanitization regex/[^a-z0-9_\-]/g preserves underscores while validation regex/^[a-z0-9-]+$/ rejects them
Expected: Consistent handling - sanitization should convert underscores to hyphens so user sees exactly what will pass validation
Files affected:
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/create-catalog-dialog.tsx:74apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx:753,1189apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/utils.ts:136
| allowedUnits={PRICE_INTERVAL_UNITS} | ||
| triggerClassName="flex h-10 w-full items-center justify-between rounded-xl border border-border bg-background px-3 text-sm font-medium capitalize text-foreground shadow-sm" | ||
| onChange={(interval)=>{ | ||
| savePriceUpdate(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
| savePriceUpdate(); | |
| savePriceUpdate({ | |
| intervalSelection:interval ?(interval[0]===1 ?interval[1] :'custom') :'one-time', | |
| intervalCount:interval ?interval[0] :1, | |
| priceInterval:interval ?interval[1] :undefined, | |
| }); |
TheonChange callback for the billing frequency IntervalPopover is not using the interval value passed to it, causing incorrect state to be saved when the user changes the billing frequency. The callback reads stale state values instead of using the provided parameter.
View Details
Analysis
IntervalPopover onChange callback ignores interval parameter causing incorrect billing frequency to be saved
What fails: ProductPriceRow IntervalPopover onChange callback inapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-price-row.tsx:186-188 ignores theinterval parameter and callssavePriceUpdate() with no arguments, causing stale state values to be saved due to React 19 automatic batching.
How to reproduce:
- Navigate to a project's payments/products page
- Edit a paid price row
- Change billing frequency from "monthly" to "weekly" using the IntervalPopover dropdown
- Check network requests - the saved interval will be
[1, "month"]instead of[1, "week"]
Result: UI shows the new selection (e.g., "weekly") but backend receives old state values (e.g.,[1, "month"]) due to React batching setState calls. ThesavePriceUpdate() function readsintervalSelection,intervalCount, andpriceInterval from closure before setState updates are applied.
Expected: Should save[1, "week"] when user selects weekly billing frequency. TheonChange callback receives the correct interval parameter[1, "week"] which should be used instead of reading stale state.
Uh oh!
There was an error while loading.Please reload this page.
Screen.Recording.2025-10-28.at.09.53.23.mov