Infinite Scroll
Load more records when the user reaches the end of the list. Backend uses
take
and
skip
(derived from
page
and
perPage
). Frontend observes a
sentinel with IntersectionObserver
.
Backend (route.php)
<?php
use Lib\Prisma\Classes\Prisma;
use Lib\Request;
$prisma = Prisma::getInstance();
$perPage = (int) (Request::$params->perPage ?? 5);
$page = max((int) (Request::$params->page ?? 1), 1);
$skip = ($page - 1) * $perPage;
$logs = $prisma->versionLog->findMany([
'orderBy' => ['createdAt' => 'desc'],
'take' => $perPage,
'skip' => $skip,
'select' => [
'id' => true,
'version' => true,
'createdAt' => true,
'title' => true,
'description'=> true,
],
]);
$nextPage = count($logs) === $perPage ? $page + 1 : null;
echo json_encode([
'data' => $logs,
'nextPage' => $nextPage,
]);
Initial Frontend (index.php)
<?php
use Lib\Prisma\Classes\Prisma;
$prisma = Prisma::getInstance();
$logs = $prisma->versionLog->findMany([
'orderBy' => ['createdAt' => 'desc'],
'take' => 5,
'select' => [
'id' => true,
'version' => true,
'createdAt' => true,
'title' => true,
'description'=> true,
],
]);
?>
<div class="w-full overflow-auto">
<div class="w-full max-w-3xl mx-auto px-4 py-12 space-y-10">
<ul id="log-list" class="space-y-8">
<?php foreach ($logs as $log):
$date = new DateTime($log->createdAt);
?>
<li class="space-y-2">
<h2 class="text-xl font-semibold"><?= $log->version ?></h2>
<p class="text-sm text-base-content/60">
Released on
<time datetime="<?= $date->format('Y-m-d') ?>"><?= $date->format('F j, Y') ?></time>
</p>
<p class="text-sm"><?= $log->title ?> — <?= $log->description ?></p>
</li>
<?php endforeach; ?>
</ul>
<div id="scroll-sentinel" class="h-4 w-full"></div>
<p id="end-message" class="text-center mt-6 text-sm text-base-content/60 hidden">
No more data to load
</p>
<div id="loader" class="flex justify-center py-6 hidden">
<span class="loading loading-spinner loading-md"></span>
</div>
</div>
</div>
<script>
let page = 2; // next page (1 already rendered)
let isLoading = false;
let hasMore = true;
const sentinel = document.getElementById('scroll-sentinel');
const listEl = document.getElementById('log-list');
const loader = document.getElementById('loader');
const endMsg = document.getElementById('end-message');
async function loadMore() {
if (isLoading || !hasMore) return;
isLoading = true;
loader.classList.remove('hidden');
try {
const res = await pphp.fetch(`/docs/change-log?page=${page}&perPage=5`);
if (!res.ok) throw new Error('Request failed');
const json = await res.json();
const data = json.data ?? [];
hasMore = Boolean(json.nextPage);
if (hasMore) page = json.nextPage;
if (data.length) {
const html = data.map(renderItem).join('');
listEl.insertAdjacentHTML('beforeend', html);
}
if (!hasMore) {
observer.disconnect();
endMsg.classList.remove('hidden');
}
} catch (e) {
console.error(e);
} finally {
loader.classList.add('hidden');
isLoading = false;
}
}
function renderItem(item) {
const d = new Date(item.createdAt);
const dateISO= d.toISOString().split('T')[0];
const dateUS = d.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
return `
<li class="space-y-2">
<h2 class="text-xl font-semibold">${item.version}</h2>
<p class="text-sm text-base-content/60">
Released on <time datetime="${dateISO}">${dateUS}</time>
</p>
<p class="text-sm">${item.title} — ${item.description}</p>
</li>
`;
}
const observer = new IntersectionObserver(entries => {
if (entries[0].isIntersecting) loadMore();
}, { rootMargin: '200px' });
observer.observe(sentinel);
</script>
Tips
- DaisyUI loader:
loading loading-spinner
for a quick spinner. - isLoading: avoids duplicate requests if the sentinel fires repeatedly.
- nextPage: when
null
, disconnect the observer and show a final message. - Error UI: add a toast/alert if the fetch fails.