diff --git a/spa/src/components/community-reviews.tsx b/spa/src/components/community-reviews.tsx index 54eb4d3..9e04e8e 100644 --- a/spa/src/components/community-reviews.tsx +++ b/spa/src/components/community-reviews.tsx @@ -7,7 +7,7 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/com import { timeAgo } from "@/lib/date" import type { SocialReviewDto } from "@/features/movies" -export function CommunityReviews({ reviews }: { reviews: { items: SocialReviewDto[] } }) { +export function CommunityReviews({ reviews, onShowDetail }: { reviews: { items: SocialReviewDto[] }; onShowDetail?: (review: SocialReviewDto) => void }) { const { t } = useTranslation() return ( @@ -36,7 +36,13 @@ export function CommunityReviews({ reviews }: { reviews: { items: SocialReviewDt {r.comment && ( -

{r.comment}

+

onShowDetail(r) : undefined} + onKeyDown={onShowDetail ? (e) => e.key === "Enter" && onShowDetail(r) : undefined} + >{r.comment}

)} diff --git a/spa/src/components/movie-card.tsx b/spa/src/components/movie-card.tsx index 605dfcb..e2d9849 100644 --- a/spa/src/components/movie-card.tsx +++ b/spa/src/components/movie-card.tsx @@ -11,9 +11,10 @@ type MovieCardProps = { subtitle?: React.ReactNode variant?: "compact" | "full" action?: React.ReactNode + onShowDetail?: () => void } -export function MovieCard({ movie, rating, comment, subtitle, variant = "full", action }: MovieCardProps) { +export function MovieCard({ movie, rating, comment, subtitle, variant = "full", action, onShowDetail }: MovieCardProps) { if (variant === "compact") { return ( @@ -23,7 +24,15 @@ export function MovieCard({ movie, rating, comment, subtitle, variant = "full",

{movie.title}

{subtitle &&

{subtitle}

} - {comment &&

{comment}

} + {comment && ( +

{ e.preventDefault(); onShowDetail() } : undefined} + onKeyDown={onShowDetail ? (e) => e.key === "Enter" && onShowDetail() : undefined} + >{comment}

+ )}
{rating != null && } @@ -41,7 +50,15 @@ export function MovieCard({ movie, rating, comment, subtitle, variant = "full",

{movie.title}

{movie.release_year}{movie.director && ` ยท ${movie.director}`}

{rating != null &&
} - {comment &&

{comment}

} + {comment && ( +

{ e.preventDefault(); onShowDetail() } : undefined} + onKeyDown={onShowDetail ? (e) => e.key === "Enter" && onShowDetail() : undefined} + >{comment}

+ )} {action &&
e.preventDefault()}>{action}
} diff --git a/spa/src/components/profile-view.tsx b/spa/src/components/profile-view.tsx index 57c169f..ba99172 100644 --- a/spa/src/components/profile-view.tsx +++ b/spa/src/components/profile-view.tsx @@ -1,5 +1,5 @@ import { Link } from "@tanstack/react-router" -import { useCallback } from "react" +import { useCallback, useState } from "react" import { useTranslation } from "react-i18next" import { Bar, BarChart, XAxis, YAxis } from "recharts" import { Globe, Search, User } from "lucide-react" @@ -16,6 +16,8 @@ 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 type { DiaryEntryDto } from "@/lib/api/common" import type { UserProfileResponse } from "@/features/users" type ProfileViewProps = { @@ -154,27 +156,39 @@ function DiaryTab({ sortBy, userId, search }: { sortBy: string; userId?: string; ) : items const loadMore = useCallback(() => fetchNextPage(), [fetchNextPage]) + const [detailEntry, setDetailEntry] = useState(null) if (isPending) return if (!filtered.length) return return ( - ( - } - variant="compact" + <> + ( + } + variant="compact" + onShowDetail={e.review.comment ? () => setDetailEntry(e) : undefined} + /> + )} + /> + {detailEntry && ( + !open && setDetailEntry(null)} + movie={detailEntry.movie} + review={detailEntry.review} /> )} - /> + ) } diff --git a/spa/src/routes/_app/diary.tsx b/spa/src/routes/_app/diary.tsx index b2033f4..7685865 100644 --- a/spa/src/routes/_app/diary.tsx +++ b/spa/src/routes/_app/diary.tsx @@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next" import { BookOpen, ChevronLeft, ChevronRight, Pencil } from "lucide-react" import { format, startOfMonth, subMonths } from "date-fns" import { ReviewSheet } from "@/components/review-sheet" +import { ReviewDetailSheet } from "@/components/review-detail-sheet" import { EditableContextMenu } from "@/components/editable-context-menu" import { MovieCard } from "@/components/movie-card" import { EmptyState } from "@/components/empty-state" @@ -39,6 +40,7 @@ function DiaryPage() { useInfiniteDiary({ sort_by: "desc", user_id: auth?.user_id }) const deleteReview = useDeleteReview() const [editingEntry, setEditingEntry] = useState(null) + const [detailEntry, setDetailEntry] = useState(null) const monthLabel = format(month, "MMMM yyyy") const monthStr = format(month, "yyyy-MM") @@ -121,6 +123,7 @@ function DiaryPage() { rating={item.entry.review.rating} comment={item.entry.review.comment} variant="full" + onShowDetail={item.entry.review.comment ? () => setDetailEntry(item.entry) : undefined} action={
{item.entry.review.watch_medium && ( @@ -154,6 +157,15 @@ function DiaryPage() { review={editingEntry.review} /> )} + + {detailEntry && ( + !open && setDetailEntry(null)} + movie={detailEntry.movie} + review={detailEntry.review} + /> + )}
) } diff --git a/spa/src/routes/_app/movies.$id.tsx b/spa/src/routes/_app/movies.$id.tsx index 302b907..4948875 100644 --- a/spa/src/routes/_app/movies.$id.tsx +++ b/spa/src/routes/_app/movies.$id.tsx @@ -1,8 +1,10 @@ import { createFileRoute, Link } from "@tanstack/react-router" +import { useState } from "react" import { useTranslation } from "react-i18next" import { Bookmark, BookmarkCheck, Star, User } from "lucide-react" import { BackButton } from "@/components/back-button" import { CommunityReviews } from "@/components/community-reviews" +import { ReviewDetailSheet } from "@/components/review-detail-sheet" import { ViewingHistory } from "@/components/viewing-history" import { RatingHistogram } from "@/components/rating-histogram" import { HorizontalStrip } from "@/components/horizontal-strip" @@ -17,7 +19,7 @@ import { useAddToWatchlist, useRemoveFromWatchlist, } from "@/features/watchlist" -import type { CastMemberDto, CrewMemberDto } from "@/features/movies" +import type { CastMemberDto, CrewMemberDto, SocialReviewDto } from "@/features/movies" export const Route = createFileRoute("/_app/movies/$id")({ component: MovieDetailPage, @@ -30,6 +32,7 @@ function MovieDetailPage() { const { data: profile } = useMovieProfile(id) const { data: history } = useMovieHistory(id) useDocumentTitle(data?.movie.title) + const [detailReview, setDetailReview] = useState(null) if (isPending) return if (!data) return null @@ -102,9 +105,19 @@ function MovieDetailPage() { )} - + {history && } + + {detailReview && ( + !open && setDetailReview(null)} + movie={movie} + review={{ id: "", rating: detailReview.rating, comment: detailReview.comment, watched_at: detailReview.watched_at, watch_medium: detailReview.watch_medium }} + userName={detailReview.user_display} + /> + )} ) } diff --git a/spa/src/routes/_app/settings/wrapup.tsx b/spa/src/routes/_app/settings/wrapup.tsx index e70a2ce..32d8320 100644 --- a/spa/src/routes/_app/settings/wrapup.tsx +++ b/spa/src/routes/_app/settings/wrapup.tsx @@ -2,6 +2,7 @@ import { createFileRoute, Link } from "@tanstack/react-router" import { useState } from "react" import { useTranslation } from "react-i18next" import { ArrowLeft, ChevronRight, Sparkles, Trash2 } from "lucide-react" +import { format, subMonths, startOfYear, endOfYear } from "date-fns" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Card, CardContent } from "@/components/ui/card" @@ -125,6 +126,7 @@ function WrapupPage() { {t("wrapup.generateWrapUp")}
+ { setStartDate(s); setEndDate(e) }} />
) } + +function PeriodPresets({ onSelect }: { onSelect: (start: string, end: string) => void }) { + const { t } = useTranslation() + const now = new Date() + const fmt = (d: Date) => format(d, "yyyy-MM-dd") + + const currentYear = now.getFullYear() + const presets = [ + { label: String(currentYear), start: fmt(startOfYear(now)), end: fmt(endOfYear(now)) }, + { label: String(currentYear - 1), start: fmt(startOfYear(new Date(currentYear - 1, 0))), end: fmt(endOfYear(new Date(currentYear - 1, 0))) }, + { label: t("wrapup.last12Months", { defaultValue: "Last 12 months" }), start: fmt(subMonths(now, 12)), end: fmt(now) }, + { label: t("wrapup.last6Months", { defaultValue: "Last 6 months" }), start: fmt(subMonths(now, 6)), end: fmt(now) }, + ] + + return ( +
+ {presets.map((p) => ( + + ))} +
+ ) +}