From 2c8bd128092cca5fbadfbe26d6b59936d02a14f6 Mon Sep 17 00:00:00 2001 From: Gavyn Caldwell Date: Fri, 13 Mar 2026 12:31:07 -0600 Subject: [PATCH] add popular apps section to home page Shows top 6 apps by download count on the home page, ranked by Obtainium install clicks from the analytics table. --- db/queries.ts | 29 +++++++++++++++++++++++++++++ src/lib/server-fns.ts | 10 ++++++++++ src/routes/index.tsx | 35 +++++++++++++++++++++++++++++++---- 3 files changed, 70 insertions(+), 4 deletions(-) diff --git a/db/queries.ts b/db/queries.ts index 94ba3ec..e4f06d8 100644 --- a/db/queries.ts +++ b/db/queries.ts @@ -407,6 +407,35 @@ export async function getRecentApps(db: DrizzleDB) { return withSlimRelations(db, rows); } +export async function getPopularApps(db: DrizzleDB, limit = 6) { + const popular = await db + .select({ appId: appDownloads.appId, count: sql`count(*)` }) + .from(appDownloads) + .groupBy(appDownloads.appId) + .orderBy(sql`count(*) desc`) + .limit(limit); + + if (popular.length === 0) return []; + + const rows = await db + .select(appCardColumns) + .from(apps) + .where( + inArray( + apps.id, + popular.map((p) => p.appId), + ), + ); + + const hydrated = await withSlimRelations(db, rows); + + // Preserve download count ordering + const orderMap = new Map(popular.map((p, i) => [p.appId, i])); + return hydrated.sort( + (a, b) => (orderMap.get(a.id) ?? 0) - (orderMap.get(b.id) ?? 0), + ); +} + // ─── Desktop App Queries ──────────────────────────────────────────── export async function listDesktopApps( diff --git a/src/lib/server-fns.ts b/src/lib/server-fns.ts index 30931e6..a54ffcf 100644 --- a/src/lib/server-fns.ts +++ b/src/lib/server-fns.ts @@ -7,6 +7,7 @@ import { getAppAlternatives, getAppBySlug, getComparisonBySlug, + getPopularApps, getProprietaryAppAlternatives, getProprietaryAppBySlug, getRecentApps, @@ -232,6 +233,15 @@ export const fetchRecentApps = createServerFn({ method: "GET" }) }); }); +export const fetchPopularApps = createServerFn({ method: "GET" }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .handler(async (): Promise => { + return kvCached(cacheKey("getPopularApps"), () => { + const db = getDb(); + return getPopularApps(db); + }); + }); + export const fetchComparisonBySlug = createServerFn({ method: "GET" }) .inputValidator((input: { slug: string }) => input) // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/src/routes/index.tsx b/src/routes/index.tsx index b0c9bce..1d84f97 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -4,7 +4,11 @@ import { JsonLd } from "~/components/json-ld"; import { PageLayout } from "~/components/layout/page-layout"; import { SearchBar } from "~/components/search-bar"; import { SITE_URL } from "~/lib/constants"; -import { fetchCategoriesWithApps, fetchRecentApps } from "~/lib/server-fns"; +import { + fetchCategoriesWithApps, + fetchPopularApps, + fetchRecentApps, +} from "~/lib/server-fns"; const popularAlternatives = [ { name: "WhatsApp", slug: "whatsapp" }, @@ -21,11 +25,12 @@ const popularAlternatives = [ export const Route = createFileRoute("/")({ loader: async () => { - const [recentApps, categories] = await Promise.all([ + const [recentApps, popularApps, categories] = await Promise.all([ fetchRecentApps(), + fetchPopularApps(), fetchCategoriesWithApps(), ]); - return { recentApps, categories }; + return { recentApps, popularApps, categories }; }, head: () => ({ meta: [ @@ -53,7 +58,7 @@ export const Route = createFileRoute("/")({ }); function HomePage() { - const { recentApps, categories } = Route.useLoaderData(); + const { recentApps, popularApps, categories } = Route.useLoaderData(); const jsonLd = { "@context": "https://schema.org", @@ -104,6 +109,28 @@ function HomePage() { + {/* Popular Apps */} + {popularApps.length > 0 && ( +
+

+ Popular Apps +

+
+ {popularApps.map((app: any) => ( + + ))} +
+
+ )} + {/* Recently Added */} {recentApps.length > 0 && (