SkateSpots/src/components/CrewRequestToJoin.tsx
SkateSpots 0b796440f1 feat(i18n): Spanish batch 2 — nav/chrome, events/shops/tours, crews/DMs, settings/auth, wallet/stash/newsletter/stats
~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).
2026-10-02 10:11:06 +07:00

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>
);
}