161 lines
6.8 KiB
TypeScript
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>
|
|
)
|
|
}
|