180 lines
6.1 KiB
TypeScript
180 lines
6.1 KiB
TypeScript
'use client';
|
|
|
|
import { Calendar, Award, UserPlus, UserMinus, Shield, Flag, AlertTriangle } from 'lucide-react';
|
|
import { useLeagueRaces } from '@/hooks/league/useLeagueRaces';
|
|
|
|
export type LeagueActivity =
|
|
| { type: 'race_completed'; raceId: string; raceName: string; timestamp: Date }
|
|
| { type: 'race_scheduled'; raceId: string; raceName: string; timestamp: Date }
|
|
| { type: 'penalty_applied'; penaltyId: string; driverName: string; reason: string; points: number; timestamp: Date }
|
|
| { type: 'member_joined'; driverId: string; driverName: string; timestamp: Date }
|
|
| { type: 'member_left'; driverId: string; driverName: string; timestamp: Date }
|
|
| { type: 'role_changed'; driverId: string; driverName: string; oldRole: string; newRole: string; timestamp: Date };
|
|
|
|
interface LeagueActivityFeedProps {
|
|
leagueId: string;
|
|
limit?: number;
|
|
}
|
|
|
|
function timeAgo(timestamp: Date): string {
|
|
const diffMs = Date.now() - timestamp.getTime();
|
|
const diffMinutes = Math.floor(diffMs / 60000);
|
|
if (diffMinutes < 1) return 'Just now';
|
|
if (diffMinutes < 60) return `${diffMinutes} min ago`;
|
|
const diffHours = Math.floor(diffMinutes / 60);
|
|
if (diffHours < 24) return `${diffHours}h ago`;
|
|
const diffDays = Math.floor(diffHours / 24);
|
|
if (diffDays === 1) return 'Yesterday';
|
|
if (diffDays < 7) return `${diffDays}d ago`;
|
|
return timestamp.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
}
|
|
|
|
export default function LeagueActivityFeed({ leagueId, limit = 10 }: LeagueActivityFeedProps) {
|
|
const { data: raceList = [], isLoading } = useLeagueRaces(leagueId);
|
|
|
|
const activities: LeagueActivity[] = [];
|
|
|
|
if (!isLoading && raceList.length > 0) {
|
|
const completedRaces = raceList
|
|
.filter((r) => r.status === 'completed')
|
|
.sort((a, b) => new Date(b.scheduledAt).getTime() - new Date(a.scheduledAt).getTime())
|
|
.slice(0, 5);
|
|
|
|
const upcomingRaces = raceList
|
|
.filter((r) => r.status === 'scheduled')
|
|
.sort((a, b) => new Date(b.scheduledAt).getTime() - new Date(a.scheduledAt).getTime())
|
|
.slice(0, 3);
|
|
|
|
for (const race of completedRaces) {
|
|
activities.push({
|
|
type: 'race_completed',
|
|
raceId: race.id,
|
|
raceName: `${race.track} - ${race.car}`,
|
|
timestamp: new Date(race.scheduledAt),
|
|
});
|
|
}
|
|
|
|
for (const race of upcomingRaces) {
|
|
activities.push({
|
|
type: 'race_scheduled',
|
|
raceId: race.id,
|
|
raceName: `${race.track} - ${race.car}`,
|
|
timestamp: new Date(new Date(race.scheduledAt).getTime() - 7 * 24 * 60 * 60 * 1000), // Simulate schedule announcement
|
|
});
|
|
}
|
|
|
|
// Sort all activities by timestamp
|
|
activities.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
|
|
activities.splice(limit); // Limit results
|
|
}
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="text-center text-gray-400 py-8">
|
|
Loading activities...
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (activities.length === 0) {
|
|
return (
|
|
<div className="text-center text-gray-400 py-8">
|
|
No recent activity
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{activities.map((activity, index) => (
|
|
<ActivityItem key={`${activity.type}-${index}`} activity={activity} />
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function ActivityItem({ activity }: { activity: LeagueActivity }) {
|
|
const getIcon = () => {
|
|
switch (activity.type) {
|
|
case 'race_completed':
|
|
return <Flag className="w-4 h-4 text-performance-green" />;
|
|
case 'race_scheduled':
|
|
return <Calendar className="w-4 h-4 text-primary-blue" />;
|
|
case 'penalty_applied':
|
|
return <AlertTriangle className="w-4 h-4 text-warning-amber" />;
|
|
case 'member_joined':
|
|
return <UserPlus className="w-4 h-4 text-performance-green" />;
|
|
case 'member_left':
|
|
return <UserMinus className="w-4 h-4 text-gray-400" />;
|
|
case 'role_changed':
|
|
return <Shield className="w-4 h-4 text-primary-blue" />;
|
|
}
|
|
};
|
|
|
|
const getContent = () => {
|
|
switch (activity.type) {
|
|
case 'race_completed':
|
|
return (
|
|
<>
|
|
<span className="text-white font-medium">Race Completed</span>
|
|
<span className="text-gray-400"> · {activity.raceName}</span>
|
|
</>
|
|
);
|
|
case 'race_scheduled':
|
|
return (
|
|
<>
|
|
<span className="text-white font-medium">Race Scheduled</span>
|
|
<span className="text-gray-400"> · {activity.raceName}</span>
|
|
</>
|
|
);
|
|
case 'penalty_applied':
|
|
return (
|
|
<>
|
|
<span className="text-white font-medium">{activity.driverName}</span>
|
|
<span className="text-gray-400"> received a </span>
|
|
<span className="text-warning-amber">{activity.points}-point penalty</span>
|
|
<span className="text-gray-400"> · {activity.reason}</span>
|
|
</>
|
|
);
|
|
case 'member_joined':
|
|
return (
|
|
<>
|
|
<span className="text-white font-medium">{activity.driverName}</span>
|
|
<span className="text-gray-400"> joined the league</span>
|
|
</>
|
|
);
|
|
case 'member_left':
|
|
return (
|
|
<>
|
|
<span className="text-white font-medium">{activity.driverName}</span>
|
|
<span className="text-gray-400"> left the league</span>
|
|
</>
|
|
);
|
|
case 'role_changed':
|
|
return (
|
|
<>
|
|
<span className="text-white font-medium">{activity.driverName}</span>
|
|
<span className="text-gray-400"> promoted to </span>
|
|
<span className="text-primary-blue">{activity.newRole}</span>
|
|
</>
|
|
);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="flex items-start gap-3 py-3 border-b border-charcoal-outline/30 last:border-0">
|
|
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-iron-gray/50 flex items-center justify-center">
|
|
{getIcon()}
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-sm leading-relaxed">
|
|
{getContent()}
|
|
</p>
|
|
<p className="text-xs text-gray-500 mt-1">
|
|
{timeAgo(activity.timestamp)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|