Skip to content

Commit 8a36ae4

Browse files
Implement medium priority audits
The AI will implement the remaining medium priority audits for Tests, GDPR/Cookies, and UX/UI. This includes setting up testing infrastructure, improving micro-interactions and mobile UX, and ensuring GDPR compliance.
1 parent 76eccdf commit 8a36ae4

File tree

11 files changed

+1376
-60
lines changed

11 files changed

+1376
-60
lines changed

package-lock.json

Lines changed: 649 additions & 60 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@
4141
"@radix-ui/react-toggle-group": "^1.1.0",
4242
"@radix-ui/react-tooltip": "^1.1.4",
4343
"@tanstack/react-query": "^5.56.2",
44+
"@testing-library/dom": "^10.4.1",
45+
"@testing-library/jest-dom": "^6.7.0",
46+
"@testing-library/react": "^16.3.0",
47+
"@testing-library/user-event": "^14.6.1",
4448
"canvas-confetti": "^1.9.3",
4549
"class-variance-authority": "^0.7.1",
4650
"clsx": "^2.1.1",
@@ -71,6 +75,7 @@
7175
"tailwind-merge": "^2.5.2",
7276
"tailwindcss-animate": "^1.0.7",
7377
"vaul": "^0.9.3",
78+
"vitest": "^3.2.4",
7479
"zod": "^3.23.8"
7580
},
7681
"devDependencies": {

src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import AdminPage from '@/pages/AdminPage';
2828
import AffiliateOnboardingPage from '@/pages/AffiliateOnboardingPage';
2929
import CommissionInfoPage from '@/pages/CommissionInfoPage';
3030
import MobileNotSupportedPage from '@/pages/MobileNotSupportedPage';
31+
import PrivacyDashboardPage from '@/pages/PrivacyDashboardPage';
3132

3233
const Index = lazy(() => import('@/pages/Index'));
3334
const AdvancedStatsPage = lazy(() => import('@/pages/AdvancedStatsPage'));
@@ -83,6 +84,7 @@ function App() {
8384
<Route path="/affiliate-onboarding" element={<AffiliateOnboardingPage />} />
8485
<Route path="/commission-info" element={<CommissionInfoPage />} />
8586
<Route path="/mobile-not-supported" element={<MobileNotSupportedPage />} />
87+
<Route path="/privacy-dashboard" element={<PrivacyDashboardPage />} />
8688
</Routes>
8789
</Suspense>
8890

Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
import { useState } from 'react';
2+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
3+
import { Button } from '@/components/ui/button';
4+
import { Switch } from '@/components/ui/switch';
5+
import { Badge } from '@/components/ui/badge';
6+
import { Separator } from '@/components/ui/separator';
7+
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
8+
import { Alert, AlertDescription } from '@/components/ui/alert';
9+
import { useCookieConsent, CookiePreferences, CookieOptions } from '@/hooks/useCookieConsent';
10+
import { Shield, Cookie, Database, Clock, AlertTriangle, Download, Trash2, Eye, Settings } from 'lucide-react';
11+
import { format } from 'date-fns';
12+
import { fr } from 'date-fns/locale';
13+
14+
export const PrivacyDashboard = () => {
15+
const {
16+
preferences,
17+
options,
18+
updatePreferences,
19+
resetConsent,
20+
getConsentTimestamp,
21+
getExpiryDate
22+
} = useCookieConsent();
23+
24+
const [localPreferences, setLocalPreferences] = useState<CookiePreferences>(preferences);
25+
const [localOptions, setLocalOptions] = useState<CookieOptions>(options);
26+
const [showDataExport, setShowDataExport] = useState(false);
27+
const [showDataDeletion, setShowDataDeletion] = useState(false);
28+
29+
const consentDate = getConsentTimestamp();
30+
const expiryDate = getExpiryDate();
31+
32+
const handleSavePreferences = () => {
33+
updatePreferences(localPreferences, localOptions);
34+
};
35+
36+
const handleExportData = () => {
37+
const data = {
38+
consentTimestamp: consentDate?.toISOString(),
39+
preferences: preferences,
40+
options: options,
41+
exportDate: new Date().toISOString(),
42+
};
43+
44+
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
45+
const url = URL.createObjectURL(blob);
46+
const a = document.createElement('a');
47+
a.href = url;
48+
a.download = `refspring-privacy-data-${format(new Date(), 'yyyy-MM-dd')}.json`;
49+
document.body.appendChild(a);
50+
a.click();
51+
document.body.removeChild(a);
52+
URL.revokeObjectURL(url);
53+
};
54+
55+
const cookieCategories = [
56+
{
57+
key: 'necessary' as keyof CookiePreferences,
58+
title: 'Cookies Nécessaires',
59+
description: 'Essentiels au fonctionnement du site',
60+
icon: Shield,
61+
required: true,
62+
examples: ['Authentification', 'Sécurité', 'Navigation'],
63+
},
64+
{
65+
key: 'analytics' as keyof CookiePreferences,
66+
title: 'Cookies Analytiques',
67+
description: 'Nous aident à comprendre l\'usage du site',
68+
icon: Database,
69+
required: false,
70+
examples: ['Google Analytics', 'Hotjar', 'Statistiques'],
71+
},
72+
{
73+
key: 'marketing' as keyof CookiePreferences,
74+
title: 'Cookies Marketing',
75+
description: 'Personnalisent la publicité',
76+
icon: Eye,
77+
required: false,
78+
examples: ['Facebook Pixel', 'Google Ads', 'Retargeting'],
79+
},
80+
{
81+
key: 'personalization' as keyof CookiePreferences,
82+
title: 'Cookies Personnalisation',
83+
description: 'Améliorent votre expérience',
84+
icon: Settings,
85+
required: false,
86+
examples: ['Préférences UI', 'Recommandations', 'Langue'],
87+
},
88+
];
89+
90+
return (
91+
<div className="space-y-6">
92+
{/* En-tête du tableau de bord */}
93+
<Card>
94+
<CardHeader>
95+
<div className="flex items-center gap-3">
96+
<Shield className="h-6 w-6 text-primary" />
97+
<div>
98+
<CardTitle>Tableau de Bord Confidentialité</CardTitle>
99+
<CardDescription>
100+
Gérez vos données personnelles et préférences de confidentialité
101+
</CardDescription>
102+
</div>
103+
</div>
104+
</CardHeader>
105+
<CardContent className="space-y-4">
106+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
107+
<div className="text-center p-4 bg-muted/50 rounded-lg">
108+
<Clock className="h-8 w-8 mx-auto text-muted-foreground mb-2" />
109+
<p className="text-sm font-medium">Consentement donné</p>
110+
<p className="text-xs text-muted-foreground">
111+
{consentDate ? format(consentDate, 'dd MMMM yyyy', { locale: fr }) : 'Jamais'}
112+
</p>
113+
</div>
114+
<div className="text-center p-4 bg-muted/50 rounded-lg">
115+
<AlertTriangle className="h-8 w-8 mx-auto text-muted-foreground mb-2" />
116+
<p className="text-sm font-medium">Expire le</p>
117+
<p className="text-xs text-muted-foreground">
118+
{expiryDate ? format(expiryDate, 'dd MMMM yyyy', { locale: fr }) : 'Jamais'}
119+
</p>
120+
</div>
121+
<div className="text-center p-4 bg-muted/50 rounded-lg">
122+
<Cookie className="h-8 w-8 mx-auto text-muted-foreground mb-2" />
123+
<p className="text-sm font-medium">Cookies actifs</p>
124+
<p className="text-xs text-muted-foreground">
125+
{Object.values(preferences).filter(Boolean).length} / {Object.keys(preferences).length}
126+
</p>
127+
</div>
128+
</div>
129+
</CardContent>
130+
</Card>
131+
132+
{/* Gestion des cookies par catégorie */}
133+
<Card>
134+
<CardHeader>
135+
<CardTitle>Préférences des Cookies</CardTitle>
136+
<CardDescription>
137+
Contrôlez quels types de cookies vous souhaitez autoriser
138+
</CardDescription>
139+
</CardHeader>
140+
<CardContent className="space-y-6">
141+
{cookieCategories.map((category, index) => (
142+
<div key={category.key}>
143+
<div className="flex items-center justify-between">
144+
<div className="flex items-start gap-3 flex-1">
145+
<category.icon className="h-5 w-5 mt-1 text-muted-foreground" />
146+
<div className="space-y-1 flex-1">
147+
<div className="flex items-center gap-2">
148+
<h3 className="font-medium">{category.title}</h3>
149+
{category.required && (
150+
<Badge variant="secondary" className="text-xs">
151+
Requis
152+
</Badge>
153+
)}
154+
</div>
155+
<p className="text-sm text-muted-foreground">
156+
{category.description}
157+
</p>
158+
<div className="flex flex-wrap gap-1 mt-2">
159+
{category.examples.map((example) => (
160+
<Badge key={example} variant="outline" className="text-xs">
161+
{example}
162+
</Badge>
163+
))}
164+
</div>
165+
</div>
166+
</div>
167+
<Switch
168+
checked={localPreferences[category.key]}
169+
onCheckedChange={(checked) =>
170+
setLocalPreferences(prev => ({
171+
...prev,
172+
[category.key]: checked
173+
}))
174+
}
175+
disabled={category.required}
176+
/>
177+
</div>
178+
{index < cookieCategories.length - 1 && <Separator className="mt-6" />}
179+
</div>
180+
))}
181+
182+
<div className="pt-4 space-y-4">
183+
<Separator />
184+
<div className="space-y-4">
185+
<h3 className="font-medium">Options Avancées</h3>
186+
187+
<div className="flex items-center justify-between">
188+
<div>
189+
<p className="text-sm font-medium">Mode Strict</p>
190+
<p className="text-xs text-muted-foreground">
191+
Bloque automatiquement les nouveaux cookies
192+
</p>
193+
</div>
194+
<Switch
195+
checked={localOptions.strictMode}
196+
onCheckedChange={(checked) =>
197+
setLocalOptions(prev => ({ ...prev, strictMode: checked }))
198+
}
199+
/>
200+
</div>
201+
202+
<div className="flex items-center justify-between">
203+
<div>
204+
<p className="text-sm font-medium">Expiration Automatique</p>
205+
<p className="text-xs text-muted-foreground">
206+
Redemander le consentement après 1 an
207+
</p>
208+
</div>
209+
<Switch
210+
checked={localOptions.autoExpiry}
211+
onCheckedChange={(checked) =>
212+
setLocalOptions(prev => ({ ...prev, autoExpiry: checked }))
213+
}
214+
/>
215+
</div>
216+
217+
<div className="flex items-center justify-between">
218+
<div>
219+
<p className="text-sm font-medium">Opt-out du Tracking</p>
220+
<p className="text-xs text-muted-foreground">
221+
Envoie le signal "Do Not Track"
222+
</p>
223+
</div>
224+
<Switch
225+
checked={localOptions.trackingOptOut}
226+
onCheckedChange={(checked) =>
227+
setLocalOptions(prev => ({ ...prev, trackingOptOut: checked }))
228+
}
229+
/>
230+
</div>
231+
</div>
232+
233+
<Button onClick={handleSavePreferences} className="w-full">
234+
Sauvegarder les Préférences
235+
</Button>
236+
</div>
237+
</CardContent>
238+
</Card>
239+
240+
{/* Actions sur les données */}
241+
<Card>
242+
<CardHeader>
243+
<CardTitle>Vos Droits sur les Données</CardTitle>
244+
<CardDescription>
245+
Exercez vos droits RGPD concernant vos données personnelles
246+
</CardDescription>
247+
</CardHeader>
248+
<CardContent className="space-y-4">
249+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
250+
<Dialog open={showDataExport} onOpenChange={setShowDataExport}>
251+
<DialogTrigger asChild>
252+
<Button variant="outline" className="h-auto p-4 flex flex-col items-center gap-2">
253+
<Download className="h-6 w-6" />
254+
<div className="text-center">
255+
<p className="font-medium">Exporter mes données</p>
256+
<p className="text-xs text-muted-foreground">Télécharger vos données</p>
257+
</div>
258+
</Button>
259+
</DialogTrigger>
260+
<DialogContent>
261+
<DialogHeader>
262+
<DialogTitle>Exporter vos données</DialogTitle>
263+
<DialogDescription>
264+
Téléchargez toutes les données que nous avons collectées à votre sujet.
265+
</DialogDescription>
266+
</DialogHeader>
267+
<div className="space-y-4">
268+
<Alert>
269+
<AlertTriangle className="h-4 w-4" />
270+
<AlertDescription>
271+
Le fichier contiendra vos préférences de confidentialité et métadonnées de consentement.
272+
</AlertDescription>
273+
</Alert>
274+
<Button onClick={handleExportData} className="w-full">
275+
<Download className="h-4 w-4 mr-2" />
276+
Télécharger mes données
277+
</Button>
278+
</div>
279+
</DialogContent>
280+
</Dialog>
281+
282+
<Dialog open={showDataDeletion} onOpenChange={setShowDataDeletion}>
283+
<DialogTrigger asChild>
284+
<Button variant="outline" className="h-auto p-4 flex flex-col items-center gap-2">
285+
<Trash2 className="h-6 w-6" />
286+
<div className="text-center">
287+
<p className="font-medium">Supprimer mes données</p>
288+
<p className="text-xs text-muted-foreground">Droit à l'effacement</p>
289+
</div>
290+
</Button>
291+
</DialogTrigger>
292+
<DialogContent>
293+
<DialogHeader>
294+
<DialogTitle>Supprimer vos données</DialogTitle>
295+
<DialogDescription>
296+
Cette action supprimera toutes vos préférences de confidentialité.
297+
</DialogDescription>
298+
</DialogHeader>
299+
<div className="space-y-4">
300+
<Alert>
301+
<AlertTriangle className="h-4 w-4" />
302+
<AlertDescription>
303+
Attention : Cette action est irréversible. Vos préférences seront perdues.
304+
</AlertDescription>
305+
</Alert>
306+
<Button
307+
variant="destructive"
308+
onClick={() => {
309+
resetConsent();
310+
setShowDataDeletion(false);
311+
}}
312+
className="w-full"
313+
>
314+
<Trash2 className="h-4 w-4 mr-2" />
315+
Confirmer la suppression
316+
</Button>
317+
</div>
318+
</DialogContent>
319+
</Dialog>
320+
321+
<Button variant="outline" className="h-auto p-4 flex flex-col items-center gap-2">
322+
<Shield className="h-6 w-6" />
323+
<div className="text-center">
324+
<p className="font-medium">Nous contacter</p>
325+
<p className="text-xs text-muted-foreground">Questions sur vos données</p>
326+
</div>
327+
</Button>
328+
</div>
329+
</CardContent>
330+
</Card>
331+
</div>
332+
);
333+
};

0 commit comments

Comments
 (0)