Skip to content

Commit 5100085

Browse files
committed
feat: annotate charts
1 parent 8e538cc commit 5100085

File tree

15 files changed

+1957
-17
lines changed

15 files changed

+1957
-17
lines changed

apps/dashboard/app/(main)/websites/[id]/_components/tabs/overview-tab.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
StatCard,
2323
UnauthorizedAccessError,
2424
} from '@/components/analytics';
25-
import { MetricsChart } from '@/components/charts/metrics-chart';
25+
import { MetricsChartWithAnnotations } from '@/components/charts/metrics-chart-with-annotations';
2626
import { BrowserIcon, OSIcon } from '@/components/icon';
2727
import { DataTable } from '@/components/table/data-table';
2828
import {
@@ -984,12 +984,18 @@ export function WebsiteOverviewTab({
984984
</div>
985985
</div>
986986
<div>
987-
<MetricsChart
987+
<MetricsChartWithAnnotations
988+
websiteId={websiteId}
988989
className="rounded border-0"
989990
data={chartData}
990991
height={350}
991992
isLoading={isLoading}
992993
onRangeSelect={setDateRangeAction}
994+
dateRange={{
995+
startDate: new Date(dateRange.start_date),
996+
endDate: new Date(dateRange.end_date),
997+
granularity: dateRange.granularity as 'hourly' | 'daily' | 'weekly' | 'monthly',
998+
}}
993999
/>
9941000
</div>
9951001
</div>
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
'use client';
2+
3+
import {
4+
NoteIcon,
5+
PencilIcon,
6+
TagIcon,
7+
TrashIcon,
8+
} from '@phosphor-icons/react';
9+
import { useState } from 'react';
10+
import { Button } from '@/components/ui/button';
11+
import { Badge } from '@/components/ui/badge';
12+
import {
13+
Sheet,
14+
SheetContent,
15+
SheetDescription,
16+
SheetHeader,
17+
SheetTitle,
18+
SheetTrigger,
19+
} from '@/components/ui/sheet';
20+
import {
21+
AlertDialog,
22+
AlertDialogAction,
23+
AlertDialogCancel,
24+
AlertDialogContent,
25+
AlertDialogDescription,
26+
AlertDialogFooter,
27+
AlertDialogHeader,
28+
AlertDialogTitle,
29+
} from '@/components/ui/alert-dialog';
30+
import type { Annotation } from '@/types/annotations';
31+
import { formatAnnotationDateRange } from '@/lib/annotation-utils';
32+
33+
// Using shared Annotation type from @/types/annotations
34+
35+
interface AnnotationsPanelProps {
36+
annotations: Annotation[];
37+
onEdit: (annotation: Annotation) => void;
38+
onDelete: (id: string) => Promise<void>;
39+
isDeleting?: boolean;
40+
}
41+
42+
export function AnnotationsPanel({
43+
annotations,
44+
onEdit,
45+
onDelete,
46+
isDeleting = false,
47+
}: AnnotationsPanelProps) {
48+
const [deleteId, setDeleteId] = useState<string | null>(null);
49+
const [isOpen, setIsOpen] = useState(false);
50+
51+
const handleDelete = async () => {
52+
if (deleteId) {
53+
await onDelete(deleteId);
54+
setDeleteId(null);
55+
}
56+
};
57+
58+
59+
return (
60+
<>
61+
<Sheet open={isOpen} onOpenChange={setIsOpen}>
62+
<SheetTrigger asChild>
63+
<Button variant="outline" size="sm" className="gap-2">
64+
<NoteIcon className="h-4 w-4" />
65+
Annotations ({annotations.length})
66+
</Button>
67+
</SheetTrigger>
68+
<SheetContent
69+
className="w-full overflow-y-auto p-4 sm:w-[60vw] sm:max-w-[600px]"
70+
side="right"
71+
>
72+
<SheetHeader className="space-y-3 border-border/50 border-b pb-6">
73+
<div className="flex items-center gap-3">
74+
<div className="rounded-xl border border-primary/20 bg-primary/10 p-3">
75+
<NoteIcon
76+
className="h-6 w-6 text-primary"
77+
size={16}
78+
weight="duotone"
79+
/>
80+
</div>
81+
<div>
82+
<SheetTitle className="font-semibold text-foreground text-xl">
83+
Chart Annotations ({annotations.length})
84+
</SheetTitle>
85+
<SheetDescription className="mt-1 text-muted-foreground">
86+
Manage your chart annotations. Click to edit or delete.
87+
</SheetDescription>
88+
</div>
89+
</div>
90+
</SheetHeader>
91+
92+
<div className="space-y-6 pt-6">
93+
{annotations.length === 0 ? (
94+
<div className="flex flex-col items-center justify-center py-12 text-center">
95+
<div className="rounded-full bg-muted p-4 mb-4">
96+
<NoteIcon className="h-8 w-8 text-muted-foreground" />
97+
</div>
98+
<p className="font-medium text-foreground">No annotations yet</p>
99+
<p className="text-sm text-muted-foreground mt-1">
100+
Drag on the chart to create your first annotation
101+
</p>
102+
</div>
103+
) : (
104+
annotations.map((annotation) => (
105+
<div
106+
key={annotation.id}
107+
className="group rounded-lg border border-border bg-background p-4 transition-all hover:border-primary/50 hover:shadow-sm"
108+
>
109+
<div className="flex items-start justify-between gap-3">
110+
<div className="flex-1 min-w-0">
111+
{/* Color indicator and date */}
112+
<div className="flex items-center gap-2 mb-2">
113+
<div
114+
className="h-3 w-3 rounded-full border-2 border-white shadow-sm"
115+
style={{ backgroundColor: annotation.color }}
116+
/>
117+
<span className="text-xs text-muted-foreground">
118+
{formatAnnotationDateRange(annotation.xValue, annotation.xEndValue)}
119+
</span>
120+
{annotation.annotationType === 'range' &&
121+
annotation.xEndValue &&
122+
new Date(annotation.xValue).getTime() !== new Date(annotation.xEndValue).getTime() && (
123+
<Badge variant="secondary" className="text-xs">
124+
Range
125+
</Badge>
126+
)}
127+
</div>
128+
129+
{/* Text */}
130+
<p className="text-sm text-foreground mb-2 break-words">
131+
{annotation.text}
132+
</p>
133+
134+
{/* Tags */}
135+
{annotation.tags && annotation.tags.length > 0 && (
136+
<div className="flex flex-wrap gap-1">
137+
{annotation.tags.map((tag) => (
138+
<Badge
139+
key={tag}
140+
variant="outline"
141+
className="text-xs"
142+
>
143+
<TagIcon className="h-3 w-3 mr-1" />
144+
{tag}
145+
</Badge>
146+
))}
147+
</div>
148+
)}
149+
</div>
150+
151+
{/* Actions */}
152+
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
153+
<Button
154+
variant="ghost"
155+
size="sm"
156+
className="h-8 w-8 p-0"
157+
onClick={() => {
158+
onEdit(annotation);
159+
setIsOpen(false);
160+
}}
161+
>
162+
<PencilIcon className="h-4 w-4" />
163+
</Button>
164+
<Button
165+
variant="ghost"
166+
size="sm"
167+
className="h-8 w-8 p-0 hover:bg-destructive hover:text-destructive-foreground"
168+
onClick={() => setDeleteId(annotation.id)}
169+
>
170+
<TrashIcon className="h-4 w-4" />
171+
</Button>
172+
</div>
173+
</div>
174+
</div>
175+
))
176+
)}
177+
</div>
178+
</SheetContent>
179+
</Sheet>
180+
181+
{/* Delete Confirmation Dialog */}
182+
<AlertDialog open={!!deleteId} onOpenChange={() => setDeleteId(null)}>
183+
<AlertDialogContent>
184+
<AlertDialogHeader>
185+
<AlertDialogTitle>Delete Annotation</AlertDialogTitle>
186+
<AlertDialogDescription>
187+
Are you sure you want to delete this annotation? This action cannot
188+
be undone.
189+
</AlertDialogDescription>
190+
</AlertDialogHeader>
191+
<AlertDialogFooter>
192+
<AlertDialogCancel>Cancel</AlertDialogCancel>
193+
<AlertDialogAction
194+
onClick={handleDelete}
195+
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
196+
disabled={isDeleting}
197+
>
198+
{isDeleting ? (
199+
<>
200+
<div className="h-4 w-4 mr-2 animate-spin rounded-full border-2 border-current border-t-transparent" />
201+
Deleting...
202+
</>
203+
) : (
204+
'Delete'
205+
)}
206+
</AlertDialogAction>
207+
</AlertDialogFooter>
208+
</AlertDialogContent>
209+
</AlertDialog>
210+
</>
211+
);
212+
}
213+

0 commit comments

Comments
 (0)