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 { useNavigate } from 'react-router-dom';
|
||||
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 { Input } from '@/components/ui/input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
|
|
@ -29,7 +30,7 @@ import { Nip05HandlesEditor } from '@/components/Nip05HandlesEditor';
|
|||
import { coordinatorAddUrl, crewCoordinatorInputValue, parseCrewCoordinatorInput, shortNpub } from '@/lib/coordinator';
|
||||
import { useRelays } from '@/lib/relays';
|
||||
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_ICONS: Record<CrewFocus, typeof Users> = {
|
||||
|
|
@ -201,6 +202,37 @@ const HEALTH_LABEL: Record<string, string> = {
|
|||
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) {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
|
@ -510,7 +542,13 @@ export function CrewForm({ existing }: CrewFormProps) {
|
|||
if (focus !== 'mixed') tags.push(['t', CREW_FOCUS_TAG[focus]]);
|
||||
// Crew AI agent — queryable marker tag when enabled.
|
||||
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 (website.trim()) tags.push(['website', website.trim()]);
|
||||
if (instagram.trim()) tags.push(['instagram', instagram.trim()]);
|
||||
|
|
@ -671,7 +709,7 @@ export function CrewForm({ existing }: CrewFormProps) {
|
|||
</div>
|
||||
<div className="space-y-2">
|
||||
<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>
|
||||
|
||||
|
|
@ -816,13 +854,15 @@ export function CrewForm({ existing }: CrewFormProps) {
|
|||
className="h-5 w-5 rounded-md object-cover"
|
||||
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>
|
||||
</Label>
|
||||
<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')}{' '}
|
||||
<strong className="text-gray-300">{t('socialUi.form.agentDescStrong')}</strong>{t('socialUi.form.agentDescC')}
|
||||
</p>
|
||||
<p className="mt-1 text-[11px] leading-snug text-gray-500">{t('socialUi.form.agentProNote')}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -1273,7 +1313,6 @@ export function CrewForm({ existing }: CrewFormProps) {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
<NofferField value={noffer} onChange={setNoffer} />
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
@ -1360,15 +1399,28 @@ export function CrewForm({ existing }: CrewFormProps) {
|
|||
{membersBlock}
|
||||
{spotsBlock}
|
||||
{locationBlock}
|
||||
{nip05Block}
|
||||
{linksBlock}
|
||||
{bandBlock}
|
||||
{agentBlock}
|
||||
{videoBlock}
|
||||
{sponsorBlock}
|
||||
{treasuryBlock}
|
||||
{labLinkBlock}
|
||||
{coordinatorBlock}
|
||||
<ExtrasGroup icon={AtSign} title={t('socialUi.form.groupIdentity')} hint={t('socialUi.form.groupIdentityHint')} defaultOpen>
|
||||
{nip05Block}
|
||||
{linksBlock}
|
||||
</ExtrasGroup>
|
||||
<ExtrasGroup icon={Sparkles} title={t('socialUi.form.groupAi')} defaultOpen={agentEnabled}>
|
||||
{agentBlock}
|
||||
</ExtrasGroup>
|
||||
<ExtrasGroup icon={Film} title={t('socialUi.form.groupShowcase')}>
|
||||
{videoBlock}
|
||||
{sponsorBlock}
|
||||
{bandBlock}
|
||||
</ExtrasGroup>
|
||||
<ExtrasGroup icon={Zap} title={t('socialUi.form.groupPayments')}>
|
||||
{treasuryBlock}
|
||||
<NofferField value={noffer} onChange={setNoffer} />
|
||||
</ExtrasGroup>
|
||||
<ExtrasGroup icon={GraduationCap} title={t('socialUi.form.groupIntegrations')}>
|
||||
{labLinkBlock}
|
||||
</ExtrasGroup>
|
||||
<ExtrasGroup icon={Server} title={t('socialUi.form.groupAdvanced')} hint={t('socialUi.form.groupAdvancedHint')}>
|
||||
{coordinatorBlock}
|
||||
</ExtrasGroup>
|
||||
<Button type="submit" className={cn('w-full uppercase font-black', TEAL)} disabled={busy}>
|
||||
{isPublishing ? (
|
||||
<><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,
|
||||
content: (
|
||||
<div className="space-y-6">
|
||||
{nip05Block}
|
||||
{linksBlock}
|
||||
{bandBlock}
|
||||
{agentBlock}
|
||||
{videoBlock}
|
||||
{sponsorBlock}
|
||||
{treasuryBlock}
|
||||
{labLinkBlock}
|
||||
{coordinatorBlock}
|
||||
<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}
|
||||
{linksBlock}
|
||||
</ExtrasGroup>
|
||||
<ExtrasGroup icon={Sparkles} title={t('socialUi.form.groupAi')} defaultOpen={agentEnabled}>
|
||||
{agentBlock}
|
||||
</ExtrasGroup>
|
||||
<ExtrasGroup icon={Film} title={t('socialUi.form.groupShowcase')}>
|
||||
{videoBlock}
|
||||
{sponsorBlock}
|
||||
{bandBlock}
|
||||
</ExtrasGroup>
|
||||
<ExtrasGroup icon={Zap} title={t('socialUi.form.groupPayments')}>
|
||||
{treasuryBlock}
|
||||
<NofferField value={noffer} onChange={setNoffer} />
|
||||
</ExtrasGroup>
|
||||
<ExtrasGroup icon={GraduationCap} title={t('socialUi.form.groupIntegrations')}>
|
||||
{labLinkBlock}
|
||||
</ExtrasGroup>
|
||||
<ExtrasGroup icon={Server} title={t('socialUi.form.groupAdvanced')} hint={t('socialUi.form.groupAdvancedHint')}>
|
||||
{coordinatorBlock}
|
||||
</ExtrasGroup>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
|
|
@ -1598,7 +1664,7 @@ export function CrewForm({ existing }: CrewFormProps) {
|
|||
goToStep(lastStep);
|
||||
}}
|
||||
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')}
|
||||
</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",
|
||||
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.",
|
||||
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',
|
||||
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.",
|
||||
useMine: 'Use my coordinator',
|
||||
},
|
||||
|
|
@ -3899,6 +3895,17 @@ socialUi: {
|
|||
stepExtras: 'Extras',
|
||||
stepExtrasShort: 'Extras',
|
||||
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}}',
|
||||
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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
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.',
|
||||
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',
|
||||
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.',
|
||||
useMine: 'Usar mi coordinador',
|
||||
},
|
||||
|
|
@ -3899,6 +3895,17 @@ socialUi: {
|
|||
stepExtras: 'Extras',
|
||||
stepExtrasShort: 'Extras',
|
||||
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}}',
|
||||
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',
|
||||
|
|
|
|||
Loading…
Reference in a new issue