169
README.md
169
README.md
@@ -1,2 +1,167 @@
|
||||
# Noderunners-relay-front
|
||||
The noderunners Nostr relay front-end
|
||||
# Noderunners Relay
|
||||
|
||||
A high-performance Nostr relay built by Bitcoiners, for Bitcoiners. This project provides a web interface for managing access to the Noderunners relay service.
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
- 🚀 Lightning-fast relay performance with strfry v1.0.3
|
||||
- ⚡ Lightning Network integration for payments
|
||||
- 🔒 Secure authentication with Nostr
|
||||
- 💻 Modern, responsive web interface
|
||||
- 📊 Real-time relay statistics
|
||||
- 🔍 Uptime monitoring
|
||||
- 🖼️ Iframe support for embedding
|
||||
- 🔑 Multiple login methods (Extension, Manual, URL-based)
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Frontend**: React + TypeScript + Vite
|
||||
- **Styling**: Tailwind CSS
|
||||
- **Icons**: Lucide React
|
||||
- **Payment**: LNbits Integration
|
||||
- **Authentication**: Nostr Protocol
|
||||
- **State Management**: Zustand
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Create a `.env` file in the root directory with the following variables:
|
||||
|
||||
```env
|
||||
# LNbits Configuration
|
||||
VITE_LNBITS_URL="your-lnbits-url"
|
||||
VITE_LNBITS_API_KEY="your-api-key"
|
||||
|
||||
# App Settings
|
||||
VITE_APP_NAME="Noderunners Relay"
|
||||
VITE_APP_DESCRIPTION="A high-performance Nostr relay built by Bitcoiners, for Bitcoiners"
|
||||
VITE_LOGO_URL="your-logo-url"
|
||||
VITE_GITHUB_URL="your-github-url"
|
||||
|
||||
# Nostr Settings
|
||||
VITE_NOSTR_RELAY_URL="wss://your-relay-url"
|
||||
VITE_API_URL="your-api-url"
|
||||
VITE_SUPPORTED_NIPS="1,2,4,9,11,22,28,40,70,77"
|
||||
VITE_RELAY_SOFTWARE="strfry v1.0.3"
|
||||
|
||||
# Payment Settings
|
||||
VITE_MIN_PAYMENT_AMOUNT=10000
|
||||
VITE_PAYMENT_MEMO="Noderunners Relay Access"
|
||||
VITE_PAYMENT_CURRENCY="sat"
|
||||
VITE_WEBHOOK_URL="your-webhook-url"
|
||||
|
||||
# Feature Flags
|
||||
VITE_ENABLE_WHITELIST=true
|
||||
VITE_ENABLE_PAYMENT_VERIFICATION=true
|
||||
VITE_ENABLE_DEMO=false
|
||||
|
||||
# Uptime Monitoring
|
||||
VITE_UPTIME_KUMA_URL="your-uptime-kuma-url"
|
||||
VITE_UPTIME_KUMA_ID="1"
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
1. Install dependencies:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
2. Start the development server:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
3. Build for production:
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Authentication Methods
|
||||
|
||||
The application supports multiple authentication methods:
|
||||
|
||||
1. **Nostr Extension**
|
||||
- Uses browser extensions like Alby for seamless authentication
|
||||
- Automatically retrieves the user's public key
|
||||
|
||||
2. **Manual Entry**
|
||||
- Users can manually input their npub or hex public key
|
||||
- Supports both formats for maximum flexibility
|
||||
|
||||
3. **URL-based Authentication**
|
||||
- Automatically logs in using URL parameters
|
||||
- Supports both `npub` and `pubkey` parameters
|
||||
- Example URLs:
|
||||
```
|
||||
https://your-domain.com?npub=npub1...
|
||||
https://your-domain.com?pubkey=abc123...
|
||||
```
|
||||
|
||||
## Iframe Integration
|
||||
|
||||
The application supports iframe embedding with a clean interface. Add `?iframe=1` to the URL to:
|
||||
- Hide header and footer
|
||||
- Show a logout button on the dashboard
|
||||
- Maintain a minimal interface
|
||||
|
||||
Example:
|
||||
```html
|
||||
<iframe src="https://your-relay-domain.com?iframe=1" width="100%" height="600px"></iframe>
|
||||
```
|
||||
|
||||
You can combine iframe mode with URL-based authentication:
|
||||
```html
|
||||
<iframe src="https://your-relay-domain.com?iframe=1&npub=npub1..." width="100%" height="600px"></iframe>
|
||||
```
|
||||
|
||||
## Supported NIPs
|
||||
|
||||
The relay supports the following Nostr Implementation Possibilities (NIPs):
|
||||
- NIP-01: Basic protocol flow description
|
||||
- NIP-02: Contact List and Petnames
|
||||
- NIP-04: Encrypted Direct Messages
|
||||
- NIP-09: Event Deletion
|
||||
- NIP-11: Relay Information Document
|
||||
- NIP-22: Event `created_at` Limits
|
||||
- NIP-28: Public Chat
|
||||
- NIP-40: Expiration Timestamp
|
||||
- NIP-70: Relay Payment Info
|
||||
- NIP-77: Lightning Network Relay Payment
|
||||
|
||||
## API Services
|
||||
|
||||
### LNbits Integration
|
||||
- Invoice creation
|
||||
- Payment verification
|
||||
- Exchange rate conversion
|
||||
- Wallet information
|
||||
|
||||
### Relay API
|
||||
- User information
|
||||
- Whitelist management
|
||||
- Payment processing
|
||||
- Status monitoring
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
||||
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
||||
4. Push to the branch (`git push origin feature/amazing-feature`)
|
||||
5. Open a Pull Request
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
## Support
|
||||
|
||||
For support, join our [Telegram group](https://t.me/noderunners) or visit [our website](https://noderunners.network).
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
- Built by the Noderunners community
|
||||
- Powered by [strfry](https://github.com/hoytech/strfry)
|
||||
- Lightning Network integration via [LNbits](https://lnbits.com)
|
||||
47
src/App.tsx
47
src/App.tsx
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
||||
import React, { useEffect } from 'react';
|
||||
import { BrowserRouter as Router, Routes, Route, useSearchParams, useNavigate } from 'react-router-dom';
|
||||
import { Home } from './pages/Home';
|
||||
import { Login } from './pages/Login';
|
||||
import { Dashboard } from './pages/Dashboard';
|
||||
@@ -7,20 +7,43 @@ import { Payment } from './pages/Payment';
|
||||
import { ThankYou } from './pages/ThankYou';
|
||||
import { Terms } from './pages/Terms';
|
||||
import { Layout } from './components/Layout';
|
||||
import { useStore } from './store/useStore';
|
||||
|
||||
// Wrapper component to handle auto-login logic
|
||||
function AutoLoginHandler({ children }: { children: React.ReactNode }) {
|
||||
const [searchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
const { user, setUser } = useStore();
|
||||
|
||||
useEffect(() => {
|
||||
const urlPubkey = searchParams.get('npub') || searchParams.get('pubkey');
|
||||
const isIframe = searchParams.get('iframe') === '1';
|
||||
|
||||
// Auto-login if pubkey/npub is in URL and user isn't already logged in
|
||||
if (urlPubkey && !user) {
|
||||
setUser({ pubkey: urlPubkey, isWhitelisted: false });
|
||||
navigate(isIframe ? '/dashboard?iframe=1' : '/dashboard');
|
||||
}
|
||||
}, [searchParams, user, setUser, navigate]);
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/" element={<Layout />}>
|
||||
<Route index element={<Home />} />
|
||||
<Route path="login" element={<Login />} />
|
||||
<Route path="dashboard" element={<Dashboard />} />
|
||||
<Route path="payment" element={<Payment />} />
|
||||
<Route path="thank-you" element={<ThankYou />} />
|
||||
<Route path="terms" element={<Terms />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
<AutoLoginHandler>
|
||||
<Routes>
|
||||
<Route path="/" element={<Layout />}>
|
||||
<Route index element={<Home />} />
|
||||
<Route path="login" element={<Login />} />
|
||||
<Route path="dashboard" element={<Dashboard />} />
|
||||
<Route path="payment" element={<Payment />} />
|
||||
<Route path="thank-you" element={<ThankYou />} />
|
||||
<Route path="terms" element={<Terms />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</AutoLoginHandler>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,21 @@
|
||||
import React from 'react';
|
||||
import { Link, Outlet } from 'react-router-dom';
|
||||
import { Link, Outlet, useSearchParams } from 'react-router-dom';
|
||||
import { Navigation } from './Navigation';
|
||||
|
||||
export function Layout() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const isIframe = searchParams.get('iframe') === '1';
|
||||
|
||||
if (isIframe) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white">
|
||||
<main className="container mx-auto px-4 py-4 md:py-8">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white">
|
||||
<Navigation />
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { Link, useLocation, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { Flame, Menu, X } from 'lucide-react';
|
||||
import { useStore } from '../store/useStore';
|
||||
|
||||
export function Navigation() {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { user, setUser } = useStore();
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const isIframe = searchParams.get('iframe') === '1';
|
||||
|
||||
const handleLogout = () => {
|
||||
setUser(null);
|
||||
setIsMenuOpen(false);
|
||||
navigate('/login', { replace: true }); // Use replace to prevent going back to dashboard
|
||||
const newPath = '/login' + (isIframe ? '?iframe=1' : '');
|
||||
navigate(newPath, { replace: true });
|
||||
};
|
||||
|
||||
const handleMenuClick = () => {
|
||||
@@ -23,11 +26,16 @@ export function Navigation() {
|
||||
setIsMenuOpen(false);
|
||||
};
|
||||
|
||||
// Helper function to add iframe parameter to paths
|
||||
const getPath = (path: string) => {
|
||||
return isIframe ? `${path}?iframe=1` : path;
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="border-b border-gray-800 bg-gray-900 sticky top-0 z-50">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="flex items-center justify-between h-16">
|
||||
<Link to="/" className="flex items-center space-x-2" onClick={handleNavigation}>
|
||||
<Link to={getPath('/')} className="flex items-center space-x-2" onClick={handleNavigation}>
|
||||
{import.meta.env.VITE_LOGO_URL ? (
|
||||
<img
|
||||
src={import.meta.env.VITE_LOGO_URL}
|
||||
@@ -47,7 +55,7 @@ export function Navigation() {
|
||||
{user ? (
|
||||
<>
|
||||
<Link
|
||||
to="/dashboard"
|
||||
to={getPath('/dashboard')}
|
||||
className="px-4 py-2 rounded-lg hover:bg-gray-800 transition-colors"
|
||||
>
|
||||
Dashboard
|
||||
@@ -61,7 +69,7 @@ export function Navigation() {
|
||||
</>
|
||||
) : (
|
||||
<Link
|
||||
to="/login"
|
||||
to={getPath('/login')}
|
||||
className="px-4 py-2 bg-orange-500 rounded-lg hover:bg-orange-600 transition-colors"
|
||||
>
|
||||
Connect Nostr
|
||||
@@ -89,7 +97,7 @@ export function Navigation() {
|
||||
{user ? (
|
||||
<>
|
||||
<Link
|
||||
to="/dashboard"
|
||||
to={getPath('/dashboard')}
|
||||
onClick={handleNavigation}
|
||||
className="px-4 py-2 rounded-lg hover:bg-gray-800 transition-colors"
|
||||
>
|
||||
@@ -104,7 +112,7 @@ export function Navigation() {
|
||||
</>
|
||||
) : (
|
||||
<Link
|
||||
to="/login"
|
||||
to={getPath('/login')}
|
||||
onClick={handleNavigation}
|
||||
className="px-4 py-2 bg-orange-500 rounded-lg hover:bg-orange-600 transition-colors text-center"
|
||||
>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Shield, AlertTriangle, Zap, Copy, Activity, Users } from 'lucide-react';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { Shield, AlertTriangle, Zap, Copy, Activity, Users, LogOut } from 'lucide-react';
|
||||
import { useStore } from '../store/useStore';
|
||||
import { apiService } from '../services/api';
|
||||
import { Notification } from '../components/Notification';
|
||||
@@ -15,6 +15,8 @@ export function Dashboard() {
|
||||
const isDemoMode = import.meta.env.VITE_ENABLE_DEMO === 'true';
|
||||
const apiUrl = import.meta.env.VITE_API_URL;
|
||||
const { isVisible, message, type, showNotification, hideNotification } = useNotification();
|
||||
const [searchParams] = useSearchParams();
|
||||
const isIframe = searchParams.get('iframe') === '1';
|
||||
|
||||
// Check user authentication and fetch status once
|
||||
useEffect(() => {
|
||||
@@ -34,7 +36,6 @@ export function Dashboard() {
|
||||
npub: userInfo.npub,
|
||||
});
|
||||
} catch (error: any) {
|
||||
// If user is not found (404) or any other error, assume not whitelisted
|
||||
if (error.response?.status === 404 || error) {
|
||||
setUser({
|
||||
...user,
|
||||
@@ -48,7 +49,7 @@ export function Dashboard() {
|
||||
};
|
||||
|
||||
checkUserStatus();
|
||||
}, [user?.pubkey, navigate, setUser, isDemoMode]); // Only run on mount and when these dependencies change
|
||||
}, [user?.pubkey, navigate, setUser, isDemoMode]);
|
||||
|
||||
// Fetch uptime and active users
|
||||
useEffect(() => {
|
||||
@@ -109,6 +110,11 @@ export function Dashboard() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
setUser(null);
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
if (loading || !user) {
|
||||
return (
|
||||
<div className="flex justify-center items-center min-h-[400px]">
|
||||
@@ -223,6 +229,19 @@ export function Dashboard() {
|
||||
<p className="text-gray-400">Active Users</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Logout Button (only shown in iframe mode) */}
|
||||
{isIframe && (
|
||||
<div className="flex justify-center">
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="flex items-center space-x-2 px-6 py-3 bg-gray-800 hover:bg-gray-700 rounded-lg transition-colors"
|
||||
>
|
||||
<LogOut className="h-5 w-5" />
|
||||
<span>Logout</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Notification
|
||||
isVisible={isVisible}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Link, useSearchParams } from 'react-router-dom';
|
||||
import { Flame, Zap, Shield, Globe, Server, Code, Cpu, Copy } from 'lucide-react';
|
||||
import { Notification } from '../components/Notification';
|
||||
import { useNotification } from '../hooks/useNotification';
|
||||
|
||||
export function Home() {
|
||||
const { isVisible, message, type, showNotification, hideNotification } = useNotification();
|
||||
const [searchParams] = useSearchParams();
|
||||
const isIframe = searchParams.get('iframe') === '1';
|
||||
|
||||
const copyToClipboard = async (text: string) => {
|
||||
try {
|
||||
@@ -26,7 +28,7 @@ export function Home() {
|
||||
A high-performance Nostr relay built by Bitcoiners, for Bitcoiners
|
||||
</p>
|
||||
<Link
|
||||
to="/login"
|
||||
to={isIframe ? '/login?iframe=1' : '/login'}
|
||||
className="inline-block px-8 py-4 bg-orange-500 rounded-lg hover:bg-orange-600 transition-colors font-semibold text-lg"
|
||||
>
|
||||
Get Started
|
||||
|
||||
@@ -1,19 +1,30 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { Loader2, Zap } from 'lucide-react';
|
||||
import { useStore } from '../store/useStore';
|
||||
|
||||
export function Login() {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { user, setUser } = useStore();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [pubkeyInput, setPubkeyInput] = useState('');
|
||||
const isIframe = searchParams.get('iframe') === '1';
|
||||
const urlPubkey = searchParams.get('npub') || searchParams.get('pubkey');
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
navigate('/dashboard');
|
||||
// Handle URL-based login
|
||||
if (urlPubkey && !user) {
|
||||
setUser({ pubkey: urlPubkey, isWhitelisted: false });
|
||||
navigate(isIframe ? '/dashboard?iframe=1' : '/dashboard');
|
||||
return;
|
||||
}
|
||||
}, [user, navigate]);
|
||||
|
||||
// Regular user redirect
|
||||
if (user) {
|
||||
navigate(isIframe ? '/dashboard?iframe=1' : '/dashboard');
|
||||
}
|
||||
}, [user, navigate, isIframe, urlPubkey, setUser]);
|
||||
|
||||
const handleExtensionLogin = async () => {
|
||||
setIsLoading(true);
|
||||
@@ -33,9 +44,8 @@ export function Login() {
|
||||
throw new Error('No public key found');
|
||||
}
|
||||
|
||||
// Simply store the pubkey and navigate to dashboard
|
||||
setUser({ pubkey, isWhitelisted: false });
|
||||
navigate('/dashboard');
|
||||
navigate(isIframe ? '/dashboard?iframe=1' : '/dashboard');
|
||||
} catch (error: any) {
|
||||
if (error.message === 'Rejected by user') {
|
||||
return;
|
||||
@@ -62,9 +72,8 @@ export function Login() {
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// Simply store the pubkey and navigate to dashboard
|
||||
setUser({ pubkey: pubkeyInput.trim(), isWhitelisted: false });
|
||||
navigate('/dashboard');
|
||||
navigate(isIframe ? '/dashboard?iframe=1' : '/dashboard');
|
||||
} catch (error) {
|
||||
console.error('Login failed:', error);
|
||||
alert('Failed to login with the provided public key. Please check the format and try again.');
|
||||
@@ -73,6 +82,15 @@ export function Login() {
|
||||
}
|
||||
};
|
||||
|
||||
// If we're processing URL-based login, show loading state
|
||||
if (urlPubkey && !user) {
|
||||
return (
|
||||
<div className="flex justify-center items-center min-h-[400px]">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-orange-500"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-md mx-auto bg-gray-800 rounded-lg p-8">
|
||||
<h1 className="text-2xl font-bold mb-6 text-center">Connect with Nostr</h1>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import { Copy, CheckCircle } from 'lucide-react';
|
||||
import { useStore } from '../store/useStore';
|
||||
@@ -11,6 +11,7 @@ import { useNotification } from '../hooks/useNotification';
|
||||
|
||||
export function Payment() {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { user, setUser } = useStore();
|
||||
const [invoice, setInvoice] = useState<LightningInvoice | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -19,15 +20,16 @@ export function Payment() {
|
||||
const { isVisible, message, type, showNotification, hideNotification } = useNotification();
|
||||
const [checkingPayment, setCheckingPayment] = useState(false);
|
||||
const [showSuccess, setShowSuccess] = useState(false);
|
||||
const isIframe = searchParams.get('iframe') === '1';
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) {
|
||||
navigate('/login');
|
||||
navigate(isIframe ? '/login?iframe=1' : '/login');
|
||||
return;
|
||||
}
|
||||
|
||||
if (user.isWhitelisted) {
|
||||
navigate('/dashboard');
|
||||
navigate(isIframe ? '/dashboard?iframe=1' : '/dashboard');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -59,12 +61,12 @@ export function Payment() {
|
||||
};
|
||||
|
||||
generateInvoice();
|
||||
}, [user, navigate]);
|
||||
}, [user, navigate, isIframe]);
|
||||
|
||||
const handlePaymentSuccess = async () => {
|
||||
setShowSuccess(true);
|
||||
setTimeout(() => {
|
||||
navigate('/thank-you');
|
||||
navigate(isIframe ? '/thank-you?iframe=1' : '/thank-you');
|
||||
}, 1500);
|
||||
return true;
|
||||
};
|
||||
@@ -147,7 +149,6 @@ export function Payment() {
|
||||
return (
|
||||
<>
|
||||
<div className="max-w-md mx-auto bg-gray-800 rounded-lg p-8 relative">
|
||||
{/* Success Animation Overlay */}
|
||||
{showSuccess && (
|
||||
<div className="absolute inset-0 bg-gray-900/95 flex items-center justify-center rounded-lg animate-fade-in z-10">
|
||||
<div className="text-center animate-success-appear">
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { Shield } from 'lucide-react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
|
||||
export function Terms() {
|
||||
const [terms, setTerms] = useState<string>('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searchParams] = useSearchParams();
|
||||
const isIframe = searchParams.get('iframe') === '1';
|
||||
|
||||
useEffect(() => {
|
||||
const fetchTerms = async () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { CheckCircle, Copy, ArrowRight } from 'lucide-react';
|
||||
import { useStore } from '../store/useStore';
|
||||
import confetti from 'canvas-confetti';
|
||||
@@ -8,13 +8,15 @@ import { useNotification } from '../hooks/useNotification';
|
||||
|
||||
export function ThankYou() {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { user } = useStore();
|
||||
const { isVisible, message, type, showNotification, hideNotification } = useNotification();
|
||||
const relayUrl = import.meta.env.VITE_NOSTR_RELAY_URL;
|
||||
const isIframe = searchParams.get('iframe') === '1';
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) {
|
||||
navigate('/login');
|
||||
navigate(isIframe ? '/login?iframe=1' : '/login');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -36,7 +38,6 @@ export function ThankYou() {
|
||||
|
||||
const particleCount = 50 * (timeLeft / duration);
|
||||
|
||||
// Since they fall down, start a bit higher than random
|
||||
confetti({
|
||||
...defaults,
|
||||
particleCount,
|
||||
@@ -50,7 +51,7 @@ export function ThankYou() {
|
||||
}, 250);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [user, navigate]);
|
||||
}, [user, navigate, isIframe]);
|
||||
|
||||
const copyToClipboard = async (text: string) => {
|
||||
try {
|
||||
@@ -114,7 +115,7 @@ export function ThankYou() {
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => navigate('/dashboard')}
|
||||
onClick={() => navigate(isIframe ? '/dashboard?iframe=1' : '/dashboard')}
|
||||
className="px-8 py-4 bg-orange-500 rounded-lg hover:bg-orange-600 transition-colors font-semibold"
|
||||
>
|
||||
Go to Dashboard
|
||||
|
||||
Reference in New Issue
Block a user