Replace native selects with NovaSelect

This commit is contained in:
2026-05-01 07:45:37 +02:00
parent 67be537c86
commit 35011001ba
55 changed files with 3136 additions and 1662 deletions

View File

@@ -2,6 +2,7 @@ import React, { startTransition, useDeferredValue, useEffect, useRef, useState }
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' },
@@ -13,6 +14,26 @@ 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 (
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
@@ -113,17 +134,21 @@ function syncQueryState({ page, sort, 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)
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(() => getInitialSearchQuery())
const [sort, setSort] = useState(() => getInitialSort())
const [currentPage, setCurrentPage] = useState(() => getInitialPage())
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)
@@ -199,6 +224,20 @@ function CategoriesPage({ apiUrl = '/api/categories', pageTitle = 'Categories',
}
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({
@@ -334,24 +373,19 @@ function CategoriesPage({ apiUrl = '/api/categories', pageTitle = 'Categories',
/>
</label>
<label className="block">
<div className="block">
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.2em] text-white/38">Sort by</span>
<select
<NovaSelect
value={sort}
onChange={(event) => {
setSort(event.target.value)
onChange={(value) => {
setSort(value)
setCurrentPage(1)
}}
aria-label="Sort categories"
className="h-14 w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 text-sm text-white focus:border-orange-300/45 focus:outline-none focus:ring-2 focus:ring-orange-300/12"
>
{SORT_OPTIONS.map((option) => (
<option key={option.value} value={option.value} className="bg-slate-950 text-white">
{option.label}
</option>
))}
</select>
</label>
id="categories-sort"
options={SORT_OPTIONS.map((option) => ({ value: option.value, label: option.label }))}
searchable={false}
/>
</div>
</div>
</div>
</div>
@@ -438,6 +472,7 @@ function CategoriesPage({ apiUrl = '/api/categories', pageTitle = 'Categories',
)
}
if (typeof document !== 'undefined') {
const mountElement = document.getElementById('categories-page-root')
if (mountElement) {
@@ -452,5 +487,6 @@ if (mountElement) {
createRoot(mountElement).render(<CategoriesPage {...props} />)
}
}
export default CategoriesPage