Skip to content
Closed
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
39 changes: 21 additions & 18 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { SpeedInsights } from "@vercel/speed-insights/next"
import { Toaster } from '@/components/ui/sonner'
import { MapToggleProvider } from '@/components/map-toggle-context'
import { ProfileToggleProvider } from '@/components/profile-toggle-context'
import { UsageToggleProvider } from '@/components/usage-toggle-context'
import { CalendarToggleProvider } from '@/components/calendar-toggle-context'
import { MapLoadingProvider } from '@/components/map-loading-context';
import ConditionalLottie from '@/components/conditional-lottie';
Expand Down Expand Up @@ -72,24 +73,26 @@ export default function RootLayout({
<CalendarToggleProvider>
<MapToggleProvider>
<ProfileToggleProvider>
<ThemeProvider
attribute="class"
defaultTheme="earth"
enableSystem
disableTransitionOnChange
themes={['light', 'dark', 'earth']}
>
<MapContextProvider>
<MapLoadingProvider>
<Header />
<ConditionalLottie />
{children}
<Sidebar />
<Footer />
<Toaster />
</MapLoadingProvider>
</MapContextProvider>
</ThemeProvider>
<UsageToggleProvider>
<ThemeProvider
attribute="class"
defaultTheme="earth"
enableSystem
disableTransitionOnChange
themes={['light', 'dark', 'earth']}
>
<MapContextProvider>
<MapLoadingProvider>
<Header />
<ConditionalLottie />
{children}
<Sidebar />
<Footer />
<Toaster />
</MapLoadingProvider>
</MapContextProvider>
</ThemeProvider>
</UsageToggleProvider>
</ProfileToggleProvider>
</MapToggleProvider>
</CalendarToggleProvider>
Expand Down
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.

7 changes: 5 additions & 2 deletions components/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import { MapProvider } from './map/map-provider'
import { useUIState, useAIState } from 'ai/rsc'
import MobileIconsBar from './mobile-icons-bar'
import { useProfileToggle, ProfileToggleEnum } from "@/components/profile-toggle-context";
import { useUsageToggle } from "@/components/usage-toggle-context";
import SettingsView from "@/components/settings/settings-view";
import { UsageView } from "@/components/usage-view";
import { MapDataProvider, useMapData } from './map/map-data-context'; // Add this and useMapData
import { updateDrawingContext } from '@/lib/actions/chat'; // Import the server action
import dynamic from 'next/dynamic'
Expand All @@ -31,6 +33,7 @@ export function Chat({ id }: ChatProps) {
const [aiState] = useAIState()
const [isMobile, setIsMobile] = useState(false)
const { activeView } = useProfileToggle();
const { isUsageOpen } = useUsageToggle();
const { isCalendarOpen } = useCalendarToggle()
const [input, setInput] = useState('')
const [showEmptyScreen, setShowEmptyScreen] = useState(false)
Expand Down Expand Up @@ -107,7 +110,7 @@ export function Chat({ id }: ChatProps) {
<HeaderSearchButton />
<div className="mobile-layout-container">
<div className="mobile-map-section">
{activeView ? <SettingsView /> : <MapProvider />}
{isUsageOpen ? <UsageView /> : activeView ? <SettingsView /> : <MapProvider />}
</div>
<div className="mobile-icons-bar">
<MobileIconsBar onAttachmentClick={handleAttachment} onSubmitClick={handleMobileSubmit} />
Expand Down Expand Up @@ -218,7 +221,7 @@ export function Chat({ id }: ChatProps) {
className="w-1/2 p-4 fixed h-[calc(100vh-0.5in)] top-0 right-0 mt-[0.5in]"
style={{ zIndex: 10 }} // Added z-index
>
{activeView ? <SettingsView /> : <MapProvider />}
{isUsageOpen ? <UsageView /> : activeView ? <SettingsView /> : <MapProvider />}
</div>
</div>
</MapDataProvider>
Expand Down
32 changes: 21 additions & 11 deletions components/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,23 @@ import {
} from 'lucide-react'
import { MapToggle } from './map-toggle'
import { ProfileToggle } from './profile-toggle'
import { PurchaseCreditsPopup } from './purchase-credits-popup'
import { useUsageToggle } from './usage-toggle-context'
import { useState, useEffect } from 'react'

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

useEffect(() => {
// Open payment popup as soon as application opens
setIsPurchaseOpen(true)
}, [])
Comment on lines +24 to +30
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid auto-opening the purchase popup on every mount.

Unconditional useEffect opens the dialog for all users and bypasses existing eligibility checks (auth + cooldown + credit state). Gate this behind an explicit user action or a purchase-eligibility signal before calling setIsPurchaseOpen(true). Based on learnings, this popup is intended to appear only after authentication with a 7‑day cooldown.

🤖 Prompt for AI Agents
In `@components/header.tsx` around lines 24 - 30, The effect that unconditionally
calls setIsPurchaseOpen(true) on mount must be removed and replaced with a gated
flow: stop auto-opening in the current useEffect and only call
setIsPurchaseOpen(true) after verifying purchase eligibility (authenticate the
user, verify the 7-day cooldown, and check credit state) or in response to an
explicit user action; update the component to either accept a purchaseEligible
boolean/signal or add a gated useEffect that waits for auth + cooldown + credits
before opening, referencing isPurchaseOpen, setIsPurchaseOpen, and the existing
useUsageToggle hook to trigger the dialog only when those checks pass.


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.

PurchaseCreditsPopup is forced open on every app load via useEffect(() => setIsPurchaseOpen(true), []). That’s a major UX regression and likely contradicts the stated goal (“Fix Usage View Integration”)—users will be blocked by a payment modal regardless of credit state. This also risks inflating conversion metrics artificially and breaking flows (e.g., deep links) because the header always mounts.

Suggestion

Gate the popup behind an explicit condition (e.g., user is out of credits, or a query param like ?upgrade=1, or an intentional click). For example:

const [isPurchaseOpen, setIsPurchaseOpen] = useState(false)

// Example: open only when a flag is set
useEffect(() => {
  if (shouldPromptForUpgrade) setIsPurchaseOpen(true)
}, [shouldPromptForUpgrade])

Or remove the auto-open entirely and open it only from UsageView/credits UI actions.

Reply with "@CharlieHelps yes please" if you’d like me to add a commit implementing a safe gating approach (query-param or credit-based).

return (
<>
<PurchaseCreditsPopup isOpen={isPurchaseOpen} onClose={() => setIsPurchaseOpen(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 +66,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={toggleUsage}>
<TentTree className="h-[1.2rem] w-[1.2rem]" />
</Button>
Comment on lines +69 to +71
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

Add accessible labels to icon-only usage buttons.

Icon-only buttons need aria-label/title for screen readers. Apply this to both desktop and mobile usage toggles.

♿ Proposed fix
-        <Button variant="ghost" size="icon" onClick={toggleUsage}>
+        <Button variant="ghost" size="icon" onClick={toggleUsage} aria-label="Open usage" title="Open usage">
           <TentTree className="h-[1.2rem] w-[1.2rem]" />
         </Button>
...
-        <Button variant="ghost" size="icon" onClick={toggleUsage}>
+        <Button variant="ghost" size="icon" onClick={toggleUsage} aria-label="Open usage" title="Open usage">
           <TentTree className="h-[1.2rem] w-[1.2rem]" />
         </Button>

Also applies to: 81-83

🤖 Prompt for AI Agents
In `@components/header.tsx` around lines 69 - 71, The icon-only usage toggle
Buttons lack accessible labels; update the Button components that call
toggleUsage (desktop) and the corresponding mobile usage toggle (the other
Button controlling usage, e.g., the one at lines referenced 81-83) to include an
appropriate aria-label and/or title (for example "Toggle usage" or more specific
text) so screen readers can identify the control; ensure both Button instances
that render the TentTree icon receive the same accessible label props.


<ModeToggle />

Expand All @@ -67,15 +78,14 @@ 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={toggleUsage}>
<TentTree className="h-[1.2rem] w-[1.2rem]" />
</Button>
<ProfileToggle/>
</div>
</header>
</>
)
}

export default Header
export default Header
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 +21 to +23
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

MDN window.open noopener noreferrer reverse tabnabbing security

💡 Result:

To prevent reverse tabnabbing, you need to ensure the newly opened page cannot access window.opener (because even cross-origin, it can often still navigate the opener via window.opener.location, enabling phishing-style redirects) [3].

Recommended patterns (per MDN)

  • Links (<a target="_blank">): add rel="noopener" to stop the new page from getting an opener reference (window.opener becomes null) [2].
    • If you also want to suppress the Referer header, use rel="noreferrer" (which also implies noopener) [1].
  • JavaScript window.open: pass noopener (or noreferrer) in the windowFeatures string:
    • window.open(url, '_blank', 'noopener')window.opener is null in the opened page [4].
    • window.open(url, '_blank', 'noreferrer') → omits the Referer header and sets noopener [4], [1].

Sources

[1] MDN: rel="noreferrer"
[2] MDN: rel="noopener"
[3] MDN: Window.opener (security implications)
[4] MDN: Window.open() (features noopener, noreferrer)


Harden window.open against reverse-tabnabbing.
Add noopener,noreferrer to prevent the opened page from accessing window.opener and potentially redirecting the original tab via phishing attacks.

Suggested fix
-    window.open('https://buy.stripe.com/3cIaEX3tRcur9EM7tbasg00', '_blank');
+    window.open(
+      'https://buy.stripe.com/3cIaEX3tRcur9EM7tbasg00',
+      '_blank',
+      'noopener,noreferrer'
+    );
📝 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 handlePurchase = () => {
window.open('https://buy.stripe.com/3cIaEX3tRcur9EM7tbasg00', '_blank');
onClose();
const handlePurchase = () => {
window.open(
'https://buy.stripe.com/3cIaEX3tRcur9EM7tbasg00',
'_blank',
'noopener,noreferrer'
);
onClose();
🤖 Prompt for AI Agents
In `@components/purchase-credits-popup.tsx` around lines 21 - 23, The
handlePurchase function uses window.open and is vulnerable to
reverse-tabnabbing; update the call in handlePurchase to pass the
noopener,noreferrer feature string as the third argument (e.g., window.open(url,
'_blank', 'noopener,noreferrer')) and, for extra safety, capture the returned
window object and if non-null set newWindow.opener = null; update the function
name handlePurchase to use this pattern to ensure the opened page cannot access
or redirect the original tab.

};
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 Stripe with window.open(url, '_blank') without noopener,noreferrer allows reverse-tabnabbing and exposes window.opener. The anchor-based version you removed had the correct rel attributes; this should preserve that security posture.

Suggestion

Prefer an <a target="_blank" rel="noopener noreferrer"> around the button, or explicitly set noopener when using window.open:

const w = window.open(url, '_blank', 'noopener,noreferrer')
w?.opener = null

Reply with "@CharlieHelps yes please" if you’d like me to add a commit updating both PurchaseCreditsPopup and UsageView to use a safer pattern.


return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="sm:max-w-[425px]">
Comment on lines +27 to +28

Choose a reason for hiding this comment

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

Dialog’s onOpenChange receives the next open state ((open: boolean) => void). Wiring it directly to onClose (no args) means the dialog will call onClose(true) when opening and will also close immediately if parent doesn’t keep isOpen in sync. This often results in flaky open/close behavior and makes the component hard to reuse.

Suggestion

Handle onOpenChange properly by only invoking onClose() when the dialog is being closed:

<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.

<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>
<p className="text-[10px] text-muted-foreground">Upgrade to get more credits</p>
</div>
)}
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 new “Credits Preview” UI is hardcoded to 0 credits and 0% progress. If this is meant as a placeholder, it should be clearly marked or hidden behind feature flags; otherwise it will confuse users and doesn’t reflect actual usage/credit state.

Suggestion

Drive the preview from real credit data (context, server call, or a prop) and render an explicit loading/unknown state when unavailable. If the data isn’t ready yet, consider removing the UI until it’s wired.

Reply with "@CharlieHelps yes please" if you’d like me to add a commit that adds a minimal credits?: number plumbing point and an empty/loading state to avoid showing misleading 0 by default.

Comment on lines +118 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.

⚠️ Potential issue | 🟠 Major

Credits preview is hardcoded (will show incorrect data).
The panel always displays 0 credits and 0% progress, which will mislead users once real credits exist. Please wire this to the actual credits source or hide the panel until data is available. Based on learnings, consider reusing the existing credits/usage display component rather than duplicating a static placeholder.

🤖 Prompt for AI Agents
In `@components/sidebar/chat-history-client.tsx` around lines 118 - 143, The
Credits Preview currently renders hardcoded values ("0" and w-[0%]) inside the
ChatHistoryClient component toggled by isCreditsVisible; replace the static
values by wiring to the real credits state (e.g., read from the existing
credits/usage hook, context, or prop such as useCredits/getUserCredits) and
compute the progress percentage for the progress bar instead of w-[0%], or
alternatively render the existing reusable CreditsDisplay/CreditsUsage component
instead of duplicating markup; also ensure to conditionally hide the panel until
credits data is loaded (check a loading flag or null/undefined) to avoid showing
incorrect data when not available.

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.

⚠️ Potential issue | 🟡 Minor

Add disclosure a11y state on the toggle button.
Expose expanded state and link it to the panel for screen readers.

🔧 Suggested diff
-        <Button
+        <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"
         >
@@
-        {isCreditsVisible && (
-          <div className="mt-2 p-3 rounded-lg bg-muted/50 border border-border/50 space-y-2">
+        {isCreditsVisible && (
+          <div
+            id="credits-preview-panel"
+            className="mt-2 p-3 rounded-lg bg-muted/50 border border-border/50 space-y-2"
+          >
📝 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>
<p className="text-[10px] text-muted-foreground">Upgrade to get more credits</p>
</div>
)}
🤖 Prompt for AI Agents
In `@components/sidebar/chat-history-client.tsx` around lines 119 - 143, The
toggle Button that shows/hides the credits panel (uses isCreditsVisible and
setIsCreditsVisible) needs disclosure accessibility: add
aria-expanded={isCreditsVisible} to the Button and aria-controls pointing to the
panel's id (create a stable id like "credits-panel"), and on the panel div add
that id plus role="region" and aria-hidden={!isCreditsVisible} (or remove
aria-hidden when visible) so screen readers can associate the control with the
panel; update the Button markup where setIsCreditsVisible is used and the panel
div that is rendered when isCreditsVisible is true (the div with className "mt-2
p-3...") accordingly.

</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
28 changes: 28 additions & 0 deletions components/usage-toggle-context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use client'

import { createContext, useContext, useState, ReactNode } from "react"

interface UsageToggleContextType {
isUsageOpen: boolean
toggleUsage: () => void
}

const UsageToggleContext = createContext<UsageToggleContextType | undefined>(undefined)

export const UsageToggleProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [isUsageOpen, setIsUsageOpen] = useState(false)

const toggleUsage = () => setIsUsageOpen(prev => !prev)

return (
<UsageToggleContext.Provider value={{ isUsageOpen, toggleUsage }}>
{children}
</UsageToggleContext.Provider>
)
}

export const useUsageToggle = () => {
const context = useContext(UsageToggleContext)
if (!context) throw new Error('useUsageToggle must be used within UsageToggleProvider')
return context
}
96 changes: 96 additions & 0 deletions components/usage-view.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
'use client'

import React, { useEffect, useState } from 'react'
import { Button } from '@/components/ui/button'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { Zap, RefreshCw, LayoutPanelLeft, Minus } from 'lucide-react'
import { useUsageToggle } from './usage-toggle-context'

export function UsageView() {
const [usage] = useState([
{ 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 },
])
const [credits] = useState(0)
Comment on lines +10 to +15
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Usage data is hardcoded (view will always be wrong).
The credits, daily refresh, and usage history are static and will misrepresent real usage. Please source these from the backend (or pass them in as props) before shipping the feature.

Also applies to: 40-61, 83-88

🤖 Prompt for AI Agents
In `@components/usage-view.tsx` around lines 10 - 15, The component currently
hardcodes usage and credits via useState (const [usage] = useState(...), const
[credits] = useState(0)) which misrepresents real data; change the component to
accept these values as props (e.g., usageHistory, credits, dailyRefresh) or
fetch them from the backend in an effect (e.g., useEffect calling
fetchUsage()/getUsage()) and set them via setUsage/setCredits, replacing the
static arrays/zeros and removing the hardcoded defaults; ensure the same fix is
applied to the other occurrences referenced around the component (lines ~40-61
and ~83-88) so the view reflects live backend data or passed-in props.

const { toggleUsage } = useUsageToggle()
Comment on lines +3 to +16

Choose a reason for hiding this comment

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

UsageView currently hardcodes both credits and usage data (including future-dated timestamps). Shipping placeholder usage/credits to production is misleading and will create support issues. Also, useEffect is imported but unused, and the data is stored in state even though it never changes.

Suggestion

Either wire this to real data (server-provided props / fetch hook) or make it explicitly empty/loading until data exists. At minimum, remove unused imports and avoid useState for constants:

const usage = [] as Array<{details: string; date: string; change: number}>
const credits = 0

If real data is available elsewhere, pass it in as props to keep the component presentational.

Reply with "@CharlieHelps yes please" if you’d like me to add a commit that removes placeholder state, deletes the unused import, and adds a simple empty-state UI.


return (
<div className="container py-8 h-full overflow-y-auto">
<div className="flex justify-between items-center mb-8">
<div>
<h1 className="text-3xl font-bold tracking-tight">Usage</h1>
<p className="text-muted-foreground">Track your credits and usage history</p>
</div>
<Button variant="ghost" size="icon" onClick={toggleUsage}>
<Minus className="h-6 w-6" />
<span className="sr-only">Close usage</span>
</Button>
</div>

<div className="space-y-6">
<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
Comment on lines +35 to +36
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

MDN window.open noopener noreferrer reverse tabnabbing security

💡 Result:

Reverse tabnabbing happens when you open an untrusted page in a new tab/window and that page can use window.opener to redirect your original tab to a phishing URL (e.g., window.opener.location = ...). OWASP describes this attack and notes it’s largely mitigated in modern browsers due to implicit noopener for target="_blank", but it can still matter for older/legacy browsers and for explicit window.open() usage. [4]

Fix for <a target="_blank">

Use rel="noopener" (prevents window.opener from being set) [1] and optionally rel="noreferrer" (also implies noopener, and additionally suppresses the Referer header). [2]

<a href="https://example.com" target="_blank" rel="noopener noreferrer">
  External link
</a>

MDN also notes that many browsers now treat target="_blank" as if rel="noopener" were set, but adding it explicitly is still the common hardening pattern. [1]

Fix for window.open()

When using window.open, pass the noopener and/or noreferrer window features:

window.open("https://example.com", "_blank", "noopener,noreferrer");

MDN: noopener makes the opened window’s Window.opener null, and noreferrer omits the Referer header and “also set[s] noopener to true”. [3]

Sources: MDN on rel="noopener" [1], MDN on rel="noreferrer" [2], MDN on Window.open() features (noopener, noreferrer) [3], OWASP on Reverse Tabnabbing [4].


Add noopener,noreferrer to hardening against reverse-tabnabbing.
Pass these window features as the third parameter to window.open() to prevent the opened page from accessing window.opener and suppress the Referer header. This mitigates reverse-tabnabbing attacks across all browsers and is especially important for explicit window.open() calls.

🔒 Suggested diff
-            <Button size="sm" className="rounded-full px-4" onClick={() => window.open('https://buy.stripe.com/3cIaEX3tRcur9EM7tbasg00', '_blank')}>
+            <Button
+              size="sm"
+              className="rounded-full px-4"
+              onClick={() =>
+                window.open(
+                  'https://buy.stripe.com/3cIaEX3tRcur9EM7tbasg00',
+                  '_blank',
+                  'noopener,noreferrer'
+                )
+              }
+            >
📝 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 size="sm" className="rounded-full px-4" onClick={() => window.open('https://buy.stripe.com/3cIaEX3tRcur9EM7tbasg00', '_blank')}>
Upgrade
<Button
size="sm"
className="rounded-full px-4"
onClick={() =>
window.open(
'https://buy.stripe.com/3cIaEX3tRcur9EM7tbasg00',
'_blank',
'noopener,noreferrer'
)
}
>
Upgrade
🤖 Prompt for AI Agents
In `@components/usage-view.tsx` around lines 35 - 36, Update the onClick handler
in the Button in usage-view.tsx (the inline arrow function passed to onClick) to
call window.open with a third argument including "noopener,noreferrer" (i.e.,
window.open(url, '_blank', 'noopener,noreferrer')) so the opened page cannot
access window.opener and the Referer header is suppressed; leave the existing
target '_blank' as the second argument.

</Button>
Comment on lines +35 to +37

Choose a reason for hiding this comment

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

UsageView opens Stripe via window.open(...) from an onClick handler, which repeats the reverse-tabnabbing issue and also makes it harder to test/override. The header previously used a plain <a> with rel="noopener noreferrer"; keeping links as links is more accessible and secure.

Suggestion

Use an anchor with proper rel attributes:

<a
  href="https://buy.stripe.com/3cIaEX3tRcur9EM7tbasg00"
  target="_blank"
  rel="noopener noreferrer"
>
  <Button size="sm" className="rounded-full px-4">Upgrade</Button>
</a>

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

</div>

<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>
</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>
))}
</TableBody>
</Table>
</div>
</div>
</div>
)
}