feat: dashboard setup with passkey authentication

Scaffold Vite + React + TypeScript project with Tailwind CSS dark theme.
Add Axios API client with JWT interceptor and auto-refresh, WebAuthn
passkey auth flow (register/login), protected route wrapper, and React
Router with public and protected routes.
This commit is contained in:
Viktor Barzin 2026-02-22 15:54:32 +00:00
parent f218865872
commit f121f376ae
No known key found for this signature in database
GPG key ID: 0EB088298288D958
20 changed files with 5274 additions and 0 deletions

View file

@ -0,0 +1,92 @@
import { useState, type FormEvent } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import { useAuth } from '../hooks/useAuth';
export default function Login() {
const [username, setUsername] = useState('');
const { login, error, loading } = useAuth();
const navigate = useNavigate();
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
if (!username.trim()) return;
try {
await login(username.trim());
navigate('/portfolio');
} catch {
// error is set in useAuth
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-slate-900">
<div className="w-full max-w-md p-8 bg-slate-800 rounded-xl shadow-2xl">
<div className="text-center mb-8">
<h1 className="text-3xl font-bold text-white mb-2">Trading Bot</h1>
<p className="text-slate-400">Sign in to your dashboard</p>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label
htmlFor="username"
className="block text-sm font-medium text-slate-300 mb-2"
>
Username
</label>
<input
id="username"
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Enter your username"
className="w-full px-4 py-3 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
autoComplete="username"
required
/>
</div>
{error && (
<div className="p-3 bg-red-900/50 border border-red-700 rounded-lg text-red-300 text-sm">
{error}
</div>
)}
<button
type="submit"
disabled={loading || !username.trim()}
className="w-full py-3 px-4 bg-blue-600 hover:bg-blue-700 disabled:bg-slate-600 disabled:cursor-not-allowed text-white font-medium rounded-lg transition-colors flex items-center justify-center gap-2"
>
{loading ? (
<span className="inline-block w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
) : (
<>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
/>
</svg>
Sign in with Passkey
</>
)}
</button>
</form>
<p className="mt-6 text-center text-slate-400 text-sm">
Don't have an account?{' '}
<Link to="/register" className="text-blue-400 hover:text-blue-300">
Register
</Link>
</p>
</div>
</div>
);
}

View file

@ -0,0 +1,92 @@
import { useState, type FormEvent } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import { useAuth } from '../hooks/useAuth';
export default function Register() {
const [username, setUsername] = useState('');
const { register, error, loading } = useAuth();
const navigate = useNavigate();
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
if (!username.trim()) return;
try {
await register(username.trim());
navigate('/portfolio');
} catch {
// error is set in useAuth
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-slate-900">
<div className="w-full max-w-md p-8 bg-slate-800 rounded-xl shadow-2xl">
<div className="text-center mb-8">
<h1 className="text-3xl font-bold text-white mb-2">Trading Bot</h1>
<p className="text-slate-400">Create your account</p>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label
htmlFor="username"
className="block text-sm font-medium text-slate-300 mb-2"
>
Username
</label>
<input
id="username"
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Choose a username"
className="w-full px-4 py-3 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
autoComplete="username"
required
/>
</div>
{error && (
<div className="p-3 bg-red-900/50 border border-red-700 rounded-lg text-red-300 text-sm">
{error}
</div>
)}
<button
type="submit"
disabled={loading || !username.trim()}
className="w-full py-3 px-4 bg-green-600 hover:bg-green-700 disabled:bg-slate-600 disabled:cursor-not-allowed text-white font-medium rounded-lg transition-colors flex items-center justify-center gap-2"
>
{loading ? (
<span className="inline-block w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
) : (
<>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"
/>
</svg>
Register Passkey
</>
)}
</button>
</form>
<p className="mt-6 text-center text-slate-400 text-sm">
Already have an account?{' '}
<Link to="/login" className="text-blue-400 hover:text-blue-300">
Sign in
</Link>
</p>
</div>
</div>
);
}