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 (
)
}
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) => (
))}
)
}