feat(courses): add type, search, and pricing filters to org landing page

- Add Types section to the Filter sidebar with all 4 course types
- Add full-text search input centered in the hero (title + description)
- Add Pricing section (Free / Paid) based on course cost
- Compact hero prop across all 10 themes to halve vertical padding
- Children snippet support in all hero themes for the search input
- All filters are URL-driven (?types=, ?search=, ?tags=, ?pricing=)
- Full-stack: validation schema, DB queries, service, API route, server load, UI
This commit is contained in:
Rotimi Best
2026-07-01 10:36:53 +01:00
parent 8d2f2a6d2d
commit 78a2b85ad3
27 changed files with 464 additions and 97 deletions
@@ -383,15 +383,21 @@ export const organizationRouter = new Hono()
*/
.get('/courses/public', zValidator('query', ZGetCoursesBySiteName), async (c) => {
try {
const { siteName, tags, page, limit } = c.req.valid('query');
const tagSlugs = tags
?.split(',')
.map((value) => value.trim())
.filter(Boolean);
const { siteName, tags, types, search, pricing, page, limit } = c.req.valid('query');
const splitParam = (param: string | undefined) =>
param
?.split(',')
.map((v) => v.trim())
.filter(Boolean);
const tagSlugs = splitParam(tags);
const courseTypes = splitParam(types);
const pagination = page !== undefined || limit !== undefined ? { page, limit } : undefined;
const courses = await getPublicCourses(
siteName,
tagSlugs && tagSlugs.length > 0 ? tagSlugs : undefined,
tagSlugs?.length ? tagSlugs : undefined,
courseTypes?.length ? courseTypes : undefined,
search || undefined,
pricing || undefined,
pagination
);
+19 -2
View File
@@ -256,6 +256,9 @@ export async function removeAudienceMember(orgId: string, memberId: number, remo
export async function getPublicCourses(
siteName: string,
tagSlugs?: string[],
courseTypes?: string[],
search?: string,
pricing?: 'free' | 'paid',
pagination?: { page?: number; limit?: number }
) {
try {
@@ -286,8 +289,22 @@ export async function getPublicCourses(
}
}
const totalCourses = await countPublishedCoursesBySiteName(siteName, filteredCourseIds);
const courses = await getPublishedCoursesBySiteName(siteName, filteredCourseIds, limit, offset);
const totalCourses = await countPublishedCoursesBySiteName(
siteName,
filteredCourseIds,
courseTypes,
search,
pricing
);
const courses = await getPublishedCoursesBySiteName(
siteName,
filteredCourseIds,
courseTypes,
search,
pricing,
limit,
offset
);
const tagsByCourseId = await getCourseTagsByCourseIdsForOrganization(
org.id,
courses.map((course) => course.id)