Fix 78 pre-existing TypeScript errors surfaced by npm test
- useDirectMessages: correct nip44 getConversationKey (Uint8Array) + finalizeEvent EventTemplate (remove pubkey/unused ephemeralPubkey/bytesToHex)
- Access current-user metadata via useCurrentUser() spread instead of user.metadata across ~10 files (rename to userMetadata where author metadata already bound)
- useCrewGoal: explicit UseQueryResult<CrewGoal|null> return type (fixes CrewForm/CrewTreasuryCard {} inference)
- New useCommentCounts hook; fix EventsPage/MapPage misuse of useComments(string[])
- CoachDetailPage: typed pget/pbool accessors for craft-specific union fields
- MapPage: shop entries include tags/category to satisfy SkateSpot
- SkateGlobe: read event.tags; LabPage: remove duplicate serviceType; LabCrewsStrip: add 'judging'
- Icon prop widened to accept style; ReportDialog subtype guard; SecuritySettings/TourCard/EventsPage comparisons; CashuWallet onClick; ConversationPage recipients; useLoggedInAccounts NLoginType import; LicensePage icon; TourDetailPage ZapButton target + real npub link; TestApp useSkateRelay
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
parent
635e52df4a
commit
aea66d11e2
31 changed files with 142 additions and 81 deletions
|
|
@ -89,7 +89,7 @@ export function CraftProfileForm({ config }: { config: CraftFormConfig }) {
|
|||
} = config;
|
||||
|
||||
const navigate = useNavigate();
|
||||
const { user } = useCurrentUser();
|
||||
const { user, metadata } = useCurrentUser();
|
||||
const author = useAuthor(user?.pubkey || '');
|
||||
const { mutateAsync: createEvent, isPending } = useNostrPublish();
|
||||
const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile();
|
||||
|
|
@ -252,7 +252,7 @@ export function CraftProfileForm({ config }: { config: CraftFormConfig }) {
|
|||
);
|
||||
}
|
||||
|
||||
const hasNip05 = user?.metadata?.nip05;
|
||||
const hasNip05 = metadata?.nip05;
|
||||
const entityCoord = `${kind}:${user.pubkey}:${identifier}`;
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { Skeleton } from '@/components/ui/skeleton';
|
|||
/** The Lab page's own ServiceType union (kept in sync with LabPage). */
|
||||
type ServiceType =
|
||||
| 'coaching' | 'filming' | 'editing' | 'photography' | 'building'
|
||||
| 'art' | 'graffiti' | 'tattoo' | 'training' | 'music';
|
||||
| 'art' | 'graffiti' | 'tattoo' | 'training' | 'music' | 'judging';
|
||||
|
||||
/** Translate the Lab page's ServiceType into a crew focus scene. */
|
||||
function focusForLabService(serviceType: ServiceType): Exclude<CrewFocus, 'mixed'> {
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ export function ReportDialog({
|
|||
if (!user || !selectedReason) return;
|
||||
|
||||
const reason = REPORT_REASONS.find(
|
||||
(r) => r.id === selectedReason || r.subtype === selectedReason
|
||||
(r) => r.id === selectedReason || ('subtype' in r && r.subtype === selectedReason)
|
||||
);
|
||||
if (!reason) return;
|
||||
|
||||
|
|
@ -159,7 +159,7 @@ export function ReportDialog({
|
|||
};
|
||||
|
||||
const getReasonKey = (reason: typeof REPORT_REASONS[number]) => {
|
||||
return reason.subtype || reason.id;
|
||||
return ('subtype' in reason ? reason.subtype : undefined) || reason.id;
|
||||
};
|
||||
|
||||
if (!user) {
|
||||
|
|
|
|||
|
|
@ -188,7 +188,7 @@ export function SecuritySettings() {
|
|||
{loginType === 'bunker' && (
|
||||
<>You're logged in with a <strong className="text-white">remote signer (bunker)</strong>. Your recovery code is held by your signer service, not SkateSpots. Back it up there.</>
|
||||
)}
|
||||
{loginType === 'extension' && (
|
||||
{(loginType as string) === 'extension' && (
|
||||
<>You're logged in with a <strong className="text-white">browser extension</strong>. Your recovery code lives in the extension — use its own backup/export option. SkateSpots never sees your key.</>
|
||||
)}
|
||||
{!loginType && (
|
||||
|
|
|
|||
|
|
@ -130,13 +130,14 @@ export function SkateGlobe({
|
|||
|
||||
if (showSpots && spots) {
|
||||
for (const s of spots) {
|
||||
if (!s?.tags || !Array.isArray(s.tags)) continue;
|
||||
const c = getCoords(s.tags);
|
||||
const evTags = s?.event?.tags;
|
||||
if (!evTags || !Array.isArray(evTags)) continue;
|
||||
const c = getCoords(evTags);
|
||||
if (!c) continue;
|
||||
out.push({
|
||||
id: `spot-${s.id}`,
|
||||
lat: c.lat, lng: c.lng,
|
||||
label: getName(s.tags, 'Unnamed Spot'),
|
||||
label: s.title || getName(evTags, 'Unnamed Spot'),
|
||||
type: 'spot', spot: s,
|
||||
});
|
||||
}
|
||||
|
|
@ -144,13 +145,14 @@ export function SkateGlobe({
|
|||
|
||||
if (showShops && shops) {
|
||||
for (const sh of shops) {
|
||||
if (!sh?.tags || !Array.isArray(sh.tags)) continue;
|
||||
const c = getCoords(sh.tags);
|
||||
const evTags = sh?.event?.tags;
|
||||
if (!evTags || !Array.isArray(evTags)) continue;
|
||||
const c = getCoords(evTags);
|
||||
if (!c) continue;
|
||||
out.push({
|
||||
id: `shop-${sh.id}`,
|
||||
lat: c.lat, lng: c.lng,
|
||||
label: getName(sh.tags, 'Skate Shop'),
|
||||
label: sh.name || getName(evTags, 'Skate Shop'),
|
||||
type: 'shop', shop: sh,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -229,7 +229,7 @@ export function TourCard({ tour, compact = false }: TourCardProps) {
|
|||
className="backdrop-blur-sm shadow-lg font-bold text-xs uppercase"
|
||||
style={{
|
||||
backgroundColor: `${categoryInfo.color}dd`,
|
||||
color: categoryInfo.color === '#ccff00' ? 'black' : 'white'
|
||||
color: (categoryInfo.color as string) === '#ccff00' ? 'black' : 'white'
|
||||
}}
|
||||
>
|
||||
{categoryInfo.emoji} {categoryInfo.label}
|
||||
|
|
|
|||
|
|
@ -513,7 +513,7 @@ export function CashuWallet() {
|
|||
className="bg-gray-800 border-gray-700 font-mono text-xs"
|
||||
/>
|
||||
<Button
|
||||
onClick={handleAddMint}
|
||||
onClick={() => handleAddMint()}
|
||||
disabled={!mintUrl}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
|
|
|
|||
45
src/hooks/useCommentCounts.ts
Normal file
45
src/hooks/useCommentCounts.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { useNostr } from '@nostrify/react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
/**
|
||||
* Batch comment counts for a list of root event IDs.
|
||||
*
|
||||
* Queries kind 1111 (NIP-22 comments) that reference any of the given event
|
||||
* ids via the uppercase root `#E` tag (and also tallies lowercase `e` for
|
||||
* resilience), returning a Map of eventId -> comment count.
|
||||
*
|
||||
* This replaces the previous (incorrect) usage of `useComments(string[])` —
|
||||
* `useComments` only accepts a SINGLE NostrEvent|URL root, so passing an array
|
||||
* silently produced a bad query and a `.filter` on the wrong shape.
|
||||
*/
|
||||
export function useCommentCounts(eventIds: string[]) {
|
||||
const { nostr } = useNostr();
|
||||
|
||||
// Stable key regardless of order/dupes.
|
||||
const ids = Array.from(new Set(eventIds)).sort();
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['comment-counts', ids],
|
||||
enabled: ids.length > 0,
|
||||
staleTime: 60 * 1000,
|
||||
queryFn: async (c) => {
|
||||
const signal = AbortSignal.any([c.signal, AbortSignal.timeout(6000)]);
|
||||
const events = await nostr.query(
|
||||
[{ kinds: [1111], '#E': ids, limit: 1000 }],
|
||||
{ signal },
|
||||
);
|
||||
|
||||
const counts = new Map<string, number>();
|
||||
for (const ev of events) {
|
||||
// Prefer the uppercase root tag; fall back to lowercase parent tag.
|
||||
const root =
|
||||
ev.tags.find(([n]) => n === 'E')?.[1] ??
|
||||
ev.tags.find(([n]) => n === 'e')?.[1];
|
||||
if (root && ids.includes(root)) {
|
||||
counts.set(root, (counts.get(root) ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
return counts;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useQuery, type UseQueryResult } from '@tanstack/react-query';
|
||||
import { useNostr } from '@nostrify/react';
|
||||
import { nip57 } from 'nostr-tools';
|
||||
import type { NostrEvent } from '@nostrify/nostrify';
|
||||
|
|
@ -62,7 +62,7 @@ function tallyZaps(receipts: NostrEvent[], closedAt?: number): { sats: number; c
|
|||
* funds settle to the goal author's own Lightning address. SkateSpots never
|
||||
* holds funds — it only reads the public tally.
|
||||
*/
|
||||
export function useCrewGoal(goalId?: string) {
|
||||
export function useCrewGoal(goalId?: string): UseQueryResult<CrewGoal | null> {
|
||||
const { nostr } = useNostr();
|
||||
|
||||
return useQuery({
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useCallback, useMemo } from 'react';
|
||||
import { useNostr } from '@nostrify/react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { nip44, nip19, generateSecretKey, getPublicKey, finalizeEvent, getEventHash } from 'nostr-tools';
|
||||
import { nip44, nip19, generateSecretKey, finalizeEvent, getEventHash } from 'nostr-tools';
|
||||
|
||||
import type { NostrEvent } from '@nostrify/nostrify';
|
||||
import { useNostrLogin } from '@nostrify/react/login';
|
||||
|
|
@ -129,21 +129,23 @@ export function useSendDirectMessage() {
|
|||
content: encryptedRumor,
|
||||
});
|
||||
|
||||
// Generate ephemeral keypair for gift wrap
|
||||
// Generate ephemeral secret key for the gift wrap (finalizeEvent derives
|
||||
// the matching pubkey internally).
|
||||
const ephemeralSecretKey = generateSecretKey();
|
||||
const ephemeralPubkey = getPublicKey(ephemeralSecretKey);
|
||||
|
||||
// Encrypt seal to target using ephemeral key
|
||||
// Encrypt seal to target using ephemeral key.
|
||||
// getConversationKey expects the raw secret key bytes (Uint8Array).
|
||||
const sealJson = JSON.stringify(sealEvent);
|
||||
const conversationKey = nip44.v2.utils.getConversationKey(
|
||||
bytesToHex(ephemeralSecretKey),
|
||||
ephemeralSecretKey,
|
||||
targetPubkey
|
||||
);
|
||||
const encryptedSeal = nip44.v2.encrypt(sealJson, conversationKey);
|
||||
|
||||
// Create gift wrap event and sign with ephemeral key
|
||||
// Create gift wrap event and sign with ephemeral key.
|
||||
// finalizeEvent takes an EventTemplate (no pubkey — it derives it from
|
||||
// the secret key) and returns a fully-signed event.
|
||||
return finalizeEvent({
|
||||
pubkey: ephemeralPubkey,
|
||||
created_at: randomPastTimestamp(),
|
||||
kind: GIFT_WRAP_KIND,
|
||||
tags: [['p', targetPubkey]],
|
||||
|
|
|
|||
|
|
@ -61,14 +61,14 @@ interface LastListingRecord {
|
|||
*/
|
||||
export function useListingRestrictions(linkedSpotPubkey?: string): ListingRestrictions {
|
||||
const { nostr } = useNostr();
|
||||
const { user } = useCurrentUser();
|
||||
const { user, metadata } = useCurrentUser();
|
||||
const [lastListingRecord, setLastListingRecord] = useLocalStorage<LastListingRecord | null>(
|
||||
'last-listing-record',
|
||||
null
|
||||
);
|
||||
|
||||
// Check if user has NIP-05 verification
|
||||
const hasNip05 = !!user?.metadata?.nip05;
|
||||
const hasNip05 = !!metadata?.nip05;
|
||||
|
||||
// Check if user is a platform owner (bypasses all restrictions)
|
||||
const isPlatformOwner = !!user?.pubkey && PLATFORM_OWNER_PUBKEYS.includes(user.pubkey);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useNostr } from '@nostrify/react';
|
||||
import { useNostrLogin } from '@nostrify/react/login';
|
||||
import { useNostrLogin, type NLoginType } from '@nostrify/react/login';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { NSchema as n, NostrEvent, NostrMetadata } from '@nostrify/nostrify';
|
||||
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ export default function BecomeBuilderPage() {
|
|||
});
|
||||
|
||||
const navigate = useNavigate();
|
||||
const { user } = useCurrentUser();
|
||||
const { user, metadata } = useCurrentUser();
|
||||
const author = useAuthor(user?.pubkey || '');
|
||||
const { mutateAsync: createEvent, isPending: isPublishing } = useNostrPublish();
|
||||
const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile();
|
||||
|
|
@ -258,7 +258,7 @@ export default function BecomeBuilderPage() {
|
|||
);
|
||||
}
|
||||
|
||||
const hasNip05 = user?.metadata?.nip05;
|
||||
const hasNip05 = metadata?.nip05;
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export default function BecomeCoachPage() {
|
|||
});
|
||||
|
||||
const navigate = useNavigate();
|
||||
const { user } = useCurrentUser();
|
||||
const { user, metadata } = useCurrentUser();
|
||||
const author = useAuthor(user?.pubkey || '');
|
||||
const { mutateAsync: createEvent, isPending: isPublishing } = useNostrPublish();
|
||||
const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile();
|
||||
|
|
@ -209,7 +209,7 @@ export default function BecomeCoachPage() {
|
|||
);
|
||||
}
|
||||
|
||||
const hasNip05 = user?.metadata?.nip05;
|
||||
const hasNip05 = metadata?.nip05;
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export default function BecomeFilmerPage() {
|
|||
});
|
||||
|
||||
const navigate = useNavigate();
|
||||
const { user } = useCurrentUser();
|
||||
const { user, metadata } = useCurrentUser();
|
||||
const author = useAuthor(user?.pubkey || '');
|
||||
const { mutateAsync: createEvent, isPending: isPublishing } = useNostrPublish();
|
||||
const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile();
|
||||
|
|
@ -225,7 +225,7 @@ export default function BecomeFilmerPage() {
|
|||
);
|
||||
}
|
||||
|
||||
const hasNip05 = user?.metadata?.nip05;
|
||||
const hasNip05 = metadata?.nip05;
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ export default function BecomePhotographerPage() {
|
|||
});
|
||||
|
||||
const navigate = useNavigate();
|
||||
const { user } = useCurrentUser();
|
||||
const { user, metadata } = useCurrentUser();
|
||||
const author = useAuthor(user?.pubkey || '');
|
||||
const { mutateAsync: createEvent, isPending: isPublishing } = useNostrPublish();
|
||||
const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile();
|
||||
|
|
@ -275,7 +275,7 @@ export default function BecomePhotographerPage() {
|
|||
);
|
||||
}
|
||||
|
||||
const hasNip05 = user?.metadata?.nip05;
|
||||
const hasNip05 = metadata?.nip05;
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
|
|
|
|||
|
|
@ -138,6 +138,18 @@ export default function CoachDetailPage() {
|
|||
};
|
||||
const accent = ACCENTS[profile.profileType] ?? ACCENTS.coach;
|
||||
|
||||
// The Lab profile is a union of craft-specific shapes. Craft-specific fields
|
||||
// (gear, studio, credentials, instagram, …) only exist on some members, so we
|
||||
// read them through a single typed accessor that returns an optional string
|
||||
// instead of scattering casts across the JSX. Presence is still gated by the
|
||||
// isCoach/isBuilder/… flags before these are shown.
|
||||
const pf = profile as Record<string, unknown>;
|
||||
const pget = (key: string): string | undefined => {
|
||||
const v = pf[key];
|
||||
return typeof v === 'string' && v.length > 0 ? v : undefined;
|
||||
};
|
||||
const pbool = (key: string): boolean => pf[key] === true;
|
||||
|
||||
const TYPE_ICONS: Record<string, typeof GraduationCap> = {
|
||||
coach: GraduationCap, filmer: Video, photographer: Camera, builder: HardHat, editor: Scissors,
|
||||
artist: Palette, graffiti: SprayCan, tattoo: Brush, trainer: Dumbbell, musician: Music, judge: Gavel,
|
||||
|
|
@ -294,30 +306,30 @@ export default function CoachDetailPage() {
|
|||
)}
|
||||
|
||||
{/* In the Bag — photographer / filmer gear (freeform) */}
|
||||
{(isPhotographer || isFilmer || isBuilder) && 'gear' in profile && profile.gear && (
|
||||
{(isPhotographer || isFilmer || isBuilder) && pget('gear') && (
|
||||
<div className="mb-6">
|
||||
<h3 className="text-lg font-bold text-[#ccff00] uppercase mb-2 flex items-center gap-2">
|
||||
{isBuilder ? <HardHat className={`h-4 w-4 ${accent.text}`} /> : <Aperture className={`h-4 w-4 ${accent.text}`} />}
|
||||
{isBuilder ? 'How We Work' : 'In the Bag'}
|
||||
</h3>
|
||||
<p className={`text-gray-300 whitespace-pre-wrap text-sm bg-black/50 border rounded-lg p-3 ${accent.border}`}>
|
||||
{profile.gear}
|
||||
{pget('gear')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Builder — service area & insured */}
|
||||
{isBuilder && 'serviceArea' in profile && (profile.serviceArea || profile.insured) && (
|
||||
{isBuilder && (pget('serviceArea') || pbool('insured')) && (
|
||||
<div className="mb-6 grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
{profile.serviceArea && (
|
||||
{pget('serviceArea') && (
|
||||
<div className={`bg-black/50 border rounded-lg p-3 ${accent.border}`}>
|
||||
<h4 className="text-xs font-bold text-[#ccff00] uppercase mb-1 flex items-center gap-1">
|
||||
<MapPin className={`h-3 w-3 ${accent.text}`} /> Service Area
|
||||
</h4>
|
||||
<p className="text-sm text-gray-300">{profile.serviceArea}</p>
|
||||
<p className="text-sm text-gray-300">{pget('serviceArea')}</p>
|
||||
</div>
|
||||
)}
|
||||
{profile.insured && (
|
||||
{pbool('insured') && (
|
||||
<div className={`bg-black/50 border rounded-lg p-3 ${accent.border} flex items-center gap-2`}>
|
||||
<HardHat className={`h-4 w-4 ${accent.text}`} />
|
||||
<span className="text-sm font-bold text-gray-200">Licensed / Insured</span>
|
||||
|
|
@ -327,17 +339,17 @@ export default function CoachDetailPage() {
|
|||
)}
|
||||
|
||||
{/* Tattoo — studio & books-open */}
|
||||
{isTattoo && (('studio' in profile && profile.studio) || ('booksOpen' in profile && profile.booksOpen)) && (
|
||||
{isTattoo && (pget('studio') || pbool('booksOpen')) && (
|
||||
<div className="mb-6 grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
{'studio' in profile && profile.studio && (
|
||||
{pget('studio') && (
|
||||
<div className={`bg-black/50 border rounded-lg p-3 ${accent.border}`}>
|
||||
<h4 className="text-xs font-bold text-[#ccff00] uppercase mb-1 flex items-center gap-1">
|
||||
<MapPin className={`h-3 w-3 ${accent.text}`} /> Studio
|
||||
</h4>
|
||||
<p className="text-sm text-gray-300">{profile.studio}</p>
|
||||
<p className="text-sm text-gray-300">{pget('studio')}</p>
|
||||
</div>
|
||||
)}
|
||||
{'booksOpen' in profile && profile.booksOpen && (
|
||||
{pbool('booksOpen') && (
|
||||
<div className={`bg-black/50 border rounded-lg p-3 ${accent.border} flex items-center gap-2`}>
|
||||
<Brush className={`h-4 w-4 ${accent.text}`} />
|
||||
<span className="text-sm font-bold text-emerald-400">Books Open</span>
|
||||
|
|
@ -347,17 +359,17 @@ export default function CoachDetailPage() {
|
|||
)}
|
||||
|
||||
{/* Trainer — credentials & remote */}
|
||||
{isTrainer && (('credentials' in profile && profile.credentials) || ('remote' in profile && profile.remote)) && (
|
||||
{isTrainer && (pget('credentials') || pbool('remote')) && (
|
||||
<div className="mb-6 grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
{'credentials' in profile && profile.credentials && (
|
||||
{pget('credentials') && (
|
||||
<div className={`bg-black/50 border rounded-lg p-3 ${accent.border}`}>
|
||||
<h4 className="text-xs font-bold text-[#ccff00] uppercase mb-1 flex items-center gap-1">
|
||||
<Dumbbell className={`h-3 w-3 ${accent.text}`} /> Credentials
|
||||
</h4>
|
||||
<p className="text-sm text-gray-300">{profile.credentials}</p>
|
||||
<p className="text-sm text-gray-300">{pget('credentials')}</p>
|
||||
</div>
|
||||
)}
|
||||
{'remote' in profile && profile.remote && (
|
||||
{pbool('remote') && (
|
||||
<div className={`bg-black/50 border rounded-lg p-3 ${accent.border} flex items-center gap-2`}>
|
||||
<Globe className={`h-4 w-4 ${accent.text}`} />
|
||||
<span className="text-sm font-bold text-gray-200">Remote / Online available</span>
|
||||
|
|
@ -367,12 +379,12 @@ export default function CoachDetailPage() {
|
|||
)}
|
||||
|
||||
{/* Graffiti — crew name */}
|
||||
{isGraffiti && 'crew' in profile && profile.crew && (
|
||||
{isGraffiti && pget('crew') && (
|
||||
<div className="mb-6">
|
||||
<h4 className="text-xs font-bold text-[#ccff00] uppercase mb-1 flex items-center gap-1">
|
||||
<SprayCan className={`h-3 w-3 ${accent.text}`} /> Graffiti Crew
|
||||
</h4>
|
||||
<p className="text-sm text-gray-300">{profile.crew}</p>
|
||||
<p className="text-sm text-gray-300">{pget('crew')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -523,10 +535,10 @@ export default function CoachDetailPage() {
|
|||
</a>
|
||||
)}
|
||||
|
||||
{isCoach && profile.instagram && (
|
||||
<a href={`https://instagram.com/${profile.instagram.replace('@', '')}`} target="_blank" rel="noopener noreferrer" className="flex items-center gap-2 text-sm text-gray-300 p-2 bg-black/50 rounded hover:bg-black/70 transition-colors">
|
||||
{isCoach && pget('instagram') && (
|
||||
<a href={`https://instagram.com/${pget('instagram')!.replace('@', '')}`} target="_blank" rel="noopener noreferrer" className="flex items-center gap-2 text-sm text-gray-300 p-2 bg-black/50 rounded hover:bg-black/70 transition-colors">
|
||||
<Instagram className="h-4 w-4 text-[#ccff00]" />
|
||||
<span className="truncate">@{profile.instagram.replace('@', '')}</span>
|
||||
<span className="truncate">@{pget('instagram')!.replace('@', '')}</span>
|
||||
</a>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ function StatCard({
|
|||
thisMonthDelta,
|
||||
to,
|
||||
}: {
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
icon: React.ComponentType<{ className?: string; style?: React.CSSProperties }>;
|
||||
value: number;
|
||||
label: string;
|
||||
sub?: string;
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ export default function ConversationPage() {
|
|||
if (!newMessage.trim() || !otherPubkey || isSending) return;
|
||||
|
||||
sendMessage(
|
||||
{ recipientPubkey: otherPubkey, content: newMessage.trim() },
|
||||
{ recipients: otherPubkey, content: newMessage.trim() },
|
||||
{
|
||||
onSuccess: () => {
|
||||
setNewMessage('');
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ export default function CreateEventPage() {
|
|||
// "Book for a gig" action on a crew that has a band.
|
||||
const prefillType = searchParams.get('type') ?? '';
|
||||
const prefillTitle = searchParams.get('title') ?? '';
|
||||
const { user } = useCurrentUser();
|
||||
const { user, metadata } = useCurrentUser();
|
||||
const { mutateAsync: createEvent, isPending: isPublishing } = useNostrPublish();
|
||||
const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile();
|
||||
const { toast } = useToast();
|
||||
|
|
@ -443,7 +443,7 @@ export default function CreateEventPage() {
|
|||
);
|
||||
}
|
||||
|
||||
const hasNip05 = user?.metadata?.nip05;
|
||||
const hasNip05 = metadata?.nip05;
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ export default function CreateListingPage() {
|
|||
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { user } = useCurrentUser();
|
||||
const { user, metadata } = useCurrentUser();
|
||||
const { mutateAsync: createEvent, isPending: isPublishing } = useNostrPublish();
|
||||
const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile();
|
||||
const { toast } = useToast();
|
||||
|
|
@ -109,7 +109,7 @@ export default function CreateListingPage() {
|
|||
const [spotSearchQuery, setSpotSearchQuery] = useState('');
|
||||
|
||||
// Check if user has NIP-05 verification (or is platform owner who bypasses this)
|
||||
const hasNip05 = !!user?.metadata?.nip05 || restrictions.isPlatformOwner;
|
||||
const hasNip05 = !!metadata?.nip05 || restrictions.isPlatformOwner;
|
||||
|
||||
// Format cooldown time remaining
|
||||
const formatCooldownTime = (ms: number | null): string => {
|
||||
|
|
@ -461,7 +461,7 @@ export default function CreateListingPage() {
|
|||
<CheckCircle className="h-5 w-5 text-green-400 flex-shrink-0" />
|
||||
<div>
|
||||
<span className="font-bold text-green-400">Verified: </span>
|
||||
<span className="text-gray-300">{user.metadata?.nip05}</span>
|
||||
<span className="text-gray-300">{metadata?.nip05}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import { useTours } from '@/hooks/useTours';
|
|||
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||||
import { useFollowing } from '@/hooks/useFollowing';
|
||||
import { useBatchTrustScores } from '@/hooks/useBatchTrustScores';
|
||||
import { useComments } from '@/hooks/useComments';
|
||||
import { useCommentCounts } from '@/hooks/useCommentCounts';
|
||||
import { useTranslation } from '@/contexts/LanguageContext';
|
||||
import {
|
||||
EVENT_TYPE_LABELS,
|
||||
|
|
@ -616,7 +616,7 @@ function FilterDrawer({
|
|||
style={{
|
||||
backgroundColor: isActive ? info.color : 'transparent',
|
||||
borderColor: isActive ? info.color : `${info.color}40`,
|
||||
color: isActive ? (info.color === '#ccff00' ? 'black' : 'white') : info.color,
|
||||
color: isActive ? ((info.color as string) === '#ccff00' ? 'black' : 'white') : info.color,
|
||||
}}
|
||||
>
|
||||
{info.emoji} {info.label}
|
||||
|
|
@ -837,10 +837,9 @@ export default function EventsPage() {
|
|||
const tIds = tours?.map((t) => t.event.id) || [];
|
||||
return [...eIds, ...tIds];
|
||||
}, [events, tours]);
|
||||
const { data: allComments } = useComments(allEventIds);
|
||||
const { data: commentCounts } = useCommentCounts(allEventIds);
|
||||
|
||||
const getCommentCount = (eventId: string) =>
|
||||
allComments?.filter((c) => c.tags.find(([tag]) => tag === 'e')?.[1] === eventId).length || 0;
|
||||
const getCommentCount = (eventId: string) => commentCounts?.get(eventId) ?? 0;
|
||||
|
||||
// ── Batch RSVP counts (one query for the whole grid, not one per card) ─────────
|
||||
const eventCoords = useMemo(() => (events ?? []).map((e) => eventCoord(e)), [events]);
|
||||
|
|
|
|||
|
|
@ -386,7 +386,6 @@ interface ToolbarProps {
|
|||
sortBy: SortOption;
|
||||
trustFilter: TrustFilterLevel;
|
||||
onClearAll: () => void;
|
||||
serviceType: ServiceType;
|
||||
}
|
||||
|
||||
function getSkillLabelForType(skill: string, serviceType: ServiceType): string {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useSeoMeta } from '@unhead/react';
|
||||
import { Layout } from '@/components/Layout';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Infinity, Scale, Skateboard } from 'lucide-react';
|
||||
import { Infinity, Scale } from 'lucide-react';
|
||||
|
||||
export default function LicensePage() {
|
||||
useSeoMeta({
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ import { useFollowing } from '@/hooks/useFollowing';
|
|||
import { useBatchTrustScores } from '@/hooks/useBatchTrustScores';
|
||||
import { useBatchSpotStatus } from '@/hooks/useSpotStatus';
|
||||
import { useMultipleSpotWeather } from '@/hooks/useSpotWeather';
|
||||
import { useComments } from '@/hooks/useComments';
|
||||
import { useCommentCounts } from '@/hooks/useCommentCounts';
|
||||
import { useStash, STASH_CATEGORIES, CONDITION_LABELS, RARITY_TIERS, type StashItem } from '@/hooks/useStash';
|
||||
import { useAuthor } from '@/hooks/useAuthor';
|
||||
import { useSatsToUsd } from '@/hooks/useExchangeRate';
|
||||
|
|
@ -737,6 +737,9 @@ export default function MapPage() {
|
|||
const shopList = (shops || []).map(shop => ({
|
||||
...shop,
|
||||
title: shop.name,
|
||||
// SkateSpot-shaped fields so shops can share the unified spot list/map.
|
||||
tags: [] as string[],
|
||||
category: 'shop',
|
||||
types: ['shop' as const],
|
||||
isShop: true,
|
||||
accessType: undefined,
|
||||
|
|
@ -760,13 +763,10 @@ export default function MapPage() {
|
|||
|
||||
// Fetch comments for popularity sorting
|
||||
const spotIds = useMemo(() => allLocations.map((l) => l.event.id), [allLocations]);
|
||||
const { data: allComments } = useComments(spotIds);
|
||||
const { data: commentCounts } = useCommentCounts(spotIds);
|
||||
|
||||
const getCommentCount = (eventId: string) => {
|
||||
return allComments?.filter((c) => {
|
||||
const eTag = c.tags.find(([tag]) => tag === 'e')?.[1];
|
||||
return eTag === eventId;
|
||||
}).length || 0;
|
||||
return commentCounts?.get(eventId) ?? 0;
|
||||
};
|
||||
|
||||
// Get geohashes for weather fetching (only non-shop spots)
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ function Section({
|
|||
title,
|
||||
children,
|
||||
}: {
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
icon: React.ComponentType<{ className?: string; style?: React.CSSProperties }>;
|
||||
color: string;
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ export default function ShopDetailPage() {
|
|||
const { pubkey, identifier } = useParams<{ pubkey: string; identifier: string }>();
|
||||
const { data: shop, isLoading, error } = useShop(identifier || '', pubkey);
|
||||
const author = useAuthor(shop?.pubkey);
|
||||
const { user } = useCurrentUser();
|
||||
const { user, metadata: userMetadata } = useCurrentUser();
|
||||
const { mutateAsync: deleteEntity } = useDeleteEntity();
|
||||
const { toast } = useToast();
|
||||
const navigate = useNavigate();
|
||||
|
|
@ -93,7 +93,7 @@ export default function ShopDetailPage() {
|
|||
// Owner can delete their own shop; a SkateSpots admin can take down any shop.
|
||||
const canDelete = !!user && !!shop && (isOwner || isAdmin);
|
||||
const adminTakedown = isAdmin && !isOwner;
|
||||
const hasNip05 = user?.metadata?.nip05;
|
||||
const hasNip05 = userMetadata?.nip05;
|
||||
|
||||
useSeoMeta({
|
||||
title: shop ? `${shop.name} - SkateSpots` : 'Skate Shop - SkateSpots',
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ export default function SpotDetailPage() {
|
|||
const { data: marketplaceItems, isLoading: isLoadingMarketplace } = useMarketplaceAtSpot(pubkey || '', identifier || '');
|
||||
const { data: spotServices, isLoading: isLoadingServices } = useSpotServices(pubkey, identifier);
|
||||
const author = useAuthor(spot?.pubkey);
|
||||
const { user } = useCurrentUser();
|
||||
const { user, metadata: userMetadata } = useCurrentUser();
|
||||
const { mutateAsync: deleteEntity } = useDeleteEntity();
|
||||
const { toast } = useToast();
|
||||
const navigate = useNavigate();
|
||||
|
|
@ -140,7 +140,7 @@ export default function SpotDetailPage() {
|
|||
const canDelete = !!user && !!spot && (isOwner || isAdmin);
|
||||
const adminTakedown = isAdmin && !isOwner;
|
||||
const isShop = spot?.types.includes('shop');
|
||||
const hasNip05 = user?.metadata?.nip05;
|
||||
const hasNip05 = userMetadata?.nip05;
|
||||
|
||||
// Community contributions
|
||||
const { data: communityPhotos } = useSpotPhotos(pubkey || '', identifier || '');
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ function SectionHeader({
|
|||
subtitle,
|
||||
color = '#ccff00',
|
||||
}: {
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
icon: React.ComponentType<{ className?: string; style?: React.CSSProperties }>;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
color?: string;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useSeoMeta } from '@unhead/react';
|
||||
import { useParams, Link, useNavigate } from 'react-router-dom';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import { Layout } from '@/components/Layout';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
|
@ -198,12 +199,12 @@ export default function TourDetailPage() {
|
|||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<ZapButton
|
||||
event={tour.event}
|
||||
target={tour.event}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-orange-400 hover:text-orange-300 hover:bg-orange-500/10"
|
||||
/>
|
||||
<Link to={`/npub1${tour.pubkey.slice(0, 8)}...`}>
|
||||
<Link to={`/${nip19.npubEncode(tour.pubkey)}`}>
|
||||
<Button variant="outline" size="sm" className="border-cyan-500/30 text-cyan-400 hover:bg-cyan-500/10">
|
||||
View Profile
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ export function TestApp({ children }: TestAppProps) {
|
|||
|
||||
const defaultConfig: AppConfig = {
|
||||
theme: 'dark',
|
||||
useSkateRelay: true,
|
||||
relayMetadata: {
|
||||
relays: [
|
||||
{ url: 'wss://auth.nostr1.com', read: true, write: true },
|
||||
|
|
|
|||
Loading…
Reference in a new issue