feat: add analytics consent UI components
- Create AnalyticsConsent modal dialog for initial consent - Add AnalyticsConsentBanner for non-intrusive consent request - Implement privacy-focused consent flow with clear data collection info - Show what data is collected and privacy protections - Support both controlled and uncontrolled component usage - Add smooth animations with Framer Motion - Include accept/decline handlers with analytics service integration
This commit is contained in:
235
src/components/AnalyticsConsent.tsx
Normal file
235
src/components/AnalyticsConsent.tsx
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import { BarChart3, Shield, X, Check, Info } from 'lucide-react';
|
||||||
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { analytics } from '@/lib/analytics';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
interface AnalyticsConsentProps {
|
||||||
|
open?: boolean;
|
||||||
|
onOpenChange?: (open: boolean) => void;
|
||||||
|
onComplete?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AnalyticsConsent: React.FC<AnalyticsConsentProps> = ({
|
||||||
|
open: controlledOpen,
|
||||||
|
onOpenChange,
|
||||||
|
onComplete,
|
||||||
|
}) => {
|
||||||
|
const [internalOpen, setInternalOpen] = useState(false);
|
||||||
|
const [hasShownConsent, setHasShownConsent] = useState(false);
|
||||||
|
|
||||||
|
const isControlled = controlledOpen !== undefined;
|
||||||
|
const open = isControlled ? controlledOpen : internalOpen;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Check if we should show the consent dialog
|
||||||
|
const checkConsent = async () => {
|
||||||
|
await analytics.initialize();
|
||||||
|
const settings = analytics.getSettings();
|
||||||
|
|
||||||
|
if (!settings?.hasConsented && !hasShownConsent) {
|
||||||
|
if (!isControlled) {
|
||||||
|
setInternalOpen(true);
|
||||||
|
}
|
||||||
|
setHasShownConsent(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkConsent();
|
||||||
|
}, [isControlled, hasShownConsent]);
|
||||||
|
|
||||||
|
const handleOpenChange = (newOpen: boolean) => {
|
||||||
|
if (isControlled && onOpenChange) {
|
||||||
|
onOpenChange(newOpen);
|
||||||
|
} else {
|
||||||
|
setInternalOpen(newOpen);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAccept = async () => {
|
||||||
|
await analytics.enable();
|
||||||
|
handleOpenChange(false);
|
||||||
|
onComplete?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDecline = async () => {
|
||||||
|
await analytics.disable();
|
||||||
|
handleOpenChange(false);
|
||||||
|
onComplete?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||||
|
<DialogContent className="max-w-2xl p-0 overflow-hidden">
|
||||||
|
<div className="p-6 pb-0">
|
||||||
|
<DialogHeader>
|
||||||
|
<div className="flex items-center gap-3 mb-2">
|
||||||
|
<div className="p-2 bg-purple-100 dark:bg-purple-900/20 rounded-lg">
|
||||||
|
<BarChart3 className="h-6 w-6 text-purple-600 dark:text-purple-400" />
|
||||||
|
</div>
|
||||||
|
<DialogTitle className="text-2xl">Help Improve Claudia</DialogTitle>
|
||||||
|
</div>
|
||||||
|
<DialogDescription className="text-base mt-2">
|
||||||
|
We'd like to collect anonymous usage data to improve your experience.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6 space-y-4">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Card className="p-4 border-green-200 dark:border-green-900 bg-green-50 dark:bg-green-950/20">
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<Check className="h-5 w-5 text-green-600 dark:text-green-400 flex-shrink-0 mt-0.5" />
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="font-medium text-green-900 dark:text-green-100">What we collect:</p>
|
||||||
|
<ul className="text-sm text-green-800 dark:text-green-200 space-y-1">
|
||||||
|
<li>• Feature usage (which tools and commands you use)</li>
|
||||||
|
<li>• Performance metrics (app speed and reliability)</li>
|
||||||
|
<li>• Error reports (to fix bugs and improve stability)</li>
|
||||||
|
<li>• General usage patterns (session frequency and duration)</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-4 border-blue-200 dark:border-blue-900 bg-blue-50 dark:bg-blue-950/20">
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<Shield className="h-5 w-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="font-medium text-blue-900 dark:text-blue-100">Your privacy is protected:</p>
|
||||||
|
<ul className="text-sm text-blue-800 dark:text-blue-200 space-y-1">
|
||||||
|
<li>• No personal information is collected</li>
|
||||||
|
<li>• No file contents, paths, or project names</li>
|
||||||
|
<li>• No API keys or sensitive data</li>
|
||||||
|
<li>• Completely anonymous with random IDs</li>
|
||||||
|
<li>• You can opt-out anytime in Settings</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-4">
|
||||||
|
<div className="flex gap-2 items-start">
|
||||||
|
<Info className="h-4 w-4 text-gray-500 flex-shrink-0 mt-0.5" />
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
This data helps us understand which features are most valuable, identify performance
|
||||||
|
issues, and prioritize improvements. Your choice won't affect any functionality.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6 pt-0 flex gap-3">
|
||||||
|
<Button
|
||||||
|
onClick={handleDecline}
|
||||||
|
variant="outline"
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
No Thanks
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleAccept}
|
||||||
|
className="flex-1 bg-purple-600 hover:bg-purple-700 text-white"
|
||||||
|
>
|
||||||
|
Allow Analytics
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface AnalyticsConsentBannerProps {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AnalyticsConsentBanner: React.FC<AnalyticsConsentBannerProps> = ({
|
||||||
|
className,
|
||||||
|
}) => {
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const [hasChecked, setHasChecked] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkConsent = async () => {
|
||||||
|
if (hasChecked) return;
|
||||||
|
|
||||||
|
await analytics.initialize();
|
||||||
|
const settings = analytics.getSettings();
|
||||||
|
|
||||||
|
if (!settings?.hasConsented) {
|
||||||
|
setVisible(true);
|
||||||
|
}
|
||||||
|
setHasChecked(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Delay banner appearance for better UX
|
||||||
|
const timer = setTimeout(checkConsent, 2000);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [hasChecked]);
|
||||||
|
|
||||||
|
const handleAccept = async () => {
|
||||||
|
await analytics.enable();
|
||||||
|
setVisible(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDecline = async () => {
|
||||||
|
await analytics.disable();
|
||||||
|
setVisible(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
{visible && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ y: 100, opacity: 0 }}
|
||||||
|
animate={{ y: 0, opacity: 1 }}
|
||||||
|
exit={{ y: 100, opacity: 0 }}
|
||||||
|
transition={{ type: "spring", damping: 25, stiffness: 300 }}
|
||||||
|
className={cn(
|
||||||
|
"fixed bottom-4 right-4 z-50 max-w-md",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Card className="p-4 shadow-lg border-purple-200 dark:border-purple-800">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<BarChart3 className="h-5 w-5 text-purple-600 dark:text-purple-400 flex-shrink-0 mt-0.5" />
|
||||||
|
<div className="space-y-2 flex-1">
|
||||||
|
<p className="text-sm font-medium">Help improve Claudia</p>
|
||||||
|
<p className="text-xs text-gray-600 dark:text-gray-400">
|
||||||
|
We collect anonymous usage data to improve your experience. No personal data is collected.
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-2 pt-1">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleDecline}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
No Thanks
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={handleAccept}
|
||||||
|
className="text-xs bg-purple-600 hover:bg-purple-700 text-white"
|
||||||
|
>
|
||||||
|
Allow
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setVisible(false)}
|
||||||
|
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
};
|
Reference in New Issue
Block a user