~3,470 new keys (5,770 total, EN=ES), ~115 more files wired. - nav + chromeUi: header, More menu, burger menu, footer, map toolbar widgets (sort/trust/weather), spot-type label helper. - placesUi: events, shops, tours, food. socialUi: crews, DMs, notifications, collectives, crew chat/coordinator. settingsUi: settings, auth/signup, onboarding, profile edit, legal. walletUi: wallet/Cashu, stash, mints, app shop, newsletter, community stats, leaderboards, profile. - Fixed 738 usages where an agent dropped the socialUi. prefix (caught by check:i18n).
129 lines
4.9 KiB
TypeScript
129 lines
4.9 KiB
TypeScript
import { useState } from 'react';
|
|
import { Card, CardContent } from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Textarea } from '@/components/ui/textarea';
|
|
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
|
import { useToast } from '@/hooks/useToast';
|
|
import { useTranslation } from '@/contexts/language';
|
|
import {
|
|
useSendCollectiveJoinRequest,
|
|
useMyCollectiveJoinRequest,
|
|
} from '@/hooks/useCollectiveJoinRequests';
|
|
import type { LabProfile } from '@/hooks/useLabProfiles';
|
|
import { Users, Hand, Loader2, Check, X } from 'lucide-react';
|
|
|
|
/**
|
|
* "Request to join this collective" — shown on a Lab collective's profile to
|
|
* logged-in skaters who aren't the owner and aren't already listed. Mirrors the
|
|
* crew join-request: it notifies the owner (who adds the member through their
|
|
* own editor). Owners never see it on their own profile.
|
|
*/
|
|
export function RequestToJoinCollective({
|
|
profile,
|
|
accentText = 'text-teal-400',
|
|
accentBorder = 'border-teal-500',
|
|
}: {
|
|
profile: LabProfile;
|
|
accentText?: string;
|
|
accentBorder?: string;
|
|
}) {
|
|
const { t } = useTranslation();
|
|
const { user } = useCurrentUser();
|
|
const { toast } = useToast();
|
|
const [open, setOpen] = useState(false);
|
|
const [note, setNote] = useState('');
|
|
|
|
const coord = `${profile.event.kind}:${profile.pubkey}:${profile.identifier}`;
|
|
const { mutateAsync: send, isPending: sending } = useSendCollectiveJoinRequest();
|
|
const { data: alreadyRequested, isLoading: checking } = useMyCollectiveJoinRequest(coord);
|
|
|
|
// Gate: must be logged in, not the owner, not already a listed member.
|
|
if (!user) return null;
|
|
const meLc = user.pubkey.toLowerCase();
|
|
if (meLc === profile.pubkey.toLowerCase()) return null;
|
|
if (profile.members.some((m) => m.toLowerCase() === meLc)) return null;
|
|
|
|
const handleSend = async () => {
|
|
try {
|
|
await send({ profile, note });
|
|
setOpen(false);
|
|
setNote('');
|
|
toast({
|
|
title: t('socialUi.requestCollective.sentTitle'),
|
|
description: t('socialUi.requestCollective.sentDesc', { name: profile.title }),
|
|
});
|
|
} catch (err) {
|
|
toast({
|
|
title: t('socialUi.requestCollective.couldNotSend'),
|
|
description: err instanceof Error ? err.message : t('common.tryAgain'),
|
|
variant: 'destructive',
|
|
});
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Card className={`bg-black/90 border-2 ${accentBorder}`}>
|
|
<CardContent className="pt-6">
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<Users className={`h-5 w-5 ${accentText}`} />
|
|
<h3 className="text-lg text-white uppercase font-black">{t('socialUi.requestCollective.title')}</h3>
|
|
</div>
|
|
|
|
{alreadyRequested ? (
|
|
<div className="flex items-center gap-2 rounded-lg border border-emerald-500/30 bg-emerald-500/10 px-3 py-2.5 text-sm text-emerald-300">
|
|
<Check className="h-4 w-4" />
|
|
{t('socialUi.requestCollective.waiting', { name: profile.title })}
|
|
</div>
|
|
) : !open ? (
|
|
<>
|
|
<p className="text-sm text-gray-400 mb-3">
|
|
{t('socialUi.requestCollective.askA')}{' '}
|
|
<span className="font-bold text-white">{profile.title}</span>{' '}
|
|
{t('socialUi.requestCollective.askB')}
|
|
</p>
|
|
<Button
|
|
onClick={() => setOpen(true)}
|
|
disabled={checking}
|
|
className="w-full gap-2 bg-white text-black font-black hover:bg-gray-200"
|
|
>
|
|
<Hand className="h-4 w-4" />
|
|
{t('socialUi.requestToJoin.trigger')}
|
|
</Button>
|
|
</>
|
|
) : (
|
|
<div className="space-y-3">
|
|
<Textarea
|
|
value={note}
|
|
onChange={(e) => setNote(e.target.value.slice(0, 500))}
|
|
placeholder={t('socialUi.requestCollective.placeholder')}
|
|
rows={3}
|
|
className="resize-none border-gray-700 bg-black/60 text-white placeholder:text-gray-600"
|
|
/>
|
|
<div className="flex items-center gap-2">
|
|
<Button
|
|
onClick={handleSend}
|
|
disabled={sending}
|
|
className="flex-1 gap-2 bg-white text-black font-black hover:bg-gray-200"
|
|
>
|
|
{sending ? <Loader2 className="h-4 w-4 animate-spin" /> : <Hand className="h-4 w-4" />}
|
|
{t('socialUi.requestCollective.sendButton')}
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => {
|
|
setOpen(false);
|
|
setNote('');
|
|
}}
|
|
disabled={sending}
|
|
className="gap-1.5 border-gray-700 text-gray-400 hover:bg-white/10 hover:text-white"
|
|
>
|
|
<X className="h-4 w-4" />
|
|
{t('common.cancel')}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|