feat(crew): clearer, consistent crew-creation form

- Coordinator block: shorter copy, a Remove (✕) button, and an honest
  "pointer only — real Cordn MLS isn't wired yet" note (the old paragraph was a
  wall of jargon).
- Save now blocks on a non-empty but unparseable coordinator (was silently
  dropped) instead of publishing a crew with no coordinator.
- One consistent block order for create (wizard "Extras") and edit, grouped:
  core -> squad/places -> identity (NIP-05, links, band) -> agent -> showcase
  (video, sponsor) -> treasury -> integrations (Lab link, coordinator).
This commit is contained in:
SkateSpots 2026-10-03 17:33:35 +07:00
parent 127819e3da
commit 3cc16d18ca
3 changed files with 50 additions and 23 deletions

View file

@ -428,6 +428,12 @@ export function CrewForm({ existing }: CrewFormProps) {
return;
}
// An invalid coordinator would be silently dropped on save — tell the user.
if (coordinatorInput.trim() && !parsedCoordinator) {
toast({ title: t('socialUi.coordinator.invalid'), variant: 'destructive' });
return;
}
try {
const identifier = existing?.identifier ?? `crew-${Date.now().toString(36)}`;
@ -1091,8 +1097,7 @@ export function CrewForm({ existing }: CrewFormProps) {
<Server className="h-4 w-4" /> {t('socialUi.coordinator.customTitle')} <span className="font-normal text-gray-500">{t('socialUi.coordinator.customBadge')}</span>
</Label>
<p className="text-xs text-gray-500">
<span className="font-semibold text-gray-300">{t('socialUi.coordinator.customDescA')}</span> {t('socialUi.coordinator.customDescB')}{' '}
<span className="font-semibold text-gray-300">{t('socialUi.coordinator.customRunOwn')}</span>{' '}
{t('socialUi.coordinator.customDescShort')}{' '}
<a
href="https://github.com/Cordn-msg/cordn"
target="_blank"
@ -1100,17 +1105,32 @@ export function CrewForm({ existing }: CrewFormProps) {
className="text-teal-400 underline underline-offset-2 hover:text-teal-300"
>
{t('socialUi.coordinator.cordnCoordinator')}
</a>{' '}
{t('socialUi.coordinator.customDescC')}
</a>
.
</p>
<Input
id="crew-coordinator"
placeholder="nprofile1… · cordn.net/chat/coordinators?c=… · https://…"
value={coordinatorInput}
onChange={(e) => setCoordinatorInput(e.target.value)}
className="bg-gray-800 border-gray-700 font-mono text-xs"
spellCheck={false}
/>
<div className="flex gap-2">
<Input
id="crew-coordinator"
placeholder="nprofile1… · cordn.net/chat/coordinators?c=… · https://…"
value={coordinatorInput}
onChange={(e) => setCoordinatorInput(e.target.value)}
className="bg-gray-800 border-gray-700 font-mono text-xs"
spellCheck={false}
/>
{coordinatorInput.trim() !== '' && (
<Button
type="button"
variant="outline"
size="icon"
className="shrink-0 border-gray-700 text-gray-400 hover:text-red-400"
onClick={() => setCoordinatorInput('')}
title={t('socialUi.coordinator.remove')}
aria-label={t('socialUi.coordinator.remove')}
>
<X className="h-4 w-4" />
</Button>
)}
</div>
{coordinatorInput.trim() !== '' &&
(parsedCoordinator ? (
<p className="break-all text-[11px] text-teal-400">
@ -1134,6 +1154,7 @@ export function CrewForm({ existing }: CrewFormProps) {
{t('socialUi.coordinator.useMine')}
</Button>
)}
<p className="text-[10px] leading-snug text-gray-600">{t('socialUi.coordinator.pointerNote')}</p>
</div>
);
@ -1281,19 +1302,19 @@ export function CrewForm({ existing }: CrewFormProps) {
{bioBlock}
{vibeBlock}
{crewTypeBlock}
{agentBlock}
{openCallBlock}
{treasuryBlock}
{membersBlock}
{spotsBlock}
{labLinkBlock}
{videoBlock}
{sponsorBlock}
{coordinatorBlock}
{locationBlock}
{nip05Block}
{linksBlock}
{bandBlock}
{agentBlock}
{videoBlock}
{sponsorBlock}
{treasuryBlock}
{labLinkBlock}
{coordinatorBlock}
<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')}</>
@ -1370,14 +1391,14 @@ export function CrewForm({ existing }: CrewFormProps) {
filled: hasExtras,
content: (
<div className="space-y-6">
{treasuryBlock}
{videoBlock}
{sponsorBlock}
{labLinkBlock}
{nip05Block}
{linksBlock}
{bandBlock}
{nip05Block}
{agentBlock}
{videoBlock}
{sponsorBlock}
{treasuryBlock}
{labLinkBlock}
{coordinatorBlock}
</div>
),

View file

@ -3600,6 +3600,9 @@ socialUi: {
privacyNote: '🔒 A coordinator sees ciphertext and metadata only — never message contents. Run your own to keep even the metadata yours.',
customTitle: 'Custom Chat Coordinator',
customBadge: '· experimental · your own relay',
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',

View file

@ -3600,6 +3600,9 @@ socialUi: {
privacyNote: '🔒 Un coordinador solo ve texto cifrado y metadatos — nunca el contenido de los mensajes. Monta el tuyo para que hasta los metadatos sean tuyos.',
customTitle: 'Coordinador de chat propio',
customBadge: '· experimental · tu propio relé',
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',