Skip to content

Commit 8583294

Browse files
chore: Enhanced Navigation & Analytics System with robust error handling and improved UX (#221)
1 parent 4d6a88b commit 8583294

5 files changed

Lines changed: 240 additions & 62 deletions

File tree

src/app/analytics/page.tsx

Lines changed: 162 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
AreaChart,
1414
Area,
1515
} from 'recharts';
16-
import { BarChart3, Download, FileSearch, Loader2, PieChart as PieChartIcon } from 'lucide-react';
16+
import { BarChart3, Download, FileSearch, Loader2, PieChart as PieChartIcon, RefreshCw } from 'lucide-react';
1717
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
1818
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
1919

@@ -40,24 +40,96 @@ export default function AnalyticsDashboard() {
4040
const [filterTheme, setFilterTheme] = useState<string>('all');
4141
const [filterTime, setFilterTime] = useState<string>('all');
4242
const [chartType, setChartType] = useState<string>('bar');
43+
const [refreshKey, setRefreshKey] = useState(0);
44+
45+
// Function to clear corrupted data
46+
const clearCorruptedData = () => {
47+
localStorage.removeItem('componentAnalytics');
48+
setAnalyticsData([]);
49+
setRefreshKey(prev => prev + 1);
50+
};
4351

4452
useEffect(() => {
4553
try {
4654
const data = typeof window !== 'undefined' ? localStorage.getItem('componentAnalytics') : null;
4755
if (data) {
48-
setAnalyticsData(JSON.parse(data));
56+
const parsedData = JSON.parse(data);
57+
console.log('Parsed analytics data:', parsedData, 'Type:', typeof parsedData, 'Is Array:', Array.isArray(parsedData));
58+
// Ensure parsedData is an array
59+
if (Array.isArray(parsedData)) {
60+
setAnalyticsData(parsedData);
61+
} else {
62+
console.warn('Analytics data is not an array, initializing with empty array. Data:', parsedData);
63+
setAnalyticsData([]);
64+
}
65+
} else {
66+
// Add sample data if no real data exists
67+
const sampleData: AnalyticsEvent[] = [
68+
{
69+
component: "Button",
70+
event: "copy",
71+
theme: "dark",
72+
timestamp: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString(), // 1 day ago
73+
},
74+
{
75+
component: "Button",
76+
event: "copy",
77+
theme: "light",
78+
timestamp: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(), // 2 days ago
79+
},
80+
{
81+
component: "Input",
82+
event: "copy",
83+
theme: "dark",
84+
timestamp: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString(), // 3 days ago
85+
},
86+
{
87+
component: "Card",
88+
event: "copy",
89+
theme: "light",
90+
timestamp: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(), // 5 days ago
91+
},
92+
{
93+
component: "Button",
94+
event: "copy",
95+
theme: "dark",
96+
timestamp: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(), // 7 days ago
97+
},
98+
{
99+
component: "Dialog",
100+
event: "copy",
101+
theme: "light",
102+
timestamp: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000).toISOString(), // 10 days ago
103+
},
104+
{
105+
component: "Select",
106+
event: "copy",
107+
theme: "dark",
108+
timestamp: new Date(Date.now() - 15 * 24 * 60 * 60 * 1000).toISOString(), // 15 days ago
109+
},
110+
{
111+
component: "Badge",
112+
event: "copy",
113+
theme: "light",
114+
timestamp: new Date(Date.now() - 20 * 24 * 60 * 60 * 1000).toISOString(), // 20 days ago
115+
},
116+
];
117+
setAnalyticsData(sampleData);
49118
}
50119
} catch (error) {
51120
// keep console.error for debugging in dev
52121
// eslint-disable-next-line no-console
53122
console.error('Error reading analytics data:', error);
123+
// Set empty array as fallback
124+
setAnalyticsData([]);
54125
} finally {
55126
setIsLoading(false);
56127
}
57-
}, []);
128+
}, [refreshKey]);
58129

59130
// Filter and aggregate data for charts
60-
const filteredData = analyticsData.filter((event) => {
131+
console.log('analyticsData before filter:', analyticsData, 'Type:', typeof analyticsData, 'Is Array:', Array.isArray(analyticsData));
132+
const filteredData = (Array.isArray(analyticsData) ? analyticsData : []).filter((event) => {
61133
const componentMatch = filterComponent === 'all' || event.component === filterComponent;
62134
const themeMatch = filterTheme === 'all' || event.theme === filterTheme;
63135
const timeMatch =
@@ -119,9 +191,15 @@ export default function AnalyticsDashboard() {
119191
return (
120192
<div className="min-h-screen bg-gradient-to-br from-background to-muted/50 dark:from-background dark:to-muted/30 py-6 px-4 sm:px-6 lg:px-8">
121193
<div className="max-w-7xl mx-auto space-y-6">
122-
<h1 className="text-2xl sm:text-3xl font-bold text-foreground tracking-tight animate-in fade-in duration-500">
123-
Analytics Dashboard
124-
</h1>
194+
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
195+
<h1 className="text-2xl sm:text-3xl font-bold text-foreground tracking-tight animate-in fade-in duration-500">
196+
Analytics Dashboard
197+
</h1>
198+
<div className="flex items-center gap-4 text-sm text-muted-foreground">
199+
<span>Total Events: {Array.isArray(analyticsData) ? analyticsData.length : 0}</span>
200+
<span>Components: {Array.isArray(analyticsData) ? new Set(analyticsData.map(d => d.component)).size : 0}</span>
201+
</div>
202+
</div>
125203

126204
{/* Filters Card */}
127205
<Card className="border-none shadow-sm">
@@ -140,11 +218,11 @@ export default function AnalyticsDashboard() {
140218
<SelectItem value="all" key="all">
141219
All Components
142220
</SelectItem>
143-
{[...new Set(analyticsData.map((d) => d.component))].map((comp) => (
221+
{Array.isArray(analyticsData) ? [...new Set(analyticsData.map((d) => d.component))].map((comp) => (
144222
<SelectItem key={comp} value={comp}>
145223
{comp}
146224
</SelectItem>
147-
))}
225+
)) : null}
148226
</SelectContent>
149227
</Select>
150228
</div>
@@ -202,9 +280,60 @@ export default function AnalyticsDashboard() {
202280
</button>
203281
</div>
204282
</div>
283+
284+
{/* Sample Data Notice */}
285+
{Array.isArray(analyticsData) && analyticsData.length > 0 && analyticsData[0]?.component === "Button" && analyticsData[0]?.timestamp.includes("2024") && (
286+
<div className="mt-4 p-3 bg-blue-50 dark:bg-blue-950/20 border border-blue-200 dark:border-blue-800 rounded-lg">
287+
<p className="text-sm text-blue-800 dark:text-blue-200">
288+
📊 <strong>Demo Mode:</strong> This is sample data. Copy some components from the home page to see real analytics!
289+
</p>
290+
<button
291+
className="mt-2 text-xs text-blue-600 dark:text-blue-400 hover:underline"
292+
onClick={() => {
293+
localStorage.removeItem('componentAnalytics');
294+
window.location.reload();
295+
}}
296+
>
297+
Clear sample data
298+
</button>
299+
</div>
300+
)}
205301
</CardContent>
206302
</Card>
207303

304+
{/* Recent Activity */}
305+
{Array.isArray(analyticsData) && analyticsData.length > 0 && (
306+
<Card className="border-none shadow-sm">
307+
<CardHeader>
308+
<CardTitle className="text-lg font-semibold text-foreground">Recent Activity</CardTitle>
309+
</CardHeader>
310+
<CardContent>
311+
<div className="space-y-2">
312+
{analyticsData
313+
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
314+
.slice(0, 5)
315+
.map((event, index) => (
316+
<div key={index} className="flex items-center justify-between p-3 bg-muted/30 rounded-lg">
317+
<div className="flex items-center gap-3">
318+
<div className="w-2 h-2 bg-primary rounded-full"></div>
319+
<span className="font-medium">{event.component}</span>
320+
<span className="text-sm text-muted-foreground">copied</span>
321+
</div>
322+
<div className="flex items-center gap-2 text-sm text-muted-foreground">
323+
<span className={`px-2 py-1 rounded-full text-xs ${
324+
event.theme === 'dark' ? 'bg-gray-800 text-gray-200' : 'bg-gray-200 text-gray-800'
325+
}`}>
326+
{event.theme}
327+
</span>
328+
<span>{new Date(event.timestamp).toLocaleString()}</span>
329+
</div>
330+
</div>
331+
))}
332+
</div>
333+
</CardContent>
334+
</Card>
335+
)}
336+
208337
{/* Loading State */}
209338
{isLoading ? (
210339
<Card className="border-none shadow-sm">
@@ -222,7 +351,22 @@ export default function AnalyticsDashboard() {
222351
<Card className="border-none shadow-sm animate-in fade-in zoom-in-95 duration-500">
223352
<CardHeader className="flex flex-col space-y-2 md:flex-row md:space-y-0 md:items-center md:justify-between">
224353
<CardTitle className="text-lg font-semibold text-foreground">Most Copied Components</CardTitle>
225-
<Select value={chartType} onValueChange={setChartType}>
354+
<div className="flex items-center gap-2">
355+
<button
356+
onClick={() => setRefreshKey(prev => prev + 1)}
357+
className="p-2 hover:bg-muted rounded-md transition-colors"
358+
title="Refresh data"
359+
>
360+
<RefreshCw className="h-4 w-4" />
361+
</button>
362+
<button
363+
onClick={clearCorruptedData}
364+
className="p-2 hover:bg-muted rounded-md transition-colors text-red-500 hover:text-red-700"
365+
title="Clear all data"
366+
>
367+
🗑️
368+
</button>
369+
<Select value={chartType} onValueChange={setChartType}>
226370
<SelectTrigger
227371
aria-label="Select chart type"
228372
className="h-9 w-full md:w-32 bg-background/80 hover:bg-background transition-colors duration-200 border-border/50 focus:ring-2 focus:ring-primary focus:ring-offset-1"
@@ -249,14 +393,20 @@ export default function AnalyticsDashboard() {
249393
</div>
250394
</SelectItem>
251395
</SelectContent>
252-
</Select>
396+
</Select>
397+
</div>
253398
</CardHeader>
254399

255400
<CardContent>
256401
{copyChartData.length === 0 ? (
257402
<div className="flex flex-col gap-y-4 items-center justify-center h-[200px] w-full">
258403
<FileSearch className="size-6 text-muted-foreground" />
259-
<p className="text-muted-foreground text-sm">No data for this period</p>
404+
<div className="text-center space-y-2">
405+
<p className="text-muted-foreground text-sm">No data for this period</p>
406+
<p className="text-xs text-muted-foreground/70">
407+
Try adjusting your filters or copy some components from the home page
408+
</p>
409+
</div>
260410
</div>
261411
) : (
262412
<div className="space-y-6">

src/app/layout.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import "./globals.css";
66
import ThemeColorPicker from "@/components/ui/ThemeColorPicker";
77
import { ThemeProvider } from "next-themes"; // ⬅️ import
88
import { Toaster } from "@/components/ui/sonner";
9+
import Header from "@/components/Header";
10+
import { SearchProvider } from "@/lib/SearchContext";
911

1012
const geistSans = Geist({
1113
variable: "--font-geist-sans",
@@ -33,12 +35,15 @@ export default function RootLayout({
3335
<body
3436
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
3537
>
36-
{/* Wrap everything in ThemeProvider */}
38+
{/* Wrap everything in ThemeProvider */}
3739
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem>
38-
<ThemeColorPicker />
39-
{children}
40-
<Toaster position="top-center" richColors />
41-
<BackToTopButton />
40+
<SearchProvider>
41+
<ThemeColorPicker />
42+
<Header />
43+
{children}
44+
<Toaster position="top-center" richColors />
45+
<BackToTopButton />
46+
</SearchProvider>
4247
</ThemeProvider>
4348
</body>
4449
</html>

src/app/page.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@
2121
} from "lucide-react";
2222
import { Button } from "@/components/ui/button";
2323
import Link from "next/link";
24-
import Header from "@/components/Header";
24+
import { useSearch } from "@/lib/SearchContext";
2525

2626
const Index = () => {
27-
// ✅ This logic STAYS here. This page will control the search.
28-
const [searchQuery, setSearchQuery] = useState("");
27+
// Use search context instead of local state
28+
const { searchQuery, setSearchQuery } = useSearch();
2929
const [debouncedQuery, setDebouncedQuery] = useState("");
3030
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
3131

@@ -115,8 +115,6 @@
115115

116116
return (
117117
<div className="min-h-screen bg-background">
118-
{/* ✅ We will now pass the search state and function to the Header */}
119-
<Header searchQuery={searchQuery} setSearchQuery={setSearchQuery} />
120118

121119
{/* Hero Section */}
122120
<section

0 commit comments

Comments
 (0)