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 (

No saved collections yet

Save collections to build a personal reference library for inspiration, campaigns, and creators you want to revisit.

Browse collections
) } function FilterLink({ href, active, children, count }) { return ( {children} {typeof count === 'number' ? {count} : null} ) } 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 ( <> {seo?.title || 'Saved Collections — Skinbase Nova'} {seo?.canonical ? : null} {seo?.canonical ? : null} {listSchema ? : null}