167 lines
7.9 KiB
JavaScript
167 lines
7.9 KiB
JavaScript
import React, { useEffect, useState } from 'react'
|
|
|
|
function SelectedArtworkCard({ artwork, onClear, disabled = false }) {
|
|
if (!artwork) return null
|
|
|
|
return (
|
|
<div className="rounded-[26px] border border-sky-300/20 bg-sky-400/[0.08] p-4">
|
|
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
|
<div className="flex min-w-0 items-center gap-4">
|
|
{artwork.thumbnail ? (
|
|
<img src={artwork.thumbnail} alt={artwork.title} className="h-20 w-20 rounded-[22px] object-cover ring-1 ring-white/10" />
|
|
) : (
|
|
<div className="flex h-20 w-20 items-center justify-center rounded-[22px] border border-white/10 bg-white/[0.04] text-slate-500">
|
|
<i className="fa-solid fa-image" />
|
|
</div>
|
|
)}
|
|
<div className="min-w-0">
|
|
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-sky-200/80">Linked original</div>
|
|
<div className="mt-2 truncate text-lg font-semibold text-white">{artwork.title}</div>
|
|
<div className="mt-1 flex flex-wrap items-center gap-2 text-sm text-slate-300">
|
|
<span>{artwork.publisher || 'Artist'}</span>
|
|
{artwork.year ? <span className="text-slate-500">{artwork.year}</span> : null}
|
|
{artwork.content_type ? <span className="rounded-full border border-white/10 bg-white/[0.04] px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-300">{artwork.content_type}</span> : null}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
{artwork.url ? (
|
|
<a
|
|
href={artwork.url}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
className="inline-flex items-center gap-2 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm font-semibold text-white transition hover:bg-white/[0.08]"
|
|
>
|
|
Open public
|
|
<i className="fa-solid fa-arrow-up-right-from-square text-slate-500" />
|
|
</a>
|
|
) : null}
|
|
<button
|
|
type="button"
|
|
onClick={onClear}
|
|
disabled={disabled}
|
|
className="inline-flex items-center gap-2 rounded-2xl border border-rose-300/20 bg-rose-400/10 px-4 py-3 text-sm font-semibold text-rose-100 transition hover:bg-rose-400/15 disabled:cursor-not-allowed disabled:opacity-60"
|
|
>
|
|
<i className="fa-solid fa-link-slash" />
|
|
Remove link
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default function ArtworkEvolutionSearchPicker({ artworkId, selected, onSelect, disabled = false }) {
|
|
const [query, setQuery] = useState('')
|
|
const [options, setOptions] = useState([])
|
|
const [loading, setLoading] = useState(false)
|
|
|
|
useEffect(() => {
|
|
if (!artworkId) return undefined
|
|
|
|
const controller = new AbortController()
|
|
const handle = window.setTimeout(async () => {
|
|
setLoading(true)
|
|
try {
|
|
const response = await fetch(`/api/studio/artworks/${artworkId}/evolution-options?search=${encodeURIComponent(query)}`, {
|
|
headers: { Accept: 'application/json' },
|
|
credentials: 'same-origin',
|
|
signal: controller.signal,
|
|
})
|
|
|
|
if (!response.ok) {
|
|
setOptions([])
|
|
return
|
|
}
|
|
|
|
const data = await response.json()
|
|
setOptions(Array.isArray(data.data) ? data.data : [])
|
|
} catch (error) {
|
|
if (error?.name !== 'AbortError') {
|
|
setOptions([])
|
|
}
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}, query.trim() === '' ? 0 : 220)
|
|
|
|
return () => {
|
|
controller.abort()
|
|
window.clearTimeout(handle)
|
|
}
|
|
}, [artworkId, query])
|
|
|
|
const visibleOptions = options.filter((option) => Number(option.id) !== Number(selected?.id))
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<SelectedArtworkCard artwork={selected} onClear={() => onSelect(null)} disabled={disabled} />
|
|
|
|
<div className="rounded-[28px] border border-white/10 bg-white/[0.03] p-4">
|
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-end">
|
|
<div className="min-w-0 flex-1">
|
|
<label className="mb-2 block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400">Search your manageable artworks</label>
|
|
<input
|
|
type="text"
|
|
value={query}
|
|
onChange={(event) => setQuery(event.target.value)}
|
|
placeholder="Search by title, slug, creator, or group"
|
|
disabled={disabled}
|
|
className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 disabled:cursor-not-allowed disabled:opacity-60"
|
|
/>
|
|
</div>
|
|
<div className="rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-slate-300">
|
|
{loading ? 'Searching…' : `${visibleOptions.length} result${visibleOptions.length === 1 ? '' : 's'}`}
|
|
</div>
|
|
</div>
|
|
|
|
<p className="mt-3 text-sm leading-relaxed text-slate-400">
|
|
Start with your own published artworks. Group-published pieces appear too when you can publish artworks for that group. Results are ranked by visual similarity when the vector index is available.
|
|
</p>
|
|
|
|
<div className="mt-4 space-y-3">
|
|
{visibleOptions.length ? visibleOptions.map((option) => (
|
|
<button
|
|
key={option.id}
|
|
type="button"
|
|
onClick={() => onSelect(option)}
|
|
disabled={disabled}
|
|
className="flex w-full flex-col gap-4 rounded-[24px] border border-white/10 bg-white/[0.04] p-4 text-left transition hover:border-white/20 hover:bg-white/[0.06] disabled:cursor-not-allowed disabled:opacity-60 md:flex-row md:items-center md:justify-between"
|
|
>
|
|
<div className="flex min-w-0 items-center gap-4">
|
|
{option.thumbnail ? (
|
|
<img src={option.thumbnail} alt={option.title} className="h-16 w-16 rounded-[18px] object-cover ring-1 ring-white/10" />
|
|
) : (
|
|
<div className="flex h-16 w-16 items-center justify-center rounded-[18px] border border-white/10 bg-white/[0.05] text-slate-500">
|
|
<i className="fa-solid fa-image" />
|
|
</div>
|
|
)}
|
|
<div className="min-w-0">
|
|
<div className="truncate text-base font-semibold text-white">{option.title}</div>
|
|
<div className="mt-1 flex flex-wrap items-center gap-2 text-sm text-slate-400">
|
|
<span>{option.publisher || 'Artist'}</span>
|
|
{option.year ? <span>{option.year}</span> : null}
|
|
{option.category ? <span>{option.category}</span> : null}
|
|
{typeof option.similarity_score === 'number' ? (
|
|
<span className="rounded-full border border-emerald-300/20 bg-emerald-400/10 px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-emerald-100">
|
|
{Math.round(option.similarity_score * 100)}% similar
|
|
</span>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<span className="inline-flex items-center gap-2 self-start rounded-2xl border border-sky-300/25 bg-sky-400/10 px-4 py-3 text-sm font-semibold text-sky-100 md:self-center">
|
|
<i className="fa-solid fa-link" />
|
|
Link older version
|
|
</span>
|
|
</button>
|
|
)) : (
|
|
<div className="rounded-[24px] border border-dashed border-white/12 bg-white/[0.02] px-5 py-10 text-center text-sm text-slate-300">
|
|
{loading ? 'Searching artworks…' : 'No manageable published artworks matched this search yet.'}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
} |