fediversion/frontend/components/feed/activity-feed.tsx
fullsizemalt b4cddf41ea feat: Initialize Fediversion multi-band platform
- Fork elmeg-demo codebase for multi-band support
- Add data importer infrastructure with base class
- Create band-specific importers:
  - phish.py: Phish.net API v5
  - grateful_dead.py: Grateful Stats API
  - setlistfm.py: Dead & Company, Billy Strings (Setlist.fm)
- Add spec-kit configuration for Gemini
- Update README with supported bands and architecture
2025-12-28 12:39:28 -08:00

161 lines
6.8 KiB
TypeScript

"use client"
import { useEffect, useState } from "react"
import { getApiUrl } from "@/lib/api-config"
import { Card, CardContent } from "@/components/ui/card"
import { Calendar, MessageSquare, Star, Users } from "lucide-react"
import Link from "next/link"
import { WikiText } from "@/components/ui/wiki-text"
import { UserAvatar } from "@/components/ui/user-avatar"
interface EntityInfo {
type: "performance" | "show" | "song" | "venue" | "tour"
slug: string
title: string
date?: string
}
interface FeedItem {
type: string
timestamp: string
data: any
user: {
id: number
username: string
display_name?: string | null
avatar_bg_color?: string
avatar_text?: string | null
}
entity?: EntityInfo | null
}
export function ActivityFeed() {
const [feed, setFeed] = useState<FeedItem[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
const fetchFeed = async () => {
try {
const res = await fetch(`${getApiUrl()}/feed/`)
if (!res.ok) {
const text = await res.text()
console.error('Feed API error:', res.status, text)
setFeed([]) // Fallback to empty
return
}
const data = await res.json()
setFeed(data)
} catch (error) {
console.error('Failed to fetch feed:', error)
setFeed([])
} finally {
setLoading(false)
}
}
fetchFeed()
}, [])
const getEntityLink = (entity: EntityInfo | null | undefined) => {
if (!entity) return null
const basePath = entity.type === "performance" ? "/performances" :
entity.type === "show" ? "/shows" :
entity.type === "song" ? "/songs" :
entity.type === "venue" ? "/venues" : "/tours"
return `${basePath}/${entity.slug}`
}
const getEntityTypeLabel = (entity: EntityInfo | null | undefined) => {
if (!entity) return "something"
switch (entity.type) {
case "performance": return "a performance of"
case "show": return "the"
case "song": return ""
case "venue": return ""
default: return ""
}
}
const displayName = (user: FeedItem["user"]) => user.display_name || user.username
if (loading) return <div>Loading activity...</div>
return (
<div className="space-y-4">
{feed.map((item, idx) => (
<Card key={idx}>
<CardContent className="pt-6">
<div className="flex items-start gap-4">
<UserAvatar
bgColor={item.user.avatar_bg_color || "#0F4C81"}
text={item.user.avatar_text || undefined}
username={displayName(item.user)}
size="sm"
/>
<div className="flex-1 space-y-1">
<p className="text-sm">
<Link
href={`/profiles/${item.user.username}`}
className="font-medium hover:text-primary transition-colors"
>
{displayName(item.user)}
</Link>
{item.type === "review" && (
<>
{" reviewed "}
{item.entity ? (
<Link
href={getEntityLink(item.entity) || "#"}
className="font-medium text-primary hover:underline"
>
{getEntityTypeLabel(item.entity)} {item.entity.title}
{item.entity.date && ` (${new Date(item.entity.date).toLocaleDateString()})`}
</Link>
) : (
"a performance"
)}
</>
)}
{item.type === "attendance" && (
<>
{" attended "}
{item.entity ? (
<Link
href={getEntityLink(item.entity) || "#"}
className="font-medium text-primary hover:underline"
>
{item.entity.title}
</Link>
) : (
"a show"
)}
</>
)}
{item.type === "post" && " posted in a group"}
</p>
{item.type === "review" && item.data.blurb && (
<div className="text-sm text-muted-foreground italic">
"<WikiText text={item.data.blurb} />"
</div>
)}
{item.type === "post" && (
<p className="text-sm text-muted-foreground line-clamp-2">{item.data.content}</p>
)}
<p className="text-xs text-muted-foreground">
{new Date(item.timestamp).toLocaleDateString(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric'
})}
</p>
</div>
</div>
</CardContent>
</Card>
))}
{feed.length === 0 && (
<p className="text-center text-muted-foreground">No recent activity.</p>
)}
</div>
)
}