import { Link } from "@tanstack/react-router" import { useCallback, useState } from "react" import { useTranslation } from "react-i18next" import { Bar, BarChart, XAxis, YAxis } from "recharts" import { Globe, Search, User } from "lucide-react" import { ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig } from "@/components/ui/chart" import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Skeleton } from "@/components/ui/skeleton" import { Input } from "@/components/ui/input" import { MovieCard } from "@/components/movie-card" import { RatingHistogram } from "@/components/rating-histogram" import { EmptyState } from "@/components/empty-state" import { SwipeTabs } from "@/components/swipe-tabs" import { VirtualList } from "@/components/virtual-list" import { useInfiniteDiary } from "@/features/diary" import { TimeAgo } from "@/components/time-ago" import { WATCH_MEDIUMS } from "@/lib/watch-mediums" import { ReviewDetailSheet } from "@/components/review-detail-sheet" import { DiaryCalendar } from "@/components/diary-calendar" import type { DiaryEntryDto } from "@/lib/api/common" import { tmdbProfileUrl } from "@/lib/api/client" import type { UserProfileResponse } from "@/features/users" type ProfileViewProps = { data: UserProfileResponse actions?: React.ReactNode headerRight?: React.ReactNode userId?: string search?: string onSearchChange?: (value: string) => void isFederated?: boolean bio?: string handle?: string actorUrl?: string } export function ProfileView({ data, actions, headerRight, userId, search, onSearchChange, bio, handle, }: ProfileViewProps) { const { t } = useTranslation() const initial = (data.username || "?")[0]?.toUpperCase() ?? "?" const avatar = data.avatar_url const profileTabs = [ { value: "recent", label: t("profile.recent") }, { value: "top_rated", label: t("profile.topRated") }, { value: "calendar", label: t("profile.calendar", { defaultValue: "Calendar" }) }, { value: "trends", label: t("profile.trends") }, ] as const return (
{avatar && } {initial}

{data.username}

{handle && (
{handle}
)} {bio &&

{bio}

}
{headerRight}
{onSearchChange && (
onSearchChange(e.target.value)} className="pl-9" />
)} {actions} {(tab) => ( <> {tab === "recent" && ( )} {tab === "top_rated" && ( )} {tab === "calendar" && } {tab === "trends" && } )}
) } function StatCell({ label, value }: { label: string; value: string | number }) { return (

{value}

{label}

) } function DiaryTab({ sortBy, userId, search }: { sortBy: string; userId?: string; search?: string }) { const { t } = useTranslation() const { data, isPending, hasNextPage, isFetchingNextPage, fetchNextPage } = useInfiniteDiary({ sort_by: sortBy, user_id: userId }) const items = data?.pages.flatMap((p) => p.items) ?? [] const filtered = search ? items.filter((e) => e.movie.title.toLowerCase().includes(search.toLowerCase()) ) : items const loadMore = useCallback(() => fetchNextPage(), [fetchNextPage]) const [detailEntry, setDetailEntry] = useState(null) if (isPending) return if (!filtered.length) return return ( <> ( } variant="compact" onShowDetail={e.review.comment ? () => setDetailEntry(e) : undefined} /> )} /> {detailEntry && ( !open && setDetailEntry(null)} movie={detailEntry.movie} review={detailEntry.review} /> )} ) } function CalendarTab({ userId }: { userId?: string }) { const { data, isPending, hasNextPage, fetchNextPage } = useInfiniteDiary({ sort_by: "date_desc", user_id: userId }) const items = data?.pages.flatMap((p) => p.items) ?? [] const [detailEntry, setDetailEntry] = useState(null) if (isPending) return if (hasNextPage) fetchNextPage() return ( <> {detailEntry && ( !open && setDetailEntry(null)} movie={detailEntry.movie} review={detailEntry.review} /> )} ) } const trendChartConfig = { count: { label: "Movies", color: "var(--primary)" }, } satisfies ChartConfig function TrendsView({ data, }: { data: { trends?: { top_directors: { director: string; count: number }[] top_actors?: { name: string; profile_path?: string; movie_count: number; score: number }[] monthly_ratings: { month_label: string avg_rating: number count: number }[] top_genres: { genre: string; count: number }[] rating_distribution: number[] watch_medium_distribution: { medium: string; count: number }[] } } }) { const { t } = useTranslation() if (!data.trends) return return (
{data.trends.top_directors.some((d) => d.count >= 2) && ( {t("profile.topDirectors")} {data.trends.top_directors.map((d) => (
{d.director} {t("common.films", { count: d.count })}
))}
)} {data.trends.top_actors && data.trends.top_actors.length > 0 && ( {t("profile.topActors")} {data.trends.top_actors.map((a) => (
{a.profile_path && ( )} {a.name[0]} {a.name} {t("common.films", { count: a.movie_count })}
))}
)} {data.trends.top_genres.length > 0 && ( {t("profile.topGenres", { defaultValue: "Top Genres" })} {data.trends.top_genres.map((g) => (
{g.genre} {t("common.films", { count: g.count })}
))}
)} {data.trends.rating_distribution.length > 0 && ( {t("profile.ratingDistribution", { defaultValue: "Rating Distribution" })} )} {data.trends.watch_medium_distribution.length > 0 && ( {t("profile.howYouWatch", { defaultValue: "How You Watch" })} {data.trends.watch_medium_distribution.map((wm) => { const def = WATCH_MEDIUMS.find((d) => d.value === wm.medium) const Icon = def?.icon return (
{Icon && } {def ? t(def.labelKey) : wm.medium} {t("common.films", { count: wm.count })}
) })}
)} {data.trends.monthly_ratings.length > 0 && ( {t("profile.monthlyActivity")} v.slice(0, 3)} tick={{ fontSize: 10, fill: "rgba(255,255,255,0.85)" }} tickLine={false} axisLine={false} /> } /> )}
) } export function ProfileSkeleton() { return (
{[1, 2, 3, 4].map((i) => ( ))}
{[1, 2, 3].map((i) => ( ))}
) }