Skip to content

Commit 9f328fa

Browse files
committed
Perf: Implement hybrid infinite scroll for public stories and paginated admin views
1 parent 58275eb commit 9f328fa

9 files changed

Lines changed: 533 additions & 88 deletions

File tree

web/src/actions/content.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,20 @@ export async function deleteLeela(id: string) {
3131
revalidatePath('/admin/leela', 'layout');
3232
}
3333

34+
export async function getLeelasPaged(page: number = 1, pageSize: number = 20) {
35+
if (!await isAdmin()) throw new Error('Unauthorized');
36+
const skip = (page - 1) * pageSize;
37+
const [items, total] = await Promise.all([
38+
prisma.leela.findMany({
39+
skip,
40+
take: pageSize,
41+
orderBy: { orderId: 'asc' }
42+
}),
43+
prisma.leela.count()
44+
]);
45+
return { items, total, hasMore: total > skip + items.length };
46+
}
47+
3448
// Bodhakatha Actions
3549
export async function saveBodhakatha(data: any) {
3650
if (!await isAdmin()) throw new Error('Unauthorized');
@@ -57,6 +71,20 @@ export async function deleteBodhakatha(id: string) {
5771
revalidatePath('/admin/bodhakatha', 'layout');
5872
}
5973

74+
export async function getBodhakathasPaged(page: number = 1, pageSize: number = 20) {
75+
if (!await isAdmin()) throw new Error('Unauthorized');
76+
const skip = (page - 1) * pageSize;
77+
const [items, total] = await Promise.all([
78+
prisma.bodhakatha.findMany({
79+
skip,
80+
take: pageSize,
81+
orderBy: { orderId: 'asc' }
82+
}),
83+
prisma.bodhakatha.count()
84+
]);
85+
return { items, total, hasMore: total > skip + items.length };
86+
}
87+
6088
// Glossary Actions
6189
export async function saveGlossary(data: any) {
6290
if (!await isAdmin()) throw new Error('Unauthorized');
@@ -82,3 +110,59 @@ export async function deleteGlossary(id: string) {
82110
revalidatePath('/admin/glossary');
83111
revalidatePath('/admin/glossary', 'layout');
84112
}
113+
114+
export async function getGlossaryPaged(page: number = 1, pageSize: number = 20) {
115+
if (!await isAdmin()) throw new Error('Unauthorized');
116+
const skip = (page - 1) * pageSize;
117+
const [items, total] = await Promise.all([
118+
prisma.glossary.findMany({
119+
skip,
120+
take: pageSize,
121+
orderBy: { term: 'asc' }
122+
}),
123+
prisma.glossary.count()
124+
]);
125+
return { items, total, hasMore: total > skip + items.length };
126+
}
127+
// Public Fetchers (No Auth Required)
128+
export async function getPublicLeelas(page: number = 1, pageSize: number = 10) {
129+
const skip = (page - 1) * pageSize;
130+
const [items, total] = await Promise.all([
131+
prisma.leela.findMany({
132+
skip,
133+
take: pageSize,
134+
orderBy: { orderId: 'asc' }
135+
}),
136+
prisma.leela.count()
137+
]);
138+
return {
139+
items: items.map((item: any) => ({
140+
...item,
141+
createdAt: item.createdAt?.toISOString(),
142+
updatedAt: item.updatedAt?.toISOString()
143+
})),
144+
total,
145+
hasMore: total > skip + items.length
146+
};
147+
}
148+
149+
export async function getPublicBodhakathas(page: number = 1, pageSize: number = 10) {
150+
const skip = (page - 1) * pageSize;
151+
const [items, total] = await Promise.all([
152+
prisma.bodhakatha.findMany({
153+
skip,
154+
take: pageSize,
155+
orderBy: { orderId: 'asc' }
156+
}),
157+
prisma.bodhakatha.count()
158+
]);
159+
return {
160+
items: items.map((item: any) => ({
161+
...item,
162+
createdAt: item.createdAt?.toISOString(),
163+
updatedAt: item.updatedAt?.toISOString()
164+
})),
165+
total,
166+
hasMore: total > skip + items.length
167+
};
168+
}
Lines changed: 8 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,26 @@
1-
import Link from 'next/link';
2-
import prisma from '@/lib/db';
1+
32
import { Lightbulb } from 'lucide-react';
3+
import BodhakathaList from '@/components/features/BodhakathaList';
4+
import { getPublicBodhakathas } from '@/actions/content';
45

56
export const revalidate = 3600; // Revalidate every hour
67

7-
88
export default async function BodhakathaPage() {
9-
const bodhakathaArticles = await prisma.bodhakatha.findMany({
10-
orderBy: { orderId: 'asc' }
11-
});
9+
const { items, total, hasMore } = await getPublicBodhakathas(1, 10);
1210

1311
return (
14-
<div className="max-w-4xl mx-auto space-y-6 pt-6 px-4">
12+
<div className="max-w-4xl mx-auto space-y-6 pt-6 px-4 pb-20">
1513
<div className="flex items-center space-x-4 pb-2 border-b border-gray-100">
1614
<div className="w-12 h-12 bg-ochre/10 rounded-full flex items-center justify-center text-ochre flex-none">
1715
<Lightbulb className="w-6 h-6" />
1816
</div>
1917
<div>
20-
<h1 className="text-2xl font-bold text-ochre">Bodhakatha</h1>
21-
<p className="text-sm text-gray-500 font-serif italic">"Instructional Stories & Wisdom"</p>
18+
<h1 className="text-2xl font-black text-ochre uppercase tracking-tight">Bodhakatha</h1>
19+
<p className="text-sm text-gray-500 font-serif italic opacity-70">"Instructional Stories & Wisdom"</p>
2220
</div>
2321
</div>
2422

25-
<div className="grid gap-6">
26-
{bodhakathaArticles.map((article: any) => (
27-
<Link
28-
key={article.id}
29-
href={`/bodhakatha/${article.id}`}
30-
className="block p-6 bg-white rounded-lg shadow-sm border border-gray-100 hover:shadow-md hover:border-gold transition-all group"
31-
>
32-
<div className="flex flex-col gap-2">
33-
<span className="text-xs font-bold text-white bg-ochre px-3 py-1 rounded-full self-start">
34-
{article.theme}
35-
</span>
36-
<h2 className="text-lg md:text-xl font-bold text-gray-800 group-hover:text-ochre transition-colors mt-2">
37-
{article.title_english}
38-
</h2>
39-
<h3 className="text-base md:text-lg text-gray-400 font-serif">
40-
{article.title_hindi}
41-
</h3>
42-
43-
</div>
44-
</Link>
45-
))}
46-
</div>
23+
<BodhakathaList initialItems={items} total={total} hasMoreInitial={hasMore} />
4724
</div>
4825
);
4926
}

web/src/app/(main)/leela/page.tsx

Lines changed: 7 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,25 @@
1-
import Link from 'next/link';
2-
import prisma from '@/lib/db';
31
import { Footprints } from 'lucide-react';
2+
import LeelaList from '@/components/features/LeelaList';
3+
import { getPublicLeelas } from '@/actions/content';
44

55
export const revalidate = 3600; // Revalidate every hour
66

7-
87
export default async function LeelaPage() {
9-
const leelaArticles = await prisma.leela.findMany({
10-
orderBy: { orderId: 'asc' }
11-
});
8+
const { items, total, hasMore } = await getPublicLeelas(1, 10);
129

1310
return (
14-
<div className="max-w-4xl mx-auto space-y-6 pt-6 px-4">
11+
<div className="max-w-4xl mx-auto space-y-6 pt-6 px-4 pb-20">
1512
<div className="flex items-center space-x-4 pb-2 border-b border-gray-100">
1613
<div className="w-12 h-12 bg-ochre/10 rounded-full flex items-center justify-center text-ochre flex-none">
1714
<Footprints className="w-6 h-6" />
1815
</div>
1916
<div>
20-
<h1 className="text-2xl font-bold text-ochre">Leela</h1>
21-
<p className="text-sm text-gray-500 font-serif italic">"Devine plays of the Lord"</p>
17+
<h1 className="text-2xl font-black text-ochre uppercase tracking-tight">Leela</h1>
18+
<p className="text-sm text-gray-500 font-serif italic opacity-70">"Divine plays of the Lord"</p>
2219
</div>
2320
</div>
2421

25-
<div className="grid gap-6">
26-
{leelaArticles.map((article: any) => (
27-
<Link
28-
key={article.id}
29-
href={`/leela/${article.id}`}
30-
className="block p-6 bg-white rounded-xl shadow-sm border border-gray-100 hover:shadow-md hover:border-gold transition-all group active:scale-[0.98]"
31-
>
32-
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
33-
<div className="space-y-2">
34-
<h2 className="text-lg md:text-xl font-bold text-gray-800 group-hover:text-ochre transition-colors">
35-
{article.title_english}
36-
</h2>
37-
<h3 className="text-base md:text-lg text-gray-400 font-serif">
38-
{article.title_hindi}
39-
</h3>
40-
<p className="text-gray-600 text-sm line-clamp-2">
41-
{article.description}
42-
</p>
43-
</div>
44-
</div>
45-
</Link>
46-
))}
47-
</div>
22+
<LeelaList initialItems={items} total={total} hasMoreInitial={hasMore} />
4823
</div>
4924
);
5025
}

web/src/app/admin/bodhakatha/page.tsx

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,62 @@
11

2+
'use client';
3+
4+
import { useState, useEffect } from 'react';
25
import Link from 'next/link';
3-
import prisma from '@/lib/db';
4-
import { Plus, Edit } from 'lucide-react';
6+
import { Plus, Edit, Loader2 } from 'lucide-react';
57
import DeleteIconButton from '@/components/admin/DeleteIconButton';
8+
import { getBodhakathasPaged } from '@/actions/content';
9+
10+
export default function AdminBodhakathaPage() {
11+
const [bodhakathas, setBodhakathas] = useState<any[]>([]);
12+
const [isLoading, setIsLoading] = useState(true);
13+
const [page, setPage] = useState(1);
14+
const [hasMore, setHasMore] = useState(false);
15+
const [total, setTotal] = useState(0);
16+
17+
useEffect(() => {
18+
fetchBodhakathas(1, true);
19+
}, []);
20+
21+
const fetchBodhakathas = async (pageNum: number, isReset: boolean = false) => {
22+
setIsLoading(true);
23+
try {
24+
const result = await getBodhakathasPaged(pageNum, 20);
25+
if (isReset) {
26+
setBodhakathas(result.items);
27+
} else {
28+
setBodhakathas(prev => [...prev, ...result.items]);
29+
}
30+
setHasMore(result.hasMore);
31+
setTotal(result.total);
32+
} catch (error) {
33+
console.error('Error fetching bodhakathas:', error);
34+
} finally {
35+
setIsLoading(false);
36+
}
37+
};
638

7-
export default async function AdminBodhakathaPage() {
8-
const bodhakathas = await prisma.bodhakatha.findMany({
9-
orderBy: { orderId: 'asc' }
10-
});
39+
const loadMore = () => {
40+
const nextPage = page + 1;
41+
setPage(nextPage);
42+
fetchBodhakathas(nextPage);
43+
};
44+
45+
if (isLoading && bodhakathas.length === 0) {
46+
return (
47+
<div className="flex flex-col items-center justify-center min-h-[60vh] text-gray-400">
48+
<Loader2 className="w-10 h-10 animate-spin mb-4 text-ochre" />
49+
<p className="text-lg uppercase font-black tracking-widest">Loading Bodhakatha...</p>
50+
</div>
51+
);
52+
}
1153

1254
return (
13-
<div className="space-y-6 md:space-y-8">
55+
<div className="space-y-6 md:space-y-8 animate-in fade-in duration-500">
1456
<div className="flex flex-col md:flex-row md:items-center justify-between border-b border-gray-100 pb-4 md:pb-6 gap-4">
1557
<div>
1658
<h1 className="text-xl md:text-3xl font-black text-gray-900 tracking-tight">Bodhakatha</h1>
17-
<p className="text-xs md:text-sm text-gray-500 font-medium">Manage instructional stories and wisdom teachings</p>
59+
<p className="text-xs md:text-sm text-gray-500 font-medium">Manage {total} instructional stories and wisdom teachings</p>
1860
</div>
1961
<Link
2062
href="/admin/bodhakatha/new"
@@ -92,6 +134,25 @@ export default async function AdminBodhakathaPage() {
92134
</div>
93135
))}
94136
</div>
137+
138+
{hasMore && (
139+
<div className="flex justify-center pt-8 pb-12">
140+
<button
141+
onClick={loadMore}
142+
disabled={isLoading}
143+
className="bg-white text-ochre px-8 py-3 rounded-2xl font-black text-xs uppercase tracking-[0.2em] border border-ochre/20 hover:bg-orange-50 transition-all flex items-center shadow-sm disabled:opacity-50"
144+
>
145+
{isLoading ? (
146+
<>
147+
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
148+
Loading...
149+
</>
150+
) : (
151+
"Explore More Bodhakatha"
152+
)}
153+
</button>
154+
</div>
155+
)}
95156
</div>
96157
);
97158
}

0 commit comments

Comments
 (0)