feature/spoiler-flag #25
@@ -45,9 +45,8 @@
|
||||
{% if let Some(wm) = entry.review().watch_medium() %}
|
||||
<span class="watch-medium">{{ wm.label() }}</span>
|
||||
{% endif %}
|
||||
{% let author = ctx.is_current_user(entry.review().user_id().value()) %}
|
||||
{% if let Some(comment) = entry.review().comment() %}
|
||||
{% if !author && entry.review().spoilers() %}
|
||||
{% if entry.review().spoilers() %}
|
||||
<details class="comment spoiler">
|
||||
<summary style="cursor:pointer;opacity:.75">Contains spoilers</summary>
|
||||
{{ comment.value() }}
|
||||
|
||||
@@ -129,9 +129,8 @@
|
||||
<span class="star {% if filled %}filled{% else %}empty{% endif %}">★</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% let author = ctx.is_current_user(entry.review().user_id().value()) %}
|
||||
{% if let Some(comment) = entry.review().comment() %}
|
||||
{% if !author && entry.review().spoilers() %}
|
||||
{% if entry.review().spoilers() %}
|
||||
<details class="comment spoiler">
|
||||
<summary style="cursor:pointer;opacity:.75">Contains spoilers</summary>
|
||||
{{ comment.value() }}
|
||||
|
||||
@@ -219,9 +219,8 @@
|
||||
<span class="star {% if filled %}filled{% else %}empty{% endif %}">★</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% let author = ctx.is_current_user(entry.review().user_id().value()) %}
|
||||
{% if let Some(comment) = entry.review().comment() %}
|
||||
{% if !author && entry.review().spoilers() %}
|
||||
{% if entry.review().spoilers() %}
|
||||
<details class="comment spoiler">
|
||||
<summary style="cursor:pointer;opacity:.75">Contains spoilers</summary>
|
||||
{{ comment.value() }}
|
||||
|
||||
@@ -10,12 +10,10 @@ import type { SocialReviewDto } from "@/features/movies"
|
||||
|
||||
type CommunityReviewsProps = {
|
||||
reviews: { items: SocialReviewDto[] }
|
||||
/** Authenticated viewer id — own reviews are never veiled. */
|
||||
currentUserId?: string
|
||||
onShowDetail?: (review: SocialReviewDto) => void
|
||||
}
|
||||
|
||||
export function CommunityReviews({ reviews, currentUserId, onShowDetail }: CommunityReviewsProps) {
|
||||
export function CommunityReviews({ reviews, onShowDetail }: CommunityReviewsProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
@@ -26,7 +24,7 @@ export function CommunityReviews({ reviews, currentUserId, onShowDetail }: Commu
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{reviews.items.map((r, i) => {
|
||||
const veiled = !!r.spoilers && currentUserId !== r.user_id
|
||||
const veiled = !!r.spoilers
|
||||
return (
|
||||
<Card key={i} size="sm">
|
||||
<CardHeader>
|
||||
|
||||
@@ -94,7 +94,6 @@ export function FeedTab() {
|
||||
onLoadMore={loadMore}
|
||||
renderItem={(entry) => {
|
||||
const isOwn = entry.user_id === auth?.user_id
|
||||
const veil = !isOwn && !!entry.review.spoilers
|
||||
const card = (
|
||||
<ReviewCard
|
||||
movie={entry.movie}
|
||||
@@ -103,7 +102,6 @@ export function FeedTab() {
|
||||
userId={entry.user_id}
|
||||
isFederated={entry.is_federated}
|
||||
actorUrl={entry.actor_url}
|
||||
veil={veil}
|
||||
onEdit={isOwn ? () => setEditingEntry(entry) : undefined}
|
||||
onShowDetail={entry.review.comment ? () => setDetailEntry(entry) : undefined}
|
||||
/>
|
||||
@@ -141,7 +139,6 @@ export function FeedTab() {
|
||||
movie={detailEntry.movie}
|
||||
review={detailEntry.review}
|
||||
userName={detailEntry.user_display_name}
|
||||
veil={detailEntry.user_id !== auth?.user_id}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -18,7 +18,6 @@ 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 { useAuth } from "@/components/auth-provider"
|
||||
import type { DiaryEntryDto } from "@/lib/api/common"
|
||||
import { tmdbProfileUrl } from "@/lib/api/client"
|
||||
import type { UserProfileResponse } from "@/features/users"
|
||||
@@ -151,7 +150,6 @@ function StatCell({ label, value }: { label: string; value: string | number }) {
|
||||
|
||||
function DiaryTab({ sortBy, userId, search }: { sortBy: string; userId?: string; search?: string }) {
|
||||
const { t } = useTranslation()
|
||||
const { auth } = useAuth()
|
||||
const { data, isPending, hasNextPage, isFetchingNextPage, fetchNextPage } =
|
||||
useInfiniteDiary({ sort_by: sortBy, user_id: userId })
|
||||
const items = data?.pages.flatMap((p) => p.items) ?? []
|
||||
@@ -181,7 +179,7 @@ function DiaryTab({ sortBy, userId, search }: { sortBy: string; userId?: string;
|
||||
comment={e.review.comment}
|
||||
subtitle={<><TimeAgo date={e.review.watched_at} /></>}
|
||||
variant="compact"
|
||||
spoilers={userId && userId === auth?.user_id ? undefined : e.review.spoilers}
|
||||
spoilers={e.review.spoilers}
|
||||
onShowDetail={e.review.comment ? () => setDetailEntry(e) : undefined}
|
||||
/>
|
||||
)}
|
||||
@@ -192,7 +190,6 @@ function DiaryTab({ sortBy, userId, search }: { sortBy: string; userId?: string;
|
||||
onOpenChange={(open) => !open && setDetailEntry(null)}
|
||||
movie={detailEntry.movie}
|
||||
review={detailEntry.review}
|
||||
veil={!!userId && userId !== auth?.user_id}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -17,13 +17,12 @@ type ReviewCardProps = {
|
||||
userId?: string
|
||||
isFederated?: boolean
|
||||
actorUrl?: string
|
||||
/** Blur the comment until revealed — set for every viewer except the author. */
|
||||
veil?: boolean
|
||||
onEdit?: () => void
|
||||
onShowDetail?: () => void
|
||||
}
|
||||
|
||||
export function ReviewCard({ movie, review, userName, userId, isFederated, actorUrl, veil, onEdit, onShowDetail }: ReviewCardProps) {
|
||||
export function ReviewCard({ movie, review, userName, userId, isFederated, actorUrl, onEdit, onShowDetail }: ReviewCardProps) {
|
||||
const veil = !!review.spoilers
|
||||
const showDetail = veil ? undefined : onShowDetail
|
||||
const card = (
|
||||
<Card size="sm">
|
||||
|
||||
@@ -13,11 +13,9 @@ type ReviewDetailSheetProps = {
|
||||
movie: MovieDto
|
||||
review: ReviewDto
|
||||
userName?: string
|
||||
/** Blur the comment until revealed — set for every viewer except the author. */
|
||||
veil?: boolean
|
||||
}
|
||||
|
||||
export function ReviewDetailSheet({ open, onOpenChange, movie, review, userName, veil }: ReviewDetailSheetProps) {
|
||||
export function ReviewDetailSheet({ open, onOpenChange, movie, review, userName }: ReviewDetailSheetProps) {
|
||||
return (
|
||||
<Drawer open={open} onOpenChange={onOpenChange}>
|
||||
<DrawerContent className="mx-auto max-w-lg">
|
||||
@@ -43,7 +41,7 @@ export function ReviewDetailSheet({ open, onOpenChange, movie, review, userName,
|
||||
</div>
|
||||
|
||||
{review.comment && (
|
||||
veil && review.spoilers ? (
|
||||
review.spoilers ? (
|
||||
<SpoilerVeil content={review.comment} className="text-sm leading-relaxed" />
|
||||
) : (
|
||||
<p className="select-text whitespace-pre-wrap text-sm leading-relaxed">
|
||||
|
||||
@@ -10,8 +10,8 @@ type SpoilerVeilProps = {
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides review text behind a blur with a reveal button — shown to everyone
|
||||
* except the review's author. Once revealed, the text stays visible.
|
||||
* Hides review text behind a blur with a reveal button — shown to every
|
||||
* viewer, the author included. Once revealed, the text stays visible.
|
||||
*/
|
||||
export function SpoilerVeil({ content, className }: SpoilerVeilProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { TrendingUp } from "lucide-react"
|
||||
import { StarDisplay } from "@/components/star-display"
|
||||
import { SpoilerVeil } from "@/components/spoiler-veil"
|
||||
import { shortDate } from "@/lib/date"
|
||||
import type { ReviewHistoryResponse } from "@/features/movies"
|
||||
|
||||
@@ -24,7 +25,11 @@ export function ViewingHistory({ history }: { history: ReviewHistoryResponse })
|
||||
<div>
|
||||
<p className="text-sm font-medium">{shortDate(v.watched_at)}</p>
|
||||
{v.comment && (
|
||||
<p className="mt-0.5 text-xs text-muted-foreground line-clamp-1">{v.comment}</p>
|
||||
v.spoilers ? (
|
||||
<SpoilerVeil content={v.comment} className="mt-0.5 text-xs text-muted-foreground line-clamp-1" />
|
||||
) : (
|
||||
<p className="mt-0.5 text-xs text-muted-foreground line-clamp-1">{v.comment}</p>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<StarDisplay rating={v.rating} size="xs" />
|
||||
|
||||
@@ -12,7 +12,6 @@ import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { posterUrl, tmdbProfileUrl } from "@/lib/api/client"
|
||||
import { useAuth } from "@/components/auth-provider"
|
||||
import { useMovie, useMovieHistory, useMovieProfile } from "@/features/movies"
|
||||
import { useDocumentTitle } from "@/hooks/use-document-title"
|
||||
import {
|
||||
@@ -30,7 +29,6 @@ function MovieDetailPage() {
|
||||
const { t } = useTranslation()
|
||||
const { id } = Route.useParams()
|
||||
const { data, isPending } = useMovie(id)
|
||||
const { auth } = useAuth()
|
||||
const { data: profile } = useMovieProfile(id)
|
||||
const { data: history } = useMovieHistory(id)
|
||||
useDocumentTitle(data?.movie.title)
|
||||
@@ -109,7 +107,6 @@ function MovieDetailPage() {
|
||||
|
||||
<CommunityReviews
|
||||
reviews={reviews}
|
||||
currentUserId={auth?.user_id}
|
||||
onShowDetail={setDetailReview}
|
||||
/>
|
||||
|
||||
@@ -122,7 +119,6 @@ function MovieDetailPage() {
|
||||
movie={movie}
|
||||
review={{ id: "", rating: detailReview.rating, comment: detailReview.comment, watched_at: detailReview.watched_at, watch_medium: detailReview.watch_medium, spoilers: detailReview.spoilers }}
|
||||
userName={detailReview.user_display}
|
||||
veil={detailReview.user_id !== auth?.user_id}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user