Conversation
* feat: navbar * feat: hero * feat: trusted by * feat: cards * feat: desc & faq * feat: footer svg * feat: footer & testimonials * feat: landing * fix: on click btns * fix: responsive * feat: light mode, dark mode, some icon fixes * feat: pricing page * feat: better pricing page * trusted by more people bro * feat: cleanup --------- Co-authored-by: Hyteq <DerpsDer.es@gmail.com>
|
The latest updates on your projects. Learn more about Vercel for Git ↗︎ 1 Skipped Deployment
|
|
Warning Rate limit exceeded@izadoesdev has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 0 minutes and 51 seconds before requesting another review. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📒 Files selected for processing (1)
WalkthroughThis update introduces new pricing, plan normalization, and estimator utilities for the documentation site, including a modular, interactive pricing page. It removes the old pricing comparison implementation and related components, restructures the landing page with new sections and visual elements, and overhauls global styles for improved responsiveness and theming. Database schema files are deleted, and API, dashboard, and DB packages receive targeted enhancements for type safety, plugin configuration, and cache handling. Changes
Sequence Diagram(s)Pricing Estimator FlowsequenceDiagram
participant User
participant PricingPage
participant Estimator
participant PlanNormalizer
participant BestPlanSelector
participant OverageCalculator
User->>PricingPage: Loads page
PricingPage->>PlanNormalizer: normalizePlans(rawPlans)
PlanNormalizer-->>PricingPage: NormalizedPlan[]
PricingPage->>BestPlanSelector: selectBestPlan(monthlyEvents, plans)
BestPlanSelector-->>PricingPage: bestPlan
PricingPage->>OverageCalculator: estimateTieredOverageCostFromTiers(events, bestPlan.eventTiers)
OverageCalculator-->>PricingPage: estimatedOverage
PricingPage->>Estimator: Render with props (events, bestPlan, estimatedOverage)
User->>Estimator: Adjusts event count
Estimator->>PricingPage: setMonthlyEvents(newValue)
loop On event count change
PricingPage->>BestPlanSelector: selectBestPlan(newEvents, plans)
PricingPage->>OverageCalculator: estimateTieredOverageCostFromTiers(newEvents, bestPlan.eventTiers)
PricingPage->>Estimator: Update props
end
Mini-Charts API Query FlowsequenceDiagram
participant Client
participant miniChartsRouter
participant Auth
participant DataFetcher
Client->>miniChartsRouter: getMiniCharts({ websiteIds, days })
miniChartsRouter->>miniChartsRouter: normalizeWebsiteIds(websiteIds)
miniChartsRouter->>Auth: getAuthorizedWebsiteIds(user, normalizedIds)
Auth-->>miniChartsRouter: authorizedIds
miniChartsRouter->>DataFetcher: getBatchedMiniChartData(authorizedIds, days)
DataFetcher-->>miniChartsRouter: chartData[]
miniChartsRouter-->>Client: chartData[]
Estimated code review effort🎯 5 (Critical) | ⏱️ ~90+ minutes Possibly related PRs
✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
Dependency ReviewThe following issues were found:
License Issuesapps/docs/package.json
OpenSSF Scorecard
Scanned Files
|
| if (href.includes('github')) { | ||
| return <FaGithub className="h-4 w-4" color="black" />; | ||
| } | ||
| if (href.includes('x.com') || item.title.toLowerCase().startsWith('x(')) { |
Check failure
Code scanning / CodeQL
Incomplete URL substring sanitization High documentation
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 7 months ago
To fix the problem, we should parse the URL and check the host component to see if it matches 'x.com' or 'www.x.com'. This avoids false positives from substring matches in other parts of the URL or in unrelated domains. The best way to do this in a React/TypeScript context is to use the built-in URL class, which is available in modern browsers and Node.js. We should update the check in getIconForLink to parse the URL (if possible) and compare the host directly. If parsing fails (e.g., for non-HTTP URLs), we can fall back to the original logic or handle gracefully.
Required changes:
- In
getIconForLink, replace thehref.includes('x.com')check with a check that parses the URL and compares the host to'x.com'or'www.x.com'. - No new imports are needed, as
URLis globally available in the browser and Node.js. - No changes to other files or logic.
| @@ -18,7 +18,15 @@ | ||
| if (href.includes('github')) { | ||
| return <FaGithub className="h-4 w-4" color="black" />; | ||
| } | ||
| if (href.includes('x.com') || item.title.toLowerCase().startsWith('x(')) { | ||
| let isXCom = false; | ||
| try { | ||
| const urlObj = new URL(item.link, 'http://dummy-base/'); | ||
| isXCom = urlObj.host === 'x.com' || urlObj.host === 'www.x.com'; | ||
| } catch (e) { | ||
| // If parsing fails, fallback to original substring check (for mailto, etc.) | ||
| isXCom = href.includes('x.com'); | ||
| } | ||
| if (isXCom || item.title.toLowerCase().startsWith('x(')) { | ||
| return <FaXTwitter className="h-4 w-4" color="black" />; | ||
| } | ||
| return null; |
| {testimonial.profession} | ||
| </p> | ||
| }) { | ||
| const socialIcon = testimonial.link?.includes('x.com') ? ( |
Check failure
Code scanning / CodeQL
Incomplete URL substring sanitization High documentation test
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 7 months ago
The best way to fix this problem is to parse the URL and check the host component explicitly, rather than using a substring match. In this case, we want to show the XLogoIcon only if the host is exactly x.com (or possibly www.x.com, if desired). To do this, we should use the standard URL constructor to parse the link, and then check if the host property matches x.com or any allowed value. This change should be made in the TestimonialCardContent function, specifically where the icon is conditionally rendered. We will need to handle cases where the URL is invalid or missing gracefully. No new dependencies are needed, as the URL class is available in modern JavaScript/TypeScript environments.
| @@ -101,14 +101,25 @@ | ||
| }: { | ||
| testimonial: (typeof testimonials)[number]; | ||
| }): ReactElement { | ||
| const socialIcon = testimonial.link?.includes('x.com') ? ( | ||
| <span | ||
| aria-hidden | ||
| className="text-muted-foreground transition-colors duration-300 group-hover:text-foreground" | ||
| > | ||
| <XLogoIcon className="h-5 w-5" weight="duotone" /> | ||
| </span> | ||
| ) : null; | ||
| let socialIcon = null; | ||
| if (testimonial.link) { | ||
| try { | ||
| const urlObj = new URL(testimonial.link); | ||
| // Only show icon if host is exactly 'x.com' or 'www.x.com' | ||
| if (urlObj.host === 'x.com' || urlObj.host === 'www.x.com') { | ||
| socialIcon = ( | ||
| <span | ||
| aria-hidden | ||
| className="text-muted-foreground transition-colors duration-300 group-hover:text-foreground" | ||
| > | ||
| <XLogoIcon className="h-5 w-5" weight="duotone" /> | ||
| </span> | ||
| ); | ||
| } | ||
| } catch (e) { | ||
| // Invalid URL, do nothing | ||
| } | ||
| } | ||
|
|
||
| return ( | ||
| <div className="group relative flex h-[200px] w-[300px] shrink-0 flex-col justify-between rounded border border-border bg-card/70 shadow-inner backdrop-blur-sm transition-all duration-300 hover:border-border/80 hover:shadow-primary/10 sm:h-[220px] sm:w-[350px] lg:h-[240px] lg:w-[400px]"> |
There was a problem hiding this comment.
Actionable comments posted: 76
🔭 Outside diff range comments (23)
apps/dashboard/app/(main)/websites/[id]/sessions/_components/session-row.tsx (1)
207-227: Remove commented-out Custom events blockDead commented JSX adds noise; delete if not needed or guard with a flag.
apps/dashboard/components/analytics/data-table.tsx (5)
1193-1211: Use rounded instead of rounded-2xl on modal containerStandardize border radius per repo rules.
- <div className="relative flex h-[92vh] w-[92vw] scale-100 animate-scalein flex-col overflow-hidden rounded-2xl border border-border bg-background shadow-2xl"> + <div className="relative flex h-[92vh] w-[92vw] scale-100 animate-scalein flex-col overflow-hidden rounded border border-border bg-background shadow-2xl">
13-19: Replace lucide icons with Phosphor icons (repo rule)This file imports icons from lucide-react; the project mandates using @phosphor-icons/react. Replace ArrowDown, ArrowUp, ArrowUpDown, DatabaseIcon, Search, X with their Phosphor counterparts (e.g., ArrowDownIcon, ArrowUpIcon, ArrowsUpDownIcon, DatabaseIcon, MagnifyingGlassIcon, XIcon) and prefer weight="fill" for arrows, weight="duotone" otherwise.
873-878: Remove incorrect role="tablist" on table containerThe table container div isn’t a tablist; the actual tablist is rendered above. Drop the role to avoid a11y confusion.
- <div - className={cn( - 'relative transition-all duration-300 ease-out', - isTransitioning && 'scale-[0.98] opacity-40' - )} - onMouseLeave={handleMouseLeave} - onMouseMove={handleMouseMove} - ref={tableContainerRef} - role="tablist" - > + <div + className={cn( + 'relative transition-all duration-300 ease-out', + isTransitioning && 'scale-[0.98] opacity-40' + )} + onMouseLeave={handleMouseLeave} + onMouseMove={handleMouseMove} + ref={tableContainerRef} + >
390-406: Make sortable headers buttons for accessibilityAvoid adding role="button" and tabIndex to th elements. Wrap header content in a button, keep aria-sort on th. This improves semantics and keyboard focus.
Also applies to: 941-955
123-152: Border radius consistency in SkeletonsStandardize rounded-md/rounded-lg/rounded-sm/full usages to rounded to comply with styling rules.
apps/dashboard/public/databuddy.ts (1)
1347-1350: Removeconsole.logstatements in production bundle.Unconditional console output breaches the “Don’t use console.” rule and can leak implementation details.
Drop these logs or gate them behind a debug flag.Also applies to: 1363-1365
apps/dashboard/components/layout/logo.tsx (1)
30-38: Add a<title>for accessibility.Inline SVG should include a
<title>element so screen-reader users hear “Databuddy logo” (or similar).<svg aria-hidden="true" + role="img" … > + <title>Databuddy logo</title>apps/dashboard/app/(main)/organizations/[slug]/components/website-selector.tsx (1)
18-23: Useroundednotrounded-lgProject style-guide mandates the generic
roundedclass exclusively. Replacerounded-lgto keep radius consistent across the UI.- 'flex w-full items-center gap-3 rounded-lg border p-3 text-left transition-all duration-200', + 'flex w-full items-center gap-3 rounded border p-3 text-left transition-all duration-200',apps/dashboard/app/demo/layout.tsx (1)
151-158: Interactive backdrop is hidden from assistive tech
aria-hidden="true"combined with click / key handlers makes this element inaccessible. Either:
- Remove
aria-hiddenand addrole="button" tabIndex={0}(plus appropriate aria-label), or- Replace with an actual
<button>element.- <div - aria-hidden="true" - className="fixed inset-0 z-30 bg-black/20 md:hidden" - onClick={closeSidebar} - onKeyDown={closeSidebar} - onKeyPress={closeSidebar} - onKeyUp={closeSidebar} - /> + <div + role="button" + aria-label="Close sidebar" + tabIndex={0} + className="fixed inset-0 z-30 bg-black/20 md:hidden" + onClick={closeSidebar} + onKeyDown={closeSidebar} + onKeyPress={closeSidebar} + onKeyUp={closeSidebar} + />apps/docs/package.json (1)
44-44: Replace date-fns with Dayjs.Guideline: “Use Dayjs NEVER date-fns.” Remove
date-fnsand migrate todayjsfor date utilities.I can help generate a codemod/checklist to migrate the few helpers typically used in docs (format, parseISO, addDays, etc.).
apps/dashboard/app/(main)/organizations/[slug]/components/member-list.tsx (1)
45-52: Type mismatch:onRemoveMemberis awaited but typed as sync.
handleRemoveusesawait onRemoveMember(...). Update the prop to returnPromise<void>or dropawait.Suggested fix:
interface MemberListProps { members: OrganizationMember[]; - onRemoveMember: (memberId: string) => void; + onRemoveMember: (memberId: string) => Promise<void>; isRemovingMember: boolean; onUpdateRole: (member: UpdateMemberData) => void; isUpdatingMember: boolean; organizationId: string; }apps/docs/components/footer.tsx (1)
137-143: Don’t usein Next.js; use next/image and whitelist domain.
Replace
<img>withnext/imagefor optimization and policy compliance. Also, addtwelve.toolstoimages.remotePatterns(see next.config comment).Apply:
+import Image from 'next/image'; @@ - <img - alt="Featured on Twelve Tools" - className="h-auto max-w-[150px] sm:max-w-[200px]" - height="40" - src="https://twelve.tools/badge0-white.svg" - width="150" - /> + <Image + alt="Featured on Twelve Tools" + src="https://twelve.tools/badge0-white.svg" + width={150} + height={40} + className="h-auto max-w-[150px] sm:max-w-[200px]" + priority + />apps/dashboard/app/(main)/organizations/[slug]/components/transfer-assets.tsx (2)
192-198: Arrow icons should useweight="fill"per icon guideline.Guideline: “For arrows use fill.” Apply to this overlay indicator.
-<ArrowRightIcon +<ArrowRightIcon + weight="fill" className={`not-dark:text-primary transition-transform duration-1000 ${ transferringWebsite.fromSide === 'organization' ? 'rotate-180' : '' }`} size={18} />Also apply the same to the button arrow (Line 244).
103-105: Normalize border-radius classes torounded(policy).Guideline: “Always use
rounded, notrounded-xl/md/full.” Replacerounded-lgandrounded-fullthroughout this component.Example:
- className="flex items-center gap-3 rounded-lg border ..." + className="flex items-center gap-3 rounded border ..." @@ - className="h-10 w-10 rounded-lg border-2 shadow-sm" + className="h-10 w-10 rounded border-2 shadow-sm" @@ - className="flex items-center gap-2 rounded-full bg-primary/20 px-4 py-2 ..." + className="flex items-center gap-2 rounded bg-primary/20 px-4 py-2 ..."Also applies to: 118-119, 166-171, 190-193, 235-236, 241-251, 279-309
apps/docs/components/navbar.tsx (1)
39-53: SVG is accessible; optionally add aria-hidden and aria-label on linkThe <title> is good. For cleaner AT behavior, either:
- Add aria-label to the enclosing NavLink, or
- Mark the SVG aria-hidden="true" and keep visible text “GitHub”.
-<NavLink external href="https://github.com/databuddy-analytics"> +<NavLink external aria-label="Databuddy GitHub" href="https://github.com/databuddy-analytics"> <svg + aria-hidden="true" className="transition-transform duration-200 hover:scale-110" height="1.4em" viewBox="0 0 496 512" width="1.4em" xmlns="http://www.w3.org/2000/svg" >apps/dashboard/hooks/use-websites.ts (2)
19-23: Disable the query until an organisation ID is available.
enabled: !isLoadingOrganizationstill triggers the query withorganizationId: undefined, causing an unnecessary round-trip. Gate on the id instead:- { enabled: !isLoadingOrganization } + { enabled: !!activeOrganization?.id && !isLoadingOrganization }
64-65: Replaceconsole.errorwith the project’s logger (or remove in production).The coding standards list “Don’t use console.”. Swap to a structured logger or wrap behind a debug flag to keep production output clean.
Also applies to: 92-94
apps/dashboard/app/(main)/websites/page.tsx (1)
60-70: Rounded-size classes violate the style guideThe design guide mandates using only
rounded, neverrounded-lg,rounded-xl, orrounded-full, yet these appear in the highlighted lines.
Replace with the plainroundedclass (and adjust spacing if bigger radii are desired through custom CSS tokens).Also applies to: 102-105, 170-177
apps/dashboard/app/(main)/organizations/[slug]/components/settings-tab.tsx (1)
208-209: Replace allrounded-lgwithroundedThe style guide forbids radius variants; only
roundedis allowed.-className="flex items-center gap-3 rounded-lg border +className="flex items-center gap-3 rounded borderApply the same change to the card rows (Lines 223-225) and the “Delete Organization” button (Line 289).
Also applies to: 223-225, 289-289
apps/dashboard/app/(main)/organizations/page.tsx (1)
275-277: Border-radius variant violates style guideline
rounded-noneis another radius variant that conflicts with the “always userounded” rule.
Use plainrounded(or omit the class if square corners are required and get design sign-off).Also applies to: 285-287
apps/dashboard/app/(main)/websites/_components/mini-chart.tsx (1)
30-93: Chart refinements are solid: accessibility, unique gradient IDs, animations, and rounded corners
- aria-label + <title> improve accessibility.
- “rounded” class complies with UI guideline.
- Unique gradient ID avoids collisions.
- Tooltip content is concise and uses Dayjs.
Optional: If your data never goes negative, consider clamping Y-axis min to 0 to avoid tiny negative padding when dataMin is close to 0.
apps/docs/components/landing/testimonials.tsx (1)
196-208: Cache duplicated testimonials withuseMemo
new Array(15).fill(rowTestimonials).flat()is rebuilt on every render, creating ~15 × N new references each time the parent re-renders. Wrapping this computation inuseMemotied torowTestimonials.length(or simply the array itself) removes needless allocations and keeps keys stable.
| --- | ||
| description: Basic guidelines for the project so vibe coders don't fuck it up | ||
| globs: | ||
| globs: | ||
| alwaysApply: true | ||
| --- |
There was a problem hiding this comment.
🧹 Nitpick (assertive)
Tone nit: make the description professional
Consider rephrasing the description to avoid profanity; this is a shared policy doc.
🤖 Prompt for AI Agents
In .cursor/rules/01-MUST-DO.mdc at lines 1 to 5, the description contains
profanity which is unprofessional for a shared policy document. Rephrase the
description to maintain a professional tone by removing any offensive language
and using neutral, respectful wording that clearly conveys the guidelines.
| globs: | ||
| alwaysApply: true | ||
| --- |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Confirm policy consistency: Zod v4 rule
The broader guidelines still reference “Use ONLY Zod V4 from zod/v4”. If this file intentionally removed or no longer includes that rule, confirm project-wide alignment to avoid mixed enforcement.
You can verify current Zod usage across the repo:
🏁 Script executed:
#!/bin/bash
# Find any imports from legacy zod entrypoint (should be avoided)
rg -n "from ['\"]zod['\"]"
# Find desired zod/v4 imports (should be preferred)
rg -n "from ['\"]zod/v4['\"]"Length of output: 1930
Enforce Zod v4 consistently: restore rule and migrate legacy imports
The rule file .cursor/rules/01-MUST-DO.mdc (lines 3–5) no longer includes the “Use ONLY Zod V4 from zod/v4” policy, yet the repo is mixing legacy import … from 'zod' (v3) and import … from 'zod/v4'. To avoid uneven enforcement:
• Pinpoint the missing rule in .cursor/rules/01-MUST-DO.mdc (it currently only contains globs:/alwaysApply: true).
• Legacy imports found in 17 files, for example:
– packages/validation/src/schemas/analytics.ts
– packages/rpc/src/routers/websites.ts
– apps/dashboard/components/website-dialog.tsx
– …and many more
• Preferred imports already in place in some files (e.g., packages/validation/src/schemas/websites.ts, packages/rpc/src/routers/mini-charts.ts).
Action required:
- Re-add the Zod v4 enforcement rule to
.cursor/rules/01-MUST-DO.mdc. - Migrate all
import … from 'zod'toimport … from 'zod/v4'.
🤖 Prompt for AI Agents
In .cursor/rules/01-MUST-DO.mdc around lines 3 to 5, the enforcement rule
requiring use of only Zod v4 imports is missing. Restore this rule by adding a
policy that enforces importing exclusively from 'zod/v4'. Then, update all
source files that currently import from 'zod' to instead import from 'zod/v4' to
ensure consistent usage of Zod v4 across the codebase.
.cursor/rules/01-MUST-DO.mdc
Outdated
There was a problem hiding this comment.
🧹 Nitpick (assertive)
Duplicate guidance with coding guidelines
“Almost NEVER use useEffect unless it's critical” already exists in the shared rules. Consider referencing the canonical rule to prevent drift.
🤖 Prompt for AI Agents
In .cursor/rules/01-MUST-DO.mdc at line 47, the guidance "Almost NEVER use
useEffect unless it's critical" is duplicated from the shared rules. Remove this
duplicate line and instead add a reference or link to the canonical rule in the
shared rules to avoid redundancy and ensure consistency.
| normalizeGeo?: boolean; | ||
| deduplicateGeo?: boolean; | ||
| mapDeviceTypes?: boolean; | ||
| }; |
There was a problem hiding this comment.
🧹 Nitpick (assertive)
Good tightening of plugin typing; consider extracting a shared PluginFlags type for reuse
Explicit flags improve safety vs index signatures. To keep it DRY across API and utils, extract the shape to a named exported type and reference it here.
Example:
+export type PluginFlags = {
+ parseReferrers?: boolean;
+ normalizeUrls?: boolean;
+ normalizeGeo?: boolean;
+ deduplicateGeo?: boolean;
+ mapDeviceTypes?: boolean;
+};
export interface SimpleQueryConfig {
…
- plugins?: {
- parseReferrers?: boolean;
- normalizeUrls?: boolean;
- normalizeGeo?: boolean;
- deduplicateGeo?: boolean;
- mapDeviceTypes?: boolean;
- };
+ plugins?: PluginFlags;
…
}📝 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.
| normalizeGeo?: boolean; | |
| deduplicateGeo?: boolean; | |
| mapDeviceTypes?: boolean; | |
| }; | |
| // extract and export the shared plugin‐flags shape | |
| export type PluginFlags = { | |
| parseReferrers?: boolean; | |
| normalizeUrls?: boolean; | |
| normalizeGeo?: boolean; | |
| deduplicateGeo?: boolean; | |
| mapDeviceTypes?: boolean; | |
| }; | |
| export interface SimpleQueryConfig { | |
| // … other config properties … | |
| // replace the inline plugin object with the shared PluginFlags type | |
| plugins?: PluginFlags; | |
| // … remaining properties … | |
| } |
🤖 Prompt for AI Agents
In apps/api/src/query/types.ts around lines 41 to 44, the explicit boolean flags
for plugin options should be extracted into a shared named exported type called
PluginFlags. Define this type separately with the same flag properties and then
reference it here to keep the code DRY and reusable across API and utils.
| const confirmDeleteAction = async () => { | ||
| if (!confirmDelete) { | ||
| return; | ||
| } | ||
|
|
||
| setDeletingId(organizationId); | ||
| setDeletingId(confirmDelete.id); | ||
| try { | ||
| deleteOrganization(organizationId); | ||
| } catch (error) { | ||
| console.error('Failed to delete organization:', error); | ||
| await deleteOrganizationAsync(confirmDelete.id); | ||
| } catch (_error) { | ||
| toast.error('Failed to delete organization'); | ||
| } finally { | ||
| setDeletingId(null); | ||
| setConfirmDelete(null); | ||
| } | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Disable dialog’s Delete button while mutation is in-flight.
confirmDeleteAction kicks off deleteOrganizationAsync, but the “Delete” button in the dialog isn’t disabled during the request. Rapid double-clicks can fire multiple deletions. Gate the action with a loading/disabled state tied to isDeletingOrganization or a local isSubmitting.
🤖 Prompt for AI Agents
In apps/dashboard/app/(main)/organizations/components/organizations-tab.tsx
around lines 79 to 92, the Delete button in the confirmation dialog is not
disabled while the deleteOrganizationAsync mutation is in progress, allowing
multiple rapid clicks to trigger duplicate deletions. Introduce a local loading
state variable (e.g., isSubmitting) that is set to true before calling
deleteOrganizationAsync and reset to false in the finally block. Use this state
to disable the Delete button during the mutation to prevent multiple
submissions.
| optimizePackageImports: [ | ||
| 'fumadocs-ui', | ||
| 'lucide-react', | ||
| '@phosphor-icons/react', | ||
| ], |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Remove lucide optimizations; phosphor-only policy.
Guideline: “Don’t use lucide for icons, ONLY use phosphor icons.” Drop lucide-react from optimizePackageImports.
Apply:
experimental: {
- optimizePackageImports: [
- 'fumadocs-ui',
- 'lucide-react',
- '@phosphor-icons/react',
- ],
+ optimizePackageImports: ['fumadocs-ui', '@phosphor-icons/react'],
},To find and remove stray lucide usages:
🏁 Script executed:
#!/bin/bash
rg -n "from 'lucide-react" appsLength of output: 9331
Enforce phosphor-only icon policy
We’ve confirmed lucide-react is still imported in dozens of files across the monorepo. Please:
- Remove
lucide-reactfromoptimizePackageImportsinapps/docs/next.config.mjs. - Replace every
import … from 'lucide-react'withimport … from '@phosphor-icons/react'. - Use the following to locate all remaining Lucide imports:
rg -n "from 'lucide-react'" apps
Apply this diff in apps/docs/next.config.mjs:
experimental: {
- optimizePackageImports: [
- 'fumadocs-ui',
- 'lucide-react',
- '@phosphor-icons/react',
- ],
+ optimizePackageImports: ['fumadocs-ui', '@phosphor-icons/react'],
},🤖 Prompt for AI Agents
In apps/docs/next.config.mjs around lines 85 to 89, remove 'lucide-react' from
the optimizePackageImports array to enforce the phosphor-only icon policy. Then,
search the entire apps directory for all import statements importing from
'lucide-react' and replace them with imports from '@phosphor-icons/react' to
ensure consistency across the monorepo.
| "react-day-picker": "^9.7.0", | ||
| "react-dom": "19.0.0", | ||
| "react-hook-form": "^7.57.0", | ||
| "react-icons": "^5.5.0", |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Remove react-icons; switch to Phosphor-only per codebase policy.
Guideline: “Don’t use lucide for icons, ONLY use phosphor icons.” Using react-icons violates this. Migrate usages (e.g., footer) to @phosphor-icons/react and drop the dependency.
Apply this diff to remove the dependency:
"dependencies": {
@@
- "react-icons": "^5.5.0",
+ // Use only @phosphor-icons/react across the appRun this to find usages to migrate:
🏁 Script executed:
#!/bin/bash
rg -n "from 'react-icons" appsLength of output: 603
Remove react-icons and migrate to @phosphor-icons/react
The docs site currently imports icons from react-icons in multiple places. Per our policy (“Don’t use lucide for icons, ONLY use phosphor icons”), please:
• Remove the react-icons dependency from apps/docs/package.json
• Add or verify @phosphor-icons/react in dependencies
• Update all imports to use Phosphor React icons
Locations to update:
- apps/docs/components/footer.tsx (lines 1–2)
- apps/docs/app/layout.config.tsx (lines 2–3)
- apps/docs/components/landing/footer-nav.tsx (lines 3–4)
Example migration in footer.tsx:
-import { FaDiscord, FaGithub, FaXTwitter } from 'react-icons/fa6';
-import { IoMdMail } from 'react-icons/io';
+import {
+ DiscordLogoIcon,
+ GithubLogoIcon,
+ XLogoIcon,
+ EnvelopeSimpleIcon
+} from '@phosphor-icons/react';In package.json:
"dependencies": {
- "react-icons": "^5.5.0",
+ "@phosphor-icons/react": "^2.0.0", // use latest Phosphor React packageVerify no remaining react-icons imports:
rg -n "from 'react-icons" apps/docs🤖 Prompt for AI Agents
In apps/docs/package.json at line 61, remove the "react-icons" dependency and
ensure "@phosphor-icons/react" is added to the dependencies section. Then,
update all imports of icons from "react-icons" to use "@phosphor-icons/react" in
the specified files: apps/docs/components/footer.tsx (lines 1-2),
apps/docs/app/layout.config.tsx (lines 2-3), and
apps/docs/components/landing/footer-nav.tsx (lines 3-4). Finally, verify no
remaining imports from "react-icons" exist by running a search command like `rg
-n "from 'react-icons" apps/docs`.
| "recharts": "^2.15.3", | ||
| "sonner": "^2.0.5", | ||
| "tailwind-merge": "^3.3.0", | ||
| "use-sound": "^5.0.0", |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Verify necessity of use-sound.
If it isn’t used in apps/docs, avoid adding it to keep the dependency surface minimal.
Run:
🏁 Script executed:
#!/bin/bash
rg -n "use-sound" apps/docsLength of output: 80
🏁 Script executed:
#!/bin/bash
set -e
echo "Checking static imports of use-sound..."
rg -n "from ['\"]use-sound['\"]" apps/docs
rg -n "require(['\"]use-sound['\"])" apps/docs
echo "Checking hook invocations..."
rg -n "useSound\(" apps/docsLength of output: 146
Remove unused use-sound dependency
Our searches did not find any imports or hook calls for use-sound in apps/docs. Please remove it to keep the dependency surface minimal.
Locations to update:
- apps/docs/package.json: remove the following line
- "use-sound": "^5.0.0",📝 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.
| "use-sound": "^5.0.0", |
🤖 Prompt for AI Agents
In apps/docs/package.json at line 66, remove the "use-sound": "^5.0.0"
dependency line entirely to eliminate the unused package and reduce the
dependency surface.
| "tailwind-merge": "^3.3.0", | ||
| "use-sound": "^5.0.0", | ||
| "vaul": "^1.1.2", | ||
| "zod": "3.25.76" |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Upgrade to Zod v4 and import from zod/v4.
Guideline: “Use ONLY Zod V4 from zod/v4; never zod 3.” You currently pin zod@3.x. Plan a migration to v4 and update all imports.
I can script an impact assessment (files importing zod) if useful.
🤖 Prompt for AI Agents
In apps/docs/package.json at line 68, the project currently uses zod version
3.25.76, but the guideline requires upgrading to zod version 4 and importing
from 'zod/v4'. To fix this, update the zod dependency version to the latest v4
release in package.json, then refactor all code files importing zod to import
from 'zod/v4' instead of 'zod'. Plan and execute a migration to ensure
compatibility with zod v4 APIs.
| organization: one(organization, { | ||
| fields: [apikey.organizationId], | ||
| references: [organization.id], | ||
| }), |
There was a problem hiding this comment.
🧹 Nitpick (assertive)
Relation added – consider adding the reverse mapping
The one-to-one link from apikey to organization is correct, but organizationRelations currently lacks a corresponding apikeys: many(apikey) entry. Adding it will simplify querying all keys for an organisation and keeps relations symmetrical.
🤖 Prompt for AI Agents
In packages/db/src/drizzle/relations.ts around lines 144 to 147, you have
defined a one-to-one relation from apikey to organization but missed adding the
reverse relation in organizationRelations. To fix this, add a many-to-one
relation in organizationRelations like apikeys: many(apikey) to enable querying
all API keys for an organization and maintain symmetrical relations.
| function applyUrlNormalization(data: DataRow[]): DataRow[] { | ||
| return data.map((row) => { | ||
| const original = getString(row.name); | ||
| if (!original) { | ||
| return row; | ||
| } | ||
| let normalized = original; | ||
| try { | ||
| if ( | ||
| normalized.startsWith('http://') || | ||
| normalized.startsWith('https://') | ||
| ) { | ||
| const url = new URL(normalized); | ||
| normalized = url.pathname || '/'; | ||
| } | ||
| if (!normalized.startsWith('/')) { | ||
| normalized = `/${normalized}`; | ||
| } | ||
| if (normalized.length > 1 && normalized.endsWith('/')) { | ||
| normalized = normalized.slice(0, -1); | ||
| } | ||
| return { ...row, name: normalized } as DataRow; | ||
| } catch { | ||
| return row; | ||
| } | ||
| }); |
There was a problem hiding this comment.
🧹 Nitpick (assertive)
applyUrlNormalization misses hash/query stripping
The normaliser converts absolute URLs to pathname, ensures leading/trailing slashes, but leaves #hash and ?query fragments when a plain path string is supplied (e.g. "about#team").
Consider removing location.hash and location.search portions for consistency with the behaviour when a full URL is passed.
🤖 Prompt for AI Agents
In apps/api/src/query/utils.ts around lines 253 to 278, the
applyUrlNormalization function does not remove hash (#) and query (?) fragments
from plain path strings, causing inconsistency with full URL normalization.
Update the function to strip any hash and query parts from the normalized string
after handling leading/trailing slashes, ensuring the returned path is clean and
consistent regardless of input format.
| if (num >= 1_000_000) { | ||
| return `${(num / 1_000_000).toFixed(1)}M`; | ||
| } | ||
| if (num >= 1000) { | ||
| return `${(num / 1000).toFixed(1)}K`; | ||
| } | ||
| return num.toString(); |
There was a problem hiding this comment.
🧹 Nitpick (assertive)
formatNumber: uses toFixed with digits; consider trimming trailing .0
Current output can be “1.0K/1.0M”. Optional polish: strip trailing “.0” to display “1K/1M”.
Apply this localized refactor:
- if (num >= 1_000_000) {
- return `${(num / 1_000_000).toFixed(1)}M`;
- }
- if (num >= 1000) {
- return `${(num / 1000).toFixed(1)}K`;
- }
+ if (num >= 1_000_000) {
+ const s = (num / 1_000_000).toFixed(1);
+ return `${s.endsWith('.0') ? s.slice(0, -2) : s}M`;
+ }
+ if (num >= 1000) {
+ const s = (num / 1000).toFixed(1);
+ return `${s.endsWith('.0') ? s.slice(0, -2) : s}K`;
+ }🤖 Prompt for AI Agents
In apps/dashboard/app/(main)/websites/_components/mini-chart.tsx around lines 21
to 27, the formatNumber function uses toFixed(1) which results in outputs like
"1.0K" or "1.0M". To improve display, modify the code to trim trailing ".0" from
the formatted string so that numbers like 1,000 or 1,000,000 show as "1K" or
"1M" instead of "1.0K" or "1.0M". Implement this by checking if the formatted
string ends with ".0" and removing it before returning.
| <Suspense | ||
| fallback={<Skeleton className="h-12 w-full rounded" />} | ||
| fallback={ | ||
| <Skeleton className="h-12 w-full rounded sm:h-16" /> | ||
| } | ||
| > | ||
| <MiniChart data={chartData.data} id={website.id} /> | ||
| <MiniChart | ||
| data={chartData.data} | ||
| days={chartData.data.length} | ||
| id={website.id} | ||
| /> | ||
| </Suspense> | ||
| </div> |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Add an error boundary around MiniChart render
Guidelines require proper error boundaries. If the chart fails to load/render, users should see a graceful fallback rather than a broken card.
I can provide a lightweight ErrorBoundary component (using react-error-boundary) wrapping the chart area; want me to add it?
🤖 Prompt for AI Agents
In apps/dashboard/app/(main)/websites/_components/website-card.tsx around lines
162 to 173, the MiniChart component is wrapped in Suspense but lacks an error
boundary. To handle potential rendering errors gracefully, wrap the MiniChart
inside an ErrorBoundary component from react-error-boundary, providing a
fallback UI for errors. If you don't have an ErrorBoundary component yet, create
a lightweight one using react-error-boundary and use it to wrap MiniChart
alongside Suspense.
🧹 Nitpick (assertive)
Avoid redundant Suspense: switch next/dynamic to suspense mode or remove wrapper
next/dynamic is configured with a loading component and ssr: false, but MiniChart is also wrapped in <Suspense>. Without suspense: true in dynamic(), the Suspense fallback won’t be used. Use one mechanism consistently.
Two options:
- Prefer Suspense: set
suspense: trueand removeloading, keeping the<Suspense>fallback. - Or keep
loadingand remove the<Suspense>wrapper.
Example (prefer Suspense):
-const MiniChart = dynamic(
- () => import('./mini-chart').then((mod) => mod.default),
- {
- loading: () => <Skeleton className="h-12 w-full rounded" />,
- ssr: false,
- }
-);
+const MiniChart = dynamic(() => import('./mini-chart').then((m) => m.default), {
+ ssr: false,
+ suspense: true,
+});🤖 Prompt for AI Agents
In apps/dashboard/app/(main)/websites/_components/website-card.tsx around lines
162 to 173, the MiniChart component is wrapped in a Suspense fallback but
next/dynamic is configured with loading and ssr: false without suspense: true,
causing the Suspense fallback to be ineffective. To fix this, either add
suspense: true to the dynamic import of MiniChart and remove the loading option
while keeping the Suspense wrapper, or remove the Suspense wrapper and keep the
loading option in dynamic. Choose one consistent approach to handle loading
states.
| function filterOrganizations<T extends { name: string; slug?: string | null }>( | ||
| orgs: T[] | undefined, | ||
| query: string | ||
| ): T[] { | ||
| if (!orgs || orgs.length === 0) { | ||
| return []; | ||
| } | ||
| if (!query) { | ||
| return orgs; | ||
| } | ||
| const q = query.toLowerCase(); | ||
| const filtered: T[] = []; | ||
| for (const org of orgs) { | ||
| const nameMatch = org.name.toLowerCase().includes(q); | ||
| const slugMatch = org.slug ? org.slug.toLowerCase().includes(q) : false; | ||
| if (nameMatch || slugMatch) { | ||
| filtered.push(org); | ||
| } | ||
| } | ||
| return filtered; | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick (assertive)
Leverage native Array.filter for clarity
filterOrganizations manually builds a new array in a for loop. The same result can be expressed more readably and with less code by using orgs.filter(...).
No behavioural change, only readability & maintainability.
🤖 Prompt for AI Agents
In apps/dashboard/components/layout/organization-selector.tsx around lines 36 to
56, the filterOrganizations function manually constructs a filtered array using
a for loop. Refactor this function to use the native Array.filter method
instead, applying the filtering condition inside the filter callback. This will
simplify the code, improve readability, and maintain the same behavior without
manually pushing elements.
| export type OrganizationRole = 'owner' | 'admin' | 'member'; | ||
|
|
There was a problem hiding this comment.
🧹 Nitpick (assertive)
Derive OrganizationRole from the auth client to avoid drift
type OrganizationRole = 'owner' | 'admin' | 'member' is hard-coded in three places. If the server adds/renames a role, this union goes stale and compilation won’t warn you. Prefer:
export type OrganizationRole = keyof typeof authClient.config.roles;(or export the enum/union from a shared package) so the source-of-truth is single and compile-time-safe.
Also applies to: 23-24, 29-31
🤖 Prompt for AI Agents
In apps/dashboard/hooks/use-organizations.ts around lines 5 to 6, the
OrganizationRole type is hard-coded as a union of string literals, which can
become outdated if roles change on the server. To fix this, replace the
hard-coded union with a type derived from the auth client's roles configuration,
such as using keyof typeof authClient.config.roles, ensuring the roles stay in
sync and are compile-time safe. Apply the same change to lines 23-24 and 29-31
where the roles are also hard-coded.
| const [isOpen, setIsOpen] = useState(false); | ||
| const triggerRef = useRef<HTMLButtonElement>(null); | ||
|
|
||
| useEffect(() => { | ||
| const trigger = triggerRef.current; | ||
| if (!trigger) { | ||
| return; | ||
| } | ||
|
|
||
| const observer = new MutationObserver(() => { | ||
| const dataState = trigger.getAttribute('data-state'); | ||
| setIsOpen(dataState === 'open'); | ||
| }); | ||
|
|
||
| observer.observe(trigger, { | ||
| attributes: true, | ||
| attributeFilter: ['data-state'], | ||
| }); | ||
|
|
||
| // Initial check | ||
| const dataState = trigger.getAttribute('data-state'); | ||
| setIsOpen(dataState === 'open'); | ||
|
|
||
| return () => observer.disconnect(); | ||
| }, []); |
There was a problem hiding this comment.
🧹 Nitpick (assertive)
MutationObserver & extra state are over-engineered
Using a MutationObserver plus useEffect to track data-state adds unnecessary complexity and violates the project rule “Almost NEVER use useEffect unless critical”.
Radix already exposes the open state:
const open = props['data-state'] === 'open'or, at a higher level, via the Accordion’s onValueChange.
Drop the observer and drive animations directly from data-state or component props – you’ll remove ~25 lines, one effect, and an extra state variable.
🤖 Prompt for AI Agents
In apps/docs/components/ui/accordion.tsx from lines 35 to 59, the current
implementation uses a MutationObserver and useEffect to track the 'data-state'
attribute and manage an extra isOpen state, which is unnecessarily complex.
Remove the MutationObserver, useEffect, and isOpen state entirely. Instead,
derive the open state directly from the 'data-state' prop or from the
Accordion's onValueChange callback, and use that to drive animations and UI
changes. This simplifies the code by eliminating about 25 lines and avoids using
useEffect unnecessarily.
| <PlusIcon | ||
| className="size-6 shrink-0 translate-y-0.5 cursor-pointer text-muted-foreground transition-transform duration-200 hover:text-accent-foreground" | ||
| strokeWidth={1.5} | ||
| /> |
There was a problem hiding this comment.
🧹 Nitpick (assertive)
Non-interactive icon styled as interactive
cursor-pointer suggests the PlusIcon is clickable, but all clicks are handled by the surrounding <button>.
Remove the cursor style to avoid confused hit-targets and redundant hover focus states.
🤖 Prompt for AI Agents
In apps/docs/components/ui/accordion.tsx around lines 85 to 88, the PlusIcon has
a cursor-pointer style that incorrectly suggests it is clickable, but the click
events are handled by the surrounding button. Remove the cursor-pointer class
from the PlusIcon's className to prevent misleading the user about the clickable
area and avoid redundant hover and focus states on the icon.
| const MIN_DAYS = 3; | ||
| const MAX_DAYS = 30; |
There was a problem hiding this comment.
🧹 Nitpick (assertive)
Align minimum days with trend requirements
calculateTrend requires at least 4 points (data.length < 4 returns null). The router clamps days at MIN_DAYS = 3. Consider bumping MIN_DAYS to 4 so the smallest query can still produce a trend.
-const MIN_DAYS = 3;
+const MIN_DAYS = 4;If you intentionally want trend=null for short ranges, keep as-is.
Also applies to: 80-84
🤖 Prompt for AI Agents
In packages/rpc/src/routers/mini-charts.ts around lines 22-23 and 80-84, the
MIN_DAYS constant is set to 3, but the calculateTrend function requires at least
4 data points to compute a trend. To fix this, update MIN_DAYS to 4 so that the
minimum days queried aligns with the trend calculation requirements, ensuring
the smallest query can produce a valid trend result.
| const authCacheKey = `auth:${userId}:${[...requestedIds].sort().join(',')}`; | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
Use a hashed IDs segment for the auth cache key to avoid oversized Redis keys
Concatenating potentially thousands of IDs creates long keys. Hash for stability and size, similar to the main cache key.
- const authCacheKey = `auth:${userId}:${[...requestedIds].sort().join(',')}`;
+ const idsHash = createHash('sha1')
+ .update([...requestedIds].sort().join(','))
+ .digest('base64url')
+ .slice(0, 16);
+ const authCacheKey = `auth:${userId}:${idsHash}`;Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In packages/rpc/src/routers/mini-charts.ts around lines 44 to 45, the
authCacheKey is constructed by concatenating all requested IDs, which can create
excessively long Redis keys. To fix this, replace the concatenated IDs segment
with a hash of the sorted requested IDs array. Use a suitable hashing function
to generate a fixed-length string representing the IDs, ensuring the
authCacheKey remains stable and compact.
| queryFn: async () => { | ||
| if (input.organizationId) { | ||
| const { success } = await websitesApi.hasPermission({ | ||
| headers: ctx.headers, | ||
| body: { permissions: { website: ['read'] } }, | ||
| }); | ||
| if (!success) { | ||
| throw new TRPCError({ | ||
| code: 'FORBIDDEN', | ||
| message: 'Missing organization permissions.', | ||
| }); | ||
| } | ||
| } | ||
| const whereClause = buildWebsiteFilter( |
There was a problem hiding this comment.
🧹 Nitpick (assertive)
Extract repeated permission check into a helper
The hasPermission block is repeated in both list and listWithCharts.
Pulling it into a small utility (e.g. assertOrgPermission(ctx, headers, ['read'])) will reduce duplication and future-maintenance risk.
🤖 Prompt for AI Agents
In packages/rpc/src/routers/websites.ts around lines 161 to 174, the permission
check using websitesApi.hasPermission is duplicated in both list and
listWithCharts functions. To fix this, create a helper function named
assertOrgPermission that accepts context, headers, and required permissions,
performs the hasPermission check, and throws a TRPCError if permissions are
missing. Replace the duplicated permission check blocks in both functions with
calls to this new helper to reduce code duplication and improve maintainability.
Pull Request
Description
Please include a summary of the change and which issue is fixed. Also include relevant motivation and context.
Checklist
Summary by CodeRabbit
New Features
Improvements
Bug Fixes
Refactor
Chores
Removals
Database