feat: improve Bitcoin grantmaker inquiry UX
Some checks are pending
Deploy to GitHub Pages / deploy (push) Waiting to run
Some checks are pending
Deploy to GitHub Pages / deploy (push) Waiting to run
This commit is contained in:
parent
9732743527
commit
1af3a5f6ef
3 changed files with 182 additions and 49 deletions
|
|
@ -6683,11 +6683,12 @@ walletUi: {
|
||||||
our: 'OUR',
|
our: 'OUR',
|
||||||
community: 'COMMUNITY',
|
community: 'COMMUNITY',
|
||||||
heroDesc: 'SkateSpots is built by and for the community. Supporters, collaborators, partners and friends keeping this free and decentralized.',
|
heroDesc: 'SkateSpots is built by and for the community. Supporters, collaborators, partners and friends keeping this free and decentralized.',
|
||||||
|
grantsTab: 'Grants',
|
||||||
grantsSection: 'Bitcoin grantmakers',
|
grantsSection: 'Bitcoin grantmakers',
|
||||||
grantsSectionDesc: 'For organizations that issue grants to open-source Bitcoin projects.',
|
grantsSectionDesc: 'For organizations that issue grants to open-source Bitcoin projects.',
|
||||||
grantsTitle: 'Open to sponsorships · Bitcoin grantmakers',
|
grantsTitle: 'Open to sponsorships · Bitcoin grantmakers',
|
||||||
grantsDisclaimer: 'Want to donate to this project? Fill the form. Not to KYC us — to KYC YOU.',
|
grantsDisclaimer: 'Grantmaker inquiry. This lane is for organizations that already KYC/KYB their grantees.',
|
||||||
grantsDisclaimerDesc: "This lane is only for grantmakers that KYC/KYB their grantees. If you're a skater who just wants to donate, use the Lightning, Silent Payments or Angor rails above — nobody needs your passport.",
|
grantsDisclaimerDesc: "If you just want to donate, use the Lightning, Silent Payments or Angor rails above — no identity check.",
|
||||||
grantsOrg: 'Organization',
|
grantsOrg: 'Organization',
|
||||||
grantsContact: 'Contact (npub, email or handle)',
|
grantsContact: 'Contact (npub, email or handle)',
|
||||||
grantsWebsite: 'Website',
|
grantsWebsite: 'Website',
|
||||||
|
|
@ -6695,6 +6696,19 @@ walletUi: {
|
||||||
grantsSize: 'Grant size',
|
grantsSize: 'Grant size',
|
||||||
grantsKyc: 'KYC / KYB process',
|
grantsKyc: 'KYC / KYB process',
|
||||||
grantsFund: 'What would you like to fund?',
|
grantsFund: 'What would you like to fund?',
|
||||||
|
grantsFundTitle: 'What a grant can fund',
|
||||||
|
grantsMoreDetails: 'Add grant details',
|
||||||
|
grantsOptional: 'Optional',
|
||||||
|
grantsRequired: 'Organization, contact and confirmation are required.',
|
||||||
|
grantsContactHint: 'Prefer an npub or NIP-05. Email or @handle is only reference text for the private DM.',
|
||||||
|
grantsWebsiteInvalid: 'Enter a valid http(s) website URL.',
|
||||||
|
grantsReview: 'Review message',
|
||||||
|
grantsReviewTitle: 'Review your private message',
|
||||||
|
grantsReviewDesc: 'Nothing has been sent. Check the message before opening the private DM.',
|
||||||
|
grantsOpenDm: 'Open private DM',
|
||||||
|
grantsCopy: 'Copy message',
|
||||||
|
grantsCopied: 'Copied',
|
||||||
|
grantsCopyFailed: 'Could not copy the message',
|
||||||
grantsAck: 'I confirm we are a grantmaker that KYC/KYB our grantees.',
|
grantsAck: 'I confirm we are a grantmaker that KYC/KYB our grantees.',
|
||||||
grantsSend: 'Open grant conversation',
|
grantsSend: 'Open grant conversation',
|
||||||
grantsNoKycTitle: 'No KYC of SkateSpots',
|
grantsNoKycTitle: 'No KYC of SkateSpots',
|
||||||
|
|
|
||||||
|
|
@ -6683,11 +6683,12 @@ walletUi: {
|
||||||
our: 'NUESTRA',
|
our: 'NUESTRA',
|
||||||
community: 'COMUNIDAD',
|
community: 'COMUNIDAD',
|
||||||
heroDesc: 'SkateSpots lo construye y lo mantiene la comunidad. Patrocinadores, colaboradores, socios y amigos que lo mantienen libre y descentralizado.',
|
heroDesc: 'SkateSpots lo construye y lo mantiene la comunidad. Patrocinadores, colaboradores, socios y amigos que lo mantienen libre y descentralizado.',
|
||||||
|
grantsTab: 'Ayudas',
|
||||||
grantsSection: 'Grantmakers de Bitcoin',
|
grantsSection: 'Grantmakers de Bitcoin',
|
||||||
grantsSectionDesc: 'Para organizaciones que conceden ayudas a proyectos Bitcoin de código abierto.',
|
grantsSectionDesc: 'Para organizaciones que conceden ayudas a proyectos Bitcoin de código abierto.',
|
||||||
grantsTitle: 'Abierto a patrocinios · grantmakers de Bitcoin',
|
grantsTitle: 'Abierto a patrocinios · grantmakers de Bitcoin',
|
||||||
grantsDisclaimer: '¿Quieres donar a este proyecto? Rellena el formulario. No para hacernos KYC a nosotros, sino para hacerte KYC a TI.',
|
grantsDisclaimer: 'Consulta para grantmakers. Este canal es para organizaciones que ya hacen KYC/KYB a sus beneficiarios.',
|
||||||
grantsDisclaimerDesc: 'Este canal es solo para grantmakers que hacen KYC/KYB a sus beneficiarios. Si eres un skater que solo quiere donar, usa las vías de Lightning, Silent Payments o Angor de arriba: nadie necesita tu pasaporte.',
|
grantsDisclaimerDesc: 'Si solo quieres donar, usa las vías de Lightning, Silent Payments o Angor de arriba: sin comprobación de identidad.',
|
||||||
grantsOrg: 'Organización',
|
grantsOrg: 'Organización',
|
||||||
grantsContact: 'Contacto (npub, email o usuario)',
|
grantsContact: 'Contacto (npub, email o usuario)',
|
||||||
grantsWebsite: 'Sitio web',
|
grantsWebsite: 'Sitio web',
|
||||||
|
|
@ -6695,6 +6696,19 @@ walletUi: {
|
||||||
grantsSize: 'Tamaño de la ayuda',
|
grantsSize: 'Tamaño de la ayuda',
|
||||||
grantsKyc: 'Proceso de KYC / KYB',
|
grantsKyc: 'Proceso de KYC / KYB',
|
||||||
grantsFund: '¿Qué te gustaría financiar?',
|
grantsFund: '¿Qué te gustaría financiar?',
|
||||||
|
grantsFundTitle: 'Qué puede financiar una ayuda',
|
||||||
|
grantsMoreDetails: 'Añadir detalles de la ayuda',
|
||||||
|
grantsOptional: 'Opcional',
|
||||||
|
grantsRequired: 'La organización, el contacto y la confirmación son obligatorios.',
|
||||||
|
grantsContactHint: 'Mejor un npub o NIP-05. El email o @usuario solo es texto de referencia para el DM privado.',
|
||||||
|
grantsWebsiteInvalid: 'Introduce una URL http(s) válida.',
|
||||||
|
grantsReview: 'Revisar mensaje',
|
||||||
|
grantsReviewTitle: 'Revisa tu mensaje privado',
|
||||||
|
grantsReviewDesc: 'Todavía no se ha enviado nada. Revisa el mensaje antes de abrir el DM privado.',
|
||||||
|
grantsOpenDm: 'Abrir DM privado',
|
||||||
|
grantsCopy: 'Copiar mensaje',
|
||||||
|
grantsCopied: 'Copiado',
|
||||||
|
grantsCopyFailed: 'No se pudo copiar el mensaje',
|
||||||
grantsAck: 'Confirmo que somos un grantmaker que hace KYC/KYB a sus beneficiarios.',
|
grantsAck: 'Confirmo que somos un grantmaker que hace KYC/KYB a sus beneficiarios.',
|
||||||
grantsSend: 'Abrir conversación de ayuda',
|
grantsSend: 'Abrir conversación de ayuda',
|
||||||
grantsNoKycTitle: 'Sin KYC de SkateSpots',
|
grantsNoKycTitle: 'Sin KYC de SkateSpots',
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useSeoMeta } from '@unhead/react';
|
import { useSeoMeta } from '@unhead/react';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import { nip19 } from 'nostr-tools';
|
import { nip19 } from 'nostr-tools';
|
||||||
import { ExternalLink, Zap, Heart, Users, Handshake, Building2, Star, ShieldCheck, Server, Globe, Bot, Code2, Briefcase, Megaphone, TrendingUp, Info, Lock, EyeOff } from 'lucide-react';
|
import { ExternalLink, Zap, Heart, Users, Handshake, Building2, Star, ShieldCheck, Server, Globe, Bot, Code2, Briefcase, Megaphone, TrendingUp, Info, Lock, EyeOff, Copy, Check, ChevronDown } from 'lucide-react';
|
||||||
import { SupportSkateSpots } from '@/components/SupportSkateSpots';
|
import { SupportSkateSpots } from '@/components/SupportSkateSpots';
|
||||||
import { SilentPaymentsCard } from '@/components/SilentPaymentsCard';
|
import { SilentPaymentsCard } from '@/components/SilentPaymentsCard';
|
||||||
import { AngorProjectCard, ANGOR_PROJECT_URL } from '@/components/AngorProjectCard';
|
import { AngorProjectCard, ANGOR_PROJECT_URL } from '@/components/AngorProjectCard';
|
||||||
|
|
@ -20,7 +20,9 @@ import { Input } from '@/components/ui/input';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
|
import { useToast } from '@/hooks/useToast';
|
||||||
|
|
||||||
|
|
||||||
// Only count support received from this point (the project's public launch era).
|
// Only count support received from this point (the project's public launch era).
|
||||||
|
|
@ -133,6 +135,7 @@ const PAYS_FOR = [
|
||||||
function GrantmakerForm() {
|
function GrantmakerForm() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { toast } = useToast();
|
||||||
const owner = PLATFORM_OWNER_PUBKEYS[0];
|
const owner = PLATFORM_OWNER_PUBKEYS[0];
|
||||||
const [org, setOrg] = useState('');
|
const [org, setOrg] = useState('');
|
||||||
const [contact, setContact] = useState('');
|
const [contact, setContact] = useState('');
|
||||||
|
|
@ -142,8 +145,13 @@ function GrantmakerForm() {
|
||||||
const [kyc, setKyc] = useState('');
|
const [kyc, setKyc] = useState('');
|
||||||
const [fund, setFund] = useState('');
|
const [fund, setFund] = useState('');
|
||||||
const [ack, setAck] = useState(false);
|
const [ack, setAck] = useState(false);
|
||||||
|
const [reviewOpen, setReviewOpen] = useState(false);
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
const canSend = !!org.trim() && !!contact.trim() && ack && !!owner;
|
const websiteError = website.trim() && !/^https?:\/\/\S+$/i.test(website.trim())
|
||||||
|
? t('walletUi.supporters.grantsWebsiteInvalid')
|
||||||
|
: '';
|
||||||
|
const canSend = !!org.trim() && !!contact.trim() && ack && !websiteError && !!owner;
|
||||||
const message = [
|
const message = [
|
||||||
'Bitcoin grantmaker inquiry — SkateSpots',
|
'Bitcoin grantmaker inquiry — SkateSpots',
|
||||||
'',
|
'',
|
||||||
|
|
@ -158,6 +166,24 @@ function GrantmakerForm() {
|
||||||
'I confirm this is a grantmaker inquiry and we KYC/KYB our grantees.',
|
'I confirm this is a grantmaker inquiry and we KYC/KYB our grantees.',
|
||||||
].filter(Boolean).join('\n');
|
].filter(Boolean).join('\n');
|
||||||
|
|
||||||
|
const copyMessage = async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(message);
|
||||||
|
setCopied(true);
|
||||||
|
toast({ title: t('walletUi.supporters.grantsCopied') });
|
||||||
|
window.setTimeout(() => setCopied(false), 2000);
|
||||||
|
} catch {
|
||||||
|
toast({ title: t('walletUi.supporters.grantsCopyFailed'), variant: 'destructive' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openDm = () => {
|
||||||
|
if (!canSend || !owner) return;
|
||||||
|
navigate(`/messages/${nip19.npubEncode(owner)}?message=${encodeURIComponent(message)}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const inputClass = 'border-[#f7931a]/25 bg-black/50 text-white placeholder:text-gray-500';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-2xl border-2 border-[#f7931a]/30 bg-gradient-to-br from-[#f7931a]/10 via-black to-transparent p-4 sm:p-6">
|
<div className="rounded-2xl border-2 border-[#f7931a]/30 bg-gradient-to-br from-[#f7931a]/10 via-black to-transparent p-4 sm:p-6">
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
|
|
@ -167,41 +193,93 @@ function GrantmakerForm() {
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<h3 className="text-lg font-black uppercase text-white">{t('walletUi.supporters.grantsTitle')}</h3>
|
<h3 className="text-lg font-black uppercase text-white">{t('walletUi.supporters.grantsTitle')}</h3>
|
||||||
<p className="mt-1 text-sm font-bold text-[#f7931a]">{t('walletUi.supporters.grantsDisclaimer')}</p>
|
<p className="mt-1 text-sm font-bold text-[#f7931a]">{t('walletUi.supporters.grantsDisclaimer')}</p>
|
||||||
<p className="mt-1 text-xs leading-relaxed text-gray-500">{t('walletUi.supporters.grantsDisclaimerDesc')}</p>
|
<p className="mt-1 text-xs leading-relaxed text-gray-400">{t('walletUi.supporters.grantsDisclaimerDesc')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4 grid grid-cols-1 gap-3 sm:grid-cols-2">
|
<p className="mt-4 text-[11px] font-bold uppercase tracking-wide text-[#f7931a]">
|
||||||
|
{t('walletUi.supporters.grantsRequired')}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-2 grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label htmlFor="grant-org">{t('walletUi.supporters.grantsOrg')} *</Label>
|
<Label htmlFor="grant-org" className="text-gray-200">{t('walletUi.supporters.grantsOrg')} *</Label>
|
||||||
<Input id="grant-org" value={org} onChange={(e) => setOrg(e.target.value)} placeholder="e.g. OpenSats" />
|
<Input id="grant-org" value={org} onChange={(e) => setOrg(e.target.value)} placeholder="e.g. OpenSats" className={inputClass} required aria-required="true" />
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label htmlFor="grant-contact">{t('walletUi.supporters.grantsContact')} *</Label>
|
<Label htmlFor="grant-contact" className="text-gray-200">{t('walletUi.supporters.grantsContact')} *</Label>
|
||||||
<Input id="grant-contact" value={contact} onChange={(e) => setContact(e.target.value)} placeholder="npub, email or @handle" />
|
<Input id="grant-contact" value={contact} onChange={(e) => setContact(e.target.value)} placeholder="npub / NIP-05 / email / @handle" className={inputClass} required aria-required="true" />
|
||||||
</div>
|
<p className="text-[10px] leading-relaxed text-gray-500">{t('walletUi.supporters.grantsContactHint')}</p>
|
||||||
<div className="space-y-1.5">
|
|
||||||
<Label htmlFor="grant-website">{t('walletUi.supporters.grantsWebsite')}</Label>
|
|
||||||
<Input id="grant-website" value={website} onChange={(e) => setWebsite(e.target.value)} placeholder="https://…" />
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
<Label htmlFor="grant-program">{t('walletUi.supporters.grantsProgram')}</Label>
|
|
||||||
<Input id="grant-program" value={program} onChange={(e) => setProgram(e.target.value)} placeholder="e.g. Q1 2027 grants" />
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
<Label htmlFor="grant-size">{t('walletUi.supporters.grantsSize')}</Label>
|
|
||||||
<Input id="grant-size" value={grantSize} onChange={(e) => setGrantSize(e.target.value)} placeholder="e.g. 5–25M sats" />
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
<Label htmlFor="grant-kyc">{t('walletUi.supporters.grantsKyc')}</Label>
|
|
||||||
<Input id="grant-kyc" value={kyc} onChange={(e) => setKyc(e.target.value)} placeholder="e.g. KYB + grantee KYC" />
|
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1.5 sm:col-span-2">
|
<div className="space-y-1.5 sm:col-span-2">
|
||||||
<Label htmlFor="grant-fund">{t('walletUi.supporters.grantsFund')}</Label>
|
<Label htmlFor="grant-website" className="text-gray-200">{t('walletUi.supporters.grantsWebsite')}</Label>
|
||||||
<Textarea id="grant-fund" value={fund} onChange={(e) => setFund(e.target.value)} rows={3} placeholder="Relay, app development, translation, design, events…" />
|
<Input
|
||||||
|
id="grant-website"
|
||||||
|
type="url"
|
||||||
|
value={website}
|
||||||
|
onChange={(e) => setWebsite(e.target.value)}
|
||||||
|
placeholder="https://…"
|
||||||
|
className={inputClass}
|
||||||
|
aria-invalid={!!websiteError}
|
||||||
|
aria-describedby={websiteError ? 'grant-website-error' : undefined}
|
||||||
|
/>
|
||||||
|
{websiteError && <p id="grant-website-error" role="alert" className="text-[10px] text-red-400">{websiteError}</p>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 rounded-xl border border-[#f7931a]/15 bg-black/25 p-3">
|
||||||
|
<p className="text-[10px] font-black uppercase tracking-widest text-[#f7931a]">{t('walletUi.supporters.grantsFundTitle')}</p>
|
||||||
|
<div className="mt-2 grid grid-cols-1 gap-2 sm:grid-cols-3">
|
||||||
|
{PAYS_FOR.slice(0, 3).map(({ icon: Icon, key, desc }) => (
|
||||||
|
<div key={key} className="flex items-start gap-2 text-xs text-gray-300">
|
||||||
|
<Icon className="mt-0.5 h-3.5 w-3.5 shrink-0 text-[#f7931a]" />
|
||||||
|
<div>
|
||||||
|
<p className="font-bold text-white">{t(key)}</p>
|
||||||
|
<p className="mt-0.5 text-[10px] leading-relaxed text-gray-500">{t(desc)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label className="mt-4 flex cursor-pointer items-start gap-2.5 rounded-xl border border-[#f7931a]/20 bg-black/30 p-3 text-xs text-gray-300">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={ack}
|
||||||
|
onChange={(e) => setAck(e.target.checked)}
|
||||||
|
className="mt-0.5 h-5 w-5 shrink-0 accent-[#f7931a]"
|
||||||
|
required
|
||||||
|
aria-required="true"
|
||||||
|
/>
|
||||||
|
<span>{t('walletUi.supporters.grantsAck')} *</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<details className="group mt-4 rounded-xl border border-white/10 bg-black/25">
|
||||||
|
<summary className="flex min-h-11 cursor-pointer list-none items-center gap-2 px-3 py-2 text-xs font-bold text-gray-300">
|
||||||
|
<ChevronDown className="h-4 w-4 text-[#f7931a] transition-transform group-open:rotate-180" />
|
||||||
|
{t('walletUi.supporters.grantsMoreDetails')}
|
||||||
|
<span className="ml-auto text-[10px] font-normal text-gray-500">{t('walletUi.supporters.grantsOptional')}</span>
|
||||||
|
</summary>
|
||||||
|
<div className="grid grid-cols-1 gap-3 border-t border-white/10 p-3 sm:grid-cols-2">
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label htmlFor="grant-program" className="text-gray-200">{t('walletUi.supporters.grantsProgram')}</Label>
|
||||||
|
<Input id="grant-program" value={program} onChange={(e) => setProgram(e.target.value)} placeholder="e.g. Q1 2027 grants" className={inputClass} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label htmlFor="grant-size" className="text-gray-200">{t('walletUi.supporters.grantsSize')}</Label>
|
||||||
|
<Input id="grant-size" value={grantSize} onChange={(e) => setGrantSize(e.target.value)} placeholder="e.g. 5–25M sats" className={inputClass} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label htmlFor="grant-kyc" className="text-gray-200">{t('walletUi.supporters.grantsKyc')}</Label>
|
||||||
|
<Input id="grant-kyc" value={kyc} onChange={(e) => setKyc(e.target.value)} placeholder="e.g. KYB + grantee KYC" className={inputClass} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5 sm:col-span-2">
|
||||||
|
<Label htmlFor="grant-fund" className="text-gray-200">{t('walletUi.supporters.grantsFund')}</Label>
|
||||||
|
<Textarea id="grant-fund" value={fund} onChange={(e) => setFund(e.target.value)} rows={3} placeholder="Relay, app development, translation, design, events…" className={inputClass} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
<div className="mt-4 grid grid-cols-1 gap-2 sm:grid-cols-3">
|
<div className="mt-4 grid grid-cols-1 gap-2 sm:grid-cols-3">
|
||||||
{[
|
{[
|
||||||
{ icon: ShieldCheck, title: t('walletUi.supporters.grantsNoKycTitle'), body: t('walletUi.supporters.grantsNoKycDesc') },
|
{ icon: ShieldCheck, title: t('walletUi.supporters.grantsNoKycTitle'), body: t('walletUi.supporters.grantsNoKycDesc') },
|
||||||
|
|
@ -212,37 +290,54 @@ function GrantmakerForm() {
|
||||||
<Icon className="mt-0.5 h-3.5 w-3.5 shrink-0 text-[#f7931a]" />
|
<Icon className="mt-0.5 h-3.5 w-3.5 shrink-0 text-[#f7931a]" />
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="text-[11px] font-bold text-white">{title}</p>
|
<p className="text-[11px] font-bold text-white">{title}</p>
|
||||||
<p className="mt-0.5 text-[10px] leading-relaxed text-gray-500">{body}</p>
|
<p className="mt-0.5 text-[10px] leading-relaxed text-gray-400">{body}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="mt-3 rounded-xl border border-[#f7931a]/15 bg-[#f7931a]/[0.04] px-3 py-2 text-[11px] leading-relaxed text-gray-500">
|
<p className="mt-3 rounded-xl border border-[#f7931a]/15 bg-[#f7931a]/[0.04] px-3 py-2 text-[11px] leading-relaxed text-gray-400">
|
||||||
{t('walletUi.supporters.grantsEthos')}
|
{t('walletUi.supporters.grantsEthos')}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<label className="mt-4 flex cursor-pointer items-start gap-2.5 rounded-xl border border-[#f7931a]/20 bg-black/30 p-3 text-xs text-gray-400">
|
|
||||||
<input type="checkbox" checked={ack} onChange={(e) => setAck(e.target.checked)} className="mt-0.5 h-4 w-4 accent-[#f7931a]" />
|
|
||||||
<span>{t('walletUi.supporters.grantsAck')}</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div className="mt-4 flex flex-col gap-2 sm:flex-row sm:items-center">
|
<div className="mt-4 flex flex-col gap-2 sm:flex-row sm:items-center">
|
||||||
<Button
|
<Button
|
||||||
|
type="button"
|
||||||
disabled={!canSend}
|
disabled={!canSend}
|
||||||
onClick={() => {
|
onClick={() => setReviewOpen(true)}
|
||||||
if (!canSend || !owner) return;
|
className="min-h-11 w-full justify-center bg-[#f7931a] font-bold text-black hover:bg-[#ffb347] disabled:opacity-50 sm:w-auto"
|
||||||
navigate(`/messages/${nip19.npubEncode(owner)}?message=${encodeURIComponent(message)}`);
|
|
||||||
}}
|
|
||||||
className="w-full justify-center bg-[#f7931a] font-bold text-black hover:bg-[#ffb347] sm:w-auto"
|
|
||||||
>
|
>
|
||||||
<Handshake className="mr-2 h-4 w-4" /> {t('walletUi.supporters.grantsSend')}
|
<Handshake className="mr-2 h-4 w-4" /> {t('walletUi.supporters.grantsReview')}
|
||||||
</Button>
|
</Button>
|
||||||
<p className="flex items-center gap-1.5 text-[11px] text-gray-600">
|
<p className="flex items-center gap-1.5 text-[11px] text-gray-400" aria-live="polite">
|
||||||
<Info className="h-3 w-3 shrink-0" />
|
<Info className="h-3 w-3 shrink-0" />
|
||||||
{t('walletUi.supporters.grantsNote')}
|
{canSend ? t('walletUi.supporters.grantsNote') : t('walletUi.supporters.grantsRequired')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Dialog open={reviewOpen} onOpenChange={setReviewOpen}>
|
||||||
|
<DialogContent className="max-h-[85dvh] max-w-lg overflow-y-auto border-[#f7931a]/30 bg-zinc-950">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-white">{t('walletUi.supporters.grantsReviewTitle')}</DialogTitle>
|
||||||
|
<DialogDescription>{t('walletUi.supporters.grantsReviewDesc')}</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<pre className="max-h-[40dvh] overflow-y-auto whitespace-pre-wrap break-words rounded-xl border border-white/10 bg-black/40 p-3 text-xs leading-relaxed text-gray-300">
|
||||||
|
{message}
|
||||||
|
</pre>
|
||||||
|
<DialogFooter className="flex-col-reverse gap-2 sm:flex-row sm:justify-end">
|
||||||
|
<Button variant="outline" onClick={() => setReviewOpen(false)} className="border-white/15">
|
||||||
|
{t('common.cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" onClick={() => void copyMessage()} className="gap-2 border-white/15">
|
||||||
|
{copied ? <Check className="h-4 w-4 text-[#ccff00]" /> : <Copy className="h-4 w-4" />}
|
||||||
|
{copied ? t('walletUi.supporters.grantsCopied') : t('walletUi.supporters.grantsCopy')}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={openDm} disabled={!canSend} className="gap-2 bg-[#f7931a] font-bold text-black hover:bg-[#ffb347]">
|
||||||
|
<Handshake className="h-4 w-4" /> {t('walletUi.supporters.grantsOpenDm')}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -559,7 +654,17 @@ export default function SupportersPage() {
|
||||||
const totalUsd = useSatsToUsd(allSats);
|
const totalUsd = useSatsToUsd(allSats);
|
||||||
const { data: stats } = useGrowthStats();
|
const { data: stats } = useGrowthStats();
|
||||||
const [showAll, setShowAll] = useState(false);
|
const [showAll, setShowAll] = useState(false);
|
||||||
const [tab, setTab] = useState<'support' | 'backers' | 'community' | 'grants'>('support');
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
const tabParam = searchParams.get('tab');
|
||||||
|
const tab: 'support' | 'backers' | 'community' | 'grants' =
|
||||||
|
tabParam === 'backers' || tabParam === 'community' || tabParam === 'grants'
|
||||||
|
? tabParam
|
||||||
|
: 'support';
|
||||||
|
const setTab = (value: 'support' | 'backers' | 'community' | 'grants') => {
|
||||||
|
const next = new URLSearchParams(searchParams);
|
||||||
|
next.set('tab', value);
|
||||||
|
setSearchParams(next, { replace: true });
|
||||||
|
};
|
||||||
const [rail, setRail] = useState<'support-lightning' | 'support-silent' | 'support-angor'>('support-lightning');
|
const [rail, setRail] = useState<'support-lightning' | 'support-silent' | 'support-angor'>('support-lightning');
|
||||||
|
|
||||||
// One ranked wall: Angor backers + Lightning zaps to the project, merged by npub.
|
// One ranked wall: Angor backers + Lightning zaps to the project, merged by npub.
|
||||||
|
|
@ -694,12 +799,12 @@ export default function SupportersPage() {
|
||||||
{ value: 'support', label: t('walletUi.supporters.support') },
|
{ value: 'support', label: t('walletUi.supporters.support') },
|
||||||
{ value: 'backers', label: t('walletUi.supporters.backers') },
|
{ value: 'backers', label: t('walletUi.supporters.backers') },
|
||||||
{ value: 'community', label: t('walletUi.supporters.community') },
|
{ value: 'community', label: t('walletUi.supporters.community') },
|
||||||
{ value: 'grants', label: t('walletUi.supporters.grantsSection') },
|
{ value: 'grants', label: t('walletUi.supporters.grantsTab') },
|
||||||
].map((item) => (
|
].map((item) => (
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
key={item.value}
|
key={item.value}
|
||||||
value={item.value}
|
value={item.value}
|
||||||
className="h-auto min-h-10 whitespace-normal rounded-xl px-1.5 py-2 text-center text-[9px] font-black uppercase leading-tight tracking-wide data-[state=active]:bg-[#ccff00] data-[state=active]:text-black data-[state=active]:shadow-none sm:px-2 sm:text-[11px]"
|
className="h-auto min-h-11 whitespace-normal rounded-xl px-1.5 py-2 text-center text-[10px] font-black uppercase leading-tight tracking-wide data-[state=active]:bg-[#ccff00] data-[state=active]:text-black data-[state=active]:shadow-none sm:px-2 sm:text-[11px]"
|
||||||
>
|
>
|
||||||
{item.label}
|
{item.label}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue