diff --git a/NIP.md b/NIP.md index 60fe62a..05ccec3 100644 --- a/NIP.md +++ b/NIP.md @@ -2022,6 +2022,9 @@ bare `p` tag on the crew is only a claim until the member self-labels. ["a", "30818::"], ["nip05", "barcelonanightcrew@bcnnightcrew.example"], ["website", "https://bcnnightcrew.example"], + ["video", "https://www.youtube.com/watch?v=dQw4w9WgXcQ"], + ["sponsor", "30017::"], + ["coordinator", "https://coordinator.bcnnightcrew.example"], ["noffer", "noffer1qqs..."], ["alt", "Skate crew: Barcelona Night Crew"] ] @@ -2058,6 +2061,7 @@ bare `p` tag on the crew is only a claim until the member self-labels. | `looking-for` | Optional **open call** — a Lab craft the crew is hiring for. Repeatable. Slugs: `filmer`, `photographer`, `editor`, `coach`, `builder`, `artist`, `musician`, `members`. Surfaces the crew's needs so matching Lab providers can reach out. | | `sponsor` | Optional **sponsoring shop** — a NIP-15 stall coordinate (`30017:pubkey:d`) for a local skate shop that backs the crew. Renders a "Sponsored by [Shop]" banner linking to the shop. Only shown when the shop resolves. | | `coordinator` | Optional **encrypted-chat coordinator** for the crew's private group. Shape: `["coordinator", , , ]`. `` is an `http(s)`/`wss` endpoint of a **Cordn** coordinator (an MLS / RFC 9420 Delivery Service reached over ContextVM). The crew event stays public and discoverable; message content, membership secrets and MLS ratchets live client-side. Nothing crew-specific is published to public relays. `` is the coordinator service pubkey (hex) for ContextVM addressing; `` is the human (hex) who runs it, if delegated. | +| `video` | Optional **crew video / montage** ("The Part") — a **YouTube** or **Instagram** URL, rendered as the crew's headline embed. Only these two sources are accepted (skate parts live on YouTube, clips on Instagram); any other URL is ignored. No native/self-hosted video. Clients must parse it to a known embed (YouTube video id / Instagram shortcode) and reject anything else rather than render an untrusted iframe. | ### Crew-Hosted Events diff --git a/src/components/CrewForm.tsx b/src/components/CrewForm.tsx index f43d2cb..0e06b43 100644 --- a/src/components/CrewForm.tsx +++ b/src/components/CrewForm.tsx @@ -16,6 +16,7 @@ import { useAuthor } from '@/hooks/useAuthor'; import { genUserName } from '@/lib/genUserName'; import { SpotPicker } from '@/components/SpotPicker'; import { LabLinkField } from '@/components/LabLinkField'; +import { isCrewVideoUrl, parseCrewVideo } from '@/lib/videoEmbed'; import { NofferField } from '@/components/NofferField'; import { SKATE_CREW_KIND, @@ -36,7 +37,7 @@ import { import { useCrewGoal } from '@/hooks/useCrewGoal'; import { useAppContext } from '@/hooks/useAppContext'; import { cn } from '@/lib/utils'; -import { Users, Upload, X, Loader2, MapPin, Plus, ShieldCheck, AtSign, HardHat, Camera, GraduationCap, Sparkles, Target, Megaphone, Music, Palette, Dumbbell, Eye, Store, Lock } from 'lucide-react'; +import { Users, Upload, X, Loader2, MapPin, Plus, ShieldCheck, AtSign, HardHat, Camera, GraduationCap, Sparkles, Target, Megaphone, Music, Palette, Dumbbell, Eye, Store, Lock, Film } from 'lucide-react'; const FOCUS_ORDER: CrewFocus[] = ['mixed', 'builder', 'photo', 'coaching', 'art', 'music', 'wellness']; const FOCUS_ICONS: Record = { @@ -173,6 +174,8 @@ export function CrewForm({ existing }: CrewFormProps) { // Sponsoring shop coordinate (30017:pubkey:d) and Cordn/MLS coordinator URL. const [sponsorRef, setSponsorRef] = useState(existing?.sponsorRef ?? ''); const [coordinatorUrl, setCoordinatorUrl] = useState(existing?.coordinator?.url ?? ''); + // Crew video / montage — YouTube or Instagram URL ("The Part"). + const [videoUrl, setVideoUrl] = useState(existing?.videoUrl ?? ''); // Crew treasury (NIP-75 zap goal). Load existing goal to prefill when editing. const { data: existingGoal } = useCrewGoal(existing?.goalRef); @@ -404,6 +407,12 @@ export function CrewForm({ existing }: CrewFormProps) { tags.push(['coordinator', coordUrl]); } + // Crew video / montage — only stored if it's a valid YouTube/Instagram URL. + const vid = videoUrl.trim(); + if (vid && isCrewVideoUrl(vid)) { + tags.push(['video', vid]); + } + await createEvent({ kind: SKATE_CREW_KIND, content: bio.trim(), tags }); toast({ title: existing ? 'Crew updated!' : 'Crew created!', description: 'Published to the scene.' }); @@ -742,6 +751,39 @@ export function CrewForm({ existing }: CrewFormProps) { {/* Linked Lab profiles (cross-link) */} + {/* The Part — crew video / montage */} +
+ +

+ Your crew's full part, montage or latest clip. Paste a{' '} + YouTube or{' '} + Instagram link — it'll play right on + your crew page. +

+ setVideoUrl(e.target.value)} + className={cn( + 'bg-gray-800 border-gray-700 text-xs', + videoUrl.trim() && !isCrewVideoUrl(videoUrl) && 'border-red-500/50', + )} + /> + {videoUrl.trim() && ( + isCrewVideoUrl(videoUrl) ? ( +

+ + {parseCrewVideo(videoUrl)?.source === 'youtube' ? 'YouTube video detected' : 'Instagram clip detected'} ✓ +

+ ) : ( +

Only YouTube and Instagram links are supported.

+ ) + )} +
+ {/* Sponsor shop */}