diff --git a/src/components/CrewForm.tsx b/src/components/CrewForm.tsx index 7a6813d..0b46600 100644 --- a/src/components/CrewForm.tsx +++ b/src/components/CrewForm.tsx @@ -26,6 +26,7 @@ import { SKATE_CREW_KIND, SKATE_SPOT_KIND, SKATE_SHOP_KIND, parseAddrCoord, ZAP_ import { CREW_VIBE_KEYS, CREW_FOCUS_KEYS, CREW_FOCUS_DESC_KEYS, CREW_LOOKING_FOR_KEYS, CREW_ROLE_KEYS } from '@/lib/labelKeys'; import { useCrewGoal } from '@/hooks/useCrewGoal'; import { useCoordinator, useCoordinatorHealth } from '@/hooks/useCoordinator'; +import { normalizeUrlInput, sanitizeUrl } from '@/lib/sanitizeUrl'; import { Nip05HandlesEditor } from '@/components/Nip05HandlesEditor'; import { coordinatorAddUrl, crewCoordinatorInputValue, parseCrewCoordinatorInput, shortNpub } from '@/lib/coordinator'; import { useRelays } from '@/lib/relays'; @@ -394,10 +395,14 @@ export function CrewForm({ existing }: CrewFormProps) { }; const addSocial = () => { - const val = socialInput.trim(); + const raw = socialInput.trim(); setSocialInput(''); - if (!val || socials.includes(val)) return; - setSocials((prev) => [...prev, val]); + if (!raw) return; + // Coerce a bare domain to https:// so it renders as a link; keep a non-URL + // (e.g. a @handle) as typed so nothing is silently dropped. + const safe = sanitizeUrl(normalizeUrlInput(raw)) ?? raw; + if (socials.includes(safe)) return; + setSocials((prev) => [...prev, safe]); }; // Crew NIP-05 handles — up to 5, each verified against the founder's key. @@ -550,11 +555,15 @@ export function CrewForm({ existing }: CrewFormProps) { } } if (image) tags.push(['image', image]); - if (website.trim()) tags.push(['website', website.trim()]); + { + const safe = website.trim() ? sanitizeUrl(normalizeUrlInput(website)) : null; + if (website.trim()) tags.push(['website', safe ?? website.trim()]); + } if (instagram.trim()) tags.push(['instagram', instagram.trim()]); if (bandName.trim()) tags.push(['band', bandName.trim()]); - if (bandUrl.trim() && /^https?:\/\//i.test(bandUrl.trim())) { - tags.push(['band-url', bandUrl.trim()]); + { + const safe = bandUrl.trim() ? sanitizeUrl(normalizeUrlInput(bandUrl)) : null; + if (bandUrl.trim()) tags.push(['band-url', safe ?? bandUrl.trim()]); } for (const s of socials) { if (s.trim()) tags.push(['social', s.trim()]); @@ -1275,6 +1284,9 @@ export function CrewForm({ existing }: CrewFormProps) {
setWebsite(e.target.value)} className="bg-gray-800 border-gray-700" /> + {website.trim() && !sanitizeUrl(normalizeUrlInput(website)) && ( +

{t('socialUi.form.invalidUrl')}

+ )}
@@ -1349,6 +1361,9 @@ export function CrewForm({ existing }: CrewFormProps) { autoCapitalize="none" className="bg-gray-800 border-gray-700" /> + {bandUrl.trim() && !sanitizeUrl(normalizeUrlInput(bandUrl)) && ( +

{t('socialUi.form.invalidUrl')}

+ )}
diff --git a/src/i18n/en.ts b/src/i18n/en.ts index ff24bec..219d3fa 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -3922,6 +3922,7 @@ socialUi: { website: 'Website', yourRoles: 'Your roles (Team Leader)', youtubeDetected: 'YouTube video detected', + invalidUrl: 'Enter a valid link (e.g. https://example.com).', }, groupConversation: { seoTitle: 'Group chat - SkateSpots', diff --git a/src/i18n/es.ts b/src/i18n/es.ts index ed88e74..41dba4b 100644 --- a/src/i18n/es.ts +++ b/src/i18n/es.ts @@ -3922,6 +3922,7 @@ socialUi: { website: 'Sitio web', yourRoles: 'Tus roles (líder del equipo)', youtubeDetected: 'Vídeo de YouTube detectado', + invalidUrl: 'Introduce un enlace válido (p. ej. https://ejemplo.com).', }, groupConversation: { seoTitle: 'Chat de grupo - SkateSpots',