fix: disable vaul repositionInputs to fix iOS keyboard in drawers
Some checks failed
CI / Check / Test (push) Failing after 6m35s

This commit is contained in:
2026-06-04 15:11:16 +02:00
parent dacc057af6
commit 4a3a99c6d2
17 changed files with 123 additions and 29 deletions

View File

@@ -0,0 +1,17 @@
import { useRouter } from "@tanstack/react-router"
import { useTranslation } from "react-i18next"
import { ArrowLeft } from "lucide-react"
export function BackButton() {
const { t } = useTranslation()
const router = useRouter()
return (
<button
onClick={() => router.history.back()}
className="inline-flex items-center gap-1 text-sm text-muted-foreground"
>
<ArrowLeft className="size-4" /> {t("common.back")}
</button>
)
}

View File

@@ -10,6 +10,7 @@ import type { MovieSelection } from "@/components/search-overlay"
import { useLogReview } from "@/hooks/use-diary"
import { toast } from "sonner"
import { posterUrl } from "@/lib/api/client"
import { hapticMedium } from "@/lib/haptics"
type LogSheetProps = {
open: boolean
@@ -48,6 +49,7 @@ export function LogSheet({ open, onOpenChange }: LogSheetProps) {
},
{
onSuccess: () => {
hapticMedium()
toast.success(t("logReview.logged", { title: movie.title }))
handleClose()
},

View File

@@ -10,9 +10,10 @@ type MovieCardProps = {
comment?: string
subtitle?: string
variant?: "compact" | "full"
action?: React.ReactNode
}
export function MovieCard({ movie, rating, comment, subtitle, variant = "full" }: MovieCardProps) {
export function MovieCard({ movie, rating, comment, subtitle, variant = "full", action }: MovieCardProps) {
if (variant === "compact") {
return (
<Link to="/movies/$id" params={{ id: movie.id }} className="glass flex items-center gap-3 rounded-xl px-3 py-2.5 transition-colors active:bg-muted/50">
@@ -41,6 +42,7 @@ export function MovieCard({ movie, rating, comment, subtitle, variant = "full" }
{rating != null && <div className="mt-1"><StarDisplay rating={rating} /></div>}
{comment && <p className="mt-1 line-clamp-2 text-xs text-muted-foreground">{comment}</p>}
</div>
{action && <div className="flex items-center" onClick={(e) => e.preventDefault()}>{action}</div>}
</CardContent>
</Card>
</Link>

View File

@@ -1,5 +1,6 @@
import { Link } from "@tanstack/react-router"
import { Globe } from "lucide-react"
import { timeAgo } from "@/lib/date"
import { StarDisplay } from "@/components/star-display"
import { Card, CardContent } from "@/components/ui/card"
import { posterUrl } from "@/lib/api/client"
@@ -32,7 +33,7 @@ export function ReviewCard({ movie, review, userName, userId, isFederated }: Rev
)}
{isFederated && <Globe className="size-3 text-muted-foreground/60" />}
<span>·</span>
<span>{review.watched_at.slice(0, 10)}</span>
<span>{timeAgo(review.watched_at)}</span>
</div>
)}
<Link to="/movies/$id" params={{ id: movie.id }} className="font-semibold hover:underline">

View File

@@ -1,5 +1,6 @@
import { Star } from "lucide-react"
import { cn } from "@/lib/utils"
import { hapticLight } from "@/lib/haptics"
type StarRatingProps = {
value: number
@@ -16,7 +17,7 @@ export function StarRating({ value, onChange, size = "lg" }: StarRatingProps) {
<button
key={star}
type="button"
onClick={() => onChange(star)}
onClick={() => { hapticLight(); onChange(star) }}
className="transition-transform active:scale-90"
>
<Star

View File

@@ -4,6 +4,7 @@ import { useDrag } from "@use-gesture/react"
import { Trash2 } from "lucide-react"
import { Button } from "@/components/ui/button"
import { ConfirmDialog } from "@/components/confirm-dialog"
import { hapticMedium } from "@/lib/haptics"
type SwipeToDeleteProps = {
onDelete: () => void
@@ -31,6 +32,7 @@ export function SwipeToDelete({
setOffsetX(Math.min(0, Math.max(mx, -100)))
} else {
if (mx < -60) {
hapticMedium()
setRevealed(true)
setOffsetX(-80)
} else {

View File

@@ -4,9 +4,10 @@ import { Drawer as DrawerPrimitive } from "vaul"
import { cn } from "@/lib/utils"
function Drawer({
repositionInputs = false,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
return <DrawerPrimitive.Root data-slot="drawer" {...props} />
return <DrawerPrimitive.Root data-slot="drawer" repositionInputs={repositionInputs} {...props} />
}
function DrawerTrigger({

19
spa/src/lib/date.ts Normal file
View File

@@ -0,0 +1,19 @@
import { formatDistanceToNow, parseISO, format } from "date-fns"
export function timeAgo(dateStr: string): string {
try {
const date = dateStr.includes("T") ? parseISO(dateStr) : new Date(dateStr.replace(" ", "T"))
return formatDistanceToNow(date, { addSuffix: true })
} catch {
return dateStr.slice(0, 10)
}
}
export function shortDate(dateStr: string): string {
try {
const date = dateStr.includes("T") ? parseISO(dateStr) : new Date(dateStr.replace(" ", "T"))
return format(date, "MMM d, yyyy")
} catch {
return dateStr.slice(0, 10)
}
}

7
spa/src/lib/haptics.ts Normal file
View File

@@ -0,0 +1,7 @@
export function hapticLight() {
navigator?.vibrate?.(10)
}
export function hapticMedium() {
navigator?.vibrate?.(20)
}

View File

@@ -1,6 +1,13 @@
import { QueryClient } from "@tanstack/react-query"
import { QueryClient, type Mutation } from "@tanstack/react-query"
import { toast } from "sonner"
import { ApiError } from "@/lib/api/client"
function onMutationError(error: Error, _vars: unknown, _ctx: unknown, mutation: Mutation) {
if (mutation.options.onError) return
const msg = error instanceof ApiError ? `Error ${error.status}` : "Something went wrong"
toast.error(msg)
}
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
@@ -13,5 +20,8 @@ export const queryClient = new QueryClient({
return failureCount < 2
},
},
mutations: {
onError: onMutationError as never,
},
},
})

View File

@@ -67,7 +67,8 @@
"removeFromWatchlist": "Remove from watchlist?",
"queueEmpty": "Queue empty",
"queueEmptyDesc": "Movies from Jellyfin/Plex appear here",
"deleteReview": "Delete this review?"
"deleteReview": "Delete this review?",
"addedToWatchlist": "Added to watchlist"
},
"diary": {
"title": "Diary",

View File

@@ -1,7 +1,7 @@
import { createFileRoute } from "@tanstack/react-router"
import { useCallback, useState } from "react"
import { useTranslation } from "react-i18next"
import { Clapperboard, Film, Inbox, Plus } from "lucide-react"
import { Clapperboard, Film, Inbox, Plus, RefreshCw } from "lucide-react"
import { ReviewCard } from "@/components/review-card"
import { MovieCard } from "@/components/movie-card"
import { EmptyState } from "@/components/empty-state"
@@ -13,6 +13,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { Skeleton } from "@/components/ui/skeleton"
import { StarRating } from "@/components/star-rating"
import { useAuth } from "@/components/auth-provider"
import { useQueryClient } from "@tanstack/react-query"
import { useInfiniteActivityFeed, useDeleteReview } from "@/hooks/use-diary"
import { SearchOverlay } from "@/components/search-overlay"
import type { MovieSelection } from "@/components/search-overlay"
@@ -52,6 +53,8 @@ function HomePage() {
function FeedTab() {
const { t } = useTranslation()
const { auth } = useAuth()
const qc = useQueryClient()
const [refreshing, setRefreshing] = useState(false)
const [sortBy, setSortBy] = useState("date")
const feedSortOptions = [
{ value: "date", label: t("feed.sortLatest") },
@@ -67,7 +70,19 @@ function FeedTab() {
return (
<div className="space-y-2">
<div className="flex justify-end">
<div className="flex items-center justify-end gap-2">
<Button
variant="ghost"
size="icon"
className="size-8"
onClick={async () => {
setRefreshing(true)
await qc.refetchQueries({ queryKey: ["activity-feed"] })
setRefreshing(false)
}}
>
<RefreshCw className={`size-4 ${refreshing ? "animate-spin" : ""}`} />
</Button>
<Select value={sortBy} onValueChange={setSortBy}>
<SelectTrigger className="w-36">
<SelectValue />

View File

@@ -1,6 +1,7 @@
import { createFileRoute, Link } from "@tanstack/react-router"
import { useTranslation } from "react-i18next"
import { ArrowLeft, Bookmark, BookmarkCheck, Globe, Star, TrendingUp, User, Users } from "lucide-react"
import { Bookmark, BookmarkCheck, Globe, Star, TrendingUp, User, Users } from "lucide-react"
import { BackButton } from "@/components/back-button"
import { StarDisplay } from "@/components/star-display"
import { RatingHistogram } from "@/components/rating-histogram"
import { EmptyState } from "@/components/empty-state"
@@ -9,6 +10,7 @@ import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
import { Skeleton } from "@/components/ui/skeleton"
import { posterUrl, tmdbProfileUrl } from "@/lib/api/client"
import { timeAgo, shortDate } from "@/lib/date"
import { useMovie, useMovieHistory, useMovieProfile } from "@/hooks/use-movies"
import {
useWatchlistStatus,
@@ -36,9 +38,7 @@ function MovieDetailPage() {
return (
<div className="space-y-5 p-4">
<Link to="/" className="inline-flex items-center gap-1 text-sm text-muted-foreground">
<ArrowLeft className="size-4" /> {t("common.back")}
</Link>
<BackButton />
<HeroSection movie={movie} stats={stats} movieId={id} tagline={profile?.tagline} />
@@ -116,7 +116,7 @@ function MovieDetailPage() {
{r.user_display}
{r.is_federated && <Globe className="size-3 text-muted-foreground/60" />}
</CardTitle>
<CardDescription className="text-[10px]">{r.watched_at.slice(0, 10)}</CardDescription>
<CardDescription className="text-[10px]">{timeAgo(r.watched_at)}</CardDescription>
</div>
<StarDisplay rating={r.rating} size="xs" />
</div>
@@ -145,7 +145,7 @@ function MovieDetailPage() {
{history.viewings.map((v) => (
<div key={v.id} className="flex items-center justify-between rounded-xl bg-card p-3">
<div>
<p className="text-sm font-medium">{v.watched_at}</p>
<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>
)}

View File

@@ -1,6 +1,7 @@
import { createFileRoute, Link } from "@tanstack/react-router"
import { createFileRoute } from "@tanstack/react-router"
import { useTranslation } from "react-i18next"
import { ArrowLeft, Film, User } from "lucide-react"
import { Film, User } from "lucide-react"
import { BackButton } from "@/components/back-button"
import { MovieCard } from "@/components/movie-card"
import { EmptyState } from "@/components/empty-state"
import { Skeleton } from "@/components/ui/skeleton"
@@ -23,9 +24,7 @@ function PersonDetailPage() {
return (
<div className="space-y-4 p-4">
<Link to="/" className="inline-flex items-center gap-1 text-sm text-muted-foreground">
<ArrowLeft className="size-4" /> {t("common.back")}
</Link>
<BackButton />
<div className="flex items-center gap-4">
<div className="size-16 flex-shrink-0 overflow-hidden rounded-full bg-muted">

View File

@@ -1,7 +1,8 @@
import { createFileRoute } from "@tanstack/react-router"
import { useCallback, useState } from "react"
import { useTranslation } from "react-i18next"
import { Search as SearchIcon, Film, Users } from "lucide-react"
import { Bookmark, Search as SearchIcon, Film, Users } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { MovieCard } from "@/components/movie-card"
import { PersonRow } from "@/components/person-row"
@@ -10,6 +11,8 @@ import { InfiniteScroll } from "@/components/infinite-scroll"
import { Skeleton } from "@/components/ui/skeleton"
import { useInfiniteSearch } from "@/hooks/use-search"
import { useDebounce } from "@/hooks/use-debounce"
import { useAddToWatchlist } from "@/hooks/use-watchlist"
import { toast } from "sonner"
export const Route = createFileRoute("/_app/search")({
component: SearchPage,
@@ -17,6 +20,7 @@ export const Route = createFileRoute("/_app/search")({
function SearchPage() {
const { t } = useTranslation()
const addToWatchlist = useAddToWatchlist()
const [query, setQuery] = useState("")
const debouncedQuery = useDebounce(query, 300)
const {
@@ -87,6 +91,21 @@ function SearchPage() {
genres: hit.genres,
}}
variant="full"
action={
<Button
variant="ghost"
size="icon"
className="size-8 text-muted-foreground"
onClick={() => {
addToWatchlist.mutate(
{ movie_id: hit.movie_id },
{ onSuccess: () => toast.success(t("feed.addedToWatchlist")) },
)
}}
>
<Bookmark className="size-4" />
</Button>
}
/>
))}
</div>

View File

@@ -1,6 +1,7 @@
import { createFileRoute, Link } from "@tanstack/react-router"
import { createFileRoute } from "@tanstack/react-router"
import { useTranslation } from "react-i18next"
import { ArrowLeft, UserCheck, UserPlus } from "lucide-react"
import { UserCheck, UserPlus } from "lucide-react"
import { BackButton } from "@/components/back-button"
import { Button } from "@/components/ui/button"
import { ProfileView, ProfileSkeleton } from "@/components/profile-view"
import { useAuth } from "@/components/auth-provider"
@@ -28,9 +29,7 @@ function UserProfilePage() {
return (
<div className="p-4">
<Link to="/" className="mb-4 inline-flex items-center gap-1 text-sm text-muted-foreground">
<ArrowLeft className="size-4" /> {t("common.back")}
</Link>
<div className="mb-4"><BackButton /></div>
<ProfileView
data={data}

View File

@@ -1,6 +1,7 @@
import { createFileRoute, Link } from "@tanstack/react-router"
import { useTranslation } from "react-i18next"
import { ArrowLeft, Star, Users } from "lucide-react"
import { Star, Users } from "lucide-react"
import { BackButton } from "@/components/back-button"
import { Badge } from "@/components/ui/badge"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Skeleton } from "@/components/ui/skeleton"
@@ -26,9 +27,7 @@ function WrapUpReportPage() {
return (
<div className="space-y-4 p-4">
<Link to="/profile" className="inline-flex items-center gap-1 text-sm text-muted-foreground">
<ArrowLeft className="size-4" /> {t("profile.title")}
</Link>
<BackButton />
{/* Hero */}
<Card>