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