Skip to content

Commit 3a52999

Browse files
committed
feat(site): upgrade guests, authors, sponsors, topic hubs, and profile feeds to infinite scroll with live filtering
1 parent 5d0727f commit 3a52999

15 files changed

Lines changed: 749 additions & 246 deletions

File tree

‎apps/site/src/components/InfiniteContentGrid.astro‎

Lines changed: 186 additions & 100 deletions
Large diffs are not rendered by default.

‎apps/site/src/pages/api/feed-cards.astro‎

Lines changed: 97 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
* `/api/feed-cards` | Astro HTML Partial Endpoint (`export const partial = true`)
44
*
55
* Streams pre-rendered `<ContentCard>` fragments for infinite scrolling and
6-
* in-page topic filtering across `/podcasts`, `/blog`, `/authors`, `/guests`,
7-
* and `/sponsors`. Zero client hydration bundle or duplicated card templates.
6+
* in-page topic/keyword filtering across `/podcasts`, `/blog`, `/guests`,
7+
* `/authors`, `/sponsors`, `/topic/[slug]`, `/author/[slug]`, `/guest/[slug]`,
8+
* and `/sponsor/[slug]`.
89
*/
910
export const partial = true;
1011
12+
import groq from "groq";
1113
import ContentCard from "@/components/ContentCard.astro";
1214
import type { ContentListItem } from "@/lib/content";
1315
import {
@@ -22,6 +24,7 @@ import {
2224
postCountQuery,
2325
sponsorCountQuery,
2426
} from "@/lib/sanity/queries";
27+
import { baseFieldsNoContent, personRefFields } from "@/lib/sanity/queries/shared";
2528
import { itemMatchesTopic, TOPICS_BY_SLUG } from "@/lib/topics";
2629
2730
type FeedType = "podcast" | "post" | "author" | "guest" | "sponsor";
@@ -43,6 +46,8 @@ if (!VALID_TYPES.has(rawType)) {
4346
}
4447
4548
const skip = params.get("skip")?.trim() || "none";
49+
const refId = params.get("refId")?.trim() || "";
50+
const searchQuery = params.get("q")?.trim().toLowerCase() || "";
4651
const rawOffset = Number.parseInt(params.get("offset") ?? "0", 10);
4752
const rawLimit = Number.parseInt(params.get("limit") ?? "8", 10);
4853
const offset = Number.isFinite(rawOffset) && rawOffset >= 0 ? rawOffset : 0;
@@ -53,20 +58,87 @@ const sliceEnd = offset + pageSize;
5358
const topicSlug = params.get("topic")?.trim() || "";
5459
const topicDef = topicSlug ? TOPICS_BY_SLUG[topicSlug] : undefined;
5560
61+
const refPodcastQuery = groq`*[_type == "podcast" && references($refId) && defined(slug.current)] | order(date desc, _updatedAt desc) [$offset...$limit] {
62+
${baseFieldsNoContent},
63+
author[]->{ ${personRefFields} },
64+
guest[]->{ ${personRefFields} }
65+
}`;
66+
const refPodcastCountQuery = groq`count(*[_type == "podcast" && references($refId) && defined(slug.current)])`;
67+
68+
const refPostQuery = groq`*[_type == "post" && references($refId) && defined(slug.current)] | order(date desc, _updatedAt desc) [$offset...$limit] {
69+
${baseFieldsNoContent},
70+
author[]->{ ${personRefFields} }
71+
}`;
72+
const refPostCountQuery = groq`count(*[_type == "post" && references($refId) && defined(slug.current)])`;
73+
74+
const matchesSearch = (item: ContentListItem, q: string) => {
75+
if (!q) {
76+
return true;
77+
}
78+
const hay = `${item.title ?? ""} ${item.excerpt ?? ""}`.toLowerCase();
79+
return hay.includes(q);
80+
};
81+
5682
let items: ContentListItem[] = [];
5783
let totalCount = 0;
5884
59-
if (topicDef && (rawType === "podcast" || rawType === "post")) {
60-
const query = rawType === "podcast" ? morePodcastQuery : morePostQuery;
61-
const allItems = ((await sanity.loadQuery(query, {
62-
skip,
63-
offset: 0,
64-
limit: 250,
65-
})) ?? []) as ContentListItem[];
66-
67-
const matched = allItems.filter((item) =>
68-
itemMatchesTopic(item.title, item.excerpt, topicDef),
69-
);
85+
if (refId && (rawType === "podcast" || rawType === "post")) {
86+
if (rawType === "podcast") {
87+
const [count, batch] = await Promise.all([
88+
sanity.loadQuery(refPodcastCountQuery, { refId }),
89+
sanity.loadQuery(refPodcastQuery, { refId, offset, limit: sliceEnd }),
90+
]);
91+
totalCount = typeof count === "number" ? count : 0;
92+
items = (batch ?? []) as ContentListItem[];
93+
} else {
94+
const [count, batch] = await Promise.all([
95+
sanity.loadQuery(refPostCountQuery, { refId }),
96+
sanity.loadQuery(refPostQuery, { refId, offset, limit: sliceEnd }),
97+
]);
98+
totalCount = typeof count === "number" ? count : 0;
99+
items = (batch ?? []) as ContentListItem[];
100+
}
101+
} else if (topicDef || searchQuery) {
102+
let allItems: ContentListItem[] = [];
103+
if (rawType === "podcast") {
104+
allItems = ((await sanity.loadQuery(morePodcastQuery, {
105+
skip,
106+
offset: 0,
107+
limit: 300,
108+
})) ?? []) as ContentListItem[];
109+
} else if (rawType === "post") {
110+
allItems = ((await sanity.loadQuery(morePostQuery, {
111+
skip,
112+
offset: 0,
113+
limit: 300,
114+
})) ?? []) as ContentListItem[];
115+
} else if (rawType === "guest") {
116+
allItems = ((await sanity.loadQuery(moreGuestQuery, {
117+
skip,
118+
offset: 0,
119+
limit: 300,
120+
})) ?? []) as ContentListItem[];
121+
} else if (rawType === "author") {
122+
allItems = ((await sanity.loadQuery(moreAuthorQuery, {
123+
skip,
124+
offset: 0,
125+
limit: 300,
126+
})) ?? []) as ContentListItem[];
127+
} else if (rawType === "sponsor") {
128+
allItems = ((await sanity.loadQuery(moreSponsorQuery, {
129+
skip,
130+
offset: 0,
131+
limit: 300,
132+
})) ?? []) as ContentListItem[];
133+
}
134+
135+
const matched = allItems.filter((item) => {
136+
const okTopic = topicDef
137+
? itemMatchesTopic(item.title, item.excerpt, topicDef)
138+
: true;
139+
const okSearch = matchesSearch(item, searchQuery);
140+
return okTopic && okSearch;
141+
});
70142
totalCount = matched.length;
71143
items = matched.slice(offset, sliceEnd);
72144
} else {
@@ -112,7 +184,10 @@ if (topicDef && (rawType === "podcast" || rawType === "post")) {
112184
limit: sliceEnd,
113185
}),
114186
]);
115-
totalCount = typeof count === "number" ? count : 0;
187+
totalCount = Math.max(
188+
0,
189+
(typeof count === "number" ? count : 0) - (skip !== "none" ? 1 : 0),
190+
);
116191
items = (batch ?? []) as ContentListItem[];
117192
break;
118193
}
@@ -125,7 +200,10 @@ if (topicDef && (rawType === "podcast" || rawType === "post")) {
125200
limit: sliceEnd,
126201
}),
127202
]);
128-
totalCount = typeof count === "number" ? count : 0;
203+
totalCount = Math.max(
204+
0,
205+
(typeof count === "number" ? count : 0) - (skip !== "none" ? 1 : 0),
206+
);
129207
items = (batch ?? []) as ContentListItem[];
130208
break;
131209
}
@@ -138,7 +216,10 @@ if (topicDef && (rawType === "podcast" || rawType === "post")) {
138216
limit: sliceEnd,
139217
}),
140218
]);
141-
totalCount = typeof count === "number" ? count : 0;
219+
totalCount = Math.max(
220+
0,
221+
(typeof count === "number" ? count : 0) - (skip !== "none" ? 1 : 0),
222+
);
142223
items = (batch ?? []) as ContentListItem[];
143224
break;
144225
}

‎apps/site/src/pages/author/[slug].astro‎

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
---
2+
import groq from "groq";
23
import Breadcrumbs from "@/components/Breadcrumbs.astro";
3-
import ContentGrid from "@/components/ContentGrid.astro";
44
import CoverMedia from "@/components/CoverMedia.astro";
5+
import InfiniteContentGrid from "@/components/InfiniteContentGrid.astro";
56
import PortableTextContent from "@/components/portable-text/PortableTextContent.astro";
67
import SectionHeader from "@/components/SectionHeader.astro";
78
import UserSocials from "@/components/UserSocials.astro";
@@ -13,11 +14,21 @@ import { breadcrumbSchema, personSchema } from "@/lib/structured-data";
1314
const { sanity } = Astro.locals;
1415
const { slug } = Astro.params;
1516
16-
const person = slug ? await sanity.loadQuery(authorQueryWithRelated, { slug }) : null;
17+
const person = slug
18+
? await sanity.loadQuery(authorQueryWithRelated, { slug })
19+
: null;
1720
if (!person?._id) {
1821
return new Response(null, { status: 404 });
1922
}
2023
24+
const countsQuery = groq`{
25+
"podcasts": count(*[_type == "podcast" && references($refId) && defined(slug.current)]),
26+
"posts": count(*[_type == "post" && references($refId) && defined(slug.current)])
27+
}`;
28+
const counts = ((await sanity.loadQuery(countsQuery, {
29+
refId: person._id,
30+
})) ?? { podcasts: 0, posts: 0 }) as { podcasts?: number; posts?: number };
31+
2132
const path = `/author/${slug}`;
2233
const origin = Astro.locals.siteUrl.origin;
2334
const personImage = sanity.resolveOpenGraphImage(
@@ -39,6 +50,8 @@ const description =
3950
4051
const relatedPosts = (person.related?.post ?? []) as ContentListItem[];
4152
const relatedPodcasts = (person.related?.podcast ?? []) as ContentListItem[];
53+
const totalPodcasts = counts.podcasts ?? relatedPodcasts.length;
54+
const totalPosts = counts.posts ?? relatedPosts.length;
4255
---
4356

4457
<BaseLayout
@@ -110,17 +123,33 @@ const relatedPodcasts = (person.related?.podcast ?? []) as ContentListItem[];
110123
{
111124
relatedPodcasts.length > 0 && (
112125
<aside>
113-
<SectionHeader title="podcast episodes" href="/podcasts/page/1" />
114-
<ContentGrid items={relatedPodcasts} priorityCount={0} />
126+
<SectionHeader title="podcast episodes" />
127+
<InfiniteContentGrid
128+
type="podcast"
129+
items={relatedPodcasts}
130+
refId={person._id}
131+
initialOffset={relatedPodcasts.length}
132+
totalCount={totalPodcasts}
133+
priorityCount={0}
134+
nounPlural="episodes"
135+
/>
115136
</aside>
116137
)
117138
}
118139

119140
{
120141
relatedPosts.length > 0 && (
121142
<aside class="mb-24">
122-
<SectionHeader title="blog posts" href="/blog/page/1" />
123-
<ContentGrid items={relatedPosts} priorityCount={0} />
143+
<SectionHeader title="blog posts" />
144+
<InfiniteContentGrid
145+
type="post"
146+
items={relatedPosts}
147+
refId={person._id}
148+
initialOffset={relatedPosts.length}
149+
totalCount={totalPosts}
150+
priorityCount={0}
151+
nounPlural="blog posts"
152+
/>
124153
</aside>
125154
)
126155
}

0 commit comments

Comments
 (0)