feat: Display user activity history

This commit is contained in:
gpt-engineer-app[bot]
2025-10-17 12:34:10 +00:00
parent cdd9e6c8c6
commit 0ff424fcee
2 changed files with 169 additions and 9 deletions

View File

@@ -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">

View File

@@ -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 {