feat: auto-grow comment textarea, review detail sheet for full text

- textarea grows as you type instead of fixed 3 rows
- tap truncated comment to open detail sheet with full selectable text
- review detail sheet shows poster, rating, medium, date, full comment
This commit is contained in:
2026-07-10 00:33:49 +02:00
parent 306f4489fd
commit 925f74bb3d
4 changed files with 114 additions and 3 deletions

View File

@@ -17,9 +17,10 @@ type ReviewCardProps = {
isFederated?: boolean
actorUrl?: string
onEdit?: () => void
onShowDetail?: () => void
}
export function ReviewCard({ movie, review, userName, userId, isFederated, actorUrl, onEdit }: ReviewCardProps) {
export function ReviewCard({ movie, review, userName, userId, isFederated, actorUrl, onEdit, onShowDetail }: ReviewCardProps) {
const card = (
<Card size="sm">
<CardContent className="flex gap-3">
@@ -59,7 +60,17 @@ export function ReviewCard({ movie, review, userName, userId, isFederated, actor
<StarDisplay rating={review.rating} />
{review.watch_medium && <WatchMediumBadge medium={review.watch_medium} />}
</div>
{review.comment && <p className="mt-1 line-clamp-2 text-xs text-muted-foreground">{review.comment}</p>}
{review.comment && (
<p
className="mt-1 line-clamp-2 text-xs text-muted-foreground"
role={onShowDetail ? "button" : undefined}
tabIndex={onShowDetail ? 0 : undefined}
onClick={onShowDetail}
onKeyDown={onShowDetail ? (e) => e.key === "Enter" && onShowDetail() : undefined}
>
{review.comment}
</p>
)}
</div>
</CardContent>
</Card>

View File

@@ -0,0 +1,51 @@
import { VisuallyHidden } from "radix-ui"
import { Drawer, DrawerContent, DrawerTitle } from "@/components/ui/drawer"
import { StarDisplay } from "@/components/star-display"
import { WatchMediumBadge } from "@/components/watch-medium-badge"
import { shortDate } from "@/lib/date"
import { posterUrl } from "@/lib/api/client"
import type { MovieDto, ReviewDto } from "@/lib/api/common"
type ReviewDetailSheetProps = {
open: boolean
onOpenChange: (open: boolean) => void
movie: MovieDto
review: ReviewDto
userName?: string
}
export function ReviewDetailSheet({ open, onOpenChange, movie, review, userName }: ReviewDetailSheetProps) {
return (
<Drawer open={open} onOpenChange={onOpenChange}>
<DrawerContent className="mx-auto max-w-lg">
<VisuallyHidden.Root><DrawerTitle>{movie.title}</DrawerTitle></VisuallyHidden.Root>
<div className="p-5 pb-8">
<div className="mb-4 flex gap-3">
<div className="h-24 w-16 flex-shrink-0 overflow-hidden rounded-lg bg-muted">
{movie.poster_path && <img src={posterUrl(movie.poster_path)} alt="" className="size-full object-cover" />}
</div>
<div>
<p className="text-lg font-bold">{movie.title}</p>
<p className="text-sm text-muted-foreground">
{movie.release_year}{movie.director && ` · ${movie.director}`}
</p>
{userName && <p className="mt-1 text-xs text-muted-foreground">{userName}</p>}
</div>
</div>
<div className="mb-4 flex items-center gap-2">
<StarDisplay rating={review.rating} />
{review.watch_medium && <WatchMediumBadge medium={review.watch_medium} />}
<span className="text-xs text-muted-foreground">{shortDate(review.watched_at)}</span>
</div>
{review.comment && (
<p className="select-text whitespace-pre-wrap text-sm leading-relaxed">
{review.comment}
</p>
)}
</div>
</DrawerContent>
</Drawer>
)
}

View File

@@ -1,3 +1,4 @@
import { useCallback, useRef } from "react"
import { useTranslation } from "react-i18next"
import { CalendarIcon } from "lucide-react"
import { format } from "date-fns"
@@ -38,7 +39,7 @@ export function ReviewFormFields({
<div className="flex justify-center"><StarRating value={rating} onChange={onRatingChange} /></div>
</div>
<Textarea value={comment} onChange={(e) => onCommentChange(e.target.value)} placeholder={t("logReview.commentPlaceholder")} className="mb-5" rows={3} />
<AutoGrowTextarea value={comment} onChange={onCommentChange} placeholder={t("logReview.commentPlaceholder")} className="mb-5" />
<div className="mb-5">
<p className="mb-2 text-xs uppercase tracking-wide text-muted-foreground">{t("logReview.watchedAt")}</p>
@@ -68,3 +69,38 @@ export function ReviewFormFields({
</>
)
}
function AutoGrowTextarea({
value,
onChange,
placeholder,
className,
}: {
value: string
onChange: (v: string) => void
placeholder?: string
className?: string
}) {
const ref = useRef<HTMLTextAreaElement>(null)
const handleInput = useCallback(() => {
const el = ref.current
if (!el) return
el.style.height = "auto"
el.style.height = `${el.scrollHeight}px`
}, [])
return (
<Textarea
ref={ref}
value={value}
onChange={(e) => {
onChange(e.target.value)
handleInput()
}}
placeholder={placeholder}
className={className}
rows={2}
style={{ overflow: "hidden", resize: "none" }}
/>
)
}

View File

@@ -16,6 +16,7 @@ import { StarRating } from "@/components/star-rating"
import { useAuth } from "@/components/auth-provider"
import { useQueryClient } from "@tanstack/react-query"
import { EditReviewSheet } from "@/components/edit-review-sheet"
import { ReviewDetailSheet } from "@/components/review-detail-sheet"
import { useInfiniteActivityFeed, useDeleteReview } from "@/hooks/use-diary"
import type { FeedEntryDto } from "@/lib/api/diary"
import { SearchOverlay } from "@/components/search-overlay"
@@ -69,6 +70,7 @@ function FeedTab() {
useInfiniteActivityFeed({ sort_by: sortBy })
const deleteReview = useDeleteReview()
const [editingEntry, setEditingEntry] = useState<FeedEntryDto | null>(null)
const [detailEntry, setDetailEntry] = useState<FeedEntryDto | null>(null)
const items = data?.pages.flatMap((p) => p.items) ?? []
const loadMore = useCallback(() => fetchNextPage(), [fetchNextPage])
@@ -123,6 +125,7 @@ function FeedTab() {
isFederated={entry.is_federated}
actorUrl={entry.actor_url}
onEdit={isOwn ? () => setEditingEntry(entry) : undefined}
onShowDetail={entry.review.comment ? () => setDetailEntry(entry) : undefined}
/>
)
return isOwn ? (
@@ -149,6 +152,16 @@ function FeedTab() {
review={editingEntry.review}
/>
)}
{detailEntry && (
<ReviewDetailSheet
open={!!detailEntry}
onOpenChange={(open) => !open && setDetailEntry(null)}
movie={detailEntry.movie}
review={detailEntry.review}
userName={detailEntry.user_display_name}
/>
)}
</div>
)
}