- labelsUi.* (221+ keys): spot categories/features/types, access, shelter, event types, gear categories/conditions, price/listings, tours, crews, member-visibility, tag categories, spot status. New src/lib/labelKeys.ts holds the shared value->key maps (one source; the five duplicated EVENT_TYPE_KEYS copies now import it). spotStatus.statusTimeAgo() uses the locale date shim. - formatPrice() now takes an optional t so FREE/Trade/Make Offer translate. - Route helpers: skateTime/distanceReadout/maneuver descriptors/formatAge translated (routeUi.*), consumers updated. - Sasquatch: PRO_TIERS labels/badges + summarizeDraft titles/lines/ctas translated (sasquatchUi.*). - Lab *_LABELS/_DESCRIPTIONS maps remain English (documented out of scope).
168 lines
5.7 KiB
TypeScript
168 lines
5.7 KiB
TypeScript
import { useState } from 'react';
|
|
import { Users, Check, Loader2, Globe, EyeOff } from 'lucide-react';
|
|
import { Card, CardContent } from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
|
import { useCollectiveMembers } from '@/hooks/useCollectiveMembers';
|
|
import { useConfirmMembership } from '@/hooks/useMembershipAttestation';
|
|
import { useToast } from '@/hooks/useToast';
|
|
import { useTranslation } from '@/contexts/language';
|
|
import { cn } from '@/lib/utils';
|
|
import {
|
|
MEMBER_VISIBILITY_DESCRIPTIONS,
|
|
type MemberVisibility,
|
|
} from '@/lib/types';
|
|
import { MEMBER_VISIBILITY_DESC_KEYS } from '@/lib/labelKeys';
|
|
|
|
interface ConfirmMembershipCardProps {
|
|
kind: number;
|
|
founderPubkey: string;
|
|
identifier: string;
|
|
collectiveName: string;
|
|
claimedMembers: string[];
|
|
accentBg?: string;
|
|
}
|
|
|
|
/** Two-option visibility picker used both when confirming and when editing. */
|
|
export function VisibilityPicker({
|
|
value,
|
|
onChange,
|
|
accentBg = 'bg-indigo-600',
|
|
}: {
|
|
value: MemberVisibility;
|
|
onChange: (v: MemberVisibility) => void;
|
|
accentBg?: string;
|
|
}) {
|
|
const { t } = useTranslation();
|
|
const options: { key: MemberVisibility; label: string; icon: React.ComponentType<{ className?: string }> }[] = [
|
|
{ key: 'public', label: t('socialUi.confirmMembership.public'), icon: Globe },
|
|
{ key: 'crew', label: t('socialUi.confirmMembership.crewOnly'), icon: EyeOff },
|
|
];
|
|
return (
|
|
<div className="space-y-2">
|
|
<div className="grid grid-cols-2 gap-2">
|
|
{options.map(({ key, label, icon: Icon }) => {
|
|
const active = value === key;
|
|
return (
|
|
<button
|
|
key={key}
|
|
type="button"
|
|
onClick={() => onChange(key)}
|
|
className={cn(
|
|
'flex items-center justify-center gap-1.5 rounded-lg border px-3 py-2 text-sm font-bold transition-all active:scale-95',
|
|
active
|
|
? cn('border-transparent text-white', accentBg)
|
|
: 'border-white/10 bg-white/5 text-gray-400 hover:bg-white/10 hover:text-white'
|
|
)}
|
|
>
|
|
<Icon className="h-4 w-4" />
|
|
{label}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
<p className="text-xs leading-relaxed text-gray-500">
|
|
{MEMBER_VISIBILITY_DESC_KEYS[value]
|
|
? t(MEMBER_VISIBILITY_DESC_KEYS[value])
|
|
: MEMBER_VISIBILITY_DESCRIPTIONS[value]}
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Shown to a logged-in user who has been LISTED as a member of a collective but
|
|
* has not yet confirmed. Lets them pick their visibility and publishes a NIP-32
|
|
* attestation on click. Hidden for everyone else (non-members, already-confirmed
|
|
* members, the founder).
|
|
*/
|
|
export function ConfirmMembershipCard({
|
|
kind,
|
|
founderPubkey,
|
|
identifier,
|
|
collectiveName,
|
|
claimedMembers,
|
|
accentBg = 'bg-indigo-600 hover:bg-indigo-700',
|
|
}: ConfirmMembershipCardProps) {
|
|
const { t } = useTranslation();
|
|
const { user } = useCurrentUser();
|
|
const { toast } = useToast();
|
|
const { mutateAsync: confirm, isPending } = useConfirmMembership();
|
|
const [visibility, setVisibility] = useState<MemberVisibility>('public');
|
|
const { data: members } = useCollectiveMembers({
|
|
kind,
|
|
founderPubkey,
|
|
identifier,
|
|
claimedMembers,
|
|
enabled: claimedMembers.length > 0,
|
|
});
|
|
|
|
if (!user) return null;
|
|
|
|
const me = user.pubkey.toLowerCase();
|
|
// Only relevant if I'm actually listed as a member and I'm not the founder.
|
|
if (me === founderPubkey.toLowerCase()) return null;
|
|
if (!claimedMembers.includes(me)) return null;
|
|
|
|
const myStatus = members?.find((m) => m.pubkey === me)?.status;
|
|
if (myStatus === 'confirmed') return null;
|
|
|
|
const handleConfirm = async () => {
|
|
try {
|
|
await confirm({ kind, founderPubkey, identifier, collectiveName, visibility });
|
|
toast({
|
|
title: t('socialUi.confirmMembership.confirmedTitle'),
|
|
description:
|
|
visibility === 'crew'
|
|
? t('socialUi.confirmMembership.confirmedCrew', { name: collectiveName })
|
|
: t('socialUi.confirmMembership.confirmedPublic', { name: collectiveName }),
|
|
});
|
|
} catch (error) {
|
|
toast({
|
|
title: t('socialUi.confirmMembership.failed'),
|
|
description: error instanceof Error ? error.message : t('common.tryAgain'),
|
|
variant: 'destructive',
|
|
});
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Card className="border-2 border-amber-500/50 bg-amber-950/20">
|
|
<CardContent className="space-y-4 p-4">
|
|
<div className="flex items-start gap-3">
|
|
<Users className="mt-0.5 h-6 w-6 shrink-0 text-amber-400" />
|
|
<div>
|
|
<h3 className="font-bold text-amber-300">{t('socialUi.confirmMembership.addedTo', { name: collectiveName })}</h3>
|
|
<p className="text-sm text-gray-300">
|
|
{t('socialUi.confirmMembership.addedDesc')}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<p className="mb-2 text-xs font-black uppercase tracking-widest text-gray-400">
|
|
{t('socialUi.confirmMembership.yourVisibility')}
|
|
</p>
|
|
<VisibilityPicker value={visibility} onChange={setVisibility} accentBg={accentBg.split(' ')[0]} />
|
|
</div>
|
|
|
|
<Button
|
|
type="button"
|
|
onClick={handleConfirm}
|
|
disabled={isPending}
|
|
className={cn('w-full gap-2 font-bold', accentBg)}
|
|
>
|
|
{isPending ? (
|
|
<>
|
|
<Loader2 className="h-4 w-4 animate-spin" /> {t('socialUi.confirmMembership.confirming')}
|
|
</>
|
|
) : (
|
|
<>
|
|
<Check className="h-4 w-4" /> {t('socialUi.confirmMembership.confirmButton')}
|
|
</>
|
|
)}
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|