Build world campaigns rewards and recaps
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
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 group challenges…</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 by challenge title, slug, or group name to link a primary challenge.</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-trophy" /></div> : null}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{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>
|
||||
<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 WorldLinkedChallengePickerModal({ open, onClose, onSave, initialChallenge, searchEntities }) {
|
||||
const [query, setQuery] = useState('')
|
||||
const [results, setResults] = useState([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [selected, setSelected] = useState(initialChallenge || null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
|
||||
setQuery(initialChallenge?.title || '')
|
||||
setSelected(initialChallenge || null)
|
||||
setResults([])
|
||||
setLoading(false)
|
||||
}, [open, initialChallenge])
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setResults([])
|
||||
setLoading(false)
|
||||
return undefined
|
||||
}
|
||||
|
||||
let cancelled = false
|
||||
const timeoutId = window.setTimeout(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const items = await searchEntities('challenge', 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 challenge</button>
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose} title="Link primary challenge" 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 challenge title, slug, or group" 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user