Crew band gig booking, hide treasury, CLINK on profiles, friendlier CLINK, remove desktop messages icon
- Add "punk" + "rap/hip-hop" crew vibe presets - Hide Crew Treasury behind SHOW_CREW_TREASURY flag (detail page + form), code kept - Improve crew band section into a dedicated card with a "Book them for a gig" action that deep-links to a Concert event hosted by the crew - CreateEventPage: prefill event type + title from ?type= / ?title= params - CLINK (a): expose CLINK tip code on user profiles (EditProfileForm field + "Tip in Bitcoin" button on profile view, stored as custom kind 0 field) - CLINK (c): relabel "Pay via CLINK" -> "Tip in Bitcoin" with a "what's this?" tooltip explaining it in plain language - Remove standalone desktop Messages icon (kept in the More menu) Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
parent
d465eabd87
commit
8056b02ad1
9 changed files with 229 additions and 77 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { Zap, Copy, Check, ExternalLink, ShieldCheck } from 'lucide-react';
|
||||
import { Zap, Copy, Check, ExternalLink, ShieldCheck, HelpCircle } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
|
|
@ -8,6 +8,12 @@ import {
|
|||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { isValidNoffer } from '@/lib/types';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
|
@ -21,6 +27,11 @@ interface ClinkPayButtonProps {
|
|||
/** Button variant — defaults to a subtle outline so it stays secondary */
|
||||
variant?: 'default' | 'outline' | 'secondary';
|
||||
size?: 'default' | 'sm' | 'lg';
|
||||
/**
|
||||
* Friendlier button label. Defaults to "Tip in Bitcoin" — CLINK is jargon,
|
||||
* so we lead with what it does and explain the tech in the tooltip/dialog.
|
||||
*/
|
||||
label?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -38,6 +49,7 @@ export function ClinkPayButton({
|
|||
className,
|
||||
variant = 'outline',
|
||||
size = 'default',
|
||||
label = 'Tip in Bitcoin',
|
||||
}: ClinkPayButtonProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
|
@ -90,31 +102,52 @@ export function ClinkPayButton({
|
|||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
'gap-2 border-indigo-500/40 text-indigo-600 hover:bg-indigo-500/10 hover:text-indigo-700 dark:text-indigo-400 dark:hover:text-indigo-300',
|
||||
className,
|
||||
)}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
<Zap className="h-4 w-4" />
|
||||
Pay via CLINK
|
||||
</Button>
|
||||
<div className={cn('flex items-center gap-1.5', className)}>
|
||||
<Button
|
||||
type="button"
|
||||
variant={variant}
|
||||
size={size}
|
||||
className="flex-1 gap-2 border-indigo-500/40 text-indigo-600 hover:bg-indigo-500/10 hover:text-indigo-700 dark:text-indigo-400 dark:hover:text-indigo-300"
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
<Zap className="h-4 w-4" />
|
||||
{label}
|
||||
</Button>
|
||||
<TooltipProvider delayDuration={150}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen(true)}
|
||||
aria-label="What is CLINK?"
|
||||
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-indigo-500/10 hover:text-indigo-500"
|
||||
>
|
||||
<HelpCircle className="h-4 w-4" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" className="max-w-[220px] text-xs">
|
||||
<p className="font-semibold">Pay directly with Bitcoin over Nostr</p>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
CLINK is a self-custodial payment code — no app account, no middleman.
|
||||
Scan it with a compatible Lightning wallet and the tip goes straight to
|
||||
them.
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className="sm:max-w-sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Zap className="h-5 w-5 text-indigo-500" />
|
||||
Pay via CLINK
|
||||
{label}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{recipientName
|
||||
? `Send a self-custodial Lightning payment to ${recipientName}.`
|
||||
: 'Send a self-custodial Lightning payment over Nostr.'}
|
||||
? `Send a self-custodial Bitcoin (Lightning) payment to ${recipientName} — no middleman.`
|
||||
: 'Send a self-custodial Bitcoin (Lightning) payment over Nostr — no middleman.'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import {
|
|||
CREW_FOCUS_DESCRIPTIONS,
|
||||
CREW_FOCUS_TAG,
|
||||
CREW_LOOKING_FOR_OPTIONS,
|
||||
SHOW_CREW_TREASURY,
|
||||
isValidNoffer,
|
||||
type SkateCrew,
|
||||
type CrewFocus,
|
||||
|
|
@ -588,7 +589,8 @@ export function CrewForm({ existing }: CrewFormProps) {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Crew treasury (NIP-75 zap goal) */}
|
||||
{/* Crew treasury (NIP-75 zap goal) — hidden for now via SHOW_CREW_TREASURY */}
|
||||
{SHOW_CREW_TREASURY && (
|
||||
<div className="space-y-3 rounded-lg border border-orange-500/30 bg-orange-500/[0.04] p-4">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
|
|
@ -651,6 +653,7 @@ export function CrewForm({ existing }: CrewFormProps) {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Members */}
|
||||
<div className="space-y-3 rounded-lg border border-gray-700 bg-gray-900/60 p-4">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useEffect, useRef } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||||
|
|
@ -17,10 +17,12 @@ import {
|
|||
import { Input } from '@/components/ui/input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Loader2, Upload } from 'lucide-react';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Loader2, Upload, Zap, ShieldCheck, AlertCircle } from 'lucide-react';
|
||||
import { NSchema as n, type NostrMetadata } from '@nostrify/nostrify';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useUploadFile } from '@/hooks/useUploadFile';
|
||||
import { isValidNoffer } from '@/lib/types';
|
||||
|
||||
export const EditProfileForm: React.FC = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
|
@ -30,6 +32,14 @@ export const EditProfileForm: React.FC = () => {
|
|||
const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile();
|
||||
const { toast } = useToast();
|
||||
|
||||
// CLINK Offer (noffer1...) — a self-custodial Bitcoin tip code. Not part of
|
||||
// the standard NostrMetadata schema, so we keep it in its own state and merge
|
||||
// it into the kind 0 content JSON on save (preserved as a custom field).
|
||||
const [noffer, setNoffer] = useState('');
|
||||
const nofferTrimmed = noffer.trim();
|
||||
const nofferInvalid = nofferTrimmed.length > 0 && !isValidNoffer(nofferTrimmed);
|
||||
const nofferValid = isValidNoffer(nofferTrimmed);
|
||||
|
||||
// Initialize the form with default values
|
||||
const form = useForm<NostrMetadata>({
|
||||
resolver: zodResolver(n.metadata()),
|
||||
|
|
@ -56,6 +66,11 @@ export const EditProfileForm: React.FC = () => {
|
|||
nip05: metadata.nip05 || '',
|
||||
bot: metadata.bot || false,
|
||||
});
|
||||
// Prefill the custom CLINK code from the raw metadata.
|
||||
const existingNoffer = (metadata as Record<string, unknown>).noffer;
|
||||
if (typeof existingNoffer === 'string') {
|
||||
setNoffer(existingNoffer);
|
||||
}
|
||||
}
|
||||
}, [metadata, form]);
|
||||
|
||||
|
|
@ -89,9 +104,18 @@ export const EditProfileForm: React.FC = () => {
|
|||
return;
|
||||
}
|
||||
|
||||
if (nofferInvalid) {
|
||||
toast({
|
||||
title: 'Invalid CLINK code',
|
||||
description: 'That doesn\'t look like a valid noffer1… code. Clear it or paste a valid one.',
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Combine existing metadata with new values
|
||||
const data = { ...metadata, ...values };
|
||||
const data: Record<string, unknown> = { ...metadata, ...values };
|
||||
|
||||
// Clean up empty values
|
||||
for (const key in data) {
|
||||
|
|
@ -100,6 +124,13 @@ export const EditProfileForm: React.FC = () => {
|
|||
}
|
||||
}
|
||||
|
||||
// Merge the custom CLINK offer code (or remove it when cleared).
|
||||
if (nofferValid) {
|
||||
data.noffer = nofferTrimmed.toLowerCase();
|
||||
} else {
|
||||
delete data.noffer;
|
||||
}
|
||||
|
||||
// Publish the metadata event (kind 0) with retry logic
|
||||
let attempts = 0;
|
||||
let lastError;
|
||||
|
|
@ -257,6 +288,47 @@ export const EditProfileForm: React.FC = () => {
|
|||
/>
|
||||
</div>
|
||||
|
||||
{/* CLINK Offer — self-custodial Bitcoin tip code (custom metadata field) */}
|
||||
<div className="space-y-2 rounded-lg border border-indigo-500/30 bg-indigo-500/5 p-4">
|
||||
<Label htmlFor="profile-noffer" className="flex items-center gap-2 text-indigo-600 dark:text-indigo-400">
|
||||
<Zap className="h-4 w-4" />
|
||||
Bitcoin Tip Code (CLINK) <span className="text-xs font-normal text-muted-foreground">(optional)</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="profile-noffer"
|
||||
placeholder="noffer1..."
|
||||
value={noffer}
|
||||
onChange={(e) => setNoffer(e.target.value)}
|
||||
spellCheck={false}
|
||||
autoCapitalize="none"
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
{nofferInvalid ? (
|
||||
<p className="flex items-center gap-1.5 text-xs text-red-500">
|
||||
<AlertCircle className="h-3.5 w-3.5 shrink-0" />
|
||||
That doesn't look like a valid <code className="mx-1">noffer1…</code> code.
|
||||
</p>
|
||||
) : nofferValid ? (
|
||||
<p className="flex items-center gap-1.5 text-xs text-green-600 dark:text-green-400">
|
||||
<ShieldCheck className="h-3.5 w-3.5 shrink-0" />
|
||||
Valid — a "Tip in Bitcoin" button will show on your profile.
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Paste a self-custodial CLINK payment code (from e.g. ShockWallet / Lightning.Pub)
|
||||
so anyone can tip you directly in Bitcoin over Nostr — no middleman.{' '}
|
||||
<a
|
||||
href="https://clinkme.dev"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-indigo-500 underline hover:text-indigo-400"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="bot"
|
||||
|
|
|
|||
|
|
@ -511,23 +511,7 @@ export function Layout({ children, noPadding }: LayoutProps) {
|
|||
</nav>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Messages Quick Access - Desktop */}
|
||||
{user && (
|
||||
<Link to="/messages">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn(
|
||||
isActive('/messages')
|
||||
? "text-[#ff0040] bg-[#ff0040]/10"
|
||||
: "text-gray-400 hover:text-[#ff0040] hover:bg-[#ff0040]/10"
|
||||
)}
|
||||
title="Messages"
|
||||
>
|
||||
<MessageCircle className="h-5 w-5" />
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
{/* Messages lives in the "More" menu — no standalone desktop icon. */}
|
||||
{/* Collections Quick Access - Desktop */}
|
||||
{user && (
|
||||
<Link to="/collections">
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ const Checkbox = React.forwardRef<
|
|||
<CheckboxPrimitive.Indicator
|
||||
className={cn("flex items-center justify-center text-current")}
|
||||
>
|
||||
<Check className="h-4 w-4" />
|
||||
<Check className="h-3.5 w-3.5" strokeWidth={3} />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
))
|
||||
|
|
|
|||
|
|
@ -931,6 +931,14 @@ export const BUILDER_SCALE_LABELS: Record<string, string> = {
|
|||
|
||||
// ----- Crews (community identity, map-anchored) -----
|
||||
|
||||
/**
|
||||
* Feature flag: the Crew Treasury (NIP-75 zap goal) is hidden for now.
|
||||
* The code path is kept intact — flip this to `true` to re-enable the
|
||||
* treasury card on the crew detail page and the treasury section in the
|
||||
* crew form.
|
||||
*/
|
||||
export const SHOW_CREW_TREASURY = false;
|
||||
|
||||
// Crew vibe / focus tags
|
||||
export const CREW_VIBE_LABELS: Record<string, string> = {
|
||||
street: 'Street',
|
||||
|
|
@ -943,6 +951,8 @@ export const CREW_VIBE_LABELS: Record<string, string> = {
|
|||
girls: 'Girl / Woman',
|
||||
youth: 'Youth / Groms',
|
||||
adaptive: 'Adaptive',
|
||||
punk: 'Punk',
|
||||
hiphop: 'Rap / Hip-Hop',
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useSeoMeta } from '@unhead/react';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import ngeohash from 'ngeohash';
|
||||
|
|
@ -60,6 +60,10 @@ export default function CreateEventPage() {
|
|||
? crewParam.split(':')
|
||||
: ['', ''];
|
||||
const { data: hostCrew } = useCrew(crewHostIdentifier, crewHostPubkey);
|
||||
// Optional deep-link prefill: `?type=concert&title=...` — used by e.g. the
|
||||
// "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 { mutateAsync: createEvent, isPending: isPublishing } = useNostrPublish();
|
||||
const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile();
|
||||
|
|
@ -140,6 +144,17 @@ export default function CreateEventPage() {
|
|||
);
|
||||
};
|
||||
|
||||
// Prefill from deep-link params once on mount (e.g. "Book for a gig").
|
||||
useEffect(() => {
|
||||
if (prefillType && EVENT_TYPE_LABELS[prefillType]) {
|
||||
setSelectedTypes((prev) => (prev.includes(prefillType) ? prev : [...prev, prefillType]));
|
||||
}
|
||||
if (prefillTitle) {
|
||||
setTitle((prev) => prev || prefillTitle);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const getCurrentLocation = () => {
|
||||
if (!navigator.geolocation) {
|
||||
toast({
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ import { useCrew } from '@/hooks/useCrews';
|
|||
import { useCrewFeed } from '@/hooks/useCrewFeed';
|
||||
import { useAuthor } from '@/hooks/useAuthor';
|
||||
import { genUserName } from '@/lib/genUserName';
|
||||
import { SKATE_CREW_KIND, SKATE_SPOT_KIND, CREW_VIBE_LABELS } from '@/lib/types';
|
||||
import { SKATE_CREW_KIND, SKATE_SPOT_KIND, CREW_VIBE_LABELS, SHOW_CREW_TREASURY } from '@/lib/types';
|
||||
import { sanitizeUrl, urlDomain } from '@/lib/sanitizeUrl';
|
||||
import { cn } from '@/lib/utils';
|
||||
import NotFound from './NotFound';
|
||||
|
|
@ -330,8 +330,8 @@ export default function CrewDetailPage() {
|
|||
{/* Open call — looking for filmers/editors/members (Lab pipeline) */}
|
||||
<CrewLookingForCard lookingFor={crew.lookingFor} />
|
||||
|
||||
{/* Crew treasury — transparent, zap-funded NIP-75 goal */}
|
||||
{crew.goalRef && <CrewTreasuryCard goalId={crew.goalRef} />}
|
||||
{/* Crew treasury — hidden for now (kept in code, re-enable by flipping SHOW_CREW_TREASURY) */}
|
||||
{SHOW_CREW_TREASURY && crew.goalRef && <CrewTreasuryCard goalId={crew.goalRef} />}
|
||||
|
||||
{/* Crew-hosted events (jams / comps / meetups) */}
|
||||
{((crewEvents && crewEvents.length > 0) || canManage) && (
|
||||
|
|
@ -504,33 +504,6 @@ export default function CrewDetailPage() {
|
|||
);
|
||||
})}
|
||||
|
||||
{/* Crew band / music project */}
|
||||
{crew.bandName && (
|
||||
crew.bandUrl && sanitizeUrl(crew.bandUrl) ? (
|
||||
<a
|
||||
href={sanitizeUrl(crew.bandUrl)!}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 text-sm p-2 bg-purple-500/10 border border-purple-500/25 rounded hover:bg-purple-500/20 transition-colors"
|
||||
title={`Listen: ${urlDomain(crew.bandUrl)}`}
|
||||
>
|
||||
<Music className="h-4 w-4 text-purple-400 shrink-0" />
|
||||
<span className="truncate text-purple-200">
|
||||
<span className="text-purple-400/70">Band · </span>
|
||||
{crew.bandName}
|
||||
</span>
|
||||
</a>
|
||||
) : (
|
||||
<div className="flex items-center gap-2 text-sm p-2 bg-purple-500/10 border border-purple-500/25 rounded">
|
||||
<Music className="h-4 w-4 text-purple-400 shrink-0" />
|
||||
<span className="truncate text-purple-200">
|
||||
<span className="text-purple-400/70">Band · </span>
|
||||
{crew.bandName}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
<div className="pt-3 border-t border-gray-700 space-y-2">
|
||||
{/* Zap the whole crew — split across founder + confirmed members */}
|
||||
{crewZapRecipients.length > 1 && (
|
||||
|
|
@ -550,6 +523,53 @@ export default function CrewDetailPage() {
|
|||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Crew Band — many crews double as a local band. Shown as its own card,
|
||||
with a "Book for a gig" action that deep-links to create a Concert
|
||||
event hosted by this crew (connects bands → the Events calendar). */}
|
||||
{crew.bandName && (
|
||||
<Card className="bg-gradient-to-br from-purple-500/10 to-black/90 border-2 border-purple-500/40">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg text-purple-300 uppercase font-black flex items-center gap-2">
|
||||
<Music className="h-5 w-5 text-purple-400" /> Crew Band
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-xl bg-purple-500/20 text-2xl">
|
||||
🎸
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-base font-black text-white">{crew.bandName}</p>
|
||||
<p className="text-xs text-purple-300/70">Live music · skate & sound</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{crew.bandUrl && sanitizeUrl(crew.bandUrl) && (
|
||||
<a
|
||||
href={sanitizeUrl(crew.bandUrl)!}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center gap-2 rounded-lg border border-purple-500/30 bg-purple-500/10 px-3 py-2 text-sm font-bold text-purple-200 transition-colors hover:bg-purple-500/20"
|
||||
title={`Listen: ${urlDomain(crew.bandUrl)}`}
|
||||
>
|
||||
<Music className="h-4 w-4" /> Listen to the tunes
|
||||
</a>
|
||||
)}
|
||||
|
||||
<Link
|
||||
to={`/create-event?crew=${crew.pubkey}:${crew.identifier}&type=concert&title=${encodeURIComponent(`${crew.bandName} live`)}`}
|
||||
className="flex items-center justify-center gap-2 rounded-lg bg-purple-500 px-3 py-2 text-sm font-black uppercase text-white transition-colors hover:bg-purple-600"
|
||||
>
|
||||
<Calendar className="h-4 w-4" /> Book them for a gig
|
||||
</Link>
|
||||
<p className="text-center text-[11px] leading-snug text-gray-500">
|
||||
Sets up a music event hosted by this crew on the Events calendar —
|
||||
sort the date & venue and it's booked.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Members */}
|
||||
{hasMembers && (
|
||||
<CollectiveMembers
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
|
|||
import { Badge } from '@/components/ui/badge';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { ZapButton } from '@/components/ZapButton';
|
||||
import { ClinkPayButton } from '@/components/ClinkPayButton';
|
||||
import { useAuthor } from '@/hooks/useAuthor';
|
||||
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||||
import { useUserSkateSpots } from '@/hooks/useSkateSpots';
|
||||
|
|
@ -44,6 +45,9 @@ function ProfilePage({ pubkey }: { pubkey: string }) {
|
|||
const website = metadata?.website;
|
||||
const nip05 = metadata?.nip05;
|
||||
const lud16 = metadata?.lud16;
|
||||
// Custom CLINK offer code stored in the kind 0 content JSON.
|
||||
const noffer = (metadata as Record<string, unknown> | undefined)?.noffer;
|
||||
const nofferCode = typeof noffer === 'string' ? noffer : undefined;
|
||||
|
||||
const npub = nip19.npubEncode(pubkey);
|
||||
const isOwnProfile = user?.pubkey === pubkey;
|
||||
|
|
@ -315,18 +319,29 @@ function ProfilePage({ pubkey }: { pubkey: string }) {
|
|||
</Card>
|
||||
|
||||
{/* Tip Card */}
|
||||
{!isOwnProfile && profileEvent && (
|
||||
{!isOwnProfile && (profileEvent || nofferCode) && (
|
||||
<Card className="bg-gradient-to-br from-[#ccff00]/10 to-transparent border-2 border-[#ccff00]/30">
|
||||
<CardContent className="p-4 text-center">
|
||||
<Zap className="h-8 w-8 mx-auto mb-2 text-[#ccff00]" />
|
||||
<h3 className="font-bold text-white mb-1">Support this mapper</h3>
|
||||
<h3 className="font-bold text-white mb-1">Support this skater</h3>
|
||||
<p className="text-xs text-gray-400 mb-3">
|
||||
Send a zap to thank them for their contributions
|
||||
Send a tip to thank them for their contributions
|
||||
</p>
|
||||
<ZapButton
|
||||
target={profileEvent}
|
||||
className="w-full justify-center bg-[#ccff00] text-black hover:bg-[#ccff00]/90 font-bold"
|
||||
/>
|
||||
<div className="space-y-2">
|
||||
{profileEvent && (
|
||||
<ZapButton
|
||||
target={profileEvent}
|
||||
className="w-full justify-center bg-[#ccff00] text-black hover:bg-[#ccff00]/90 font-bold"
|
||||
/>
|
||||
)}
|
||||
{nofferCode && (
|
||||
<ClinkPayButton
|
||||
noffer={nofferCode}
|
||||
recipientName={displayName}
|
||||
className="w-full"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Reference in a new issue