mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-21 13:51:12 -05:00
Refactor code structure and remove redundant changes
This commit is contained in:
435
src-old/components/settings/DataExportTab.tsx
Normal file
435
src-old/components/settings/DataExportTab.tsx
Normal file
@@ -0,0 +1,435 @@
|
||||
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<UserStatistics | null>(null);
|
||||
const [recentActivity, setRecentActivity] = useState<ActivityLogEntry[]>([]);
|
||||
const [exportOptions, setExportOptions] = useState<ExportOptions>(DEFAULT_EXPORT_OPTIONS);
|
||||
const [rateLimited, setRateLimited] = useState(false);
|
||||
const [nextAvailableAt, setNextAvailableAt] = useState<string | null>(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<string, any> = {};
|
||||
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<ExportRequestResult>(
|
||||
'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 (
|
||||
<div className="space-y-8">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-6 w-48" />
|
||||
<Skeleton className="h-4 w-full max-w-md" />
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<Skeleton className="h-12 w-full" />
|
||||
<Skeleton className="h-12 w-full" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Statistics + Recent Activity Grid */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Personal Statistics */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<BarChart3 className="w-5 h-5" />
|
||||
<CardTitle>Personal Statistics</CardTitle>
|
||||
</div>
|
||||
<CardDescription>
|
||||
Your activity and contribution statistics on ThrillWiki
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{statistics && (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-muted-foreground">Reviews</p>
|
||||
<p className="text-2xl font-bold">{statistics.review_count}</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-muted-foreground">Rides Tracked</p>
|
||||
<p className="text-2xl font-bold">{statistics.ride_count}</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-muted-foreground">Coasters</p>
|
||||
<p className="text-2xl font-bold">{statistics.coaster_count}</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-muted-foreground">Parks Visited</p>
|
||||
<p className="text-2xl font-bold">{statistics.park_count}</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-muted-foreground">Photos</p>
|
||||
<p className="text-2xl font-bold">{statistics.photo_count}</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-muted-foreground">Lists</p>
|
||||
<p className="text-2xl font-bold">{statistics.list_count}</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-muted-foreground">Submissions</p>
|
||||
<p className="text-2xl font-bold">{statistics.submission_count}</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-muted-foreground">Reputation</p>
|
||||
<p className="text-2xl font-bold">{statistics.reputation_score}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Account Activity */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<Activity className="w-5 h-5" />
|
||||
<CardTitle>Recent Activity</CardTitle>
|
||||
</div>
|
||||
<CardDescription>
|
||||
Recent account activity and changes
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{recentActivity.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground text-center py-4">
|
||||
No recent activity to display
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{recentActivity.map((activity) => (
|
||||
<div key={activity.id} className="flex items-start gap-3 pb-4 border-b last:border-0 last:pb-0">
|
||||
<Activity className="w-4 h-4 text-muted-foreground mt-1" />
|
||||
<div className="flex-1 space-y-1">
|
||||
<p className="text-sm font-medium">
|
||||
{formatActionName(activity.action)}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatDate(activity.created_at)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Export Your Data - Full Width */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<Download className="w-5 h-5" />
|
||||
<CardTitle>Export Your Data</CardTitle>
|
||||
</div>
|
||||
<CardDescription>
|
||||
Export all your ThrillWiki data in JSON format. This includes your profile, reviews, lists, and activity history.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{rateLimited && nextAvailableAt && (
|
||||
<div className="flex items-start gap-3 p-4 border border-yellow-500/20 bg-yellow-500/10 rounded-lg">
|
||||
<Clock className="w-5 h-5 text-yellow-500 mt-0.5" />
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium">Rate Limited</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
You can export your data once per hour. Next export available at{' '}
|
||||
{formatDate(nextAvailableAt)}.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Choose what to include in your export:
|
||||
</p>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="include_reviews">Include Reviews</Label>
|
||||
<Switch
|
||||
id="include_reviews"
|
||||
checked={exportOptions.include_reviews}
|
||||
onCheckedChange={(checked) =>
|
||||
setExportOptions({ ...exportOptions, include_reviews: checked })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="include_lists">Include Lists</Label>
|
||||
<Switch
|
||||
id="include_lists"
|
||||
checked={exportOptions.include_lists}
|
||||
onCheckedChange={(checked) =>
|
||||
setExportOptions({ ...exportOptions, include_lists: checked })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="include_activity_log">Include Activity Log</Label>
|
||||
<Switch
|
||||
id="include_activity_log"
|
||||
checked={exportOptions.include_activity_log}
|
||||
onCheckedChange={(checked) =>
|
||||
setExportOptions({ ...exportOptions, include_activity_log: checked })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="include_preferences">Include Preferences</Label>
|
||||
<Switch
|
||||
id="include_preferences"
|
||||
checked={exportOptions.include_preferences}
|
||||
onCheckedChange={(checked) =>
|
||||
setExportOptions({ ...exportOptions, include_preferences: checked })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3 p-4 border rounded-lg">
|
||||
<AlertCircle className="w-5 h-5 text-blue-500 mt-0.5" />
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium">GDPR Compliance</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
This export includes all personal data we store about you. You can use this for backup purposes or to migrate to another service.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleDataExport}
|
||||
disabled={exporting || rateLimited}
|
||||
className="w-full"
|
||||
>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
{exporting ? 'Exporting Data...' : 'Export My Data'}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user