From 41bed3583dda7ad7b9bd357fc316bf3a9fbecc6f Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Fri, 10 Jul 2026 22:32:47 +0200 Subject: [PATCH] feat: calendar diary view on profile (#15) --- spa/src/components/diary-calendar.tsx | 261 +++++++++++++++++++------- spa/src/components/profile-view.tsx | 14 +- 2 files changed, 194 insertions(+), 81 deletions(-) diff --git a/spa/src/components/diary-calendar.tsx b/spa/src/components/diary-calendar.tsx index 26066e2..eceda85 100644 --- a/spa/src/components/diary-calendar.tsx +++ b/spa/src/components/diary-calendar.tsx @@ -1,23 +1,42 @@ -import { useMemo } from "react" -import { Calendar } from "@/components/ui/calendar" +import { useMemo, useState } from "react" +import { useTranslation } from "react-i18next" +import { ChevronLeft, ChevronRight } from "lucide-react" +import { VisuallyHidden } from "radix-ui" +import { + startOfMonth, + endOfMonth, + startOfWeek, + endOfWeek, + addMonths, + subMonths, + eachDayOfInterval, + isSameMonth, + isToday, + format, +} from "date-fns" +import { Button } from "@/components/ui/button" +import { Drawer, DrawerContent, DrawerTitle } from "@/components/ui/drawer" +import { StarDisplay } from "@/components/star-display" +import { WATCH_MEDIUMS } from "@/lib/watch-mediums" import { posterUrl } from "@/lib/api/client" +import { shortDate } from "@/lib/date" import type { DiaryEntryDto } from "@/lib/api/common" -import type { DayButton } from "react-day-picker" type DiaryCalendarProps = { entries: DiaryEntryDto[] - onSelectDate?: (entries: DiaryEntryDto[]) => void + onSelectEntry?: (entry: DiaryEntryDto) => void } -function parseDate(dateStr: string): string { - return dateStr.slice(0, 10) -} +const WEEKDAYS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"] + +export function DiaryCalendar({ entries, onSelectEntry }: DiaryCalendarProps) { + const [month, setMonth] = useState(() => startOfMonth(new Date())) + const [dayEntries, setDayEntries] = useState(null) -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 key = e.review.watched_at.slice(0, 10) const list = map.get(key) if (list) list.push(e) else map.set(key, [e]) @@ -25,67 +44,173 @@ export function DiaryCalendar({ entries, onSelectDate }: DiaryCalendarProps) { return map }, [entries]) - const watchedDates = useMemo( - () => [...byDate.keys()].map((d) => new Date(d + "T00:00:00")), - [byDate], - ) + const days = useMemo(() => { + const start = startOfWeek(startOfMonth(month)) + const end = endOfWeek(endOfMonth(month)) + return eachDayOfInterval({ start, end }) + }, [month]) + + function handleDayClick(clicked: DiaryEntryDto[]) { + if (clicked.length === 1) { + onSelectEntry?.(clicked[0]!) + } else { + setDayEntries(clicked) + } + } 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 ( - + + {format(month, "MMMM yyyy")} - )} - {dayEntries && dayEntries.length === 1 && !poster && ( - - )} - + + + +
+ {WEEKDAYS.map((d) => ( +
+ {d} +
+ ))} + + {days.map((day) => { + const key = format(day, "yyyy-MM-dd") + const de = byDate.get(key) + const inMonth = isSameMonth(day, month) + const today = isToday(day) + const poster = de?.[0]?.movie.poster_path + + return ( + + ) + })} +
+ + setDayEntries(null)} + onSelect={(e) => { + setDayEntries(null) + onSelectEntry?.(e) + }} + /> + + ) +} + +function DayDrawer({ + entries, + onClose, + onSelect, +}: { + entries: DiaryEntryDto[] | null + onClose: () => void + onSelect: (entry: DiaryEntryDto) => void +}) { + const { t } = useTranslation() + if (!entries) return null + + const dateLabel = shortDate(entries[0]!.review.watched_at) + + return ( + !open && onClose()}> + + + {dateLabel} + +
+

+ {dateLabel} · {t("common.films", { count: entries.length })} +

+
+ {entries.map((e) => ( + + ))} +
+
+
+
) } diff --git a/spa/src/components/profile-view.tsx b/spa/src/components/profile-view.tsx index 8cf2f35..c283bd4 100644 --- a/spa/src/components/profile-view.tsx +++ b/spa/src/components/profile-view.tsx @@ -199,7 +199,6 @@ 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 @@ -209,10 +208,7 @@ function CalendarTab({ userId }: { userId?: string }) { <> { - if (entries.length === 1) setDetailEntry(entries[0]!) - else setDetailEntries(entries) - }} + onSelectEntry={setDetailEntry} /> {detailEntry && ( )} - {detailEntries && ( - !open && setDetailEntries(null)} - movie={detailEntries[0]!.movie} - review={detailEntries[0]!.review} - /> - )} ) }