~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).
115 lines
3.8 KiB
TypeScript
115 lines
3.8 KiB
TypeScript
import { useState } from 'react';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} from '@/components/ui/dialog';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Textarea } from '@/components/ui/textarea';
|
|
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
|
import { useSendJoinRequest } from '@/hooks/useCrewInvites';
|
|
import { useToast } from '@/hooks/useToast';
|
|
import { useTranslation } from '@/contexts/language';
|
|
import { UserPlus, Loader2, Bell } from 'lucide-react';
|
|
import type { SkateCrew } from '@/lib/types';
|
|
|
|
interface CrewRequestToJoinProps {
|
|
crew: SkateCrew;
|
|
}
|
|
|
|
/**
|
|
* Lets a skater request to join a crew by publishing a structured, notifiable
|
|
* join-request (kind 31969) addressed to the founder + admins. Their clients
|
|
* surface it in the notification bell with a one-click "Add to crew" (founder),
|
|
* so requests aren't lost in a DM inbox. Nothing sensitive is published — just
|
|
* an optional note.
|
|
*/
|
|
export function CrewRequestToJoin({ crew }: CrewRequestToJoinProps) {
|
|
const { t } = useTranslation();
|
|
const { user } = useCurrentUser();
|
|
const { mutateAsync: sendJoinRequest, isPending } = useSendJoinRequest();
|
|
const { toast } = useToast();
|
|
const [open, setOpen] = useState(false);
|
|
const [note, setNote] = useState('');
|
|
|
|
if (!user) return null;
|
|
|
|
const recipients = Array.from(new Set([crew.pubkey, ...crew.admins])).filter(
|
|
(pk) => pk !== user.pubkey,
|
|
);
|
|
|
|
// Don't show for people already in the leadership.
|
|
if (recipients.length === 0) return null;
|
|
|
|
const handleSend = async () => {
|
|
try {
|
|
await sendJoinRequest({ crew, note });
|
|
toast({
|
|
title: t('socialUi.requestToJoin.sentTitle'),
|
|
description: t('socialUi.requestToJoin.sentDesc', { crew: crew.name }),
|
|
});
|
|
setOpen(false);
|
|
setNote('');
|
|
} catch (err) {
|
|
toast({
|
|
title: t('socialUi.requestToJoin.couldNotSend'),
|
|
description: err instanceof Error ? err.message : t('common.tryAgain'),
|
|
variant: 'destructive',
|
|
});
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
<DialogTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="gap-1.5 border-teal-500/40 text-teal-400 hover:bg-teal-500/10"
|
|
>
|
|
<UserPlus className="h-3.5 w-3.5" /> {t('socialUi.requestToJoin.trigger')}
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent className="max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle className="flex items-center gap-2">
|
|
<UserPlus className="h-5 w-5 text-teal-400" /> {t('socialUi.requestToJoin.title', { crew: crew.name })}
|
|
</DialogTitle>
|
|
<DialogDescription className="flex items-center gap-1.5">
|
|
<Bell className="h-3 w-3 text-teal-400" />
|
|
{t('socialUi.requestToJoin.description')}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<Textarea
|
|
value={note}
|
|
onChange={(e) => setNote(e.target.value)}
|
|
placeholder={t('socialUi.requestToJoin.placeholder')}
|
|
rows={4}
|
|
maxLength={1000}
|
|
className="resize-none"
|
|
/>
|
|
<DialogFooter>
|
|
<Button
|
|
onClick={handleSend}
|
|
disabled={isPending}
|
|
className="w-full bg-teal-500 font-bold text-black hover:bg-teal-600"
|
|
>
|
|
{isPending ? (
|
|
<>
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> {t('socialUi.requestToJoin.sending')}
|
|
</>
|
|
) : (
|
|
<>
|
|
<UserPlus className="mr-2 h-4 w-4" /> {t('socialUi.requestToJoin.sendButton')}
|
|
</>
|
|
)}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|