@@ -651,6 +653,7 @@ export function CrewForm({ existing }: CrewFormProps) {
)}
+ )}
{/* Members */}
diff --git a/src/components/EditProfileForm.tsx b/src/components/EditProfileForm.tsx
index f81a140..a27cd02 100644
--- a/src/components/EditProfileForm.tsx
+++ b/src/components/EditProfileForm.tsx
@@ -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({
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).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 = { ...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 = () => {
/>
+ {/* CLINK Offer — self-custodial Bitcoin tip code (custom metadata field) */}
+
+
+
+ Bitcoin Tip Code (CLINK) (optional)
+
+
setNoffer(e.target.value)}
+ spellCheck={false}
+ autoCapitalize="none"
+ className="font-mono text-sm"
+ />
+ {nofferInvalid ? (
+
+
+ That doesn't look like a valid noffer1… code.
+
+ ) : nofferValid ? (
+
+
+ Valid — a "Tip in Bitcoin" button will show on your profile.
+
+ ) : (
+
+ 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.{' '}
+
+ Learn more
+
+
+ )}
+
+
- {/* Messages Quick Access - Desktop */}
- {user && (
-
-
-
-
-
- )}
+ {/* Messages lives in the "More" menu — no standalone desktop icon. */}
{/* Collections Quick Access - Desktop */}
{user && (
diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx
index ddbdd01..2629221 100644
--- a/src/components/ui/checkbox.tsx
+++ b/src/components/ui/checkbox.tsx
@@ -19,7 +19,7 @@ const Checkbox = React.forwardRef<
-
+
))
diff --git a/src/lib/types.ts b/src/lib/types.ts
index c9bbc29..a2fc030 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -931,6 +931,14 @@ export const BUILDER_SCALE_LABELS: Record
= {
// ----- 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 = {
street: 'Street',
@@ -943,6 +951,8 @@ export const CREW_VIBE_LABELS: Record = {
girls: 'Girl / Woman',
youth: 'Youth / Groms',
adaptive: 'Adaptive',
+ punk: 'Punk',
+ hiphop: 'Rap / Hip-Hop',
};
/**
diff --git a/src/pages/CreateEventPage.tsx b/src/pages/CreateEventPage.tsx
index f0bcb20..82794a2 100644
--- a/src/pages/CreateEventPage.tsx
+++ b/src/pages/CreateEventPage.tsx
@@ -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({
diff --git a/src/pages/CrewDetailPage.tsx b/src/pages/CrewDetailPage.tsx
index 6c2f36f..70aea0e 100644
--- a/src/pages/CrewDetailPage.tsx
+++ b/src/pages/CrewDetailPage.tsx
@@ -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) */}
- {/* Crew treasury — transparent, zap-funded NIP-75 goal */}
- {crew.goalRef && }
+ {/* Crew treasury — hidden for now (kept in code, re-enable by flipping SHOW_CREW_TREASURY) */}
+ {SHOW_CREW_TREASURY && 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) ? (
-
-
-
- Band ·
- {crew.bandName}
-
-
- ) : (
-
-
-
- Band ·
- {crew.bandName}
-
-
- )
- )}
-
{/* Zap the whole crew — split across founder + confirmed members */}
{crewZapRecipients.length > 1 && (
@@ -550,6 +523,53 @@ export default function CrewDetailPage() {
+ {/* 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 && (
+
+
+
+ Crew Band
+
+
+
+
+
+ 🎸
+
+
+
{crew.bandName}
+
Live music · skate & sound
+
+
+
+ {crew.bandUrl && sanitizeUrl(crew.bandUrl) && (
+
+ Listen to the tunes
+
+ )}
+
+
+ Book them for a gig
+
+
+ Sets up a music event hosted by this crew on the Events calendar —
+ sort the date & venue and it's booked.
+
+
+
+ )}
+
{/* Members */}
{hasMembers && (
| 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 }) {
{/* Tip Card */}
- {!isOwnProfile && profileEvent && (
+ {!isOwnProfile && (profileEvent || nofferCode) && (
- Support this mapper
+ Support this skater
- Send a zap to thank them for their contributions
+ Send a tip to thank them for their contributions
-
+
+ {profileEvent && (
+
+ )}
+ {nofferCode && (
+
+ )}
+
)}