mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 13:51:13 -05:00
feat: Display user activity history
This commit is contained in:
@@ -174,7 +174,7 @@ export default function Profile() {
|
|||||||
|
|
||||||
if (creditsError) throw creditsError;
|
if (creditsError) throw creditsError;
|
||||||
|
|
||||||
// Fetch last 10 submissions
|
// Fetch last 10 submissions with enriched data
|
||||||
let submissionsQuery = supabase
|
let submissionsQuery = supabase
|
||||||
.from('content_submissions')
|
.from('content_submissions')
|
||||||
.select('id, submission_type, content, status, created_at')
|
.select('id, submission_type, content, status, created_at')
|
||||||
@@ -191,6 +191,59 @@ export default function Profile() {
|
|||||||
const { data: submissions, error: submissionsError } = await submissionsQuery;
|
const { data: submissions, error: submissionsError } = await submissionsQuery;
|
||||||
if (submissionsError) throw submissionsError;
|
if (submissionsError) throw submissionsError;
|
||||||
|
|
||||||
|
// Enrich submissions with entity data and photos
|
||||||
|
const enrichedSubmissions = await Promise.all((submissions || []).map(async (sub) => {
|
||||||
|
const enriched: any = { ...sub };
|
||||||
|
|
||||||
|
// For photo submissions, get photo count and preview
|
||||||
|
if (sub.submission_type === 'photo') {
|
||||||
|
const { data: photoSubs } = await supabase
|
||||||
|
.from('photo_submissions')
|
||||||
|
.select('id, entity_type, entity_id')
|
||||||
|
.eq('submission_id', sub.id)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
if (photoSubs) {
|
||||||
|
const { data: photoItems, count } = await supabase
|
||||||
|
.from('photo_submission_items')
|
||||||
|
.select('cloudflare_image_url', { count: 'exact' })
|
||||||
|
.eq('photo_submission_id', photoSubs.id)
|
||||||
|
.order('order_index', { ascending: true })
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
enriched.photo_count = count || 0;
|
||||||
|
enriched.photo_preview = photoItems?.[0]?.cloudflare_image_url;
|
||||||
|
enriched.entity_type = photoSubs.entity_type;
|
||||||
|
enriched.entity_id = photoSubs.entity_id;
|
||||||
|
|
||||||
|
// Get entity name/slug for linking
|
||||||
|
if (photoSubs.entity_type === 'park') {
|
||||||
|
const { data: park } = await supabase
|
||||||
|
.from('parks')
|
||||||
|
.select('name, slug')
|
||||||
|
.eq('id', photoSubs.entity_id)
|
||||||
|
.single();
|
||||||
|
enriched.content = { ...enriched.content, entity_name: park?.name, entity_slug: park?.slug };
|
||||||
|
} else if (photoSubs.entity_type === 'ride') {
|
||||||
|
const { data: ride } = await supabase
|
||||||
|
.from('rides')
|
||||||
|
.select('name, slug, parks!inner(name, slug)')
|
||||||
|
.eq('id', photoSubs.entity_id)
|
||||||
|
.single();
|
||||||
|
enriched.content = {
|
||||||
|
...enriched.content,
|
||||||
|
entity_name: ride?.name,
|
||||||
|
entity_slug: ride?.slug,
|
||||||
|
park_name: ride?.parks?.name,
|
||||||
|
park_slug: ride?.parks?.slug
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return enriched;
|
||||||
|
}));
|
||||||
|
|
||||||
// Fetch last 10 rankings (public top lists)
|
// Fetch last 10 rankings (public top lists)
|
||||||
let rankingsQuery = supabase
|
let rankingsQuery = supabase
|
||||||
.from('user_top_lists')
|
.from('user_top_lists')
|
||||||
@@ -210,7 +263,7 @@ export default function Profile() {
|
|||||||
const combined = [
|
const combined = [
|
||||||
...(reviews?.map(r => ({ ...r, type: 'review' as const })) || []),
|
...(reviews?.map(r => ({ ...r, type: 'review' as const })) || []),
|
||||||
...(credits?.map(c => ({ ...c, type: 'credit' as const })) || []),
|
...(credits?.map(c => ({ ...c, type: 'credit' as const })) || []),
|
||||||
...(submissions?.map(s => ({ ...s, type: 'submission' as const })) || []),
|
...(enrichedSubmissions?.map(s => ({ ...s, type: 'submission' as const })) || []),
|
||||||
...(rankings?.map(r => ({ ...r, type: 'ranking' as const })) || [])
|
...(rankings?.map(r => ({ ...r, type: 'ranking' as const })) || [])
|
||||||
].sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
|
].sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
|
||||||
.slice(0, 15) as ActivityEntry[];
|
.slice(0, 15) as ActivityEntry[];
|
||||||
@@ -731,8 +784,9 @@ export default function Profile() {
|
|||||||
<>
|
<>
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<div className="flex items-center gap-2 mb-1">
|
||||||
<p className="font-medium">
|
<p className="font-medium">
|
||||||
Submitted {(activity as any).submission_type || 'content'}
|
{(activity as any).content?.action === 'edit' ? 'Edited' : 'Submitted'}{' '}
|
||||||
{(activity as any).content?.name && `: ${(activity as any).content.name}`}
|
{(activity as any).submission_type === 'photo' ? 'photos for' : (activity as any).submission_type || 'content'}
|
||||||
|
{(activity as any).content?.name && ` ${(activity as any).content.name}`}
|
||||||
</p>
|
</p>
|
||||||
{(activity as any).status === 'pending' && (
|
{(activity as any).status === 'pending' && (
|
||||||
<Badge variant="secondary" className="text-xs">Pending</Badge>
|
<Badge variant="secondary" className="text-xs">Pending</Badge>
|
||||||
@@ -743,8 +797,93 @@ export default function Profile() {
|
|||||||
{(activity as any).status === 'rejected' && (
|
{(activity as any).status === 'rejected' && (
|
||||||
<Badge variant="destructive" className="text-xs">Rejected</Badge>
|
<Badge variant="destructive" className="text-xs">Rejected</Badge>
|
||||||
)}
|
)}
|
||||||
|
{(activity as any).status === 'partially_approved' && (
|
||||||
|
<Badge variant="outline" className="text-xs">Partially Approved</Badge>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{(activity as any).content?.description && (
|
|
||||||
|
{/* Photo preview for photo submissions */}
|
||||||
|
{(activity as any).submission_type === 'photo' && (activity as any).photo_preview && (
|
||||||
|
<div className="flex gap-2 items-center mb-2">
|
||||||
|
<img
|
||||||
|
src={(activity as any).photo_preview}
|
||||||
|
alt="Photo preview"
|
||||||
|
className="w-16 h-16 rounded object-cover border"
|
||||||
|
/>
|
||||||
|
{(activity as any).photo_count > 1 && (
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
+{(activity as any).photo_count - 1} more
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Entity link for photo submissions */}
|
||||||
|
{(activity as any).submission_type === 'photo' && (activity as any).content?.entity_slug && (
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
{(activity as any).entity_type === 'park' ? (
|
||||||
|
<Link to={`/parks/${(activity as any).content.entity_slug}`} className="hover:text-accent transition-colors">
|
||||||
|
{(activity as any).content.entity_name || 'View park'}
|
||||||
|
</Link>
|
||||||
|
) : (activity as any).entity_type === 'ride' ? (
|
||||||
|
<>
|
||||||
|
<Link to={`/parks/${(activity as any).content.park_slug}/rides/${(activity as any).content.entity_slug}`} className="hover:text-accent transition-colors">
|
||||||
|
{(activity as any).content.entity_name || 'View ride'}
|
||||||
|
</Link>
|
||||||
|
{(activity as any).content.park_name && (
|
||||||
|
<span className="text-muted-foreground/70"> at {(activity as any).content.park_name}</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Links for entity submissions */}
|
||||||
|
{(activity as any).status === 'approved' && (activity as any).submission_type !== 'photo' && (
|
||||||
|
<>
|
||||||
|
{(activity as any).submission_type === 'park' && (activity as any).content?.slug && (
|
||||||
|
<Link
|
||||||
|
to={`/parks/${(activity as any).content.slug}`}
|
||||||
|
className="text-sm text-accent hover:underline"
|
||||||
|
>
|
||||||
|
View park →
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
{(activity as any).submission_type === 'ride' && (activity as any).content?.slug && (activity as any).content?.park_slug && (
|
||||||
|
<div className="text-sm">
|
||||||
|
<Link
|
||||||
|
to={`/parks/${(activity as any).content.park_slug}/rides/${(activity as any).content.slug}`}
|
||||||
|
className="text-accent hover:underline"
|
||||||
|
>
|
||||||
|
View ride →
|
||||||
|
</Link>
|
||||||
|
{(activity as any).content.park_name && (
|
||||||
|
<span className="text-muted-foreground ml-1">
|
||||||
|
at {(activity as any).content.park_name}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{(activity as any).submission_type === 'company' && (activity as any).content?.slug && (
|
||||||
|
<Link
|
||||||
|
to={`/${(activity as any).content.company_type === 'operator' ? 'operators' : (activity as any).content.company_type === 'property_owner' ? 'owners' : (activity as any).content.company_type === 'manufacturer' ? 'manufacturers' : 'designers'}/${(activity as any).content.slug}`}
|
||||||
|
className="text-sm text-accent hover:underline"
|
||||||
|
>
|
||||||
|
View {(activity as any).content.company_type || 'company'} →
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
{(activity as any).submission_type === 'ride_model' && (activity as any).content?.slug && (activity as any).content?.manufacturer_slug && (
|
||||||
|
<Link
|
||||||
|
to={`/manufacturers/${(activity as any).content.manufacturer_slug}/models/${(activity as any).content.slug}`}
|
||||||
|
className="text-sm text-accent hover:underline"
|
||||||
|
>
|
||||||
|
View model →
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(activity as any).content?.description && (activity as any).submission_type !== 'photo' && (
|
||||||
<p className="text-sm text-muted-foreground line-clamp-2">
|
<p className="text-sm text-muted-foreground line-clamp-2">
|
||||||
{(activity as any).content.description}
|
{(activity as any).content.description}
|
||||||
</p>
|
</p>
|
||||||
@@ -753,10 +892,18 @@ export default function Profile() {
|
|||||||
) : activity.type === 'ranking' ? (
|
) : activity.type === 'ranking' ? (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<div className="flex items-center gap-2 mb-1">
|
||||||
<p className="font-medium">Created ranking: {(activity as any).title || 'Untitled'}</p>
|
<Link
|
||||||
|
to={`/profile/${profile?.username}/lists`}
|
||||||
|
className="font-medium hover:text-accent transition-colors"
|
||||||
|
>
|
||||||
|
Created ranking: {(activity as any).title || 'Untitled'}
|
||||||
|
</Link>
|
||||||
<Badge variant="outline" className="text-xs capitalize">
|
<Badge variant="outline" className="text-xs capitalize">
|
||||||
{((activity as any).list_type || '').replace('_', ' ')}
|
{((activity as any).list_type || '').replace('_', ' ')}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
{(activity as any).is_public === false && (
|
||||||
|
<Badge variant="secondary" className="text-xs">Private</Badge>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{(activity as any).description && (
|
{(activity as any).description && (
|
||||||
<p className="text-sm text-muted-foreground line-clamp-2">
|
<p className="text-sm text-muted-foreground line-clamp-2">
|
||||||
|
|||||||
@@ -331,15 +331,28 @@ export interface SubmissionActivity {
|
|||||||
submission_type?: string;
|
submission_type?: string;
|
||||||
entity_type?: string;
|
entity_type?: string;
|
||||||
action?: string;
|
action?: string;
|
||||||
|
content?: {
|
||||||
|
name?: string;
|
||||||
|
slug?: string;
|
||||||
|
description?: string;
|
||||||
|
entity_id?: string;
|
||||||
|
entity_slug?: string;
|
||||||
|
park_slug?: string;
|
||||||
|
park_name?: string;
|
||||||
|
action?: string;
|
||||||
|
};
|
||||||
|
photo_count?: number;
|
||||||
|
photo_preview?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RankingActivity {
|
export interface RankingActivity {
|
||||||
id: string;
|
id: string;
|
||||||
type: 'ranking';
|
type: 'ranking';
|
||||||
created_at: string;
|
created_at: string;
|
||||||
parks?: { slug?: string; name?: string } | null;
|
title?: string;
|
||||||
name?: string;
|
description?: string;
|
||||||
position?: number;
|
list_type?: string;
|
||||||
|
is_public?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GenericActivity {
|
export interface GenericActivity {
|
||||||
|
|||||||
Reference in New Issue
Block a user