15 changed files with 462 additions and 74 deletions
@ -0,0 +1,54 @@
@@ -0,0 +1,54 @@
|
||||
import { useFetchEvent } from '@/hooks' |
||||
import { getZapInfoFromEvent } from '@/lib/event-metadata' |
||||
import { formatAmount } from '@/lib/lightning' |
||||
import { cn } from '@/lib/utils' |
||||
import { Zap } from 'lucide-react' |
||||
import { Event } from 'nostr-tools' |
||||
import { useMemo } from 'react' |
||||
import { useTranslation } from 'react-i18next' |
||||
import Username from '../Username' |
||||
|
||||
export default function ZapPreview({ event, className }: { event: Event; className?: string }) { |
||||
const { t } = useTranslation() |
||||
const zapInfo = useMemo(() => getZapInfoFromEvent(event), [event]) |
||||
const { event: targetEvent } = useFetchEvent(zapInfo?.eventId) |
||||
|
||||
if (!zapInfo || !zapInfo.senderPubkey || !zapInfo.amount) { |
||||
return ( |
||||
<div className={cn('text-sm text-muted-foreground', className)}> |
||||
[{t('Invalid zap receipt')}] |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
const { senderPubkey, recipientPubkey, amount, comment } = zapInfo |
||||
|
||||
return ( |
||||
<div className={cn('flex items-start gap-3 p-3 rounded-lg border bg-card', className)}> |
||||
<Zap size={24} className="text-yellow-400 shrink-0 mt-0.5" fill="currentColor" /> |
||||
<div className="flex-1 min-w-0"> |
||||
<div className="flex items-center gap-2 flex-wrap"> |
||||
<Username userId={senderPubkey} className="font-semibold" /> |
||||
<span className="text-muted-foreground text-sm">{t('zapped')}</span> |
||||
{recipientPubkey && recipientPubkey !== senderPubkey && ( |
||||
<Username userId={recipientPubkey} className="font-semibold" /> |
||||
)} |
||||
</div> |
||||
<div className="font-bold text-yellow-400 mt-1"> |
||||
{formatAmount(amount)} {t('sats')} |
||||
</div> |
||||
{comment && ( |
||||
<div className="text-sm text-muted-foreground mt-2 break-words"> |
||||
{comment} |
||||
</div> |
||||
)} |
||||
{targetEvent && ( |
||||
<div className="text-xs text-muted-foreground mt-2"> |
||||
{t('on note')} {targetEvent.id.substring(0, 8)}... |
||||
</div> |
||||
)} |
||||
</div> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
@ -0,0 +1,80 @@
@@ -0,0 +1,80 @@
|
||||
import { useFetchEvent } from '@/hooks' |
||||
import { getZapInfoFromEvent } from '@/lib/event-metadata' |
||||
import { formatAmount } from '@/lib/lightning' |
||||
import { toNote, toProfile } from '@/lib/link' |
||||
import { cn } from '@/lib/utils' |
||||
import { Zap as ZapIcon } from 'lucide-react' |
||||
import { Event } from 'nostr-tools' |
||||
import { useMemo } from 'react' |
||||
import { useTranslation } from 'react-i18next' |
||||
import { useSecondaryPage } from '@/PageManager' |
||||
import Username from '../Username' |
||||
import UserAvatar from '../UserAvatar' |
||||
|
||||
export default function Zap({ event, className }: { event: Event; className?: string }) { |
||||
const { t } = useTranslation() |
||||
const { push } = useSecondaryPage() |
||||
const zapInfo = useMemo(() => getZapInfoFromEvent(event), [event]) |
||||
const { event: targetEvent } = useFetchEvent(zapInfo?.eventId) |
||||
|
||||
if (!zapInfo || !zapInfo.senderPubkey || !zapInfo.amount) { |
||||
return ( |
||||
<div className={cn('text-sm text-muted-foreground p-4 border rounded-lg', className)}> |
||||
[{t('Invalid zap receipt')}] |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
const { senderPubkey, recipientPubkey, amount, comment } = zapInfo |
||||
|
||||
return ( |
||||
<div className={cn('relative border rounded-lg p-4 bg-gradient-to-br from-yellow-50/50 to-amber-50/50 dark:from-yellow-950/20 dark:to-amber-950/20', className)}> |
||||
{/* Zapped note/profile link in top-right corner */} |
||||
{(targetEvent || recipientPubkey) && ( |
||||
<button |
||||
onClick={() => { |
||||
if (targetEvent) { |
||||
push(toNote(targetEvent.id)) |
||||
} else if (recipientPubkey) { |
||||
push(toProfile(recipientPubkey)) |
||||
} |
||||
}} |
||||
className="absolute top-2 right-2 text-xs text-muted-foreground hover:text-foreground hover:underline" |
||||
> |
||||
{targetEvent ? t('Zapped note') : t('Zapped profile')} |
||||
</button> |
||||
)} |
||||
|
||||
<div className="flex items-start gap-3"> |
||||
<ZapIcon size={28} className="text-yellow-500 shrink-0 mt-1" fill="currentColor" /> |
||||
<div className="flex-1 min-w-0"> |
||||
<div className="flex items-center gap-2 flex-wrap mb-2"> |
||||
<UserAvatar userId={senderPubkey} size="small" /> |
||||
<Username userId={senderPubkey} className="font-semibold" /> |
||||
<span className="text-muted-foreground text-sm">{t('zapped')}</span> |
||||
{recipientPubkey && recipientPubkey !== senderPubkey && ( |
||||
<> |
||||
<UserAvatar userId={recipientPubkey} size="small" /> |
||||
<Username userId={recipientPubkey} className="font-semibold" /> |
||||
</> |
||||
)} |
||||
</div> |
||||
<div className="flex items-baseline gap-2"> |
||||
<span className="text-3xl font-bold text-yellow-600 dark:text-yellow-400"> |
||||
{formatAmount(amount)} |
||||
</span> |
||||
<span className="text-lg font-semibold text-yellow-600/70 dark:text-yellow-400/70"> |
||||
{t('sats')} |
||||
</span> |
||||
</div> |
||||
{comment && ( |
||||
<div className="mt-3 text-sm bg-white/50 dark:bg-black/20 rounded-lg p-3 break-words"> |
||||
{comment} |
||||
</div> |
||||
)} |
||||
</div> |
||||
</div> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
@ -0,0 +1,46 @@
@@ -0,0 +1,46 @@
|
||||
import { Input } from '@/components/ui/input' |
||||
import { Label } from '@/components/ui/label' |
||||
import { useZap } from '@/providers/ZapProvider' |
||||
import { useState } from 'react' |
||||
import { useTranslation } from 'react-i18next' |
||||
|
||||
export default function ZapReplyThresholdInput() { |
||||
const { t } = useTranslation() |
||||
const { zapReplyThreshold, updateZapReplyThreshold } = useZap() |
||||
const [zapReplyThresholdInput, setZapReplyThresholdInput] = useState(zapReplyThreshold) |
||||
|
||||
return ( |
||||
<div className="w-full space-y-1"> |
||||
<Label htmlFor="zap-reply-threshold-input"> |
||||
<div className="text-base font-medium">{t('Zap reply threshold')}</div> |
||||
<div className="text-muted-foreground text-sm"> |
||||
{t('Zaps above this amount will appear as replies in threads')} |
||||
</div> |
||||
</Label> |
||||
<div className="flex items-center gap-2"> |
||||
<Input |
||||
id="zap-reply-threshold-input" |
||||
className="w-20" |
||||
value={zapReplyThresholdInput} |
||||
onChange={(e) => { |
||||
setZapReplyThresholdInput((pre) => { |
||||
if (e.target.value === '') { |
||||
return 0 |
||||
} |
||||
let num = parseInt(e.target.value, 10) |
||||
if (isNaN(num) || num < 0) { |
||||
num = pre |
||||
} |
||||
return num |
||||
}) |
||||
}} |
||||
onBlur={() => { |
||||
updateZapReplyThreshold(zapReplyThresholdInput) |
||||
}} |
||||
/> |
||||
<span className="text-sm text-muted-foreground shrink-0">{t('sats')}</span> |
||||
</div> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
Loading…
Reference in new issue