Files
gridpilot.gg/apps/website/components/feed/FeedItemCard.tsx
2025-12-10 15:41:44 +01:00

124 lines
3.7 KiB
TypeScript

import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import Image from 'next/image';
import type { FeedItem } from '@gridpilot/social/domain/entities/FeedItem';
import { getDriverRepository, getImageService, getSocialRepository } from '@/lib/di-container';
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);
return `${diffDays} d ago`;
}
async function resolveActor(item: FeedItem) {
const driverRepo = getDriverRepository();
const imageService = getImageService();
const socialRepo = getSocialRepository();
if (item.actorFriendId) {
// Try social graph first (friend display name/avatar)
try {
const friend = await socialRepo.getFriendByDriverId?.(item.actorFriendId);
if (friend) {
return {
name: friend.displayName ?? friend.driverName ?? `Driver ${item.actorFriendId}`,
avatarUrl: friend.avatarUrl ?? imageService.getDriverAvatar(item.actorFriendId),
};
}
} catch {
// fall through to driver lookup
}
// Fallback to driver entity + image service
try {
const driver = await driverRepo.findById(item.actorFriendId);
if (driver) {
return {
name: driver.name,
avatarUrl: imageService.getDriverAvatar(driver.id),
};
}
} catch {
// ignore and return null below
}
}
return null;
}
interface FeedItemCardProps {
item: FeedItem;
}
export default function FeedItemCard({ item }: FeedItemCardProps) {
const [actor, setActor] = useState<{ name: string; avatarUrl: string } | null>(null);
useEffect(() => {
let cancelled = false;
void (async () => {
const resolved = await resolveActor(item);
if (!cancelled) {
setActor(resolved);
}
})();
return () => {
cancelled = true;
};
}, [item]);
return (
<div className="flex gap-4">
<div className="flex-shrink-0">
{actor ? (
<div className="w-10 h-10 rounded-full overflow-hidden bg-charcoal-outline">
<Image
src={actor.avatarUrl}
alt={actor.name}
width={40}
height={40}
className="w-full h-full object-cover"
/>
</div>
) : (
<Card className="w-10 h-10 flex items-center justify-center rounded-full bg-primary-blue/10 border-primary-blue/40 p-0">
<span className="text-xs text-primary-blue font-semibold">
{item.type.startsWith('friend') ? 'FR' : 'LG'}
</span>
</Card>
)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-2">
<div>
<p className="text-sm text-white">{item.headline}</p>
{item.body && (
<p className="text-xs text-gray-400 mt-1">{item.body}</p>
)}
</div>
<span className="text-[11px] text-gray-500 whitespace-nowrap">
{timeAgo(item.timestamp)}
</span>
</div>
{item.ctaHref && item.ctaLabel && (
<div className="mt-3">
<Button
as="a"
href={item.ctaHref}
variant="secondary"
className="text-xs px-4 py-2"
>
{item.ctaLabel}
</Button>
</div>
)}
</div>
</div>
);
}