Files
SkinbaseNova/resources/js/components/worlds/editor/WorldRecapArticlePickerModal.jsx

111 lines
5.5 KiB
JavaScript

import React, { useEffect, useMemo, useState } from 'react'
import Modal from '../../ui/Modal'
function SearchResultList({ items, loading, selectedId, onSelect }) {
if (loading) {
return <div className="rounded-2xl border border-dashed border-white/10 bg-white/[0.02] p-4 text-sm text-slate-400">Searching recap articles</div>
}
if (!Array.isArray(items) || items.length === 0) {
return <div className="rounded-2xl border border-dashed border-white/10 bg-white/[0.02] p-4 text-sm text-slate-400">Search published news by title, slug, or category to link a recap article.</div>
}
return (
<div className="grid gap-3">
{items.map((item) => (
<button key={item.id} type="button" onClick={() => onSelect(item)} className={`min-w-0 flex items-start gap-3 rounded-[24px] border px-4 py-4 text-left transition ${String(selectedId) === String(item.id) ? 'border-emerald-300/25 bg-emerald-400/10' : 'border-white/10 bg-black/20 hover:border-white/20'}`}>
<div className="relative h-14 w-14 shrink-0 overflow-hidden rounded-2xl border border-white/10 bg-slate-950">
{item.image ? <img src={item.image} alt="" className="h-full w-full object-cover" /> : null}
{!item.image && item.avatar ? <img src={item.avatar} alt="" className="h-full w-full object-cover" /> : null}
{!item.image && !item.avatar ? <div className="flex h-full w-full items-center justify-center text-slate-500"><i className="fa-solid fa-newspaper" /></div> : null}
</div>
<div className="min-w-0 flex-1">
{item.entity_label ? <span className="rounded-full border border-sky-300/20 bg-sky-400/10 px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-sky-100">{item.entity_label}</span> : null}
<div className="mt-2 text-sm font-semibold text-white">{item.title}</div>
{item.subtitle ? <div className="mt-1 text-xs uppercase tracking-[0.14em] text-slate-500">{item.subtitle}</div> : null}
{item.description ? <div className="mt-2 line-clamp-2 text-sm leading-6 text-slate-400">{item.description}</div> : null}
{Array.isArray(item.meta) && item.meta.length > 0 ? <div className="mt-2 flex flex-wrap gap-2 text-xs text-slate-500">{item.meta.map((meta) => <span key={meta}>{meta}</span>)}</div> : null}
</div>
</button>
))}
</div>
)
}
export default function WorldRecapArticlePickerModal({ open, onClose, onSave, initialArticle, searchEntities }) {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
const [loading, setLoading] = useState(false)
const [selected, setSelected] = useState(initialArticle || null)
useEffect(() => {
if (!open) return
setQuery(initialArticle?.title || '')
setSelected(initialArticle || null)
setResults([])
setLoading(false)
}, [open, initialArticle])
useEffect(() => {
if (!open) {
setResults([])
setLoading(false)
return undefined
}
let cancelled = false
const timeoutId = window.setTimeout(async () => {
setLoading(true)
try {
const items = await searchEntities('news', query || '')
if (!cancelled) {
setResults(Array.isArray(items) ? items : [])
}
} finally {
if (!cancelled) {
setLoading(false)
}
}
}, query ? 220 : 0)
return () => {
cancelled = true
window.clearTimeout(timeoutId)
}
}, [open, query, searchEntities])
const selectedPreview = useMemo(() => selected || null, [selected])
const footer = (
<>
<button type="button" onClick={onClose} className="rounded-full border border-white/10 px-4 py-2 text-sm font-semibold text-white">Cancel</button>
<button type="button" onClick={() => selectedPreview && onSave(selectedPreview)} disabled={!selectedPreview} className="rounded-full border border-sky-300/20 bg-sky-400/10 px-4 py-2 text-sm font-semibold text-sky-100 disabled:cursor-not-allowed disabled:opacity-50">Link article</button>
</>
)
return (
<Modal open={open} onClose={onClose} title="Link recap article" size="2xl" footer={footer}>
<div className="grid gap-5 overflow-x-hidden">
<label className="grid gap-2 text-sm text-slate-300">
<span className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">Search</span>
<input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="Search article title, slug, or category" className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" />
</label>
<SearchResultList items={results} loading={loading} selectedId={selectedPreview?.id} onSelect={(item) => {
setSelected(item)
setQuery(item.title)
}} />
{selectedPreview ? (
<div className="rounded-[24px] border border-emerald-300/20 bg-emerald-400/10 p-4 text-sm text-emerald-50">
<div className="break-words font-semibold">Selected: {selectedPreview.title}</div>
{selectedPreview.subtitle ? <div className="mt-1 break-words text-xs uppercase tracking-[0.14em] text-emerald-100/70">{selectedPreview.subtitle}</div> : null}
{Array.isArray(selectedPreview.meta) && selectedPreview.meta.length > 0 ? <div className="mt-2 flex flex-wrap gap-2 text-xs text-emerald-100/75">{selectedPreview.meta.map((entry) => <span key={entry}>{entry}</span>)}</div> : null}
</div>
) : null}
</div>
</Modal>
)
}