~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).
120 lines
4.8 KiB
TypeScript
120 lines
4.8 KiB
TypeScript
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
import { ZapButton } from '@/components/ZapButton';
|
|
import { useCrewGoal } from '@/hooks/useCrewGoal';
|
|
import { useTranslation } from '@/contexts/language';
|
|
import { cn } from '@/lib/utils';
|
|
import { Target, Zap, Users, CheckCircle2 } from 'lucide-react';
|
|
|
|
/**
|
|
* Crew treasury — a transparent, zap-funded NIP-75 goal. Contributions are
|
|
* ordinary zaps to the goal event that settle to the founder's own Lightning
|
|
* wallet; SkateSpots never custodies funds. Progress is tallied from public
|
|
* zap receipts, so anyone can audit it.
|
|
*/
|
|
export function CrewTreasuryCard({ goalId }: { goalId?: string }) {
|
|
const { t } = useTranslation();
|
|
const { data: goal, isLoading } = useCrewGoal(goalId);
|
|
|
|
if (!goalId) return null;
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<Card className="bg-black/90 border-2 border-orange-500/30">
|
|
<CardHeader>
|
|
<Skeleton className="h-5 w-40" />
|
|
</CardHeader>
|
|
<CardContent className="space-y-3">
|
|
<Skeleton className="h-3 w-full" />
|
|
<Skeleton className="h-3 w-2/3" />
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
if (!goal) return null;
|
|
|
|
const pct = Math.min(100, goal.percent);
|
|
const reached = goal.percent >= 100;
|
|
const now = Math.floor(Date.now() / 1000);
|
|
const closed = goal.closedAt ? goal.closedAt < now : false;
|
|
const daysLeft = goal.closedAt ? Math.ceil((goal.closedAt - now) / 86400) : null;
|
|
|
|
return (
|
|
<Card
|
|
className={cn(
|
|
'overflow-hidden border-2 bg-black/90',
|
|
reached ? 'border-green-500/50' : 'border-orange-500/40',
|
|
)}
|
|
>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="flex items-center gap-2 text-lg font-black uppercase text-white">
|
|
<Target className={cn('h-5 w-5', reached ? 'text-green-400' : 'text-orange-400')} />
|
|
{t('socialUi.treasury.title')}
|
|
</CardTitle>
|
|
{goal.summary && <p className="text-xs text-gray-400">{goal.summary}</p>}
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{goal.description && (
|
|
<p className="whitespace-pre-wrap text-sm text-gray-300">{goal.description}</p>
|
|
)}
|
|
|
|
{/* Progress bar */}
|
|
<div className="space-y-1.5">
|
|
<div className="flex items-end justify-between text-sm">
|
|
<span className={cn('font-black', reached ? 'text-green-400' : 'text-orange-400')}>
|
|
{goal.raisedSats.toLocaleString()} {t('common.sats')}
|
|
</span>
|
|
<span className="text-xs text-gray-500">
|
|
{t('socialUi.treasury.ofGoal', { target: goal.targetSats.toLocaleString() })}
|
|
</span>
|
|
</div>
|
|
<div className="h-3 w-full overflow-hidden rounded-full bg-white/5">
|
|
<div
|
|
className={cn(
|
|
'h-full rounded-full transition-all duration-700',
|
|
reached
|
|
? 'bg-gradient-to-r from-green-500 to-emerald-400'
|
|
: 'bg-gradient-to-r from-orange-500 to-amber-400',
|
|
)}
|
|
style={{ width: `${pct}%` }}
|
|
/>
|
|
</div>
|
|
<div className="flex items-center justify-between text-[11px] text-gray-500">
|
|
<span className="flex items-center gap-1">
|
|
{reached ? (
|
|
<span className="flex items-center gap-1 font-bold text-green-400">
|
|
<CheckCircle2 className="h-3 w-3" /> {t('socialUi.treasury.goalReached')}
|
|
</span>
|
|
) : (
|
|
<span className="font-bold text-orange-300">{t('socialUi.treasury.funded', { percent: goal.percent })}</span>
|
|
)}
|
|
</span>
|
|
<span className="flex items-center gap-1">
|
|
<Users className="h-3 w-3" />
|
|
{goal.contributions === 1
|
|
? t('socialUi.treasury.contributionOne', { count: goal.contributions })
|
|
: t('socialUi.treasury.contributionMany', { count: goal.contributions })}
|
|
{daysLeft !== null && daysLeft > 0 && ` · ${t('socialUi.treasury.daysLeft', { days: daysLeft })}`}
|
|
{closed && ` · ${t('socialUi.treasury.closed')}`}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Contribute — zaps the goal event; settles to the founder's wallet. */}
|
|
{!closed && (
|
|
<div className="space-y-2">
|
|
<ZapButton
|
|
target={goal.event}
|
|
className="w-full justify-center bg-orange-500 text-black hover:bg-orange-600"
|
|
/>
|
|
<p className="flex items-center justify-center gap-1 text-center text-[10px] text-gray-600">
|
|
<Zap className="h-2.5 w-2.5" />
|
|
{t('socialUi.treasury.nonCustodial')}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|