fix: portal search overlay to escape transform stacking context, auto-fill IMDB from ttXXXX input
All checks were successful
CI / Check / Test (push) Successful in 1h12m50s

This commit is contained in:
2026-07-11 01:44:08 +02:00
parent 85285b2a52
commit 95b839355f

View File

@@ -1,4 +1,5 @@
import { useState } from "react" import { useState } from "react"
import { createPortal } from "react-dom"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { PenLine, Search, X } from "lucide-react" import { PenLine, Search, X } from "lucide-react"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
@@ -27,6 +28,8 @@ type SearchOverlayProps = {
onSelect: (movie: MovieSelection) => void onSelect: (movie: MovieSelection) => void
} }
const IMDB_RE = /^tt\d{4,}$/i
export function SearchOverlay({ open, onClose, onSelect }: SearchOverlayProps) { export function SearchOverlay({ open, onClose, onSelect }: SearchOverlayProps) {
const { t } = useTranslation() const { t } = useTranslation()
const [query, setQuery] = useState("") const [query, setQuery] = useState("")
@@ -38,6 +41,15 @@ export function SearchOverlay({ open, onClose, onSelect }: SearchOverlayProps) {
const debouncedQuery = useDebounce(query, 300) const debouncedQuery = useDebounce(query, 300)
const { data, isPending } = useSearch({ q: debouncedQuery || undefined }) const { data, isPending } = useSearch({ q: debouncedQuery || undefined })
function handleQueryChange(value: string) {
setQuery(value)
if (IMDB_RE.test(value.trim())) {
setManualImdbId(value.trim())
setManual(true)
setQuery("")
}
}
if (!open) return null if (!open) return null
const hasImdbId = manualImdbId.trim().length > 0 const hasImdbId = manualImdbId.trim().length > 0
@@ -56,62 +68,58 @@ export function SearchOverlay({ open, onClose, onSelect }: SearchOverlayProps) {
}) })
} }
if (manual) {
return (
<div className="fixed inset-0 z-50 flex flex-col glass-heavy">
<div className="flex items-center justify-between p-4">
<Button variant="ghost" size="sm" onClick={() => setManual(false)}>
{t("searchOverlay.backToSearch")}
</Button>
<Button variant="ghost" size="sm" onClick={onClose}>
{t("common.cancel")}
</Button>
</div>
<div className="flex-1 overflow-auto px-4">
<Card>
<CardHeader>
<CardTitle>{t("searchOverlay.addManuallyTitle")}</CardTitle>
<CardDescription>{t("searchOverlay.addManuallyDesc")}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-1.5">
<Label htmlFor="manual-imdb">{t("searchOverlay.imdbId")}</Label>
<Input id="manual-imdb" value={manualImdbId} onChange={(e) => setManualImdbId(e.target.value)} placeholder={t("searchOverlay.imdbPlaceholder")} autoFocus />
<p className="text-xs text-muted-foreground">{t("searchOverlay.imdbHelp")}</p>
</div>
<Separator />
<p className="text-xs text-muted-foreground">{t("searchOverlay.orSearchByTitle")}</p>
<div className="space-y-1.5">
<Label htmlFor="manual-title">{t("searchOverlay.titleLabel")} {!hasImdbId && "*"}</Label>
<Input id="manual-title" value={manualTitle} onChange={(e) => setManualTitle(e.target.value)} placeholder={t("searchOverlay.titlePlaceholder")} />
</div>
<div className="space-y-1.5">
<Label htmlFor="manual-year">{t("searchOverlay.releaseYear")} {!hasImdbId && "*"}</Label>
<Input id="manual-year" type="number" value={manualYear} onChange={(e) => setManualYear(e.target.value)} placeholder={t("searchOverlay.yearPlaceholder")} />
</div>
<div className="space-y-1.5">
<Label htmlFor="manual-director">{t("searchOverlay.director")}</Label>
<Input id="manual-director" value={manualDirector} onChange={(e) => setManualDirector(e.target.value)} placeholder={t("searchOverlay.directorPlaceholder")} />
</div>
<Button onClick={handleManualSubmit} disabled={!canSubmitManual} className="w-full">
{t("common.continue")}
</Button>
</CardContent>
</Card>
</div>
</div>
)
}
const hasResults = (data?.movies?.items?.length ?? 0) > 0 const hasResults = (data?.movies?.items?.length ?? 0) > 0
const searched = debouncedQuery.length > 0 && !isPending const searched = debouncedQuery.length > 0 && !isPending
return ( const content = manual ? (
<div className="fixed inset-0 z-50 flex flex-col glass-heavy">
<div className="flex items-center justify-between p-4">
<Button variant="ghost" size="sm" onClick={() => setManual(false)}>
{t("searchOverlay.backToSearch")}
</Button>
<Button variant="ghost" size="sm" onClick={onClose}>
{t("common.cancel")}
</Button>
</div>
<div className="flex-1 overflow-auto px-4">
<Card>
<CardHeader>
<CardTitle>{t("searchOverlay.addManuallyTitle")}</CardTitle>
<CardDescription>{t("searchOverlay.addManuallyDesc")}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-1.5">
<Label htmlFor="manual-imdb">{t("searchOverlay.imdbId")}</Label>
<Input id="manual-imdb" value={manualImdbId} onChange={(e) => setManualImdbId(e.target.value)} placeholder={t("searchOverlay.imdbPlaceholder")} autoFocus />
<p className="text-xs text-muted-foreground">{t("searchOverlay.imdbHelp")}</p>
</div>
<Separator />
<p className="text-xs text-muted-foreground">{t("searchOverlay.orSearchByTitle")}</p>
<div className="space-y-1.5">
<Label htmlFor="manual-title">{t("searchOverlay.titleLabel")} {!hasImdbId && "*"}</Label>
<Input id="manual-title" value={manualTitle} onChange={(e) => setManualTitle(e.target.value)} placeholder={t("searchOverlay.titlePlaceholder")} />
</div>
<div className="space-y-1.5">
<Label htmlFor="manual-year">{t("searchOverlay.releaseYear")} {!hasImdbId && "*"}</Label>
<Input id="manual-year" type="number" value={manualYear} onChange={(e) => setManualYear(e.target.value)} placeholder={t("searchOverlay.yearPlaceholder")} />
</div>
<div className="space-y-1.5">
<Label htmlFor="manual-director">{t("searchOverlay.director")}</Label>
<Input id="manual-director" value={manualDirector} onChange={(e) => setManualDirector(e.target.value)} placeholder={t("searchOverlay.directorPlaceholder")} />
</div>
<Button onClick={handleManualSubmit} disabled={!canSubmitManual} className="w-full">
{t("common.continue")}
</Button>
</CardContent>
</Card>
</div>
</div>
) : (
<div className="fixed inset-0 z-50 flex flex-col glass-heavy"> <div className="fixed inset-0 z-50 flex flex-col glass-heavy">
<div className="flex items-center gap-3 p-4"> <div className="flex items-center gap-3 p-4">
<div className="relative flex-1"> <div className="relative flex-1">
<Search className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" /> <Search className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
<Input value={query} onChange={(e) => setQuery(e.target.value)} placeholder={t("searchOverlay.searchPlaceholder")} className="pl-9" autoFocus /> <Input value={query} onChange={(e) => handleQueryChange(e.target.value)} placeholder={t("searchOverlay.searchPlaceholder")} className="pl-9" autoFocus />
{query && ( {query && (
<Button variant="ghost" size="icon" onClick={() => setQuery("")} className="absolute right-3 top-1/2 size-6 -translate-y-1/2"> <Button variant="ghost" size="icon" onClick={() => setQuery("")} className="absolute right-3 top-1/2 size-6 -translate-y-1/2">
<X className="size-4 text-muted-foreground" /> <X className="size-4 text-muted-foreground" />
@@ -149,7 +157,7 @@ export function SearchOverlay({ open, onClose, onSelect }: SearchOverlayProps) {
}} }}
className="flex w-full items-center gap-3 rounded-lg p-2 text-left transition-colors active:bg-muted" className="flex w-full items-center gap-3 rounded-lg p-2 text-left transition-colors active:bg-muted"
> >
<div className="h-14 w-10 flex-shrink-0 overflow-hidden rounded-md bg-muted"> <div className="h-14 w-10 shrink-0 overflow-hidden rounded-md bg-muted">
{hit.poster_path && <img src={posterUrl(hit.poster_path)} alt="" className="size-full object-cover" />} {hit.poster_path && <img src={posterUrl(hit.poster_path)} alt="" className="size-full object-cover" />}
</div> </div>
<div> <div>
@@ -182,4 +190,6 @@ export function SearchOverlay({ open, onClose, onSelect }: SearchOverlayProps) {
</div> </div>
</div> </div>
) )
return createPortal(content, document.body)
} }