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_COACH_KIND,
|
||||||
SKATE_FILMER_KIND,
|
SKATE_FILMER_KIND,
|
||||||
SKATE_EDITOR_KIND,
|
SKATE_EDITOR_KIND,
|
||||||
|
SKATE_PHOTOGRAPHER_KIND,
|
||||||
|
SKATE_BUILDER_KIND,
|
||||||
SKATE_TOUR_KIND,
|
SKATE_TOUR_KIND,
|
||||||
SKATE_CREW_KIND,
|
SKATE_CREW_KIND,
|
||||||
SKATE_RELAY,
|
SKATE_RELAY,
|
||||||
|
|
@ -25,6 +27,8 @@ export interface MonthBucket {
|
||||||
coaches: number;
|
coaches: number;
|
||||||
filmers: number;
|
filmers: number;
|
||||||
editors: number;
|
editors: number;
|
||||||
|
photographers: number;
|
||||||
|
builders: number;
|
||||||
tours: number;
|
tours: number;
|
||||||
crews: number;
|
crews: number;
|
||||||
total: number;
|
total: number;
|
||||||
|
|
@ -46,6 +50,8 @@ export interface GrowthStats {
|
||||||
totalCoaches: number;
|
totalCoaches: number;
|
||||||
totalFilmers: number;
|
totalFilmers: number;
|
||||||
totalEditors: number;
|
totalEditors: number;
|
||||||
|
totalPhotographers: number;
|
||||||
|
totalBuilders: number;
|
||||||
totalTours: number;
|
totalTours: number;
|
||||||
totalCrews: number;
|
totalCrews: number;
|
||||||
/** All unique pubkeys that have published any content */
|
/** All unique pubkeys that have published any content */
|
||||||
|
|
@ -118,6 +124,8 @@ export function useGrowthStats() {
|
||||||
coachEvents,
|
coachEvents,
|
||||||
filmerEvents,
|
filmerEvents,
|
||||||
editorEvents,
|
editorEvents,
|
||||||
|
photographerEvents,
|
||||||
|
builderEvents,
|
||||||
tourEvents,
|
tourEvents,
|
||||||
crewEvents,
|
crewEvents,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
|
|
@ -127,6 +135,8 @@ export function useGrowthStats() {
|
||||||
relay.query([{ kinds: [SKATE_COACH_KIND], limit: 500 }], { signal }).catch(() => []),
|
relay.query([{ kinds: [SKATE_COACH_KIND], limit: 500 }], { signal }).catch(() => []),
|
||||||
relay.query([{ kinds: [SKATE_FILMER_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_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_TOUR_KIND], limit: 500 }], { signal }).catch(() => []),
|
||||||
relay.query([{ kinds: [SKATE_CREW_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 coaches = dedup(coachEvents);
|
||||||
const filmers = dedup(filmerEvents);
|
const filmers = dedup(filmerEvents);
|
||||||
const editors = dedup(editorEvents);
|
const editors = dedup(editorEvents);
|
||||||
|
const photographers = dedup(photographerEvents);
|
||||||
|
const builders = dedup(builderEvents);
|
||||||
const tours = dedup(tourEvents);
|
const tours = dedup(tourEvents);
|
||||||
const crews = dedup(crewEvents);
|
const crews = dedup(crewEvents);
|
||||||
|
|
||||||
|
|
@ -157,6 +169,7 @@ export function useGrowthStats() {
|
||||||
const bucketMap = new Map<string, {
|
const bucketMap = new Map<string, {
|
||||||
spots: Set<string>; shops: Set<string>; events: Set<string>;
|
spots: Set<string>; shops: Set<string>; events: Set<string>;
|
||||||
coaches: Set<string>; filmers: Set<string>; editors: 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>;
|
tours: Set<string>; crews: Set<string>; pubkeys: Set<string>;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
|
@ -165,6 +178,7 @@ export function useGrowthStats() {
|
||||||
bucketMap.set(key, {
|
bucketMap.set(key, {
|
||||||
spots: new Set(), shops: new Set(), events: new Set(),
|
spots: new Set(), shops: new Set(), events: new Set(),
|
||||||
coaches: new Set(), filmers: new Set(), editors: 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(),
|
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 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 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 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 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); }
|
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 co = b?.coaches.size ?? 0;
|
||||||
const fi = b?.filmers.size ?? 0;
|
const fi = b?.filmers.size ?? 0;
|
||||||
const ed = b?.editors.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 to = b?.tours.size ?? 0;
|
||||||
const cr = b?.crews.size ?? 0;
|
const cr = b?.crews.size ?? 0;
|
||||||
return {
|
return {
|
||||||
label: toMonthLabel(key),
|
label: toMonthLabel(key),
|
||||||
key,
|
key,
|
||||||
spots: s, shops: sh, events: ev,
|
spots: s, shops: sh, events: ev,
|
||||||
coaches: co, filmers: fi, editors: ed, tours: to, crews: cr,
|
coaches: co, filmers: fi, editors: ed,
|
||||||
total: s + sh + ev + co + fi + ed + to + cr,
|
photographers: ph, builders: bu, tours: to, crews: cr,
|
||||||
|
total: s + sh + ev + co + fi + ed + ph + bu + to + cr,
|
||||||
activeMappers: b?.pubkeys.size ?? 0,
|
activeMappers: b?.pubkeys.size ?? 0,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
@ -216,7 +235,7 @@ export function useGrowthStats() {
|
||||||
const mapperTotal = new Map<string, number>();
|
const mapperTotal = new Map<string, number>();
|
||||||
|
|
||||||
for (const e of spots) { mapperSpots.set(e.pubkey, (mapperSpots.get(e.pubkey) ?? 0) + 1); }
|
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); }
|
for (const e of allContent) { mapperTotal.set(e.pubkey, (mapperTotal.get(e.pubkey) ?? 0) + 1); }
|
||||||
|
|
||||||
const topMappers: TopMapper[] = Array.from(mapperSpots.entries())
|
const topMappers: TopMapper[] = Array.from(mapperSpots.entries())
|
||||||
|
|
@ -254,6 +273,8 @@ export function useGrowthStats() {
|
||||||
totalCoaches: coaches.length,
|
totalCoaches: coaches.length,
|
||||||
totalFilmers: filmers.length,
|
totalFilmers: filmers.length,
|
||||||
totalEditors: editors.length,
|
totalEditors: editors.length,
|
||||||
|
totalPhotographers: photographers.length,
|
||||||
|
totalBuilders: builders.length,
|
||||||
totalTours: tours.length,
|
totalTours: tours.length,
|
||||||
totalCrews: crews.length,
|
totalCrews: crews.length,
|
||||||
totalMappers: allPubkeys.size,
|
totalMappers: allPubkeys.size,
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,9 @@ import {
|
||||||
import {
|
import {
|
||||||
MapPin, Store, Calendar, GraduationCap,
|
MapPin, Store, Calendar, GraduationCap,
|
||||||
Video, Scissors, Route, Users, Users2,
|
Video, Scissors, Route, Users, Users2,
|
||||||
|
Camera, Hammer,
|
||||||
TrendingUp, Radio, Zap, ExternalLink,
|
TrendingUp, Radio, Zap, ExternalLink,
|
||||||
Trophy,
|
Trophy, FlaskConical,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -159,7 +160,7 @@ export default function CommunityStatsPage() {
|
||||||
return [
|
return [
|
||||||
{ name: 'Spots', value: stats.totalSpots, color: '#ff0040' },
|
{ name: 'Spots', value: stats.totalSpots, color: '#ff0040' },
|
||||||
{ name: 'Events', value: stats.totalEvents, color: '#ccff00' },
|
{ 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: 'Shops', value: stats.totalShops, color: '#4ade80' },
|
||||||
{ name: 'Tours', value: stats.totalTours, color: '#c084fc' },
|
{ name: 'Tours', value: stats.totalTours, color: '#c084fc' },
|
||||||
{ name: 'Crews', value: stats.totalCrews, color: '#2dd4bf' },
|
{ 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
|
// Current month delta — how many items were added this month
|
||||||
const thisMonth = useMemo(() => stats?.monthly[stats.monthly.length - 1], [stats]);
|
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 (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<div className="container max-w-6xl px-4 py-10 md:py-14">
|
<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={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} />
|
<StatCard icon={Users} value={stats?.totalMappers ?? 0} label="Mappers" color="#facc15" loading={isLoading} trustedValue={stats?.trustedMappers} sub="unique contributors" thisMonthDelta={thisMonth?.activeMappers} />
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3 mt-3">
|
</section>
|
||||||
<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} />
|
{/* ── Section 1b: The Lab — creators & services ── */}
|
||||||
<StatCard icon={Scissors} value={stats?.totalEditors ?? 0} label="Editors" color="#c084fc" loading={isLoading} thisMonthDelta={thisMonth?.editors} />
|
<section className="mb-12">
|
||||||
<StatCard icon={Route} value={stats?.totalTours ?? 0} label="Tours" color="#f472b6" loading={isLoading} thisMonthDelta={thisMonth?.tours} />
|
<div className="flex items-center justify-between mb-4 gap-2">
|
||||||
<StatCard icon={Users2} value={stats?.totalCrews ?? 0} label="Crews" color="#2dd4bf" loading={isLoading} thisMonthDelta={thisMonth?.crews} />
|
<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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue