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' 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 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 = '' }) { const [categories, setCategories] = useState([]) const [popularCategories, setPopularCategories] = useState([]) const [meta, setMeta] = useState({ current_page: 1, last_page: 1, per_page: PAGE_SIZE, total: 0 }) const [summary, setSummary] = useState({ total_categories: 0, total_artworks: 0 }) const [loading, setLoading] = useState(true) const [loadingMore, setLoadingMore] = useState(false) const [error, setError] = useState(false) const [searchQuery, setSearchQuery] = useState(() => getInitialSearchQuery()) const [sort, setSort] = useState(() => getInitialSort()) const [currentPage, setCurrentPage] = useState(() => 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 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

{!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) => ( ))}
) } 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