import { useState, useEffect } from 'react'; import { invokeWithTracking } from '@/lib/edgeFunctionTracking'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Separator } from '@/components/ui/separator'; import { Skeleton } from '@/components/ui/skeleton'; import { Switch } from '@/components/ui/switch'; import { Label } from '@/components/ui/label'; import { useAuth } from '@/hooks/useAuth'; import { useProfile } from '@/hooks/useProfile'; import { supabase } from '@/lib/supabaseClient'; import { handleError, handleSuccess, handleNonCriticalError, AppError } from '@/lib/errorHandler'; import { Download, Activity, BarChart3, AlertCircle, Clock } from 'lucide-react'; import type { UserStatistics, ActivityLogEntry, ExportOptions, ExportRequestResult } from '@/types/data-export'; import { exportOptionsSchema, DEFAULT_EXPORT_OPTIONS } from '@/lib/dataExportValidation'; export function DataExportTab() { const { user } = useAuth(); const { data: profile } = useProfile(user?.id); const [loading, setLoading] = useState(true); const [exporting, setExporting] = useState(false); const [statistics, setStatistics] = useState(null); const [recentActivity, setRecentActivity] = useState([]); const [exportOptions, setExportOptions] = useState(DEFAULT_EXPORT_OPTIONS); const [rateLimited, setRateLimited] = useState(false); const [nextAvailableAt, setNextAvailableAt] = useState(null); useEffect(() => { if (user && profile) { loadStatistics(); loadRecentActivity(); } }, [user, profile]); const loadStatistics = async () => { if (!user || !profile) return; try { // Fetch additional counts const { count: photoCount } = await supabase .from('photos') .select('*', { count: 'exact', head: true }) .eq('submitted_by', user.id); // Note: user_lists table needs to be created for this to work // For now, setting to 0 as placeholder const listCount = 0; const { count: submissionCount } = await supabase .from('content_submissions') .select('*', { count: 'exact', head: true }) .eq('user_id', user.id); const stats: UserStatistics = { ride_count: profile.ride_count || 0, coaster_count: profile.coaster_count || 0, park_count: profile.park_count || 0, review_count: profile.review_count || 0, reputation_score: profile.reputation_score || 0, photo_count: photoCount || 0, list_count: listCount || 0, submission_count: submissionCount || 0, account_created: profile.created_at, last_updated: profile.updated_at }; setStatistics(stats); } catch (error: unknown) { handleError(error, { action: 'Load statistics', userId: user.id }); } }; const loadRecentActivity = async () => { if (!user) return; try { const { data, error } = await supabase .from('profile_audit_log') .select(` id, action, created_at, changed_by, ip_address_hash, user_agent, profile_change_fields(field_name, old_value, new_value) `) .eq('user_id', user.id) .order('created_at', { ascending: false }) .limit(10); if (error) { throw error; } // Transform the data to match our type const activityData: ActivityLogEntry[] = (data || []).map(item => { const changes: Record = {}; if (item.profile_change_fields) { for (const field of item.profile_change_fields) { changes[field.field_name] = { old: field.old_value, new: field.new_value }; } } return { id: item.id, action: item.action, changes, created_at: item.created_at, changed_by: item.changed_by, ip_address_hash: item.ip_address_hash || undefined, user_agent: item.user_agent || undefined }; }); setRecentActivity(activityData); } catch (error: unknown) { handleError(error, { action: 'Load activity log', userId: user.id }); } finally { setLoading(false); } }; const handleDataExport = async () => { if (!user) return; setExporting(true); try { // Validate export options const validatedOptions = exportOptionsSchema.parse(exportOptions); // Call edge function for secure export const { data, error, requestId } = await invokeWithTracking( 'export-user-data', validatedOptions, user.id ); if (error) { throw error; } if (!data?.success) { if (data?.rate_limited) { setRateLimited(true); setNextAvailableAt(data.next_available_at || null); handleError( new AppError( 'Rate limited', 'RATE_LIMIT_EXCEEDED', data.error || 'You can export your data once per hour. Please try again later.' ), { action: 'Export data', userId: user.id } ); return; } throw new Error(data?.error || 'Export failed'); } // Download the data as JSON const blob = new Blob([JSON.stringify(data.data, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `thrillwiki-data-export-${new Date().toISOString().split('T')[0]}.json`; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); handleSuccess( 'Data exported successfully', 'Your data has been downloaded as a JSON file.' ); // Refresh activity log to show the export action await loadRecentActivity(); } catch (error: unknown) { handleError(error, { action: 'Export data', userId: user.id }); } finally { setExporting(false); } }; const formatActionName = (action: string): string => { return action .split('_') .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join(' '); }; const formatDate = (dateString: string): string => { return new Date(dateString).toLocaleString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); }; if (loading) { return (
); } return (
{/* Statistics + Recent Activity Grid */}
{/* Personal Statistics */}
Personal Statistics
Your activity and contribution statistics on ThrillWiki
{statistics && (

Reviews

{statistics.review_count}

Rides Tracked

{statistics.ride_count}

Coasters

{statistics.coaster_count}

Parks Visited

{statistics.park_count}

Photos

{statistics.photo_count}

Lists

{statistics.list_count}

Submissions

{statistics.submission_count}

Reputation

{statistics.reputation_score}

)}
{/* Account Activity */}
Recent Activity
Recent account activity and changes
{recentActivity.length === 0 ? (

No recent activity to display

) : (
{recentActivity.map((activity) => (

{formatActionName(activity.action)}

{formatDate(activity.created_at)}

))}
)}
{/* Export Your Data - Full Width */}
Export Your Data
Export all your ThrillWiki data in JSON format. This includes your profile, reviews, lists, and activity history.
{rateLimited && nextAvailableAt && (

Rate Limited

You can export your data once per hour. Next export available at{' '} {formatDate(nextAvailableAt)}.

)}

Choose what to include in your export:

setExportOptions({ ...exportOptions, include_reviews: checked }) } />
setExportOptions({ ...exportOptions, include_lists: checked }) } />
setExportOptions({ ...exportOptions, include_activity_log: checked }) } />
setExportOptions({ ...exportOptions, include_preferences: checked }) } />

GDPR Compliance

This export includes all personal data we store about you. You can use this for backup purposes or to migrate to another service.

); }