init elas atelier #1
@@ -1,6 +1,8 @@
|
|||||||
import { useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { deletePost } from '../../lib/api';
|
import { deletePost } from '../../lib/api';
|
||||||
|
|
||||||
|
const PAGE_SIZE = 9;
|
||||||
|
|
||||||
interface CoverImage {
|
interface CoverImage {
|
||||||
url: string;
|
url: string;
|
||||||
alt: string;
|
alt: string;
|
||||||
@@ -70,6 +72,8 @@ const LAYOUT_CYCLE: Array<{ col: number; aspect: string; tilt: number }> = [
|
|||||||
export default function PostList({ posts: initialPosts, isAdmin = false }: Props) {
|
export default function PostList({ posts: initialPosts, isAdmin = false }: Props) {
|
||||||
const [posts, setPosts] = useState(initialPosts);
|
const [posts, setPosts] = useState(initialPosts);
|
||||||
const [deleting, setDeleting] = useState<string | null>(null);
|
const [deleting, setDeleting] = useState<string | null>(null);
|
||||||
|
const [visible, setVisible] = useState(() => Math.min(PAGE_SIZE, initialPosts.length));
|
||||||
|
const sentinelRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
async function handleDelete(slug: string, title: string) {
|
async function handleDelete(slug: string, title: string) {
|
||||||
if (deleting) return;
|
if (deleting) return;
|
||||||
@@ -85,13 +89,37 @@ export default function PostList({ posts: initialPosts, isAdmin = false }: Props
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setVisible(v => Math.min(Math.max(v, PAGE_SIZE), posts.length));
|
||||||
|
}, [posts.length]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (visible >= posts.length) return;
|
||||||
|
const el = sentinelRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
const io = new IntersectionObserver(
|
||||||
|
entries => {
|
||||||
|
if (entries.some(e => e.isIntersecting)) {
|
||||||
|
setVisible(v => Math.min(v + PAGE_SIZE, posts.length));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ rootMargin: '600px 0px' },
|
||||||
|
);
|
||||||
|
io.observe(el);
|
||||||
|
return () => io.disconnect();
|
||||||
|
}, [visible, posts.length]);
|
||||||
|
|
||||||
if (posts.length === 0) {
|
if (posts.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const shown = posts.slice(0, visible);
|
||||||
|
const hasMore = visible < posts.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-8 md:gap-x-10 md:gap-y-16">
|
<div className="grid grid-cols-1 md:grid-cols-12 gap-8 md:gap-x-10 md:gap-y-16">
|
||||||
{posts.map((post, idx) => {
|
{shown.map((post, idx) => {
|
||||||
const displayTitle = post.title || formatSlug(post.slug);
|
const displayTitle = post.title || formatSlug(post.slug);
|
||||||
const isDeleting = deleting === post.slug;
|
const isDeleting = deleting === post.slug;
|
||||||
const layout = LAYOUT_CYCLE[idx % LAYOUT_CYCLE.length];
|
const layout = LAYOUT_CYCLE[idx % LAYOUT_CYCLE.length];
|
||||||
@@ -205,5 +233,15 @@ export default function PostList({ posts: initialPosts, isAdmin = false }: Props
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
{hasMore && (
|
||||||
|
<div
|
||||||
|
ref={sentinelRef}
|
||||||
|
className="mt-12 md:mt-16 flex items-center justify-center text-[var(--subtext0)] font-display italic text-sm tracking-[0.2em] uppercase"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<span className="opacity-60">arranging more…</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user