import React, { startTransition, useDeferredValue, useEffect, useRef, useState } from 'react' import { createRoot } from 'react-dom/client' import CategoryCard from '../components/category/CategoryCard' import Pagination from '../components/forum/Pagination' import NovaSelect from '../components/ui/NovaSelect' const SORT_OPTIONS = [ { value: 'popular', label: 'Popular' }, { value: 'az', label: 'A-Z' }, { value: 'artworks', label: 'Most artworks' }, ] const PAGE_SIZE = 24 const numberFormatter = new Intl.NumberFormat() function normalizeInitialData(initialData) { if (!initialData || typeof initialData !== 'object') { return { data: [], meta: { current_page: 1, last_page: 1, per_page: PAGE_SIZE, total: 0 }, summary: { total_categories: 0, total_artworks: 0 }, popular_categories: [], request: { query: '', sort: 'popular', page: 1 }, } } return { data: Array.isArray(initialData.data) ? initialData.data : [], meta: initialData.meta || { current_page: 1, last_page: 1, per_page: PAGE_SIZE, total: 0 }, summary: initialData.summary || { total_categories: 0, total_artworks: 0 }, popular_categories: Array.isArray(initialData.popular_categories) ? initialData.popular_categories : [], request: initialData.request || { query: '', sort: 'popular', page: 1 }, } } function LoadingGrid() { return (
{Array.from({ length: 8 }).map((_, index) => (
))}
) } function EmptyState({ query }) { return (

No matching categories

Nothing matched "{query}"

Try a shorter term or switch sorting to browse the full category directory again.

) } function ErrorState({ onRetry }) { return (

Unable to load categories

The directory API did not respond cleanly.

Refresh the list and try again. If this persists, the API route or cache payload needs inspection.

) } function getInitialPage() { if (typeof window === 'undefined') { return 1 } const rawPage = Number(new URL(window.location.href).searchParams.get('page') || 1) if (!Number.isFinite(rawPage) || rawPage < 1) { return 1 } return Math.floor(rawPage) } function getInitialSort() { if (typeof window === 'undefined') { return 'popular' } const sort = new URL(window.location.href).searchParams.get('sort') || 'popular' return SORT_OPTIONS.some((option) => option.value === sort) ? sort : 'popular' } function getInitialSearchQuery() { if (typeof window === 'undefined') { return '' } return new URL(window.location.href).searchParams.get('q') || '' } function syncQueryState({ page, sort, query }) { if (typeof window === 'undefined') { return } const url = new URL(window.location.href) if (page <= 1) { url.searchParams.delete('page') } else { url.searchParams.set('page', String(page)) } if (sort === 'popular') { url.searchParams.delete('sort') } else { url.searchParams.set('sort', sort) } if (query.trim() === '') { url.searchParams.delete('q') } else { url.searchParams.set('q', query) } window.history.replaceState({}, '', url.toString()) } function CategoriesPage({ apiUrl = '/api/categories', pageTitle = 'Categories', pageDescription = '', initialData = null }) { const bootstrap = normalizeInitialData(initialData) const hasInitialData = initialData !== null const initialRequestRef = useRef(hasInitialData ? bootstrap.request : null) const [categories, setCategories] = useState(() => (hasInitialData ? bootstrap.data : [])) const [popularCategories, setPopularCategories] = useState(() => (hasInitialData ? bootstrap.popular_categories : [])) const [meta, setMeta] = useState(() => (hasInitialData ? bootstrap.meta : { current_page: 1, last_page: 1, per_page: PAGE_SIZE, total: 0 })) const [summary, setSummary] = useState(() => (hasInitialData ? bootstrap.summary : { total_categories: 0, total_artworks: 0 })) const [loading, setLoading] = useState(() => !hasInitialData) const [loadingMore, setLoadingMore] = useState(false) const [error, setError] = useState(false) const [searchQuery, setSearchQuery] = useState(() => (hasInitialData ? (bootstrap.request.query || '') : getInitialSearchQuery())) const [sort, setSort] = useState(() => (hasInitialData ? (bootstrap.request.sort || 'popular') : getInitialSort())) const [currentPage, setCurrentPage] = useState(() => (hasInitialData ? (bootstrap.request.page || 1) : getInitialPage())) const deferredQuery = useDeferredValue(searchQuery) const sentinelRef = useRef(null) const loadCategories = async ({ signal, page, query, activeSort, append = false }) => { if (append) { setLoadingMore(true) } else { setLoading(true) } setError(false) try { const params = new URLSearchParams({ page: String(page), per_page: String(PAGE_SIZE), sort: activeSort, }) if (query.trim() !== '') { params.set('q', query.trim()) } const response = await fetch(`${apiUrl}?${params.toString()}`, { headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest', }, credentials: 'same-origin', signal, }) if (!response.ok) { throw new Error('Failed to load categories') } const payload = await response.json() const nextCategories = Array.isArray(payload?.data) ? payload.data : [] setCategories((previous) => { if (!append) { return nextCategories } const seenIds = new Set(previous.map((category) => category.id)) const merged = [...previous] nextCategories.forEach((category) => { if (!seenIds.has(category.id)) { merged.push(category) } }) return merged }) setPopularCategories(Array.isArray(payload?.popular_categories) ? payload.popular_categories : []) setMeta(payload?.meta || { current_page: 1, last_page: 1, per_page: PAGE_SIZE, total: 0 }) setSummary(payload?.summary || { total_categories: 0, total_artworks: 0 }) if ((payload?.meta?.current_page ?? page) !== currentPage) { setCurrentPage(payload?.meta?.current_page ?? page) } } catch (requestError) { if (requestError?.name !== 'AbortError') { setError(true) } } finally { if (!signal?.aborted || signal === undefined) { setLoading(false) setLoadingMore(false) } } } useEffect(() => { const initialRequest = initialRequestRef.current if (initialRequest) { const sameQuery = (initialRequest.query || '') === deferredQuery const sameSort = (initialRequest.sort || 'popular') === sort const samePage = Number(initialRequest.page || 1) === currentPage initialRequestRef.current = null if (sameQuery && sameSort && samePage) { return undefined } } const controller = new AbortController() void loadCategories({ signal: controller.signal, page: currentPage, query: deferredQuery, activeSort: sort, append: false, }) return () => controller.abort() }, [apiUrl, deferredQuery, sort]) useEffect(() => { syncQueryState({ page: currentPage, sort, query: deferredQuery }) }, [currentPage, deferredQuery, sort]) const handlePageChange = (page) => { setCategories([]) setCurrentPage(page) void loadCategories({ page, query: deferredQuery, activeSort: sort, append: false, }) if (typeof window !== 'undefined') { window.scrollTo({ top: 0, behavior: 'smooth' }) } } useEffect(() => { const sentinel = sentinelRef.current const hasMore = meta.current_page < meta.last_page if (!sentinel || loading || loadingMore || error || !hasMore) { return undefined } const observer = new IntersectionObserver((entries) => { const firstEntry = entries[0] if (!firstEntry?.isIntersecting) { return } const nextPage = meta.current_page + 1 setCurrentPage(nextPage) void loadCategories({ page: nextPage, query: deferredQuery, activeSort: sort, append: true, }) }, { rootMargin: '320px 0px' }) observer.observe(sentinel) return () => observer.disconnect() }, [deferredQuery, error, loading, loadingMore, meta.current_page, meta.last_page, sort]) const handleRetry = () => { void loadCategories({ page: currentPage, query: deferredQuery, activeSort: sort, append: false, }) } const loadedCount = categories.length const showingStart = loadedCount > 0 ? 1 : 0 const showingEnd = loadedCount const hasMorePages = meta.current_page < meta.last_page return (
Category directory

{pageTitle}

{pageDescription || 'Browse all wallpapers, skins, themes and digital art categories'}

Categories

{numberFormatter.format(summary.total_categories)}

Artworks indexed

{numberFormatter.format(summary.total_artworks)}

View

Grid

Sort by { setSort(value) setCurrentPage(1) }} id="categories-sort" options={SORT_OPTIONS.map((option) => ({ value: option.value, label: option.label }))} searchable={false} />
{!loading && !error && deferredQuery.trim() === '' && popularCategories.length > 0 && (

Popular categories

Start with the busiest destinations

)}

Directory results

{numberFormatter.format(meta.total)} categories visible

{!loading && !error && meta.total > 0 ? (

Showing {numberFormatter.format(showingStart)} to {numberFormatter.format(showingEnd)} of {numberFormatter.format(meta.total)} categories.

) : (

Browse all wallpapers, skins, themes and digital art categories.

)}
{loading && } {!loading && error && } {!loading && !error && meta.total === 0 && } {!loading && !error && meta.total > 0 && ( <>
{categories.map((category, index) => ( ))}
) } if (typeof document !== 'undefined') { const mountElement = document.getElementById('categories-page-root') if (mountElement) { let props = {} try { const propsElement = document.getElementById('categories-page-props') props = propsElement ? JSON.parse(propsElement.textContent || '{}') : {} } catch { props = {} } createRoot(mountElement).render() } } export default CategoriesPage