update project

This commit is contained in:
ZOMBIIIIIII
2026-04-14 13:30:26 +03:00
parent a81e29807c
commit 37146f7375
65 changed files with 3782 additions and 629 deletions

View File

@@ -42,6 +42,9 @@ export default function BridgePage() {
const [amount, setAmount] = useState('');
const [confirmed, setConfirmed] = useState(false);
useEffect(() => {
if (!user) router.push('/login');
}, [router, user]);
const destChainOptions = useMemo(() => getDestinationChainOptions(sourceChain), [sourceChain]);
const sourceTokenOptions = useMemo(() => getTokenOptions(sourceChain), [sourceChain]);

View File

@@ -1,20 +1,31 @@
'use client';
import { useEffect } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { useBalances } from '@/hooks/useBalances';
import type { ChainBalance } from '@/lib/balances/types';
import { useAuthStore } from '@/store/auth-store';
export default function DashboardPage() {
const { user, wallets } = useAuthStore();
const router = useRouter();
const { user, wallets, logout } = useAuthStore();
const { portfolio, loading, refreshing, error, refresh } = useBalances();
useEffect(() => {
if (!user) {
router.push('/login');
}
}, [user, router]);
if (!user) return null;
return (
<div style={{ maxWidth: 820, margin: '50px auto', padding: 20 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 16 }}>
<h1>Dashboard</h1>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap', justifyContent: 'flex-end' }}>
<span>{user?.email || 'Not authenticated'}</span>
<span>{user.email}</span>
<Link href="/send" style={navButtonStyle}>
Send
</Link>
@@ -33,6 +44,9 @@ export default function DashboardPage() {
<button onClick={() => void refresh()} style={navButtonStyle} disabled={loading || refreshing}>
{refreshing ? 'Refreshing...' : 'Refresh'}
</button>
<button onClick={logout} style={{ padding: '6px 12px' }}>
Logout
</button>
</div>
</div>

View File

@@ -0,0 +1,125 @@
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { useAuthStore } from '@/store/auth-store';
type Step = 'email' | 'code';
export default function LoginPage() {
const router = useRouter();
const { loginStart, loginComplete, loading, error, clearError } = useAuthStore();
const [step, setStep] = useState<Step>('email');
const [email, setEmail] = useState('');
const [code, setCode] = useState('');
const [password, setPassword] = useState('');
const handleEmailSubmit = async (e: React.FormEvent) => {
e.preventDefault();
clearError();
await loginStart(email);
const state = useAuthStore.getState();
if (!state.error) {
setStep('code');
}
};
const handleCodeSubmit = async (e: React.FormEvent) => {
e.preventDefault();
clearError();
await loginComplete(email, password, code);
const state = useAuthStore.getState();
if (state.user) {
if (!state.mnemonicShown) {
router.push('/mnemonic');
} else {
router.push('/dashboard');
}
}
};
return (
<div style={{ maxWidth: 400, margin: '100px auto', padding: 20 }}>
<h1>Login</h1>
{step === 'email' && (
<form onSubmit={handleEmailSubmit}>
<div style={{ marginBottom: 12 }}>
<label>Email</label><br />
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
style={{ width: '100%', padding: 8 }}
placeholder="you@example.com"
/>
</div>
{error && <p style={{ color: 'red' }}>{error}</p>}
<button
type="submit"
disabled={loading}
style={{ padding: '8px 24px', background: '#007bff', color: '#fff', border: 'none', borderRadius: 4, cursor: 'pointer' }}
>
{loading ? 'Sending code...' : 'Send verification code'}
</button>
</form>
)}
{step === 'code' && (
<form onSubmit={handleCodeSubmit}>
<p style={{ color: '#666', fontSize: 13, marginBottom: 16 }}>
A verification code was sent to <strong>{email}</strong>.
</p>
<div style={{ marginBottom: 12 }}>
<label>Verification code</label><br />
<input
type="text"
value={code}
onChange={(e) => setCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
required
minLength={6}
maxLength={6}
style={{ width: '100%', padding: 8, letterSpacing: 4, fontSize: 18, textAlign: 'center' }}
placeholder="000000"
inputMode="numeric"
autoFocus
/>
</div>
<div style={{ marginBottom: 12 }}>
<label>Password</label><br />
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
minLength={8}
style={{ width: '100%', padding: 8 }}
/>
</div>
{error && <p style={{ color: 'red' }}>{error}</p>}
<button
type="submit"
disabled={loading}
style={{ padding: '8px 24px', background: '#007bff', color: '#fff', border: 'none', borderRadius: 4, cursor: 'pointer' }}
>
{loading ? 'Logging in...' : 'Login'}
</button>
<button
type="button"
onClick={() => { setStep('email'); setCode(''); setPassword(''); clearError(); }}
style={{ marginLeft: 8, padding: '8px 16px', background: '#fff', border: '1px solid #ccc', borderRadius: 4, cursor: 'pointer' }}
>
Back
</button>
</form>
)}
<p style={{ marginTop: 16 }}>
No account? <a href="/register">Register</a>
</p>
</div>
);
}

View File

@@ -0,0 +1,148 @@
'use client';
import { useEffect, useMemo, useState } from 'react';
import { useRouter } from 'next/navigation';
import { useAuthStore } from '@/store/auth-store';
export default function MnemonicPage() {
const router = useRouter();
const { mnemonic, wallets, confirmMnemonic } = useAuthStore();
const [step, setStep] = useState<'show' | 'verify' | 'keys'>('show');
const [answers, setAnswers] = useState<Record<number, string>>({});
const [verifyError, setVerifyError] = useState('');
const words = useMemo(() => mnemonic?.split(' ') || [], [mnemonic]);
useEffect(() => {
if (!mnemonic) {
router.push('/dashboard');
}
}, [mnemonic, router]);
const quizIndices = useMemo(() => {
if (words.length < 3) return [];
const indices: number[] = [];
while (indices.length < 3) {
const idx = Math.floor(Math.random() * words.length);
if (!indices.includes(idx)) indices.push(idx);
}
return indices.sort((a, b) => a - b);
}, [words.length]);
if (!mnemonic) return null;
const handleVerify = () => {
setVerifyError('');
for (const idx of quizIndices) {
if (answers[idx]?.trim().toLowerCase() !== words[idx]) {
setVerifyError(`Wrong word for position #${idx + 1}. Try again.`);
return;
}
}
setStep('keys');
};
const handleConfirm = async () => {
await confirmMnemonic();
router.push('/dashboard');
};
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
setTimeout(() => {
try {
void navigator.clipboard.writeText('');
} catch {
// Document may have lost focus; ignore
}
}, 60000);
} catch {
// Fallback for older browsers or denied permission
}
};
if (step === 'show') {
return (
<div style={{ maxWidth: 600, margin: '50px auto', padding: 20 }}>
<h1>Save Your Mnemonic Phrase</h1>
<p style={{ color: 'red', fontWeight: 'bold' }}>
Write these words down and store them safely. You will NOT be able to see them again!
</p>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 8, margin: '20px 0' }}>
{words.map((word, i) => (
<div key={i} style={{ padding: 8, border: '1px solid #ccc' }}>
<strong>{i + 1}.</strong> {word}
</div>
))}
</div>
<button onClick={() => copyToClipboard(mnemonic)} style={{ padding: '8px 16px', marginRight: 8 }}>
Copy Mnemonic
</button>
<button onClick={() => setStep('verify')} style={{ padding: '8px 16px' }}>
Next: Verify
</button>
</div>
);
}
if (step === 'verify') {
return (
<div style={{ maxWidth: 400, margin: '50px auto', padding: 20 }}>
<h1>Verify Mnemonic</h1>
<p>Enter the following words from your mnemonic to confirm you saved it.</p>
{quizIndices.map((idx) => (
<div key={idx} style={{ marginBottom: 12 }}>
<label>Word #{idx + 1}</label><br />
<input
type="text"
value={answers[idx] || ''}
onChange={(e) => setAnswers({ ...answers, [idx]: e.target.value })}
style={{ width: '100%', padding: 8 }}
/>
</div>
))}
{verifyError && <p style={{ color: 'red' }}>{verifyError}</p>}
<button onClick={() => setStep('show')} style={{ padding: '8px 16px', marginRight: 8 }}>
Back
</button>
<button onClick={handleVerify} style={{ padding: '8px 16px' }}>
Verify
</button>
</div>
);
}
return (
<div style={{ maxWidth: 600, margin: '50px auto', padding: 20 }}>
<h1>Your Private Keys</h1>
<p style={{ color: 'red', fontWeight: 'bold' }}>
Save these private keys. They will NOT be shown again!
</p>
{wallets.map((w) => (
<div key={w.chain} style={{ border: '1px solid #ccc', padding: 12, marginBottom: 12 }}>
<h3>{w.chain}</h3>
<p><strong>Address:</strong> {w.address}</p>
<p style={{ wordBreak: 'break-all' }}>
<strong>Private Key:</strong> {w.privateKey}
</p>
<button onClick={() => copyToClipboard(w.privateKey)} style={{ padding: '4px 12px' }}>
Copy Key
</button>
</div>
))}
<div style={{ marginTop: 20 }}>
<label>
<input type="checkbox" id="confirm-checkbox" />
{' '}I have saved all my private keys
</label>
</div>
<button
onClick={handleConfirm}
style={{ padding: '8px 24px', marginTop: 12 }}
>
Continue to Dashboard
</button>
</div>
);
}

View File

@@ -1,5 +1,5 @@
import { redirect } from 'next/navigation';
export default function Home() {
redirect('/dashboard');
redirect('/login');
}

View File

@@ -24,6 +24,9 @@ export default function ReceivePage() {
const [amount, setAmount] = useState('');
const [copied, setCopied] = useState(false);
useEffect(() => {
if (!user) router.push('/login');
}, [user, router]);
// Reset token when chain changes
useEffect(() => {

View File

@@ -0,0 +1,144 @@
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { useAuthStore } from '@/store/auth-store';
type Step = 'email' | 'code';
export default function RegisterPage() {
const router = useRouter();
const { registerStart, registerComplete, loading, error, clearError } = useAuthStore();
const [step, setStep] = useState<Step>('email');
const [email, setEmail] = useState('');
const [code, setCode] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [localError, setLocalError] = useState<string | null>(null);
const handleEmailSubmit = async (e: React.FormEvent) => {
e.preventDefault();
clearError();
setLocalError(null);
await registerStart(email);
const state = useAuthStore.getState();
if (!state.error) {
setStep('code');
}
};
const handleCodeSubmit = async (e: React.FormEvent) => {
e.preventDefault();
clearError();
setLocalError(null);
if (password !== confirmPassword) {
setLocalError('Passwords do not match');
return;
}
await registerComplete(email, password, code);
const state = useAuthStore.getState();
if (state.user) {
router.push('/mnemonic');
}
};
const displayError = localError || error;
return (
<div style={{ maxWidth: 400, margin: '100px auto', padding: 20 }}>
<h1>Register</h1>
{step === 'email' && (
<form onSubmit={handleEmailSubmit}>
<div style={{ marginBottom: 12 }}>
<label>Email</label><br />
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
style={{ width: '100%', padding: 8 }}
placeholder="you@example.com"
/>
</div>
{displayError && <p style={{ color: 'red' }}>{displayError}</p>}
<button
type="submit"
disabled={loading}
style={{ padding: '8px 24px', background: '#007bff', color: '#fff', border: 'none', borderRadius: 4, cursor: 'pointer' }}
>
{loading ? 'Sending code...' : 'Send verification code'}
</button>
</form>
)}
{step === 'code' && (
<form onSubmit={handleCodeSubmit}>
<p style={{ color: '#666', fontSize: 13, marginBottom: 16 }}>
A verification code was sent to <strong>{email}</strong>.
</p>
<div style={{ marginBottom: 12 }}>
<label>Verification code</label><br />
<input
type="text"
value={code}
onChange={(e) => setCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
required
minLength={6}
maxLength={6}
style={{ width: '100%', padding: 8, letterSpacing: 4, fontSize: 18, textAlign: 'center' }}
placeholder="000000"
inputMode="numeric"
autoFocus
/>
</div>
<div style={{ marginBottom: 12 }}>
<label>Password (min 8 characters)</label><br />
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
minLength={8}
style={{ width: '100%', padding: 8 }}
/>
</div>
<div style={{ marginBottom: 12 }}>
<label>Confirm password</label><br />
<input
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
required
minLength={8}
style={{ width: '100%', padding: 8 }}
/>
</div>
{displayError && <p style={{ color: 'red' }}>{displayError}</p>}
<button
type="submit"
disabled={loading}
style={{ padding: '8px 24px', background: '#007bff', color: '#fff', border: 'none', borderRadius: 4, cursor: 'pointer' }}
>
{loading ? 'Creating account...' : 'Register'}
</button>
<button
type="button"
onClick={() => { setStep('email'); setCode(''); setPassword(''); setConfirmPassword(''); clearError(); setLocalError(null); }}
style={{ marginLeft: 8, padding: '8px 16px', background: '#fff', border: '1px solid #ccc', borderRadius: 4, cursor: 'pointer' }}
>
Back
</button>
</form>
)}
<p style={{ marginTop: 16 }}>
Already have an account? <a href="/login">Login</a>
</p>
</div>
);
}

View File

@@ -48,6 +48,9 @@ export default function SendPage() {
const [result, setResult] = useState<SendResult | null>(null);
const [scannerOpen, setScannerOpen] = useState(false);
useEffect(() => {
if (!user) router.push('/login');
}, [user, router]);
// Reset token on chain change
useEffect(() => {

View File

@@ -1,11 +1,23 @@
'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { useAuthStore } from '@/store/auth-store';
import { SeedPhraseModal } from '@/components/SeedPhraseModal';
export default function SettingsPage() {
const router = useRouter();
const { user } = useAuthStore();
const [showSeedModal, setShowSeedModal] = useState(false);
useEffect(() => {
if (!user) {
router.push('/login');
}
}, [user, router]);
if (!user) return null;
return (
<div style={{ maxWidth: 520, margin: '50px auto', padding: 20 }}>
@@ -16,15 +28,40 @@ export default function SettingsPage() {
</Link>
</div>
{/* Security Section */}
<div style={{ border: '1px solid #ccc', borderRadius: 4, padding: 16, marginBottom: 16 }}>
<h3 style={{ marginTop: 0 }}>Security</h3>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 16 }}>
<div>
<p style={{ margin: 0, fontWeight: 600 }}>Seed Phrase</p>
<p style={{ margin: '4px 0 0', color: '#666', fontSize: 13 }}>
View your 12-word recovery phrase. Requires password verification.
</p>
</div>
<button
onClick={() => setShowSeedModal(true)}
style={primaryButtonStyle}
>
View
</button>
</div>
</div>
{/* Account Section */}
<div style={{ border: '1px solid #ccc', borderRadius: 4, padding: 16, marginBottom: 16 }}>
<h3 style={{ marginTop: 0 }}>Account</h3>
<div>
<p style={{ margin: 0, fontWeight: 600 }}>Email</p>
<p style={{ margin: '4px 0 0', color: '#666', fontSize: 13 }}>{user?.email || 'Not authenticated'}</p>
<p style={{ margin: '4px 0 0', color: '#666', fontSize: 13 }}>{user.email}</p>
</div>
</div>
<SeedPhraseModal
isOpen={showSeedModal}
onClose={() => setShowSeedModal(false)}
/>
</div>
);
}
@@ -43,3 +80,15 @@ const navButtonStyle: React.CSSProperties = {
cursor: 'pointer',
background: '#fff',
};
const primaryButtonStyle: React.CSSProperties = {
padding: '8px 16px',
border: '1px solid #333',
borderRadius: 4,
cursor: 'pointer',
background: '#333',
color: '#fff',
fontSize: 14,
fontWeight: 600,
whiteSpace: 'nowrap',
};

View File

@@ -66,6 +66,11 @@ export default function SwapPage() {
[chain, amount, fromSymbol, slippageBps, toSymbol]
);
useEffect(() => {
if (!user) {
router.push('/login');
}
}, [router, user]);
useEffect(() => {
estimateOutput(request);