feat: zap (#107)
This commit is contained in:
@@ -8,11 +8,11 @@ import { Separator } from '@/components/ui/separator'
|
||||
import { SimpleUserAvatar } from '@/components/UserAvatar'
|
||||
import { SimpleUsername } from '@/components/Username'
|
||||
import PrimaryPageLayout from '@/layouts/PrimaryPageLayout'
|
||||
import { toProfile, toSettings } from '@/lib/link'
|
||||
import { toProfile, toSettings, toWallet } from '@/lib/link'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useSecondaryPage } from '@/PageManager'
|
||||
import { useNostr } from '@/providers/NostrProvider'
|
||||
import { ArrowDownUp, ChevronRight, LogOut, Settings, UserRound } from 'lucide-react'
|
||||
import { ArrowDownUp, ChevronRight, LogOut, Settings, UserRound, Wallet } from 'lucide-react'
|
||||
import { forwardRef, HTMLProps, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@@ -54,6 +54,10 @@ const MePage = forwardRef((_, ref) => {
|
||||
<UserRound />
|
||||
{t('Profile')}
|
||||
</Item>
|
||||
<Item onClick={() => push(toWallet())}>
|
||||
<Wallet />
|
||||
{t('Wallet')}
|
||||
</Item>
|
||||
<Item onClick={() => setLoginDialogOpen(true)}>
|
||||
<ArrowDownUp /> {t('Switch account')}
|
||||
</Item>
|
||||
|
||||
@@ -24,9 +24,7 @@ const NotificationListPage = forwardRef((_, ref) => {
|
||||
titlebar={<NotificationListPageTitlebar />}
|
||||
displayScrollToTopButton
|
||||
>
|
||||
<div className="px-4">
|
||||
<NotificationList ref={notificationListRef} />
|
||||
</div>
|
||||
<NotificationList ref={notificationListRef} />
|
||||
</PrimaryPageLayout>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import NoteList from '@/components/NoteList'
|
||||
import { SEARCHABLE_RELAY_URLS } from '@/constants'
|
||||
import { useFetchRelayInfos, useSearchParams } from '@/hooks'
|
||||
import { useFetchRelayInfos } from '@/hooks'
|
||||
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
|
||||
import { useFeed } from '@/providers/FeedProvider'
|
||||
import { Filter } from 'nostr-tools'
|
||||
@@ -11,7 +11,6 @@ const NoteListPage = forwardRef(({ index }: { index?: number }, ref) => {
|
||||
const { t } = useTranslation()
|
||||
const { relayUrls } = useFeed()
|
||||
const { searchableRelayUrls } = useFetchRelayInfos(relayUrls)
|
||||
const { searchParams } = useSearchParams()
|
||||
const {
|
||||
title = '',
|
||||
filter,
|
||||
@@ -21,6 +20,7 @@ const NoteListPage = forwardRef(({ index }: { index?: number }, ref) => {
|
||||
filter?: Filter
|
||||
urls: string[]
|
||||
}>(() => {
|
||||
const searchParams = new URLSearchParams(window.location.search)
|
||||
const hashtag = searchParams.get('t')
|
||||
if (hashtag) {
|
||||
return {
|
||||
@@ -40,7 +40,7 @@ const NoteListPage = forwardRef(({ index }: { index?: number }, ref) => {
|
||||
}
|
||||
}
|
||||
return { urls: relayUrls }
|
||||
}, [searchParams, JSON.stringify(relayUrls)])
|
||||
}, [JSON.stringify(relayUrls)])
|
||||
|
||||
return (
|
||||
<SecondaryPageLayout ref={ref} index={index} title={title} displayScrollToTopButton>
|
||||
|
||||
@@ -4,8 +4,8 @@ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { EMAIL_REGEX } from '@/constants'
|
||||
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
|
||||
import { isEmail } from '@/lib/common'
|
||||
import { createProfileDraftEvent } from '@/lib/draft-event'
|
||||
import { generateImageByPubkey } from '@/lib/pubkey'
|
||||
import { useSecondaryPage } from '@/PageManager'
|
||||
@@ -24,6 +24,8 @@ const ProfileEditorPage = forwardRef(({ index }: { index?: number }, ref) => {
|
||||
const [about, setAbout] = useState<string>('')
|
||||
const [nip05, setNip05] = useState<string>('')
|
||||
const [nip05Error, setNip05Error] = useState<string>('')
|
||||
const [lightningAddress, setLightningAddress] = useState<string>('')
|
||||
const [lightningAddressError, setLightningAddressError] = useState<string>('')
|
||||
const [hasChanged, setHasChanged] = useState(false)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [uploadingBanner, setUploadingBanner] = useState(false)
|
||||
@@ -40,22 +42,38 @@ const ProfileEditorPage = forwardRef(({ index }: { index?: number }, ref) => {
|
||||
setUsername(profile.original_username ?? '')
|
||||
setAbout(profile.about ?? '')
|
||||
setNip05(profile.nip05 ?? '')
|
||||
setLightningAddress(profile.lightningAddress || '')
|
||||
} else {
|
||||
setBanner('')
|
||||
setAvatar('')
|
||||
setUsername('')
|
||||
setAbout('')
|
||||
setNip05('')
|
||||
setLightningAddress('')
|
||||
}
|
||||
}, [profile])
|
||||
|
||||
if (!account || !profile) return null
|
||||
|
||||
const save = async () => {
|
||||
if (nip05 && !EMAIL_REGEX.test(nip05)) {
|
||||
if (nip05 && !isEmail(nip05)) {
|
||||
setNip05Error(t('Invalid NIP-05 address'))
|
||||
return
|
||||
}
|
||||
|
||||
let lud06 = profile.lud06
|
||||
let lud16 = profile.lud16
|
||||
if (lightningAddress) {
|
||||
if (isEmail(lightningAddress)) {
|
||||
lud16 = lightningAddress
|
||||
} else if (lightningAddress.startsWith('lnurl')) {
|
||||
lud06 = lightningAddress
|
||||
} else {
|
||||
setLightningAddressError(t('Invalid Lightning Address'))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
setSaving(true)
|
||||
setHasChanged(false)
|
||||
const oldProfileContent = profileEvent ? JSON.parse(profileEvent.content) : {}
|
||||
@@ -67,7 +85,9 @@ const ProfileEditorPage = forwardRef(({ index }: { index?: number }, ref) => {
|
||||
about,
|
||||
nip05,
|
||||
banner,
|
||||
picture: avatar
|
||||
picture: avatar,
|
||||
lud06,
|
||||
lud16
|
||||
}
|
||||
const profileDraftEvent = createProfileDraftEvent(
|
||||
JSON.stringify(newProfileContent),
|
||||
@@ -100,7 +120,7 @@ const ProfileEditorPage = forwardRef(({ index }: { index?: number }, ref) => {
|
||||
return (
|
||||
<SecondaryPageLayout ref={ref} index={index} title={profile.username} controls={controls}>
|
||||
<div className="px-4">
|
||||
<div className="relative bg-cover bg-center w-full aspect-[21/9] rounded-lg mb-2">
|
||||
<div className="relative bg-cover bg-center rounded-lg mb-2">
|
||||
<Uploader
|
||||
onUploadSuccess={onBannerUploadSuccess}
|
||||
onUploadingChange={(uploading) => setTimeout(() => setUploadingBanner(uploading), 50)}
|
||||
@@ -109,7 +129,7 @@ const ProfileEditorPage = forwardRef(({ index }: { index?: number }, ref) => {
|
||||
<ProfileBanner
|
||||
banner={banner}
|
||||
pubkey={account.pubkey}
|
||||
className="w-full aspect-video object-cover"
|
||||
className="w-full aspect-video object-cover rounded-lg"
|
||||
/>
|
||||
<div className="absolute top-0 bg-muted/30 w-full h-full rounded-lg flex flex-col justify-center items-center">
|
||||
{uploadingBanner ? (
|
||||
@@ -170,6 +190,21 @@ const ProfileEditorPage = forwardRef(({ index }: { index?: number }, ref) => {
|
||||
/>
|
||||
{nip05Error && <div className="text-xs text-destructive pl-3">{nip05Error}</div>}
|
||||
</Item>
|
||||
<Item>
|
||||
<ItemTitle>{t('Lightning Address (or LNURL)')}</ItemTitle>
|
||||
<Input
|
||||
value={lightningAddress}
|
||||
onChange={(e) => {
|
||||
setLightningAddressError('')
|
||||
setLightningAddress(e.target.value)
|
||||
setHasChanged(true)
|
||||
}}
|
||||
className={lightningAddressError ? 'border-destructive' : ''}
|
||||
/>
|
||||
{lightningAddressError && (
|
||||
<div className="text-xs text-destructive pl-3">{lightningAddressError}</div>
|
||||
)}
|
||||
</Item>
|
||||
</div>
|
||||
</div>
|
||||
</SecondaryPageLayout>
|
||||
@@ -179,7 +214,7 @@ ProfileEditorPage.displayName = 'ProfileEditorPage'
|
||||
export default ProfileEditorPage
|
||||
|
||||
function ItemTitle({ children }: { children: React.ReactNode }) {
|
||||
return <div className="text-sm font-semibold text-muted-foreground pl-3">{children}</div>
|
||||
return <div className="text-sm font-semibold text-muted-foreground">{children}</div>
|
||||
}
|
||||
|
||||
function Item({ children }: { children: React.ReactNode }) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import UserItem from '@/components/UserItem'
|
||||
import { SEARCHABLE_RELAY_URLS } from '@/constants'
|
||||
import { useFetchRelayInfos, useSearchParams } from '@/hooks'
|
||||
import { useFetchRelayInfos } from '@/hooks'
|
||||
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
|
||||
import { useFeed } from '@/providers/FeedProvider'
|
||||
import client from '@/services/client.service'
|
||||
@@ -13,7 +13,6 @@ const LIMIT = 50
|
||||
|
||||
const ProfileListPage = forwardRef(({ index }: { index?: number }, ref) => {
|
||||
const { t } = useTranslation()
|
||||
const { searchParams } = useSearchParams()
|
||||
const { relayUrls } = useFeed()
|
||||
const { searchableRelayUrls } = useFetchRelayInfos(relayUrls)
|
||||
const [until, setUntil] = useState<number>(() => dayjs().unix())
|
||||
@@ -22,12 +21,13 @@ const ProfileListPage = forwardRef(({ index }: { index?: number }, ref) => {
|
||||
const bottomRef = useRef<HTMLDivElement>(null)
|
||||
const filter = useMemo(() => {
|
||||
const f: Filter = { until }
|
||||
const searchParams = new URLSearchParams(window.location.search)
|
||||
const search = searchParams.get('s')
|
||||
if (search) {
|
||||
f.search = search
|
||||
}
|
||||
return f
|
||||
}, [searchParams, until])
|
||||
}, [until])
|
||||
const urls = useMemo(() => {
|
||||
return filter.search ? searchableRelayUrls.concat(SEARCHABLE_RELAY_URLS).slice(0, 4) : relayUrls
|
||||
}, [relayUrls, searchableRelayUrls, filter])
|
||||
|
||||
@@ -4,6 +4,7 @@ import NoteList from '@/components/NoteList'
|
||||
import ProfileAbout from '@/components/ProfileAbout'
|
||||
import ProfileBanner from '@/components/ProfileBanner'
|
||||
import ProfileOptions from '@/components/ProfileOptions'
|
||||
import ProfileZapButton from '@/components/ProfileZapButton'
|
||||
import PubkeyCopy from '@/components/PubkeyCopy'
|
||||
import QrCodePopover from '@/components/QrCodePopover'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
@@ -18,7 +19,7 @@ import { SecondaryPageLink, useSecondaryPage } from '@/PageManager'
|
||||
import { useFeed } from '@/providers/FeedProvider'
|
||||
import { useMuteList } from '@/providers/MuteListProvider'
|
||||
import { useNostr } from '@/providers/NostrProvider'
|
||||
import { Link } from 'lucide-react'
|
||||
import { Link, Zap } from 'lucide-react'
|
||||
import { forwardRef, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import NotFoundPage from '../NotFoundPage'
|
||||
@@ -55,11 +56,13 @@ const ProfilePage = forwardRef(({ id, index }: { id?: string; index?: number },
|
||||
if (!profile && isFetching) {
|
||||
return (
|
||||
<SecondaryPageLayout index={index} ref={ref}>
|
||||
<div className="px-4">
|
||||
<div className="relative bg-cover bg-center w-full aspect-[21/9] rounded-lg mb-2">
|
||||
<Skeleton className="w-full h-full object-cover rounded-lg" />
|
||||
<Skeleton className="w-24 h-24 absolute bottom-0 left-4 translate-y-1/2 border-4 border-background rounded-full" />
|
||||
<div className="sm:px-4">
|
||||
<div className="relative bg-cover bg-center mb-2">
|
||||
<Skeleton className="w-full aspect-video sm:rounded-lg" />
|
||||
<Skeleton className="w-24 h-24 absolute bottom-0 left-3 translate-y-1/2 border-4 border-background rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-4">
|
||||
<Skeleton className="h-5 w-28 mt-14 mb-1" />
|
||||
<Skeleton className="h-5 w-56 mt-2 my-1 rounded-full" />
|
||||
</div>
|
||||
@@ -68,29 +71,32 @@ const ProfilePage = forwardRef(({ id, index }: { id?: string; index?: number },
|
||||
}
|
||||
if (!profile) return <NotFoundPage />
|
||||
|
||||
const { banner, username, about, avatar, pubkey, website } = profile
|
||||
const { banner, username, about, avatar, pubkey, website, lightningAddress } = profile
|
||||
return (
|
||||
<SecondaryPageLayout index={index} title={username} displayScrollToTopButton ref={ref}>
|
||||
<div className="px-4">
|
||||
<div className="relative bg-cover bg-center w-full aspect-[21/9] rounded-lg mb-2">
|
||||
<div className="sm:px-4">
|
||||
<div className="relative bg-cover bg-center mb-2">
|
||||
<ProfileBanner
|
||||
banner={banner}
|
||||
pubkey={pubkey}
|
||||
className="w-full aspect-video object-cover"
|
||||
className="w-full aspect-video sm:rounded-lg"
|
||||
/>
|
||||
<Avatar className="w-24 h-24 absolute bottom-0 left-4 translate-y-1/2 border-4 border-background">
|
||||
<Avatar className="w-24 h-24 absolute left-3 bottom-0 translate-y-1/2 border-4 border-background">
|
||||
<AvatarImage src={avatar} className="object-cover object-center" />
|
||||
<AvatarFallback>
|
||||
<img src={defaultImage} />
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-4">
|
||||
<div className="flex justify-end h-8 gap-2 items-center">
|
||||
{isFollowingYou && (
|
||||
<div className="text-muted-foreground rounded-full bg-muted text-xs h-fit px-2">
|
||||
{t('Follows you')}
|
||||
</div>
|
||||
)}
|
||||
<ProfileOptions pubkey={pubkey} />
|
||||
{isSelf ? (
|
||||
<Button
|
||||
className="w-20 min-w-20 rounded-full"
|
||||
@@ -100,13 +106,21 @@ const ProfilePage = forwardRef(({ id, index }: { id?: string; index?: number },
|
||||
{t('Edit')}
|
||||
</Button>
|
||||
) : (
|
||||
<FollowButton pubkey={pubkey} />
|
||||
<>
|
||||
{!!lightningAddress && <ProfileZapButton pubkey={pubkey} />}
|
||||
<FollowButton pubkey={pubkey} />
|
||||
</>
|
||||
)}
|
||||
<ProfileOptions pubkey={pubkey} />
|
||||
</div>
|
||||
<div className="pt-2">
|
||||
<div className="text-xl font-semibold">{username}</div>
|
||||
<Nip05 pubkey={pubkey} />
|
||||
{lightningAddress && (
|
||||
<div className="text-sm text-yellow-400 flex gap-1 items-center">
|
||||
<Zap className="size-4" />
|
||||
{lightningAddress}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-1 mt-1">
|
||||
<PubkeyCopy pubkey={pubkey} />
|
||||
<QrCodePopover pubkey={pubkey} />
|
||||
|
||||
@@ -1,14 +1,25 @@
|
||||
import AboutInfoDialog from '@/components/AboutInfoDialog'
|
||||
import Donation from '@/components/Donation'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select'
|
||||
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
|
||||
import { toRelaySettings } from '@/lib/link'
|
||||
import { toRelaySettings, toWallet } from '@/lib/link'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useSecondaryPage } from '@/PageManager'
|
||||
import { useNostr } from '@/providers/NostrProvider'
|
||||
import { useTheme } from '@/providers/ThemeProvider'
|
||||
import { TLanguage } from '@/types'
|
||||
import { SelectValue } from '@radix-ui/react-select'
|
||||
import { Check, ChevronRight, Copy, Info, KeyRound, Languages, Server, SunMoon } from 'lucide-react'
|
||||
import {
|
||||
Check,
|
||||
ChevronRight,
|
||||
Copy,
|
||||
Info,
|
||||
KeyRound,
|
||||
Languages,
|
||||
Server,
|
||||
SunMoon,
|
||||
Wallet
|
||||
} from 'lucide-react'
|
||||
import { forwardRef, HTMLProps, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@@ -66,6 +77,13 @@ const SettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
|
||||
</div>
|
||||
<ChevronRight />
|
||||
</SettingItem>
|
||||
<SettingItem onClick={() => push(toWallet())}>
|
||||
<div className="flex items-center gap-4">
|
||||
<Wallet />
|
||||
<div>{t('Wallet')}</div>
|
||||
</div>
|
||||
<ChevronRight />
|
||||
</SettingItem>
|
||||
{!!nsec && (
|
||||
<SettingItem
|
||||
onClick={() => {
|
||||
@@ -110,6 +128,9 @@ const SettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
|
||||
</div>
|
||||
</SettingItem>
|
||||
</AboutInfoDialog>
|
||||
<div className="px-4 mt-4">
|
||||
<Donation />
|
||||
</div>
|
||||
</SecondaryPageLayout>
|
||||
)
|
||||
})
|
||||
|
||||
38
src/pages/secondary/WalletPage/DefaultZapAmountInput.tsx
Normal file
38
src/pages/secondary/WalletPage/DefaultZapAmountInput.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
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 DefaultZapAmountInput() {
|
||||
const { t } = useTranslation()
|
||||
const { defaultZapSats, updateDefaultSats } = useZap()
|
||||
const [defaultZapAmountInput, setDefaultZapAmountInput] = useState(defaultZapSats)
|
||||
|
||||
return (
|
||||
<div className="w-full space-y-1">
|
||||
<Label htmlFor="default-zap-amount-input">{t('Default zap amount')}</Label>
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<Input
|
||||
id="default-zap-amount-input"
|
||||
value={defaultZapAmountInput}
|
||||
onChange={(e) => {
|
||||
setDefaultZapAmountInput((pre) => {
|
||||
if (e.target.value === '') {
|
||||
return 0
|
||||
}
|
||||
let num = parseInt(e.target.value, 10)
|
||||
if (isNaN(num) || num < 0) {
|
||||
num = pre
|
||||
}
|
||||
return num
|
||||
})
|
||||
}}
|
||||
onBlur={() => {
|
||||
updateDefaultSats(defaultZapAmountInput)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
27
src/pages/secondary/WalletPage/DefaultZapCommentInput.tsx
Normal file
27
src/pages/secondary/WalletPage/DefaultZapCommentInput.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
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 DefaultZapCommentInput() {
|
||||
const { t } = useTranslation()
|
||||
const { defaultZapComment, updateDefaultComment } = useZap()
|
||||
const [defaultZapCommentInput, setDefaultZapCommentInput] = useState(defaultZapComment)
|
||||
|
||||
return (
|
||||
<div className="w-full space-y-1">
|
||||
<Label htmlFor="default-zap-comment-input">{t('Default zap comment')}</Label>
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<Input
|
||||
id="default-zap-comment-input"
|
||||
value={defaultZapCommentInput}
|
||||
onChange={(e) => setDefaultZapCommentInput(e.target.value)}
|
||||
onBlur={() => {
|
||||
updateDefaultComment(defaultZapCommentInput)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
82
src/pages/secondary/WalletPage/LightningAddressInput.tsx
Normal file
82
src/pages/secondary/WalletPage/LightningAddressInput.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { useToast } from '@/hooks'
|
||||
import { isEmail } from '@/lib/common'
|
||||
import { createProfileDraftEvent } from '@/lib/draft-event'
|
||||
import { useNostr } from '@/providers/NostrProvider'
|
||||
import { Loader } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function LightningAddressInput() {
|
||||
const { t } = useTranslation()
|
||||
const { toast } = useToast()
|
||||
const { profile, profileEvent, publish, updateProfileEvent } = useNostr()
|
||||
const [lightningAddress, setLightningAddress] = useState('')
|
||||
const [hasChanged, setHasChanged] = useState(false)
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (profile) {
|
||||
setLightningAddress(profile.lightningAddress || '')
|
||||
}
|
||||
}, [profile])
|
||||
|
||||
if (!profile || !profileEvent) {
|
||||
return null
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true)
|
||||
let lud06 = profile.lud06
|
||||
let lud16 = profile.lud16
|
||||
if (lightningAddress.startsWith('lnurl')) {
|
||||
lud06 = lightningAddress
|
||||
} else if (isEmail(lightningAddress)) {
|
||||
lud16 = lightningAddress
|
||||
} else {
|
||||
toast({
|
||||
title: 'Invalid Lightning Address',
|
||||
description: 'Please enter a valid Lightning Address or LNURL',
|
||||
variant: 'destructive'
|
||||
})
|
||||
setSaving(false)
|
||||
return
|
||||
}
|
||||
|
||||
const oldProfileContent = profileEvent ? JSON.parse(profileEvent.content) : {}
|
||||
const newProfileContent = {
|
||||
...oldProfileContent,
|
||||
lud06,
|
||||
lud16
|
||||
}
|
||||
const profileDraftEvent = createProfileDraftEvent(
|
||||
JSON.stringify(newProfileContent),
|
||||
profileEvent?.tags
|
||||
)
|
||||
const newProfileEvent = await publish(profileDraftEvent)
|
||||
await updateProfileEvent(newProfileEvent)
|
||||
setSaving(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full space-y-1">
|
||||
<Label htmlFor="ln-address">{t('Lightning Address (or LNURL)')}</Label>
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<Input
|
||||
id="ln-address"
|
||||
placeholder="xxxxxxxx@xxx.xxx"
|
||||
value={lightningAddress}
|
||||
onChange={(e) => {
|
||||
setLightningAddress(e.target.value)
|
||||
setHasChanged(true)
|
||||
}}
|
||||
/>
|
||||
<Button onClick={handleSave} disabled={saving || !hasChanged} className="w-20">
|
||||
{saving ? <Loader className="animate-spin" /> : 'Save'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
21
src/pages/secondary/WalletPage/QuickZapSwitch.tsx
Normal file
21
src/pages/secondary/WalletPage/QuickZapSwitch.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { useZap } from '@/providers/ZapProvider'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function QuickZapSwitch() {
|
||||
const { t } = useTranslation()
|
||||
const { quickZap, updateQuickZap } = useZap()
|
||||
|
||||
return (
|
||||
<div className="w-full flex justify-between items-center">
|
||||
<Label htmlFor="quick-zap-switch">
|
||||
<div className="text-base font-medium">{t('Quick zap')}</div>
|
||||
<div className="text-muted-foreground text-sm">
|
||||
{t('If enabled, you can zap with a single click')}
|
||||
</div>
|
||||
</Label>
|
||||
<Switch id="quick-zap-switch" checked={quickZap} onCheckedChange={updateQuickZap} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
26
src/pages/secondary/WalletPage/index.tsx
Normal file
26
src/pages/secondary/WalletPage/index.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
|
||||
import { Button as BcButton } from '@getalby/bitcoin-connect-react'
|
||||
import { forwardRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import DefaultZapAmountInput from './DefaultZapAmountInput'
|
||||
import DefaultZapCommentInput from './DefaultZapCommentInput'
|
||||
import LightningAddressInput from './LightningAddressInput'
|
||||
import QuickZapSwitch from './QuickZapSwitch'
|
||||
|
||||
const WalletPage = forwardRef(({ index }: { index?: number }, ref) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<SecondaryPageLayout ref={ref} index={index} title={t('Wallet')}>
|
||||
<div className="px-4 pt-2 space-y-4">
|
||||
<BcButton />
|
||||
<LightningAddressInput />
|
||||
<DefaultZapAmountInput />
|
||||
<DefaultZapCommentInput />
|
||||
<QuickZapSwitch />
|
||||
</div>
|
||||
</SecondaryPageLayout>
|
||||
)
|
||||
})
|
||||
WalletPage.displayName = 'WalletPage'
|
||||
export default WalletPage
|
||||
Reference in New Issue
Block a user