From 74f948e6caf5eae87898aebc363bd40fb546eb5b Mon Sep 17 00:00:00 2001 From: MartinBraquet Date: Sun, 8 Mar 2026 16:55:58 +0100 Subject: [PATCH] Add gender retio to stats --- backend/api/src/stats.ts | 46 ++++++++++++++++++++++++++++++++++++++-- common/src/api/schema.ts | 3 +++ web/pages/stats.tsx | 29 +++++++++++++++++++------ 3 files changed, 70 insertions(+), 8 deletions(-) diff --git a/backend/api/src/stats.ts b/backend/api/src/stats.ts index 390dd7b7..526b57e7 100644 --- a/backend/api/src/stats.ts +++ b/backend/api/src/stats.ts @@ -1,22 +1,64 @@ import {getMessagesCount} from 'api/get-messages-count' +import {HOUR_MS} from 'common/util/time' import {createSupabaseDirectClient} from 'shared/supabase/init' import {APIHandler} from './helpers/endpoint' +// Server-side cache for stats data +let cachedData: any = null +let cacheTimestamp: number = 0 +const CACHE_DURATION_MS = HOUR_MS + export const stats: APIHandler<'stats'> = async (_, _auth) => { + const now = Date.now() + + // Return cached data if still valid + if (cachedData && now - cacheTimestamp < CACHE_DURATION_MS) { + console.log('cached stats') + console.log(cachedData) + return cachedData + } + const pg = createSupabaseDirectClient() - const [userCount, profileCount, eventsCount, messagesCount] = await Promise.all([ + const [userCount, profileCount, eventsCount, messagesCount, genderStats] = await Promise.all([ pg.one(`SELECT COUNT(*)::int as count FROM users`), pg.one(`SELECT COUNT(*)::int as count FROM profiles`), pg.one(`SELECT COUNT(*)::int as count FROM events WHERE event_start_time > now()`), getMessagesCount(), + pg.manyOrNone( + `SELECT gender, COUNT(*)::int as count FROM profiles WHERE gender IS NOT NULL GROUP BY gender`, + ), ]) - return { + // Calculate gender ratios + const genderRatio: Record = {} + let totalWithGender = 0 + + genderStats?.forEach((stat: any) => { + if (!['male', 'female'].includes(stat.gender)) return + genderRatio[stat.gender] = stat.count + totalWithGender += stat.count + }) + + // Convert to percentages + const genderPercentage: Record = {} + Object.entries(genderRatio).forEach(([gender, count]) => { + genderPercentage[gender] = Math.round((count / totalWithGender) * 100) + }) + + const result = { users: userCount.count, profiles: profileCount.count, upcomingEvents: eventsCount.count, messages: messagesCount.count, + genderRatio: genderPercentage, + genderCounts: genderRatio, } + + // Update cache + cachedData = result + cacheTimestamp = now + + return result } diff --git a/common/src/api/schema.ts b/common/src/api/schema.ts index aa807774..301d39fc 100644 --- a/common/src/api/schema.ts +++ b/common/src/api/schema.ts @@ -154,6 +154,9 @@ export const API = (_apiTypeCheck = { users: number profiles: number upcomingEvents: number + messages: number + genderRatio: Record + genderCounts: Record }, summary: 'Get platform statistics', tag: 'General', diff --git a/web/pages/stats.tsx b/web/pages/stats.tsx index 79ef0371..47bd6360 100644 --- a/web/pages/stats.tsx +++ b/web/pages/stats.tsx @@ -5,12 +5,14 @@ import {PageBase} from 'web/components/page-base' import {SEO} from 'web/components/SEO' import ChartMembers from 'web/components/widgets/charts' import StatBox from 'web/components/widgets/stat-box' +import {api} from 'web/lib/api' import {useT} from 'web/lib/locale' import {getCount} from 'web/lib/supabase/users' export default function Stats() { const t = useT() const [data, setData] = useState>({}) + const [statsData, setStatsData] = useState(null) useEffect(() => { async function load() { @@ -27,14 +29,23 @@ export default function Stats() { 'vote_results', ] as const - const settled = await Promise.allSettled(tables.map((t) => getCount(t))) + const [settled, statsResult] = await Promise.allSettled([ + Promise.allSettled(tables.map((t) => getCount(t))), + api('stats', {}), + ]) const result: Record = {} - settled.forEach((res, i) => { - const key = tables[i] - if (res.status === 'fulfilled') result[key] = res.value - else result[key] = null - }) + if (settled.status === 'fulfilled') { + settled.value.forEach((res, i) => { + const key = tables[i] + if (res.status === 'fulfilled') result[key] = res.value + else result[key] = null + }) + } + + if (statsResult.status === 'fulfilled') { + setStatsData(statsResult.value) + } setData(result) } @@ -108,6 +119,12 @@ export default function Stats() { label={t('stats.endorsements', 'Endorsements')} /> )} + {!!statsData?.genderRatio && ( + + )}