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;
|
} = config;
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { user } = useCurrentUser();
|
const { user, metadata } = useCurrentUser();
|
||||||
const author = useAuthor(user?.pubkey || '');
|
const author = useAuthor(user?.pubkey || '');
|
||||||
const { mutateAsync: createEvent, isPending } = useNostrPublish();
|
const { mutateAsync: createEvent, isPending } = useNostrPublish();
|
||||||
const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile();
|
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}`;
|
const entityCoord = `${kind}:${user.pubkey}:${identifier}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { Skeleton } from '@/components/ui/skeleton';
|
||||||
/** The Lab page's own ServiceType union (kept in sync with LabPage). */
|
/** The Lab page's own ServiceType union (kept in sync with LabPage). */
|
||||||
type ServiceType =
|
type ServiceType =
|
||||||
| 'coaching' | 'filming' | 'editing' | 'photography' | 'building'
|
| '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. */
|
/** Translate the Lab page's ServiceType into a crew focus scene. */
|
||||||
function focusForLabService(serviceType: ServiceType): Exclude<CrewFocus, 'mixed'> {
|
function focusForLabService(serviceType: ServiceType): Exclude<CrewFocus, 'mixed'> {
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,7 @@ export function ReportDialog({
|
||||||
if (!user || !selectedReason) return;
|
if (!user || !selectedReason) return;
|
||||||
|
|
||||||
const reason = REPORT_REASONS.find(
|
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;
|
if (!reason) return;
|
||||||
|
|
||||||
|
|
@ -159,7 +159,7 @@ export function ReportDialog({
|
||||||
};
|
};
|
||||||
|
|
||||||
const getReasonKey = (reason: typeof REPORT_REASONS[number]) => {
|
const getReasonKey = (reason: typeof REPORT_REASONS[number]) => {
|
||||||
return reason.subtype || reason.id;
|
return ('subtype' in reason ? reason.subtype : undefined) || reason.id;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
|
|
||||||
|
|
@ -188,7 +188,7 @@ export function SecuritySettings() {
|
||||||
{loginType === 'bunker' && (
|
{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.</>
|
<>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.</>
|
<>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 && (
|
{!loginType && (
|
||||||
|
|
|
||||||
|
|
@ -130,13 +130,14 @@ export function SkateGlobe({
|
||||||
|
|
||||||
if (showSpots && spots) {
|
if (showSpots && spots) {
|
||||||
for (const s of spots) {
|
for (const s of spots) {
|
||||||
if (!s?.tags || !Array.isArray(s.tags)) continue;
|
const evTags = s?.event?.tags;
|
||||||
const c = getCoords(s.tags);
|
if (!evTags || !Array.isArray(evTags)) continue;
|
||||||
|
const c = getCoords(evTags);
|
||||||
if (!c) continue;
|
if (!c) continue;
|
||||||
out.push({
|
out.push({
|
||||||
id: `spot-${s.id}`,
|
id: `spot-${s.id}`,
|
||||||
lat: c.lat, lng: c.lng,
|
lat: c.lat, lng: c.lng,
|
||||||
label: getName(s.tags, 'Unnamed Spot'),
|
label: s.title || getName(evTags, 'Unnamed Spot'),
|
||||||
type: 'spot', spot: s,
|
type: 'spot', spot: s,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -144,13 +145,14 @@ export function SkateGlobe({
|
||||||
|
|
||||||
if (showShops && shops) {
|
if (showShops && shops) {
|
||||||
for (const sh of shops) {
|
for (const sh of shops) {
|
||||||
if (!sh?.tags || !Array.isArray(sh.tags)) continue;
|
const evTags = sh?.event?.tags;
|
||||||
const c = getCoords(sh.tags);
|
if (!evTags || !Array.isArray(evTags)) continue;
|
||||||
|
const c = getCoords(evTags);
|
||||||
if (!c) continue;
|
if (!c) continue;
|
||||||
out.push({
|
out.push({
|
||||||
id: `shop-${sh.id}`,
|
id: `shop-${sh.id}`,
|
||||||
lat: c.lat, lng: c.lng,
|
lat: c.lat, lng: c.lng,
|
||||||
label: getName(sh.tags, 'Skate Shop'),
|
label: sh.name || getName(evTags, 'Skate Shop'),
|
||||||
type: 'shop', shop: sh,
|
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"
|
className="backdrop-blur-sm shadow-lg font-bold text-xs uppercase"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: `${categoryInfo.color}dd`,
|
backgroundColor: `${categoryInfo.color}dd`,
|
||||||
color: categoryInfo.color === '#ccff00' ? 'black' : 'white'
|
color: (categoryInfo.color as string) === '#ccff00' ? 'black' : 'white'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{categoryInfo.emoji} {categoryInfo.label}
|
{categoryInfo.emoji} {categoryInfo.label}
|
||||||
|
|
|
||||||
|
|
@ -513,7 +513,7 @@ export function CashuWallet() {
|
||||||
className="bg-gray-800 border-gray-700 font-mono text-xs"
|
className="bg-gray-800 border-gray-700 font-mono text-xs"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleAddMint}
|
onClick={() => handleAddMint()}
|
||||||
disabled={!mintUrl}
|
disabled={!mintUrl}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
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 { useNostr } from '@nostrify/react';
|
||||||
import { nip57 } from 'nostr-tools';
|
import { nip57 } from 'nostr-tools';
|
||||||
import type { NostrEvent } from '@nostrify/nostrify';
|
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
|
* funds settle to the goal author's own Lightning address. SkateSpots never
|
||||||
* holds funds — it only reads the public tally.
|
* holds funds — it only reads the public tally.
|
||||||
*/
|
*/
|
||||||
export function useCrewGoal(goalId?: string) {
|
export function useCrewGoal(goalId?: string): UseQueryResult<CrewGoal | null> {
|
||||||
const { nostr } = useNostr();
|
const { nostr } = useNostr();
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import { useNostr } from '@nostrify/react';
|
import { useNostr } from '@nostrify/react';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
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 type { NostrEvent } from '@nostrify/nostrify';
|
||||||
import { useNostrLogin } from '@nostrify/react/login';
|
import { useNostrLogin } from '@nostrify/react/login';
|
||||||
|
|
@ -129,21 +129,23 @@ export function useSendDirectMessage() {
|
||||||
content: encryptedRumor,
|
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 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 sealJson = JSON.stringify(sealEvent);
|
||||||
const conversationKey = nip44.v2.utils.getConversationKey(
|
const conversationKey = nip44.v2.utils.getConversationKey(
|
||||||
bytesToHex(ephemeralSecretKey),
|
ephemeralSecretKey,
|
||||||
targetPubkey
|
targetPubkey
|
||||||
);
|
);
|
||||||
const encryptedSeal = nip44.v2.encrypt(sealJson, conversationKey);
|
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({
|
return finalizeEvent({
|
||||||
pubkey: ephemeralPubkey,
|
|
||||||
created_at: randomPastTimestamp(),
|
created_at: randomPastTimestamp(),
|
||||||
kind: GIFT_WRAP_KIND,
|
kind: GIFT_WRAP_KIND,
|
||||||
tags: [['p', targetPubkey]],
|
tags: [['p', targetPubkey]],
|
||||||
|
|
|
||||||
|
|
@ -61,14 +61,14 @@ interface LastListingRecord {
|
||||||
*/
|
*/
|
||||||
export function useListingRestrictions(linkedSpotPubkey?: string): ListingRestrictions {
|
export function useListingRestrictions(linkedSpotPubkey?: string): ListingRestrictions {
|
||||||
const { nostr } = useNostr();
|
const { nostr } = useNostr();
|
||||||
const { user } = useCurrentUser();
|
const { user, metadata } = useCurrentUser();
|
||||||
const [lastListingRecord, setLastListingRecord] = useLocalStorage<LastListingRecord | null>(
|
const [lastListingRecord, setLastListingRecord] = useLocalStorage<LastListingRecord | null>(
|
||||||
'last-listing-record',
|
'last-listing-record',
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check if user has NIP-05 verification
|
// 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)
|
// Check if user is a platform owner (bypasses all restrictions)
|
||||||
const isPlatformOwner = !!user?.pubkey && PLATFORM_OWNER_PUBKEYS.includes(user.pubkey);
|
const isPlatformOwner = !!user?.pubkey && PLATFORM_OWNER_PUBKEYS.includes(user.pubkey);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useNostr } from '@nostrify/react';
|
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 { useQuery } from '@tanstack/react-query';
|
||||||
import { NSchema as n, NostrEvent, NostrMetadata } from '@nostrify/nostrify';
|
import { NSchema as n, NostrEvent, NostrMetadata } from '@nostrify/nostrify';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ export default function BecomeBuilderPage() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { user } = useCurrentUser();
|
const { user, metadata } = useCurrentUser();
|
||||||
const author = useAuthor(user?.pubkey || '');
|
const author = useAuthor(user?.pubkey || '');
|
||||||
const { mutateAsync: createEvent, isPending: isPublishing } = useNostrPublish();
|
const { mutateAsync: createEvent, isPending: isPublishing } = useNostrPublish();
|
||||||
const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile();
|
const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile();
|
||||||
|
|
@ -258,7 +258,7 @@ export default function BecomeBuilderPage() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasNip05 = user?.metadata?.nip05;
|
const hasNip05 = metadata?.nip05;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ export default function BecomeCoachPage() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { user } = useCurrentUser();
|
const { user, metadata } = useCurrentUser();
|
||||||
const author = useAuthor(user?.pubkey || '');
|
const author = useAuthor(user?.pubkey || '');
|
||||||
const { mutateAsync: createEvent, isPending: isPublishing } = useNostrPublish();
|
const { mutateAsync: createEvent, isPending: isPublishing } = useNostrPublish();
|
||||||
const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile();
|
const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile();
|
||||||
|
|
@ -209,7 +209,7 @@ export default function BecomeCoachPage() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasNip05 = user?.metadata?.nip05;
|
const hasNip05 = metadata?.nip05;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ export default function BecomeFilmerPage() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { user } = useCurrentUser();
|
const { user, metadata } = useCurrentUser();
|
||||||
const author = useAuthor(user?.pubkey || '');
|
const author = useAuthor(user?.pubkey || '');
|
||||||
const { mutateAsync: createEvent, isPending: isPublishing } = useNostrPublish();
|
const { mutateAsync: createEvent, isPending: isPublishing } = useNostrPublish();
|
||||||
const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile();
|
const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile();
|
||||||
|
|
@ -225,7 +225,7 @@ export default function BecomeFilmerPage() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasNip05 = user?.metadata?.nip05;
|
const hasNip05 = metadata?.nip05;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ export default function BecomePhotographerPage() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { user } = useCurrentUser();
|
const { user, metadata } = useCurrentUser();
|
||||||
const author = useAuthor(user?.pubkey || '');
|
const author = useAuthor(user?.pubkey || '');
|
||||||
const { mutateAsync: createEvent, isPending: isPublishing } = useNostrPublish();
|
const { mutateAsync: createEvent, isPending: isPublishing } = useNostrPublish();
|
||||||
const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile();
|
const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile();
|
||||||
|
|
@ -275,7 +275,7 @@ export default function BecomePhotographerPage() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasNip05 = user?.metadata?.nip05;
|
const hasNip05 = metadata?.nip05;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
|
|
|
||||||
|
|
@ -138,6 +138,18 @@ export default function CoachDetailPage() {
|
||||||
};
|
};
|
||||||
const accent = ACCENTS[profile.profileType] ?? ACCENTS.coach;
|
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> = {
|
const TYPE_ICONS: Record<string, typeof GraduationCap> = {
|
||||||
coach: GraduationCap, filmer: Video, photographer: Camera, builder: HardHat, editor: Scissors,
|
coach: GraduationCap, filmer: Video, photographer: Camera, builder: HardHat, editor: Scissors,
|
||||||
artist: Palette, graffiti: SprayCan, tattoo: Brush, trainer: Dumbbell, musician: Music, judge: Gavel,
|
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) */}
|
{/* In the Bag — photographer / filmer gear (freeform) */}
|
||||||
{(isPhotographer || isFilmer || isBuilder) && 'gear' in profile && profile.gear && (
|
{(isPhotographer || isFilmer || isBuilder) && pget('gear') && (
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h3 className="text-lg font-bold text-[#ccff00] uppercase mb-2 flex items-center gap-2">
|
<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 ? <HardHat className={`h-4 w-4 ${accent.text}`} /> : <Aperture className={`h-4 w-4 ${accent.text}`} />}
|
||||||
{isBuilder ? 'How We Work' : 'In the Bag'}
|
{isBuilder ? 'How We Work' : 'In the Bag'}
|
||||||
</h3>
|
</h3>
|
||||||
<p className={`text-gray-300 whitespace-pre-wrap text-sm bg-black/50 border rounded-lg p-3 ${accent.border}`}>
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Builder — service area & insured */}
|
{/* 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">
|
<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}`}>
|
<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">
|
<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
|
<MapPin className={`h-3 w-3 ${accent.text}`} /> Service Area
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-sm text-gray-300">{profile.serviceArea}</p>
|
<p className="text-sm text-gray-300">{pget('serviceArea')}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{profile.insured && (
|
{pbool('insured') && (
|
||||||
<div className={`bg-black/50 border rounded-lg p-3 ${accent.border} flex items-center gap-2`}>
|
<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}`} />
|
<HardHat className={`h-4 w-4 ${accent.text}`} />
|
||||||
<span className="text-sm font-bold text-gray-200">Licensed / Insured</span>
|
<span className="text-sm font-bold text-gray-200">Licensed / Insured</span>
|
||||||
|
|
@ -327,17 +339,17 @@ export default function CoachDetailPage() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Tattoo — studio & books-open */}
|
{/* 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">
|
<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}`}>
|
<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">
|
<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
|
<MapPin className={`h-3 w-3 ${accent.text}`} /> Studio
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-sm text-gray-300">{profile.studio}</p>
|
<p className="text-sm text-gray-300">{pget('studio')}</p>
|
||||||
</div>
|
</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`}>
|
<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}`} />
|
<Brush className={`h-4 w-4 ${accent.text}`} />
|
||||||
<span className="text-sm font-bold text-emerald-400">Books Open</span>
|
<span className="text-sm font-bold text-emerald-400">Books Open</span>
|
||||||
|
|
@ -347,17 +359,17 @@ export default function CoachDetailPage() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Trainer — credentials & remote */}
|
{/* 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">
|
<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}`}>
|
<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">
|
<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
|
<Dumbbell className={`h-3 w-3 ${accent.text}`} /> Credentials
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-sm text-gray-300">{profile.credentials}</p>
|
<p className="text-sm text-gray-300">{pget('credentials')}</p>
|
||||||
</div>
|
</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`}>
|
<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}`} />
|
<Globe className={`h-4 w-4 ${accent.text}`} />
|
||||||
<span className="text-sm font-bold text-gray-200">Remote / Online available</span>
|
<span className="text-sm font-bold text-gray-200">Remote / Online available</span>
|
||||||
|
|
@ -367,12 +379,12 @@ export default function CoachDetailPage() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Graffiti — crew name */}
|
{/* Graffiti — crew name */}
|
||||||
{isGraffiti && 'crew' in profile && profile.crew && (
|
{isGraffiti && pget('crew') && (
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h4 className="text-xs font-bold text-[#ccff00] uppercase mb-1 flex items-center gap-1">
|
<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
|
<SprayCan className={`h-3 w-3 ${accent.text}`} /> Graffiti Crew
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-sm text-gray-300">{profile.crew}</p>
|
<p className="text-sm text-gray-300">{pget('crew')}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -523,10 +535,10 @@ export default function CoachDetailPage() {
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isCoach && profile.instagram && (
|
{isCoach && pget('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">
|
<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]" />
|
<Instagram className="h-4 w-4 text-[#ccff00]" />
|
||||||
<span className="truncate">@{profile.instagram.replace('@', '')}</span>
|
<span className="truncate">@{pget('instagram')!.replace('@', '')}</span>
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ function StatCard({
|
||||||
thisMonthDelta,
|
thisMonthDelta,
|
||||||
to,
|
to,
|
||||||
}: {
|
}: {
|
||||||
icon: React.ComponentType<{ className?: string }>;
|
icon: React.ComponentType<{ className?: string; style?: React.CSSProperties }>;
|
||||||
value: number;
|
value: number;
|
||||||
label: string;
|
label: string;
|
||||||
sub?: string;
|
sub?: string;
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,7 @@ export default function ConversationPage() {
|
||||||
if (!newMessage.trim() || !otherPubkey || isSending) return;
|
if (!newMessage.trim() || !otherPubkey || isSending) return;
|
||||||
|
|
||||||
sendMessage(
|
sendMessage(
|
||||||
{ recipientPubkey: otherPubkey, content: newMessage.trim() },
|
{ recipients: otherPubkey, content: newMessage.trim() },
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setNewMessage('');
|
setNewMessage('');
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@ export default function CreateEventPage() {
|
||||||
// "Book for a gig" action on a crew that has a band.
|
// "Book for a gig" action on a crew that has a band.
|
||||||
const prefillType = searchParams.get('type') ?? '';
|
const prefillType = searchParams.get('type') ?? '';
|
||||||
const prefillTitle = searchParams.get('title') ?? '';
|
const prefillTitle = searchParams.get('title') ?? '';
|
||||||
const { user } = useCurrentUser();
|
const { user, metadata } = useCurrentUser();
|
||||||
const { mutateAsync: createEvent, isPending: isPublishing } = useNostrPublish();
|
const { mutateAsync: createEvent, isPending: isPublishing } = useNostrPublish();
|
||||||
const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile();
|
const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
@ -443,7 +443,7 @@ export default function CreateEventPage() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasNip05 = user?.metadata?.nip05;
|
const hasNip05 = metadata?.nip05;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ export default function CreateListingPage() {
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const { user } = useCurrentUser();
|
const { user, metadata } = useCurrentUser();
|
||||||
const { mutateAsync: createEvent, isPending: isPublishing } = useNostrPublish();
|
const { mutateAsync: createEvent, isPending: isPublishing } = useNostrPublish();
|
||||||
const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile();
|
const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
@ -109,7 +109,7 @@ export default function CreateListingPage() {
|
||||||
const [spotSearchQuery, setSpotSearchQuery] = useState('');
|
const [spotSearchQuery, setSpotSearchQuery] = useState('');
|
||||||
|
|
||||||
// Check if user has NIP-05 verification (or is platform owner who bypasses this)
|
// 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
|
// Format cooldown time remaining
|
||||||
const formatCooldownTime = (ms: number | null): string => {
|
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" />
|
<CheckCircle className="h-5 w-5 text-green-400 flex-shrink-0" />
|
||||||
<div>
|
<div>
|
||||||
<span className="font-bold text-green-400">Verified: </span>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import { useTours } from '@/hooks/useTours';
|
||||||
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||||||
import { useFollowing } from '@/hooks/useFollowing';
|
import { useFollowing } from '@/hooks/useFollowing';
|
||||||
import { useBatchTrustScores } from '@/hooks/useBatchTrustScores';
|
import { useBatchTrustScores } from '@/hooks/useBatchTrustScores';
|
||||||
import { useComments } from '@/hooks/useComments';
|
import { useCommentCounts } from '@/hooks/useCommentCounts';
|
||||||
import { useTranslation } from '@/contexts/LanguageContext';
|
import { useTranslation } from '@/contexts/LanguageContext';
|
||||||
import {
|
import {
|
||||||
EVENT_TYPE_LABELS,
|
EVENT_TYPE_LABELS,
|
||||||
|
|
@ -616,7 +616,7 @@ function FilterDrawer({
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: isActive ? info.color : 'transparent',
|
backgroundColor: isActive ? info.color : 'transparent',
|
||||||
borderColor: isActive ? info.color : `${info.color}40`,
|
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}
|
{info.emoji} {info.label}
|
||||||
|
|
@ -837,10 +837,9 @@ export default function EventsPage() {
|
||||||
const tIds = tours?.map((t) => t.event.id) || [];
|
const tIds = tours?.map((t) => t.event.id) || [];
|
||||||
return [...eIds, ...tIds];
|
return [...eIds, ...tIds];
|
||||||
}, [events, tours]);
|
}, [events, tours]);
|
||||||
const { data: allComments } = useComments(allEventIds);
|
const { data: commentCounts } = useCommentCounts(allEventIds);
|
||||||
|
|
||||||
const getCommentCount = (eventId: string) =>
|
const getCommentCount = (eventId: string) => commentCounts?.get(eventId) ?? 0;
|
||||||
allComments?.filter((c) => c.tags.find(([tag]) => tag === 'e')?.[1] === eventId).length || 0;
|
|
||||||
|
|
||||||
// ── Batch RSVP counts (one query for the whole grid, not one per card) ─────────
|
// ── Batch RSVP counts (one query for the whole grid, not one per card) ─────────
|
||||||
const eventCoords = useMemo(() => (events ?? []).map((e) => eventCoord(e)), [events]);
|
const eventCoords = useMemo(() => (events ?? []).map((e) => eventCoord(e)), [events]);
|
||||||
|
|
|
||||||
|
|
@ -386,7 +386,6 @@ interface ToolbarProps {
|
||||||
sortBy: SortOption;
|
sortBy: SortOption;
|
||||||
trustFilter: TrustFilterLevel;
|
trustFilter: TrustFilterLevel;
|
||||||
onClearAll: () => void;
|
onClearAll: () => void;
|
||||||
serviceType: ServiceType;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSkillLabelForType(skill: string, serviceType: ServiceType): string {
|
function getSkillLabelForType(skill: string, serviceType: ServiceType): string {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useSeoMeta } from '@unhead/react';
|
import { useSeoMeta } from '@unhead/react';
|
||||||
import { Layout } from '@/components/Layout';
|
import { Layout } from '@/components/Layout';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Infinity, Scale, Skateboard } from 'lucide-react';
|
import { Infinity, Scale } from 'lucide-react';
|
||||||
|
|
||||||
export default function LicensePage() {
|
export default function LicensePage() {
|
||||||
useSeoMeta({
|
useSeoMeta({
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ import { useFollowing } from '@/hooks/useFollowing';
|
||||||
import { useBatchTrustScores } from '@/hooks/useBatchTrustScores';
|
import { useBatchTrustScores } from '@/hooks/useBatchTrustScores';
|
||||||
import { useBatchSpotStatus } from '@/hooks/useSpotStatus';
|
import { useBatchSpotStatus } from '@/hooks/useSpotStatus';
|
||||||
import { useMultipleSpotWeather } from '@/hooks/useSpotWeather';
|
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 { useStash, STASH_CATEGORIES, CONDITION_LABELS, RARITY_TIERS, type StashItem } from '@/hooks/useStash';
|
||||||
import { useAuthor } from '@/hooks/useAuthor';
|
import { useAuthor } from '@/hooks/useAuthor';
|
||||||
import { useSatsToUsd } from '@/hooks/useExchangeRate';
|
import { useSatsToUsd } from '@/hooks/useExchangeRate';
|
||||||
|
|
@ -737,6 +737,9 @@ export default function MapPage() {
|
||||||
const shopList = (shops || []).map(shop => ({
|
const shopList = (shops || []).map(shop => ({
|
||||||
...shop,
|
...shop,
|
||||||
title: shop.name,
|
title: shop.name,
|
||||||
|
// SkateSpot-shaped fields so shops can share the unified spot list/map.
|
||||||
|
tags: [] as string[],
|
||||||
|
category: 'shop',
|
||||||
types: ['shop' as const],
|
types: ['shop' as const],
|
||||||
isShop: true,
|
isShop: true,
|
||||||
accessType: undefined,
|
accessType: undefined,
|
||||||
|
|
@ -760,13 +763,10 @@ export default function MapPage() {
|
||||||
|
|
||||||
// Fetch comments for popularity sorting
|
// Fetch comments for popularity sorting
|
||||||
const spotIds = useMemo(() => allLocations.map((l) => l.event.id), [allLocations]);
|
const spotIds = useMemo(() => allLocations.map((l) => l.event.id), [allLocations]);
|
||||||
const { data: allComments } = useComments(spotIds);
|
const { data: commentCounts } = useCommentCounts(spotIds);
|
||||||
|
|
||||||
const getCommentCount = (eventId: string) => {
|
const getCommentCount = (eventId: string) => {
|
||||||
return allComments?.filter((c) => {
|
return commentCounts?.get(eventId) ?? 0;
|
||||||
const eTag = c.tags.find(([tag]) => tag === 'e')?.[1];
|
|
||||||
return eTag === eventId;
|
|
||||||
}).length || 0;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get geohashes for weather fetching (only non-shop spots)
|
// Get geohashes for weather fetching (only non-shop spots)
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ function Section({
|
||||||
title,
|
title,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
icon: React.ComponentType<{ className?: string }>;
|
icon: React.ComponentType<{ className?: string; style?: React.CSSProperties }>;
|
||||||
color: string;
|
color: string;
|
||||||
title: string;
|
title: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ export default function ShopDetailPage() {
|
||||||
const { pubkey, identifier } = useParams<{ pubkey: string; identifier: string }>();
|
const { pubkey, identifier } = useParams<{ pubkey: string; identifier: string }>();
|
||||||
const { data: shop, isLoading, error } = useShop(identifier || '', pubkey);
|
const { data: shop, isLoading, error } = useShop(identifier || '', pubkey);
|
||||||
const author = useAuthor(shop?.pubkey);
|
const author = useAuthor(shop?.pubkey);
|
||||||
const { user } = useCurrentUser();
|
const { user, metadata: userMetadata } = useCurrentUser();
|
||||||
const { mutateAsync: deleteEntity } = useDeleteEntity();
|
const { mutateAsync: deleteEntity } = useDeleteEntity();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const navigate = useNavigate();
|
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.
|
// Owner can delete their own shop; a SkateSpots admin can take down any shop.
|
||||||
const canDelete = !!user && !!shop && (isOwner || isAdmin);
|
const canDelete = !!user && !!shop && (isOwner || isAdmin);
|
||||||
const adminTakedown = isAdmin && !isOwner;
|
const adminTakedown = isAdmin && !isOwner;
|
||||||
const hasNip05 = user?.metadata?.nip05;
|
const hasNip05 = userMetadata?.nip05;
|
||||||
|
|
||||||
useSeoMeta({
|
useSeoMeta({
|
||||||
title: shop ? `${shop.name} - SkateSpots` : 'Skate Shop - SkateSpots',
|
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: marketplaceItems, isLoading: isLoadingMarketplace } = useMarketplaceAtSpot(pubkey || '', identifier || '');
|
||||||
const { data: spotServices, isLoading: isLoadingServices } = useSpotServices(pubkey, identifier);
|
const { data: spotServices, isLoading: isLoadingServices } = useSpotServices(pubkey, identifier);
|
||||||
const author = useAuthor(spot?.pubkey);
|
const author = useAuthor(spot?.pubkey);
|
||||||
const { user } = useCurrentUser();
|
const { user, metadata: userMetadata } = useCurrentUser();
|
||||||
const { mutateAsync: deleteEntity } = useDeleteEntity();
|
const { mutateAsync: deleteEntity } = useDeleteEntity();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
@ -140,7 +140,7 @@ export default function SpotDetailPage() {
|
||||||
const canDelete = !!user && !!spot && (isOwner || isAdmin);
|
const canDelete = !!user && !!spot && (isOwner || isAdmin);
|
||||||
const adminTakedown = isAdmin && !isOwner;
|
const adminTakedown = isAdmin && !isOwner;
|
||||||
const isShop = spot?.types.includes('shop');
|
const isShop = spot?.types.includes('shop');
|
||||||
const hasNip05 = user?.metadata?.nip05;
|
const hasNip05 = userMetadata?.nip05;
|
||||||
|
|
||||||
// Community contributions
|
// Community contributions
|
||||||
const { data: communityPhotos } = useSpotPhotos(pubkey || '', identifier || '');
|
const { data: communityPhotos } = useSpotPhotos(pubkey || '', identifier || '');
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ function SectionHeader({
|
||||||
subtitle,
|
subtitle,
|
||||||
color = '#ccff00',
|
color = '#ccff00',
|
||||||
}: {
|
}: {
|
||||||
icon: React.ComponentType<{ className?: string }>;
|
icon: React.ComponentType<{ className?: string; style?: React.CSSProperties }>;
|
||||||
title: string;
|
title: string;
|
||||||
subtitle?: string;
|
subtitle?: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useSeoMeta } from '@unhead/react';
|
import { useSeoMeta } from '@unhead/react';
|
||||||
import { useParams, Link, useNavigate } from 'react-router-dom';
|
import { useParams, Link, useNavigate } from 'react-router-dom';
|
||||||
|
import { nip19 } from 'nostr-tools';
|
||||||
import { Layout } from '@/components/Layout';
|
import { Layout } from '@/components/Layout';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
|
@ -198,12 +199,12 @@ export default function TourDetailPage() {
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<ZapButton
|
<ZapButton
|
||||||
event={tour.event}
|
target={tour.event}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="text-orange-400 hover:text-orange-300 hover:bg-orange-500/10"
|
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">
|
<Button variant="outline" size="sm" className="border-cyan-500/30 text-cyan-400 hover:bg-cyan-500/10">
|
||||||
View Profile
|
View Profile
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ export function TestApp({ children }: TestAppProps) {
|
||||||
|
|
||||||
const defaultConfig: AppConfig = {
|
const defaultConfig: AppConfig = {
|
||||||
theme: 'dark',
|
theme: 'dark',
|
||||||
|
useSkateRelay: true,
|
||||||
relayMetadata: {
|
relayMetadata: {
|
||||||
relays: [
|
relays: [
|
||||||
{ url: 'wss://auth.nostr1.com', read: true, write: true },
|
{ url: 'wss://auth.nostr1.com', read: true, write: true },
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue