From cf514a7c3f202c4c9ee3e0e72458bd92b5bd60c3 Mon Sep 17 00:00:00 2001 From: "shakespeare.diy" Date: Fri, 24 Jul 2026 15:06:18 +0700 Subject: [PATCH] Add Photographers & Builders to community stats + restructure Lab section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - useGrowthStats now queries kind 35004 (photographers) and 35005 (builders), which were previously omitted; added to totals, monthly buckets, allContent, and the content-mix donut (folded into the "Lab" slice) - Community Stats page reorganized into clear sub-sections: * "The Lab · Creators & Services" — Coaches, Filmers, Editors, Photographers, Builders, with a total badge * "Community" — Tours, Crews This scales the growing role list cleanly instead of cramming one row. Co-authored-by: shakespeare.diy --- src/hooks/useGrowthStats.ts | 27 ++++++++++++++-- src/pages/CommunityStatsPage.tsx | 54 +++++++++++++++++++++++++++----- 2 files changed, 70 insertions(+), 11 deletions(-) diff --git a/src/hooks/useGrowthStats.ts b/src/hooks/useGrowthStats.ts index 4791628..ae2bb30 100644 --- a/src/hooks/useGrowthStats.ts +++ b/src/hooks/useGrowthStats.ts @@ -7,6 +7,8 @@ import { SKATE_COACH_KIND, SKATE_FILMER_KIND, SKATE_EDITOR_KIND, + SKATE_PHOTOGRAPHER_KIND, + SKATE_BUILDER_KIND, SKATE_TOUR_KIND, SKATE_CREW_KIND, SKATE_RELAY, @@ -25,6 +27,8 @@ export interface MonthBucket { coaches: number; filmers: number; editors: number; + photographers: number; + builders: number; tours: number; crews: number; total: number; @@ -46,6 +50,8 @@ export interface GrowthStats { totalCoaches: number; totalFilmers: number; totalEditors: number; + totalPhotographers: number; + totalBuilders: number; totalTours: number; totalCrews: number; /** All unique pubkeys that have published any content */ @@ -118,6 +124,8 @@ export function useGrowthStats() { coachEvents, filmerEvents, editorEvents, + photographerEvents, + builderEvents, tourEvents, crewEvents, ] = await Promise.all([ @@ -127,6 +135,8 @@ export function useGrowthStats() { relay.query([{ kinds: [SKATE_COACH_KIND], limit: 500 }], { signal }).catch(() => []), relay.query([{ kinds: [SKATE_FILMER_KIND], limit: 500 }], { signal }).catch(() => []), relay.query([{ kinds: [SKATE_EDITOR_KIND], limit: 500 }], { signal }).catch(() => []), + relay.query([{ kinds: [SKATE_PHOTOGRAPHER_KIND], limit: 500 }], { signal }).catch(() => []), + relay.query([{ kinds: [SKATE_BUILDER_KIND], limit: 500 }], { signal }).catch(() => []), relay.query([{ kinds: [SKATE_TOUR_KIND], limit: 500 }], { signal }).catch(() => []), relay.query([{ kinds: [SKATE_CREW_KIND], limit: 500 }], { signal }).catch(() => []), ]); @@ -149,6 +159,8 @@ export function useGrowthStats() { const coaches = dedup(coachEvents); const filmers = dedup(filmerEvents); const editors = dedup(editorEvents); + const photographers = dedup(photographerEvents); + const builders = dedup(builderEvents); const tours = dedup(tourEvents); const crews = dedup(crewEvents); @@ -157,6 +169,7 @@ export function useGrowthStats() { const bucketMap = new Map; shops: Set; events: Set; coaches: Set; filmers: Set; editors: Set; + photographers: Set; builders: Set; tours: Set; crews: Set; pubkeys: Set; }>(); @@ -165,6 +178,7 @@ export function useGrowthStats() { bucketMap.set(key, { spots: new Set(), shops: new Set(), events: new Set(), coaches: new Set(), filmers: new Set(), editors: new Set(), + photographers: new Set(), builders: new Set(), tours: new Set(), crews: new Set(), pubkeys: new Set(), }); } @@ -177,6 +191,8 @@ export function useGrowthStats() { for (const e of coaches) { const b = ensureBucket(toMonthKey(e.created_at)); b.coaches.add(e.id); b.pubkeys.add(e.pubkey); } for (const e of filmers) { const b = ensureBucket(toMonthKey(e.created_at)); b.filmers.add(e.id); b.pubkeys.add(e.pubkey); } for (const e of editors) { const b = ensureBucket(toMonthKey(e.created_at)); b.editors.add(e.id); b.pubkeys.add(e.pubkey); } + for (const e of photographers) { const b = ensureBucket(toMonthKey(e.created_at)); b.photographers.add(e.id); b.pubkeys.add(e.pubkey); } + for (const e of builders) { const b = ensureBucket(toMonthKey(e.created_at)); b.builders.add(e.id); b.pubkeys.add(e.pubkey); } for (const e of tours) { const b = ensureBucket(toMonthKey(e.created_at)); b.tours.add(e.id); b.pubkeys.add(e.pubkey); } for (const e of crews) { const b = ensureBucket(toMonthKey(e.created_at)); b.crews.add(e.id); b.pubkeys.add(e.pubkey); } @@ -191,14 +207,17 @@ export function useGrowthStats() { const co = b?.coaches.size ?? 0; const fi = b?.filmers.size ?? 0; const ed = b?.editors.size ?? 0; + const ph = b?.photographers.size ?? 0; + const bu = b?.builders.size ?? 0; const to = b?.tours.size ?? 0; const cr = b?.crews.size ?? 0; return { label: toMonthLabel(key), key, spots: s, shops: sh, events: ev, - coaches: co, filmers: fi, editors: ed, tours: to, crews: cr, - total: s + sh + ev + co + fi + ed + to + cr, + coaches: co, filmers: fi, editors: ed, + photographers: ph, builders: bu, tours: to, crews: cr, + total: s + sh + ev + co + fi + ed + ph + bu + to + cr, activeMappers: b?.pubkeys.size ?? 0, }; }); @@ -216,7 +235,7 @@ export function useGrowthStats() { const mapperTotal = new Map(); for (const e of spots) { mapperSpots.set(e.pubkey, (mapperSpots.get(e.pubkey) ?? 0) + 1); } - const allContent = [...spots, ...shops, ...events, ...coaches, ...filmers, ...editors, ...tours, ...crews]; + const allContent = [...spots, ...shops, ...events, ...coaches, ...filmers, ...editors, ...photographers, ...builders, ...tours, ...crews]; for (const e of allContent) { mapperTotal.set(e.pubkey, (mapperTotal.get(e.pubkey) ?? 0) + 1); } const topMappers: TopMapper[] = Array.from(mapperSpots.entries()) @@ -254,6 +273,8 @@ export function useGrowthStats() { totalCoaches: coaches.length, totalFilmers: filmers.length, totalEditors: editors.length, + totalPhotographers: photographers.length, + totalBuilders: builders.length, totalTours: tours.length, totalCrews: crews.length, totalMappers: allPubkeys.size, diff --git a/src/pages/CommunityStatsPage.tsx b/src/pages/CommunityStatsPage.tsx index 43de710..62642b1 100644 --- a/src/pages/CommunityStatsPage.tsx +++ b/src/pages/CommunityStatsPage.tsx @@ -20,8 +20,9 @@ import { import { MapPin, Store, Calendar, GraduationCap, Video, Scissors, Route, Users, Users2, + Camera, Hammer, TrendingUp, Radio, Zap, ExternalLink, - Trophy, + Trophy, FlaskConical, } from 'lucide-react'; // ─── Helpers ───────────────────────────────────────────────────────────────── @@ -159,7 +160,7 @@ export default function CommunityStatsPage() { return [ { name: 'Spots', value: stats.totalSpots, color: '#ff0040' }, { name: 'Events', value: stats.totalEvents, color: '#ccff00' }, - { name: 'Lab', value: stats.totalCoaches + stats.totalFilmers + stats.totalEditors, color: '#ff6600' }, + { name: 'Lab', value: stats.totalCoaches + stats.totalFilmers + stats.totalEditors + stats.totalPhotographers + stats.totalBuilders, color: '#ff6600' }, { name: 'Shops', value: stats.totalShops, color: '#4ade80' }, { name: 'Tours', value: stats.totalTours, color: '#c084fc' }, { name: 'Crews', value: stats.totalCrews, color: '#2dd4bf' }, @@ -173,6 +174,18 @@ export default function CommunityStatsPage() { // Current month delta — how many items were added this month const thisMonth = useMemo(() => stats?.monthly[stats.monthly.length - 1], [stats]); + // Total across all Lab creator/service roles + const labTotal = useMemo(() => { + if (!stats) return 0; + return ( + stats.totalCoaches + + stats.totalFilmers + + stats.totalEditors + + stats.totalPhotographers + + stats.totalBuilders + ); + }, [stats]); + return (
@@ -216,12 +229,37 @@ export default function CommunityStatsPage() {
-
- - - - - + + + {/* ── Section 1b: The Lab — creators & services ── */} +
+
+

+ The Lab · Creators & Services +

+ {!isLoading && labTotal > 0 && ( + + {labTotal.toLocaleString()} total + + )} +
+
+ + + + + +
+
+ + {/* ── Section 1c: Community — tours & crews ── */} +
+

+ Community +

+
+ +