feat: Journal list, category pages, pagination, tokens and pl/en strings
- index and category pages paginate with ?page=N and 404 out of range - category content renders above the list through the html gate - related posts fall back to the latest posts - --journal-* tokens and neutral base styles - pl/en strings registered only for locales the app enables
This commit is contained in:
36
app/utils/pagination.ts
Normal file
36
app/utils/pagination.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
// Page-number rules for Journal listings. Pages use ?page=N (never a path
|
||||
// segment). Anything but a positive integer, and any page past the last one,
|
||||
// is a real 404.
|
||||
|
||||
/**
|
||||
* Parses the ?page query value: 1 when absent, the integer for a string of
|
||||
* digits greater than 0, and null (a 404) for anything else: non-numeric,
|
||||
* empty, 0, negative, decimal, repeated (array) or beyond the safe range.
|
||||
*/
|
||||
export function parsePageParam(raw: unknown): number | null {
|
||||
if (raw === undefined || raw === null) return 1
|
||||
if (typeof raw !== 'string' || !/^\d+$/.test(raw)) return null
|
||||
const n = Number(raw)
|
||||
if (!Number.isSafeInteger(n) || n < 1) return null
|
||||
return n
|
||||
}
|
||||
|
||||
/**
|
||||
* True when `page` lies past the last page. Page 1 is always in range, so an
|
||||
* empty listing renders its empty state instead of a 404.
|
||||
*/
|
||||
export function pageOutOfRange(page: number, lastPage: number): boolean {
|
||||
return page > 1 && page > lastPage
|
||||
}
|
||||
|
||||
/**
|
||||
* The page numbers to render around `current`: at most `size` consecutive
|
||||
* pages, clipped to 1..last and kept full-width near either end.
|
||||
*/
|
||||
export function paginationWindow(current: number, last: number, size: number = 5): number[] {
|
||||
if (last < 1 || size < 1) return []
|
||||
const width = Math.min(size, last)
|
||||
let start = Math.max(1, current - Math.floor(width / 2))
|
||||
if (start + width - 1 > last) start = last - width + 1
|
||||
return Array.from({ length: width }, (_, i) => start + i)
|
||||
}
|
||||
@@ -16,6 +16,22 @@ export interface JournalPostView extends JournalPostDetail {
|
||||
content_blocked: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats an API timestamp as a long date for `language` (a BCP 47 tag such
|
||||
* as "pl-PL"). UTC on both server and browser, so hydration matches. Returns
|
||||
* "" for a missing or invalid value.
|
||||
*/
|
||||
export function formatJournalDate(iso: string | null | undefined, language: string): string {
|
||||
if (!iso) return ''
|
||||
const d = new Date(iso)
|
||||
if (Number.isNaN(d.getTime())) return ''
|
||||
try {
|
||||
return new Intl.DateTimeFormat(language || 'en', { dateStyle: 'long', timeZone: 'UTC' }).format(d)
|
||||
} catch {
|
||||
return new Intl.DateTimeFormat('en', { dateStyle: 'long', timeZone: 'UTC' }).format(d)
|
||||
}
|
||||
}
|
||||
|
||||
export function toPostView(post: JournalPostDetail): JournalPostView {
|
||||
const gated = gateJournalHtml(post.content_html)
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user