Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 21 additions & 10 deletions components/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,24 @@ import {
} from 'lucide-react'
import { MapToggle } from './map-toggle'
import { ProfileToggle } from './profile-toggle'
import { PurchaseCreditsPopup } from './purchase-credits-popup'
import { UsageSidebar } from './usage-sidebar'
import { useState, useEffect } from 'react'
Comment on lines +18 to +20
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Minor import inconsistency.

useState and useEffect are imported separately from the React import on line 2. Consider consolidating for consistency.

♻️ Suggested consolidation
 "use client"
-import React from 'react'
+import React, { useState, useEffect } from 'react'
 import Image from 'next/image'
 ...
-import { useState, useEffect } from 'react'
🤖 Prompt for AI Agents
In `@components/header.tsx` around lines 18 - 20, The imports at the top of
components/header.tsx are inconsistent: React hooks useState and useEffect are
imported separately rather than from the React import; update the import
statements so hooks are consolidated into the same import that brings in React
(e.g., combine into a single import that includes useState and useEffect
alongside other React imports) and remove the standalone import of
useState/useEffect; ensure symbols referenced (PurchaseCreditsPopup,
UsageSidebar, useState, useEffect) remain correctly imported.


export const Header = () => {
const { toggleCalendar } = useCalendarToggle()
const [isPurchaseOpen, setIsPurchaseOpen] = useState(false)
const [isUsageOpen, setIsUsageOpen] = useState(false)

useEffect(() => {
// Open payment popup as soon as application opens
setIsPurchaseOpen(true)
}, [])

Comment on lines +24 to +31

Choose a reason for hiding this comment

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

Auto-opening the purchase dialog on every app load is a major UX regression and will likely block onboarding/normal usage. The intent described in the PR context sounds like “integrate” rather than force-open. This should be gated behind a real condition (e.g., user is out of credits / feature flag / query param) and ideally persisted so it doesn’t show repeatedly once dismissed.

Suggestion

Gate the auto-open behind a condition and persist dismissal. For example:

const [isPurchaseOpen, setIsPurchaseOpen] = useState(false)

useEffect(() => {
  const dismissed = localStorage.getItem('purchasePopupDismissed') === '1'
  if (!dismissed && shouldPromptForUpgrade) setIsPurchaseOpen(true)
}, [shouldPromptForUpgrade])

const closePurchase = () => {
  localStorage.setItem('purchasePopupDismissed', '1')
  setIsPurchaseOpen(false)
}

Then pass closePurchase to onClose. Reply with "@CharlieHelps yes please" if you'd like me to add a commit implementing this gating/persistence pattern.

return (
<>
<PurchaseCreditsPopup isOpen={isPurchaseOpen} onClose={() => setIsPurchaseOpen(false)} />
<UsageSidebar isOpen={isUsageOpen} onClose={() => setIsUsageOpen(false)} />
<header className="fixed w-full p-1 md:p-2 flex justify-between items-center z-20 backdrop-blur bg-background/95 border-b border-border/40">
<div>
<a href="/">
Expand Down Expand Up @@ -53,11 +67,9 @@ export const Header = () => {

<div id="header-search-portal" />

<a href="https://buy.stripe.com/3cIaEX3tRcur9EM7tbasg00" target="_blank" rel="noopener noreferrer">
<Button variant="ghost" size="icon">
<TentTree className="h-[1.2rem] w-[1.2rem]" />
</Button>
</a>
<Button variant="ghost" size="icon" onClick={() => setIsUsageOpen(true)}>
<TentTree className="h-[1.2rem] w-[1.2rem]" />
</Button>

<ModeToggle />

Expand All @@ -67,14 +79,13 @@ export const Header = () => {
{/* Mobile menu buttons */}
<div className="flex md:hidden gap-2">

<a href="https://buy.stripe.com/3cIaEX3tRcur9EM7tbasg00" target="_blank" rel="noopener noreferrer">
<Button variant="ghost" size="icon">
<TentTree className="h-[1.2rem] w-[1.2rem]" />
</Button>
</a>
<Button variant="ghost" size="icon" onClick={() => setIsUsageOpen(true)}>
<TentTree className="h-[1.2rem] w-[1.2rem]" />
</Button>
<ProfileToggle/>
</div>
</header>
</>
)
}

Expand Down
57 changes: 57 additions & 0 deletions components/purchase-credits-popup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
'use client';

import React from 'react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { CreditCard, Zap } from 'lucide-react';

interface PurchaseCreditsPopupProps {
isOpen: boolean;
onClose: () => void;
}

export function PurchaseCreditsPopup({ isOpen, onClose }: PurchaseCreditsPopupProps) {
const handlePurchase = () => {
window.open('https://buy.stripe.com/3cIaEX3tRcur9EM7tbasg00', '_blank');
onClose();
};
Comment on lines +20 to +24
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider user feedback before closing the dialog.

The dialog closes immediately after opening the Stripe URL. If the popup is blocked or the user switches back quickly, they may be confused. Consider keeping the dialog open or providing visual feedback.

🤖 Prompt for AI Agents
In `@components/purchase-credits-popup.tsx` around lines 20 - 24, The
PurchaseCreditsPopup currently closes immediately in handlePurchase; change
handlePurchase to capture the return value of window.open (e.g., const popup =
window.open(...)) and only call onClose() if popup is non-null (popup opened).
If popup is null (blocked) or closed quickly, keep the dialog open and set local
state (e.g., popupBlocked or isLoading) to show an inline error/notification or
a "Open Stripe in new tab" link/button; also consider adding a short timeout
before auto-closing to allow the user to switch to the new tab. Update the
PurchaseCreditsPopup component to add that state and conditional UI based on
popupBlocked/isLoading.

Comment on lines +21 to +24

Choose a reason for hiding this comment

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

Opening a new tab via window.open without noopener,noreferrer leaves you vulnerable to reverse-tabnabbing (the newly opened page can access window.opener). The previous implementation used rel="noopener noreferrer"; this should retain equivalent protection.

Suggestion

Use the third parameter to disable opener, and consider noreferrer as well:

window.open(url, '_blank', 'noopener,noreferrer')

Alternatively, render a normal <a target="_blank" rel="noopener noreferrer"> for navigation. Reply with "@CharlieHelps yes please" if you'd like me to add a commit applying this everywhere window.open is used in this PR.


return (
<Dialog open={isOpen} onOpenChange={onClose}>

Choose a reason for hiding this comment

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

Dialog's onOpenChange typically receives the next open state ((open: boolean) => void). Passing onClose directly means it will be invoked for both open and close transitions, and it also ignores the intended next state. This can cause surprising behavior (e.g., clicking the trigger/open could immediately call onClose).

Suggestion

Handle the boolean argument and only close when open === false:

<Dialog
  open={isOpen}
  onOpenChange={(open) => {
    if (!open) onClose()
  }}
>

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this fix.

<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Zap className="text-yellow-500" />
Upgrade Your Plan
</DialogTitle>
<DialogDescription>
You&apos;ve reached your credit limit. Upgrade now to continue using all features seamlessly.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="flex items-center justify-between p-4 border rounded-lg bg-muted/50">
<div>
<p className="font-medium">Standard Tier</p>
<p className="text-sm text-muted-foreground">Unlimited searches & more</p>
</div>
<p className="font-bold">$20/mo</p>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={onClose}>Later</Button>
<Button onClick={handlePurchase} className="gap-2">
<CreditCard size={16} />
Pay Now
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
30 changes: 30 additions & 0 deletions components/sidebar/chat-history-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
} from '@/components/ui/alert-dialog';
import { toast } from 'sonner';
import { Spinner } from '@/components/ui/spinner';
import { Zap, ChevronDown, ChevronUp } from 'lucide-react';
import HistoryItem from '@/components/history-item'; // Adjust path if HistoryItem is moved or renamed
import type { Chat as DrizzleChat } from '@/lib/actions/chat-db'; // Use the Drizzle-based Chat type

Expand All @@ -31,6 +32,7 @@ export function ChatHistoryClient({}: ChatHistoryClientProps) {
const [error, setError] = useState<string | null>(null);
const [isClearPending, startClearTransition] = useTransition();
const [isAlertDialogOpen, setIsAlertDialogOpen] = useState(false);
const [isCreditsVisible, setIsCreditsVisible] = useState(false);
const router = useRouter();

useEffect(() => {
Expand Down Expand Up @@ -113,6 +115,34 @@ export function ChatHistoryClient({}: ChatHistoryClientProps) {

return (
<div className="flex flex-col flex-1 space-y-3 h-full">
<div className="px-2">
<Button
variant="ghost"
size="sm"
className="w-full flex items-center justify-between text-muted-foreground hover:text-foreground"
onClick={() => setIsCreditsVisible(!isCreditsVisible)}
>
<div className="flex items-center gap-2">
<Zap size={14} className="text-yellow-500" />
<span className="text-xs font-medium">Credits Preview</span>
</div>
{isCreditsVisible ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
</Button>

{isCreditsVisible && (
<div className="mt-2 p-3 rounded-lg bg-muted/50 border border-border/50 space-y-2">
<div className="flex justify-between items-center text-xs">
<span>Available Credits</span>
<span className="font-bold">0</span>
</div>
<div className="w-full bg-secondary h-1.5 rounded-full overflow-hidden">
<div className="bg-yellow-500 h-full w-[0%]" />
</div>
Comment on lines +138 to +140
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Add ARIA attributes to the progress bar for accessibility.

The custom progress bar should include appropriate ARIA attributes to be accessible to screen readers.

♻️ Proposed fix
-<div className="w-full bg-secondary h-1.5 rounded-full overflow-hidden">
-  <div className="bg-yellow-500 h-full w-[0%]" />
-</div>
+<div 
+  role="progressbar" 
+  aria-valuenow={0} 
+  aria-valuemin={0} 
+  aria-valuemax={100}
+  aria-label="Credits usage"
+  className="w-full bg-secondary h-1.5 rounded-full overflow-hidden"
+>
+  <div className="bg-yellow-500 h-full w-[0%]" />
+</div>
📝 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
<div className="w-full bg-secondary h-1.5 rounded-full overflow-hidden">
<div className="bg-yellow-500 h-full w-[0%]" />
</div>
<div
role="progressbar"
aria-valuenow={0}
aria-valuemin={0}
aria-valuemax={100}
aria-label="Credits usage"
className="w-full bg-secondary h-1.5 rounded-full overflow-hidden"
>
<div className="bg-yellow-500 h-full w-[0%]" />
</div>
🤖 Prompt for AI Agents
In `@components/sidebar/chat-history-client.tsx` around lines 138 - 140, The
progress bar markup lacks ARIA attributes; update the outer container (the div
with "w-full bg-secondary h-1.5 rounded-full overflow-hidden") to include
role="progressbar" plus aria-valuemin="0" and aria-valuemax="100", and set
aria-valuenow to the current numeric progress value (e.g., progressPercent)
and/or aria-valuetext for a human-readable label; also ensure the inner bar (the
div with "bg-yellow-500 h-full w-[0%]") uses a computed width from the same
progress value (not a hardcoded w-[0%]) so the ARIA value and visual width stay
in sync inside the ChatHistoryClient progress markup.

<p className="text-[10px] text-muted-foreground">Upgrade to get more credits</p>
</div>
)}
Comment on lines +119 to +143
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Add accessibility attributes for the collapsible section.

The toggle button should include aria-expanded and aria-controls attributes to properly communicate state to assistive technologies.

Additionally, consider making "Upgrade to get more credits" an interactive element (link or button) that triggers the PurchaseCreditsPopup introduced elsewhere in this PR.

♻️ Proposed accessibility fix
 <Button
   variant="ghost"
   size="sm"
   className="w-full flex items-center justify-between text-muted-foreground hover:text-foreground"
   onClick={() => setIsCreditsVisible(!isCreditsVisible)}
+  aria-expanded={isCreditsVisible}
+  aria-controls="credits-preview-panel"
 >
   <div className="flex items-center gap-2">
     <Zap size={14} className="text-yellow-500" />
     <span className="text-xs font-medium">Credits Preview</span>
   </div>
   {isCreditsVisible ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
 </Button>

 {isCreditsVisible && (
-  <div className="mt-2 p-3 rounded-lg bg-muted/50 border border-border/50 space-y-2">
+  <div 
+    id="credits-preview-panel"
+    className="mt-2 p-3 rounded-lg bg-muted/50 border border-border/50 space-y-2"
+  >
     <div className="flex justify-between items-center text-xs">
       <span>Available Credits</span>
       <span className="font-bold">0</span>
     </div>
     <div className="w-full bg-secondary h-1.5 rounded-full overflow-hidden">
       <div className="bg-yellow-500 h-full w-[0%]" />
     </div>
-    <p className="text-[10px] text-muted-foreground">Upgrade to get more credits</p>
+    <Button 
+      variant="link" 
+      size="sm" 
+      className="text-[10px] text-muted-foreground p-0 h-auto"
+    >
+      Upgrade to get more credits
+    </Button>
   </div>
 )}
📝 Committable suggestion

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

Suggested change
<Button
variant="ghost"
size="sm"
className="w-full flex items-center justify-between text-muted-foreground hover:text-foreground"
onClick={() => setIsCreditsVisible(!isCreditsVisible)}
>
<div className="flex items-center gap-2">
<Zap size={14} className="text-yellow-500" />
<span className="text-xs font-medium">Credits Preview</span>
</div>
{isCreditsVisible ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
</Button>
{isCreditsVisible && (
<div className="mt-2 p-3 rounded-lg bg-muted/50 border border-border/50 space-y-2">
<div className="flex justify-between items-center text-xs">
<span>Available Credits</span>
<span className="font-bold">0</span>
</div>
<div className="w-full bg-secondary h-1.5 rounded-full overflow-hidden">
<div className="bg-yellow-500 h-full w-[0%]" />
</div>
<p className="text-[10px] text-muted-foreground">Upgrade to get more credits</p>
</div>
)}
<Button
variant="ghost"
size="sm"
className="w-full flex items-center justify-between text-muted-foreground hover:text-foreground"
onClick={() => setIsCreditsVisible(!isCreditsVisible)}
aria-expanded={isCreditsVisible}
aria-controls="credits-preview-panel"
>
<div className="flex items-center gap-2">
<Zap size={14} className="text-yellow-500" />
<span className="text-xs font-medium">Credits Preview</span>
</div>
{isCreditsVisible ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
</Button>
{isCreditsVisible && (
<div
id="credits-preview-panel"
className="mt-2 p-3 rounded-lg bg-muted/50 border border-border/50 space-y-2"
>
<div className="flex justify-between items-center text-xs">
<span>Available Credits</span>
<span className="font-bold">0</span>
</div>
<div className="w-full bg-secondary h-1.5 rounded-full overflow-hidden">
<div className="bg-yellow-500 h-full w-[0%]" />
</div>
<Button
variant="link"
size="sm"
className="text-[10px] text-muted-foreground p-0 h-auto"
>
Upgrade to get more credits
</Button>
</div>
)}
🤖 Prompt for AI Agents
In `@components/sidebar/chat-history-client.tsx` around lines 119 - 143, The
collapsible "Credits Preview" control lacks ARIA attributes and a trigger for
purchasing; update the Button component that toggles isCreditsVisible (onClick
=> setIsCreditsVisible) to include aria-expanded={isCreditsVisible} and
aria-controls referencing the collapsible region's id (e.g.,
credits-preview-panel) and give the collapsible div that id and role="region".
Also replace or wrap the "Upgrade to get more credits" text with an interactive
element that opens the existing PurchaseCreditsPopup (call the same handler/prop
used elsewhere in the PR) so keyboard and assistive users can activate the
purchase flow.

Comment on lines +118 to +143

Choose a reason for hiding this comment

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

The credits UI in the chat history is currently hard-coded to 0 and 0%, which makes the “Credits Preview” look broken rather than “preview”. If the real data source isn’t ready, this should be clearly marked as placeholder or wired to whatever the app already knows (even if it’s just a prop/config).

Suggestion

At minimum, extract constants and label as placeholder to avoid implying live data:

const availableCredits = 0 // TODO: wire to billing API
const percent = 0

Or hide the block unless data exists. Reply with "@CharlieHelps yes please" if you'd like me to add a commit that guards this UI behind a hasCreditsData flag and TODO plumbing.

</div>

<div className="flex flex-col gap-2 flex-1 overflow-y-auto">
{!chats?.length ? (
<div className="text-foreground/30 text-sm text-center py-4">
Expand Down
113 changes: 113 additions & 0 deletions components/usage-sidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
'use client';

import React, { useEffect, useState } from 'react';
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
} from '@/components/ui/sheet';
import { Button } from '@/components/ui/button';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Zap, RefreshCw, LayoutPanelLeft, X } from 'lucide-react';

interface UsageSidebarProps {
isOpen: boolean;
onClose: () => void;
}

export function UsageSidebar({ isOpen, onClose }: UsageSidebarProps) {
const [usage, setUsage] = useState<any[]>([]);
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Avoid any[] type for usage state.

Using any[] loses type safety. Define a proper interface for usage items.

♻️ Suggested type definition
+interface UsageItem {
+  details: string;
+  date: string;
+  change: number;
+}
+
 export function UsageSidebar({ isOpen, onClose }: UsageSidebarProps) {
-  const [usage, setUsage] = useState<any[]>([]);
+  const [usage, setUsage] = useState<UsageItem[]>([]);
📝 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
const [usage, setUsage] = useState<any[]>([]);
interface UsageItem {
details: string;
date: string;
change: number;
}
export function UsageSidebar({ isOpen, onClose }: UsageSidebarProps) {
const [usage, setUsage] = useState<UsageItem[]>([]);
🤖 Prompt for AI Agents
In `@components/usage-sidebar.tsx` at line 20, The state declaration uses a loose
any[] which loses type safety; define a concrete interface (e.g., UsageItem)
describing the properties used by this component (fields like id, date, amount,
description — matching how usage is accessed) and change the state to use
useState<UsageItem[]>([]) instead of any[], then update any references to usage,
setUsage, and props/handlers (e.g., where items are fetched or passed) to use
the new UsageItem type so TypeScript enforces correct shape across functions
like setUsage and rendering logic in the UsageSidebar component.

const [credits, setCredits] = useState(0);

Comment on lines +19 to +22

Choose a reason for hiding this comment

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

This introduces useState<any[]> for usage. That’s an unsafe-but-type-valid escape hatch that makes future refactors harder and can hide real mistakes (e.g., typos in details/date/change). Prefer a small local type for the usage row shape.

Suggestion

Replace any[] with an explicit type:

type UsageItem = {
  details: string
  date: string
  change: number
}

const [usage, setUsage] = useState<UsageItem[]>([])

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this change.

useEffect(() => {
if (isOpen) {
// Mock data for now as per the screenshot
setUsage([
{ details: 'Efficiently Fix Pull Request ...', date: '2026-01-17 08:05', change: -418 },
{ details: 'Fix Build and Add Parallel S...', date: '2026-01-16 06:10', change: -482 },
{ details: 'How to Add a Feature to a ...', date: '2026-01-14 10:42', change: -300 },
]);
setCredits(0);
}
}, [isOpen]);
Comment on lines +23 to +33

Choose a reason for hiding this comment

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

useEffect sets mock usage data every time the sheet opens. That’s fine for scaffolding, but shipping this will present fake billing/usage to users. At minimum, gate it behind a feature flag or a TODO that prevents production exposure, and consider rendering an explicit “No usage data yet” empty state until a real API is wired.

Also, credits is always set to 0, which makes the UI misleading when combined with an “Upgrade” CTA.

Suggestion

Replace mock mutation with an empty-state and a placeholder loader until API integration:

useEffect(() => {
  if (!isOpen) return;
  // TODO: fetch usage/credits from API
  setUsage([]);
}, [isOpen]);

And in the UI, render a row like "No usage yet" when usage.length === 0.

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with an empty-state + TODO and remove the mock rows.


return (
<Sheet open={isOpen} onOpenChange={onClose}>

Choose a reason for hiding this comment

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

onOpenChange={onClose} has the same issue here as in the dialog: the Sheet component’s open-change callback commonly receives the next open state. Calling onClose unconditionally can cause state thrash or unexpected close calls.

Suggestion

Mirror the boolean-handling pattern:

<Sheet open={isOpen} onOpenChange={(open) => {
  if (!open) onClose()
}}>

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this fix.

<SheetContent className="w-full sm:max-w-[400px] p-0 overflow-y-auto">
<div className="p-6 space-y-6">
<div className="flex justify-between items-center">
<h2 className="text-xl font-semibold">Usage</h2>
<Button variant="ghost" size="icon" onClick={onClose}>
<X size={20} />
</Button>
</div>
Comment on lines +40 to +44

Choose a reason for hiding this comment

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

DialogContent/SheetContent close buttons are missing accessible labels. The X icon button should include an aria-label or an sr-only span for screen readers.

Suggestion

Add an accessible label:

<Button variant="ghost" size="icon" onClick={onClose} aria-label="Close usage sidebar">
  <X size={20} />
</Button>

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this accessibility improvement.


<div className="p-4 border rounded-xl space-y-4">
<div className="flex justify-between items-center">
<span className="italic font-medium text-lg">Free</span>
<Button size="sm" className="rounded-full px-4" onClick={() => window.open('https://buy.stripe.com/3cIaEX3tRcur9EM7tbasg00', '_blank')}>Upgrade</Button>
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Duplicate hardcoded Stripe URL.

This same Stripe URL appears in purchase-credits-popup.tsx. Extract it to a shared constant to ensure consistency and simplify future updates.

♻️ Suggested refactor

Create a constants file:

// lib/constants.ts
export const STRIPE_CHECKOUT_URL = 'https://buy.stripe.com/3cIaEX3tRcur9EM7tbasg00';

Then import and use it in both components.

🤖 Prompt for AI Agents
In `@components/usage-sidebar.tsx` at line 49, The Stripe checkout URL is
duplicated in the UsageSidebar Button onClick and in purchase-credits-popup.tsx;
extract the literal into a shared exported constant (e.g., STRIPE_CHECKOUT_URL)
and import it where needed. Create a constants module (for example
lib/constants.ts) exporting STRIPE_CHECKOUT_URL =
'https://buy.stripe.com/3cIaEX3tRcur9EM7tbasg00', then replace the hardcoded
string in the UsageSidebar Button onClick handler (the Button component) and in
purchase-credits-popup.tsx to reference the imported STRIPE_CHECKOUT_URL
constant.

</div>
Comment on lines +48 to +50

Choose a reason for hiding this comment

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

The Upgrade button again uses window.open without noopener,noreferrer, reintroducing reverse-tabnabbing risk.

Suggestion

Use a safe open:

window.open('https://buy.stripe.com/...', '_blank', 'noopener,noreferrer')

Or render an <a target="_blank" rel="noopener noreferrer"> around the button. Reply with "@CharlieHelps yes please" if you'd like me to add a commit to fix this across the PR.


<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<div className="flex items-center gap-2">
<Zap size={16} className="text-muted-foreground" />
<span>Credits</span>
</div>
<span className="font-bold">{credits}</span>
</div>
<div className="flex items-center justify-between text-xs text-muted-foreground pl-6">
<span>Free credits</span>
<span>0</span>
</div>
</div>

<div className="space-y-1">
<div className="flex items-center justify-between text-sm">
<div className="flex items-center gap-2">
<RefreshCw size={16} className="text-muted-foreground" />
<span>Daily refresh credits</span>
</div>
<span className="font-bold">300</span>
</div>
<p className="text-[10px] text-muted-foreground pl-6">Refresh to 300 at 00:00 every day</p>
</div>
</div>

<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<LayoutPanelLeft size={18} />
<span className="font-medium">Website usage & billing</span>
</div>
<Button variant="ghost" size="icon" className="h-4 w-4">
<span className="sr-only">View more</span>
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" className="h-4 w-4"><path d="M6.1584 3.1356C6.35366 2.94034 6.67024 2.94034 6.8655 3.1356L10.8655 7.13561C11.0608 7.33087 11.0608 7.64745 10.8655 7.84271L6.8655 11.8427C6.67024 12.038 6.35366 12.038 6.1584 11.8427C5.96314 11.6474 5.96314 11.3309 6.1584 11.1356L9.80485 7.48916L6.1584 3.84271C5.96314 3.64745 5.96314 3.33087 6.1584 3.1356Z" fill="currentColor" fillRule="evenodd" clipRule="evenodd"></path></svg>
</Button>
Comment on lines +84 to +87
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

SVG missing accessible title or aria-label.

The chevron icon SVG lacks accessible text. Screen readers cannot identify its purpose. Based on static analysis hint from Biome.

♿ Proposed accessibility fix
-              <Button variant="ghost" size="icon" className="h-4 w-4">
-                <span className="sr-only">View more</span>
-                <svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" className="h-4 w-4"><path d="M6.1584 3.1356C6.35366 2.94034 6.67024 2.94034 6.8655 3.1356L10.8655 7.13561C11.0608 7.33087 11.0608 7.64745 10.8655 7.84271L6.8655 11.8427C6.67024 12.038 6.35366 12.038 6.1584 11.8427C5.96314 11.6474 5.96314 11.3309 6.1584 11.1356L9.80485 7.48916L6.1584 3.84271C5.96314 3.64745 5.96314 3.33087 6.1584 3.1356Z" fill="currentColor" fillRule="evenodd" clipRule="evenodd"></path></svg>
-              </Button>
+              <Button variant="ghost" size="icon" className="h-4 w-4" aria-label="View more">
+                <svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" aria-hidden="true"><path d="M6.1584 3.1356C6.35366 2.94034 6.67024 2.94034 6.8655 3.1356L10.8655 7.13561C11.0608 7.33087 11.0608 7.64745 10.8655 7.84271L6.8655 11.8427C6.67024 12.038 6.35366 12.038 6.1584 11.8427C5.96314 11.6474 5.96314 11.3309 6.1584 11.1356L9.80485 7.48916L6.1584 3.84271C5.96314 3.64745 5.96314 3.33087 6.1584 3.1356Z" fill="currentColor" fillRule="evenodd" clipRule="evenodd"></path></svg>
+              </Button>

Alternatively, consider using a lucide-react icon (e.g., ChevronRight) for consistency with other icons in this file.

📝 Committable suggestion

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

Suggested change
<Button variant="ghost" size="icon" className="h-4 w-4">
<span className="sr-only">View more</span>
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" className="h-4 w-4"><path d="M6.1584 3.1356C6.35366 2.94034 6.67024 2.94034 6.8655 3.1356L10.8655 7.13561C11.0608 7.33087 11.0608 7.64745 10.8655 7.84271L6.8655 11.8427C6.67024 12.038 6.35366 12.038 6.1584 11.8427C5.96314 11.6474 5.96314 11.3309 6.1584 11.1356L9.80485 7.48916L6.1584 3.84271C5.96314 3.64745 5.96314 3.33087 6.1584 3.1356Z" fill="currentColor" fillRule="evenodd" clipRule="evenodd"></path></svg>
</Button>
<Button variant="ghost" size="icon" className="h-4 w-4" aria-label="View more">
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" aria-hidden="true"><path d="M6.1584 3.1356C6.35366 2.94034 6.67024 2.94034 6.8655 3.1356L10.8655 7.13561C11.0608 7.33087 11.0608 7.64745 10.8655 7.84271L6.8655 11.8427C6.67024 12.038 6.35366 12.038 6.1584 11.8427C5.96314 11.6474 5.96314 11.3309 6.1584 11.1356L9.80485 7.48916L6.1584 3.84271C5.96314 3.64745 5.96314 3.33087 6.1584 3.1356Z" fill="currentColor" fillRule="evenodd" clipRule="evenodd"></path></svg>
</Button>
🧰 Tools
🪛 Biome (2.1.2)

[error] 86-86: Alternative text title element cannot be empty

For accessibility purposes, SVGs should have an alternative text, provided via title element. If the svg element has role="img", you should add the aria-label or aria-labelledby attribute.

(lint/a11y/noSvgWithoutTitle)

🤖 Prompt for AI Agents
In `@components/usage-sidebar.tsx` around lines 84 - 87, The SVG used inside the
Button lacks accessible labeling; update the Button/SVG so screen readers get a
meaningful name: add an explicit aria-label (or aria-labelledby) on the Button
component (e.g., "View more") and mark the inline SVG as decorative with
aria-hidden="true" (or alternatively add a <title> inside the SVG and role="img"
on the SVG if you want the SVG itself to be announced). Locate the Button and
its child <svg> in components/usage-sidebar.tsx and apply one of these fixes (or
replace the inline SVG with the lucide-react ChevronRight icon for consistent,
accessible icons).

</div>

<Table>
<TableHeader>
<TableRow>
<TableHead className="text-xs">Details</TableHead>
<TableHead className="text-xs">Date</TableHead>
<TableHead className="text-xs text-right">Credits change</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{usage.map((item, i) => (
<TableRow key={i}>
<TableCell className="text-xs font-medium">{item.details}</TableCell>
<TableCell className="text-[10px] text-muted-foreground">{item.date}</TableCell>
<TableCell className="text-xs text-right font-medium">{item.change}</TableCell>
</TableRow>
))}
Comment on lines +99 to +105
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Using array index as React key.

Using i as the key can cause rendering issues if items are reordered or filtered. Once real data is integrated, use a unique identifier.

♻️ Suggested improvement
-                {usage.map((item, i) => (
-                  <TableRow key={i}>
+                {usage.map((item) => (
+                  <TableRow key={`${item.date}-${item.details}`}>
📝 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
{usage.map((item, i) => (
<TableRow key={i}>
<TableCell className="text-xs font-medium">{item.details}</TableCell>
<TableCell className="text-[10px] text-muted-foreground">{item.date}</TableCell>
<TableCell className="text-xs text-right font-medium">{item.change}</TableCell>
</TableRow>
))}
{usage.map((item) => (
<TableRow key={`${item.date}-${item.details}`}>
<TableCell className="text-xs font-medium">{item.details}</TableCell>
<TableCell className="text-[10px] text-muted-foreground">{item.date}</TableCell>
<TableCell className="text-xs text-right font-medium">{item.change}</TableCell>
</TableRow>
))}
🤖 Prompt for AI Agents
In `@components/usage-sidebar.tsx` around lines 99 - 105, The map over usage
currently uses the array index as the React key which can break reconciliation;
in the usage.map(...) replace key={i} on the TableRow with a stable unique
identifier from each item (e.g., item.id or another invariant like a
concatenation of item.date and item.details) so TableRow (and its children) use
a persistent key across reorders/filters; update any type/interface for the
items if needed to ensure the chosen unique field exists.

Comment on lines +99 to +105

Choose a reason for hiding this comment

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

Using array index (key={i}) for rows is fragile if the list ever changes order or items are inserted/removed, which can cause incorrect row reuse and subtle UI bugs. Prefer a stable key derived from the data (e.g., an id, or a composite key such as date+details if that’s unique).

Suggestion

Use a stable key. If you don’t have an id yet, add one to the mock data and keep it when moving to real data:

{usage.map((item) => (
  <TableRow key={item.id}>
    ...
  </TableRow>
))}

Reply with "@CharlieHelps yes please" if you'd like me to add a commit that introduces stable keys for usage rows.

</TableBody>
</Table>
</div>
</div>
</SheetContent>
</Sheet>
);
}