105 lines
3.0 KiB
TypeScript
105 lines
3.0 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import Card from '@/ui/Card';
|
|
import Button from '@/ui/Button';
|
|
import Image from 'next/image';
|
|
|
|
interface FeedItemData {
|
|
id: string;
|
|
type: string;
|
|
headline: string;
|
|
body?: string;
|
|
timestamp: string;
|
|
formattedTime: string;
|
|
ctaHref?: string;
|
|
ctaLabel?: string;
|
|
}
|
|
|
|
function timeAgo(timestamp: Date | string): string {
|
|
const date = typeof timestamp === 'string' ? new Date(timestamp) : timestamp;
|
|
const diffMs = Date.now() - date.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: FeedItemData) {
|
|
// Actor resolution is not wired through the API in this build.
|
|
// Keep rendering deterministic and decoupled (no core repos).
|
|
return null;
|
|
}
|
|
|
|
interface FeedItemCardProps {
|
|
item: FeedItemData;
|
|
}
|
|
|
|
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>
|
|
);
|
|
} |