30 changed files with 254 additions and 2194 deletions
@ -1,95 +0,0 @@ |
|||||||
import { useSecondaryPage, useSmartRelayNavigation } from '@/PageManager' |
|
||||||
import RelaySimpleInfo from '@/components/RelaySimpleInfo' |
|
||||||
import { Button } from '@/components/ui/button' |
|
||||||
import { RECOMMENDED_RELAYS } from '@/constants' |
|
||||||
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' |
|
||||||
import { toRelay } from '@/lib/link' |
|
||||||
import relayInfoService from '@/services/relay-info.service' |
|
||||||
import { TRelayInfo } from '@/types' |
|
||||||
import { ArrowRight, Server } from 'lucide-react' |
|
||||||
import { forwardRef, useEffect, useState } from 'react' |
|
||||||
import { useTranslation } from 'react-i18next' |
|
||||||
import logger from '@/lib/logger' |
|
||||||
|
|
||||||
const HomePage = forwardRef(({ index }: { index?: number }, ref) => { |
|
||||||
const { t } = useTranslation() |
|
||||||
const { navigateToPrimaryPage } = useSecondaryPage() |
|
||||||
const { navigateToRelay } = useSmartRelayNavigation() |
|
||||||
// DEPRECATED: updateShowRecommendedRelaysPanel removed - double-panel functionality disabled
|
|
||||||
const [recommendedRelayInfos, setRecommendedRelayInfos] = useState<TRelayInfo[]>([]) |
|
||||||
|
|
||||||
useEffect(() => { |
|
||||||
const init = async () => { |
|
||||||
try { |
|
||||||
const relays = await relayInfoService.getRelayInfos(RECOMMENDED_RELAYS) |
|
||||||
setRecommendedRelayInfos(relays.filter(Boolean) as TRelayInfo[]) |
|
||||||
} catch (error) { |
|
||||||
logger.error('Failed to fetch recommended relays', { error }) |
|
||||||
} |
|
||||||
} |
|
||||||
init() |
|
||||||
}, []) |
|
||||||
|
|
||||||
if (!recommendedRelayInfos.length) { |
|
||||||
return ( |
|
||||||
<SecondaryPageLayout ref={ref} index={index} hideBackButton hideTitlebarBottomBorder> |
|
||||||
<div className="text-muted-foreground w-full h-screen flex items-center justify-center"> |
|
||||||
{t('Welcome! 🥳')} |
|
||||||
</div> |
|
||||||
</SecondaryPageLayout> |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
return ( |
|
||||||
<SecondaryPageLayout |
|
||||||
ref={ref} |
|
||||||
index={index} |
|
||||||
title={ |
|
||||||
<div className="flex items-center gap-2"> |
|
||||||
<Server /> |
|
||||||
<div>{t('Recommended relays')}</div> |
|
||||||
</div> |
|
||||||
} |
|
||||||
controls={ |
|
||||||
// DEPRECATED: Close button removed - double-panel functionality disabled
|
|
||||||
null |
|
||||||
} |
|
||||||
hideBackButton |
|
||||||
hideTitlebarBottomBorder |
|
||||||
> |
|
||||||
<div className="px-4 pt-2"> |
|
||||||
<div className="grid grid-cols-2 gap-3"> |
|
||||||
{recommendedRelayInfos.map((relayInfo) => ( |
|
||||||
<RelaySimpleInfo |
|
||||||
key={relayInfo.url} |
|
||||||
className="clickable h-auto px-4 py-3 rounded-lg border" |
|
||||||
relayInfo={relayInfo} |
|
||||||
onClick={(e) => { |
|
||||||
e.stopPropagation() |
|
||||||
navigateToRelay(toRelay(relayInfo.url)) |
|
||||||
}} |
|
||||||
/> |
|
||||||
))} |
|
||||||
</div> |
|
||||||
<div className="flex mt-2 justify-center"> |
|
||||||
<Button |
|
||||||
variant="ghost" |
|
||||||
onClick={() => { |
|
||||||
navigateToPrimaryPage('home') |
|
||||||
requestAnimationFrame(() => { |
|
||||||
window.dispatchEvent( |
|
||||||
new CustomEvent('restorePageTab', { detail: { page: 'home', tab: 'explore' } }) |
|
||||||
) |
|
||||||
}) |
|
||||||
}} |
|
||||||
> |
|
||||||
<div>{t('Explore more')}</div> |
|
||||||
<ArrowRight /> |
|
||||||
</Button> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</SecondaryPageLayout> |
|
||||||
) |
|
||||||
}) |
|
||||||
HomePage.displayName = 'HomePage' |
|
||||||
export default HomePage |
|
||||||
@ -1,203 +0,0 @@ |
|||||||
import { Button } from '@/components/ui/button' |
|
||||||
import { Input } from '@/components/ui/input' |
|
||||||
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' |
|
||||||
import { createProfileDraftEvent } from '@/lib/draft-event' |
|
||||||
import { isEmail } from '@/lib/utils' |
|
||||||
import { useNostr } from '@/providers/NostrProvider' |
|
||||||
import { useZap } from '@/providers/ZapProvider' |
|
||||||
import { connectNWC, WebLNProviders } from '@getalby/bitcoin-connect' |
|
||||||
import { Check, CheckCircle2, Copy, ExternalLink, Loader2 } from 'lucide-react' |
|
||||||
import { forwardRef, useEffect, useState } from 'react' |
|
||||||
import { useTranslation } from 'react-i18next' |
|
||||||
import { toast } from 'sonner' |
|
||||||
|
|
||||||
const RIZFUL_URL = 'https://rizful.com' |
|
||||||
const RIZFUL_SIGNUP_URL = `${RIZFUL_URL}/create-account` |
|
||||||
const RIZFUL_GET_TOKEN_URL = `${RIZFUL_URL}/nostr_onboarding_auth_token/get_token` |
|
||||||
const RIZFUL_TOKEN_EXCHANGE_URL = `${RIZFUL_URL}/nostr_onboarding_auth_token/post_for_secrets` |
|
||||||
|
|
||||||
const RizfulPage = forwardRef(({ index }: { index?: number }, ref) => { |
|
||||||
const { t } = useTranslation() |
|
||||||
const { pubkey, profile, profileEvent, publish, updateProfileEvent } = useNostr() |
|
||||||
const { provider } = useZap() |
|
||||||
const [token, setToken] = useState('') |
|
||||||
const [connecting, setConnecting] = useState(false) |
|
||||||
const [connected, setConnected] = useState(false) |
|
||||||
const [copiedLightningAddress, setCopiedLightningAddress] = useState(false) |
|
||||||
const [lightningAddress, setLightningAddress] = useState('') |
|
||||||
|
|
||||||
useEffect(() => { |
|
||||||
if (provider instanceof WebLNProviders.NostrWebLNProvider) { |
|
||||||
const lud16 = provider.client.lud16 |
|
||||||
const domain = lud16?.split('@')[1] |
|
||||||
if (domain !== 'rizful.com') return |
|
||||||
|
|
||||||
if (lud16) { |
|
||||||
setConnected(true) |
|
||||||
setLightningAddress(lud16) |
|
||||||
} |
|
||||||
} |
|
||||||
}, [provider]) |
|
||||||
|
|
||||||
const updateUserProfile = async (address: string) => { |
|
||||||
try { |
|
||||||
if (address === profile?.lightningAddress) { |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
const profileContent = profileEvent ? JSON.parse(profileEvent.content) : {} |
|
||||||
if (isEmail(address)) { |
|
||||||
profileContent.lud16 = address |
|
||||||
} else if (address.startsWith('lnurl')) { |
|
||||||
profileContent.lud06 = address |
|
||||||
} else { |
|
||||||
throw new Error(t('Invalid Lightning Address')) |
|
||||||
} |
|
||||||
|
|
||||||
if (!profileContent.nip05) { |
|
||||||
profileContent.nip05 = address |
|
||||||
} |
|
||||||
|
|
||||||
const profileDraftEvent = createProfileDraftEvent( |
|
||||||
JSON.stringify(profileContent), |
|
||||||
profileEvent?.tags |
|
||||||
) |
|
||||||
const newProfileEvent = await publish(profileDraftEvent) |
|
||||||
await updateProfileEvent(newProfileEvent) |
|
||||||
} catch (e: unknown) { |
|
||||||
toast.error(e instanceof Error ? e.message : String(e)) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
const connectRizful = async () => { |
|
||||||
setConnecting(true) |
|
||||||
try { |
|
||||||
const r = await fetch(RIZFUL_TOKEN_EXCHANGE_URL, { |
|
||||||
method: 'POST', |
|
||||||
headers: { 'Content-Type': 'application/json' }, |
|
||||||
credentials: 'omit', |
|
||||||
body: JSON.stringify({ |
|
||||||
secret_code: token.trim(), |
|
||||||
nostr_public_key: pubkey |
|
||||||
}) |
|
||||||
}) |
|
||||||
|
|
||||||
if (!r.ok) { |
|
||||||
const errorText = await r.text() |
|
||||||
throw new Error(errorText || 'Exchange failed') |
|
||||||
} |
|
||||||
|
|
||||||
const j = (await r.json()) as { |
|
||||||
nwc_uri?: string |
|
||||||
lightning_address?: string |
|
||||||
} |
|
||||||
|
|
||||||
if (j.nwc_uri) { |
|
||||||
connectNWC(j.nwc_uri) |
|
||||||
} |
|
||||||
if (j.lightning_address) { |
|
||||||
updateUserProfile(j.lightning_address) |
|
||||||
} |
|
||||||
} catch (e: unknown) { |
|
||||||
toast.error(e instanceof Error ? e.message : String(e)) |
|
||||||
} finally { |
|
||||||
setConnecting(false) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if (connected) { |
|
||||||
return ( |
|
||||||
<SecondaryPageLayout ref={ref} index={index} title={t('Rizful Vault')}> |
|
||||||
<div className="px-4 pt-3 space-y-6 flex flex-col items-center"> |
|
||||||
<CheckCircle2 className="size-40 fill-green-400 text-background" /> |
|
||||||
<div className="font-semibold text-2xl">{t('Rizful Vault connected!')}</div> |
|
||||||
<div className="text-center text-sm text-muted-foreground"> |
|
||||||
{t('You can now use your Rizful Vault to zap your favorite notes and creators.')} |
|
||||||
</div> |
|
||||||
{lightningAddress && ( |
|
||||||
<div className="flex flex-col items-center gap-2"> |
|
||||||
<div>{t('Your Lightning Address')}:</div> |
|
||||||
<div |
|
||||||
className="font-semibold text-lg rounded-lg px-4 py-1 flex justify-center items-center gap-2 cursor-pointer hover:bg-accent/80" |
|
||||||
onClick={() => { |
|
||||||
navigator.clipboard.writeText(lightningAddress) |
|
||||||
setCopiedLightningAddress(true) |
|
||||||
setTimeout(() => setCopiedLightningAddress(false), 2000) |
|
||||||
}} |
|
||||||
> |
|
||||||
{lightningAddress}{' '} |
|
||||||
{copiedLightningAddress ? ( |
|
||||||
<Check className="size-4" /> |
|
||||||
) : ( |
|
||||||
<Copy className="size-4" /> |
|
||||||
)} |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
)} |
|
||||||
</div> |
|
||||||
</SecondaryPageLayout> |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
return ( |
|
||||||
<SecondaryPageLayout ref={ref} index={index} title={t('Rizful Vault')}> |
|
||||||
<div className="px-4 pt-3 space-y-6"> |
|
||||||
<div className="space-y-2"> |
|
||||||
<div className="font-semibold">1. {t('New to Rizful?')}</div> |
|
||||||
<Button |
|
||||||
className="bg-lime-500 hover:bg-lime-500/90 w-64" |
|
||||||
onClick={() => window.open(RIZFUL_SIGNUP_URL, '_blank')} |
|
||||||
> |
|
||||||
{t('Sign up for Rizful')} <ExternalLink /> |
|
||||||
</Button> |
|
||||||
<div className="text-sm text-muted-foreground"> |
|
||||||
{t('If you already have a Rizful account, you can skip this step.')} |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div className="space-y-2"> |
|
||||||
<div className="font-semibold">2. {t('Get your one-time code')}</div> |
|
||||||
<Button |
|
||||||
className="bg-orange-500 hover:bg-orange-500/90 w-64" |
|
||||||
onClick={() => openPopup(RIZFUL_GET_TOKEN_URL, 'rizful_codes')} |
|
||||||
> |
|
||||||
{t('Get code')} |
|
||||||
<ExternalLink /> |
|
||||||
</Button> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div className="space-y-2"> |
|
||||||
<div className="font-semibold">3. {t('Connect to your Rizful Vault')}</div> |
|
||||||
<Input |
|
||||||
placeholder={t('Paste your one-time code here')} |
|
||||||
value={token} |
|
||||||
onChange={(e) => { |
|
||||||
setToken(e.target.value.trim()) |
|
||||||
}} |
|
||||||
/> |
|
||||||
<Button |
|
||||||
className="bg-sky-500 hover:bg-sky-500/90 w-64" |
|
||||||
disabled={!token || connecting} |
|
||||||
onClick={() => connectRizful()} |
|
||||||
> |
|
||||||
{connecting && <Loader2 className="animate-spin" />} |
|
||||||
{t('Connect')} |
|
||||||
</Button> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</SecondaryPageLayout> |
|
||||||
) |
|
||||||
}) |
|
||||||
RizfulPage.displayName = 'RizfulPage' |
|
||||||
export default RizfulPage |
|
||||||
|
|
||||||
function openPopup(url: string, name: string, width = 520, height = 700) { |
|
||||||
const left = Math.max((window.screenX || 0) + (window.innerWidth - width) / 2, 0) |
|
||||||
const top = Math.max((window.screenY || 0) + (window.innerHeight - height) / 2, 0) |
|
||||||
|
|
||||||
return window.open( |
|
||||||
url, |
|
||||||
name, |
|
||||||
`width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes,menubar=no,toolbar=no,location=no,status=no` |
|
||||||
) |
|
||||||
} |
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,3 @@ |
|||||||
|
@import './katex-fonts-subset.css'; |
||||||
|
@import './katex-core.min.css'; |
||||||
|
@import './katex-overrides.css'; |
||||||
File diff suppressed because one or more lines are too long
@ -0,0 +1,73 @@ |
|||||||
|
/* Subset of KaTeX fonts: Main + Math + Size1–4 (woff2 only). Drops AMS, Caligraphic, |
||||||
|
Fraktur, SansSerif, Script, Typewriter to shrink the build / PWA precache. */ |
||||||
|
|
||||||
|
@font-face { |
||||||
|
font-family: KaTeX_Main; |
||||||
|
font-style: normal; |
||||||
|
font-weight: 700; |
||||||
|
font-display: swap; |
||||||
|
src: url('../../node_modules/katex/dist/fonts/KaTeX_Main-Bold.woff2') format('woff2'); |
||||||
|
} |
||||||
|
@font-face { |
||||||
|
font-family: KaTeX_Main; |
||||||
|
font-style: italic; |
||||||
|
font-weight: 700; |
||||||
|
font-display: swap; |
||||||
|
src: url('../../node_modules/katex/dist/fonts/KaTeX_Main-BoldItalic.woff2') format('woff2'); |
||||||
|
} |
||||||
|
@font-face { |
||||||
|
font-family: KaTeX_Main; |
||||||
|
font-style: italic; |
||||||
|
font-weight: 400; |
||||||
|
font-display: swap; |
||||||
|
src: url('../../node_modules/katex/dist/fonts/KaTeX_Main-Italic.woff2') format('woff2'); |
||||||
|
} |
||||||
|
@font-face { |
||||||
|
font-family: KaTeX_Main; |
||||||
|
font-style: normal; |
||||||
|
font-weight: 400; |
||||||
|
font-display: swap; |
||||||
|
src: url('../../node_modules/katex/dist/fonts/KaTeX_Main-Regular.woff2') format('woff2'); |
||||||
|
} |
||||||
|
@font-face { |
||||||
|
font-family: KaTeX_Math; |
||||||
|
font-style: italic; |
||||||
|
font-weight: 700; |
||||||
|
font-display: swap; |
||||||
|
src: url('../../node_modules/katex/dist/fonts/KaTeX_Math-BoldItalic.woff2') format('woff2'); |
||||||
|
} |
||||||
|
@font-face { |
||||||
|
font-family: KaTeX_Math; |
||||||
|
font-style: italic; |
||||||
|
font-weight: 400; |
||||||
|
font-display: swap; |
||||||
|
src: url('../../node_modules/katex/dist/fonts/KaTeX_Math-Italic.woff2') format('woff2'); |
||||||
|
} |
||||||
|
@font-face { |
||||||
|
font-family: KaTeX_Size1; |
||||||
|
font-style: normal; |
||||||
|
font-weight: 400; |
||||||
|
font-display: swap; |
||||||
|
src: url('../../node_modules/katex/dist/fonts/KaTeX_Size1-Regular.woff2') format('woff2'); |
||||||
|
} |
||||||
|
@font-face { |
||||||
|
font-family: KaTeX_Size2; |
||||||
|
font-style: normal; |
||||||
|
font-weight: 400; |
||||||
|
font-display: swap; |
||||||
|
src: url('../../node_modules/katex/dist/fonts/KaTeX_Size2-Regular.woff2') format('woff2'); |
||||||
|
} |
||||||
|
@font-face { |
||||||
|
font-family: KaTeX_Size3; |
||||||
|
font-style: normal; |
||||||
|
font-weight: 400; |
||||||
|
font-display: swap; |
||||||
|
src: url('../../node_modules/katex/dist/fonts/KaTeX_Size3-Regular.woff2') format('woff2'); |
||||||
|
} |
||||||
|
@font-face { |
||||||
|
font-family: KaTeX_Size4; |
||||||
|
font-style: normal; |
||||||
|
font-weight: 400; |
||||||
|
font-display: swap; |
||||||
|
src: url('../../node_modules/katex/dist/fonts/KaTeX_Size4-Regular.woff2') format('woff2'); |
||||||
|
} |
||||||
@ -0,0 +1,41 @@ |
|||||||
|
/* Map styles that referenced bundled-out fonts onto KaTeX_Main so layout still works; |
||||||
|
\mathbb, \mathcal, \mathfrak, etc. will not match canonical TeX glyphs. */ |
||||||
|
|
||||||
|
.katex .texttt, |
||||||
|
.katex .mathtt { |
||||||
|
font-family: KaTeX_Main, ui-monospace, monospace; |
||||||
|
} |
||||||
|
.katex .textsf, |
||||||
|
.katex .mathsf, |
||||||
|
.katex .mathboldsf, |
||||||
|
.katex .textboldsf, |
||||||
|
.katex .mathitsf, |
||||||
|
.katex .mathsfit, |
||||||
|
.katex .textitsf { |
||||||
|
font-family: KaTeX_Main, sans-serif; |
||||||
|
} |
||||||
|
.katex .mathboldsf, |
||||||
|
.katex .textboldsf { |
||||||
|
font-weight: 700; |
||||||
|
} |
||||||
|
.katex .mathitsf, |
||||||
|
.katex .mathsfit, |
||||||
|
.katex .textitsf { |
||||||
|
font-style: italic; |
||||||
|
} |
||||||
|
.katex .amsrm, |
||||||
|
.katex .mathbb, |
||||||
|
.katex .textbb, |
||||||
|
.katex .mathcal, |
||||||
|
.katex .mathfrak, |
||||||
|
.katex .textfrak, |
||||||
|
.katex .mathboldfrak, |
||||||
|
.katex .textboldfrak, |
||||||
|
.katex .mathscr, |
||||||
|
.katex .textscr { |
||||||
|
font-family: KaTeX_Main, Times New Roman, serif; |
||||||
|
} |
||||||
|
.katex .mathboldfrak, |
||||||
|
.katex .textboldfrak { |
||||||
|
font-weight: 700; |
||||||
|
} |
||||||
Loading…
Reference in new issue