-
-
Notifications
You must be signed in to change notification settings - Fork 7
Integrate billing popup, usage sidebar, and credit preview toggle #441
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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' | ||
|
|
||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. SuggestionGate 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 |
||
| 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="/"> | ||
|
|
@@ -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 /> | ||
|
|
||
|
|
@@ -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> | ||
| </> | ||
| ) | ||
| } | ||
|
|
||
|
|
||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Comment on lines
+21
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Opening a new tab via SuggestionUse the third parameter to disable opener, and consider window.open(url, '_blank', 'noopener,noreferrer')Alternatively, render a normal |
||
|
|
||
| return ( | ||
| <Dialog open={isOpen} onOpenChange={onClose}> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
SuggestionHandle the boolean argument and only close when <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'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> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -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(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <p className="text-[10px] text-muted-foreground">Upgrade to get more credits</p> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+119
to
+143
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 Additionally, consider making "Upgrade to get more credits" an interactive element (link or button) that triggers the ♻️ 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
Suggested change
🤖 Prompt for AI Agents
Comment on lines
+118
to
+143
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The credits UI in the chat history is currently hard-coded to SuggestionAt minimum, extract constants and label as placeholder to avoid implying live data: const availableCredits = 0 // TODO: wire to billing API
const percent = 0Or 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 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </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"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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[]>([]); | ||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Avoid Using ♻️ 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
| const [credits, setCredits] = useState(0); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
Comment on lines
+19
to
+22
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This introduces SuggestionReplace 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Also, SuggestionReplace 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 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}> | ||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
SuggestionMirror 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
SuggestionAdd 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> | ||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 ♻️ Suggested refactorCreate 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 |
||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
|
Comment on lines
+48
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The Upgrade button again uses SuggestionUse a safe open: window.open('https://buy.stripe.com/...', '_blank', 'noopener,noreferrer')Or render an |
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| <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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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 📝 Committable suggestion
Suggested change
🧰 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 |
||||||||||||||||||||||||||||||
| </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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Using array index as React key. Using ♻️ Suggested improvement- {usage.map((item, i) => (
- <TableRow key={i}>
+ {usage.map((item) => (
+ <TableRow key={`${item.date}-${item.details}`}>📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
Comment on lines
+99
to
+105
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using array index ( SuggestionUse a stable key. If you don’t have an {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> | ||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Minor import inconsistency.
useStateanduseEffectare imported separately from theReactimport on line 2. Consider consolidating for consistency.♻️ Suggested consolidation
🤖 Prompt for AI Agents