Sifa professional network frontend (Next.js, React, TailwindCSS) sifa.id/
at main 102 lines 3.6 kB view raw
1'use client'; 2 3import { lazy, Suspense, useEffect, useState } from 'react'; 4import Link from 'next/link'; 5import { useTranslations } from 'next-intl'; 6import { ArrowRight } from '@phosphor-icons/react'; 7import { fetchActivityTeaser } from '@/lib/api'; 8import type { ActivityItem } from '@/lib/api'; 9import { ActivityIndicators } from './activity-indicators'; 10import { getCardComponent } from './activity-cards/card-registry'; 11import { GenericActivityCard } from './activity-cards/generic-activity-card'; 12import { CardErrorBoundary } from './activity-cards/card-error-boundary'; 13import type { ActiveApp } from '@/lib/types'; 14 15const ActivityHeatmap = lazy(() => 16 import('./activity-heatmap/activity-heatmap').then((m) => ({ default: m.ActivityHeatmap })), 17); 18 19interface ActivityOverviewProps { 20 handle: string; 21 activeApps?: ActiveApp[]; 22} 23 24export function ActivityOverview({ handle, activeApps = [] }: ActivityOverviewProps) { 25 const t = useTranslations('activityOverview'); 26 const [items, setItems] = useState<ActivityItem[] | null>(null); 27 const [loaded, setLoaded] = useState(false); 28 29 useEffect(() => { 30 let cancelled = false; 31 fetchActivityTeaser(handle).then((data) => { 32 if (cancelled) return; 33 setItems(data?.items ?? null); 34 setLoaded(true); 35 }); 36 return () => { 37 cancelled = true; 38 }; 39 }, [handle]); 40 41 const validItems = (items ?? []).filter((item) => { 42 if (item.record == null) return false; 43 // Hide image-only Bluesky posts — they render as empty rows in compact mode 44 if ( 45 item.collection === 'app.bsky.feed.post' && 46 !(item.record as { text?: string }).text?.trim() 47 ) { 48 return false; 49 } 50 return true; 51 }); 52 const teaserItems = validItems.slice(0, 5); 53 54 return ( 55 <section className="mt-8" aria-label={t('title')} data-testid="activity-overview"> 56 <div className="flex items-center justify-between"> 57 <h2 className="text-xl font-semibold">{t('title')}</h2> 58 </div> 59 {activeApps.length > 0 && ( 60 <div className="mt-3"> 61 <ActivityIndicators apps={activeApps} maxVisible={5} /> 62 </div> 63 )} 64 <div className="mt-4"> 65 <Suspense fallback={<div className="h-[100px] w-full animate-pulse rounded-lg bg-muted" />}> 66 <ActivityHeatmap handle={handle} days={365} variant="compact" /> 67 </Suspense> 68 </div> 69 {teaserItems.length > 0 && ( 70 <div className="mt-4 space-y-2"> 71 {teaserItems.map((item) => { 72 const SpecificCard = getCardComponent(item.collection); 73 const CardComponent = SpecificCard ?? GenericActivityCard; 74 const did = item.uri.split('/')[2] ?? ''; 75 return ( 76 <CardErrorBoundary key={item.uri}> 77 <CardComponent 78 uri={item.uri} 79 collection={item.collection} 80 rkey={item.rkey} 81 record={item.record} 82 authorDid={did} 83 authorHandle={handle} 84 showAuthor={false} 85 compact={true} 86 /> 87 </CardErrorBoundary> 88 ); 89 })} 90 </div> 91 )} 92 <Link 93 href={`/p/${handle}/activity`} 94 className="mt-4 flex w-full items-center justify-center gap-2 rounded-lg bg-muted py-3 text-sm font-medium text-foreground transition-colors hover:bg-muted/80" 95 data-testid="activity-view-all" 96 > 97 {t('viewAll')} 98 <ArrowRight className="h-4 w-4" weight="bold" aria-hidden="true" /> 99 </Link> 100 </section> 101 ); 102}