- Integrate real page view data from the database for accurate analytics. - Implement cache-busting for fresh data retrieval in analytics dashboard. - Calculate and display bounce rate, average session duration, and unique users. - Refactor performance metrics to ensure only real data is considered. - Improve user experience with toast notifications for success and error messages. - Update project editor with undo/redo functionality and enhanced content management.
579 lines
24 KiB
TypeScript
579 lines
24 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect, useCallback } from 'react';
|
|
import { motion } from 'framer-motion';
|
|
import {
|
|
BarChart3,
|
|
TrendingUp,
|
|
Eye,
|
|
Heart,
|
|
Zap,
|
|
Globe,
|
|
Activity,
|
|
Target,
|
|
Award,
|
|
RefreshCw,
|
|
MousePointer,
|
|
RotateCcw,
|
|
Trash2,
|
|
AlertTriangle
|
|
} from 'lucide-react';
|
|
import { useToast } from '@/components/Toast';
|
|
|
|
interface AnalyticsData {
|
|
overview: {
|
|
totalProjects: number;
|
|
publishedProjects: number;
|
|
featuredProjects: number;
|
|
totalViews: number;
|
|
avgLighthouse: number;
|
|
};
|
|
projects: Array<{
|
|
id: number;
|
|
title: string;
|
|
category: string;
|
|
difficulty: string;
|
|
views: number;
|
|
lighthouse: number;
|
|
published: boolean;
|
|
featured: boolean;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}>;
|
|
categories: Record<string, number>;
|
|
difficulties: Record<string, number>;
|
|
performance: {
|
|
avgLighthouse: number;
|
|
totalViews: number;
|
|
};
|
|
metrics: {
|
|
bounceRate: number;
|
|
avgSessionDuration: number;
|
|
pagesPerSession: number;
|
|
newUsers: number;
|
|
};
|
|
}
|
|
|
|
interface AnalyticsDashboardProps {
|
|
isAuthenticated: boolean;
|
|
}
|
|
|
|
export function AnalyticsDashboard({ isAuthenticated }: AnalyticsDashboardProps) {
|
|
const [data, setData] = useState<AnalyticsData | null>(null);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [timeRange, setTimeRange] = useState<'7d' | '30d' | '90d' | '1y'>('30d');
|
|
const [showResetModal, setShowResetModal] = useState(false);
|
|
const [resetType, setResetType] = useState<'analytics' | 'pageviews' | 'interactions' | 'performance' | 'all'>('analytics');
|
|
const [resetting, setResetting] = useState(false);
|
|
const { showSuccess, showError } = useToast();
|
|
|
|
const fetchAnalyticsData = useCallback(async () => {
|
|
if (!isAuthenticated) return;
|
|
|
|
try {
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
// Add cache-busting parameter to ensure fresh data after reset
|
|
const cacheBust = `?nocache=true&t=${Date.now()}`;
|
|
const [analyticsRes, performanceRes] = await Promise.all([
|
|
fetch(`/api/analytics/dashboard${cacheBust}`, {
|
|
headers: { 'x-admin-request': 'true' }
|
|
}),
|
|
fetch(`/api/analytics/performance${cacheBust}`, {
|
|
headers: { 'x-admin-request': 'true' }
|
|
})
|
|
]);
|
|
|
|
if (!analyticsRes.ok || !performanceRes.ok) {
|
|
throw new Error('Failed to fetch analytics data');
|
|
}
|
|
|
|
const [analytics, performance] = await Promise.all([
|
|
analyticsRes.json(),
|
|
performanceRes.json()
|
|
]);
|
|
|
|
setData({
|
|
overview: analytics.overview || {
|
|
totalProjects: 0,
|
|
publishedProjects: 0,
|
|
featuredProjects: 0,
|
|
totalViews: 0,
|
|
avgLighthouse: 90
|
|
},
|
|
projects: analytics.projects || [],
|
|
categories: analytics.categories || {},
|
|
difficulties: analytics.difficulties || {},
|
|
performance: {
|
|
avgLighthouse: performance.avgLighthouse || analytics.overview?.avgLighthouse || 0,
|
|
totalViews: performance.totalViews || analytics.overview?.totalViews || 0,
|
|
},
|
|
metrics: performance.metrics || analytics.metrics || {
|
|
bounceRate: 0,
|
|
avgSessionDuration: 0,
|
|
pagesPerSession: 0,
|
|
newUsers: 0
|
|
}
|
|
});
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Failed to load analytics');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [isAuthenticated]);
|
|
|
|
const resetAnalytics = async () => {
|
|
if (!isAuthenticated || resetting) return;
|
|
|
|
setResetting(true);
|
|
setError(null);
|
|
try {
|
|
const response = await fetch('/api/analytics/reset', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'x-admin-request': 'true'
|
|
},
|
|
body: JSON.stringify({ type: resetType })
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (response.ok) {
|
|
showSuccess(
|
|
'Analytics Reset',
|
|
`Successfully reset ${resetType === 'all' ? 'all analytics data' : resetType} data.`
|
|
);
|
|
setShowResetModal(false);
|
|
// Clear cache and refresh data
|
|
await fetchAnalyticsData();
|
|
} else {
|
|
const errorMsg = result.error || 'Failed to reset analytics';
|
|
setError(errorMsg);
|
|
showError('Reset Failed', errorMsg);
|
|
}
|
|
} catch (err) {
|
|
const errorMsg = 'Failed to reset analytics. Please try again.';
|
|
setError(errorMsg);
|
|
showError('Reset Failed', errorMsg);
|
|
console.error('Reset error:', err);
|
|
} finally {
|
|
setResetting(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (isAuthenticated) {
|
|
fetchAnalyticsData();
|
|
}
|
|
}, [isAuthenticated, fetchAnalyticsData]);
|
|
|
|
const StatCard = ({ title, value, icon: Icon, color, description, tooltip }: {
|
|
title: string;
|
|
value: number | string;
|
|
icon: React.ComponentType<{ className?: string; size?: number }>;
|
|
color: string;
|
|
description?: string;
|
|
tooltip?: string;
|
|
}) => (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
className="bg-white border border-stone-200 p-6 rounded-xl hover:shadow-md transition-all duration-200 group relative"
|
|
>
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex-1">
|
|
<div className="flex items-center space-x-3 mb-4">
|
|
<div className={`p-3 rounded-xl ${color}`}>
|
|
<Icon className="w-6 h-6" size={24} />
|
|
</div>
|
|
<div>
|
|
<p className="text-stone-500 text-sm font-medium">{title}</p>
|
|
{description && <p className="text-stone-400 text-xs">{description}</p>}
|
|
</div>
|
|
</div>
|
|
<p className="text-3xl font-bold text-stone-900 mb-2">{value}</p>
|
|
</div>
|
|
</div>
|
|
{tooltip && (
|
|
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-3 py-2 bg-stone-900/95 text-stone-50 text-xs font-medium rounded-lg opacity-0 group-hover:opacity-100 transition-opacity whitespace-normal max-w-xs z-50 shadow-xl backdrop-blur-sm pointer-events-none">
|
|
{tooltip}
|
|
<div className="absolute top-full left-1/2 -translate-x-1/2 -mt-1 w-2 h-2 bg-stone-900/95 rotate-45"></div>
|
|
</div>
|
|
)}
|
|
</motion.div>
|
|
);
|
|
|
|
const getDifficultyColor = (difficulty: string) => {
|
|
switch (difficulty) {
|
|
case 'Beginner': return 'bg-stone-50 text-stone-700 border-stone-200';
|
|
case 'Intermediate': return 'bg-stone-100 text-stone-700 border-stone-300';
|
|
case 'Advanced': return 'bg-stone-200 text-stone-800 border-stone-400';
|
|
case 'Expert': return 'bg-stone-300 text-stone-900 border-stone-500';
|
|
default: return 'bg-stone-50 text-stone-600 border-stone-200';
|
|
}
|
|
};
|
|
|
|
const getCategoryColor = (index: number) => {
|
|
const colors = [
|
|
'bg-stone-100 text-stone-700',
|
|
'bg-stone-200 text-stone-800',
|
|
'bg-stone-300 text-stone-900',
|
|
'bg-stone-100 text-stone-700',
|
|
'bg-stone-200 text-stone-800'
|
|
];
|
|
return colors[index % colors.length];
|
|
};
|
|
|
|
// Authentication disabled - show analytics directly
|
|
|
|
return (
|
|
<div className="space-y-8">
|
|
{/* Header */}
|
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
|
<div>
|
|
<h1 className="text-3xl font-bold text-stone-900 flex items-center">
|
|
<BarChart3 className="w-8 h-8 mr-3 text-stone-600" />
|
|
Analytics Dashboard
|
|
</h1>
|
|
<p className="text-stone-500 mt-2">Portfolio performance and analytics metrics</p>
|
|
</div>
|
|
<div className="flex items-center space-x-3">
|
|
{/* Time Range Selector */}
|
|
<div className="flex items-center space-x-1 bg-white border border-stone-200 rounded-xl p-1">
|
|
{(['7d', '30d', '90d', '1y'] as const).map((range) => (
|
|
<button
|
|
key={range}
|
|
onClick={() => setTimeRange(range)}
|
|
className={`px-3 py-2 rounded-lg text-sm font-medium transition-all duration-200 ${
|
|
timeRange === range
|
|
? 'bg-stone-100 text-stone-900 shadow-sm'
|
|
: 'text-stone-500 hover:text-stone-800 hover:bg-stone-50'
|
|
}`}
|
|
>
|
|
{range === '7d' ? '7 Days' : range === '30d' ? '30 Days' : range === '90d' ? '90 Days' : '1 Year'}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<button
|
|
onClick={fetchAnalyticsData}
|
|
disabled={loading}
|
|
className="flex items-center space-x-2 px-4 py-2 bg-white border border-stone-200 rounded-xl hover:bg-stone-50 transition-all duration-200 disabled:opacity-50 text-stone-600"
|
|
>
|
|
<RefreshCw className={`w-4 h-4 text-stone-600 ${loading ? 'animate-spin' : ''}`} />
|
|
<span className="font-medium">Refresh</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => setShowResetModal(true)}
|
|
className="flex items-center space-x-2 px-4 py-2 bg-red-50 text-red-600 border border-red-100 rounded-xl hover:bg-red-100 transition-all"
|
|
>
|
|
<RotateCcw className="w-4 h-4" />
|
|
<span>Reset</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{loading && (
|
|
<div className="bg-white border border-stone-200 p-8 rounded-xl shadow-sm">
|
|
<div className="flex items-center justify-center space-x-3">
|
|
<RefreshCw className="w-6 h-6 text-stone-600 animate-spin" />
|
|
<span className="text-stone-500 text-lg">Loading analytics data...</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{error && (
|
|
<div className="bg-white border border-red-200 p-6 rounded-xl">
|
|
<div className="flex items-center space-x-3 text-red-600">
|
|
<Activity className="w-5 h-5" />
|
|
<span>Error: {error}</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{data && !loading && (
|
|
<>
|
|
{/* Overview Stats */}
|
|
<div>
|
|
<h2 className="text-xl font-bold text-stone-900 mb-6 flex items-center">
|
|
<Target className="w-5 h-5 mr-2 text-stone-600" />
|
|
Overview
|
|
</h2>
|
|
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
|
|
<StatCard
|
|
title="Total Views"
|
|
value={data.overview.totalViews.toLocaleString()}
|
|
icon={Eye}
|
|
color="bg-stone-100 text-stone-600"
|
|
description="All-time page views"
|
|
tooltip="✅ REAL DATA: Total page views tracked from the PageView database table. Each visit to a project page or the homepage is automatically recorded with IP, user agent, and timestamp."
|
|
/>
|
|
<StatCard
|
|
title="Projects"
|
|
value={data.overview.totalProjects}
|
|
icon={Globe}
|
|
color="bg-stone-100 text-stone-600"
|
|
description={`${data.overview.publishedProjects} published`}
|
|
tooltip="✅ REAL DATA: Total number of projects in your portfolio. Shows published vs unpublished projects from your database."
|
|
/>
|
|
<StatCard
|
|
title="Performance"
|
|
value={data.overview.avgLighthouse > 0 ? data.overview.avgLighthouse : 'N/A'}
|
|
icon={Zap}
|
|
color="bg-stone-100 text-stone-600"
|
|
description={data.overview.avgLighthouse > 0 ? "Avg Lighthouse score" : "No performance data yet"}
|
|
tooltip={data.overview.avgLighthouse > 0
|
|
? "✅ REAL DATA: Average Lighthouse performance score (0-100) calculated from real Web Vitals metrics (LCP, FCP, CLS, FID, TTFB) collected from actual page visits. Only shown when real performance data exists."
|
|
: "No performance data collected yet. Scores will appear after visitors load your pages and Web Vitals are tracked."}
|
|
/>
|
|
<StatCard
|
|
title="Bounce Rate"
|
|
value={`${data.metrics?.bounceRate || 0}%`}
|
|
icon={MousePointer}
|
|
color="bg-stone-100 text-stone-600"
|
|
description="User retention"
|
|
tooltip="✅ REAL DATA: Percentage of sessions where users viewed only one page before leaving. Calculated from PageView records grouped by IP address. Lower is better."
|
|
/>
|
|
<StatCard
|
|
title="Avg Session"
|
|
value={data.metrics?.avgSessionDuration ? `${Math.round(data.metrics.avgSessionDuration / 60)}m` : '0m'}
|
|
icon={Activity}
|
|
color="bg-stone-100 text-stone-600"
|
|
description="Average session duration"
|
|
tooltip="✅ REAL DATA: Average time users spend on your site per session, calculated from the time difference between first and last pageview per IP address. Only calculated for sessions with multiple pageviews."
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Project Performance */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
{/* Top Projects */}
|
|
<div className="bg-white border border-stone-200 p-6 rounded-xl shadow-sm">
|
|
<h3 className="text-xl font-bold text-stone-900 mb-6 flex items-center">
|
|
<Award className="w-5 h-5 mr-2 text-stone-600" />
|
|
Top Performing Projects
|
|
</h3>
|
|
<div className="space-y-4">
|
|
{data.projects
|
|
.sort((a, b) => b.views - a.views)
|
|
.slice(0, 5)
|
|
.map((project, index) => (
|
|
<motion.div
|
|
key={project.id}
|
|
initial={{ opacity: 0, x: -20 }}
|
|
animate={{ opacity: 1, x: 0 }}
|
|
transition={{ delay: index * 0.1 }}
|
|
className="flex items-center justify-between p-4 bg-stone-50 rounded-xl border border-stone-100"
|
|
>
|
|
<div className="flex items-center space-x-4">
|
|
<div className="w-8 h-8 bg-stone-600 rounded-lg flex items-center justify-center text-white font-bold shadow-sm">
|
|
#{index + 1}
|
|
</div>
|
|
<div>
|
|
<p className="text-stone-900 font-medium">{project.title}</p>
|
|
<p className="text-stone-500 text-sm">{project.category}</p>
|
|
</div>
|
|
</div>
|
|
<div className="text-right group/views relative">
|
|
<p className="text-stone-900 font-bold">{project.views.toLocaleString()}</p>
|
|
<p className="text-stone-500 text-sm">views</p>
|
|
<div className="absolute bottom-full right-0 mb-2 px-3 py-2 bg-stone-900/95 text-stone-50 text-xs font-medium rounded-lg opacity-0 group-hover/views:opacity-100 transition-opacity whitespace-normal max-w-xs z-50 shadow-xl backdrop-blur-sm pointer-events-none">
|
|
✅ REAL DATA: Page views tracked from PageView table for this project. Each visit is automatically recorded.
|
|
<div className="absolute top-full right-4 -mt-1 w-2 h-2 bg-stone-900/95 rotate-45"></div>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Categories Distribution */}
|
|
<div className="bg-white border border-stone-200 p-6 rounded-xl shadow-sm">
|
|
<h3 className="text-xl font-bold text-stone-900 mb-6 flex items-center">
|
|
<BarChart3 className="w-5 h-5 mr-2 text-stone-600" />
|
|
Categories
|
|
</h3>
|
|
<div className="space-y-4">
|
|
{Object.entries(data.categories).map(([category, count], index) => (
|
|
<motion.div
|
|
key={category}
|
|
initial={{ opacity: 0, x: 20 }}
|
|
animate={{ opacity: 1, x: 0 }}
|
|
transition={{ delay: index * 0.1 }}
|
|
className="flex items-center justify-between"
|
|
>
|
|
<div className="flex items-center space-x-3">
|
|
<div className={`w-4 h-4 rounded-full ${getCategoryColor(index)}`}></div>
|
|
<span className="text-stone-700 font-medium">{category}</span>
|
|
</div>
|
|
<div className="flex items-center space-x-3">
|
|
<div className="w-32 h-2 bg-stone-100 rounded-full overflow-hidden">
|
|
<div
|
|
className={`h-full ${getCategoryColor(index)} transition-all duration-500`}
|
|
style={{ width: `${(count / Math.max(...Object.values(data.categories))) * 100}%` }}
|
|
></div>
|
|
</div>
|
|
<span className="text-stone-500 font-medium w-8 text-right">{count}</span>
|
|
</div>
|
|
</motion.div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Difficulty & Activity */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
{/* Difficulty Distribution */}
|
|
<div className="bg-white border border-stone-200 p-6 rounded-xl shadow-sm">
|
|
<h3 className="text-xl font-bold text-stone-900 mb-6 flex items-center">
|
|
<Target className="w-5 h-5 mr-2 text-stone-600" />
|
|
Difficulty Levels
|
|
</h3>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
{Object.entries(data.difficulties).map(([difficulty, count]) => (
|
|
<motion.div
|
|
key={difficulty}
|
|
initial={{ opacity: 0, scale: 0.9 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
className={`p-4 rounded-xl border ${getDifficultyColor(difficulty)}`}
|
|
>
|
|
<div className="text-center">
|
|
<p className="text-2xl font-bold mb-1">{count}</p>
|
|
<p className="text-sm font-medium">{difficulty}</p>
|
|
</div>
|
|
</motion.div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Recent Activity */}
|
|
<div className="bg-white border border-stone-200 p-6 rounded-xl shadow-sm">
|
|
<h3 className="text-xl font-bold text-stone-900 mb-6 flex items-center">
|
|
<Activity className="w-5 h-5 mr-2 text-blue-600" />
|
|
Recent Activity
|
|
</h3>
|
|
<div className="space-y-4">
|
|
{data.projects
|
|
.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime())
|
|
.slice(0, 4)
|
|
.map((project, index) => (
|
|
<motion.div
|
|
key={project.id}
|
|
initial={{ opacity: 0, y: 10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: index * 0.1 }}
|
|
className="flex items-center space-x-4 p-3 bg-stone-50 rounded-xl border border-stone-100"
|
|
>
|
|
<div className="w-2 h-2 bg-stone-500 rounded-full animate-pulse"></div>
|
|
<div className="flex-1">
|
|
<p className="text-stone-900 font-medium text-sm">{project.title}</p>
|
|
<p className="text-stone-500 text-xs">
|
|
Updated {new Date(project.updatedAt).toLocaleDateString()}
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
{project.featured && (
|
|
<span className="px-2 py-1 bg-stone-100 text-stone-700 rounded-full text-xs font-medium">
|
|
Featured
|
|
</span>
|
|
)}
|
|
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
|
|
project.published
|
|
? 'bg-stone-100 text-stone-700'
|
|
: 'bg-stone-200 text-stone-700'
|
|
}`}>
|
|
{project.published ? 'Live' : 'Draft'}
|
|
</span>
|
|
</div>
|
|
</motion.div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* Reset Modal */}
|
|
{showResetModal && (
|
|
<div className="fixed inset-0 bg-stone-900/20 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
|
<motion.div
|
|
initial={{ opacity: 0, scale: 0.95 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
exit={{ opacity: 0, scale: 0.95 }}
|
|
className="bg-white border border-stone-200 rounded-2xl p-6 w-full max-w-md shadow-xl"
|
|
>
|
|
<div className="flex items-center space-x-3 mb-4">
|
|
<div className="w-10 h-10 bg-red-100 rounded-lg flex items-center justify-center">
|
|
<AlertTriangle className="w-5 h-5 text-red-600" />
|
|
</div>
|
|
<div>
|
|
<h3 className="text-lg font-bold text-stone-900">Reset Analytics Data</h3>
|
|
<p className="text-stone-500 text-sm">This action cannot be undone</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-4 mb-6">
|
|
<div>
|
|
<label className="block text-stone-600 text-sm mb-2">Reset Type</label>
|
|
<select
|
|
value={resetType}
|
|
onChange={(e) => setResetType(e.target.value as 'analytics' | 'pageviews' | 'interactions' | 'performance' | 'all')}
|
|
className="w-full px-3 py-2 bg-stone-50 border border-stone-200 rounded-lg text-stone-900 focus:outline-none focus:ring-2 focus:ring-red-500"
|
|
>
|
|
<option value="analytics">Analytics Only (project view counts)</option>
|
|
<option value="pageviews">Page Views Only (all tracked visits)</option>
|
|
<option value="interactions">User Interactions Only</option>
|
|
<option value="performance">Performance Metrics Only (Lighthouse scores)</option>
|
|
<option value="all">Everything (Complete Reset)</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div className="bg-red-50 border border-red-100 rounded-lg p-3">
|
|
<div className="flex items-start space-x-2">
|
|
<AlertTriangle className="w-4 h-4 text-red-500 mt-0.5 flex-shrink-0" />
|
|
<div className="text-sm text-red-700">
|
|
<p className="font-medium mb-1">Warning:</p>
|
|
<p>This will permanently delete the selected analytics data. This action cannot be reversed.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-3">
|
|
<button
|
|
onClick={() => setShowResetModal(false)}
|
|
disabled={resetting}
|
|
className="flex-1 px-4 py-2 bg-white border border-stone-200 text-stone-700 rounded-lg hover:bg-stone-50 transition-all disabled:opacity-50"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
onClick={resetAnalytics}
|
|
disabled={resetting}
|
|
className="flex-1 flex items-center justify-center space-x-2 px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-all disabled:opacity-50"
|
|
>
|
|
{resetting ? (
|
|
<>
|
|
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
|
<span>Resetting...</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Trash2 className="w-4 h-4" />
|
|
<span>Reset {resetType === 'all' ? 'Everything' : 'Data'}</span>
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</motion.div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default AnalyticsDashboard;
|