chore: audit fixes, designer stats and form FAB handling

This commit is contained in:
SkateSpots 2026-10-29 23:14:12 +07:00
parent dddfe51f86
commit 262eb32e7c
33 changed files with 53 additions and 24 deletions

13
package-lock.json generated
View file

@ -2946,12 +2946,6 @@
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
"node_modules/@protobufjs/inquire": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.2.tgz",
"integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/path": { "node_modules/@protobufjs/path": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
@ -10926,9 +10920,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/protobufjs": { "node_modules/protobufjs": {
"version": "7.6.3", "version": "7.6.5",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.3.tgz", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz",
"integrity": "sha512-+k0vdJKNdW+Vu+dYe8tZA/VvQb6XKNWexC6URwBFXxNnjLJz9nQJCemGyNgRAWD+B7+nGNc9qMPGwcD7s4nzUw==", "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"dependencies": { "dependencies": {
@ -10938,7 +10932,6 @@
"@protobufjs/eventemitter": "^1.1.1", "@protobufjs/eventemitter": "^1.1.1",
"@protobufjs/fetch": "^1.1.1", "@protobufjs/fetch": "^1.1.1",
"@protobufjs/float": "^1.0.2", "@protobufjs/float": "^1.0.2",
"@protobufjs/inquire": "^1.1.2",
"@protobufjs/path": "^1.1.2", "@protobufjs/path": "^1.1.2",
"@protobufjs/pool": "^1.1.0", "@protobufjs/pool": "^1.1.0",
"@protobufjs/utf8": "^1.1.1", "@protobufjs/utf8": "^1.1.1",

View file

@ -184,7 +184,7 @@
}, },
"overrides": { "overrides": {
"@tanstack/query-core": "$@tanstack/react-query", "@tanstack/query-core": "$@tanstack/react-query",
"protobufjs": "7.6.3", "protobufjs": "7.6.5",
"sharp": "0.35.5" "sharp": "0.35.5"
} }
} }

View file

@ -522,6 +522,8 @@ export function Layout({ children, noPadding }: LayoutProps) {
// The map is full-bleed (it manages its own height), so it gets no bottom // The map is full-bleed (it manages its own height), so it gets no bottom
// padding even though it shares this one persistent layout with every route. // padding even though it shares this one persistent layout with every route.
const isFullBleed = noPadding || location.pathname === '/' || location.pathname === '/map'; const isFullBleed = noPadding || location.pathname === '/' || location.pathname === '/map';
// The floating assistant must not cover form controls on long-form pages.
const hideSasquatch = /^\/(become|create|edit|supporters|onboarding)(\/|$)/.test(location.pathname);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [mobileLanguageView, setMobileLanguageView] = useState(false); const [mobileLanguageView, setMobileLanguageView] = useState(false);
// One auth dialog for the header + burger menu. Owning it here (not inside // One auth dialog for the header + burger menu. Owning it here (not inside
@ -1262,12 +1264,14 @@ export function Layout({ children, noPadding }: LayoutProps) {
</Suspense> </Suspense>
{/* Sasquatch Guide (all screens) — lazy; not needed at first paint */} {/* Sasquatch Guide (all screens) — lazy; not needed at first paint */}
<Suspense fallback={null}> {!hideSasquatch && (
<LazySasquatchGuide <Suspense fallback={null}>
message={currentMessage} <LazySasquatchGuide
onDismiss={dismissCurrentMessage} message={currentMessage}
/> onDismiss={dismissCurrentMessage}
</Suspense> />
</Suspense>
)}
{/* Set Status Dialog — lazy (on-demand modal) */} {/* Set Status Dialog — lazy (on-demand modal) */}
<Suspense fallback={null}> <Suspense fallback={null}>

View file

@ -56,7 +56,7 @@ export function useFollowers(pubkey?: string) {
// Query all kind 3 events that have a p tag with our pubkey // Query all kind 3 events that have a p tag with our pubkey
const events = await nostr.query( const events = await nostr.query(
[{ kinds: [3], '#p': [targetPubkey], limit: 100 }], [{ kinds: [3], '#p': [targetPubkey], limit: 500 }],
{ signal } { signal }
); );

View file

@ -17,7 +17,7 @@ import {
SKATE_MUSICIAN_KIND, SKATE_MUSICIAN_KIND,
SKATE_JUDGE_KIND, SKATE_JUDGE_KIND,
SKATE_ORGANIZER_KIND, SKATE_ORGANIZER_KIND,
SKATE_BARBER_KIND, SKATE_TRANSPORT_KIND, SKATE_BARBER_KIND, SKATE_TRANSPORT_KIND, SKATE_DESIGNER_KIND,
SKATE_TOUR_KIND, SKATE_TOUR_KIND,
SKATE_CREW_KIND, SKATE_CREW_KIND,
MARKETPLACE_ITEM_KIND, MARKETPLACE_ITEM_KIND,
@ -80,6 +80,7 @@ export interface GrowthStats {
totalOrganizers: number; totalOrganizers: number;
totalBarbers: number; totalBarbers: number;
totalTransport: number; totalTransport: number;
totalDesigners: 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 */
@ -232,6 +233,7 @@ export function useGrowthStats() { // Community stats read from the CANONICAL c
organizerEvents, organizerEvents,
barberEvents, barberEvents,
transportEvents, transportEvents,
designerEvents,
listingEvents, listingEvents,
tradeEvents, tradeEvents,
] = await Promise.all([ ] = await Promise.all([
@ -256,6 +258,7 @@ export function useGrowthStats() { // Community stats read from the CANONICAL c
querySkateContent([{ kinds: [SKATE_ORGANIZER_KIND], limit: 500 }], { signal }).catch(() => []), querySkateContent([{ kinds: [SKATE_ORGANIZER_KIND], limit: 500 }], { signal }).catch(() => []),
querySkateContent([{ kinds: [SKATE_BARBER_KIND], limit: 500 }], { signal }).catch(() => []), querySkateContent([{ kinds: [SKATE_BARBER_KIND], limit: 500 }], { signal }).catch(() => []),
querySkateContent([{ kinds: [SKATE_TRANSPORT_KIND], limit: 500 }], { signal }).catch(() => []), querySkateContent([{ kinds: [SKATE_TRANSPORT_KIND], limit: 500 }], { signal }).catch(() => []),
querySkateContent([{ kinds: [SKATE_DESIGNER_KIND], limit: 500 }], { signal }).catch(() => []),
// Marketplace: live listings + completed-trade attestations. // Marketplace: live listings + completed-trade attestations.
querySkateContent([{ kinds: [MARKETPLACE_ITEM_KIND], '#t': ['skateboarding'], limit: 2000 }], { signal }).catch(() => []), querySkateContent([{ kinds: [MARKETPLACE_ITEM_KIND], '#t': ['skateboarding'], limit: 2000 }], { signal }).catch(() => []),
querySkateContent([{ kinds: [TRADE_ATTESTATION_KIND], limit: 1000 }], { signal }).catch(() => []), querySkateContent([{ kinds: [TRADE_ATTESTATION_KIND], limit: 1000 }], { signal }).catch(() => []),
@ -299,8 +302,9 @@ export function useGrowthStats() { // Community stats read from the CANONICAL c
const organizers = dedup(organizerEvents); const organizers = dedup(organizerEvents);
const barbers = dedup(barberEvents); const barbers = dedup(barberEvents);
const transport = dedup(transportEvents); const transport = dedup(transportEvents);
const designers = dedup(designerEvents);
// Aggregate of the "other" crafts for active-mapper / total rollups. // Aggregate of the "other" crafts for active-mapper / total rollups.
const otherLab = [...artists, ...graffiti, ...tattoo, ...trainers, ...musicians, ...judges, ...organizers, ...barbers, ...transport]; const otherLab = [...artists, ...graffiti, ...tattoo, ...trainers, ...musicians, ...judges, ...organizers, ...barbers, ...transport, ...designers];
// ── Marketplace ────────────────────────────────────────────────────── // ── Marketplace ──────────────────────────────────────────────────────
// Showcase-only stash items are public but NOT for sale, so exclude them. // Showcase-only stash items are public but NOT for sale, so exclude them.
@ -579,6 +583,7 @@ export function useGrowthStats() { // Community stats read from the CANONICAL c
totalOrganizers: organizers.length, totalOrganizers: organizers.length,
totalBarbers: barbers.length, totalBarbers: barbers.length,
totalTransport: transport.length, totalTransport: transport.length,
totalDesigners: designers.length,
totalTours: tours.length, totalTours: tours.length,
totalCrews: crews.length, totalCrews: crews.length,
totalMappers: allPubkeys.size, totalMappers: allPubkeys.size,

View file

@ -714,6 +714,7 @@ const batch6 = {
judges: "Jutges", judges: "Jutges",
organizers: "Organitzadors", organizers: "Organitzadors",
barbers: "Barbers", barbers: "Barbers",
designers: "Dissenyadors",
showFewer: "Mostra'n menys", showFewer: "Mostra'n menys",
showAllCrafts: "Mostra els 15 oficis", showAllCrafts: "Mostra els 15 oficis",
community: "Comunitat", community: "Comunitat",

View file

@ -688,6 +688,7 @@ export const csBatch4 = {
judges: "Rozhodčí", judges: "Rozhodčí",
organizers: "Organizátoři", organizers: "Organizátoři",
barbers: "Barbeři", barbers: "Barbeři",
designers: "Designéři",
showFewer: "Zobrazit méně", showFewer: "Zobrazit méně",
showAllCrafts: "Zobrazit všech 15 řemesel", showAllCrafts: "Zobrazit všech 15 řemesel",
community: "Komunita", community: "Komunita",

View file

@ -688,6 +688,7 @@ const deBatch4 = {
judges: "Juroren", judges: "Juroren",
organizers: "Organisatoren", organizers: "Organisatoren",
barbers: "Barbiere", barbers: "Barbiere",
designers: "Designer:innen",
showFewer: "Weniger anzeigen", showFewer: "Weniger anzeigen",
showAllCrafts: "Alle 15 Handwerke anzeigen", showAllCrafts: "Alle 15 Handwerke anzeigen",
community: "Community", community: "Community",

View file

@ -688,6 +688,7 @@ const elBatch4 = {
judges: "Κριτές", judges: "Κριτές",
organizers: "Διοργανωτές", organizers: "Διοργανωτές",
barbers: "Κουρείς", barbers: "Κουρείς",
designers: "Σχεδιαστές",
showFewer: "Εμφάνιση λιγότερων", showFewer: "Εμφάνιση λιγότερων",
showAllCrafts: "Εμφάνιση και των 15 ειδικοτήτων", showAllCrafts: "Εμφάνιση και των 15 ειδικοτήτων",
community: "Κοινότητα", community: "Κοινότητα",

View file

@ -6521,6 +6521,7 @@ walletUi: {
organizers: 'Organizers', organizers: 'Organizers',
barbers: 'Barbers', barbers: 'Barbers',
transport: 'Transport', transport: 'Transport',
designers: 'Designers',
showFewer: 'Show fewer', showFewer: 'Show fewer',
showAllCrafts: 'Show all 15 crafts', showAllCrafts: 'Show all 15 crafts',
community: 'Community', community: 'Community',

View file

@ -6521,6 +6521,7 @@ walletUi: {
organizers: 'Organizadores', organizers: 'Organizadores',
barbers: 'Barberos', barbers: 'Barberos',
transport: 'Transporte', transport: 'Transporte',
designers: 'Diseñadores',
showFewer: 'Mostrar menos', showFewer: 'Mostrar menos',
showAllCrafts: 'Mostrar los 15 oficios', showAllCrafts: 'Mostrar los 15 oficios',
community: 'Comunidad', community: 'Comunidad',

View file

@ -714,6 +714,7 @@ const batch6 = {
judges: "Epaileak", judges: "Epaileak",
organizers: "Antolatzaileak", organizers: "Antolatzaileak",
barbers: "Bizarginak", barbers: "Bizarginak",
designers: "Diseinatzaileak",
showFewer: "Erakutsi gutxiago", showFewer: "Erakutsi gutxiago",
showAllCrafts: "Erakutsi 15 lanbide guztiak", showAllCrafts: "Erakutsi 15 lanbide guztiak",
community: "Komunitatea", community: "Komunitatea",

View file

@ -688,6 +688,7 @@ const frBatch4 = {
judges: "Juges", judges: "Juges",
organizers: "Organisateurs", organizers: "Organisateurs",
barbers: "Coiffeurs", barbers: "Coiffeurs",
designers: "Concepteurs",
showFewer: "Afficher moins", showFewer: "Afficher moins",
showAllCrafts: "Afficher les 15 disciplines", showAllCrafts: "Afficher les 15 disciplines",
community: "Communauté", community: "Communauté",

View file

@ -714,6 +714,7 @@ const batch6 = {
"judges": "Xuíces", "judges": "Xuíces",
"organizers": "Organizadores", "organizers": "Organizadores",
"barbers": "Barbeiros", "barbers": "Barbeiros",
"designers": "Deseñadores",
"showFewer": "Amosar menos", "showFewer": "Amosar menos",
"showAllCrafts": "Amosar os 15 oficios", "showAllCrafts": "Amosar os 15 oficios",
"community": "Comunidade", "community": "Comunidade",

View file

@ -714,6 +714,7 @@ const batch6 = {
"judges": "जज", "judges": "जज",
"organizers": "आयोजक", "organizers": "आयोजक",
"barbers": "नाई", "barbers": "नाई",
"designers": "डिज़ाइनर",
"showFewer": "कम दिखाएँ", "showFewer": "कम दिखाएँ",
"showAllCrafts": "सभी 15 क्राफ़्ट दिखाएँ", "showAllCrafts": "सभी 15 क्राफ़्ट दिखाएँ",
"community": "समुदाय", "community": "समुदाय",

View file

@ -694,6 +694,7 @@ const idBatch4 = {
judges: 'Juri', judges: 'Juri',
organizers: 'Penyelenggara', organizers: 'Penyelenggara',
barbers: 'Tukang Cukur', barbers: 'Tukang Cukur',
designers: 'Desainer',
showFewer: 'Tampilkan lebih sedikit', showFewer: 'Tampilkan lebih sedikit',
showAllCrafts: 'Tampilkan semua 15 keahlian', showAllCrafts: 'Tampilkan semua 15 keahlian',
community: 'Komunitas', community: 'Komunitas',

View file

@ -688,6 +688,7 @@ const itBatch4 = {
judges: "Giudici", judges: "Giudici",
organizers: "Organizzatori", organizers: "Organizzatori",
barbers: "Barbieri", barbers: "Barbieri",
designers: "Progettisti",
showFewer: "Mostra meno", showFewer: "Mostra meno",
showAllCrafts: "Mostra tutti i 15 mestieri", showAllCrafts: "Mostra tutti i 15 mestieri",
community: "Community", community: "Community",

View file

@ -697,6 +697,7 @@ const jaBatch7 = {
judges: 'ジャッジ', judges: 'ジャッジ',
organizers: 'オーガナイザー', organizers: 'オーガナイザー',
barbers: 'バーバー', barbers: 'バーバー',
designers: 'デザイナー',
showFewer: '表示を減らす', showFewer: '表示を減らす',
showAllCrafts: '15職種すべて表示', showAllCrafts: '15職種すべて表示',
community: 'コミュニティ', community: 'コミュニティ',

View file

@ -696,6 +696,7 @@ const koBatch4 = {
judges: '심판', judges: '심판',
organizers: '주최자', organizers: '주최자',
barbers: '바버', barbers: '바버',
designers: '디자이너',
showFewer: '접기', showFewer: '접기',
showAllCrafts: '15개 분야 모두 보기', showAllCrafts: '15개 분야 모두 보기',
community: '커뮤니티', community: '커뮤니티',

View file

@ -696,6 +696,7 @@ const nlBatch4 = {
judges: 'Juryleden', judges: 'Juryleden',
organizers: 'Organisatoren', organizers: 'Organisatoren',
barbers: 'Barbers', barbers: 'Barbers',
designers: 'Ontwerpers',
showFewer: 'Minder tonen', showFewer: 'Minder tonen',
showAllCrafts: 'Alle 15 ambachten tonen', showAllCrafts: 'Alle 15 ambachten tonen',
community: 'Community', community: 'Community',

View file

@ -688,6 +688,7 @@ const plBatch4 = {
judges: "Sędziowie", judges: "Sędziowie",
organizers: "Organizatorzy", organizers: "Organizatorzy",
barbers: "Barberzy", barbers: "Barberzy",
designers: "Projektanci",
showFewer: "Pokaż mniej", showFewer: "Pokaż mniej",
showAllCrafts: "Pokaż wszystkie 15 rzemiosł", showAllCrafts: "Pokaż wszystkie 15 rzemiosł",
community: "Społeczność", community: "Społeczność",

View file

@ -775,6 +775,7 @@ const ptBatch4 = {
judges: 'Jurados', judges: 'Jurados',
organizers: 'Organizadores', organizers: 'Organizadores',
barbers: 'Barbeiros', barbers: 'Barbeiros',
designers: 'Projetistas',
showFewer: 'Mostrar menos', showFewer: 'Mostrar menos',
showAllCrafts: 'Mostrar todos os 15 ofícios', showAllCrafts: 'Mostrar todos os 15 ofícios',
community: 'Comunidade', community: 'Comunidade',

View file

@ -688,6 +688,7 @@ const roBatch4 = {
judges: 'Juri', judges: 'Juri',
organizers: 'Organizatori', organizers: 'Organizatori',
barbers: 'Frizeri', barbers: 'Frizeri',
designers: 'Designeri',
showFewer: 'Arată mai puține', showFewer: 'Arată mai puține',
showAllCrafts: 'Arată toate cele 15 meserii', showAllCrafts: 'Arată toate cele 15 meserii',
community: 'Comunitate', community: 'Comunitate',

View file

@ -688,6 +688,7 @@ const ruBatch4 = {
judges: 'Судьи', judges: 'Судьи',
organizers: 'Организаторы', organizers: 'Организаторы',
barbers: 'Барберы', barbers: 'Барберы',
designers: 'Дизайнеры',
showFewer: 'Показать меньше', showFewer: 'Показать меньше',
showAllCrafts: 'Показать все 15 ремёсел', showAllCrafts: 'Показать все 15 ремёсел',
community: 'Сообщество', community: 'Сообщество',

View file

@ -688,6 +688,7 @@ const svBatch4 = {
judges: "Domare", judges: "Domare",
organizers: "Arrangörer", organizers: "Arrangörer",
barbers: "Barberare", barbers: "Barberare",
designers: "Formgivare",
showFewer: "Visa färre", showFewer: "Visa färre",
showAllCrafts: "Visa alla 15 hantverk", showAllCrafts: "Visa alla 15 hantverk",
community: "Community", community: "Community",

View file

@ -768,6 +768,7 @@ const thBatch9 = {
judges: 'กรรมการ', judges: 'กรรมการ',
organizers: 'ผู้จัด', organizers: 'ผู้จัด',
barbers: 'ช่างตัดผม', barbers: 'ช่างตัดผม',
designers: 'นักออกแบบ',
showFewer: 'แสดงน้อยลง', showFewer: 'แสดงน้อยลง',
showAllCrafts: 'แสดงทั้ง 15 อาชีพ', showAllCrafts: 'แสดงทั้ง 15 อาชีพ',
community: 'ชุมชน', community: 'ชุมชน',

View file

@ -688,6 +688,7 @@ const trBatch4 = {
judges: "Jüri üyeleri", judges: "Jüri üyeleri",
organizers: "Organizatörler", organizers: "Organizatörler",
barbers: "Berberler", barbers: "Berberler",
designers: "Tasarımcılar",
showFewer: "Daha az göster", showFewer: "Daha az göster",
showAllCrafts: "15 zanaatın tümünü göster", showAllCrafts: "15 zanaatın tümünü göster",
community: "Topluluk", community: "Topluluk",

View file

@ -688,6 +688,7 @@ const ukBatch4 = {
judges: "Судді", judges: "Судді",
organizers: "Організатори", organizers: "Організатори",
barbers: "Барбери", barbers: "Барбери",
designers: "Дизайнери",
showFewer: "Показати менше", showFewer: "Показати менше",
showAllCrafts: "Показати всі 15 напрямів", showAllCrafts: "Показати всі 15 напрямів",
community: "Спільнота", community: "Спільнота",

View file

@ -696,6 +696,7 @@ const viBatch4 = {
judges: 'Giám khảo', judges: 'Giám khảo',
organizers: 'Nhà tổ chức', organizers: 'Nhà tổ chức',
barbers: 'Thợ cắt tóc', barbers: 'Thợ cắt tóc',
designers: 'Nhà thiết kế',
showFewer: 'Hiện ít hơn', showFewer: 'Hiện ít hơn',
showAllCrafts: 'Hiện cả 15 nghề', showAllCrafts: 'Hiện cả 15 nghề',
community: 'Cộng đồng', community: 'Cộng đồng',

View file

@ -693,6 +693,7 @@ const zhCNBatch4 = {
judges: "裁判", judges: "裁判",
organizers: "组织者", organizers: "组织者",
barbers: "理发师", barbers: "理发师",
designers: "设计师",
showFewer: "收起", showFewer: "收起",
showAllCrafts: "显示全部 15 种手艺", showAllCrafts: "显示全部 15 种手艺",
community: "社区", community: "社区",

View file

@ -693,6 +693,7 @@ const zhTWBatch4 = {
judges: "裁判", judges: "裁判",
organizers: "組織者", organizers: "組織者",
barbers: "理髮師", barbers: "理髮師",
designers: "設計師",
showFewer: "收起", showFewer: "收起",
showAllCrafts: "顯示全部 15 種手藝", showAllCrafts: "顯示全部 15 種手藝",
community: "社群", community: "社群",

View file

@ -16,7 +16,7 @@ import { formatAge } from '@/lib/routeCache';
const ANGOR_PROJECT_URL = 'https://angor.io/project/angor1qtunna00uqyx52rr0nvqa059z2gknhrmn7urd6z'; const ANGOR_PROJECT_URL = 'https://angor.io/project/angor1qtunna00uqyx52rr0nvqa059z2gknhrmn7urd6z';
import { SKATE_RELAY } from '@/lib/types'; import { SKATE_RELAY } from '@/lib/types';
import { AreaChart, Area, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell, Legend } from 'recharts'; import { AreaChart, Area, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell, Legend } from 'recharts';
import { MapPin, Store, Calendar, GraduationCap, Video, Scissors, Route, Users, Users2, Camera, Hammer, TrendingUp, Radio, Zap, ExternalLink, Trophy, FlaskConical, Palette, SprayCan, PenTool, Dumbbell, Music, Gavel, Tent, Sparkles, Truck, ShoppingBag, Tag, Handshake, Globe, RefreshCw } from 'lucide-react'; import { MapPin, Store, Calendar, GraduationCap, Video, Scissors, Route, Users, Users2, Camera, Hammer, TrendingUp, Radio, Zap, ExternalLink, Trophy, FlaskConical, Palette, SprayCan, PenTool, Dumbbell, Music, Gavel, Tent, Sparkles, Truck, Shirt, ShoppingBag, Tag, Handshake, Globe, RefreshCw } from 'lucide-react';
// ─── Helpers ───────────────────────────────────────────────────────────────── // ─── Helpers ─────────────────────────────────────────────────────────────────
@ -25,13 +25,13 @@ function labCraftTotal(s: {
totalCoaches: number; totalFilmers: number; totalEditors: number; totalCoaches: number; totalFilmers: number; totalEditors: number;
totalPhotographers: number; totalBuilders: number; totalArtists: number; totalPhotographers: number; totalBuilders: number; totalArtists: number;
totalGraffiti: number; totalTattoo: number; totalTrainers: number; totalGraffiti: number; totalTattoo: number; totalTrainers: number;
totalMusicians: number; totalJudges: number; totalOrganizers: number; totalBarbers: number; totalTransport: number; totalMusicians: number; totalJudges: number; totalOrganizers: number; totalBarbers: number; totalTransport: number; totalDesigners: number;
}): number { }): number {
return ( return (
s.totalCoaches + s.totalFilmers + s.totalEditors + s.totalPhotographers + s.totalCoaches + s.totalFilmers + s.totalEditors + s.totalPhotographers +
s.totalBuilders + s.totalArtists + s.totalGraffiti + s.totalTattoo + s.totalBuilders + s.totalArtists + s.totalGraffiti + s.totalTattoo +
s.totalTrainers + s.totalMusicians + s.totalJudges + s.totalOrganizers + s.totalTrainers + s.totalMusicians + s.totalJudges + s.totalOrganizers +
s.totalBarbers + (s.totalTransport ?? 0) s.totalBarbers + (s.totalTransport ?? 0) + (s.totalDesigners ?? 0)
); );
} }
@ -323,6 +323,7 @@ export default function CommunityStatsPage() {
<StatCard icon={Tent} value={stats?.totalOrganizers ?? 0} label={t('walletUi.stats.organizers')} color="#2dd4bf" loading={isLoading} to="/lab?craft=organizing" /> <StatCard icon={Tent} value={stats?.totalOrganizers ?? 0} label={t('walletUi.stats.organizers')} color="#2dd4bf" loading={isLoading} to="/lab?craft=organizing" />
<StatCard icon={Sparkles} value={stats?.totalBarbers ?? 0} label={t('walletUi.stats.barbers')} color="#22d3ee" loading={isLoading} to="/lab?craft=barbering" /> <StatCard icon={Sparkles} value={stats?.totalBarbers ?? 0} label={t('walletUi.stats.barbers')} color="#22d3ee" loading={isLoading} to="/lab?craft=barbering" />
<StatCard icon={Truck} value={stats?.totalTransport ?? 0} label={t('walletUi.stats.transport')} color="#f59e0b" loading={isLoading} to="/lab?craft=transport" /> <StatCard icon={Truck} value={stats?.totalTransport ?? 0} label={t('walletUi.stats.transport')} color="#f59e0b" loading={isLoading} to="/lab?craft=transport" />
<StatCard icon={Shirt} value={stats?.totalDesigners ?? 0} label={t('walletUi.stats.designers')} color="#a78bfa" loading={isLoading} to="/lab?craft=clothing" />
</div> </div>
</div> </div>
<button <button

View file

@ -628,7 +628,7 @@ export default function SupportersPage() {
? num(stats.totalCoaches) + num(stats.totalFilmers) + num(stats.totalEditors) + num(stats.totalPhotographers) ? num(stats.totalCoaches) + num(stats.totalFilmers) + num(stats.totalEditors) + num(stats.totalPhotographers)
+ num(stats.totalBuilders) + num(stats.totalArtists) + num(stats.totalGraffiti) + num(stats.totalTattoo) + num(stats.totalBuilders) + num(stats.totalArtists) + num(stats.totalGraffiti) + num(stats.totalTattoo)
+ num(stats.totalTrainers) + num(stats.totalMusicians) + num(stats.totalJudges) + num(stats.totalOrganizers) + num(stats.totalTrainers) + num(stats.totalMusicians) + num(stats.totalJudges) + num(stats.totalOrganizers)
+ num(stats.totalBarbers) + num(stats.totalTransport) + num(stats.totalBarbers) + num(stats.totalTransport) + num(stats.totalDesigners)
: 0; : 0;
const impact = stats const impact = stats