From bc1987f78c425e3e02a0089ff39803a8b13a309d Mon Sep 17 00:00:00 2001
From: SkateSpots
Date: Thu, 29 Oct 2026 20:20:49 +0700
Subject: [PATCH] feat: sponsor-ready profiles with follow, events and audience
stats
---
src/hooks/useFollowing.ts | 4 +-
src/hooks/useSkateEvents.ts | 36 +++++++++
src/i18n/ca/batch6.ts | 10 +++
src/i18n/cs/batch4.ts | 10 +++
src/i18n/de/batch4.ts | 10 +++
src/i18n/el/batch4.ts | 10 +++
src/i18n/en.ts | 10 +++
src/i18n/es.ts | 10 +++
src/i18n/eu/batch6.ts | 10 +++
src/i18n/fr/batch4.ts | 10 +++
src/i18n/gl/batch6.ts | 10 +++
src/i18n/hi/batch6.ts | 10 +++
src/i18n/id/batch4.ts | 10 +++
src/i18n/it/batch4.ts | 10 +++
src/i18n/ja/batch7.ts | 10 +++
src/i18n/ko/batch4.ts | 10 +++
src/i18n/nl/batch4.ts | 10 +++
src/i18n/pl/batch4.ts | 10 +++
src/i18n/pt/batch4.ts | 10 +++
src/i18n/ro/batch4.ts | 10 +++
src/i18n/ru/batch4.ts | 10 +++
src/i18n/sv/batch4.ts | 10 +++
src/i18n/th/batch9.ts | 10 +++
src/i18n/tr/batch4.ts | 10 +++
src/i18n/uk/batch4.ts | 10 +++
src/i18n/vi/batch4.ts | 10 +++
src/i18n/zh-CN/batch4.ts | 10 +++
src/i18n/zh-TW/batch4.ts | 10 +++
src/pages/NIP19Page.tsx | 148 +++++++++++++++++++++++++++++++++---
29 files changed, 438 insertions(+), 10 deletions(-)
diff --git a/src/hooks/useFollowing.ts b/src/hooks/useFollowing.ts
index 794ec22..21e7741 100644
--- a/src/hooks/useFollowing.ts
+++ b/src/hooks/useFollowing.ts
@@ -119,6 +119,7 @@ export function useFollowActions() {
onSuccess: (_targetPubkey) => {
// Invalidate queries to refetch
queryClient.invalidateQueries({ queryKey: ['following', user?.pubkey] });
+ queryClient.invalidateQueries({ queryKey: ['followers', _targetPubkey] });
queryClient.invalidateQueries({ queryKey: ['trust-score'] });
toast({
@@ -165,9 +166,10 @@ export function useFollowActions() {
return targetPubkey;
},
- onSuccess: () => {
+ onSuccess: (targetPubkey) => {
// Invalidate queries
queryClient.invalidateQueries({ queryKey: ['following', user?.pubkey] });
+ queryClient.invalidateQueries({ queryKey: ['followers', targetPubkey] });
queryClient.invalidateQueries({ queryKey: ['trust-score'] });
toast({
diff --git a/src/hooks/useSkateEvents.ts b/src/hooks/useSkateEvents.ts
index d3c1436..a9cac27 100644
--- a/src/hooks/useSkateEvents.ts
+++ b/src/hooks/useSkateEvents.ts
@@ -105,6 +105,42 @@ export function useUpcomingSkateEvents(limit = 20, options: { enabled?: boolean
});
}
+/** Events authored by one pubkey (profile page). Upcoming first, then recent. */
+export function useUserEvents(pubkey: string | undefined) {
+ const { querySkateContent, getQueryKeySuffix } = useSkateRelays();
+ const hidden = useAdminModeration();
+
+ return useQuery({
+ queryKey: ['user-skate-events', pubkey, getQueryKeySuffix()],
+ enabled: !!pubkey,
+ select: (events: SkateEvent[]) => filterAdminHidden(events, CALENDAR_EVENT_KIND, hidden),
+ queryFn: async () => {
+ if (!pubkey) return [];
+ const signal = AbortSignal.timeout(12000);
+ const events = await querySkateContent(
+ [{
+ kinds: [CALENDAR_EVENT_KIND],
+ authors: [pubkey],
+ '#t': ['skateboarding'],
+ limit: 50,
+ }],
+ { signal },
+ );
+ const now = Math.floor(Date.now() / 1000);
+ return events
+ .map(parseSkateEvent)
+ .filter((event): event is SkateEvent => !!event)
+ .sort((a, b) => {
+ const aUpcoming = a.start >= now;
+ const bUpcoming = b.start >= now;
+ if (aUpcoming !== bUpcoming) return aUpcoming ? -1 : 1;
+ return aUpcoming ? a.start - b.start : b.start - a.start;
+ });
+ },
+ staleTime: 60_000,
+ });
+}
+
export function useSkateEvent(identifier: string, pubkey?: string) {
const { querySkateContent, getQueryKeySuffix } = useSkateRelays();
const hidden = useAdminModeration();
diff --git a/src/i18n/ca/batch6.ts b/src/i18n/ca/batch6.ts
index 3458204..387ab7e 100644
--- a/src/i18n/ca/batch6.ts
+++ b/src/i18n/ca/batch6.ts
@@ -962,6 +962,16 @@ const batch6 = {
nip05Verified: "Verificat amb NIP-05",
nip05NotVerified: "NIP-05 no verificat amb aquesta clau",
message: "Missatge",
+ eventsHosted: "Esdeveniments organitzats",
+ forSponsors: "Per a patrocinadors i col·laboracions",
+ sponsorPitch: "Patrocini, col·laboració, foto/vídeo o esdeveniment? Comença una conversa.",
+ startCollab: "Inicia un xat de col·laboració",
+ viewRiderCard: "Veure la fitxa del patinador",
+ collabMessage: "Hola {{name}}, voldria parlar d'un patrocini o d'una col·laboració.",
+ followers: "Seguidors",
+ following: "Seguint",
+ follow: "Segueix",
+ unfollow: "Deixa de seguir",
zapsReceived: "zaps rebuts",
memberSince: "Membre des de",
badges: "Insígnies",
diff --git a/src/i18n/cs/batch4.ts b/src/i18n/cs/batch4.ts
index 95f592e..dcd3ebf 100644
--- a/src/i18n/cs/batch4.ts
+++ b/src/i18n/cs/batch4.ts
@@ -936,6 +936,16 @@ export const csBatch4 = {
nip05Verified: "NIP-05 ověřeno",
nip05NotVerified: "NIP-05 neověřeno vůči tomuto klíči",
message: "Zpráva",
+ eventsHosted: "Uspořádané akce",
+ forSponsors: "Pro sponzory a spolupráce",
+ sponsorPitch: "Sponzorství, spolupráce, foto/video nebo akce? Začni konverzaci.",
+ startCollab: "Začít chat o spolupráci",
+ viewRiderCard: "Zobrazit kartu jezdce",
+ collabMessage: "Ahoj {{name}}, chtěl bych mluvit o sponzorství nebo spolupráci.",
+ followers: "Sledující",
+ following: "Sleduji",
+ follow: "Sledovat",
+ unfollow: "Přestat sledovat",
zapsReceived: "přijatých zaps",
memberSince: "Členem od",
badges: "Odznaky",
diff --git a/src/i18n/de/batch4.ts b/src/i18n/de/batch4.ts
index 10a94ba..d7b3727 100644
--- a/src/i18n/de/batch4.ts
+++ b/src/i18n/de/batch4.ts
@@ -936,6 +936,16 @@ const deBatch4 = {
nip05Verified: "NIP-05 verifiziert",
nip05NotVerified: "NIP-05 nicht gegen diesen Schlüssel verifiziert",
message: "Nachricht",
+ eventsHosted: "Organisierte Events",
+ forSponsors: "Für Sponsoren & Kooperationen",
+ sponsorPitch: "Sponsoring, Kooperation, Foto/Video oder Event? Starte ein Gespräch.",
+ startCollab: "Kooperations-Chat starten",
+ viewRiderCard: "Skateboarder-Karte ansehen",
+ collabMessage: "Hi {{name}}, ich würde gern über ein Sponsoring oder eine Kooperation sprechen.",
+ followers: "Follower",
+ following: "Folge ich",
+ follow: "Folgen",
+ unfollow: "Entfolgen",
zapsReceived: "erhaltene Zaps",
memberSince: "Mitglied seit",
badges: "Abzeichen",
diff --git a/src/i18n/el/batch4.ts b/src/i18n/el/batch4.ts
index f692491..c182344 100644
--- a/src/i18n/el/batch4.ts
+++ b/src/i18n/el/batch4.ts
@@ -936,6 +936,16 @@ const elBatch4 = {
nip05Verified: "Επαληθευμένο NIP-05",
nip05NotVerified: "Το NIP-05 δεν επαληθεύτηκε με αυτό το κλειδί",
message: "Μήνυμα",
+ eventsHosted: "Εκδηλώσεις που διοργάνωσες",
+ forSponsors: "Για χορηγούς και συνεργασίες",
+ sponsorPitch: "Χορηγία, συνεργασία, φωτογραφία/βίντεο ή εκδήλωση; Ξεκίνα μια συζήτηση.",
+ startCollab: "Έναρξη συνομιλίας συνεργασίας",
+ viewRiderCard: "Προβολή κάρτας σκέιτερ",
+ collabMessage: "Γεια σου {{name}}, θα ήθελα να μιλήσουμε για χορηγία ή συνεργασία.",
+ followers: "Ακόλουθοι",
+ following: "Ακολουθείς",
+ follow: "Ακολούθησε",
+ unfollow: "Σταμάτα να ακολουθείς",
zapsReceived: "zaps που ελήφθησαν",
memberSince: "Μέλος από",
badges: "Badges",
diff --git a/src/i18n/en.ts b/src/i18n/en.ts
index 8fa4a9a..9da8539 100644
--- a/src/i18n/en.ts
+++ b/src/i18n/en.ts
@@ -6744,6 +6744,16 @@ walletUi: {
nip05Verified: 'NIP-05 verified',
nip05NotVerified: 'NIP-05 not verified against this key',
message: 'Message',
+ eventsHosted: 'Events hosted',
+ forSponsors: 'For sponsors & collabs',
+ sponsorPitch: 'Sponsorship, collab, photo/video or event? Start a conversation.',
+ startCollab: 'Start a collab chat',
+ viewRiderCard: 'View rider card',
+ collabMessage: "Hi {{name}}, I'd like to talk about a sponsorship or collaboration.",
+ followers: 'Followers',
+ following: 'Following',
+ follow: 'Follow',
+ unfollow: 'Unfollow',
zapsReceived: 'zaps received',
memberSince: 'Member since',
badges: 'Badges',
diff --git a/src/i18n/es.ts b/src/i18n/es.ts
index c23e7b5..9a7efd2 100644
--- a/src/i18n/es.ts
+++ b/src/i18n/es.ts
@@ -6744,6 +6744,16 @@ walletUi: {
nip05Verified: 'NIP-05 verificado',
nip05NotVerified: 'NIP-05 no verificado con esta clave',
message: 'Mensaje',
+ eventsHosted: 'Eventos organizados',
+ forSponsors: 'Para patrocinadores y colaboraciones',
+ sponsorPitch: '¿Patrocinio, colaboración, foto/vídeo o evento? Empieza una conversación.',
+ startCollab: 'Iniciar chat de colaboración',
+ viewRiderCard: 'Ver tarjeta de rider',
+ collabMessage: 'Hola {{name}}, me gustaría hablar sobre un patrocinio o una colaboración.',
+ followers: 'Seguidores',
+ following: 'Siguiendo',
+ follow: 'Seguir',
+ unfollow: 'Dejar de seguir',
zapsReceived: 'zaps recibidos',
memberSince: 'Miembro desde',
badges: 'Insignias',
diff --git a/src/i18n/eu/batch6.ts b/src/i18n/eu/batch6.ts
index 322a4a7..5eddf2f 100644
--- a/src/i18n/eu/batch6.ts
+++ b/src/i18n/eu/batch6.ts
@@ -962,6 +962,16 @@ const batch6 = {
nip05Verified: "NIP-05 egiaztatua",
nip05NotVerified: "NIP-05 ez da gako honekin egiaztatu",
message: "Mezua",
+ eventsHosted: "Antolatutako ekitaldiak",
+ forSponsors: "Babesleentzat eta lankidetzetarako",
+ sponsorPitch: "Babesa, lankidetza, argazki/bideoa edo ekitaldia? Hasi solasaldia.",
+ startCollab: "Hasi lankidetza-txata",
+ viewRiderCard: "Ikusi patinatzailearen txartela",
+ collabMessage: "Kaixo {{name}}, babes edo lankidetza bati buruz hitz egin nahi nuke.",
+ followers: "Jarraitzaileak",
+ following: "Jarraitzen",
+ follow: "Jarraitu",
+ unfollow: "Utzi jarraitzeari",
zapsReceived: "jasotako zapak",
memberSince: "Kidea noiztik",
badges: "Dominak",
diff --git a/src/i18n/fr/batch4.ts b/src/i18n/fr/batch4.ts
index c42656b..df95192 100644
--- a/src/i18n/fr/batch4.ts
+++ b/src/i18n/fr/batch4.ts
@@ -936,6 +936,16 @@ const frBatch4 = {
nip05Verified: "NIP-05 vérifié",
nip05NotVerified: "NIP-05 non vérifié pour cette clé",
message: "Message",
+ eventsHosted: "Événements organisés",
+ forSponsors: "Pour les sponsors et collaborations",
+ sponsorPitch: "Sponsoring, collaboration, photo/vidéo ou événement ? Lance la conversation.",
+ startCollab: "Démarrer une discussion de collaboration",
+ viewRiderCard: "Voir la fiche du skateur",
+ collabMessage: "Salut {{name}}, j'aimerais parler d'un sponsoring ou d'une collaboration.",
+ followers: "Abonnés",
+ following: "Abonnements",
+ follow: "Suivre",
+ unfollow: "Ne plus suivre",
zapsReceived: "zaps reçus",
memberSince: "Membre depuis",
badges: "Badges",
diff --git a/src/i18n/gl/batch6.ts b/src/i18n/gl/batch6.ts
index ab99777..108b4f0 100644
--- a/src/i18n/gl/batch6.ts
+++ b/src/i18n/gl/batch6.ts
@@ -962,6 +962,16 @@ const batch6 = {
"nip05Verified": "Verificado por NIP-05",
"nip05NotVerified": "NIP-05 non verificado con esta chave",
"message": "Mensaxe",
+ "eventsHosted": "Eventos organizados",
+ "forSponsors": "Para patrocinadores e colaboracións",
+ "sponsorPitch": "Patrocinio, colaboración, foto/vídeo ou evento? Comeza unha conversa.",
+ "startCollab": "Iniciar chat de colaboración",
+ "viewRiderCard": "Ver a ficha do patinador",
+ "collabMessage": "Ola {{name}}, gustaríame falar dun patrocinio ou dunha colaboración.",
+ "followers": "Seguidores",
+ "following": "Seguindo",
+ "follow": "Seguir",
+ "unfollow": "Deixar de seguir",
"zapsReceived": "zaps recibidos",
"memberSince": "Membro desde",
"badges": "Insignias",
diff --git a/src/i18n/hi/batch6.ts b/src/i18n/hi/batch6.ts
index da48249..7305bd2 100644
--- a/src/i18n/hi/batch6.ts
+++ b/src/i18n/hi/batch6.ts
@@ -962,6 +962,16 @@ const batch6 = {
"nip05Verified": "NIP-05 सत्यापित",
"nip05NotVerified": "NIP-05 इस कुंजी से सत्यापित नहीं",
"message": "संदेश",
+ "eventsHosted": "आयोजित कार्यक्रम",
+ "forSponsors": "प्रायोजकों और सहयोगों के लिए",
+ "sponsorPitch": "प्रायोजन, सहयोग, फ़ोटो/वीडियो या कार्यक्रम? बातचीत शुरू करें।",
+ "startCollab": "सहयोग बातचीत शुरू करें",
+ "viewRiderCard": "स्केटर कार्ड देखें",
+ "collabMessage": "नमस्ते {{name}}, क्या हम प्रायोजन या सहयोग पर बात कर सकते हैं?",
+ "followers": "फ़ॉलोअर",
+ "following": "फ़ॉलो कर रहे",
+ "follow": "फ़ॉलो करें",
+ "unfollow": "फ़ॉलो हटाएँ",
"zapsReceived": "zaps प्राप्त",
"memberSince": "सदस्यता से",
"badges": "बैज",
diff --git a/src/i18n/id/batch4.ts b/src/i18n/id/batch4.ts
index 0215384..ea28995 100644
--- a/src/i18n/id/batch4.ts
+++ b/src/i18n/id/batch4.ts
@@ -942,6 +942,16 @@ const idBatch4 = {
nip05Verified: 'Terverifikasi NIP-05',
nip05NotVerified: 'NIP-05 tidak terverifikasi terhadap kunci ini',
message: 'Pesan',
+ eventsHosted: 'Acara yang diselenggarakan',
+ forSponsors: 'Untuk sponsor & kolaborasi',
+ sponsorPitch: 'Sponsorship, kolaborasi, foto/video, atau acara? Mulai percakapan.',
+ startCollab: 'Mulai chat kolaborasi',
+ viewRiderCard: 'Lihat kartu peseluncur',
+ collabMessage: 'Hai {{name}}, saya ingin membahas sponsorship atau kolaborasi.',
+ followers: 'Pengikut',
+ following: 'Mengikuti',
+ follow: 'Ikuti',
+ unfollow: 'Berhenti Ikuti',
zapsReceived: 'zap diterima',
memberSince: 'Anggota sejak',
badges: 'Lencana',
diff --git a/src/i18n/it/batch4.ts b/src/i18n/it/batch4.ts
index 4c1819e..15a0b22 100644
--- a/src/i18n/it/batch4.ts
+++ b/src/i18n/it/batch4.ts
@@ -936,6 +936,16 @@ const itBatch4 = {
nip05Verified: "Verificato NIP-05",
nip05NotVerified: "NIP-05 non verificato con questa chiave",
message: "Messaggio",
+ eventsHosted: "Eventi organizzati",
+ forSponsors: "Per sponsor e collaborazioni",
+ sponsorPitch: "Sponsorizzazione, collaborazione, foto/video o evento? Inizia una conversazione.",
+ startCollab: "Avvia una chat di collaborazione",
+ viewRiderCard: "Vedi la scheda dello skater",
+ collabMessage: "Ciao {{name}}, vorrei parlare di una sponsorizzazione o di una collaborazione.",
+ followers: "Follower",
+ following: "Seguiti",
+ follow: "Segui",
+ unfollow: "Smetti di seguire",
zapsReceived: "zap ricevuti",
memberSince: "Membro dal",
badges: "Distintivi",
diff --git a/src/i18n/ja/batch7.ts b/src/i18n/ja/batch7.ts
index f401d33..dbce5bc 100644
--- a/src/i18n/ja/batch7.ts
+++ b/src/i18n/ja/batch7.ts
@@ -945,6 +945,16 @@ const jaBatch7 = {
nip05Verified: 'NIP-05認証済み',
nip05NotVerified: 'NIP-05がこの鍵で認証されていません',
message: 'メッセージ',
+ eventsHosted: '主催イベント',
+ forSponsors: 'スポンサー・コラボ募集',
+ sponsorPitch: 'スポンサー、コラボ、写真/動画、イベントの依頼はありませんか?まずはご相談ください。',
+ startCollab: 'コラボチャットを始める',
+ viewRiderCard: 'ライダーカードを見る',
+ collabMessage: 'こんにちは{{name}}さん、スポンサーやコラボについてお話ししたいです。',
+ followers: 'フォロワー',
+ following: 'フォロー中',
+ follow: 'フォロー',
+ unfollow: 'フォロー解除',
zapsReceived: '受信したzaps',
memberSince: '登録日',
badges: 'バッジ',
diff --git a/src/i18n/ko/batch4.ts b/src/i18n/ko/batch4.ts
index 4aa87d4..b7fdab0 100644
--- a/src/i18n/ko/batch4.ts
+++ b/src/i18n/ko/batch4.ts
@@ -944,6 +944,16 @@ const koBatch4 = {
nip05Verified: 'NIP-05 인증됨',
nip05NotVerified: '이 키에 대해 NIP-05가 인증되지 않음',
message: '메시지',
+ eventsHosted: '주최한 이벤트',
+ forSponsors: '스폰서 및 협업 문의',
+ sponsorPitch: '스폰서십, 협업, 사진/영상 또는 이벤트? 대화를 시작해 보세요.',
+ startCollab: '협업 채팅 시작',
+ viewRiderCard: '라이더 카드 보기',
+ collabMessage: '안녕하세요 {{name}}님, 스폰서십이나 협업에 대해 이야기하고 싶어요.',
+ followers: '팔로워',
+ following: '팔로잉',
+ follow: '팔로우',
+ unfollow: '언팔로우',
zapsReceived: '받은 Zap',
memberSince: '가입일',
badges: '배지',
diff --git a/src/i18n/nl/batch4.ts b/src/i18n/nl/batch4.ts
index 962419d..20571fe 100644
--- a/src/i18n/nl/batch4.ts
+++ b/src/i18n/nl/batch4.ts
@@ -944,6 +944,16 @@ const nlBatch4 = {
nip05Verified: 'NIP-05 geverifieerd',
nip05NotVerified: 'NIP-05 niet geverifieerd tegen deze sleutel',
message: 'Bericht',
+ eventsHosted: 'Georganiseerde evenementen',
+ forSponsors: 'Voor sponsors & samenwerkingen',
+ sponsorPitch: 'Sponsoring, samenwerking, foto/video of evenement? Start een gesprek.',
+ startCollab: 'Start een samenwerkingschat',
+ viewRiderCard: 'Bekijk de skaterkaart',
+ collabMessage: 'Hoi {{name}}, ik wil graag praten over een sponsoring of samenwerking.',
+ followers: 'Volgers',
+ following: 'Volgend',
+ follow: 'Volgen',
+ unfollow: 'Ontvolgen',
zapsReceived: 'zaps ontvangen',
memberSince: 'Lid sinds',
badges: 'Badges',
diff --git a/src/i18n/pl/batch4.ts b/src/i18n/pl/batch4.ts
index a8aaeff..b0be6a4 100644
--- a/src/i18n/pl/batch4.ts
+++ b/src/i18n/pl/batch4.ts
@@ -936,6 +936,16 @@ const plBatch4 = {
nip05Verified: "Zweryfikowano NIP-05",
nip05NotVerified: "NIP-05 niezweryfikowany względem tego klucza",
message: "Wiadomość",
+ eventsHosted: "Organizowane wydarzenia",
+ forSponsors: "Dla sponsorów i współprac",
+ sponsorPitch: "Sponsoring, współpraca, foto/wideo lub wydarzenie? Rozpocznij rozmowę.",
+ startCollab: "Rozpocznij czat o współpracy",
+ viewRiderCard: "Zobacz kartę deskorolkarza",
+ collabMessage: "Cześć {{name}}, chciałbym porozmawiać o sponsoringu lub współpracy.",
+ followers: "Obserwujący",
+ following: "Obserwowani",
+ follow: "Obserwuj",
+ unfollow: "Przestań obserwować",
zapsReceived: "otrzymanych zapów",
memberSince: "Członek od",
badges: "Odznaki",
diff --git a/src/i18n/pt/batch4.ts b/src/i18n/pt/batch4.ts
index 6625576..2fe7355 100644
--- a/src/i18n/pt/batch4.ts
+++ b/src/i18n/pt/batch4.ts
@@ -1071,6 +1071,16 @@ const ptBatch4 = {
nip05Verified: 'Verificado por NIP-05',
nip05NotVerified: 'NIP-05 não verificado para esta chave',
message: 'Mensagem',
+ eventsHosted: 'Eventos organizados',
+ forSponsors: 'Para patrocinadores e colaborações',
+ sponsorPitch: 'Patrocínio, colaboração, foto/vídeo ou evento? Comece uma conversa.',
+ startCollab: 'Iniciar chat de colaboração',
+ viewRiderCard: 'Ver cartão do skatista',
+ collabMessage: 'Oi {{name}}, quero falar sobre um patrocínio ou uma colaboração.',
+ followers: 'Seguidores',
+ following: 'Seguindo',
+ follow: 'Seguir',
+ unfollow: 'Deixar de seguir',
zapsReceived: 'zaps recebidos',
memberSince: 'Membro desde',
badges: 'Selos',
diff --git a/src/i18n/ro/batch4.ts b/src/i18n/ro/batch4.ts
index 2dcdb51..40985b2 100644
--- a/src/i18n/ro/batch4.ts
+++ b/src/i18n/ro/batch4.ts
@@ -936,6 +936,16 @@ const roBatch4 = {
nip05Verified: 'Verificat NIP-05',
nip05NotVerified: 'NIP-05 neverificat pentru această cheie',
message: 'Mesaj',
+ eventsHosted: 'Evenimente organizate',
+ forSponsors: 'Pentru sponsori și colaborări',
+ sponsorPitch: 'Sponsorizare, colaborare, foto/video sau eveniment? Începe o conversație.',
+ startCollab: 'Începe un chat de colaborare',
+ viewRiderCard: 'Vezi cardul riderului',
+ collabMessage: 'Salut {{name}}, aș vrea să vorbim despre o sponsorizare sau o colaborare.',
+ followers: 'Urmăritori',
+ following: 'Urmărești',
+ follow: 'Urmărește',
+ unfollow: 'Nu mai urmări',
zapsReceived: 'zap-uri primite',
memberSince: 'Membru din',
badges: 'Insigne',
diff --git a/src/i18n/ru/batch4.ts b/src/i18n/ru/batch4.ts
index e085142..b29b6d2 100644
--- a/src/i18n/ru/batch4.ts
+++ b/src/i18n/ru/batch4.ts
@@ -936,6 +936,16 @@ const ruBatch4 = {
nip05Verified: 'NIP-05 подтверждён',
nip05NotVerified: 'NIP-05 не подтверждён для этого ключа',
message: 'Сообщение',
+ eventsHosted: 'Проведённые мероприятия',
+ forSponsors: 'Для спонсоров и коллабораций',
+ sponsorPitch: 'Спонсорство, коллаборация, фото/видео или мероприятие? Начните разговор.',
+ startCollab: 'Начать чат о коллаборации',
+ viewRiderCard: 'Посмотреть карточку райдера',
+ collabMessage: 'Привет, {{name}}! Хочу обсудить спонсорство или коллаборацию.',
+ followers: 'Подписчики',
+ following: 'Подписки',
+ follow: 'Подписаться',
+ unfollow: 'Отписаться',
zapsReceived: 'zaps получено',
memberSince: 'Участник с',
badges: 'Значки',
diff --git a/src/i18n/sv/batch4.ts b/src/i18n/sv/batch4.ts
index 8ea3056..b9d4294 100644
--- a/src/i18n/sv/batch4.ts
+++ b/src/i18n/sv/batch4.ts
@@ -936,6 +936,16 @@ const svBatch4 = {
nip05Verified: "NIP-05 verifierad",
nip05NotVerified: "NIP-05 inte verifierad mot den här nyckeln",
message: "Meddelande",
+ eventsHosted: "Arrangerade evenemang",
+ forSponsors: "För sponsorer & samarbeten",
+ sponsorPitch: "Sponsring, samarbete, foto/video eller evenemang? Starta ett samtal.",
+ startCollab: "Starta en samarbetschatt",
+ viewRiderCard: "Visa skejtarkortet",
+ collabMessage: "Hej {{name}}, jag skulle vilja prata om sponsring eller samarbete.",
+ followers: "Följare",
+ following: "Följer",
+ follow: "Följ",
+ unfollow: "Sluta följa",
zapsReceived: "zaps mottagna",
memberSince: "Medlem sedan",
badges: "Märken",
diff --git a/src/i18n/th/batch9.ts b/src/i18n/th/batch9.ts
index 8bc7e32..4829e53 100644
--- a/src/i18n/th/batch9.ts
+++ b/src/i18n/th/batch9.ts
@@ -1045,6 +1045,16 @@ const thBatch9 = {
nip05Verified: 'ยืนยัน NIP-05 แล้ว',
nip05NotVerified: 'NIP-05 ยังไม่ได้รับการยืนยันกับกุญแจนี้',
message: 'ข้อความ',
+ eventsHosted: 'กิจกรรมที่จัดขึ้น',
+ forSponsors: 'สำหรับสปอนเซอร์และความร่วมมือ',
+ sponsorPitch: 'การสนับสนุน ความร่วมมือ ภาพ/วิดีโอ หรืออีเวนต์? เริ่มคุยกันได้เลย',
+ startCollab: 'เริ่มแชตความร่วมมือ',
+ viewRiderCard: 'ดูการ์ดนักสเก็ต',
+ collabMessage: 'สวัสดี {{name}} ฉันอยากคุยเรื่องการสนับสนุนหรือความร่วมมือ',
+ followers: 'ผู้ติดตาม',
+ following: 'กำลังติดตาม',
+ follow: 'ติดตาม',
+ unfollow: 'เลิกติดตาม',
zapsReceived: 'Zap ที่ได้รับ',
memberSince: 'สมาชิกตั้งแต่',
badges: 'ป้าย',
diff --git a/src/i18n/tr/batch4.ts b/src/i18n/tr/batch4.ts
index e63d3c7..0c57cb2 100644
--- a/src/i18n/tr/batch4.ts
+++ b/src/i18n/tr/batch4.ts
@@ -936,6 +936,16 @@ const trBatch4 = {
nip05Verified: "NIP-05 doğrulandı",
nip05NotVerified: "NIP-05 bu anahtara karşı doğrulanmadı",
message: "Mesaj",
+ eventsHosted: "Düzenlenen etkinlikler",
+ forSponsors: "Sponsorlar ve iş birlikleri için",
+ sponsorPitch: "Sponsorluk, iş birliği, fotoğraf/video veya etkinlik mi? Hemen konuşmaya başla.",
+ startCollab: "İş birliği sohbeti başlat",
+ viewRiderCard: "Rider kartını görüntüle",
+ collabMessage: "Merhaba {{name}}, bir sponsorluk veya iş birliği hakkında konuşmak istiyorum.",
+ followers: "Takipçiler",
+ following: "Takip Edilen",
+ follow: "Takip Et",
+ unfollow: "Takibi Bırak",
zapsReceived: "zap alındı",
memberSince: "Üyelik tarihi",
badges: "Rozetler",
diff --git a/src/i18n/uk/batch4.ts b/src/i18n/uk/batch4.ts
index d902ace..5893bff 100644
--- a/src/i18n/uk/batch4.ts
+++ b/src/i18n/uk/batch4.ts
@@ -936,6 +936,16 @@ const ukBatch4 = {
nip05Verified: "Підтверджено NIP-05",
nip05NotVerified: "NIP-05 не підтверджено для цього ключа",
message: "Повідомлення",
+ eventsHosted: "Проведені події",
+ forSponsors: "Для спонсорів і колаборацій",
+ sponsorPitch: "Спонсорство, колаборація, фото/відео чи подія? Почніть розмову.",
+ startCollab: "Почати чат про колаборацію",
+ viewRiderCard: "Переглянути картку райдера",
+ collabMessage: "Привіт, {{name}}! Хочу обговорити спонсорство або колаборацію.",
+ followers: "Підписники",
+ following: "Підписки",
+ follow: "Підписатися",
+ unfollow: "Відписатися",
zapsReceived: "отримано zaps",
memberSince: "Учасник з",
badges: "Значки",
diff --git a/src/i18n/vi/batch4.ts b/src/i18n/vi/batch4.ts
index 3b0d86a..e572739 100644
--- a/src/i18n/vi/batch4.ts
+++ b/src/i18n/vi/batch4.ts
@@ -944,6 +944,16 @@ const viBatch4 = {
nip05Verified: 'Đã xác minh NIP-05',
nip05NotVerified: 'NIP-05 chưa được xác minh với khóa này',
message: 'Nhắn tin',
+ eventsHosted: 'Sự kiện đã tổ chức',
+ forSponsors: 'Dành cho nhà tài trợ & hợp tác',
+ sponsorPitch: 'Tài trợ, hợp tác, ảnh/video hay sự kiện? Bắt đầu trò chuyện nhé.',
+ startCollab: 'Bắt đầu trò chuyện hợp tác',
+ viewRiderCard: 'Xem thẻ người trượt ván',
+ collabMessage: 'Chào {{name}}, tôi muốn trao đổi về tài trợ hoặc hợp tác.',
+ followers: 'Người theo dõi',
+ following: 'Đang theo dõi',
+ follow: 'Theo dõi',
+ unfollow: 'Bỏ theo dõi',
zapsReceived: 'zap đã nhận',
memberSince: 'Thành viên từ',
badges: 'Huy hiệu',
diff --git a/src/i18n/zh-CN/batch4.ts b/src/i18n/zh-CN/batch4.ts
index 0c95f17..9264038 100644
--- a/src/i18n/zh-CN/batch4.ts
+++ b/src/i18n/zh-CN/batch4.ts
@@ -941,6 +941,16 @@ const zhCNBatch4 = {
nip05Verified: "NIP-05 已验证",
nip05NotVerified: "NIP-05 与此密钥未通过验证",
message: "私信",
+ eventsHosted: "主办的活动",
+ forSponsors: "赞助与合作",
+ sponsorPitch: "赞助、合作、拍摄照片/视频还是办活动?来聊聊吧。",
+ startCollab: "发起合作聊天",
+ viewRiderCard: "查看滑手名片",
+ collabMessage: "你好 {{name}},我想聊聊赞助或合作的事。",
+ followers: "粉丝",
+ following: "关注中",
+ follow: "关注",
+ unfollow: "取消关注",
zapsReceived: "收到的 zaps",
memberSince: "加入于",
badges: "徽章",
diff --git a/src/i18n/zh-TW/batch4.ts b/src/i18n/zh-TW/batch4.ts
index 339cced..c4a0557 100644
--- a/src/i18n/zh-TW/batch4.ts
+++ b/src/i18n/zh-TW/batch4.ts
@@ -941,6 +941,16 @@ const zhTWBatch4 = {
nip05Verified: "NIP-05 已驗證",
nip05NotVerified: "NIP-05 與此金鑰未透過驗證",
message: "私信",
+ eventsHosted: "主辦的活動",
+ forSponsors: "贊助與合作",
+ sponsorPitch: "贊助、合作、拍攝照片/影片還是辦活動?來聊聊吧。",
+ startCollab: "發起合作聊天",
+ viewRiderCard: "查看滑手名片",
+ collabMessage: "你好 {{name}},我想聊聊贊助或合作的事。",
+ followers: "粉絲",
+ following: "追蹤中",
+ follow: "關注",
+ unfollow: "取消關注",
zapsReceived: "收到的 zaps",
memberSince: "加入於",
badges: "徽章",
diff --git a/src/pages/NIP19Page.tsx b/src/pages/NIP19Page.tsx
index 63238d7..aa85d92 100644
--- a/src/pages/NIP19Page.tsx
+++ b/src/pages/NIP19Page.tsx
@@ -16,6 +16,7 @@ import { ClinkPayButton } from '@/components/ClinkPayButton';
import { useAuthor } from '@/hooks/useAuthor';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useUserSkateSpots } from '@/hooks/useSkateSpots';
+import { useUserEvents } from '@/hooks/useSkateEvents';
import { useUserShops } from '@/hooks/useShops';
import { useUserCrews } from '@/hooks/useCrews';
import { useUserPublicStash } from '@/hooks/useStashShowcase';
@@ -28,19 +29,21 @@ import { useCoordinator } from '@/hooks/useCoordinator';
import { coordinatorAddUrl, parseCoordinatorInput, shortNpub, CORDN_RELAY_URL } from '@/lib/coordinator';
import { toast } from '@/hooks/useToast';
import { SpotCard, SpotCardSkeleton } from '@/components/SpotCard';
+import { EventCard } from '@/components/EventCard';
import { genUserName } from '@/lib/genUserName';
import { sanitizeUrl } from '@/lib/sanitizeUrl';
import { cn } from '@/lib/utils';
import { SKATER_STANCES, SKATER_STYLES, SKATER_LEVELS, NEWSLETTER_KIND } from '@/lib/types';
import { NewsletterIssueView } from '@/components/NewsletterIssueView';
import NotFound from './NotFound';
-import { MapPin, Store, MessageCircle, Globe, Zap, ExternalLink, Copy, Check, ShieldCheck, Instagram, Share2, Users, Crown, Bot, Pencil, Package, Tag, ArrowRightLeft, Bitcoin, Award, MessageSquareLock, PersonStanding, Sparkles, Home, Clock, Server, Plus } from 'lucide-react';
+import { MapPin, Store, MessageCircle, Globe, Zap, ExternalLink, Copy, Check, ShieldCheck, ShieldAlert, Loader2, Instagram, Share2, Users, Crown, Bot, Pencil, Package, Tag, ArrowRightLeft, Bitcoin, Award, MessageSquareLock, PersonStanding, Sparkles, Home, Clock, Server, Plus, UserPlus, UserCheck, Handshake } from 'lucide-react';
import { useState, type ReactNode } from 'react';
import { UserStatusIndicator } from '@/components/UserStatusIndicator';
import { ShareProfileDialog } from '@/components/ShareProfileDialog';
import { ProfileBadges } from '@/components/ProfileBadges';
import { SKATESPOTS_BADGE, isSkateSpotsNip05, parseBadges } from '@/lib/badges';
import { useRiderStats } from '@/hooks/useRiderStats';
+import { useFollowers, useFollowing, useFollowActions, useIsFollowing } from '@/hooks/useFollowing';
import { useTranslation } from '@/contexts/language';
/** Format a "member since" year from a kind-0 event timestamp (seconds). */
@@ -122,6 +125,11 @@ function ProfilePage({ pubkey }: { pubkey: string }) {
const { data: shops, isLoading: shopsLoading } = useUserShops(pubkey);
const { memberships: crews, isLoading: crewsLoading } = useUserCrews(pubkey);
const { data: publicStash, isLoading: publicStashLoading } = useUserPublicStash(pubkey);
+ const { data: events = [], isLoading: eventsLoading } = useUserEvents(pubkey);
+ const { data: followers = [], isLoading: followersLoading } = useFollowers(pubkey);
+ const { data: following = [] } = useFollowing(pubkey);
+ const { follow, unfollow, isFollowing: isFollowPending, isUnfollowing } = useFollowActions();
+ const isFollowingProfile = useIsFollowing(pubkey);
const { data: skater } = useUserSkaterProfile(pubkey);
const [copied, setCopied] = useState(false);
@@ -136,7 +144,7 @@ function ProfilePage({ pubkey }: { pubkey: string }) {
const lud16 = metadata?.lud16;
const isBot = metadata?.bot === true;
// Soft NIP-05 verification — confirms the handle resolves to THIS pubkey.
- const { data: nip05Result } = useCrewNip05(nip05, pubkey);
+ const { data: nip05Result, isLoading: nip05Loading } = useCrewNip05(nip05, pubkey);
const nip05Verified = nip05Result?.status === 'verified';
// Custom CLINK offer code stored in the kind 0 content JSON.
const raw = metadata as Record | undefined;
@@ -174,6 +182,9 @@ function ProfilePage({ pubkey }: { pubkey: string }) {
!!stanceMeta || !!levelMeta || !!skater.customLevel || skater.styles.length > 0 ||
!!skater.yearsSkating || !!skater.homeSpot || !!skater.location || !!skater.instagram
);
+ // Only invite sponsor/collab conversations on profiles that actually show
+ // skate activity — an empty kind-0 shouldn't read as a media kit.
+ const sponsorReady = hasSkateLayer || crews.length > 0 || (spots?.length ?? 0) > 0 || events.length > 0;
const handleCopyNpub = () => {
navigator.clipboard.writeText(npub);
@@ -259,13 +270,23 @@ function ProfilePage({ pubkey }: { pubkey: string }) {
)}
title={nip05Verified ? t('walletUi.profile.nip05Verified') : t('walletUi.profile.nip05NotVerified')}
>
- {nip05Verified ? : }
+ {nip05Loading ? (
+
+ ) : nip05Verified ? (
+
+ ) : (
+
+ )}
{nip05}
- {nip05Verified && (
+ {nip05Loading ? (
+
+ {t('auth.verifying')}
+
+ ) : nip05Verified ? (
{t('common.verified')}
- )}
+ ) : null}
)}
{/* Extra verified handles (separate record) — a skater on several domains. */}
@@ -309,7 +330,29 @@ function ProfilePage({ pubkey }: { pubkey: string }) {
{/* Actions */}
-
+
+ {!isOwnProfile && user && (
+
+ )}
{/* Share — available on every profile */}
)}
+ {/* Events hosted by this skater — sponsor/community signal */}
+ {(eventsLoading || events.length > 0) && (
+
+
+
+
+ {t('walletUi.profile.eventsHosted')}
+ {events.length > 0 && (
+
+ {events.length}
+
+ )}
+
+
+ {eventsLoading ? (
+
+ {Array.from({ length: 2 }).map((_, i) => (
+
+ ))}
+
+ ) : (
+
+ {events.slice(0, 4).map((event) => (
+
+ ))}
+
+ )}
+
+ )}
+
{/* Public Stash — gear this skater chose to show publicly */}
{(publicStashLoading || (publicStash && publicStash.length > 0)) && (
@@ -657,11 +738,11 @@ function ProfilePage({ pubkey }: { pubkey: string }) {
>
{g.images[0] ? (
-

) : (
@@ -711,6 +792,55 @@ function ProfilePage({ pubkey }: { pubkey: string }) {
+ {/* Sponsor / collab card — neutral, no assumed opt-in */}
+ {!isOwnProfile && sponsorReady && (
+
+
+
+
+
+
+
+
+
+ {t('walletUi.profile.forSponsors')}
+
+
{t('walletUi.profile.sponsorPitch')}
+
+
+
+
+ {[
+ { label: t('walletUi.profile.followers'), value: followersLoading ? '—' : followers.length },
+ { label: t('nav.events'), value: eventsLoading ? '—' : events.length },
+ { label: t('walletUi.stats.spots'), value: spotsLoading ? '—' : (spots?.length ?? 0) },
+ { label: t('walletUi.profile.zapsReceived'), value: riderStats ? riderStats.zapsReceived : '—' },
+ ].map((stat) => (
+
+
{stat.value}
+
{stat.label}
+
+ ))}
+
+
+
+
+
+
+
+
+ )}
+
{/* Quick Info Card */}