Commit workspace changes
This commit is contained in:
@@ -2,6 +2,7 @@ import React, { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import SearchOverlay from './SearchOverlay'
|
||||
|
||||
const ARTWORKS_API = '/api/search/artworks'
|
||||
const GROUPS_API = '/api/search/groups'
|
||||
const TAGS_API = '/api/tags/search'
|
||||
const USERS_API = '/api/search/users'
|
||||
const DEBOUNCE_MS = 300
|
||||
@@ -17,10 +18,11 @@ function useDebounce(value, delay) {
|
||||
|
||||
const isMac = typeof navigator !== 'undefined' && /Mac|iPhone|iPod|iPad/.test(navigator.platform)
|
||||
|
||||
export default function SearchBar({ placeholder = 'Search artworks, artists, tags\u2026' }) {
|
||||
export default function SearchBar({ placeholder = 'Search artworks, groups, artists, tags\u2026' }) {
|
||||
const [phase, setPhase] = useState('idle') // idle | opening | open | closing
|
||||
const [query, setQuery] = useState('')
|
||||
const [artworks, setArtworks] = useState([])
|
||||
const [groups, setGroups] = useState([])
|
||||
const [tags, setTags] = useState([])
|
||||
const [users, setUsers] = useState([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
@@ -40,12 +42,13 @@ export default function SearchBar({ placeholder = 'Search artworks, artists, tag
|
||||
const debouncedQuery = useDebounce(query, DEBOUNCE_MS)
|
||||
const isExpanded = phase === 'opening' || phase === 'open'
|
||||
const isMobileOverlayVisible = mobileOverlayPhase !== 'closed'
|
||||
const hasSuggestions = artworks.length > 0 || users.length > 0 || tags.length > 0
|
||||
const hasSuggestions = artworks.length > 0 || groups.length > 0 || users.length > 0 || tags.length > 0
|
||||
const suggestionListId = open && hasSuggestions ? 'sb-suggestions' : undefined
|
||||
|
||||
// flat list of navigable items: artworks → users → tags
|
||||
// flat list of navigable items: artworks → groups → users → tags
|
||||
const allItems = [
|
||||
...artworks.map(a => ({ type: 'artwork', ...a })),
|
||||
...groups.map(g => ({ type: 'group', ...g })),
|
||||
...users.map(u => ({ type: 'user', ...u })),
|
||||
...tags.map(t => ({ type: 'tag', ...t })),
|
||||
]
|
||||
@@ -70,6 +73,7 @@ export default function SearchBar({ placeholder = 'Search artworks, artists, tag
|
||||
setPhase('idle')
|
||||
setQuery('')
|
||||
setArtworks([])
|
||||
setGroups([])
|
||||
setTags([])
|
||||
setUsers([])
|
||||
}, 160)
|
||||
@@ -95,6 +99,7 @@ export default function SearchBar({ placeholder = 'Search artworks, artists, tag
|
||||
setActiveIdx(-1)
|
||||
setOpen(false)
|
||||
setArtworks([])
|
||||
setGroups([])
|
||||
setTags([])
|
||||
setUsers([])
|
||||
}, 150)
|
||||
@@ -150,7 +155,7 @@ export default function SearchBar({ placeholder = 'Search artworks, artists, tag
|
||||
const fetchSuggestions = useCallback(async (q) => {
|
||||
const bare = q?.replace(/^@+/, '') ?? ''
|
||||
if (!bare || bare.length < 2) {
|
||||
setArtworks([]); setTags([]); setUsers([]); setOpen(false); return
|
||||
setArtworks([]); setGroups([]); setTags([]); setUsers([]); setOpen(false); return
|
||||
}
|
||||
if (abortRef.current) abortRef.current.abort()
|
||||
abortRef.current = new AbortController()
|
||||
@@ -162,22 +167,28 @@ export default function SearchBar({ placeholder = 'Search artworks, artists, tag
|
||||
const fetchArt = isAtMention
|
||||
? Promise.resolve(null)
|
||||
: fetch(`${ARTWORKS_API}?q=${encodeURIComponent(bare)}&per_page=4`, { signal: sig })
|
||||
const fetchGroups = isAtMention
|
||||
? Promise.resolve(null)
|
||||
: fetch(`${GROUPS_API}?q=${encodeURIComponent(bare)}&per_page=4`, { signal: sig })
|
||||
const fetchUsers = fetch(`${USERS_API}?q=${encodeURIComponent(q)}&per_page=4`, { signal: sig })
|
||||
const fetchTags = isAtMention
|
||||
? Promise.resolve(null)
|
||||
: fetch(`${TAGS_API}?q=${encodeURIComponent(bare)}&per_page=3`, { signal: sig })
|
||||
const [artRes, userRes, tagRes] = await Promise.all([fetchArt, fetchUsers, fetchTags])
|
||||
const [artRes, groupRes, userRes, tagRes] = await Promise.all([fetchArt, fetchGroups, fetchUsers, fetchTags])
|
||||
const artJson = artRes && artRes.ok ? await artRes.json() : {}
|
||||
const groupJson = groupRes && groupRes.ok ? await groupRes.json() : {}
|
||||
const userJson = userRes && userRes.ok ? await userRes.json() : {}
|
||||
const tagJson = tagRes && tagRes.ok ? await tagRes.json() : {}
|
||||
const artItems = Array.isArray(artJson.data ?? artJson) ? (artJson.data ?? artJson).slice(0, 4) : []
|
||||
const groupItems = Array.isArray(groupJson.data ?? groupJson) ? (groupJson.data ?? groupJson).slice(0, 4) : []
|
||||
const userItems = Array.isArray(userJson.data ?? userJson) ? (userJson.data ?? userJson).slice(0, 4) : []
|
||||
const tagItems = Array.isArray(tagJson.data ?? tagJson) ? (tagJson.data ?? tagJson).slice(0, 3) : []
|
||||
setArtworks(artItems)
|
||||
setGroups(groupItems)
|
||||
setUsers(userItems)
|
||||
setTags(tagItems)
|
||||
setActiveIdx(-1)
|
||||
setOpen(artItems.length > 0 || userItems.length > 0 || tagItems.length > 0)
|
||||
setOpen(artItems.length > 0 || groupItems.length > 0 || userItems.length > 0 || tagItems.length > 0)
|
||||
} catch (e) {
|
||||
if (e.name !== 'AbortError') console.error('SearchBar fetch error', e)
|
||||
} finally {
|
||||
@@ -190,6 +201,7 @@ export default function SearchBar({ placeholder = 'Search artworks, artists, tag
|
||||
// ── navigation helpers ───────────────────────────────────────────────────
|
||||
function navigate(item) {
|
||||
if (item.type === 'artwork') window.location.href = item.urls?.web ?? `/${item.slug ?? ''}`
|
||||
else if (item.type === 'group') window.location.href = item.urls?.public ?? item.profile_url ?? `/groups/${item.slug ?? ''}`
|
||||
else if (item.type === 'user') window.location.href = item.profile_url ?? `/@${item.username}`
|
||||
else window.location.href = `/tags/${item.slug ?? item.name}`
|
||||
}
|
||||
@@ -240,6 +252,7 @@ export default function SearchBar({ placeholder = 'Search artworks, artists, tag
|
||||
inputRef={mobileInputRef}
|
||||
loading={loading}
|
||||
artworks={artworks}
|
||||
groups={groups}
|
||||
users={users}
|
||||
tags={tags}
|
||||
activeIdx={activeIdx}
|
||||
@@ -362,12 +375,39 @@ export default function SearchBar({ placeholder = 'Search artworks, artists, tag
|
||||
</>
|
||||
)}
|
||||
|
||||
{groups.length > 0 && (
|
||||
<>
|
||||
<li role="presentation" className={`px-3 pb-1 text-[10px] font-semibold uppercase tracking-wider text-white/30 select-none ${artworks.length > 0 ? 'pt-2 border-t border-white/[0.06]' : 'pt-2.5'}`}>Groups</li>
|
||||
{groups.map((group, j) => {
|
||||
const flatIdx = artworks.length + j
|
||||
return (
|
||||
<li key={group.slug ?? group.id ?? j} role="option" id={`sb-item-${flatIdx}`} aria-selected={activeIdx === flatIdx}>
|
||||
<button type="button" onClick={() => navigate({ type: 'group', ...group })}
|
||||
className={`w-full flex items-center gap-3 px-3 py-2 text-left transition-colors ${activeIdx === flatIdx ? 'bg-white/[0.09]' : 'hover:bg-white/[0.06]'}`}>
|
||||
{group.avatar_url ? (
|
||||
<img src={group.avatar_url} alt="" aria-hidden="true" className="w-9 h-9 rounded-2xl object-cover shrink-0 bg-white/[0.04] border border-white/[0.08]" loading="lazy" />
|
||||
) : (
|
||||
<span className="w-9 h-9 rounded-2xl bg-white/[0.04] border border-white/[0.07] inline-flex items-center justify-center shrink-0">
|
||||
<i className="fa-solid fa-people-group text-white/40" />
|
||||
</span>
|
||||
)}
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-medium text-white truncate">{group.name}</div>
|
||||
<div className="text-xs text-neutral-400 truncate">{group.headline || `${Number(group.counts?.followers || group.followers_count || 0).toLocaleString()} followers`}</div>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Users / Creators section */}
|
||||
{users.length > 0 && (
|
||||
<>
|
||||
<li role="presentation" className={`px-3 pb-1 text-[10px] font-semibold uppercase tracking-wider text-white/30 select-none ${artworks.length > 0 ? 'pt-2 border-t border-white/[0.06]' : 'pt-2.5'}`}>Creators</li>
|
||||
<li role="presentation" className={`px-3 pb-1 text-[10px] font-semibold uppercase tracking-wider text-white/30 select-none ${artworks.length > 0 || groups.length > 0 ? 'pt-2 border-t border-white/[0.06]' : 'pt-2.5'}`}>Creators</li>
|
||||
{users.map((user, j) => {
|
||||
const flatIdx = artworks.length + j
|
||||
const flatIdx = artworks.length + groups.length + j
|
||||
return (
|
||||
<li key={user.username} role="option" id={`sb-item-${flatIdx}`} aria-selected={activeIdx === flatIdx}>
|
||||
<button type="button" onClick={() => navigate({ type: 'user', ...user })}
|
||||
@@ -388,9 +428,9 @@ export default function SearchBar({ placeholder = 'Search artworks, artists, tag
|
||||
{/* Tags section */}
|
||||
{tags.length > 0 && (
|
||||
<>
|
||||
<li role="presentation" className={`px-3 pb-1 text-[10px] font-semibold uppercase tracking-wider text-white/30 select-none ${artworks.length > 0 || users.length > 0 ? 'pt-2 border-t border-white/[0.06]' : 'pt-2.5'}`}>Tags</li>
|
||||
<li role="presentation" className={`px-3 pb-1 text-[10px] font-semibold uppercase tracking-wider text-white/30 select-none ${artworks.length > 0 || groups.length > 0 || users.length > 0 ? 'pt-2 border-t border-white/[0.06]' : 'pt-2.5'}`}>Tags</li>
|
||||
{tags.map((tag, j) => {
|
||||
const flatIdx = artworks.length + users.length + j
|
||||
const flatIdx = artworks.length + groups.length + users.length + j
|
||||
return (
|
||||
<li key={tag.slug ?? tag.name ?? j} role="option" id={`sb-item-${flatIdx}`} aria-selected={activeIdx === flatIdx}>
|
||||
<button type="button" onClick={() => navigate({ type: 'tag', ...tag })}
|
||||
|
||||
@@ -8,6 +8,7 @@ export default function SearchOverlay({
|
||||
inputRef,
|
||||
loading,
|
||||
artworks,
|
||||
groups,
|
||||
users,
|
||||
tags,
|
||||
activeIdx,
|
||||
@@ -19,7 +20,7 @@ export default function SearchOverlay({
|
||||
}) {
|
||||
if (phase === 'closed') return null
|
||||
|
||||
const hasResults = artworks.length > 0 || users.length > 0 || tags.length > 0
|
||||
const hasResults = artworks.length > 0 || groups.length > 0 || users.length > 0 || tags.length > 0
|
||||
const isVisible = OPENING_OR_OPEN.has(phase)
|
||||
|
||||
return (
|
||||
@@ -55,7 +56,7 @@ export default function SearchOverlay({
|
||||
value={query}
|
||||
onChange={(e) => onQueryChange(e.target.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
placeholder="Search artworks, creators, tags..."
|
||||
placeholder="Search artworks, groups, creators, tags..."
|
||||
aria-label="Search"
|
||||
aria-autocomplete="list"
|
||||
aria-controls="sb-mobile-suggestions"
|
||||
@@ -116,11 +117,41 @@ export default function SearchOverlay({
|
||||
</>
|
||||
)}
|
||||
|
||||
{groups.length > 0 && (
|
||||
<>
|
||||
<li role="presentation" className={`px-3 pb-1 text-[10px] font-semibold uppercase tracking-wider text-white/35 select-none ${artworks.length > 0 ? 'pt-2 border-t border-white/[0.06]' : 'pt-2'}`}>Groups</li>
|
||||
{groups.map((group, j) => {
|
||||
const flatIdx = artworks.length + j
|
||||
return (
|
||||
<li key={group.slug ?? group.id ?? j} role="option" id={`sb-mobile-item-${flatIdx}`} aria-selected={activeIdx === flatIdx}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onNavigate({ type: 'group', ...group })}
|
||||
className={`w-full min-h-12 flex items-center gap-3 px-3 py-2 text-left rounded-lg transition-colors ${activeIdx === flatIdx ? 'bg-white/[0.14]' : 'hover:bg-white/[0.08]'}`}
|
||||
>
|
||||
{group.avatar_url ? (
|
||||
<img src={group.avatar_url} alt="" aria-hidden="true" className="w-10 h-10 rounded-2xl object-cover shrink-0 bg-white/[0.04] border border-white/[0.08]" loading="lazy" />
|
||||
) : (
|
||||
<span className="w-10 h-10 rounded-2xl bg-white/[0.04] border border-white/[0.08] inline-flex items-center justify-center shrink-0" aria-hidden="true">
|
||||
<i className="fa-solid fa-people-group text-white/45" />
|
||||
</span>
|
||||
)}
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-medium text-white truncate">{group.name}</div>
|
||||
<div className="text-xs text-neutral-400 truncate">{group.headline || `${Number(group.counts?.followers || group.followers_count || 0).toLocaleString()} followers`}</div>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
|
||||
{users.length > 0 && (
|
||||
<>
|
||||
<li role="presentation" className={`px-3 pb-1 text-[10px] font-semibold uppercase tracking-wider text-white/35 select-none ${artworks.length > 0 ? 'pt-2 border-t border-white/[0.06]' : 'pt-2'}`}>Creators</li>
|
||||
<li role="presentation" className={`px-3 pb-1 text-[10px] font-semibold uppercase tracking-wider text-white/35 select-none ${artworks.length > 0 || groups.length > 0 ? 'pt-2 border-t border-white/[0.06]' : 'pt-2'}`}>Creators</li>
|
||||
{users.map((user, j) => {
|
||||
const flatIdx = artworks.length + j
|
||||
const flatIdx = artworks.length + groups.length + j
|
||||
return (
|
||||
<li key={user.username} role="option" id={`sb-mobile-item-${flatIdx}`} aria-selected={activeIdx === flatIdx}>
|
||||
<button
|
||||
@@ -142,9 +173,9 @@ export default function SearchOverlay({
|
||||
|
||||
{tags.length > 0 && (
|
||||
<>
|
||||
<li role="presentation" className={`px-3 pb-1 text-[10px] font-semibold uppercase tracking-wider text-white/35 select-none ${artworks.length > 0 || users.length > 0 ? 'pt-2 border-t border-white/[0.06]' : 'pt-2'}`}>Tags</li>
|
||||
<li role="presentation" className={`px-3 pb-1 text-[10px] font-semibold uppercase tracking-wider text-white/35 select-none ${artworks.length > 0 || groups.length > 0 || users.length > 0 ? 'pt-2 border-t border-white/[0.06]' : 'pt-2'}`}>Tags</li>
|
||||
{tags.map((tag, j) => {
|
||||
const flatIdx = artworks.length + users.length + j
|
||||
const flatIdx = artworks.length + groups.length + users.length + j
|
||||
return (
|
||||
<li key={tag.slug ?? tag.name ?? j} role="option" id={`sb-mobile-item-${flatIdx}`} aria-selected={activeIdx === flatIdx}>
|
||||
<button
|
||||
@@ -179,7 +210,7 @@ export default function SearchOverlay({
|
||||
</ul>
|
||||
) : (
|
||||
<div className="mx-2 my-2 px-6 py-10 rounded-xl border border-white/[0.10] bg-nova-900/95 backdrop-blur-sm shadow-2xl text-sm text-white/60">
|
||||
{query.trim().length >= 2 ? 'No results found.' : 'Start typing to search artworks, creators, and tags.'}
|
||||
{query.trim().length >= 2 ? 'No results found.' : 'Start typing to search artworks, groups, creators, and tags.'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user