shared ui: poster cards, content rows, scrollers, lazy mount
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
import { useState } from 'react'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { X } from '../../lib/icons'
|
||||
import { normaliseLetterboxdListUrl } from '../../api/letterboxd'
|
||||
import { useLetterboxdLists } from '../../stores/letterboxd-lists-store'
|
||||
|
||||
interface Props {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export default function LetterboxdAddModal({ open, onClose }: Props) {
|
||||
const add = useLetterboxdLists(s => s.add)
|
||||
const [url, setUrl] = useState('')
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
function save() {
|
||||
const norm = normaliseLetterboxdListUrl(url)
|
||||
if (!norm) {
|
||||
setError('That doesn\'t look like a Letterboxd list URL.')
|
||||
return
|
||||
}
|
||||
add(norm)
|
||||
setUrl('')
|
||||
setError(null)
|
||||
onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.18 }}
|
||||
onClick={onClose}
|
||||
className="fixed inset-0 z-[80] grid place-items-center bg-black/65 backdrop-blur-sm p-6"
|
||||
>
|
||||
<motion.div
|
||||
initial={{ y: 20, scale: 0.96 }}
|
||||
animate={{ y: 0, scale: 1 }}
|
||||
exit={{ y: 20, scale: 0.96 }}
|
||||
transition={{ duration: 0.32, ease: [0.16, 1, 0.3, 1] }}
|
||||
onClick={e => e.stopPropagation()}
|
||||
className="relative w-full max-w-md rounded-2xl bg-surface ring-1 ring-border-strong shadow-[0_30px_80px_-20px_rgba(0,0,0,0.85)] p-5"
|
||||
>
|
||||
<button
|
||||
onClick={onClose}
|
||||
aria-label="Close"
|
||||
className="absolute top-3 right-3 w-8 h-8 grid place-items-center rounded-full text-text-3 hover:text-text-1 hover:bg-elevated transition focus-ring"
|
||||
>
|
||||
<X size={14} stroke={2} />
|
||||
</button>
|
||||
<h2 className="text-[16px] font-semibold tracking-tight text-text-1 mb-1">
|
||||
Add a Letterboxd list
|
||||
</h2>
|
||||
<p className="text-[12px] text-text-3 mb-4 leading-relaxed">
|
||||
Paste the URL of any public Letterboxd list. We'll cross-check it against
|
||||
your library so missing entries are obvious.
|
||||
</p>
|
||||
<input
|
||||
type="url"
|
||||
value={url}
|
||||
onChange={e => {
|
||||
setUrl(e.target.value)
|
||||
if (error) setError(null)
|
||||
}}
|
||||
onKeyDown={e => { if (e.key === 'Enter') save() }}
|
||||
placeholder="https://letterboxd.com/USER/list/SLUG/"
|
||||
autoFocus
|
||||
className="w-full h-10 px-3 rounded-md bg-elevated/60 ring-1 ring-border focus:ring-accent/50 outline-none text-[13px] tracking-tight"
|
||||
/>
|
||||
{error && (
|
||||
<p className="text-[11.5px] text-red-300 mt-2">{error}</p>
|
||||
)}
|
||||
<div className="flex items-center justify-end gap-2 mt-5">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="h-10 px-4 rounded-full text-[12.5px] text-text-2 hover:text-text-1 hover:bg-elevated transition focus-ring"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={save}
|
||||
disabled={!url.trim()}
|
||||
className="h-10 px-5 rounded-full bg-accent text-void text-[12.5px] font-semibold tracking-tight transition disabled:opacity-40 disabled:cursor-not-allowed hover:bg-accent-hover focus-ring"
|
||||
>
|
||||
Add list
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user