feat: pagination in the SPA for the Sites and Authors lists

This commit is contained in:
Florian Sylvain committed 2026-10-19 04:00:47 +02:00
1 parent 500ad59458
commit 340a0ad461
34 files changed
+2833 -896

No files matched your search

+18 -29
View File
@@ -1,19 +1,21 @@
import type { LocationQuery } from 'vue-router'
import { SITE_KEY_MAX } from '@/lib/directory'
import {
DEFAULT_PAGE_SIZE,
PAGE_SIZES,
SEARCH_MAX_LENGTH,
firstQueryValue,
parsePositiveInteger,
} from '@/lib/pageQuery'
export type ArticleStatus = 'online' | 'draft'
export const ARTICLE_PAGE_SIZES = [5, 25, 50, 100]
export const DEFAULT_ARTICLE_PAGE_SIZE = 25
export const ARTICLE_SEARCH_MAX_LENGTH = 200
export const DEFAULT_ARTICLE_SORT = 'updated_at:desc'
export const ARTICLE_SORT_FIELDS = ['title', 'status', 'created_at', 'updated_at'] as const
export type ArticleSortField = (typeof ARTICLE_SORT_FIELDS)[number]
export const SEO_FILTER = 'suggestions'
const SITE_KEY_MAX = 32
const POSITIVE_INTEGER = /^[1-9]\d*$/
export interface ArticleListQuery {
page: number
size: number
@@ -24,11 +26,6 @@ export interface ArticleListQuery {
seo: '' | typeof SEO_FILTER
}
function first(value: LocationQuery[string] | undefined) {
const single = Array.isArray(value) ? value[0] : value
return single ?? ''
}
const SORT_PATTERN = new RegExp(`^(${ARTICLE_SORT_FIELDS.join('|')}):(asc|desc)$`)
function validSort(value: string) {
@@ -45,17 +42,17 @@ export function nextSort(current: string, field: ArticleSortField) {
}
export function parseArticleQuery(query: LocationQuery): ArticleListQuery {
const page = first(query.page)
const size = Number(first(query.size))
const status = first(query.status)
const page = firstQueryValue(query.page)
const size = Number(firstQueryValue(query.size))
const status = firstQueryValue(query.status)
return {
page: POSITIVE_INTEGER.test(page) ? Number(page) : 1,
size: ARTICLE_PAGE_SIZES.includes(size) ? size : DEFAULT_ARTICLE_PAGE_SIZE,
q: first(query.q).slice(0, ARTICLE_SEARCH_MAX_LENGTH),
page: parsePositiveInteger(page) ?? 1,
size: PAGE_SIZES.includes(size) ? size : DEFAULT_PAGE_SIZE,
q: firstQueryValue(query.q).slice(0, SEARCH_MAX_LENGTH),
status: status === 'online' || status === 'draft' ? status : '',
sort: validSort(first(query.sort)),
site: first(query.site).slice(0, SITE_KEY_MAX),
seo: first(query.seo) === SEO_FILTER ? SEO_FILTER : '',
sort: validSort(firstQueryValue(query.sort)),
site: firstQueryValue(query.site).slice(0, SITE_KEY_MAX),
seo: firstQueryValue(query.seo) === SEO_FILTER ? SEO_FILTER : '',
}
}
@@ -64,7 +61,7 @@ export function toRouteQuery(query: ArticleListQuery) {
if (query.page !== 1) {
route.page = String(query.page)
}
if (query.size !== DEFAULT_ARTICLE_PAGE_SIZE) {
if (query.size !== DEFAULT_PAGE_SIZE) {
route.size = String(query.size)
}
if (query.q) {
@@ -96,11 +93,3 @@ export function sameArticleQuery(left: ArticleListQuery, right: ArticleListQuery
effectiveSort(left.sort) === effectiveSort(right.sort)
)
}
export function sameRouteQuery(left: LocationQuery, right: Record<string, string>) {
const leftKeys = Object.keys(left)
return (
leftKeys.length === Object.keys(right).length &&
leftKeys.every((key) => first(left[key]) === right[key])
)
}
+13 -18
View File
@@ -1,9 +1,11 @@
import type { LocationQuery } from 'vue-router'
import {
ARTICLE_PAGE_SIZES,
ARTICLE_SEARCH_MAX_LENGTH,
DEFAULT_ARTICLE_PAGE_SIZE,
} from '@/lib/articleListQuery'
DEFAULT_PAGE_SIZE,
PAGE_SIZES,
SEARCH_MAX_LENGTH,
firstQueryValue,
parsePositiveInteger,
} from '@/lib/pageQuery'
export interface DirectoryQuery {
q: string
@@ -11,20 +13,13 @@ export interface DirectoryQuery {
size: number
}
const POSITIVE_INTEGER = /^[1-9]\d*$/
function first(value: LocationQuery[string] | undefined) {
const single = Array.isArray(value) ? value[0] : value
return single ?? ''
}
export function parseDirectoryQuery(query: LocationQuery): DirectoryQuery {
const page = first(query.page)
const size = Number(first(query.size))
const page = firstQueryValue(query.page)
const size = Number(firstQueryValue(query.size))
return {
q: first(query.q).slice(0, ARTICLE_SEARCH_MAX_LENGTH),
page: POSITIVE_INTEGER.test(page) ? Number(page) : 1,
size: ARTICLE_PAGE_SIZES.includes(size) ? size : DEFAULT_ARTICLE_PAGE_SIZE,
q: firstQueryValue(query.q).slice(0, SEARCH_MAX_LENGTH),
page: parsePositiveInteger(page) ?? 1,
size: PAGE_SIZES.includes(size) ? size : DEFAULT_PAGE_SIZE,
}
}
@@ -32,7 +27,7 @@ export function toDirectoryRouteQuery(query: DirectoryQuery, rest: LocationQuery
const route: Record<string, string> = {}
for (const [key, value] of Object.entries(rest)) {
if (key !== 'q' && key !== 'page' && key !== 'size') {
route[key] = first(value)
route[key] = firstQueryValue(value)
}
}
if (query.q) {
@@ -41,7 +36,7 @@ export function toDirectoryRouteQuery(query: DirectoryQuery, rest: LocationQuery
if (query.page !== 1) {
route.page = String(query.page)
}
if (query.size !== DEFAULT_ARTICLE_PAGE_SIZE) {
if (query.size !== DEFAULT_PAGE_SIZE) {
route.size = String(query.size)
}
return route
+9
View File
@@ -5,3 +5,12 @@ export function jsonInit(method: string, body: unknown): RequestInit {
body: JSON.stringify(body),
}
}
export function directoryListPath(base: string, query: { q: string; page: number; size: number }) {
const parts = [
...(query.q ? [`q=${encodeURIComponent(query.q)}`] : []),
`page=${query.page}`,
`size=${query.size}`,
]
return `${base}?${parts.join('&')}`
}
+24
View File
@@ -0,0 +1,24 @@
import type { LocationQuery } from 'vue-router'
export const PAGE_SIZES = [5, 25, 50, 100]
export const DEFAULT_PAGE_SIZE = 25
export const SEARCH_MAX_LENGTH = 200
const POSITIVE_INTEGER = /^[1-9]\d*$/
export function firstQueryValue(value: LocationQuery[string] | undefined) {
const single = Array.isArray(value) ? value[0] : value
return single ?? ''
}
export function parsePositiveInteger(value: string) {
return POSITIVE_INTEGER.test(value) ? Number(value) : null
}
export function sameRouteQuery(left: LocationQuery, right: Record<string, string>) {
const leftKeys = Object.keys(left)
return (
leftKeys.length === Object.keys(right).length &&
leftKeys.every((key) => firstQueryValue(left[key]) === right[key])
)
}