mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 08:11: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;
|
||||
|
||||
// Fetch last 10 submissions
|
||||
// Fetch last 10 submissions with enriched data
|
||||
let submissionsQuery = supabase
|
||||
.from('content_submissions')
|
||||
.select('id, submission_type, content, status, created_at')
|
||||
@@ -191,6 +191,59 @@ export default function Profile() {
|
||||
const { data: submissions, error: submissionsError } = await submissionsQuery;
|
||||
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)
|
||||
let rankingsQuery = supabase
|
||||
.from('user_top_lists')
|
||||
@@ -210,7 +263,7 @@ export default function Profile() {
|
||||
const combined = [
|
||||
...(reviews?.map(r => ({ ...r, type: 'review' 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 })) || [])
|
||||
].sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
|
||||
.slice(0, 15) as ActivityEntry[];
|
||||
@@ -731,8 +784,9 @@ export default function Profile() {
|
||||
<>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<p className="font-medium">
|
||||
Submitted {(activity as any).submission_type || 'content'}
|
||||
{(activity as any).content?.name && `: ${(activity as any).content.name}`}
|
||||
{(activity as any).content?.action === 'edit' ? 'Edited' : 'Submitted'}{' '}
|
||||
{(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>
|
||||
{(activity as any).status === 'pending' && (
|
||||
<Badge variant="secondary" className="text-xs">Pending</Badge>
|
||||
@@ -743,8 +797,93 @@ export default function Profile() {
|
||||
{(activity as any).status === 'rejected' && (
|
||||
<Badge variant="destructive" className="text-xs">Rejected</Badge>
|
||||
)}
|
||||
{(activity as any).status === 'partially_approved' && (
|
||||
<Badge variant="outline" className="text-xs">Partially Approved</Badge>
|
||||
)}
|
||||
</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">
|
||||
{(activity as any).content.description}
|
||||
</p>
|
||||
@@ -753,10 +892,18 @@ export default function Profile() {
|
||||
) : activity.type === 'ranking' ? (
|
||||
<>
|
||||
<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">
|
||||
{((activity as any).list_type || '').replace('_', ' ')}
|
||||
</Badge>
|
||||
{(activity as any).is_public === false && (
|
||||
<Badge variant="secondary" className="text-xs">Private</Badge>
|
||||
)}
|
||||
</div>
|
||||
{(activity as any).description && (
|
||||
<p className="text-sm text-muted-foreground line-clamp-2">
|
||||
|
||||
@@ -331,15 +331,28 @@ export interface SubmissionActivity {
|
||||
submission_type?: string;
|
||||
entity_type?: 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 {
|
||||
id: string;
|
||||
type: 'ranking';
|
||||
created_at: string;
|
||||
parks?: { slug?: string; name?: string } | null;
|
||||
name?: string;
|
||||
position?: number;
|
||||
title?: string;
|
||||
description?: string;
|
||||
list_type?: string;
|
||||
is_public?: boolean;
|
||||
}
|
||||
|
||||
export interface GenericActivity {
|
||||
|
||||
Reference in New Issue
Block a user