Forum: - TipTap WYSIWYG editor with full toolbar - @emoji-mart/react emoji picker (consistent with tweets) - @mention autocomplete with user search API - Fix PHP 8.4 parse errors in Blade templates - Fix thread data display (paginator items) - Align forum page widths to max-w-5xl Discover: - Extract shared _nav.blade.php partial - Add missing nav links to for-you page - Add Following link for authenticated users Feed/Posts: - Post model, controllers, policies, migrations - Feed page components (PostComposer, FeedCard, etc) - Post reactions, comments, saves, reports, sharing - Scheduled publishing support - Link preview controller Profile: - Profile page components (ProfileHero, ProfileTabs) - Profile API controller Uploads: - Upload wizard enhancements - Scheduled publish picker - Studio status bar and readiness checklist
94 lines
3.2 KiB
JavaScript
94 lines
3.2 KiB
JavaScript
import React from 'react'
|
|
|
|
/**
|
|
* Pagination control that mirrors Laravel's paginator shape.
|
|
* Expects: { current_page, last_page, per_page, total, path } or links array.
|
|
*/
|
|
export default function Pagination({ meta, onPageChange }) {
|
|
const current = meta?.current_page ?? 1
|
|
const lastPage = meta?.last_page ?? 1
|
|
|
|
if (lastPage <= 1) return null
|
|
|
|
const pages = buildPages(current, lastPage)
|
|
|
|
const go = (page) => {
|
|
if (page < 1 || page > lastPage || page === current) return
|
|
if (onPageChange) {
|
|
onPageChange(page)
|
|
} else {
|
|
// Fallback: navigate via URL
|
|
const url = new URL(window.location.href)
|
|
url.searchParams.set('page', page)
|
|
window.location.href = url.toString()
|
|
}
|
|
}
|
|
|
|
return (
|
|
<nav aria-label="Pagination" className="flex items-center justify-center gap-1.5">
|
|
{/* Prev */}
|
|
<button
|
|
onClick={() => go(current - 1)}
|
|
disabled={current <= 1}
|
|
className="inline-flex h-9 w-9 items-center justify-center rounded-xl border border-white/10 text-zinc-400 transition-colors hover:border-white/20 hover:text-white disabled:opacity-30 disabled:cursor-not-allowed"
|
|
aria-label="Previous page"
|
|
>
|
|
<svg width="14" height="14" viewBox="0 0 16 16" fill="none">
|
|
<path d="M10 4L6 8l4 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
</svg>
|
|
</button>
|
|
|
|
{pages.map((p, i) =>
|
|
p === '...' ? (
|
|
<span key={`dots-${i}`} className="px-1 text-xs text-zinc-600">…</span>
|
|
) : (
|
|
<button
|
|
key={p}
|
|
onClick={() => go(p)}
|
|
aria-current={p === current ? 'page' : undefined}
|
|
className={[
|
|
'inline-flex h-9 min-w-[2.25rem] items-center justify-center rounded-xl text-sm font-medium transition-colors',
|
|
p === current
|
|
? 'bg-sky-600/20 text-sky-300 border border-sky-500/30'
|
|
: 'border border-white/10 text-zinc-400 hover:border-white/20 hover:text-white',
|
|
].join(' ')}
|
|
>
|
|
{p}
|
|
</button>
|
|
)
|
|
)}
|
|
|
|
{/* Next */}
|
|
<button
|
|
onClick={() => go(current + 1)}
|
|
disabled={current >= lastPage}
|
|
className="inline-flex h-9 w-9 items-center justify-center rounded-xl border border-white/10 text-zinc-400 transition-colors hover:border-white/20 hover:text-white disabled:opacity-30 disabled:cursor-not-allowed"
|
|
aria-label="Next page"
|
|
>
|
|
<svg width="14" height="14" viewBox="0 0 16 16" fill="none">
|
|
<path d="M6 4l4 4-4 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
</svg>
|
|
</button>
|
|
</nav>
|
|
)
|
|
}
|
|
|
|
/** Build a compact page-number array with ellipsis. */
|
|
function buildPages(current, last) {
|
|
if (last <= 7) {
|
|
return Array.from({ length: last }, (_, i) => i + 1)
|
|
}
|
|
|
|
const pages = new Set([1, 2, current - 1, current, current + 1, last - 1, last])
|
|
const sorted = [...pages].filter(p => p >= 1 && p <= last).sort((a, b) => a - b)
|
|
|
|
const result = []
|
|
for (let i = 0; i < sorted.length; i++) {
|
|
if (i > 0 && sorted[i] - sorted[i - 1] > 1) {
|
|
result.push('...')
|
|
}
|
|
result.push(sorted[i])
|
|
}
|
|
return result
|
|
}
|