Files
SkinbaseNova/resources/js/Pages/Collection/SavedCollections.jsx
2026-03-28 19:15:39 +01:00

543 lines
30 KiB
JavaScript

import React from 'react'
import { Head, usePage } from '@inertiajs/react'
import CollectionCard from '../../components/profile/collections/CollectionCard'
function getCsrfToken() {
if (typeof document === 'undefined') return ''
return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
}
async function requestJson(url, { method = 'POST', body } = {}) {
const response = await fetch(url, {
method,
credentials: 'same-origin',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'X-CSRF-TOKEN': getCsrfToken(),
'X-Requested-With': 'XMLHttpRequest',
},
body: body ? JSON.stringify(body) : undefined,
})
const payload = await response.json().catch(() => ({}))
if (!response.ok) {
throw new Error(payload?.message || 'Request failed.')
}
return payload
}
function buildFilterUrl(next, baseUrl) {
if (typeof window === 'undefined' && !baseUrl) return '#'
const url = new URL(baseUrl || window.location.href, window.location.origin)
Object.entries(next).forEach(([key, value]) => {
if (value === null || value === undefined || value === '' || value === 'all') {
url.searchParams.delete(key)
} else {
url.searchParams.set(key, String(value))
}
})
return `${url.pathname}?${url.searchParams.toString()}`.replace(/\?$/, '')
}
function buildSearchActionUrl(baseUrl) {
if (typeof window === 'undefined' && !baseUrl) return '#'
const url = new URL(baseUrl || window.location.href, window.location.origin)
url.searchParams.delete('q')
return `${url.pathname}?${url.searchParams.toString()}`.replace(/\?$/, '')
}
function reorderCollectionIds(collections, collectionId, direction) {
const currentIndex = collections.findIndex((item) => Number(item.id) === Number(collectionId))
if (currentIndex === -1) return null
const targetIndex = direction === 'up' ? currentIndex - 1 : currentIndex + 1
if (targetIndex < 0 || targetIndex >= collections.length) return null
const nextCollections = [...collections]
const [movedCollection] = nextCollections.splice(currentIndex, 1)
nextCollections.splice(targetIndex, 0, movedCollection)
return nextCollections.map((item) => Number(item.id))
}
function EmptyState({ browseUrl }) {
return (
<div className="rounded-[32px] border border-dashed border-white/12 bg-white/[0.03] px-6 py-16 text-center">
<div className="mx-auto flex h-20 w-20 items-center justify-center rounded-[24px] border border-white/12 bg-white/[0.05] text-slate-400">
<i className="fa-solid fa-bookmark text-3xl" />
</div>
<h2 className="mt-5 text-2xl font-semibold text-white">No saved collections yet</h2>
<p className="mx-auto mt-3 max-w-xl text-sm leading-relaxed text-slate-300">
Save collections to build a personal reference library for inspiration, campaigns, and creators you want to revisit.
</p>
<div className="mt-6 flex justify-center">
<a href={browseUrl} className="inline-flex items-center gap-2 rounded-2xl border border-white/12 bg-white/[0.05] px-5 py-3 text-sm font-semibold text-white transition hover:bg-white/[0.08]">
<i className="fa-solid fa-compass fa-fw" />
Browse collections
</a>
</div>
</div>
)
}
function FilterLink({ href, active, children, count }) {
return (
<a href={href} className={`flex items-center justify-between rounded-2xl border px-4 py-3 text-sm transition ${active ? 'border-sky-300/20 bg-sky-400/10 text-sky-100' : 'border-white/10 bg-white/[0.04] text-white hover:bg-white/[0.07]'}`}>
<span>{children}</span>
{typeof count === 'number' ? <span className={`text-xs ${active ? 'text-sky-100/80' : 'text-slate-400'}`}>{count}</span> : null}
</a>
)
}
function formatDateTime(value) {
if (!value) return null
const date = new Date(value)
if (Number.isNaN(date.getTime())) return null
return date.toLocaleString()
}
export default function SavedCollections() {
const { props } = usePage()
const seo = props.seo || {}
const initialCollections = Array.isArray(props.collections) ? props.collections : []
const recentlyRevisited = Array.isArray(props.recentlyRevisited) ? props.recentlyRevisited : []
const recommendedCollections = Array.isArray(props.recommendedCollections) ? props.recommendedCollections : []
const browseUrl = props.browseUrl || '/collections/featured'
const libraryUrl = props.libraryUrl || '/me/saved/collections'
const activeFilters = props.activeFilters || { q: '', filter: 'all', sort: 'saved_desc', list: null }
const activeList = props.activeList || null
const filterOptions = Array.isArray(props.filterOptions) ? props.filterOptions : []
const sortOptions = Array.isArray(props.sortOptions) ? props.sortOptions : []
const searchBaseUrl = activeList?.url || libraryUrl
const [collections, setCollections] = React.useState(initialCollections)
const [savedLists, setSavedLists] = React.useState(Array.isArray(props.savedLists) ? props.savedLists : [])
const [newListTitle, setNewListTitle] = React.useState('')
const [selectedLists, setSelectedLists] = React.useState({})
const [notes, setNotes] = React.useState(() => Object.fromEntries(initialCollections.map((collection) => [collection.id, collection.saved_note || ''])))
const [search, setSearch] = React.useState(activeFilters.q || '')
const [notice, setNotice] = React.useState('')
const [busy, setBusy] = React.useState('')
React.useEffect(() => {
setCollections(initialCollections)
setNotes(Object.fromEntries(initialCollections.map((collection) => [collection.id, collection.saved_note || ''])))
}, [initialCollections])
React.useEffect(() => {
setSearch(activeFilters.q || '')
}, [activeFilters.q])
React.useEffect(() => {
setSavedLists(Array.isArray(props.savedLists) ? props.savedLists : [])
}, [props.savedLists])
const listSchema = seo?.canonical ? {
'@context': 'https://schema.org',
'@type': 'CollectionPage',
name: 'Saved collections',
description: seo?.description || 'Your saved collections on Skinbase Nova.',
url: seo.canonical,
mainEntity: {
'@type': 'ItemList',
numberOfItems: collections.length,
itemListElement: collections.slice(0, 18).map((collection, index) => ({
'@type': 'ListItem',
position: index + 1,
url: collection.url,
name: collection.title,
})),
},
} : null
async function handleCreateList(event) {
event.preventDefault()
if (!newListTitle.trim() || !props.endpoints?.createList) return
setBusy('create-list')
setNotice('')
try {
const payload = await requestJson(props.endpoints.createList, {
method: 'POST',
body: { title: newListTitle.trim() },
})
setSavedLists((current) => [...current, payload.list].sort((left, right) => String(left.title).localeCompare(String(right.title))))
setNewListTitle('')
setNotice('Saved list created.')
} catch (error) {
setNotice(error.message || 'Failed to create list.')
} finally {
setBusy('')
}
}
async function handleAddToList(collectionId) {
const listId = selectedLists[collectionId] || savedLists[0]?.id
if (!listId || !props.endpoints?.addToListPattern) return
setBusy(`list-${collectionId}`)
setNotice('')
try {
const payload = await requestJson(props.endpoints.addToListPattern.replace('__COLLECTION__', String(collectionId)), {
method: 'POST',
body: { saved_list_id: Number(listId) },
})
setSavedLists((current) => current.map((list) => (
Number(list.id) === Number(listId)
? { ...list, items_count: Number(payload?.list?.items_count || list.items_count || 0) }
: list
)))
setNotice(payload?.added ? 'Collection added to saved list.' : 'Collection already exists in that saved list.')
} catch (error) {
setNotice(error.message || 'Failed to add collection to list.')
} finally {
setBusy('')
}
}
async function handleUnsave(collection) {
if (!collection?.id || !props.endpoints?.unsavePattern) return
setBusy(`unsave-${collection.id}`)
setNotice('')
try {
await requestJson(props.endpoints.unsavePattern.replace('__COLLECTION__', String(collection.id)), {
method: 'DELETE',
})
setCollections((current) => current.filter((item) => Number(item.id) !== Number(collection.id)))
setSavedLists((current) => current.map((list) => {
const isMember = Array.isArray(collection.saved_list_ids) && collection.saved_list_ids.includes(Number(list.id))
return isMember
? { ...list, items_count: Math.max(0, Number(list.items_count || 0) - 1) }
: list
}))
setNotice('Collection removed from your saved library.')
} catch (error) {
setNotice(error.message || 'Failed to remove collection from your saved library.')
} finally {
setBusy('')
}
}
async function handleRemoveFromList(collection) {
if (!activeList?.id || !collection?.id || !props.endpoints?.removeFromListPattern) return
setBusy(`remove-${collection.id}`)
setNotice('')
try {
const payload = await requestJson(
props.endpoints.removeFromListPattern
.replace('__LIST__', String(activeList.id))
.replace('__COLLECTION__', String(collection.id)),
{ method: 'DELETE' },
)
setCollections((current) => current.filter((item) => Number(item.id) !== Number(collection.id)))
setSavedLists((current) => current.map((list) => (
Number(list.id) === Number(payload?.list?.id)
? { ...list, items_count: Number(payload?.list?.items_count || 0) }
: list
)))
setNotice('Collection removed from this saved list.')
} catch (error) {
setNotice(error.message || 'Failed to remove collection from this saved list.')
} finally {
setBusy('')
}
}
async function handleReorderCollection(collectionId, direction) {
if (!activeList?.id || !props.endpoints?.reorderItemsPattern) return
const reorderedCollectionIds = reorderCollectionIds(collections, collectionId, direction)
if (!reorderedCollectionIds) return
setBusy(`reorder-${collectionId}`)
setNotice('')
try {
await requestJson(
props.endpoints.reorderItemsPattern.replace('__LIST__', String(activeList.id)),
{
method: 'POST',
body: { collection_ids: reorderedCollectionIds },
},
)
setCollections((current) => {
const order = new Map(reorderedCollectionIds.map((id, index) => [Number(id), index]))
return [...current].sort((left, right) => {
const leftOrder = order.get(Number(left.id)) ?? Number.MAX_SAFE_INTEGER
const rightOrder = order.get(Number(right.id)) ?? Number.MAX_SAFE_INTEGER
return leftOrder - rightOrder
})
})
setNotice('Saved list order updated.')
} catch (error) {
setNotice(error.message || 'Failed to update saved list order.')
} finally {
setBusy('')
}
}
async function handleSaveNote(collectionId) {
if (!props.endpoints?.updateNotePattern) return
setBusy(`note-${collectionId}`)
setNotice('')
try {
const payload = await requestJson(
props.endpoints.updateNotePattern.replace('__COLLECTION__', String(collectionId)),
{
method: 'PATCH',
body: { note: notes[collectionId] || '' },
},
)
setCollections((current) => current.map((collection) => (
Number(collection.id) === Number(collectionId)
? { ...collection, saved_note: payload?.note?.note || null }
: collection
)))
setNotice(payload?.note ? 'Saved note updated.' : 'Saved note removed.')
} catch (error) {
setNotice(error.message || 'Failed to update saved note.')
} finally {
setBusy('')
}
}
return (
<>
<Head>
<title>{seo?.title || 'Saved Collections — Skinbase Nova'}</title>
<meta name="description" content={seo?.description || 'Your saved collections on Skinbase Nova.'} />
{seo?.canonical ? <link rel="canonical" href={seo.canonical} /> : null}
<meta name="robots" content={seo?.robots || 'noindex,follow'} />
<meta property="og:title" content={seo?.title || 'Saved Collections — Skinbase Nova'} />
<meta property="og:description" content={seo?.description || 'Your saved collections on Skinbase Nova.'} />
{seo?.canonical ? <meta property="og:url" content={seo.canonical} /> : null}
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content={seo?.title || 'Saved Collections — Skinbase Nova'} />
<meta name="twitter:description" content={seo?.description || 'Your saved collections on Skinbase Nova.'} />
{listSchema ? <script type="application/ld+json">{JSON.stringify(listSchema)}</script> : null}
</Head>
<div className="relative min-h-screen overflow-hidden pb-16">
<div aria-hidden="true" className="pointer-events-none absolute inset-x-0 top-0 -z-10 h-[34rem] opacity-95" style={{ background: 'radial-gradient(circle at 15% 14%, rgba(245,158,11,0.16), transparent 26%), radial-gradient(circle at 82% 18%, rgba(56,189,248,0.16), transparent 24%), linear-gradient(180deg, #07101d 0%, #0a1220 42%, #08111f 100%)' }} />
<div aria-hidden="true" className="pointer-events-none absolute inset-0 -z-10 opacity-[0.05]" style={{ backgroundImage: 'url(/gfx/noise.png)', backgroundSize: '180px' }} />
<div className="mx-auto max-w-7xl px-4 pt-8 md:px-6">
<div className="flex flex-wrap items-center gap-3 text-sm text-slate-300">
<a href={browseUrl} className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 transition hover:bg-white/[0.07] hover:text-white">
<i className="fa-solid fa-arrow-left fa-fw text-[11px]" />
Browse collections
</a>
</div>
<section className="mt-6 overflow-hidden rounded-[34px] border border-white/10 bg-white/[0.04] p-6 shadow-[0_30px_90px_rgba(2,6,23,0.28)] backdrop-blur-sm md:p-8">
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-amber-200/80">Library</p>
<h1 className="mt-3 text-4xl font-semibold tracking-[-0.05em] text-white md:text-5xl">Saved collections</h1>
<p className="mt-4 max-w-3xl text-sm leading-relaxed text-slate-300 md:text-[15px]">
A personal shortlist of collections worth revisiting. Organize them into saved lists, pivot by editorial or campaign relevance, and keep a working shelf of what should influence your next publish.
</p>
{activeList ? <p className="mt-4 inline-flex items-center gap-2 rounded-full border border-sky-300/20 bg-sky-400/10 px-4 py-2 text-sm font-semibold text-sky-100"><i className="fa-solid fa-folder-open fa-fw" />Viewing list: {activeList.title}</p> : null}
{activeFilters.q ? <p className="mt-4 inline-flex items-center gap-2 rounded-full border border-amber-300/20 bg-amber-400/10 px-4 py-2 text-sm font-semibold text-amber-100"><i className="fa-solid fa-magnifying-glass fa-fw" />Search: {activeFilters.q}</p> : null}
{notice ? <p className="mt-4 text-sm text-sky-100">{notice}</p> : null}
</section>
<section className="mt-8 grid gap-6 xl:grid-cols-[320px_minmax(0,1fr)]">
<aside className="space-y-5">
<section className="rounded-[30px] border border-white/10 bg-white/[0.04] p-5 backdrop-blur-sm">
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/80">Search</p>
<form method="GET" action={buildSearchActionUrl(searchBaseUrl)} className="mt-4 space-y-3">
<input name="q" value={search} onChange={(event) => setSearch(event.target.value)} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35" placeholder="Search titles, notes context, or curator" maxLength={120} />
{activeFilters.filter && activeFilters.filter !== 'all' ? <input type="hidden" name="filter" value={activeFilters.filter} /> : null}
{activeFilters.sort && activeFilters.sort !== 'saved_desc' ? <input type="hidden" name="sort" value={activeFilters.sort} /> : null}
<div className="flex gap-3">
<button type="submit" className="inline-flex flex-1 items-center justify-center gap-2 rounded-2xl border border-sky-300/20 bg-sky-400/10 px-4 py-3 text-sm font-semibold text-sky-100 transition hover:bg-sky-400/15"><i className="fa-solid fa-magnifying-glass fa-fw" />Apply</button>
{(activeFilters.q || search) ? <a href={buildFilterUrl({ q: null }, searchBaseUrl)} className="inline-flex items-center justify-center 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.07]">Clear</a> : null}
</div>
</form>
</section>
<section className="rounded-[30px] border border-white/10 bg-white/[0.04] p-5 backdrop-blur-sm">
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/80">Filters</p>
<div className="mt-4 space-y-3">
{filterOptions.map((option) => (
<FilterLink
key={option.key}
href={buildFilterUrl({ q: activeFilters.q, filter: option.key, sort: activeFilters.sort }, searchBaseUrl)}
active={activeFilters.filter === option.key}
count={option.count}
>
{option.label}
</FilterLink>
))}
</div>
</section>
<section className="rounded-[30px] border border-white/10 bg-white/[0.04] p-5 backdrop-blur-sm">
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/80">Sort</p>
<div className="mt-4 space-y-3">
{sortOptions.map((option) => (
<FilterLink
key={option.key}
href={buildFilterUrl({ q: activeFilters.q, filter: activeFilters.filter, sort: option.key }, searchBaseUrl)}
active={activeFilters.sort === option.key}
>
{option.label}
</FilterLink>
))}
</div>
</section>
<section className="rounded-[30px] border border-white/10 bg-white/[0.04] p-5 backdrop-blur-sm">
<div className="flex items-center justify-between gap-3">
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/80">Saved Lists</p>
<span className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-xs font-semibold text-slate-300">{savedLists.length}</span>
</div>
<div className="mt-4 space-y-3">
<FilterLink href={buildFilterUrl({ q: activeFilters.q, filter: activeFilters.filter, sort: activeFilters.sort }, libraryUrl)} active={!activeFilters.list}>All saved collections</FilterLink>
{savedLists.map((list) => (
<FilterLink key={list.id} href={buildFilterUrl({ q: activeFilters.q, filter: activeFilters.filter, sort: activeFilters.sort }, list.url || libraryUrl)} active={Number(activeFilters.list) === Number(list.id)} count={list.items_count}>{list.title}</FilterLink>
))}
</div>
<form onSubmit={handleCreateList} className="mt-5 space-y-3">
<input value={newListTitle} onChange={(event) => setNewListTitle(event.target.value)} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35" placeholder="Create a saved list" maxLength={120} />
<button type="submit" disabled={busy === 'create-list'} className="inline-flex w-full items-center justify-center gap-2 rounded-2xl border border-sky-300/20 bg-sky-400/10 px-4 py-3 text-sm font-semibold text-sky-100 transition hover:bg-sky-400/15 disabled:opacity-60"><i className={`fa-solid ${busy === 'create-list' ? 'fa-circle-notch fa-spin' : 'fa-folder-plus'} fa-fw`} />Create list</button>
</form>
</section>
</aside>
<div className="space-y-8">
{recentlyRevisited.length ? (
<section className="rounded-[32px] border border-white/10 bg-white/[0.04] p-6 backdrop-blur-sm md:p-7">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/80">Recently Revisited</p>
<h2 className="mt-2 text-2xl font-semibold text-white">Jump back into active references</h2>
</div>
<span className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-xs font-semibold text-slate-300">{recentlyRevisited.length}</span>
</div>
<div className="mt-6 grid grid-cols-1 gap-5 xl:grid-cols-3">
{recentlyRevisited.map((collection) => (
<CollectionCard key={`revisited-${collection.id}`} collection={collection} isOwner={false} />
))}
</div>
</section>
) : null}
<section>
{collections.length ? (
<div className="grid grid-cols-1 gap-5 xl:grid-cols-2">
{collections.map((collection, index) => (
<div key={collection.id} className="space-y-3">
<CollectionCard collection={collection} isOwner={false} />
{(collection.saved_because || collection.last_viewed_at) ? (
<div className="flex flex-wrap items-center gap-3 rounded-[24px] border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-slate-300">
{collection.saved_because ? <span className="inline-flex items-center gap-2 rounded-full border border-amber-300/20 bg-amber-400/10 px-3 py-2 text-xs font-semibold text-amber-100"><i className="fa-solid fa-lightbulb fa-fw" />{collection.saved_because}</span> : null}
{collection.last_viewed_at ? <span className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-[#0d1726] px-3 py-2 text-xs font-semibold text-slate-200"><i className="fa-solid fa-clock-rotate-left fa-fw" />Last revisited {formatDateTime(collection.last_viewed_at)}</span> : null}
</div>
) : null}
<div className="flex flex-wrap items-center gap-3 rounded-[24px] border border-white/10 bg-white/[0.04] px-4 py-3">
<button type="button" onClick={() => handleUnsave(collection)} disabled={busy === `unsave-${collection.id}`} className="inline-flex items-center gap-2 rounded-2xl border border-rose-300/20 bg-rose-400/10 px-4 py-2.5 text-sm font-semibold text-rose-100 transition hover:bg-rose-400/15 disabled:opacity-60"><i className={`fa-solid ${busy === `unsave-${collection.id}` ? 'fa-circle-notch fa-spin' : 'fa-bookmark-slash'} fa-fw`} />Remove from saved</button>
{activeList ? <button type="button" onClick={() => handleRemoveFromList(collection)} disabled={busy === `remove-${collection.id}`} className="inline-flex items-center gap-2 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-2.5 text-sm font-semibold text-white transition hover:bg-white/[0.07] disabled:opacity-60"><i className={`fa-solid ${busy === `remove-${collection.id}` ? 'fa-circle-notch fa-spin' : 'fa-folder-minus'} fa-fw`} />Remove from list</button> : null}
{activeList && collections.length > 1 ? (
<div className="ml-auto inline-flex items-center gap-2 rounded-2xl border border-white/10 bg-[#0d1726] p-1">
<button
type="button"
onClick={() => handleReorderCollection(collection.id, 'up')}
disabled={index === 0 || busy === `reorder-${collection.id}`}
className="inline-flex h-10 w-10 items-center justify-center rounded-xl text-sm text-white transition hover:bg-white/[0.08] disabled:cursor-not-allowed disabled:opacity-40"
aria-label={`Move ${collection.title} up`}
>
<i className={`fa-solid ${busy === `reorder-${collection.id}` ? 'fa-circle-notch fa-spin' : 'fa-arrow-up'} fa-fw`} />
</button>
<button
type="button"
onClick={() => handleReorderCollection(collection.id, 'down')}
disabled={index === collections.length - 1 || busy === `reorder-${collection.id}`}
className="inline-flex h-10 w-10 items-center justify-center rounded-xl text-sm text-white transition hover:bg-white/[0.08] disabled:cursor-not-allowed disabled:opacity-40"
aria-label={`Move ${collection.title} down`}
>
<i className={`fa-solid ${busy === `reorder-${collection.id}` ? 'fa-circle-notch fa-spin' : 'fa-arrow-down'} fa-fw`} />
</button>
</div>
) : null}
</div>
{savedLists.length ? (
<div className="flex flex-wrap items-center gap-3 rounded-[24px] border border-white/10 bg-white/[0.04] px-4 py-3">
<select value={selectedLists[collection.id] || savedLists[0]?.id || ''} onChange={(event) => setSelectedLists((current) => ({ ...current, [collection.id]: event.target.value }))} className="min-w-[180px] rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-2.5 text-sm text-white outline-none">
{savedLists.map((list) => <option key={list.id} value={list.id}>{list.title}</option>)}
</select>
<button type="button" onClick={() => handleAddToList(collection.id)} disabled={busy === `list-${collection.id}`} className="inline-flex items-center gap-2 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-2.5 text-sm font-semibold text-white transition hover:bg-white/[0.07] disabled:opacity-60"><i className={`fa-solid ${busy === `list-${collection.id}` ? 'fa-circle-notch fa-spin' : 'fa-folder-plus'} fa-fw`} />Add to list</button>
</div>
) : null}
<div className="rounded-[24px] border border-white/10 bg-white/[0.04] px-4 py-4">
<div className="flex items-center justify-between gap-3">
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400">Private Note</p>
<button type="button" onClick={() => handleSaveNote(collection.id)} disabled={busy === `note-${collection.id}`} className="inline-flex items-center gap-2 rounded-2xl border border-sky-300/20 bg-sky-400/10 px-4 py-2 text-xs font-semibold text-sky-100 transition hover:bg-sky-400/15 disabled:opacity-60"><i className={`fa-solid ${busy === `note-${collection.id}` ? 'fa-circle-notch fa-spin' : 'fa-note-sticky'} fa-fw`} />Save note</button>
</div>
<textarea
value={notes[collection.id] || ''}
onChange={(event) => setNotes((current) => ({ ...current, [collection.id]: event.target.value }))}
rows={3}
maxLength={1000}
placeholder="Why did you save this collection? Add campaign context, inspiration notes, or follow-up ideas."
className="mt-3 w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-sm text-white outline-none transition focus:border-sky-300/35"
/>
</div>
</div>
))}
</div>
) : (
activeFilters.q || activeFilters.filter !== 'all' || activeFilters.sort !== 'saved_desc' || activeFilters.list
? <div className="rounded-[32px] border border-dashed border-white/12 bg-white/[0.03] px-6 py-16 text-center text-sm text-slate-300">No saved collections match the current search or filters.</div>
: <EmptyState browseUrl={browseUrl} />
)}
</section>
{recommendedCollections.length ? (
<section className="rounded-[32px] border border-white/10 bg-white/[0.04] p-6 backdrop-blur-sm md:p-7">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-amber-200/80">Recommended Next</p>
<h2 className="mt-2 text-2xl font-semibold text-white">Because of what you save</h2>
</div>
<span className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-xs font-semibold text-slate-300">{recommendedCollections.length}</span>
</div>
<div className="mt-6 grid grid-cols-1 gap-5 xl:grid-cols-3">
{recommendedCollections.map((collection) => (
<CollectionCard key={collection.id} collection={collection} isOwner={false} />
))}
</div>
</section>
) : null}
</div>
</section>
</div>
</div>
</>
)
}