feat: forum rich-text editor, emoji picker, mentions, discover nav, feed, uploads, profile
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
This commit is contained in:
93
resources/js/components/forum/Pagination.jsx
Normal file
93
resources/js/components/forum/Pagination.jsx
Normal file
@@ -0,0 +1,93 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user