Add Photographers & Builders to community stats + restructure Lab section
- 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 <assistant@shakespeare.diy>
This commit is contained in:
parent
3b25e7bcf6
commit
cf514a7c3f
2 changed files with 70 additions and 11 deletions
|
|
@ -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<string, {
|
||||
spots: Set<string>; shops: Set<string>; events: Set<string>;
|
||||
coaches: Set<string>; filmers: Set<string>; editors: Set<string>;
|
||||
photographers: Set<string>; builders: Set<string>;
|
||||
tours: Set<string>; crews: Set<string>; pubkeys: Set<string>;
|
||||
}>();
|
||||
|
||||
|
|
@ -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<string, number>();
|
||||
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Layout>
|
||||
<div className="container max-w-6xl px-4 py-10 md:py-14">
|
||||
|
|
@ -216,12 +229,37 @@ export default function CommunityStatsPage() {
|
|||
<StatCard icon={Calendar} value={stats?.totalEvents ?? 0} label="Events" color="#ccff00" loading={isLoading} thisMonthDelta={thisMonth?.events} />
|
||||
<StatCard icon={Users} value={stats?.totalMappers ?? 0} label="Mappers" color="#facc15" loading={isLoading} trustedValue={stats?.trustedMappers} sub="unique contributors" thisMonthDelta={thisMonth?.activeMappers} />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3 mt-3">
|
||||
<StatCard icon={GraduationCap} value={stats?.totalCoaches ?? 0} label="Coaches" color="#ff6600" loading={isLoading} thisMonthDelta={thisMonth?.coaches} />
|
||||
<StatCard icon={Video} value={stats?.totalFilmers ?? 0} label="Filmers" color="#60a5fa" loading={isLoading} thisMonthDelta={thisMonth?.filmers} />
|
||||
<StatCard icon={Scissors} value={stats?.totalEditors ?? 0} label="Editors" color="#c084fc" loading={isLoading} thisMonthDelta={thisMonth?.editors} />
|
||||
<StatCard icon={Route} value={stats?.totalTours ?? 0} label="Tours" color="#f472b6" loading={isLoading} thisMonthDelta={thisMonth?.tours} />
|
||||
<StatCard icon={Users2} value={stats?.totalCrews ?? 0} label="Crews" color="#2dd4bf" loading={isLoading} thisMonthDelta={thisMonth?.crews} />
|
||||
</section>
|
||||
|
||||
{/* ── Section 1b: The Lab — creators & services ── */}
|
||||
<section className="mb-12">
|
||||
<div className="flex items-center justify-between mb-4 gap-2">
|
||||
<h2 className="text-xs font-black uppercase tracking-widest text-gray-500 flex items-center gap-2">
|
||||
<FlaskConical className="h-3.5 w-3.5 text-[#ff6600]" /> The Lab · Creators & Services
|
||||
</h2>
|
||||
{!isLoading && labTotal > 0 && (
|
||||
<span className="text-[10px] font-black uppercase tracking-wide px-2 py-0.5 rounded-full bg-[#ff6600]/15 text-[#ff6600]">
|
||||
{labTotal.toLocaleString()} total
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3">
|
||||
<StatCard icon={GraduationCap} value={stats?.totalCoaches ?? 0} label="Coaches" color="#ff6600" loading={isLoading} thisMonthDelta={thisMonth?.coaches} />
|
||||
<StatCard icon={Video} value={stats?.totalFilmers ?? 0} label="Filmers" color="#60a5fa" loading={isLoading} thisMonthDelta={thisMonth?.filmers} />
|
||||
<StatCard icon={Scissors} value={stats?.totalEditors ?? 0} label="Editors" color="#c084fc" loading={isLoading} thisMonthDelta={thisMonth?.editors} />
|
||||
<StatCard icon={Camera} value={stats?.totalPhotographers ?? 0} label="Photographers" color="#38bdf8" loading={isLoading} thisMonthDelta={thisMonth?.photographers} />
|
||||
<StatCard icon={Hammer} value={stats?.totalBuilders ?? 0} label="Builders" color="#fb923c" loading={isLoading} thisMonthDelta={thisMonth?.builders} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── Section 1c: Community — tours & crews ── */}
|
||||
<section className="mb-12">
|
||||
<h2 className="text-xs font-black uppercase tracking-widest text-gray-500 mb-4 flex items-center gap-2">
|
||||
<Users2 className="h-3.5 w-3.5 text-[#2dd4bf]" /> Community
|
||||
</h2>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-2 lg:grid-cols-4 gap-3">
|
||||
<StatCard icon={Route} value={stats?.totalTours ?? 0} label="Tours" color="#f472b6" loading={isLoading} thisMonthDelta={thisMonth?.tours} />
|
||||
<StatCard icon={Users2} value={stats?.totalCrews ?? 0} label="Crews" color="#2dd4bf" loading={isLoading} thisMonthDelta={thisMonth?.crews} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue