Skip to content

Conversation

@jshwrnr
Copy link
Contributor

@jshwrnr jshwrnr commented Nov 24, 2025

Replace Tremor charts with NewBarChart, restructure stats pages with MainStatChart, BarListCard, and preset DatePickerWithRange, and update Tailwind to remove Tremor tokens to support Common Topics work

Switch charting to NewBarChart across analytics, introduce MainStatChart and BarListCard, refactor ActionBar to layout-only, add preset-driven DatePickerWithRange, remove Tremor dependencies and tokens, and add favicon support via t1.gstatic.com. Key entry points: [file:apps/web/app/(app)/[emailAccountId]/stats/Stats.tsx], [file:apps/web/app/(app)/[emailAccountId]/stats/MainStatChart.tsx], [file:apps/web/app/(app)/[emailAccountId]/stats/NewBarChart.tsx], and [file:apps/web/tailwind.config.js].

📍Where to Start

Start with Stats page composition in Stats.tsx, then review chart foundations in NewBarChart ([file:apps/web/app/(app)/[emailAccountId]/stats/NewBarChart.tsx]) and summary logic in MainStatChart ([file:apps/web/app/(app)/[emailAccountId]/stats/MainStatChart.tsx]); validate theme changes in [file:apps/web/tailwind.config.js].


📊 Macroscope summarized 480449f. 46 files reviewed, 41 issues evaluated, 30 issues filtered, 4 comments posted

🗂️ Filtered Issues

apps/web/app/(app)/[emailAccountId]/bulk-unsubscribe/BulkUnsubscribeSection.tsx — 0 comments posted, 3 evaluated, 2 filtered
  • line 171: readPercentage and archivedPercentage are computed by dividing by item.value without guarding against 0. If item.value is 0, both (item.readEmails / item.value) * 100 and ((item.value - item.inboxEmails) / item.value) * 100 will yield Infinity or NaN, which then propagates to UI components (e.g., progress bars and percentage text), causing broken rendering and potentially invalid value props. Add a zero-check and fall back to 0 or skip rendering when item.value === 0. [ Out of scope ]
  • line 201: When sortColumn === "emails", the sorting iteratee returns undefined, so sortBy doesn’t sort by the intended emails count. This yields an unsorted or implementation-defined order rather than sorting by item.value. Use item.value (the emails count) as the iteratee for the "emails" case. [ Out of scope ]
apps/web/app/(app)/[emailAccountId]/stats/DetailedStatsFilter.tsx — 0 comments posted, 1 evaluated, 1 filtered
  • line 36: DetailedStatsFilter sets DropdownMenu to a controlled open state when keepOpenOnSelect is true, but its onOpenChange handler ignores the open argument and only ever sets isOpen to true if it was previously false. This prevents normal close actions (e.g., pressing Escape or clicking the trigger again) from closing the menu; the menu can remain stuck open unless onInteractOutside fires. Use the open parameter to set state (setIsOpen(open)) or otherwise allow close events when appropriate. [ Low confidence ]
apps/web/app/(app)/[emailAccountId]/stats/MainStatChart.tsx — 0 comments posted, 1 evaluated, 1 filtered
  • line 38: format(date, "yyyy-MM-dd") is called on date parsed from item.startOfPeriod without validating the parse result. If item.startOfPeriod does not match "MMM dd, yyyy", parse(...) yields an Invalid Date and date-fns format(...) will throw a RangeError: Invalid time value. Add validation (e.g., isValid(date)) or a fallback before formatting. [ Low confidence ]
apps/web/app/(app)/[emailAccountId]/stats/NewBarChart.tsx — 0 comments posted, 2 evaluated, 2 filtered
  • line 50: Date formatting in NewBarChart assumes xAxisKey values are valid dates. new Date(value) followed by toLocaleDateString(...) will throw RangeError: Invalid time value if value is missing or unparsable. This occurs in the default tick formatter and in the tooltip (new Date(data.payload[xAxisKey])). Add validation and fallback formatting, or guard against missing/invalid xAxisKey values. [ Low confidence ]
  • line 74: config[key].color is accessed without existence checks while iterating keys derived from dataKeys || Object.keys(config). If dataKeys includes a key not present in config, accessing config[key].color will throw at runtime. Guard with config[key] ? config[key].color : fallback, validate dataKeys against config, or derive keys solely from config. Occurs in gradient stopColor and in <Bar color={...}>. [ Low confidence ]
apps/web/app/(app)/[emailAccountId]/stats/NewsletterModal.tsx — 0 comments posted, 3 evaluated, 2 filtered
  • line 59: Interactive element nesting: a Button (renders as a native <button> when asChild is false) contains an <a> anchor inside it (lines 59-67). Nesting interactive controls is invalid HTML and can cause broken keyboard/focus behavior and inconsistent click handling. Use Button with asChild and render the anchor as the root (e.g., <Button asChild><Link ... /></Button>) or make the anchor the primary element without wrapping it in a button. [ Out of scope ]
  • line 82: Provider-specific link mismatch: When newsletter.autoArchived is truthy, the UI always shows a View Skip Inbox Filter link built with getGmailFilterSettingsUrl(userEmail) (lines 82-91). For non-Gmail providers (e.g., Outlook), this link is incorrect and misleading. Gate this by provider (show Gmail link only for Google accounts) or render the appropriate settings destination per provider. [ Out of scope ]
apps/web/app/api/user/stats/by-period/route.ts — 0 comments posted, 1 evaluated, 1 filtered
  • line 25: getEmailStatsByPeriod uses truthy checks for fromDate and toDate (if (fromDate), if (toDate)), which will skip valid timestamp values of 0 (Unix epoch). Since statsByWeekParams allows any number via z.coerce.number().nullish(), 0 is an explicitly valid input and should be included. Use explicit null/undefined checks (e.g., if (fromDate != null)) to avoid dropping epoch filters. [ Out of scope ]
apps/web/components/DatePickerWithRange.tsx — 1 comment posted, 3 evaluated, 2 filtered
  • line 99: Selecting the "All" option (commonly represented with a value of "0") now sets dateRange to { from: subDays(now, 0), to: now } (i.e., a single day) instead of clearing the range. Previously, "All" cleared the date range (undefined). This breaks contract parity and produces inconsistent semantics: the label shows "All" while the selection is actually a one-day range. [ Low confidence ]
  • line 100: Selecting a non-numeric or malformed value in selectOptions will pass NaN to Number.parseInt(value), resulting in subDays(now, NaN) which yields an invalid Date. That invalid Date is set into dateRange ({ from: Invalid Date, to: now }), and then passed to <Calendar selected={dateRange}>, risking runtime errors or inconsistent rendering in the date picker. [ Low confidence ]
apps/web/components/PageHeader.tsx — 0 comments posted, 1 evaluated, 1 filtered
  • line 28: The conditional video && (video.youtubeVideoId || video.muxPlaybackId) relies on JavaScript truthiness to decide whether to render <WatchVideo />. This will render the button when either youtubeVideoId or muxPlaybackId is a non-empty string, including whitespace-only values (e.g., " "), which are truthy but invalid IDs. That can cause downstream runtime errors or a broken player when <OnboardingDialogContent> receives an invalid ID. Use explicit validation (e.g., trim and non-empty checks) before rendering: const hasValidId = !!(video.youtubeVideoId?.trim() || video.muxPlaybackId?.trim()); and guard on hasValidId. [ Low confidence ]
apps/web/components/ProgressPanel.tsx — 0 comments posted, 2 evaluated, 2 filtered
  • line 22: progress is not clamped to the [0, 100] range. If remainingItems is greater than totalItems, totalProcessed becomes negative and progress becomes negative; if remainingItems is negative or totalItems is smaller than totalProcessed, progress can exceed 100. This yields incorrect UI and out-of-bounds indicator motion when passed to <Progress value={progress} />. Clamp progress to [0, 100] before rendering. [ Low confidence ]
  • line 50: Invalid HTML structure: a block-level <div> is rendered inside an inline <span> (<span> ... <div className="flex items-center gap-1"> ... </div> ... </span>). This produces malformed markup and may lead to inconsistent layout/semantics across browsers and screen readers. Replace the outer <span> with a <div> or change the inner container to a <span>. [ Out of scope ]
apps/web/components/TabSelect.tsx — 0 comments posted, 4 evaluated, 4 filtered
  • line 65: When href is provided, As is Link which renders an <a> element, and a <button> is rendered inside it. Nesting interactive elements (<button> inside <a>) violates accessibility and can cause unexpected click/keyboard behavior. Consider making the clickable element a single <a> styled as a button, or handling selection/navigation on one element only. [ Out of scope ]
  • line 68: When href is falsy, As becomes a div, but href and target props are still applied (href={href ?? "#"} and target={target ?? undefined}), resulting in invalid DOM attributes on a <div>. This can produce console warnings and malformed semantics (a non-interactive element with link attributes). Guard these props so they are only passed when As is Link, or conditionally render attributes based on href presence. [ Out of scope ]
  • line 69: Opening links in a new tab (target="_blank") without rel="noopener noreferrer" is a security/performance issue (tabnabbing). Add rel="noopener noreferrer" when target="_blank". [ Out of scope ]
  • line 82: UI contract inconsistency: the external link icon (ArrowUpRight) renders when target === "_blank" even if href is absent (so As is a div). This suggests an external link but results in a non-link element with target attr incorrectly applied, confusing users and a11y. Gate the icon and target by the presence of href. [ Out of scope ]
apps/web/components/charts/DomainIcon.tsx — 0 comments posted, 1 evaluated, 1 filtered
  • line 6: getFavicon attempts to derive the apex domain by removing only the first label when it detects a subdomain (lines 6-10 in code object 0). For multi-level subdomains (e.g., a.b.example.com), it returns b.example.com rather than example.com. For special public suffixes (e.g., example.co.uk with www), this heuristic can produce incorrect hosts. This yields incorrect favicon URLs and avoidable onError fallbacks. Consider using a proper public suffix list or leave the domain unchanged. [ Low confidence ]
apps/web/components/charts/HorizontalBarChart.tsx — 0 comments posted, 4 evaluated, 4 filtered
  • line 33: When item.name is an email missing the domain part (e.g., "user@") or contains multiple @, domain computed in HorizontalBarChart (lines 33-35 in code object 8) can be empty or invalid, leading DomainIcon to build a favicon URL with url=http:// (no host). This reliably triggers an error and onError fallback on each render. Add a guard to skip favicon fetch when the extracted domain is empty or invalid. [ Low confidence ]
  • line 56: Anchor tags opened in a new tab (target="_blank") are rendered without rel="noopener noreferrer" (lines 53-59 in code object 8). This exposes users to reverse tabnabbing and leaves the opener reference accessible to the new page. Add rel="noopener noreferrer" when target==="_blank". [ Low confidence ]
  • line 83: HorizontalBarChart renders a <button> container when onItemClick is provided (lines 83-91 in code object 8), but content may contain an interactive <a> element when item.href is set (lines 53-59 in code object 8). This nests an interactive control inside another interactive control, which is invalid HTML, can cause conflicting click/keyboard behaviors, and may lead to double-handling (both navigation and onItemClick firing). Use either a single interactive element per item or conditionally avoid wrapping link content inside a button. [ Low confidence ]
  • line 84: React keys use item.name (lines 84 and 96 in code object 8). If data contains duplicate names, keys collide, causing incorrect reconciliation, stale UI state, or event handler mix-ups. Use a stable unique key (e.g., an ID or index combined with name) or validate uniqueness. [ Low confidence ]
apps/web/components/new-landing/BrandScroller.tsx — 0 comments posted, 2 evaluated, 2 filtered
  • line 23: The className on line 23 contains an invalid Tailwind arbitrary style token: [gap:var(--gap))] has an extra ) which prevents Tailwind from generating the rule. As a result, the intended gap: var(--gap) will not apply, breaking spacing between items in the scroller. It should be [gap:var(--gap)]. [ Out of scope ]
  • line 33: Using alt as the React key for brand items (line 33) is unsafe because alt values are not guaranteed unique across a customizable brandList. Duplicate keys cause React reconciliation issues (items may be merged or updated incorrectly), leading to visual glitches during animation. Use a guaranteed-unique identifier (e.g., an index scoped to the list or a stable unique id) or enforce uniqueness of alt upstream. [ Low confidence ]
apps/web/components/new-landing/UnicornScene.tsx — 0 comments posted, 3 evaluated, 1 filtered
  • line 38: Potential TypeError on UnicornStudio.init() if the CDN script respects an existing global and does not overwrite window.UnicornStudio because it was set to the flag object earlier. In that case, UnicornStudio would still refer to the flag object without an init method, and script.onload will attempt UnicornStudio.init() leading to a runtime error. [ Low confidence ]
apps/web/components/new-landing/common/Badge.tsx — 0 comments posted, 1 evaluated, 1 filtered
  • line 94: Using icon || null can silently drop valid React.ReactNode values like the number 0. React supports rendering numbers, but because 0 is falsy, icon || null will render null instead of 0. This causes unexpected loss of output when a numeric icon or count of 0 is passed. Prefer {icon} (React ignores undefined/null automatically) or an explicit check for icon !== undefined && icon !== null to avoid dropping 0. [ Out of scope ]
apps/web/components/new-landing/sections/Pricing.tsx — 0 comments posted, 1 evaluated, 1 filtered
  • line 106: In Pricing, the RadioGroup’s className contains an invalid RGBA alpha value rgba(0,0,0,0.0.07) within the arbitrary shadow class: shadow-[0_0_7px_0_rgba(0,0,0,0.0.07)]. CSS numbers cannot have multiple decimal points, so this likely results in the entire box-shadow being ignored by the browser. Use a valid value like rgba(0,0,0,0.07). [ Out of scope ]
apps/web/components/ui/progress.tsx — 0 comments posted, 1 evaluated, 1 filtered
  • line 23: Progress does not defensively clamp value to [0, 100]. When given out-of-range values (e.g., negative or >100), the computed transform translateX(-${100 - (value || 0)}%) can produce unexpected motion beyond the track. Clamp value to [0, 100] before computing the transform to ensure consistent visuals. [ Low confidence ]
apps/web/next.config.ts — 0 comments posted, 1 evaluated, 1 filtered
  • line 237: Setting the Access-Control-Allow-Origin header globally to a fixed value env.NEXT_PUBLIC_BASE_URL on all routes (source: '/(.*)') can cause CORS failures for requests originating from any other valid deployment domain (e.g., preview/staging URLs). When the browser origin doesn’t exactly match env.NEXT_PUBLIC_BASE_URL, cross-origin requests to Next.js API routes or assets will be blocked by CORS, breaking functionality in those environments. Consider reflecting the request’s Origin when appropriate or scoping CORS headers only to API endpoints that require them. [ Low confidence ]

Summary by CodeRabbit

Release Notes

  • New Features

    • Rule selection now uses dropdown menu for streamlined navigation.
    • Enhanced date range picker with quick-select options (e.g., "Last week").
    • Topic distribution analytics view for trend insights.
  • Improvements

    • Reorganized dashboard layout with consolidated statistics and controls.
    • Email analytics now presented in tabbed card format for better organization.
    • Enhanced progress indicators with improved visual feedback.
    • Refined color system and design consistency across stats interfaces.
  • Removals

    • Simplified summary stats display by consolidating legacy components.

✏️ Tip: You can customize this high-level summary in your review settings.

@vercel
Copy link

vercel bot commented Nov 24, 2025

@jshwrnr is attempting to deploy a commit to the Inbox Zero OSS Program Team on Vercel.

A member of the Team first needs to authorize it.

@CLAassistant
Copy link

CLAassistant commented Nov 24, 2025

CLA assistant check
All committers have signed the CLA.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 24, 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.

Walkthrough

The pull request refactors the stats dashboard by replacing Tremor chart components with custom implementations (NewBarChart, BarListCard, MainStatChart, HorizontalBarChart), restructuring the layout with an ActionBar component, and removing obsolete dashboard components. Additionally, a comprehensive migration replaces cx from class-variance-authority with a local cn utility across landing page components, updates UI primitives (Progress, Calendar, Button), removes Tremor dependency, and introduces new color theming while deleting legacy Tremor theme configuration.

Changes

Cohort / File(s) Summary
Stats Dashboard - New Chart Components
NewBarChart.tsx, MainStatChart.tsx, BarListCard.tsx, TopicDistribution.tsx
Introduces custom chart components: NewBarChart (Recharts-based bar chart with period formatting and gradients), MainStatChart (aggregates stats with category selection), BarListCard (tabbed horizontal bars with dialog detail view), TopicDistribution (mock data visualization component).
Stats Dashboard - Deleted Components
CombinedStatsChart.tsx, DetailedStats.tsx, StatsChart.tsx, BulkUnsubscribeSummary.tsx
Removes outdated stat visualization components; eliminated multi-source chart aggregation, detailed period-based filtering UI, and newsletter summary card.
Stats Dashboard - Layout & Controls
ActionBar.tsx, Stats.tsx, DetailedStatsFilter.tsx
Simplifies ActionBar to a layout container (left/right content), restructures Stats.tsx page with new section layout and integrated DatePickerWithRange, adds ChevronDown affordance to DetailedStatsFilter.
Stats Dashboard - Chart Updates
StatsSummary.tsx, RuleStatsChart.tsx, NewsletterModal.tsx, EmailActionsAnalytics.tsx, EmailAnalytics.tsx
Migrates from Tremor BarChart to NewBarChart/BarListCard; adds period prop to StatsSummary; updates chart config patterns; EmailAnalytics consolidates two BarLists into single BarListCard with tabs.
Bulk Unsubscribe UI
BulkUnsubscribeDesktop.tsx, BulkUnsubscribeSection.tsx
Replaces Tremor ProgressBar with custom Progress + Tooltip; wraps controls in ActionBar; integrates DatePickerWithRange.
Assistant Rules
RulesSelect.tsx
Converts Tabs-based UI to DropdownMenu; adds router-based query param state management for ruleId; updates loading skeleton width to 200px.
API & Backend
by-period/route.ts
Simplifies SQL query: removes dateFormat/period_group CTE; returns direct camelCase aliases (startOfPeriod, totalCount, etc.); adjusts StatsResult type.
Shared Charts & Components
HorizontalBarChart.tsx, DomainIcon.tsx
New HorizontalBarChart with percentage bars, optional domain icons/fallback gradients, and click handlers; DomainIcon fetches favicons with seed-based gradient fallback.
UI Primitives & Controls
DatePickerWithRange.tsx, Calendar.tsx, Progress.tsx, button.tsx, PageHeader.tsx, List.tsx, StatsCard.tsx, TabSelect.tsx, ProgressPanel.tsx, Banner.tsx
Extends DatePickerWithRange with predefined ranges and dropdown selection; Calendar accepts rightContent; Progress supports innerClassName styling; Button adds "xs-2" size variant; PageHeader refactored to interface-based props; new List component; Banner API updated (title/children only); Tremor ProgressBar replaced.
Landing Page - cx→cn Migration
BrandScroller.tsx, CallToAction.tsx, FooterLineLogo.tsx, HeaderLinks.tsx, UnicornScene.tsx, Anchor.tsx, Badge.tsx, BlurFade.tsx, new-landing/common/Button.tsx, Card.tsx, DisplayCard.tsx, Section.tsx, Typography.tsx, WordReveal.tsx, Pricing.tsx
Replaces class-variance-authority's cx utility with local cn across 15+ landing page files; HeaderLinks updates color tokens to semantic values; Badge replaces hex colors with new token system.
Deleted Components
BarList.tsx, common/Banner.tsx
Removes old BarList wrapper component; removes duplicate Banner from common folder.
Theme & Config
tailwind.config.js, colors.ts, next.config.ts, package.json
Removes Tremor theme tokens (brand, background, dark-tremor); adds sidebar color scale and new color palette groups (purple, green, blue, indigo, pink, orange, etc.); new COLORS constant (analytics, footer); adds gstatic.com to Next.js image patterns; removes @tremor/react dependency.
Submodules
app/marketing
Updates submodule commit reference.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant RulesSelect
    participant Router
    participant URL

    User->>RulesSelect: Click DropdownMenu trigger
    RulesSelect->>RulesSelect: Read current ruleId from searchParams
    RulesSelect->>RulesSelect: Display getCurrentLabel()
    User->>RulesSelect: Select rule from dropdown
    RulesSelect->>RulesSelect: handleValueChange(newRuleId)
    RulesSelect->>Router: router.push with updated ruleId param
    Router->>URL: Update URL query params
    RulesSelect->>RulesSelect: Re-render with new ruleId
Loading
sequenceDiagram
    participant BarListCard
    participant TabSelect
    participant HorizontalBarChart
    participant Dialog

    BarListCard->>BarListCard: Initialize with tabs (Email, Domain, etc.)
    Note over BarListCard: selectedTab = tabs[0]
    
    BarListCard->>TabSelect: Render tab buttons
    BarListCard->>HorizontalBarChart: Render main chart with selectedTab.data
    
    rect rgba(200, 150, 255, 0.1)
        Note over BarListCard,Dialog: User interaction: click item
        HorizontalBarChart->>BarListCard: onItemClick(item)
        BarListCard->>Dialog: Open with item details
        Dialog->>HorizontalBarChart: Render larger chart in dialog
    end
    
    rect rgba(200, 200, 200, 0.1)
        Note over TabSelect: User changes tab
        TabSelect->>BarListCard: setSelectedTab(newTab)
        BarListCard->>HorizontalBarChart: Update data from new tab
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~70 minutes

  • NewBarChart implementation: Custom Recharts integration with period-based date formatting, gradient fills, and dynamic axis rendering requires careful validation of data transformation and axis configuration.
  • BarListCard and TopicDistribution: Tab switching, dialog integration, and mock data generation (generateDailyData) need verification for correct behavior and accessibility.
  • MainStatChart data transformation: useMemo-based aggregation and per-period mapping logic should be validated against test cases.
  • HorizontalBarChart domain extraction and DomainIcon favicon logic: Domain parsing (substring after "@"), favicon URL construction, and fallback gradient generation warrant careful review.
  • Stats.tsx structural reorganization: Major layout changes, removal of three components, integration of ActionBar, DatePickerWithRange, and new chart instances need holistic validation.
  • RulesSelect router-based state management: URL param reading/updating with getCurrentLabel derivation should be tested for edge cases (all/skipped/ruleId resolution).
  • API endpoint changes (by-period/route.ts): SQL simplification and CTE removal need validation against existing client expectations and data shape compatibility.
  • Repetitive cx→cn migration: While mostly mechanical, verify that all class compositions resolve correctly and that no conditional logic was inadvertently altered during the utility swap.

Possibly related PRs

Suggested reviewers

  • edulelis
  • anakarentorosserrano-star

Poem

🐰 Charts take new shape, old Tremor fades away,
Custom bars and lists now lead the stats display.
From dropdown rules to painted gradients bright,
New themes emerge—the dashboard shines just right! ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.92% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title '[WIP] Common Topics' is vague and generic. While it references 'Common Topics,' it does not clearly convey the scope or significance of the extensive changes across 50+ files, including removal of Tremor dependencies, UI component refactoring, and stats dashboard restructuring. Consider updating the title to better summarize the primary change, such as 'Refactor stats dashboard and remove Tremor dependency' or 'Replace Tremor components with custom chart components,' to provide clearer context for reviewers.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

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: 8

🧹 Nitpick comments (20)
apps/web/next.config.ts (1)

54-57: New image remotePattern for t1.gstatic.com is consistent; consider whether you want finer scoping

Adding t1.gstatic.com here matches how the other hosts are configured and should let Next/Image handle those avatars correctly. Since CSP already includes img-src ... https: ..., no extra CSP changes are required right now. If you later tighten CSP (e.g., remove the bare https: source), remember to keep t1.gstatic.com in sync there, and optionally consider adding a pathname constraint if you want stricter scoping of allowed images from this host.

apps/web/app/api/user/stats/by-period/route.ts (1)

58-58: Inconsistent alias naming.

The unread alias is not quoted, while all other aliases use double quotes. For consistency and to avoid potential case-sensitivity issues, quote this alias like the others.

-      SUM(CASE WHEN read = false THEN 1 ELSE 0 END) AS unread,
+      SUM(CASE WHEN read = false THEN 1 ELSE 0 END) AS "unread",
apps/web/components/StatsCard.tsx (1)

4-39: StatsCards implementation looks good; consider conditional subvalue rendering

The grid + Card composition is clean and data-driven. One small improvement: avoid rendering an empty <span> when subvalue is undefined.

-              <div className="">
-                <span className="text-2xl font-bold">{stat.value}</span>
-                <span className="ml-2 text-sm text-muted-foreground">
-                  {stat.subvalue}
-                </span>
-              </div>
+              <div>
+                <span className="text-2xl font-bold">{stat.value}</span>
+                {stat.subvalue && (
+                  <span className="ml-2 text-sm text-muted-foreground">
+                    {stat.subvalue}
+                  </span>
+                )}
+              </div>
apps/web/components/ui/progress.tsx (1)

9-23: Progress innerClassName extension is reasonable; confirm default color and import path

The innerClassName prop is a nice way to customize the indicator bar without forking the component. Two things to double‑check:

  • The default "bg-blue-500" changes behavior if you previously relied on a theme token like bg-primary. Make sure this is intentional for the new design system.
  • Other files import cn from "@/utils" while this one uses "@/utils/index". Aligning on a single path would avoid accidental duplication.
apps/web/components/ui/calendar.tsx (1)

28-29: Consider using theme-aware border colors.

The border color border-gray-200 is hardcoded in multiple places. This may not adapt well to dark mode or custom themes.

Consider using a theme-aware token:

-        caption_start: "p-3",
-        caption_end: "p-3 border-l border-gray-200",
+        caption_start: "p-3",
+        caption_end: "p-3 border-l border-border",
-              <div className="p-3 border-l border-gray-200">{rightContent}</div>
+              <div className="p-3 border-l border-border">{rightContent}</div>

Also applies to: 68-68

apps/web/components/DatePickerWithRange.tsx (2)

44-44: useMemo for current date may not provide intended stability.

Using useMemo(() => new Date(), []) creates a "now" timestamp that remains constant for the component's lifetime, which means the "Last day" selection won't update to the actual current day if the component stays mounted across midnight.

Consider if this behavior is intentional. If you want "now" to always reflect the actual current time, remove the memoization:

- const now = useMemo(() => new Date(), []);
+ const now = new Date();

Alternatively, if you want to cache it but update periodically, add a refresh mechanism.


100-100: Add radix parameter to parseInt for clarity.

The Number.parseInt(value) call should include the radix parameter (base 10) for explicit clarity and to avoid potential parsing issues.

-                  from: subDays(now, Number.parseInt(value)),
+                  from: subDays(now, Number.parseInt(value, 10)),
apps/web/components/charts/DomainIcon.tsx (1)

55-75: Implement favicon caching and request deduplication to reduce dependency on Google's undocumented favicon service.

Verification confirms the concerns are valid:

  • The t1.gstatic.com/faviconV2 endpoint is undocumented and Google does not publish rate limits or formal SLAs
  • Current code (line 5-10: apps/web/components/charts/DomainIcon.tsx) constructs the favicon URL on every call without caching
  • DomainIcon is used in loops (HorizontalBarChart.tsx, line 50), generating multiple independent API requests
  • Existing fallback (FallbackIcon) prevents failures but does not prevent repeated or throttled requests

Recommended improvements:

  1. Implement domain-level favicon caching (e.g., at module level or via React Query/SWR to deduplicate requests)
  2. Add request timeout/retry logic to gracefully handle service throttling
  3. Consider a favicon CDN alternative or self-hosted fallback service for critical deployments
apps/web/components/ui/button.tsx (1)

34-34: Consider a more semantic size variant name.

The "xs-2" naming is unconventional. Consider alternatives like "xs-md", "2xs", or a more descriptive name that clearly indicates it sits between "xs" and "sm".

Example refactor:

-        "xs-2": "h-7 rounded-md px-2 text-xs",
+        "2xs": "h-7 rounded-md px-2 text-xs",

Or:

-        "xs-2": "h-7 rounded-md px-2 text-xs",
+        "xs-md": "h-7 rounded-md px-2 text-xs",
apps/web/app/(app)/[emailAccountId]/stats/BarListCard.tsx (2)

18-18: Address the TODO for type safety.

The data property in the tabs array is typed as any. Consider defining a proper type for the data structure to improve type safety.

Do you want me to help define the type based on how HorizontalBarChart expects the data to be structured?


32-34: Memoize selected tab lookup for better performance.

The expression tabs.find((d) => d.id === selected) is repeated multiple times (lines 59, 64, 83). Consider memoizing this lookup to avoid recalculating on each render.

Apply this refactor:

+import { TabSelect } from "@/components/TabSelect";
+import { Card, CardContent, CardHeader } from "@/components/ui/card";
+import { HorizontalBarChart } from "@/components/charts/HorizontalBarChart";
-import { useState } from "react";
+import { useState, useMemo } from "react";
 import { Button } from "@/components/ui/button";
 // ... rest of imports

 export function BarListCard({
   tabs,
   icon,
   title,
   onItemClick,
   hideIcons,
 }: BarListCardProps) {
   const [selected, setSelected] = useState<string | null>(
     tabs?.length > 0 ? tabs[0]?.id : null,
   );

+  const selectedTab = useMemo(
+    () => tabs.find((d) => d.id === selected),
+    [tabs, selected]
+  );
+
   return (
     <Card className="h-full bg-background relative">
       {/* ... */}
       <HorizontalBarChart
-        data={tabs.find((d) => d.id === selected)?.data || []}
+        data={selectedTab?.data || []}
         onItemClick={onItemClick}
         hideIcon={hideIcons}
       />
       <div className="absolute w-full left-0 bottom-0 pb-6 z-30">
-        {tabs.find((d) => d.id === selected)?.data.length > 0 && (
+        {(selectedTab?.data.length ?? 0) > 0 && (
           <div className="flex justify-center">
             <Dialog>
               {/* ... */}
               <HorizontalBarChart
-                data={tabs.find((d) => d.id === selected)?.data || []}
+                data={selectedTab?.data || []}
                 onItemClick={onItemClick}
                 hideIcon={hideIcons}
               />
             </Dialog>
           </div>
         )}
       </div>
     </Card>
   );
 }

Also applies to: 59-59, 64-64, 83-83

apps/web/app/(app)/[emailAccountId]/bulk-unsubscribe/BulkUnsubscribeSection.tsx (1)

250-276: Simplify nested div structure.

The Toggle component is wrapped in unnecessary nested divs (lines 251-253). The outer div has flex styling, but the inner divs add no semantic or styling value.

Apply this diff to simplify:

-        <ActionBar rightContent={<LoadStatsButton />}>
-          <div className="flex items-center justify-end gap-1">
-            <div className="">
-              <Toggle
-                name="show-unhandled"
-                label="Only unhandled"
-                enabled={onlyUnhandled}
-                onChange={() =>
-                  setFilters(
-                    onlyUnhandled
-                      ? {
-                          unhandled: true,
-                          autoArchived: true,
-                          unsubscribed: true,
-                          approved: true,
-                        }
-                      : {
-                          unhandled: true,
-                          autoArchived: false,
-                          unsubscribed: false,
-                          approved: false,
-                        },
-                  )
-                }
-              />
-            </div>
-          </div>
+        <ActionBar rightContent={<LoadStatsButton />}>
+          <Toggle
+            name="show-unhandled"
+            label="Only unhandled"
+            enabled={onlyUnhandled}
+            onChange={() =>
+              setFilters(
+                onlyUnhandled
+                  ? {
+                      unhandled: true,
+                      autoArchived: true,
+                      unsubscribed: true,
+                      approved: true,
+                    }
+                  : {
+                      unhandled: true,
+                      autoArchived: false,
+                      unsubscribed: false,
+                      approved: false,
+                    },
+              )
+            }
+          />
           <SearchBar onSearch={setSearch} />
apps/web/app/(app)/[emailAccountId]/stats/EmailActionsAnalytics.tsx (1)

8-15: NewBarChart wiring and color config look solid; consider sharing types with the API response

The chartConfig keys and dataKeys={["Archived", "Deleted"]} line up, and using COLORS.analytics keeps things consistent across charts. The only thing to watch is that /api/user/stats/email-actions continues to return date, Archived, and Deleted fields with those exact names; if the backend ever changes casing or naming, this chart will silently break. If you have a shared type for the response shape, you could reuse it here (or derive dataKeys from that type) to catch such changes at compile time.

Also applies to: 30-38

apps/web/components/Banner.tsx (1)

3-20: Banner API change is clear; double‑check all callsites and children usage

The new { title, children } shape and UnicornScene background look good. Since onClose and description were removed and title is now React.ReactNode, just make sure all existing usages have been updated (especially anything relying on a dismissible banner). Also, the typeof children === "string" branch is fine for simple messages, but if callers might pass numbers or arrays you may want to broaden that condition or always wrap in a <p> for consistent spacing.

apps/web/utils/colors.ts (1)

25-37: COLORS token map is straightforward; consider as const for stronger typing

The COLORS structure is clear and works well with the chart config usage. To get stricter typing (and prevent accidental mutation), you could append as const to the object so consumers see literal keys and values:

export const COLORS = {
  analytics: {
    blue: "#006EFF80",
    purple: "#6410FF80",
    pink: "#C942B2",
    lightPink: "#C942B260",
    green: "#17A34A",
    lightGreen: "#17A34A60",
  },
  footer: {
    gray: "#4E4E4E",
  },
} as const;
apps/web/app/(app)/[emailAccountId]/stats/TopicDistribution.tsx (2)

16-79: Mock topic + daily data generation is fine; consider stabilizing per topic

The mock topics and generateDailyData function are clear and produce reasonable demo data. Because generateDailyData(selectedTopic.count) is called directly in the JSX, every re‑render of the dialog regenerates random values, which can cause the chart to jump even when nothing “changed” from the user’s perspective (and may run twice in React strict mode). If you want the daily series to stay stable per topic while the dialog is open, you could memoize by topic name or precompute dailyData as part of MOCK_TOPICS.


81-151: Dialog open handling works but could use the open argument for clarity

Using open={!!selectedTopic} and onOpenChange={() => setSelectedTopic(null)} correctly closes the dialog whenever the user dismisses it. For a slightly clearer contract with the Dialog API, you might capture the open value and only clear the selection when it becomes false:

<Dialog
  open={!!selectedTopic}
  onOpenChange={(open) => {
    if (!open) setSelectedTopic(null);
  }}
>

This makes it obvious that you’re reacting to the dialog closing, and gives you flexibility if you ever add a trigger that toggles it open.

apps/web/app/(app)/[emailAccountId]/stats/MainStatChart.tsx (1)

11-103: MainStatChart data mapping looks correct; only minor polish opportunities

The chartConfig, chartData mapping, and total derivations all line up with the expected field names (All, Read, Sent, Archived, Unread, Unarchived / inboxCount), and the activeChart logic cleanly switches between the different views while still showing paired series for read/unread and archived/inbox. A few small polish ideas:

  • getActiveChart(activChart: ...) has a minor typo in the parameter name; renaming to activeChart would improve readability.
  • If the API ever changes the startOfPeriod format away from "MMM dd, yyyy", the parse call will start producing Invalid Date silently. If there’s a shared type or helper for this date field elsewhere, reusing it here would help keep things in sync.
  • Optionally, you could derive the header button list from chartConfig (or a small config array) instead of hard‑coding ["received", "sent", "read", "archived"] to reduce duplication if new metrics are added later.

Functionally, though, this component looks good as‑is.

apps/web/app/(app)/[emailAccountId]/stats/Stats.tsx (1)

15-21: Stats header and period/date wiring look correct; minor layout nit

  • Title derivation and PageHeading usage are clear and avoid duplication.
  • DatePickerWithRange + onSetDateDropdown logic keeps period reasonable for shorter ranges and is side‑effect safe.
  • StatsSummary, EmailAnalytics, TopicDistribution, and RuleStatsChart all share the same dateRange/refreshInterval/period as expected.

One minor UX nit: the wrapper around TopicDistribution uses grid grid-cols-2 but only contains a single child, so half the width is unused. If that isn’t intentional, consider grid-cols-1 md:grid-cols-2 or dropping the grid until a second card is added.

Also applies to: 73-77, 80-133

apps/web/components/charts/HorizontalBarChart.tsx (1)

81-92: Key uniqueness assumption.

The component uses item.name as the React key (lines 84, 96), which assumes that names are unique within the data array. If duplicate names could exist, consider using an index or a more unique identifier.

If duplicates are possible, apply this diff:

-{data.map((item) => {
+{data.map((item, index) => {
   // ... content ...
   if (onItemClick) {
     return (
       <button
-        key={item.name}
+        key={`${item.name}-${index}`}
         type="button"
         // ...

Also applies to: 94-101

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d979f3c and 480449f.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (55)
  • apps/web/app/(app)/[emailAccountId]/assistant/RulesSelect.tsx (1 hunks)
  • apps/web/app/(app)/[emailAccountId]/bulk-unsubscribe/BulkUnsubscribeDesktop.tsx (2 hunks)
  • apps/web/app/(app)/[emailAccountId]/bulk-unsubscribe/BulkUnsubscribeSection.tsx (3 hunks)
  • apps/web/app/(app)/[emailAccountId]/bulk-unsubscribe/BulkUnsubscribeSummary.tsx (0 hunks)
  • apps/web/app/(app)/[emailAccountId]/stats/ActionBar.tsx (1 hunks)
  • apps/web/app/(app)/[emailAccountId]/stats/BarListCard.tsx (1 hunks)
  • apps/web/app/(app)/[emailAccountId]/stats/CombinedStatsChart.tsx (0 hunks)
  • apps/web/app/(app)/[emailAccountId]/stats/DetailedStats.tsx (0 hunks)
  • apps/web/app/(app)/[emailAccountId]/stats/DetailedStatsFilter.tsx (2 hunks)
  • apps/web/app/(app)/[emailAccountId]/stats/EmailActionsAnalytics.tsx (2 hunks)
  • apps/web/app/(app)/[emailAccountId]/stats/EmailAnalytics.tsx (2 hunks)
  • apps/web/app/(app)/[emailAccountId]/stats/LoadStatsButton.tsx (2 hunks)
  • apps/web/app/(app)/[emailAccountId]/stats/MainStatChart.tsx (1 hunks)
  • apps/web/app/(app)/[emailAccountId]/stats/NewBarChart.tsx (1 hunks)
  • apps/web/app/(app)/[emailAccountId]/stats/NewsletterModal.tsx (4 hunks)
  • apps/web/app/(app)/[emailAccountId]/stats/RuleStatsChart.tsx (5 hunks)
  • apps/web/app/(app)/[emailAccountId]/stats/Stats.tsx (2 hunks)
  • apps/web/app/(app)/[emailAccountId]/stats/StatsChart.tsx (0 hunks)
  • apps/web/app/(app)/[emailAccountId]/stats/StatsSummary.tsx (1 hunks)
  • apps/web/app/(app)/[emailAccountId]/stats/TopicDistribution.tsx (1 hunks)
  • apps/web/app/(marketing) (1 hunks)
  • apps/web/app/api/user/stats/by-period/route.ts (1 hunks)
  • apps/web/components/Banner.tsx (1 hunks)
  • apps/web/components/DatePickerWithRange.tsx (2 hunks)
  • apps/web/components/List.tsx (1 hunks)
  • apps/web/components/PageHeader.tsx (1 hunks)
  • apps/web/components/ProgressPanel.tsx (2 hunks)
  • apps/web/components/StatsCard.tsx (1 hunks)
  • apps/web/components/TabSelect.tsx (2 hunks)
  • apps/web/components/charts/BarList.tsx (0 hunks)
  • apps/web/components/charts/DomainIcon.tsx (1 hunks)
  • apps/web/components/charts/HorizontalBarChart.tsx (1 hunks)
  • apps/web/components/common/Banner.tsx (0 hunks)
  • apps/web/components/new-landing/BrandScroller.tsx (3 hunks)
  • apps/web/components/new-landing/CallToAction.tsx (2 hunks)
  • apps/web/components/new-landing/FooterLineLogo.tsx (2 hunks)
  • apps/web/components/new-landing/HeaderLinks.tsx (1 hunks)
  • apps/web/components/new-landing/UnicornScene.tsx (2 hunks)
  • apps/web/components/new-landing/common/Anchor.tsx (2 hunks)
  • apps/web/components/new-landing/common/Badge.tsx (2 hunks)
  • apps/web/components/new-landing/common/BlurFade.tsx (2 hunks)
  • apps/web/components/new-landing/common/Button.tsx (2 hunks)
  • apps/web/components/new-landing/common/Card.tsx (2 hunks)
  • apps/web/components/new-landing/common/DisplayCard.tsx (2 hunks)
  • apps/web/components/new-landing/common/Section.tsx (3 hunks)
  • apps/web/components/new-landing/common/Typography.tsx (5 hunks)
  • apps/web/components/new-landing/common/WordReveal.tsx (2 hunks)
  • apps/web/components/new-landing/sections/Pricing.tsx (2 hunks)
  • apps/web/components/ui/button.tsx (1 hunks)
  • apps/web/components/ui/calendar.tsx (2 hunks)
  • apps/web/components/ui/progress.tsx (2 hunks)
  • apps/web/next.config.ts (1 hunks)
  • apps/web/package.json (0 hunks)
  • apps/web/tailwind.config.js (1 hunks)
  • apps/web/utils/colors.ts (1 hunks)
💤 Files with no reviewable changes (7)
  • apps/web/app/(app)/[emailAccountId]/stats/CombinedStatsChart.tsx
  • apps/web/app/(app)/[emailAccountId]/bulk-unsubscribe/BulkUnsubscribeSummary.tsx
  • apps/web/app/(app)/[emailAccountId]/stats/StatsChart.tsx
  • apps/web/components/common/Banner.tsx
  • apps/web/app/(app)/[emailAccountId]/stats/DetailedStats.tsx
  • apps/web/package.json
  • apps/web/components/charts/BarList.tsx
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2024-08-23T11:37:26.779Z
Learnt from: aryanprince
Repo: elie222/inbox-zero PR: 210
File: apps/web/app/(app)/stats/NewsletterModal.tsx:2-4
Timestamp: 2024-08-23T11:37:26.779Z
Learning: `MoreDropdown` is a React component and `useUnsubscribeButton` is a custom React hook, and they should not be imported using `import type`.

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/bulk-unsubscribe/BulkUnsubscribeDesktop.tsx
  • apps/web/app/(app)/[emailAccountId]/stats/NewsletterModal.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RulesSelect.tsx
  • apps/web/app/(app)/[emailAccountId]/stats/DetailedStatsFilter.tsx
  • apps/web/components/new-landing/sections/Pricing.tsx
  • apps/web/components/DatePickerWithRange.tsx
📚 Learning: 2025-07-17T04:19:57.099Z
Learnt from: edulelis
Repo: elie222/inbox-zero PR: 576
File: packages/resend/emails/digest.tsx:78-83
Timestamp: 2025-07-17T04:19:57.099Z
Learning: In packages/resend/emails/digest.tsx, the DigestEmailProps type uses `[key: string]: DigestItem[] | undefined | string | Date | undefined` instead of intersection types like `& Record<string, DigestItem[] | undefined>` due to implementation constraints. This was the initial implementation approach and cannot be changed to more restrictive typing.

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/stats/NewsletterModal.tsx
  • apps/web/app/(app)/[emailAccountId]/stats/EmailAnalytics.tsx
📚 Learning: 2025-06-05T09:49:12.168Z
Learnt from: elie222
Repo: elie222/inbox-zero PR: 485
File: apps/web/app/(landing)/login/page.tsx:41-43
Timestamp: 2025-06-05T09:49:12.168Z
Learning: In Next.js App Router, components that use the `useSearchParams` hook require a Suspense boundary to handle the asynchronous nature of search parameter access. The Suspense wrapper is necessary and should not be removed when a component uses useSearchParams.

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RulesSelect.tsx
🧬 Code graph analysis (15)
apps/web/app/(app)/[emailAccountId]/stats/TopicDistribution.tsx (3)
apps/web/utils/colors.ts (1)
  • COLORS (25-37)
apps/web/app/(app)/[emailAccountId]/stats/BarListCard.tsx (1)
  • BarListCard (25-96)
apps/web/app/(app)/[emailAccountId]/stats/NewBarChart.tsx (1)
  • NewBarChart (18-152)
apps/web/app/(app)/[emailAccountId]/stats/BarListCard.tsx (4)
apps/web/components/new-landing/common/Card.tsx (3)
  • Card (67-105)
  • CardHeader (22-54)
  • CardContent (10-12)
apps/web/components/TabSelect.tsx (1)
  • TabSelect (44-101)
apps/web/components/charts/HorizontalBarChart.tsx (1)
  • HorizontalBarChart (21-105)
apps/web/components/new-landing/common/Button.tsx (1)
  • Button (16-100)
apps/web/components/new-landing/FooterLineLogo.tsx (1)
apps/web/utils/colors.ts (1)
  • COLORS (25-37)
apps/web/app/(app)/[emailAccountId]/bulk-unsubscribe/BulkUnsubscribeDesktop.tsx (2)
apps/web/components/Tooltip.tsx (1)
  • Tooltip (18-41)
apps/web/components/ui/progress.tsx (1)
  • Progress (29-29)
apps/web/app/(app)/[emailAccountId]/stats/EmailActionsAnalytics.tsx (3)
apps/web/utils/colors.ts (1)
  • COLORS (25-37)
apps/web/components/ui/chart.tsx (1)
  • ChartConfig (11-19)
apps/web/app/(app)/[emailAccountId]/stats/NewBarChart.tsx (1)
  • NewBarChart (18-152)
apps/web/app/(app)/[emailAccountId]/stats/StatsSummary.tsx (3)
apps/web/app/api/user/stats/by-period/route.ts (2)
  • StatsByWeekParams (15-15)
  • StatsByWeekResponse (16-16)
apps/web/hooks/useOrgSWR.ts (1)
  • useOrgSWR (10-45)
apps/web/app/(app)/[emailAccountId]/stats/MainStatChart.tsx (1)
  • MainStatChart (28-104)
apps/web/app/(app)/[emailAccountId]/stats/MainStatChart.tsx (5)
apps/web/utils/colors.ts (1)
  • COLORS (25-37)
apps/web/components/ui/chart.tsx (1)
  • ChartConfig (11-19)
apps/web/app/api/user/stats/by-period/route.ts (1)
  • StatsByWeekResponse (16-16)
apps/web/components/new-landing/common/Card.tsx (2)
  • Card (67-105)
  • CardContent (10-12)
apps/web/app/(app)/[emailAccountId]/stats/NewBarChart.tsx (1)
  • NewBarChart (18-152)
apps/web/components/StatsCard.tsx (1)
apps/web/components/new-landing/common/Card.tsx (3)
  • Card (67-105)
  • CardHeader (22-54)
  • CardContent (10-12)
apps/web/app/(app)/[emailAccountId]/stats/NewsletterModal.tsx (2)
apps/web/app/(app)/[emailAccountId]/stats/NewBarChart.tsx (1)
  • NewBarChart (18-152)
apps/web/utils/colors.ts (1)
  • COLORS (25-37)
apps/web/components/charts/HorizontalBarChart.tsx (1)
apps/web/components/charts/DomainIcon.tsx (1)
  • DomainIcon (55-75)
apps/web/components/ProgressPanel.tsx (1)
apps/web/components/ui/progress.tsx (1)
  • Progress (29-29)
apps/web/components/Banner.tsx (1)
apps/web/components/new-landing/UnicornScene.tsx (1)
  • UnicornScene (26-55)
apps/web/app/(app)/[emailAccountId]/bulk-unsubscribe/BulkUnsubscribeSection.tsx (3)
apps/web/app/(app)/[emailAccountId]/stats/ActionBar.tsx (1)
  • ActionBar (9-25)
apps/web/app/(app)/[emailAccountId]/stats/LoadStatsButton.tsx (1)
  • LoadStatsButton (8-27)
apps/web/components/DatePickerWithRange.tsx (1)
  • DatePickerWithRange (37-110)
apps/web/app/(app)/[emailAccountId]/stats/Stats.tsx (6)
apps/web/components/PageWrapper.tsx (1)
  • PageWrapper (3-20)
apps/web/app/(app)/[emailAccountId]/stats/ActionBar.tsx (1)
  • ActionBar (9-25)
apps/web/app/(app)/[emailAccountId]/stats/LoadStatsButton.tsx (1)
  • LoadStatsButton (8-27)
apps/web/app/(app)/[emailAccountId]/stats/DetailedStatsFilter.tsx (1)
  • DetailedStatsFilter (18-79)
apps/web/app/(app)/[emailAccountId]/stats/StatsSummary.tsx (1)
  • StatsSummary (14-49)
apps/web/app/(app)/[emailAccountId]/stats/TopicDistribution.tsx (1)
  • TopicDistribution (81-154)
apps/web/components/DatePickerWithRange.tsx (1)
apps/web/components/List.tsx (1)
  • List (16-39)
⏰ 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). (3)
  • GitHub Check: cubic · AI code reviewer
  • GitHub Check: Jit Security
  • GitHub Check: Review for correctness
🔇 Additional comments (32)
apps/web/app/(marketing) (1)

1-1: Verify the submodule reference update is intentional and document its purpose.

This commit updates the submodule pointer. Since the actual code changes occur within the submodule (outside this view), verify that:

  1. The new commit hash (e3c81eb5e0d7950b89d45a8c3a57e568d3072028) contains the intended changes.
  2. The submodule update aligns with the PR's refactoring goals (dashboard components, Tremor removal, etc.).
  3. No breaking changes or new dependencies are introduced in the submodule.

Please confirm the submodule changes are intentional and document what functional updates are included in the new commit reference.

apps/web/components/new-landing/sections/Pricing.tsx (1)

35-35: LGTM! Clean refactoring from cx to cn utility.

The migration from class-variance-authority's cx to the local cn utility is correctly applied—both the import and usage are updated consistently. The conditional class merging logic remains unchanged.

Also applies to: 114-117

apps/web/components/PageHeader.tsx (1)

14-20: LGTM! Clean refactor to extract prop types.

The interface is well-defined and the function signature correctly uses it. This improves code organization by extracting inline types into a named interface.

apps/web/components/new-landing/common/Anchor.tsx (1)

1-1: LGTM! Clean migration to cn utility.

The replacement of cx with cn is consistent with the broader refactor across the codebase.

Also applies to: 16-16

apps/web/components/new-landing/common/Section.tsx (1)

1-1: LGTM! Consistent utility migration.

The migration from cx to cn is applied correctly across both components.

Also applies to: 11-11, 29-29

apps/web/components/new-landing/common/Typography.tsx (1)

1-2: LGTM! Proper separation of utilities.

The migration correctly replaces cx with cn for class merging while appropriately retaining cva for variant styles in the Paragraph component.

Also applies to: 12-15, 37-39, 49-51, 63-66

apps/web/components/new-landing/BrandScroller.tsx (1)

8-8: LGTM! Consistent migration.

The replacement of cx with cn is applied correctly throughout the component.

Also applies to: 26-29, 39-39

apps/web/components/new-landing/common/BlurFade.tsx (1)

11-11: LGTM! Clean utility replacement.

The migration from cx to cn is correctly applied.

Also applies to: 66-66

apps/web/components/TabSelect.tsx (1)

32-32: Verify the visual impact of removing horizontal padding.

The removal of px-1.5 from the indicator will change its width. Ensure this visual change is intentional and aligns with the design.

apps/web/components/new-landing/CallToAction.tsx (1)

8-8: LGTM! Consistent with the migration pattern.

The replacement of cx with cn is correctly applied.

Also applies to: 24-24

apps/web/components/ProgressPanel.tsx (1)

6-6: ✓ Verified: Progress component supports innerClassName prop for dynamic color control

The Progress component in apps/web/components/ui/progress.tsx properly implements the innerClassName prop as an optional string parameter (defaulting to "bg-blue-500"), which is correctly applied to the progress indicator element. The implementation supports dynamic color changes based on completion state as intended.

apps/web/components/new-landing/common/DisplayCard.tsx (1)

2-36: cn migration in DisplayCard looks correct

cn import and both className={cn(..., className)} usages preserve the previous behavior and keep the centerContent conditional intact. No issues from this refactor.

apps/web/components/new-landing/common/Card.tsx (1)

2-43: Consistent cn usage in Card helpers

Switching to cn for CardContent and the CardHeader title preserves existing styles while aligning with the repo-wide utility. No functional concerns.

apps/web/components/new-landing/HeaderLinks.tsx (1)

35-88: Semantic color tokens and EnhancedListItem wiring look consistent

All useCases entries provide the expected iconColor, borderColor, gradient, and hoverBg values, and EnhancedListItem cleanly threads them into cn(...) calls. This should make future theming straightforward with no behavioral regressions.

Also applies to: 169-188

apps/web/app/(app)/[emailAccountId]/bulk-unsubscribe/BulkUnsubscribeDesktop.tsx (1)

18-23: Progress + tooltip refactor is sound; verify data invariants

Replacing Tremor with <Progress> and Radix tooltips retains the xl/2xl breakpoint behavior and adds clearer percentage labels. Tooltip messages correctly derive:

  • read: readEmails vs value - readEmails
  • archived: archivedEmails vs item.inboxEmails

Just ensure upstream data keeps readEmails <= value and archivedEmails + inboxEmails in sync to avoid confusing counts.

Also applies to: 124-154

apps/web/app/(app)/[emailAccountId]/assistant/RulesSelect.tsx (1)

4-66: Dropdown-based rule selection + URL sync looks correct; ensure Suspense wrapper upstream

The new DropdownMenu flow correctly:

  • Reads ruleId from useSearchParams with "all" fallback.
  • Updates just the ruleId query parameter while preserving others via URLSearchParams.
  • Derives the trigger label from the current value, gracefully falling back to "All rules" if a rule id is missing.
  • Narrows the loading skeleton to match the trigger width.

Since this component now uses useSearchParams, please confirm that its parent tree still wraps it in <Suspense> to avoid App Router warnings or hydration issues. Based on learnings, components using useSearchParams in this repo are expected to sit under a Suspense boundary.

apps/web/components/new-landing/UnicornScene.tsx (1)

3-3: LGTM! Clean migration from cx to cn utility.

The replacement of cx with the local cn utility is consistent with the broader refactoring effort across the codebase. No behavioral changes, and the implementation remains correct.

Also applies to: 52-52

apps/web/app/(app)/[emailAccountId]/stats/DetailedStatsFilter.tsx (1)

14-14: LGTM! Improved dropdown affordance.

The addition of the ChevronDown icon and the change from "ghost" to "outline" variant improve the visual clarity that this is a dropdown button. Good UX enhancement.

Also applies to: 46-46, 52-52

apps/web/components/new-landing/common/Button.tsx (1)

1-3: LGTM! Correct migration from cx to cn.

The replacement of cx with cn while keeping cva is correct. The cva library is still needed for variant composition, while cn handles the className merging. This aligns with the broader refactoring pattern.

Also applies to: 63-63

apps/web/components/new-landing/common/WordReveal.tsx (1)

2-2: LGTM! Consistent with cx to cn migration.

Clean replacement of the cx utility with the local cn utility. No behavioral changes and implementation remains correct.

Also applies to: 32-32

apps/web/tailwind.config.js (1)

86-176: Color tokens are actively used throughout the codebase.

Verification confirms the sidebar colors and new color palette are properly integrated:

  • Sidebar colors are extensively used in apps/web/components/ui/sidebar.tsx and related components via Tailwind classes (bg-sidebar, text-sidebar-foreground, bg-sidebar-accent, etc.)
  • New color palette is actively used in apps/web/components/new-landing/HeaderLinks.tsx and apps/web/components/new-landing/common/Badge.tsx for landing page styling across all color families (purple, green, blue, indigo, pink, orange, yellow, brown, red, cyan, gray)

No bloat detected.

apps/web/components/ui/calendar.tsx (1)

23-23: I've located the Calendar component usages. There are two places where the Calendar UI component is used:

  1. apps/web/components/DatePickerWithRange.tsx (line 82) — using mode="range"
  2. apps/web/app/(app)/[emailAccountId]/assistant/SetDateDropdown.tsx (line 45) — using mode="single"

Both usages are inside PopoverContent components with className="w-auto p-0", meaning the Calendar is responsible for its own internal spacing.

Looking at the updated calendar.tsx, the padding is now applied via the classNames object:

  • caption_start: "p-3" and caption_end: "p-3 border-l border-gray-200" provide padding to the caption (month header) elements
  • The day grid itself has spacing defined through space-y-1 and cell dimensions (h-9 w-9)

The structural change appears sound—padding is moved from an outer wrapper to specific caption elements. However, this requires manual verification since the concern about visual coverage cannot be definitively resolved without seeing actual rendering or visual regression tests. The caption padding applies to the month header row, but the complete visual appearance of the calendar (including spacing around the entire day grid) depends on whether the classNames provide sufficient spacing when there's no outer padding.

Please verify that both Calendar usages render with appropriate spacing and no visual regressions in the date picker and date dropdown components.

apps/web/app/(app)/[emailAccountId]/stats/LoadStatsButton.tsx (1)

3-3: LGTM!

The icon change from AreaChartIcon to RefreshCcw and the updated button labels ("Loading more..." / "Load more") better convey the button's purpose of loading additional data.

Also applies to: 21-23

apps/web/components/new-landing/FooterLineLogo.tsx (1)

1-1: LGTM!

Migrating from the hardcoded hex value to COLORS.footer.gray aligns with the project-wide color system refactoring. This improves maintainability by centralizing color definitions.

Also applies to: 20-20

apps/web/components/new-landing/common/Badge.tsx (1)

35-76: LGTM!

The migration from explicit hex colors to token-based color classes (e.g., text-new-blue-600, from-new-blue-150) is consistent across all badge variants. This aligns with the project-wide color system refactoring and improves maintainability.

apps/web/app/(app)/[emailAccountId]/stats/NewsletterModal.tsx (2)

51-51: Verify dialog content scrolling behavior.

The overflow-scroll class was removed from DialogContent. Ensure that content overflow is handled appropriately, especially when displaying large datasets in the modal.


154-160: Data structure verified—migration is correct.

The getSenderEmails function returns { result: Array<{ startOfPeriod: string; Emails: number }> }, which matches the NewBarChart expectations perfectly. The startOfPeriod key is used for xAxisKey and Emails is configured in the chart config. No issues found.

apps/web/app/(app)/[emailAccountId]/bulk-unsubscribe/BulkUnsubscribeSection.tsx (1)

250-341: LGTM!

The integration with ActionBar and DatePickerWithRange follows the new consolidated header pattern correctly. The LoadStatsButton is appropriately placed in the rightContent slot, and all necessary props are passed to DatePickerWithRange.

apps/web/app/(app)/[emailAccountId]/stats/ActionBar.tsx (1)

3-23: Simplified ActionBar API and layout look solid

Props and flex layout are straightforward and match how ActionBar is used elsewhere (header controls + right-aligned actions). No functional concerns here.

apps/web/app/(app)/[emailAccountId]/stats/RuleStatsChart.tsx (1)

25-27: NewBarChart integration and configs are consistent

  • barChartData (group + executed) matches barChartConfig and dataKeys={["executed"]}.
  • Using xAxisKey="group" with xAxisFormatter={(value) => value} correctly bypasses the date formatter.
  • Pie chart config and “no data” state in CardBasic are wired sensibly.

No issues from a data/typing standpoint.

Also applies to: 53-55, 57-60, 82-90, 100-117, 149-152, 155-160

apps/web/app/(app)/[emailAccountId]/stats/StatsSummary.tsx (1)

12-38: Clean period threading into API and MainStatChart

  • Adding period to StatsSummary props and including it in StatsByWeekParams aligns with the new chart behavior.
  • The Object.entries + toString + URLSearchParams construction avoids non‑string values in the query.
  • Rendering MainStatChart with {data, period} is consistent with its interface.

This looks good as‑is.

Also applies to: 44-47

apps/web/app/(app)/[emailAccountId]/stats/EmailAnalytics.tsx (1)

12-13: LGTM! Clean refactoring to BarListCard.

The refactoring successfully consolidates the two separate BarList components into BarListCard components with tabs. The formatEmailItem helper cleanly adds Gmail search URLs to items, and the layout adjustment from 3 to 2 columns aligns with the new structure.

Also applies to: 41-47, 50-99

Comment on lines +27 to +54
const defaultFormatter = (value: any) => {
const date = new Date(value);

if (period === "year") {
return date.toLocaleDateString("en-US", {
year: "numeric",
});
}

if (period === "month") {
return date.toLocaleDateString("en-US", {
month: "short",
year: "numeric",
});
}

if (period === "week" || period === "day") {
return date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
});
}

return date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
});
};
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

Tooltip date formatting breaks for non‑date x‑axes

defaultFormatter and the tooltip both assume data[xAxisKey] is a date. For usages like RuleStatsChart (xAxisKey="group" with rule names), the tooltip header will render “Invalid Date”.

Consider guarding against non‑date values in the tooltip and falling back to the raw x value (or the same formatter used on the axis). For example:

-        const data = payload[0];
-        const date = new Date(data.payload[xAxisKey]);
-
-        let dateFormat: Intl.DateTimeFormatOptions;
-        if (period === "year") {
-          dateFormat = { year: "numeric" };
-        } else if (period === "month") {
-          dateFormat = { month: "short", year: "numeric" };
-        } else {
-          dateFormat = { month: "short", day: "numeric", year: "numeric" };
-        }
+        const dataPoint = payload[0];
+        const rawValue = dataPoint.payload[xAxisKey];
+        const date = new Date(rawValue);
+
+        let headerLabel: string;
+        if (!Number.isNaN(date.getTime())) {
+          let dateFormat: Intl.DateTimeFormatOptions;
+          if (period === "year") {
+            dateFormat = { year: "numeric" };
+          } else if (period === "month") {
+            dateFormat = { month: "short", year: "numeric" };
+          } else {
+            dateFormat = { month: "short", day: "numeric", year: "numeric" };
+          }
+          headerLabel = date.toLocaleDateString("en-US", dateFormat);
+        } else {
+          headerLabel = String(rawValue ?? "");
+        }
@@
-                <p className="mb-2 font-medium">
-                  {date.toLocaleDateString("en-US", dateFormat)}
-                </p>
+                <p className="mb-2 font-medium">{headerLabel}</p>

This keeps nice date formatting when appropriate while avoiding broken tooltips for categorical axes.

Also applies to: 56-57, 95-135, 137-148

🤖 Prompt for AI Agents
In apps/web/app/(app)/[emailAccountId]/stats/NewBarChart.tsx around lines 27-54
(and also apply same changes at 56-57, 95-135, 137-148), the current
defaultFormatter and tooltip assume data[xAxisKey] is always a date which yields
"Invalid Date" for categorical x values; update the formatter and any tooltip
header logic to first detect if the incoming value is a valid date (e.g.,
attempt new Date(value) and check !isNaN(date.getTime())) and only apply the
date formatting branches when valid, otherwise return the raw x value (or call
the axis label formatter for consistency); ensure the tooltip uses this guarded
formatter for its header so categorical axes show the original string instead of
"Invalid Date."

Comment on lines +52 to 64
SELECT
DATE_TRUNC(${Prisma.raw(`'${period}'`)}, date) AS "startOfPeriod",
COUNT(*) AS "totalCount",
SUM(CASE WHEN inbox = true THEN 1 ELSE 0 END) AS "inboxCount",
SUM(CASE WHEN inbox = false THEN 1 ELSE 0 END) AS "notInbox",
SUM(CASE WHEN read = true THEN 1 ELSE 0 END) AS "readCount",
SUM(CASE WHEN read = false THEN 1 ELSE 0 END) AS unread,
SUM(CASE WHEN sent = true THEN 1 ELSE 0 END) AS "sentCount"
FROM "EmailMessage"
${whereClause}${dateClause}
GROUP BY "startOfPeriod"
ORDER BY "startOfPeriod"
`;
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 | 🟡 Minor

🧩 Analysis chain

Verify SQL injection protection in zodPeriod validation.

Line 53 uses Prisma.raw() with string interpolation of the period parameter. Even though period is validated by zodPeriod, this pattern is risky and could lead to SQL injection if the validation doesn't strictly whitelist allowed values (e.g., 'day', 'week', 'month', 'year').

Run the following script to verify that zodPeriod provides strict validation:

Consider refactoring to use a safer approach with a whitelist mapping:

+  // Safe period mapping
+  const periodMap: Record<string, string> = {
+    day: 'day',
+    week: 'week', 
+    month: 'month',
+    year: 'year'
+  };
+  
+  const safePeriod = periodMap[period];
+  if (!safePeriod) {
+    throw new Error('Invalid period');
+  }
+
   return prisma.$queryRaw<StatsResult[]>`
     SELECT
-      DATE_TRUNC(${Prisma.raw(`'${period}'`)}, date) AS "startOfPeriod",
+      DATE_TRUNC(${Prisma.raw(`'${safePeriod}'`)}, date) AS "startOfPeriod",

🏁 Script executed:

#!/bin/bash
# Description: Verify zodPeriod validation only allows safe period values

# Find the zodPeriod definition
ast-grep --pattern 'export const zodPeriod = $$$'

# Also search for any z.enum or z.literal that might define allowed periods
rg -n -A5 'zodPeriod' --type=ts

Length of output: 2783


Alias inconsistency on line 58: unquoted unread should be quoted.

Line 53's use of Prisma.raw() with the period parameter is safe because zodPeriod enforces a strict whitelist via z.enum(["day", "week", "month", "year"]). This Zod validation prevents SQL injection.

However, line 58 uses an unquoted alias AS unread while all other aliases are quoted (e.g., "startOfPeriod", "totalCount"). Ensure consistency by quoting this alias: AS "unread".

🤖 Prompt for AI Agents
In apps/web/app/api/user/stats/by-period/route.ts around lines 52 to 64, the SQL
SELECT uses an unquoted alias "unread" as AS unread while all other aliases are
quoted; update the query to use a quoted alias (AS "unread") to match the
existing alias style and maintain consistency across the SELECT clause.

Comment on lines +6 to +9
const hasSubdomain = domain.split(".").length > 2;
const apexDomain = hasSubdomain
? domain.split(".").slice(1).join(".")
: domain;
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 | 🟡 Minor

Subdomain detection logic is oversimplified.

The current logic assumes any domain with more than 2 parts (split by ".") has a subdomain, but this doesn't account for:

  • Country-code TLDs (e.g., example.co.uk would be treated as having a subdomain)
  • Single-part domains or invalid formats

Consider using a more robust approach or add validation:

 function getFavicon(domain: string) {
+  // Handle invalid or empty domains
+  if (!domain || !domain.includes('.')) {
+    return null;
+  }
+  
   const hasSubdomain = domain.split(".").length > 2;
   const apexDomain = hasSubdomain
     ? domain.split(".").slice(1).join(".")
     : domain;
   return `https://t1.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=http://${apexDomain}&size=64`;
 }

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

🤖 Prompt for AI Agents
In apps/web/components/charts/DomainIcon.tsx around lines 6–9, the current
subdomain detection (splitting on ".") misclassifies domains with multi-part
TLDs (e.g., example.co.uk) and doesn't validate input; replace the naive split
with a proper domain parse using a public suffix aware library (e.g., tldts or
psl): validate the domain string first (non-empty, contains a dot), use the
library to extract hostname, domain (apex) and subdomain, and then derive
hasSubdomain from the parsed subdomain being non-empty; add a fallback that
treats malformed or single-label hosts as no subdomain to avoid runtime errors.

Comment on lines +6 to +12
export interface HorizontalBarChartItem {
name: string;
value: number;
href?: string;
target?: string;
icon?: string;
}
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

Fix type mismatch for icon property.

The icon property is typed as string but is rendered as JSX content at line 48 (<span className="text-base">{item.icon}</span>). This works for emoji strings but the typing doesn't reflect that it's intended for text-based icons rather than React components.

Apply this diff to clarify the type:

 export interface HorizontalBarChartItem {
   name: string;
   value: number;
   href?: string;
   target?: string;
-  icon?: string;
+  icon?: React.ReactNode;
 }

Or, if you specifically want only emoji/text strings, add a comment to clarify the intent:

 export interface HorizontalBarChartItem {
   name: string;
   value: number;
   href?: string;
   target?: string;
+  // Icon should be an emoji or text character, not a React component
   icon?: string;
 }
📝 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
export interface HorizontalBarChartItem {
name: string;
value: number;
href?: string;
target?: string;
icon?: string;
}
export interface HorizontalBarChartItem {
name: string;
value: number;
href?: string;
target?: string;
icon?: React.ReactNode;
}
🤖 Prompt for AI Agents
In apps/web/components/charts/HorizontalBarChart.tsx around lines 6–12, the icon
field is typed as string but is rendered as JSX at line 48; change the type to
reflect that it can be rendered (e.g., icon?: React.ReactNode or icon?:
JSX.Element | string) and update imports if needed (import React or the
appropriate JSX types), or if you intend icon to be strictly text/emoji, keep
string but add a clarifying comment and ensure usage only passes strings; update
the interface accordingly and adjust any callers to match the new type.

Comment on lines +81 to +92
if (onItemClick) {
return (
<button
key={item.name}
type="button"
className="w-full flex items-center justify-between gap-4 group"
onClick={() => onItemClick(item)}
>
{content}
</button>
);
}
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 | 🟡 Minor

Consider adding aria-label for better accessibility.

The interactive button lacks an explicit accessible label. While the button contains text content (item.name), adding an aria-label would make the button's purpose clearer to screen reader users.

Apply this diff to improve accessibility:

 <button
   key={item.name}
   type="button"
   className="w-full flex items-center justify-between gap-4 group"
   onClick={() => onItemClick(item)}
+  aria-label={`View details for ${item.name}`}
 >
   {content}
 </button>
📝 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
if (onItemClick) {
return (
<button
key={item.name}
type="button"
className="w-full flex items-center justify-between gap-4 group"
onClick={() => onItemClick(item)}
>
{content}
</button>
);
}
if (onItemClick) {
return (
<button
key={item.name}
type="button"
className="w-full flex items-center justify-between gap-4 group"
onClick={() => onItemClick(item)}
aria-label={`View details for ${item.name}`}
>
{content}
</button>
);
}
🤖 Prompt for AI Agents
In apps/web/components/charts/HorizontalBarChart.tsx around lines 81 to 92, the
interactive button rendered for each item lacks an explicit accessible label;
add an aria-label attribute to the button (e.g., aria-label={`Select
${item.name}`} or a more descriptive localized string) so screen readers have a
clear, unique label for each button while keeping the existing onClick, key, and
visual content unchanged.

Comment on lines +23 to +31
<Button
variant={isSelected ? "ghost" : "ghost"}
size="sm"
className={cn(
"text-left justify-start",
isSelected ? "font-bold" : "font-normal",
)}
onClick={() => onSelect?.(item)}
key={item.value}
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 | 🟡 Minor

Remove redundant ternary operator.

Line 24 uses the same "ghost" variant for both selected and unselected states. Either remove the ternary or use different variants to distinguish selection state more clearly beyond just font weight.

Apply this diff to simplify:

           <Button
-            variant={isSelected ? "ghost" : "ghost"}
+            variant="ghost"
             size="sm"
             className={cn(
               "text-left justify-start",
               isSelected ? "font-bold" : "font-normal",
             )}
-            onClick={() => onSelect?.(item)}
+            onClick={() => onSelect(item)}
             key={item.value}
           >

Note: Also removed optional chaining on onSelect since it's a required prop.

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

🤖 Prompt for AI Agents
In apps/web/components/List.tsx around lines 23 to 31, the Button's variant prop
uses a redundant ternary that always returns "ghost" and the onClick uses
optional chaining on onSelect even though onSelect is required; remove the
ternary and set variant="ghost" directly (or pick a different variant for
selected state if you want visual distinction), and remove the optional chaining
so onClick calls onSelect(item) directly.

className={cn(
tabSelectButtonVariants({ variant }),
target === "_blank" && "group flex items-center gap-1.5",
isSelected ? "" : "text-gray-500 hover:text-gray-700",
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 | 🟡 Minor

Redundant color styling conflicts with variant system.

The conditional text color isSelected ? "" : "text-gray-500 hover:text-gray-700" duplicates and potentially conflicts with the variant's existing color handling on lines 22-24, which already manages selected/unselected states via data-[selected=true] and data-[selected=false] pseudo-classes.

Consider removing this line to rely on the variant system:

                  tabSelectButtonVariants({ variant }),
                  target === "_blank" && "group flex items-center gap-1.5",
-                  isSelected ? "" : "text-gray-500 hover:text-gray-700",
                )}
📝 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
isSelected ? "" : "text-gray-500 hover:text-gray-700",
tabSelectButtonVariants({ variant }),
target === "_blank" && "group flex items-center gap-1.5",
)}
🤖 Prompt for AI Agents
In apps/web/components/TabSelect.tsx around line 77, the inline conditional
color classes `isSelected ? "" : "text-gray-500 hover:text-gray-700"` duplicate
and conflict with the component's variant-driven color handling
(data-[selected=true]/[false] on lines ~22-24); remove that conditional from the
className expression so the variant pseudo-class rules control
selected/unselected colors, and adjust the className concatenation to avoid
leaving extra spaces or empty strings (ensure classNames/utils call still
receives only deliberate class tokens).

border: "hsl(var(--sidebar-border))",
ring: "hsl(var(--sidebar-ring))",
},
// TODO: rename
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

Address the TODO: Rename the "new" color group.

The TODO comment indicates this color group needs a proper name before this PR is merged. A generic "new" namespace will cause confusion and maintenance issues.

Please provide a descriptive name that reflects the purpose of this color palette (e.g., "brand", "analytics", "dashboard", etc.).

🤖 Prompt for AI Agents
In apps/web/tailwind.config.js around line 96 the color group currently named
"new" must be renamed to a descriptive name (e.g., "brand", "dashboard",
"analytics") to avoid ambiguity; pick the name that matches this palette's
purpose, replace the "new" key in the tailwind config with that chosen name, and
then update all code references (class names, theme lookups, tests, and docs) to
use the new key (perform a repo-wide search-and-replace for "new:" and "new-"
style usages and run the build/test to verify no breakages).

Copy link

Choose a reason for hiding this comment

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

Issue on line in apps/web/components/new-landing/common/Anchor.tsx:15:

Opening links with target="_blank" without rel="noopener noreferrer" enables tabnabbing. Consider adding rel="noopener noreferrer" whenever opening a new tab.

Suggested change
target={newTab ? "_blank" : undefined}
rel={newTab ? "noopener noreferrer" : undefined}
className={cn("underline", className)}

🚀 Reply to ask Macroscope to explain or update this suggestion.

👍 Helpful? React to give us feedback.

onSetDateDropdown,
}: DatePickerWithRangeProps) {
const now = useMemo(() => new Date(), []);
const days =
Copy link

Choose a reason for hiding this comment

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

All shows when dateRange is undefined. Consider only computing relativeDateLabel when both from and to are set, so the button falls back to Pick a date.

-      : 0;
-  const relativeDateLabel = getRelativeDateLabel(days);
+      : undefined;
+  const relativeDateLabel = days != null ? getRelativeDateLabel(days) : undefined;

🚀 Reply to ask Macroscope to explain or update this suggestion.

👍 Helpful? React to give us feedback.

hideIcon={hideIcons}
/>
<div className="absolute w-full left-0 bottom-0 pb-6 z-30">
{tabs.find((d) => d.id === selected)?.data.length > 0 && (
Copy link

Choose a reason for hiding this comment

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

tabs.find(... )?.data.length can crash when no tab matches because .length is read on undefined. Consider guarding the result, e.g., compare (tabs.find(... )?.data?.length ?? 0) > 0.

Suggested change
{tabs.find((d) => d.id === selected)?.data.length > 0 && (
{(tabs.find((d) => d.id === selected)?.data?.length ?? 0) > 0 && (

🚀 Reply to ask Macroscope to explain or update this suggestion.

👍 Helpful? React to give us feedback.

/>
</ActionBar>
<div className="grid gap-2 sm:gap-4 mt-2 sm:mt-4">
<StatsSummary
Copy link

Choose a reason for hiding this comment

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

Selecting All should clear the date filter. Passing a now‑to‑now dateRange constrains results; consider passing undefined when All is selected so queries fetch all time and the UI stays consistent.

🚀 Reply to ask Macroscope to explain or update this suggestion.

👍 Helpful? React to give us feedback.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

10 issues found across 56 files

Prompt for AI agents (all 10 issues)

Understand the root cause of the following 10 issues and fix them.


<file name="apps/web/app/(app)/[emailAccountId]/assistant/RulesSelect.tsx">

<violation number="1" location="apps/web/app/(app)/[emailAccountId]/assistant/RulesSelect.tsx:16">
Mark the component as a client component before using `useSearchParams`/`useRouter`; otherwise Next.js will crash because these hooks are client-only.</violation>
</file>

<file name="apps/web/components/DatePickerWithRange.tsx">

<violation number="1" location="apps/web/components/DatePickerWithRange.tsx:44">
Memoizing the current date causes every quick-select range to be anchored to the initial render time, so choosing a preset later yields an incorrect range.</violation>
</file>

<file name="apps/web/components/charts/HorizontalBarChart.tsx">

<violation number="1" location="apps/web/components/charts/HorizontalBarChart.tsx:52">
Avoid rendering an &lt;a&gt; when the row is wrapped in a &lt;button&gt;; the current structure nests interactive elements and causes double activation.</violation>

<violation number="2" location="apps/web/components/charts/HorizontalBarChart.tsx:55">
Links that can open in a new tab need `rel=&quot;noopener noreferrer&quot;` to prevent reverse-tabnabbing when `target=&quot;_blank&quot;`.</violation>
</file>

<file name="apps/web/app/(app)/[emailAccountId]/stats/MainStatChart.tsx">

<violation number="1" location="apps/web/app/(app)/[emailAccountId]/stats/MainStatChart.tsx:38">
Storing the x-axis date as a timezone-less `yyyy-MM-dd` string makes browsers parse it as UTC, so chart labels shift by a day for users outside UTC. Preserve the timestamp (e.g., keep the ISO string with timezone) before passing it to `NewBarChart`.</violation>
</file>

<file name="apps/web/app/(app)/[emailAccountId]/stats/Stats.tsx">

<violation number="1" location="apps/web/app/(app)/[emailAccountId]/stats/Stats.tsx:122">
EmailAnalytics is no longer gated by `isAccountOwner`, so non‑owners can now fetch and view another mailbox’s sender/recipient analytics. Wrap this component back in the ownership check to prevent exposing private stats or triggering unauthorized API calls.</violation>
</file>

<file name="apps/web/app/(app)/[emailAccountId]/bulk-unsubscribe/BulkUnsubscribeSection.tsx">

<violation number="1" location="apps/web/app/(app)/[emailAccountId]/bulk-unsubscribe/BulkUnsubscribeSection.tsx:250">
Wrapping all of the bulk-unsubscribe toolbar controls inside ActionBar removes the flex-wrap that previously let them stack, so on mobile/tablet widths the toggle, search bar, filter, date range picker, and Load Stats button overflow and become unusable.</violation>
</file>

<file name="apps/web/app/(app)/[emailAccountId]/bulk-unsubscribe/BulkUnsubscribeDesktop.tsx">

<violation number="1" location="apps/web/app/(app)/[emailAccountId]/bulk-unsubscribe/BulkUnsubscribeDesktop.tsx:128">
The new `&lt;Progress&gt;` bars never announce their percentage because the Progress component discards the `value` prop, so Radix treats them as indeterminate; forward the `value` (and max) to `ProgressPrimitive.Root` before using it here to avoid the accessibility regression.</violation>
</file>

<file name="apps/web/components/charts/DomainIcon.tsx">

<violation number="1" location="apps/web/components/charts/DomainIcon.tsx:57">
Reset the `fallbackEnabled` state when the `domain` prop changes so that a new favicon is attempted instead of permanently showing the fallback after the first error.</violation>
</file>

<file name="apps/web/app/(app)/[emailAccountId]/stats/NewBarChart.tsx">

<violation number="1" location="apps/web/app/(app)/[emailAccountId]/stats/NewBarChart.tsx:99">
Guard the tooltip header so it only applies date formatting when the x-axis value can be parsed as a valid date, otherwise fall back to the raw value to avoid showing “Invalid Date” for categorical charts.</violation>
</file>

Reply to cubic to teach it or ask questions. Re-run a review with @cubic-dev-ai review this PR


export function RulesSelect() {
const { data, isLoading, error } = useRules();
const searchParams = useSearchParams();
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Nov 24, 2025

Choose a reason for hiding this comment

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

Mark the component as a client component before using useSearchParams/useRouter; otherwise Next.js will crash because these hooks are client-only.

Prompt for AI agents
Address the following comment on apps/web/app/(app)/[emailAccountId]/assistant/RulesSelect.tsx at line 16:

<comment>Mark the component as a client component before using `useSearchParams`/`useRouter`; otherwise Next.js will crash because these hooks are client-only.</comment>

<file context>
@@ -1,28 +1,69 @@
 
 export function RulesSelect() {
   const { data, isLoading, error } = useRules();
+  const searchParams = useSearchParams();
+  const router = useRouter();
+  const currentValue = searchParams.get(&quot;ruleId&quot;) || &quot;all&quot;;
</file context>
Fix with Cubic

dateDropdown,
onSetDateDropdown,
}: DatePickerWithRangeProps) {
const now = useMemo(() => new Date(), []);
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Nov 24, 2025

Choose a reason for hiding this comment

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

Memoizing the current date causes every quick-select range to be anchored to the initial render time, so choosing a preset later yields an incorrect range.

Prompt for AI agents
Address the following comment on apps/web/components/DatePickerWithRange.tsx at line 44:

<comment>Memoizing the current date causes every quick-select range to be anchored to the initial render time, so choosing a preset later yields an incorrect range.</comment>

<file context>
@@ -13,48 +12,97 @@ import {
+  dateDropdown,
+  onSetDateDropdown,
+}: DatePickerWithRangeProps) {
+  const now = useMemo(() =&gt; new Date(), []);
+  const days =
+    dateRange?.from &amp;&amp; dateRange?.to
</file context>
Fix with Cubic

{item.href ? (
<a
href={item.href}
target={item.target}
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Nov 24, 2025

Choose a reason for hiding this comment

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

Links that can open in a new tab need rel="noopener noreferrer" to prevent reverse-tabnabbing when target="_blank".

Prompt for AI agents
Address the following comment on apps/web/components/charts/HorizontalBarChart.tsx at line 55:

<comment>Links that can open in a new tab need `rel=&quot;noopener noreferrer&quot;` to prevent reverse-tabnabbing when `target=&quot;_blank&quot;`.</comment>

<file context>
@@ -0,0 +1,105 @@
+                  {item.href ? (
+                    &lt;a
+                      href={item.href}
+                      target={item.target}
+                      className=&quot;text-sm text-gray-900 truncate block z-10 relative hover:underline&quot;
+                    &gt;
</file context>
Fix with Cubic

) : (
<DomainIcon domain={domain} />
))}
{item.href ? (
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Nov 24, 2025

Choose a reason for hiding this comment

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

Avoid rendering an when the row is wrapped in a ; the current structure nests interactive elements and causes double activation.

Prompt for AI agents
Address the following comment on apps/web/components/charts/HorizontalBarChart.tsx at line 52:

<comment>Avoid rendering an &lt;a&gt; when the row is wrapped in a &lt;button&gt;; the current structure nests interactive elements and causes double activation.</comment>

<file context>
@@ -0,0 +1,105 @@
+                    ) : (
+                      &lt;DomainIcon domain={domain} /&gt;
+                    ))}
+                  {item.href ? (
+                    &lt;a
+                      href={item.href}
</file context>
Fix with Cubic

const chartData = React.useMemo(() => {
return props.data.result.map((item) => {
const date = parse(item.startOfPeriod, "MMM dd, yyyy", new Date());
const dateStr = format(date, "yyyy-MM-dd");
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Nov 24, 2025

Choose a reason for hiding this comment

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

Storing the x-axis date as a timezone-less yyyy-MM-dd string makes browsers parse it as UTC, so chart labels shift by a day for users outside UTC. Preserve the timestamp (e.g., keep the ISO string with timezone) before passing it to NewBarChart.

Prompt for AI agents
Address the following comment on apps/web/app/(app)/[emailAccountId]/stats/MainStatChart.tsx at line 38:

<comment>Storing the x-axis date as a timezone-less `yyyy-MM-dd` string makes browsers parse it as UTC, so chart labels shift by a day for users outside UTC. Preserve the timestamp (e.g., keep the ISO string with timezone) before passing it to `NewBarChart`.</comment>

<file context>
@@ -0,0 +1,104 @@
+  const chartData = React.useMemo(() =&gt; {
+    return props.data.result.map((item) =&gt; {
+      const date = parse(item.startOfPeriod, &quot;MMM dd, yyyy&quot;, new Date());
+      const dateStr = format(date, &quot;yyyy-MM-dd&quot;);
+
+      return {
</file context>
Suggested change
const dateStr = format(date, "yyyy-MM-dd");
const dateStr = date.toISOString();
Fix with Cubic

refreshInterval={refreshInterval}
period={period}
/>
<EmailAnalytics
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Nov 24, 2025

Choose a reason for hiding this comment

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

EmailAnalytics is no longer gated by isAccountOwner, so non‑owners can now fetch and view another mailbox’s sender/recipient analytics. Wrap this component back in the ownership check to prevent exposing private stats or triggering unauthorized API calls.

Prompt for AI agents
Address the following comment on apps/web/app/(app)/[emailAccountId]/stats/Stats.tsx at line 122:

<comment>EmailAnalytics is no longer gated by `isAccountOwner`, so non‑owners can now fetch and view another mailbox’s sender/recipient analytics. Wrap this component back in the ownership check to prevent exposing private stats or triggering unauthorized API calls.</comment>

<file context>
@@ -71,63 +70,68 @@ export function Stats() {
+          refreshInterval={refreshInterval}
           period={period}
+        /&gt;
+        &lt;EmailAnalytics
+          dateRange={dateRange}
           refreshInterval={refreshInterval}
</file context>
Fix with Cubic

)
}
/>
<ActionBar rightContent={<LoadStatsButton />}>
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Nov 24, 2025

Choose a reason for hiding this comment

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

Wrapping all of the bulk-unsubscribe toolbar controls inside ActionBar removes the flex-wrap that previously let them stack, so on mobile/tablet widths the toggle, search bar, filter, date range picker, and Load Stats button overflow and become unusable.

Prompt for AI agents
Address the following comment on apps/web/app/(app)/[emailAccountId]/bulk-unsubscribe/BulkUnsubscribeSection.tsx at line 250:

<comment>Wrapping all of the bulk-unsubscribe toolbar controls inside ActionBar removes the flex-wrap that previously let them stack, so on mobile/tablet widths the toggle, search bar, filter, date range picker, and Load Stats button overflow and become unusable.</comment>

<file context>
@@ -246,34 +247,34 @@ export function BulkUnsubscribe() {
-                )
-              }
-            /&gt;
+        &lt;ActionBar rightContent={&lt;LoadStatsButton /&gt;}&gt;
+          &lt;div className=&quot;flex items-center justify-end gap-1&quot;&gt;
+            &lt;div className=&quot;&quot;&gt;
</file context>
Fix with Cubic

<div className="flex items-center gap-4">
<Tooltip>
<TooltipTrigger asChild>
<Progress value={readPercentage} className="h-2 w-[150px]" />
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Nov 24, 2025

Choose a reason for hiding this comment

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

The new <Progress> bars never announce their percentage because the Progress component discards the value prop, so Radix treats them as indeterminate; forward the value (and max) to ProgressPrimitive.Root before using it here to avoid the accessibility regression.

Prompt for AI agents
Address the following comment on apps/web/app/(app)/[emailAccountId]/bulk-unsubscribe/BulkUnsubscribeDesktop.tsx at line 128:

<comment>The new `&lt;Progress&gt;` bars never announce their percentage because the Progress component discards the `value` prop, so Radix treats them as indeterminate; forward the `value` (and max) to `ProgressPrimitive.Root` before using it here to avoid the accessibility regression.</comment>

<file context>
@@ -117,27 +122,36 @@ export function BulkUnsubscribeRowDesktop({
+          &lt;div className=&quot;flex items-center gap-4&quot;&gt;
+            &lt;Tooltip&gt;
+              &lt;TooltipTrigger asChild&gt;
+                &lt;Progress value={readPercentage} className=&quot;h-2 w-[150px]&quot; /&gt;
+              &lt;/TooltipTrigger&gt;
+              &lt;TooltipContent&gt;
</file context>
Fix with Cubic


export function DomainIcon({ domain }: DomainIconProps) {
const domainFavicon = getFavicon(domain);
const [fallbackEnabled, setFallbackEnabled] = useState(false);
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Nov 24, 2025

Choose a reason for hiding this comment

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

Reset the fallbackEnabled state when the domain prop changes so that a new favicon is attempted instead of permanently showing the fallback after the first error.

Prompt for AI agents
Address the following comment on apps/web/components/charts/DomainIcon.tsx at line 57:

<comment>Reset the `fallbackEnabled` state when the `domain` prop changes so that a new favicon is attempted instead of permanently showing the fallback after the first error.</comment>

<file context>
@@ -0,0 +1,75 @@
+
+export function DomainIcon({ domain }: DomainIconProps) {
+  const domainFavicon = getFavicon(domain);
+  const [fallbackEnabled, setFallbackEnabled] = useState(false);
+
+  return (
</file context>
Fix with Cubic

content={({ active, payload }) => {
if (!active || !payload?.length) return null;
const data = payload[0];
const date = new Date(data.payload[xAxisKey]);
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Nov 24, 2025

Choose a reason for hiding this comment

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

Guard the tooltip header so it only applies date formatting when the x-axis value can be parsed as a valid date, otherwise fall back to the raw value to avoid showing “Invalid Date” for categorical charts.

Prompt for AI agents
Address the following comment on apps/web/app/(app)/[emailAccountId]/stats/NewBarChart.tsx at line 99:

<comment>Guard the tooltip header so it only applies date formatting when the x-axis value can be parsed as a valid date, otherwise fall back to the raw value to avoid showing “Invalid Date” for categorical charts.</comment>

<file context>
@@ -0,0 +1,152 @@
+          content={({ active, payload }) =&gt; {
+            if (!active || !payload?.length) return null;
+            const data = payload[0];
+            const date = new Date(data.payload[xAxisKey]);
+
+            let dateFormat: Intl.DateTimeFormatOptions;
</file context>
Fix with Cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants