Skip to content

Commit efd1989

Browse files
committed
feat: add usage tracking, password template, and fix folder pagination
- Add usage tracking UI with storage and notes limits display - Create UsageDialog and UsageSection components with progress bars - Add password entry template to note templates dropdown - Fix folder pagination to fetch all folders across multiple pages - Fix note folder data preservation after API updates - Remove unused SettingsDialog component
1 parent 0e20d80 commit efd1989

File tree

7 files changed

+337
-14
lines changed

7 files changed

+337
-14
lines changed

src/components/folders/index.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import FolderDeleteModal from '@/components/folders/modals/FolderDeleteModal';
66
import EditFolderModal from '@/components/folders/modals/EditFolderModal';
77
import MoveFolderModal from '@/components/folders/modals/MoveFolderModal';
88
import { ChangeMasterPasswordDialog } from '@/components/password/ChangeMasterPasswordDialog';
9+
import { UsageDialog } from '@/components/settings/UsageDialog';
910
import { Button } from '@/components/ui/button';
1011
import { Input } from '@/components/ui/input';
1112
import { ThemeToggle } from '@/components/ui/theme-toggle';
@@ -86,6 +87,7 @@ export default function FolderPanel({
8687
string | null
8788
>(null);
8889
const [showChangePassword, setShowChangePassword] = useState(false);
90+
const [showUsage, setShowUsage] = useState(false);
8991

9092
const hasPassword = user?.id ? hasMasterPassword(user.id) : false;
9193

@@ -301,6 +303,26 @@ export default function FolderPanel({
301303
afterSignOutUrl="/"
302304
>
303305
<UserButton.MenuItems>
306+
<UserButton.Action
307+
label="Usage"
308+
labelIcon={
309+
<svg
310+
xmlns="http://www.w3.org/2000/svg"
311+
width="16"
312+
height="16"
313+
viewBox="0 0 24 24"
314+
fill="none"
315+
stroke="currentColor"
316+
strokeWidth="2"
317+
strokeLinecap="round"
318+
strokeLinejoin="round"
319+
>
320+
<path d="M3 3v18h18"/>
321+
<path d="m19 9-5 5-4-4-3 3"/>
322+
</svg>
323+
}
324+
onClick={() => setShowUsage(true)}
325+
/>
304326
<UserButton.Action
305327
label="Typelets Open Source"
306328
labelIcon={
@@ -457,6 +479,11 @@ export default function FolderPanel({
457479
onOpenChange={setShowChangePassword}
458480
onSuccess={handlePasswordChangeSuccess}
459481
/>
482+
483+
<UsageDialog
484+
open={showUsage}
485+
onOpenChange={setShowUsage}
486+
/>
460487
</>
461488
);
462489
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import * as Dialog from '@radix-ui/react-dialog';
2+
import { BarChart3, X } from 'lucide-react';
3+
import { Button } from '@/components/ui/button';
4+
import { UsageSection } from './UsageSection';
5+
6+
interface UsageDialogProps {
7+
open: boolean;
8+
onOpenChange: (open: boolean) => void;
9+
}
10+
11+
export function UsageDialog({ open, onOpenChange }: UsageDialogProps) {
12+
return (
13+
<Dialog.Root open={open} onOpenChange={onOpenChange}>
14+
<Dialog.Portal>
15+
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/80" />
16+
<Dialog.Content className="bg-background fixed top-[50%] left-[50%] z-50 max-h-[85vh] w-[90vw] max-w-[500px] translate-x-[-50%] translate-y-[-50%] gap-4 overflow-hidden rounded-lg border shadow-lg">
17+
{/* Header */}
18+
<div className="border-border flex items-center justify-between border-b p-4">
19+
<div className="space-y-1">
20+
<div className="flex items-center gap-2">
21+
<BarChart3 className="h-5 w-5" />
22+
<Dialog.Title className="text-lg font-semibold">Usage & Limits</Dialog.Title>
23+
</div>
24+
<Dialog.Description className="text-muted-foreground text-sm">
25+
Monitor your account usage and limits
26+
</Dialog.Description>
27+
</div>
28+
</div>
29+
30+
{/* Content */}
31+
<div className="p-4 min-h-[400px]">
32+
<UsageSection />
33+
</div>
34+
35+
<Dialog.Close asChild>
36+
<Button
37+
variant="outline"
38+
size="icon"
39+
className="absolute top-4 right-4"
40+
aria-label="Close"
41+
>
42+
<X className="h-4 w-4" />
43+
</Button>
44+
</Dialog.Close>
45+
</Dialog.Content>
46+
</Dialog.Portal>
47+
</Dialog.Root>
48+
);
49+
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import { useEffect, useState } from 'react';
2+
import { HardDrive, FileText, AlertCircle } from 'lucide-react';
3+
import { Progress } from '@/components/ui/progress';
4+
import { api, type ApiUserUsage } from '@/lib/api/api';
5+
import { cn } from '@/lib/utils';
6+
7+
interface UsageSectionProps {
8+
className?: string;
9+
}
10+
11+
export function UsageSection({ className }: UsageSectionProps) {
12+
const [usage, setUsage] = useState<ApiUserUsage | null>(null);
13+
const [loading, setLoading] = useState(true);
14+
const [error, setError] = useState<string | null>(null);
15+
16+
useEffect(() => {
17+
fetchUsage();
18+
}, []);
19+
20+
const fetchUsage = async () => {
21+
try {
22+
setLoading(true);
23+
setError(null);
24+
const user = await api.getCurrentUser(true);
25+
if (user.usage) {
26+
setUsage(user.usage);
27+
}
28+
} catch (err) {
29+
console.error('Failed to fetch usage data:', err);
30+
setError('Failed to load usage data');
31+
} finally {
32+
setLoading(false);
33+
}
34+
};
35+
36+
37+
const formatBytes = (bytes: number) => {
38+
if (bytes < 1024 * 1024) {
39+
return `${(bytes / 1024).toFixed(1)} KB`;
40+
}
41+
if (bytes < 1024 * 1024 * 1024) {
42+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
43+
}
44+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
45+
};
46+
47+
if (loading) {
48+
return (
49+
<div className={cn('flex items-center justify-center h-full min-h-[350px]', className)}>
50+
<div className="h-8 w-8 animate-spin rounded-full border-4 border-muted border-t-primary" />
51+
</div>
52+
);
53+
}
54+
55+
if (error || !usage) {
56+
return (
57+
<div className={cn('space-y-4', className)}>
58+
<h3 className="text-lg font-semibold">Usage & Limits</h3>
59+
<div className="rounded-lg border border-destructive/50 bg-destructive/10 p-4">
60+
<div className="flex items-center gap-2 text-destructive">
61+
<AlertCircle className="h-4 w-4" />
62+
<p className="text-sm">{error || 'Usage data not available'}</p>
63+
</div>
64+
</div>
65+
</div>
66+
);
67+
}
68+
69+
return (
70+
<div className={cn('space-y-4', className)}>
71+
<h3 className="text-lg font-semibold">Usage & Limits</h3>
72+
73+
<div className="space-y-4">
74+
{/* Storage Usage */}
75+
<div className="rounded-lg border p-4 space-y-3">
76+
<div className="flex items-center justify-between">
77+
<div className="flex items-center gap-2">
78+
<HardDrive className="h-4 w-4 text-muted-foreground" />
79+
<span className="font-medium">Storage</span>
80+
</div>
81+
<span className="text-sm text-muted-foreground">
82+
{formatBytes(usage.storage.totalBytes)} / {usage.storage.limitGB} GB
83+
</span>
84+
</div>
85+
86+
<Progress
87+
value={usage.storage.usagePercent}
88+
className="h-2"
89+
/>
90+
91+
<div className="flex justify-between text-xs text-muted-foreground">
92+
<span>{usage.storage.usagePercent.toFixed(1)}% used</span>
93+
<span>{formatBytes((usage.storage.limitGB * 1024 * 1024 * 1024) - usage.storage.totalBytes)} free</span>
94+
</div>
95+
96+
{usage.storage.usagePercent >= 80 && (
97+
<div className={cn(
98+
'text-xs p-2 rounded-md',
99+
usage.storage.usagePercent >= 95
100+
? 'bg-red-50 text-red-700 dark:bg-red-900/20 dark:text-red-400'
101+
: 'bg-yellow-50 text-yellow-700 dark:bg-yellow-900/20 dark:text-yellow-400'
102+
)}>
103+
{usage.storage.usagePercent >= 95
104+
? '⚠️ Storage almost full. Consider upgrading soon.'
105+
: '📊 Storage usage is getting high.'}
106+
</div>
107+
)}
108+
</div>
109+
110+
{/* Notes Count */}
111+
<div className="rounded-lg border p-4 space-y-3">
112+
<div className="flex items-center justify-between">
113+
<div className="flex items-center gap-2">
114+
<FileText className="h-4 w-4 text-muted-foreground" />
115+
<span className="font-medium">Notes</span>
116+
</div>
117+
<span className="text-sm text-muted-foreground">
118+
{usage.notes.count} / {usage.notes.limit}
119+
</span>
120+
</div>
121+
122+
<Progress
123+
value={usage.notes.usagePercent}
124+
className="h-2"
125+
/>
126+
127+
<div className="flex justify-between text-xs text-muted-foreground">
128+
<span>{usage.notes.usagePercent.toFixed(1)}% used</span>
129+
<span>{usage.notes.limit - usage.notes.count} notes remaining</span>
130+
</div>
131+
132+
{usage.notes.usagePercent >= 80 && (
133+
<div className={cn(
134+
'text-xs p-2 rounded-md',
135+
usage.notes.usagePercent >= 95
136+
? 'bg-red-50 text-red-700 dark:bg-red-900/20 dark:text-red-400'
137+
: 'bg-yellow-50 text-yellow-700 dark:bg-yellow-900/20 dark:text-yellow-400'
138+
)}>
139+
{usage.notes.usagePercent >= 95
140+
? '⚠️ Note limit almost reached. Consider upgrading soon.'
141+
: '📝 Approaching note limit.'}
142+
</div>
143+
)}
144+
</div>
145+
</div>
146+
147+
{/* Plan Information */}
148+
<div className="rounded-lg bg-muted/50 p-3">
149+
<p className="text-xs text-muted-foreground">
150+
You're on the <span className="font-medium">Free Plan</span>.
151+
{(usage.storage.usagePercent >= 80 || usage.notes.usagePercent >= 80) && (
152+
<span> Consider upgrading for more storage and unlimited notes.</span>
153+
)}
154+
</p>
155+
</div>
156+
</div>
157+
);
158+
}

src/components/ui/separator.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import * as React from "react"
2+
3+
import { cn } from "@/lib/utils"
4+
5+
interface SeparatorProps extends React.HTMLAttributes<HTMLDivElement> {
6+
orientation?: "horizontal" | "vertical";
7+
decorative?: boolean;
8+
}
9+
10+
const Separator = React.forwardRef<HTMLDivElement, SeparatorProps>(
11+
(
12+
{ className, orientation = "horizontal", decorative = true, ...props },
13+
ref
14+
) => (
15+
<div
16+
ref={ref}
17+
role={decorative ? "none" : "separator"}
18+
aria-orientation={orientation}
19+
className={cn(
20+
"shrink-0 bg-border",
21+
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
22+
className
23+
)}
24+
{...props}
25+
/>
26+
)
27+
)
28+
Separator.displayName = "Separator"
29+
30+
export { Separator }

src/constants/templates.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,11 @@ export const NOTE_TEMPLATES: NoteTemplate[] = [
4242
title: 'Research: [Topic]',
4343
content: `<h1>Research: [Topic]</h1><h2>Research Question</h2><p><em>What am I trying to find out?</em></p><p></p><h2>Sources</h2><ul><li><p></p></li><li><p></p></li><li><p></p></li></ul><h2>Key Findings</h2><h3>Finding 1</h3><p></p><h3>Finding 2</h3><p></p><h3>Finding 3</h3><p></p><h2>Summary &amp; Conclusions</h2><p></p><h2>Next Steps</h2><ul data-type="taskList"><li data-type="taskItem" data-checked="false"><label><input type="checkbox"><span></span></label><div><p></p></div></li><li data-type="taskItem" data-checked="false"><label><input type="checkbox"><span></span></label><div><p></p></div></li></ul><hr><p><em>Research conducted on: ${new Date().toLocaleDateString()}</em></p>`,
4444
},
45+
{
46+
id: 'password-entry',
47+
name: 'Password Entry',
48+
description: 'Secure template for storing credentials',
49+
title: '🔐 Password Entry',
50+
content: `<h1>🔐 Password Entry</h1><p><strong>Service/Website:</strong> [Enter service name]<br><strong>Username/Email:</strong> [Enter username or email]<br><strong>Password:</strong> [Enter password - this note is encrypted]<br><strong>URL:</strong> [Enter website URL]</p><h2>Additional Information</h2><ul><li><p><strong>Security Questions:</strong><br>Q: [Question 1]<br>A: [Answer 1]</p></li><li><p><strong>Backup Codes:</strong> [Enter backup codes if any]</p></li><li><p><strong>Notes:</strong> [Any additional notes]</p></li></ul><h2>Recovery Information</h2><ul><li><p><strong>Recovery Email:</strong> [Recovery email if different]</p></li><li><p><strong>Recovery Phone:</strong> [Recovery phone number]</p></li></ul><hr><p><em>Last Updated: ${new Date().toLocaleDateString()}</em><br><em>Created: ${new Date().toLocaleDateString()}</em></p>`,
51+
},
4552
];

0 commit comments

Comments
 (0)