diff --git a/spa/src/components/diary-calendar.tsx b/spa/src/components/diary-calendar.tsx new file mode 100644 index 0000000..26066e2 --- /dev/null +++ b/spa/src/components/diary-calendar.tsx @@ -0,0 +1,91 @@ +import { useMemo } from "react" +import { Calendar } from "@/components/ui/calendar" +import { posterUrl } from "@/lib/api/client" +import type { DiaryEntryDto } from "@/lib/api/common" +import type { DayButton } from "react-day-picker" + +type DiaryCalendarProps = { + entries: DiaryEntryDto[] + onSelectDate?: (entries: DiaryEntryDto[]) => void +} + +function parseDate(dateStr: string): string { + return dateStr.slice(0, 10) +} + +export function DiaryCalendar({ entries, onSelectDate }: DiaryCalendarProps) { + const byDate = useMemo(() => { + const map = new Map() + for (const e of entries) { + const key = parseDate(e.review.watched_at) + const list = map.get(key) + if (list) list.push(e) + else map.set(key, [e]) + } + return map + }, [entries]) + + const watchedDates = useMemo( + () => [...byDate.keys()].map((d) => new Date(d + "T00:00:00")), + [byDate], + ) + + return ( + { + const key = day.toISOString().slice(0, 10) + const dayEntries = byDate.get(key) + if (dayEntries?.length && onSelectDate) onSelectDate(dayEntries) + }} + components={{ + DayButton: (props) => ( + + ), + }} + /> + ) +} + +function CalendarDay({ + day, + byDate, + ...props +}: React.ComponentProps & { + byDate: Map +}) { + const key = day.date.toISOString().slice(0, 10) + const dayEntries = byDate.get(key) + const poster = dayEntries?.[0]?.movie.poster_path + + return ( + + ) +} diff --git a/spa/src/components/profile-view.tsx b/spa/src/components/profile-view.tsx index ba99172..8cf2f35 100644 --- a/spa/src/components/profile-view.tsx +++ b/spa/src/components/profile-view.tsx @@ -17,6 +17,7 @@ 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 type { UserProfileResponse } from "@/features/users" @@ -51,6 +52,7 @@ export function ProfileView({ 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 @@ -128,6 +130,7 @@ export function ProfileView({ search={search} /> )} + {tab === "calendar" && } {tab === "trends" && } )} @@ -192,6 +195,45 @@ function DiaryTab({ sortBy, userId, search }: { sortBy: string; userId?: string; ) } +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 [detailEntries, setDetailEntries] = useState(null) + const [detailEntry, setDetailEntry] = useState(null) + + if (isPending) return + if (hasNextPage) fetchNextPage() + + return ( + <> + { + if (entries.length === 1) setDetailEntry(entries[0]!) + else setDetailEntries(entries) + }} + /> + {detailEntry && ( + !open && setDetailEntry(null)} + movie={detailEntry.movie} + review={detailEntry.review} + /> + )} + {detailEntries && ( + !open && setDetailEntries(null)} + movie={detailEntries[0]!.movie} + review={detailEntries[0]!.review} + /> + )} + + ) +} + const trendChartConfig = { count: { label: "Movies", color: "var(--primary)" }, } satisfies ChartConfig