feat: sponsor-ready profiles with follow, events and audience stats
This commit is contained in:
parent
9c22bd78f8
commit
bc1987f78c
29 changed files with 438 additions and 10 deletions
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": "बैज",
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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: 'バッジ',
|
||||
|
|
|
|||
|
|
@ -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: '배지',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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: 'Значки',
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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: 'ป้าย',
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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: "Значки",
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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: "徽章",
|
||||
|
|
|
|||
|
|
@ -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: "徽章",
|
||||
|
|
|
|||
|
|
@ -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<string, unknown> | 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 ? <ShieldCheck className="h-3.5 w-3.5" /> : <Check className="h-3 w-3" />}
|
||||
{nip05Loading ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
) : nip05Verified ? (
|
||||
<ShieldCheck className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<ShieldAlert className="h-3.5 w-3.5" />
|
||||
)}
|
||||
{nip05}
|
||||
{nip05Verified && (
|
||||
{nip05Loading ? (
|
||||
<span className="text-[10px] font-bold uppercase tracking-wide text-gray-500">
|
||||
{t('auth.verifying')}
|
||||
</span>
|
||||
) : nip05Verified ? (
|
||||
<span className="text-[10px] font-black uppercase tracking-wide bg-[#ccff00]/15 text-[#ccff00] px-1.5 py-0.5 rounded-full ml-0.5">
|
||||
{t('common.verified')}
|
||||
</span>
|
||||
)}
|
||||
) : null}
|
||||
</p>
|
||||
)}
|
||||
{/* Extra verified handles (separate record) — a skater on several domains. */}
|
||||
|
|
@ -309,7 +330,29 @@ function ProfilePage({ pubkey }: { pubkey: string }) {
|
|||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{!isOwnProfile && user && (
|
||||
<Button
|
||||
variant={isFollowingProfile ? 'outline' : 'default'}
|
||||
onClick={() => {
|
||||
if (isFollowingProfile) void unfollow(pubkey);
|
||||
else void follow(pubkey);
|
||||
}}
|
||||
disabled={isFollowPending || isUnfollowing}
|
||||
className={cn(
|
||||
'gap-2 font-bold',
|
||||
isFollowingProfile
|
||||
? 'border-teal-500/50 text-teal-300 hover:bg-teal-500/10'
|
||||
: 'bg-[#ccff00] text-black hover:bg-[#ccff00]/90',
|
||||
)}
|
||||
aria-label={isFollowingProfile ? t('walletUi.profile.unfollow') : t('walletUi.profile.follow')}
|
||||
>
|
||||
{isFollowingProfile ? <UserCheck className="h-4 w-4" /> : <UserPlus className="h-4 w-4" />}
|
||||
<span className="hidden sm:inline">
|
||||
{isFollowingProfile ? t('walletUi.profile.unfollow') : t('walletUi.profile.follow')}
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
{/* Share — available on every profile */}
|
||||
<Button
|
||||
variant="outline"
|
||||
|
|
@ -346,11 +389,13 @@ function ProfilePage({ pubkey }: { pubkey: string }) {
|
|||
</div>
|
||||
|
||||
{/* ── At-a-glance stats strip ── */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2 sm:gap-3 mb-3">
|
||||
<div className="grid grid-cols-3 sm:grid-cols-6 gap-2 sm:gap-3 mb-3">
|
||||
<StatPill value={spots?.length ?? 0} label={t('walletUi.stats.spots')} color="#ff0040" icon={<MapPin className="h-4 w-4" />} loading={spotsLoading} onClick={() => scrollToSection('section-spots')} />
|
||||
<StatPill value={shops?.length ?? 0} label={t('walletUi.stats.shops')} color="#ccff00" icon={<Store className="h-4 w-4" />} loading={shopsLoading} onClick={() => scrollToSection('section-shops')} />
|
||||
<StatPill value={crews.length} label={t('walletUi.profile.crews')} color="#2dd4bf" icon={<Users className="h-4 w-4" />} loading={crewsLoading} onClick={() => scrollToSection('section-crews')} />
|
||||
<StatPill value={events.length} label={t('nav.events')} color="#ccff00" icon={<Clock className="h-4 w-4" />} loading={eventsLoading} onClick={() => scrollToSection('section-events')} />
|
||||
<StatPill value={publicStash?.length ?? 0} label={t('walletUi.profile.gear')} color="#ff6600" icon={<Package className="h-4 w-4" />} loading={publicStashLoading} onClick={() => scrollToSection('section-stash')} />
|
||||
<StatPill value={followers.length} label={t('walletUi.profile.followers')} color="#ec4899" icon={<Users className="h-4 w-4" />} loading={followersLoading} />
|
||||
</div>
|
||||
|
||||
{/* Secondary meta line: zaps received + member since */}
|
||||
|
|
@ -368,6 +413,12 @@ function ProfilePage({ pubkey }: { pubkey: string }) {
|
|||
{t('walletUi.profile.memberSince')} <span className="font-bold text-gray-300">{memberSince}</span>
|
||||
</span>
|
||||
)}
|
||||
{following.length > 0 && (
|
||||
<span className="flex items-center gap-1.5">
|
||||
<UserPlus className="h-3.5 w-3.5 text-gray-400" />
|
||||
<span className="font-black text-white">{following.length}</span> {t('walletUi.profile.following')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -622,6 +673,36 @@ function ProfilePage({ pubkey }: { pubkey: string }) {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Events hosted by this skater — sponsor/community signal */}
|
||||
{(eventsLoading || events.length > 0) && (
|
||||
<div id="section-events" className="scroll-mt-24">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-xl font-black uppercase flex items-center gap-2">
|
||||
<Clock className="h-5 w-5 text-[#ccff00]" />
|
||||
{t('walletUi.profile.eventsHosted')}
|
||||
{events.length > 0 && (
|
||||
<Badge className="bg-[#ccff00]/20 text-[#ccff00] border-[#ccff00]/50">
|
||||
{events.length}
|
||||
</Badge>
|
||||
)}
|
||||
</h2>
|
||||
</div>
|
||||
{eventsLoading ? (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{Array.from({ length: 2 }).map((_, i) => (
|
||||
<Skeleton key={i} className="h-48 rounded-xl" />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{events.slice(0, 4).map((event) => (
|
||||
<EventCard key={event.id} event={event} compact />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Public Stash — gear this skater chose to show publicly */}
|
||||
{(publicStashLoading || (publicStash && publicStash.length > 0)) && (
|
||||
<div id="section-stash" className="scroll-mt-24">
|
||||
|
|
@ -657,11 +738,11 @@ function ProfilePage({ pubkey }: { pubkey: string }) {
|
|||
>
|
||||
<div className="relative aspect-square bg-gradient-to-br from-[#ff6600]/10 to-[#ff0040]/5 flex items-center justify-center overflow-hidden">
|
||||
{g.images[0] ? (
|
||||
<img
|
||||
<ImageWithFallback
|
||||
src={g.images[0]}
|
||||
alt={g.title}
|
||||
fallback={null}
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
|
||||
loading="lazy"
|
||||
/>
|
||||
) : (
|
||||
<Package className="h-10 w-10 text-[#ff6600]/40" />
|
||||
|
|
@ -711,6 +792,55 @@ function ProfilePage({ pubkey }: { pubkey: string }) {
|
|||
<TradeReputationCard pubkey={pubkey} />
|
||||
<TradeRecommendationsCard pubkey={pubkey} />
|
||||
|
||||
{/* Sponsor / collab card — neutral, no assumed opt-in */}
|
||||
{!isOwnProfile && sponsorReady && (
|
||||
<Card className="relative overflow-hidden border-2 border-pink-500/30 bg-gradient-to-br from-pink-500/10 via-black to-transparent">
|
||||
<div className="absolute -top-12 -right-12 h-36 w-36 rounded-full bg-pink-500 opacity-10 blur-3xl pointer-events-none" />
|
||||
<CardContent className="relative p-4">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="flex h-9 w-9 items-center justify-center rounded-xl bg-pink-500/15 text-pink-300">
|
||||
<Handshake className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-[10px] font-black uppercase tracking-[0.15em] text-pink-300">
|
||||
{t('walletUi.profile.forSponsors')}
|
||||
</p>
|
||||
<p className="text-xs text-gray-400">{t('walletUi.profile.sponsorPitch')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 grid grid-cols-2 gap-2">
|
||||
{[
|
||||
{ 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) => (
|
||||
<div key={stat.label} className="rounded-lg border border-white/10 bg-black/30 px-2.5 py-2">
|
||||
<p className="text-lg font-black text-white leading-none">{stat.value}</p>
|
||||
<p className="mt-0.5 text-[10px] uppercase tracking-wide text-gray-500">{stat.label}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-3 grid grid-cols-1 gap-2">
|
||||
<Button asChild className="w-full justify-center bg-pink-500 font-bold text-black hover:bg-pink-400">
|
||||
<Link to={`/messages/${npub}?message=${encodeURIComponent(t('walletUi.profile.collabMessage', { name: displayName }))}`}>
|
||||
<MessageCircle className="mr-2 h-4 w-4" />
|
||||
{t('walletUi.profile.startCollab')}
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" className="w-full justify-center border-white/15">
|
||||
<Link to={`/card/${npub}`}>
|
||||
<Award className="mr-2 h-4 w-4" />
|
||||
{t('walletUi.profile.viewRiderCard')}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Quick Info Card */}
|
||||
<Card className="bg-black/90 border-2 border-gray-800">
|
||||
<CardHeader className="pb-3">
|
||||
|
|
|
|||
Loading…
Reference in a new issue