feat(crew): tidy the create-crew "Extras" step into collapsible groups
The Extras step stacked 9 ungrouped blocks. Now: - Identity (NIP-05, links) — open by default. - Sasquatch AI — open when the agent is on. - Showcase (video, sponsor, band). - Payments & treasury (treasury + the CLINK tip jar, moved out of "Links"). - Integrations (Lab link). - Advanced (Coordinator) — collapsed + last, so a new founder isn't asked about experimental plumbing. Other fixes: - The crew AI agent is now labelled "Sasquatch · Pro" (it runs on the same Pro-gated Sasquatch proxy, falling back to a free model), with an honest note. - crew founded year is clamped to 1954..current on save and bounded in the input. - "Skip to end" is no longer hidden on mobile. - Removed the coordinator copy keys left unused by the previous pass.
This commit is contained in:
parent
c93d7e4e41
commit
0548a7a7c6
3 changed files with 112 additions and 32 deletions
|
|
@ -3,6 +3,7 @@ import { nip19 } from 'nostr-tools';
|
||||||
import ngeohash from 'ngeohash';
|
import ngeohash from 'ngeohash';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '@/components/ui/collapsible';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
|
@ -29,7 +30,7 @@ import { Nip05HandlesEditor } from '@/components/Nip05HandlesEditor';
|
||||||
import { coordinatorAddUrl, crewCoordinatorInputValue, parseCrewCoordinatorInput, shortNpub } from '@/lib/coordinator';
|
import { coordinatorAddUrl, crewCoordinatorInputValue, parseCrewCoordinatorInput, shortNpub } from '@/lib/coordinator';
|
||||||
import { useRelays } from '@/lib/relays';
|
import { useRelays } from '@/lib/relays';
|
||||||
import { cn } from '@/lib/utils';
|
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, Film, Check, ArrowRight, ArrowLeft as ArrowLeftIcon, Server } from 'lucide-react';
|
import { Users, Upload, X, Loader2, MapPin, Plus, ShieldCheck, AtSign, HardHat, Camera, GraduationCap, Sparkles, Target, Megaphone, Music, Palette, Dumbbell, Eye, Store, Film, Check, ArrowRight, ArrowLeft as ArrowLeftIcon, Server, ChevronDown, Zap } from 'lucide-react';
|
||||||
|
|
||||||
const FOCUS_ORDER: CrewFocus[] = ['mixed', 'builder', 'photo', 'coaching', 'art', 'music', 'wellness'];
|
const FOCUS_ORDER: CrewFocus[] = ['mixed', 'builder', 'photo', 'coaching', 'art', 'music', 'wellness'];
|
||||||
const FOCUS_ICONS: Record<CrewFocus, typeof Users> = {
|
const FOCUS_ICONS: Record<CrewFocus, typeof Users> = {
|
||||||
|
|
@ -201,6 +202,37 @@ const HEALTH_LABEL: Record<string, string> = {
|
||||||
error: 'socialUi.coordinator.statusError',
|
error: 'socialUi.coordinator.statusError',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A collapsible group for the "Extras" step, so optional power-ups don't wall off
|
||||||
|
* a new founder. Header is the group label; the blocks inside keep their own.
|
||||||
|
*/
|
||||||
|
function ExtrasGroup({
|
||||||
|
icon: Icon,
|
||||||
|
title,
|
||||||
|
hint,
|
||||||
|
defaultOpen = false,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
icon: typeof Users;
|
||||||
|
title: string;
|
||||||
|
hint?: string;
|
||||||
|
defaultOpen?: boolean;
|
||||||
|
children: ReactNode;
|
||||||
|
}) {
|
||||||
|
const [open, setOpen] = useState(defaultOpen);
|
||||||
|
return (
|
||||||
|
<Collapsible open={open} onOpenChange={setOpen} className="rounded-xl border border-white/10 bg-white/[0.02]">
|
||||||
|
<CollapsibleTrigger className="flex w-full items-center gap-2 p-3 text-left">
|
||||||
|
<Icon className="h-4 w-4 shrink-0 text-teal-400" />
|
||||||
|
<span className="flex-1 text-sm font-black uppercase tracking-wide text-gray-200">{title}</span>
|
||||||
|
{hint && <span className="hidden text-[10px] text-gray-500 sm:block">{hint}</span>}
|
||||||
|
<ChevronDown className={cn('h-4 w-4 shrink-0 text-gray-500 transition-transform', open && 'rotate-180')} />
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent className="space-y-4 px-3 pb-3">{children}</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function CrewForm({ existing }: CrewFormProps) {
|
export function CrewForm({ existing }: CrewFormProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
@ -510,7 +542,13 @@ export function CrewForm({ existing }: CrewFormProps) {
|
||||||
if (focus !== 'mixed') tags.push(['t', CREW_FOCUS_TAG[focus]]);
|
if (focus !== 'mixed') tags.push(['t', CREW_FOCUS_TAG[focus]]);
|
||||||
// Crew AI agent — queryable marker tag when enabled.
|
// Crew AI agent — queryable marker tag when enabled.
|
||||||
if (agentEnabled) tags.push(['t', 'crew-agent']);
|
if (agentEnabled) tags.push(['t', 'crew-agent']);
|
||||||
if (founded.trim()) tags.push(['founded', founded.trim()]);
|
if (founded.trim()) {
|
||||||
|
const year = parseInt(founded, 10);
|
||||||
|
const maxYear = new Date().getFullYear();
|
||||||
|
if (Number.isFinite(year)) {
|
||||||
|
tags.push(['founded', String(Math.min(maxYear, Math.max(1954, year)))]);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (image) tags.push(['image', image]);
|
if (image) tags.push(['image', image]);
|
||||||
if (website.trim()) tags.push(['website', website.trim()]);
|
if (website.trim()) tags.push(['website', website.trim()]);
|
||||||
if (instagram.trim()) tags.push(['instagram', instagram.trim()]);
|
if (instagram.trim()) tags.push(['instagram', instagram.trim()]);
|
||||||
|
|
@ -671,7 +709,7 @@ export function CrewForm({ existing }: CrewFormProps) {
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="crew-founded">{t('socialUi.form.founded')}</Label>
|
<Label htmlFor="crew-founded">{t('socialUi.form.founded')}</Label>
|
||||||
<Input id="crew-founded" type="number" placeholder="2015" value={founded} onChange={(e) => setFounded(e.target.value)} className="bg-gray-800 border-gray-700" />
|
<Input id="crew-founded" type="number" min={1954} max={new Date().getFullYear()} placeholder="2015" value={founded} onChange={(e) => setFounded(e.target.value)} className="bg-gray-800 border-gray-700" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -816,13 +854,15 @@ export function CrewForm({ existing }: CrewFormProps) {
|
||||||
className="h-5 w-5 rounded-md object-cover"
|
className="h-5 w-5 rounded-md object-cover"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
{t('socialUi.crew.agent')}
|
{t('socialUi.form.sasquatch')}
|
||||||
|
<span className="rounded-full bg-[#ccff00]/15 px-1.5 py-0.5 text-[10px] font-bold uppercase text-[#ccff00]">{t('socialUi.form.pro')}</span>
|
||||||
<span className="rounded-full bg-teal-500/15 px-1.5 py-0.5 text-[10px] font-bold uppercase text-teal-300">{t('socialUi.form.beta')}</span>
|
<span className="rounded-full bg-teal-500/15 px-1.5 py-0.5 text-[10px] font-bold uppercase text-teal-300">{t('socialUi.form.beta')}</span>
|
||||||
</Label>
|
</Label>
|
||||||
<p className="mt-1 text-xs text-gray-500">
|
<p className="mt-1 text-xs text-gray-500">
|
||||||
{t('socialUi.form.agentDescA')} <strong className="text-gray-300">{t('socialUi.form.sasquatch')}</strong>{t('socialUi.form.agentDescB')}{' '}
|
{t('socialUi.form.agentDescA')} <strong className="text-gray-300">{t('socialUi.form.sasquatch')}</strong>{t('socialUi.form.agentDescB')}{' '}
|
||||||
<strong className="text-gray-300">{t('socialUi.form.agentDescStrong')}</strong>{t('socialUi.form.agentDescC')}
|
<strong className="text-gray-300">{t('socialUi.form.agentDescStrong')}</strong>{t('socialUi.form.agentDescC')}
|
||||||
</p>
|
</p>
|
||||||
|
<p className="mt-1 text-[11px] leading-snug text-gray-500">{t('socialUi.form.agentProNote')}</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -1273,7 +1313,6 @@ export function CrewForm({ existing }: CrewFormProps) {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<NofferField value={noffer} onChange={setNoffer} />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -1360,15 +1399,28 @@ export function CrewForm({ existing }: CrewFormProps) {
|
||||||
{membersBlock}
|
{membersBlock}
|
||||||
{spotsBlock}
|
{spotsBlock}
|
||||||
{locationBlock}
|
{locationBlock}
|
||||||
|
<ExtrasGroup icon={AtSign} title={t('socialUi.form.groupIdentity')} hint={t('socialUi.form.groupIdentityHint')} defaultOpen>
|
||||||
{nip05Block}
|
{nip05Block}
|
||||||
{linksBlock}
|
{linksBlock}
|
||||||
{bandBlock}
|
</ExtrasGroup>
|
||||||
|
<ExtrasGroup icon={Sparkles} title={t('socialUi.form.groupAi')} defaultOpen={agentEnabled}>
|
||||||
{agentBlock}
|
{agentBlock}
|
||||||
|
</ExtrasGroup>
|
||||||
|
<ExtrasGroup icon={Film} title={t('socialUi.form.groupShowcase')}>
|
||||||
{videoBlock}
|
{videoBlock}
|
||||||
{sponsorBlock}
|
{sponsorBlock}
|
||||||
|
{bandBlock}
|
||||||
|
</ExtrasGroup>
|
||||||
|
<ExtrasGroup icon={Zap} title={t('socialUi.form.groupPayments')}>
|
||||||
{treasuryBlock}
|
{treasuryBlock}
|
||||||
|
<NofferField value={noffer} onChange={setNoffer} />
|
||||||
|
</ExtrasGroup>
|
||||||
|
<ExtrasGroup icon={GraduationCap} title={t('socialUi.form.groupIntegrations')}>
|
||||||
{labLinkBlock}
|
{labLinkBlock}
|
||||||
|
</ExtrasGroup>
|
||||||
|
<ExtrasGroup icon={Server} title={t('socialUi.form.groupAdvanced')} hint={t('socialUi.form.groupAdvancedHint')}>
|
||||||
{coordinatorBlock}
|
{coordinatorBlock}
|
||||||
|
</ExtrasGroup>
|
||||||
<Button type="submit" className={cn('w-full uppercase font-black', TEAL)} disabled={busy}>
|
<Button type="submit" className={cn('w-full uppercase font-black', TEAL)} disabled={busy}>
|
||||||
{isPublishing ? (
|
{isPublishing ? (
|
||||||
<><Loader2 className="h-4 w-4 mr-2 animate-spin" />{t('socialUi.form.publishing')}</>
|
<><Loader2 className="h-4 w-4 mr-2 animate-spin" />{t('socialUi.form.publishing')}</>
|
||||||
|
|
@ -1445,15 +1497,29 @@ export function CrewForm({ existing }: CrewFormProps) {
|
||||||
filled: hasExtras,
|
filled: hasExtras,
|
||||||
content: (
|
content: (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
<p className="text-xs text-gray-500">{t('socialUi.form.extrasAllOptional')}</p>
|
||||||
|
<ExtrasGroup icon={AtSign} title={t('socialUi.form.groupIdentity')} hint={t('socialUi.form.groupIdentityHint')} defaultOpen>
|
||||||
{nip05Block}
|
{nip05Block}
|
||||||
{linksBlock}
|
{linksBlock}
|
||||||
{bandBlock}
|
</ExtrasGroup>
|
||||||
|
<ExtrasGroup icon={Sparkles} title={t('socialUi.form.groupAi')} defaultOpen={agentEnabled}>
|
||||||
{agentBlock}
|
{agentBlock}
|
||||||
|
</ExtrasGroup>
|
||||||
|
<ExtrasGroup icon={Film} title={t('socialUi.form.groupShowcase')}>
|
||||||
{videoBlock}
|
{videoBlock}
|
||||||
{sponsorBlock}
|
{sponsorBlock}
|
||||||
|
{bandBlock}
|
||||||
|
</ExtrasGroup>
|
||||||
|
<ExtrasGroup icon={Zap} title={t('socialUi.form.groupPayments')}>
|
||||||
{treasuryBlock}
|
{treasuryBlock}
|
||||||
|
<NofferField value={noffer} onChange={setNoffer} />
|
||||||
|
</ExtrasGroup>
|
||||||
|
<ExtrasGroup icon={GraduationCap} title={t('socialUi.form.groupIntegrations')}>
|
||||||
{labLinkBlock}
|
{labLinkBlock}
|
||||||
|
</ExtrasGroup>
|
||||||
|
<ExtrasGroup icon={Server} title={t('socialUi.form.groupAdvanced')} hint={t('socialUi.form.groupAdvancedHint')}>
|
||||||
{coordinatorBlock}
|
{coordinatorBlock}
|
||||||
|
</ExtrasGroup>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
@ -1598,7 +1664,7 @@ export function CrewForm({ existing }: CrewFormProps) {
|
||||||
goToStep(lastStep);
|
goToStep(lastStep);
|
||||||
}}
|
}}
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
className="hidden text-gray-400 hover:text-white sm:inline-flex"
|
className="text-xs text-gray-400 hover:text-white sm:text-sm"
|
||||||
>
|
>
|
||||||
{t('socialUi.form.skipToEnd')}
|
{t('socialUi.form.skipToEnd')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -3603,11 +3603,7 @@ socialUi: {
|
||||||
customDescShort: "Optional — your crew already has end-to-end encrypted chat built in, so you don't need this. Only set it if you run your own",
|
customDescShort: "Optional — your crew already has end-to-end encrypted chat built in, so you don't need this. Only set it if you run your own",
|
||||||
remove: 'Remove',
|
remove: 'Remove',
|
||||||
pointerNote: "Pointer only for now: a wss:// endpoint is used as the crew's chat relay; an https:// one via the shared ContextVM relay. Real Cordn MLS isn't wired yet.",
|
pointerNote: "Pointer only for now: a wss:// endpoint is used as the crew's chat relay; an https:// one via the shared ContextVM relay. Real Cordn MLS isn't wired yet.",
|
||||||
customDescA: "You don't need this.",
|
|
||||||
customDescB: 'Your crew already has private, end-to-end encrypted group chat built in — no setup. This field is only for crews who',
|
|
||||||
customRunOwn: 'run their own',
|
|
||||||
cordnCoordinator: 'Cordn coordinator',
|
cordnCoordinator: 'Cordn coordinator',
|
||||||
customDescC: "— even on an old phone. Paste its nprofile, a cordn.net link, or an endpoint URL. A wss:// endpoint is used as the crew's own chat relay (point it at a private one to keep even the metadata off public relays); an https:// coordinator is addressed by its pubkey over the shared ContextVM relay. Leave blank to use the built-in chat.",
|
|
||||||
invalid: "That doesn't look like a coordinator. Paste an nprofile, a cordn.net link, or an https/wss URL.",
|
invalid: "That doesn't look like a coordinator. Paste an nprofile, a cordn.net link, or an https/wss URL.",
|
||||||
useMine: 'Use my coordinator',
|
useMine: 'Use my coordinator',
|
||||||
},
|
},
|
||||||
|
|
@ -3899,6 +3895,17 @@ socialUi: {
|
||||||
stepExtras: 'Extras',
|
stepExtras: 'Extras',
|
||||||
stepExtrasShort: 'Extras',
|
stepExtrasShort: 'Extras',
|
||||||
stepExtrasHint: 'Power-ups for later — skip anything you’re unsure about.',
|
stepExtrasHint: 'Power-ups for later — skip anything you’re unsure about.',
|
||||||
|
extrasAllOptional: 'Everything on this step is optional.',
|
||||||
|
groupIdentity: 'Identity',
|
||||||
|
groupIdentityHint: 'Handle, links',
|
||||||
|
groupAi: 'Sasquatch AI',
|
||||||
|
groupShowcase: 'Showcase',
|
||||||
|
groupPayments: 'Payments & treasury',
|
||||||
|
groupIntegrations: 'Integrations',
|
||||||
|
groupAdvanced: 'Advanced',
|
||||||
|
groupAdvancedHint: 'experimental',
|
||||||
|
pro: 'Pro',
|
||||||
|
agentProNote: 'Powered by Sasquatch. Pro gets the best models; without Pro it uses a free fallback.',
|
||||||
stepOf: 'Step {{step}} of {{total}}',
|
stepOf: 'Step {{step}} of {{total}}',
|
||||||
treasuryDescA: "Raise sats for a shared goal — fixing the local DIY spot, a crew trip, a comp prize pot. It's a transparent Lightning goal; zaps go",
|
treasuryDescA: "Raise sats for a shared goal — fixing the local DIY spot, a crew trip, a comp prize pot. It's a transparent Lightning goal; zaps go",
|
||||||
treasuryDescStrong: 'straight to your wallet',
|
treasuryDescStrong: 'straight to your wallet',
|
||||||
|
|
|
||||||
|
|
@ -3603,11 +3603,7 @@ socialUi: {
|
||||||
customDescShort: 'Opcional: tu cuadrilla ya tiene chat cifrado de extremo a extremo integrado, así que no lo necesitas. Configúralo solo si usas tu propio',
|
customDescShort: 'Opcional: tu cuadrilla ya tiene chat cifrado de extremo a extremo integrado, así que no lo necesitas. Configúralo solo si usas tu propio',
|
||||||
remove: 'Quitar',
|
remove: 'Quitar',
|
||||||
pointerNote: 'Por ahora es solo un puntero: un endpoint wss:// se usa como relé del chat de la cuadrilla y uno https:// a través del relé ContextVM compartido. El MLS real de Cordn aún no está conectado.',
|
pointerNote: 'Por ahora es solo un puntero: un endpoint wss:// se usa como relé del chat de la cuadrilla y uno https:// a través del relé ContextVM compartido. El MLS real de Cordn aún no está conectado.',
|
||||||
customDescA: 'No lo necesitas.',
|
|
||||||
customDescB: 'Tu cuadrilla ya tiene chat de grupo privado y cifrado de extremo a extremo integrado — sin configurar nada. Este campo es solo para cuadrillas que',
|
|
||||||
customRunOwn: 'montan el suyo',
|
|
||||||
cordnCoordinator: 'coordinador Cordn',
|
cordnCoordinator: 'coordinador Cordn',
|
||||||
customDescC: '— incluso en un móvil viejo. Pega su nprofile, un enlace de cordn.net o una URL de endpoint. Un endpoint wss:// se usa como relé de chat propio de la cuadrilla (apúntalo a uno privado para mantener hasta los metadatos fuera de los relés públicos); un coordinador https:// se direcciona por su pubkey a través del relé ContextVM compartido. Déjalo en blanco para usar el chat integrado.',
|
|
||||||
invalid: 'Eso no parece un coordinador. Pega un nprofile, un enlace de cordn.net o una URL https/wss.',
|
invalid: 'Eso no parece un coordinador. Pega un nprofile, un enlace de cordn.net o una URL https/wss.',
|
||||||
useMine: 'Usar mi coordinador',
|
useMine: 'Usar mi coordinador',
|
||||||
},
|
},
|
||||||
|
|
@ -3899,6 +3895,17 @@ socialUi: {
|
||||||
stepExtras: 'Extras',
|
stepExtras: 'Extras',
|
||||||
stepExtrasShort: 'Extras',
|
stepExtrasShort: 'Extras',
|
||||||
stepExtrasHint: 'Mejoras para más adelante — sáltate lo que no tengas claro.',
|
stepExtrasHint: 'Mejoras para más adelante — sáltate lo que no tengas claro.',
|
||||||
|
extrasAllOptional: 'Todo en este paso es opcional.',
|
||||||
|
groupIdentity: 'Identidad',
|
||||||
|
groupIdentityHint: 'Identificador, enlaces',
|
||||||
|
groupAi: 'IA Sasquatch',
|
||||||
|
groupShowcase: 'Escaparate',
|
||||||
|
groupPayments: 'Pagos y tesorería',
|
||||||
|
groupIntegrations: 'Integraciones',
|
||||||
|
groupAdvanced: 'Avanzado',
|
||||||
|
groupAdvancedHint: 'experimental',
|
||||||
|
pro: 'Pro',
|
||||||
|
agentProNote: 'Con la tecnología de Sasquatch. Pro usa los mejores modelos; sin Pro usa un respaldo gratuito.',
|
||||||
stepOf: 'Paso {{step}} de {{total}}',
|
stepOf: 'Paso {{step}} de {{total}}',
|
||||||
treasuryDescA: 'Recauda sats para un objetivo común — arreglar el spot DIY local, un viaje de la cuadrilla, un bote para un concurso. Es un objetivo Lightning transparente; los zaps van',
|
treasuryDescA: 'Recauda sats para un objetivo común — arreglar el spot DIY local, un viaje de la cuadrilla, un bote para un concurso. Es un objetivo Lightning transparente; los zaps van',
|
||||||
treasuryDescStrong: 'directos a tu monedero',
|
treasuryDescStrong: 'directos a tu monedero',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue