feat: Implement recent activity feed

This commit is contained in:
gpt-engineer-app[bot]
2025-09-29 19:14:56 +00:00
parent 35bb2e09a8
commit 73e9afb7a6

View File

@@ -1,5 +1,6 @@
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useParams, useNavigate, Link } from 'react-router-dom';
import { formatDistanceToNow } from 'date-fns';
import { Header } from '@/components/layout/Header';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
@@ -54,6 +55,8 @@ export default function Profile() {
coasterCount: 0,
parkCount: 0
});
const [recentActivity, setRecentActivity] = useState<any[]>([]);
const [activityLoading, setActivityLoading] = useState(false);
// Profile field access checking
const { canViewField, loading: fieldAccessLoading } = useProfileFieldAccess(profile?.user_id);
@@ -112,6 +115,45 @@ export default function Profile() {
});
}
};
const fetchRecentActivity = async (userId: string) => {
setActivityLoading(true);
try {
// Fetch last 10 reviews
const { data: reviews, error: reviewsError } = await supabase
.from('reviews')
.select('id, rating, title, created_at, moderation_status, park_id, ride_id, parks(name, slug), rides(name, slug, parks(name, slug))')
.eq('user_id', userId)
.order('created_at', { ascending: false })
.limit(10);
if (reviewsError) throw reviewsError;
// Fetch last 10 ride credits
const { data: credits, error: creditsError } = await supabase
.from('user_ride_credits')
.select('id, ride_count, first_ride_date, created_at, rides(name, slug, parks(name, slug))')
.eq('user_id', userId)
.order('created_at', { ascending: false })
.limit(10);
if (creditsError) throw creditsError;
// Combine and sort by date
const combined = [
...(reviews?.map(r => ({ ...r, type: 'review' })) || []),
...(credits?.map(c => ({ ...c, type: 'credit' })) || [])
].sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
.slice(0, 15);
setRecentActivity(combined);
} catch (error: any) {
console.error('Error fetching recent activity:', error);
setRecentActivity([]);
} finally {
setActivityLoading(false);
}
};
const getCurrentUser = async () => {
const {
data: {
@@ -140,8 +182,9 @@ export default function Profile() {
setAvatarUrl(data.avatar_url || '');
setAvatarImageId(data.avatar_image_id || '');
// Fetch calculated stats for this user
// Fetch calculated stats and recent activity for this user
await fetchCalculatedStats(data.user_id);
await fetchRecentActivity(data.user_id);
}
} catch (error: any) {
console.error('Error fetching profile:', error);
@@ -180,8 +223,9 @@ export default function Profile() {
setAvatarUrl(data.avatar_url || '');
setAvatarImageId(data.avatar_image_id || '');
// Fetch calculated stats for the current user
// Fetch calculated stats and recent activity for the current user
await fetchCalculatedStats(user.id);
await fetchRecentActivity(user.id);
}
} catch (error: any) {
console.error('Error fetching profile:', error);
@@ -539,13 +583,93 @@ export default function Profile() {
</CardDescription>
</CardHeader>
<CardContent>
<div className="text-center py-12">
<Trophy className="w-16 h-16 text-muted-foreground mx-auto mb-4" />
<h3 className="text-xl font-semibold mb-2">Activity Feed Coming Soon</h3>
<p className="text-muted-foreground">
Track reviews, ratings, and achievements
</p>
</div>
{activityLoading ? (
<div className="flex justify-center py-12">
<Loader2 className="w-8 h-8 animate-spin text-muted-foreground" />
</div>
) : recentActivity.length === 0 ? (
<div className="text-center py-12">
<Trophy className="w-16 h-16 text-muted-foreground mx-auto mb-4" />
<h3 className="text-xl font-semibold mb-2">No recent activity yet</h3>
<p className="text-muted-foreground">
Reviews and ride credits will appear here
</p>
</div>
) : (
<div className="space-y-4">
{recentActivity.map(activity => (
<div key={`${activity.type}-${activity.id}`} className="flex gap-4 p-4 rounded-lg border bg-card hover:bg-accent/5 transition-colors">
<div className="flex-shrink-0 mt-1">
{activity.type === 'review' ? (
<Star className="w-5 h-5 text-accent" />
) : (
<Trophy className="w-5 h-5 text-accent" />
)}
</div>
<div className="flex-1 min-w-0">
{activity.type === 'review' ? (
<>
<div className="flex items-center gap-2 mb-1">
<p className="font-medium">
{activity.title || 'Left a review'}
</p>
{activity.moderation_status === 'pending' && (
<Badge variant="secondary" className="text-xs">Pending</Badge>
)}
{activity.moderation_status === 'flagged' && (
<Badge variant="destructive" className="text-xs">Flagged</Badge>
)}
</div>
<div className="flex items-center gap-1 mb-2">
{[...Array(5)].map((_, i) => (
<Star key={i} className={`w-3 h-3 ${i < activity.rating ? 'fill-accent text-accent' : 'text-muted-foreground'}`} />
))}
</div>
{activity.park_id && activity.parks ? (
<Link to={`/parks/${activity.parks.slug}`} className="text-sm text-muted-foreground hover:text-accent transition-colors">
{activity.parks.name}
</Link>
) : activity.ride_id && activity.rides ? (
<div className="text-sm text-muted-foreground">
<Link to={`/parks/${activity.rides.parks?.slug}/rides/${activity.rides.slug}`} className="hover:text-accent transition-colors">
{activity.rides.name}
</Link>
{activity.rides.parks && (
<span className="text-muted-foreground/70"> at {activity.rides.parks.name}</span>
)}
</div>
) : null}
</>
) : (
<>
<p className="font-medium mb-1">Added ride credit</p>
{activity.rides && (
<div className="text-sm text-muted-foreground">
<Link to={`/parks/${activity.rides.parks?.slug}/rides/${activity.rides.slug}`} className="hover:text-accent transition-colors">
{activity.rides.name}
</Link>
{activity.rides.parks && (
<span className="text-muted-foreground/70"> at {activity.rides.parks.name}</span>
)}
</div>
)}
{activity.ride_count > 1 && (
<p className="text-xs text-muted-foreground mt-1">
Ridden {activity.ride_count} times
</p>
)}
</>
)}
</div>
<div className="flex-shrink-0 text-xs text-muted-foreground">
{formatDistanceToNow(new Date(activity.created_at), { addSuffix: true })}
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
</TabsContent>