aboutsummaryrefslogtreecommitdiffstats
path: root/frontend/src/pages
diff options
context:
space:
mode:
authorPinapelz <yukais@pinapelz.com>2025-06-29 01:28:39 -0700
committerPinapelz <yukais@pinapelz.com>2025-06-29 01:28:39 -0700
commitff37cca46430ed714015647469f88ce06781457a (patch)
tree63f406a3c7fb463fed7f34efc8d04d58fe96e0cb /frontend/src/pages
parente4fa1e69e7ebfb627c7198fd1a9881e9327ec4d4 (diff)
scaffold register,login,and auth endpoints
Diffstat (limited to 'frontend/src/pages')
-rw-r--r--frontend/src/pages/Home.tsx119
-rw-r--r--frontend/src/pages/Landing.tsx154
-rw-r--r--frontend/src/pages/Login.tsx207
-rw-r--r--frontend/src/pages/Register.tsx230
4 files changed, 710 insertions, 0 deletions
diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx
new file mode 100644
index 0000000..1d87c8a
--- /dev/null
+++ b/frontend/src/pages/Home.tsx
@@ -0,0 +1,119 @@
+import { Link, useNavigate } from 'react-router';
+import { useAuth } from '../contexts/AuthContext';
+
+const Home = () => {
+ const { user, isLoading, logout } = useAuth();
+ const navigate = useNavigate();
+
+ const handleLogout = async () => {
+ try {
+ await logout();
+ navigate('/');
+ } catch (error) {
+ console.error('Logout failed:', error);
+ alert('Network error during logout. Please try again.');
+ }
+ };
+
+ if (isLoading) {
+ return (
+ <div className="min-h-screen bg-slate-950 flex items-center justify-center">
+ <div className="text-center">
+ <div className="w-8 h-8 border-2 border-violet-500 border-t-transparent rounded-full animate-spin mx-auto mb-4"></div>
+ <p className="text-slate-300">Loading dashboard...</p>
+ </div>
+ </div>
+ );
+ }
+
+ if (!user) {
+ return (
+ <div className="min-h-screen bg-slate-950 flex items-center justify-center">
+ <div className="text-center max-w-md mx-auto px-6">
+ <div className="bg-slate-900 rounded-lg p-8 border border-slate-700">
+ <h2 className="text-2xl font-bold text-white mb-4">Session Expired</h2>
+ <p className="text-slate-300 mb-6">Please sign in to access your dashboard.</p>
+ <div className="space-y-3">
+ <Link
+ to="/login"
+ className="block w-full bg-violet-600 hover:bg-violet-700 text-white py-3 rounded-md font-medium transition-colors"
+ >
+ Sign In
+ </Link>
+ <Link
+ to="/"
+ className="block w-full border border-slate-600 hover:border-violet-500 text-slate-300 hover:text-white py-3 rounded-md font-medium transition-colors"
+ >
+ Back to Home
+ </Link>
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+ return (
+ <div className="min-h-screen bg-slate-950">
+ {/* Navigation */}
+ <nav className="border-b border-slate-800 bg-slate-950/95 backdrop-blur-sm sticky top-0 z-50">
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
+ <div className="flex items-center justify-between h-16">
+ <div className="flex items-center space-x-3">
+ <div className="w-8 h-8 bg-violet-600 rounded-md flex items-center justify-center">
+ <span className="text-white font-bold text-sm">M</span>
+ </div>
+ <span className="text-white font-semibold text-lg">Mirage</span>
+ </div>
+ <div className="flex items-center space-x-4">
+ <span className="text-slate-300 text-sm">Welcome back, {user.username}</span>
+ <button
+ onClick={handleLogout}
+ className="text-slate-300 hover:text-white px-3 py-2 rounded-md text-sm font-medium transition-colors"
+ >
+ Sign Out
+ </button>
+ </div>
+ </div>
+ </div>
+ </nav>
+
+ {/* Main Content */}
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
+ {/* Header */}
+ <div className="mb-8">
+ <h1 className="text-3xl font-bold text-white mb-2">Dashboard</h1>
+ <p className="text-slate-400">Track your rhythm game progress and performance</p>
+ </div>
+
+ {/* Coming Soon Card */}
+ <div className="bg-slate-900 rounded-lg border border-slate-700 p-12 text-center">
+ <div className="w-16 h-16 bg-violet-600/20 rounded-full flex items-center justify-center mx-auto mb-6">
+ <svg className="w-8 h-8 text-violet-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
+ </svg>
+ </div>
+ <h2 className="text-2xl font-bold text-white mb-4">Dashboard Coming Soon</h2>
+ <p className="text-slate-300 mb-8 max-w-2xl mx-auto">
+ We're working hard to bring you an amazing dashboard experience. Track your scores,
+ analyze your performance, and compete with friends - all coming soon!
+ </p>
+ <div className="flex flex-col sm:flex-row items-center justify-center gap-4">
+ <div className="bg-slate-800 px-6 py-3 rounded-lg border border-slate-600">
+ <p className="text-sm text-slate-300">
+ <span className="font-semibold text-violet-300">User ID:</span> {user.id}
+ </p>
+ </div>
+ <div className="bg-slate-800 px-6 py-3 rounded-lg border border-slate-600">
+ <p className="text-sm text-slate-300">
+ <span className="font-semibold text-violet-300">Email:</span> {user.email}
+ </p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+};
+
+export default Home;
diff --git a/frontend/src/pages/Landing.tsx b/frontend/src/pages/Landing.tsx
new file mode 100644
index 0000000..7cd0f43
--- /dev/null
+++ b/frontend/src/pages/Landing.tsx
@@ -0,0 +1,154 @@
+import { Link } from 'react-router';
+
+const Landing = () => {
+ return (
+ <div className="min-h-screen bg-slate-950 text-slate-100">
+ {/* Navigation */}
+ <nav className="border-b border-slate-800 bg-slate-950/95 backdrop-blur-sm sticky top-0 z-50">
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
+ <div className="flex items-center justify-between h-16">
+ <div className="flex items-center space-x-3">
+ <div className="w-8 h-8 bg-violet-600 rounded-md flex items-center justify-center">
+ <span className="text-white font-bold text-sm">M</span>
+ </div>
+ <span className="text-white font-semibold text-lg">Mirage</span>
+ </div>
+ <div className="flex items-center space-x-4">
+ <Link
+ to="/login"
+ className="text-slate-300 hover:text-white px-3 py-2 rounded-md text-sm font-medium transition-colors"
+ >
+ Log In
+ </Link>
+ <Link
+ to="/register"
+ className="bg-violet-600 hover:bg-violet-700 text-white px-4 py-2 rounded-md text-sm font-medium transition-colors"
+ >
+ Create Account
+ </Link>
+ </div>
+ </div>
+ </div>
+ </nav>
+
+ {/* Hero Section with Banner */}
+ <section className="relative py-32 lg:py-40 overflow-hidden">
+ {/* Background Image */}
+ <div className="absolute inset-0 bg-cover bg-center bg-no-repeat opacity-20"
+ style={{backgroundImage: "url('https://images.unsplash.com/photo-1493225457124-a3eb161ffa5f?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80')"}}></div>
+
+ {/* Overlay */}
+ <div className="absolute inset-0 bg-slate-950/60"></div>
+
+ {/* Content */}
+ <div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
+ <div className="max-w-4xl">
+ <h1 className="text-5xl md:text-6xl font-bold text-white mb-6 leading-tight">
+ Welcome to <span className="text-violet-400">Mirage!</span>
+ </h1>
+ <p className="text-xl text-slate-300 mb-8 leading-relaxed">
+ Looks like you're not logged in. If you've got an account, <Link to="/login" className="text-violet-400 hover:text-violet-300 underline">Login!</Link>
+ </p>
+ </div>
+ </div>
+ </section>
+
+ {/* Introduction Section */}
+ <section className="py-16 border-t border-slate-800">
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
+ <div className="max-w-4xl">
+ <h2 className="text-3xl md:text-4xl font-bold text-white mb-6">
+ I'm New Around Here, What is this?
+ </h2>
+ <p className="text-lg text-slate-300 leading-relaxed">
+ <span className="font-semibold text-violet-300">Mirage</span> is a Rhythm Game Score Tracker. That means we...
+ </p>
+ </div>
+ </div>
+ </section>
+
+ {/* Track Your Scores Section */}
+ <section className="py-16 border-t border-slate-800">
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
+ <div className="max-w-4xl">
+ <h2 className="text-3xl md:text-4xl font-bold text-white mb-6">
+ <span className="text-violet-400">Track</span> Your Scores.
+ </h2>
+ <p className="text-lg text-slate-300 leading-relaxed">
+ Mirage supports a bunch of your favourite games, and integrates with many existing services to
+ make sure no score is lost to the void. Furthermore, it's backed by an Open-Source API, so your
+ scores are always available!
+ </p>
+ </div>
+ </div>
+ </section>
+
+ {/* Analyse Your Scores Section */}
+ <section className="py-16 border-t border-slate-800">
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
+ <div className="max-w-4xl">
+ <h2 className="text-3xl md:text-4xl font-bold text-white mb-6">
+ <span className="text-purple-400">Analyse</span> Your Scores.
+ </h2>
+ <p className="text-lg text-slate-300 leading-relaxed">
+ Mirage analyses your scores for you, breaking them down into all the statistics you'll ever need.
+ No more spreadsheets!
+ </p>
+ </div>
+ </div>
+ </section>
+
+ {/* Provide Cool Features Section */}
+ <section className="py-16 border-t border-slate-800">
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
+ <div className="max-w-4xl">
+ <h2 className="text-3xl md:text-4xl font-bold text-white mb-6">
+ Provide <span className="text-violet-400">Cool Features</span>.
+ </h2>
+ <p className="text-lg text-slate-300 leading-relaxed">
+ Mirage implements the features rhythm gamers already talk about. Break your scores down
+ into sessions, showcase your best metrics on your profile, study your progress on folders - it's all
+ there, and done for you!
+ </p>
+ </div>
+ </div>
+ </section>
+
+ {/* Call to Action Section */}
+ <section className="py-20 border-t border-slate-800 bg-slate-900">
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
+ <div className="text-center max-w-2xl mx-auto">
+ <h2 className="text-3xl md:text-4xl font-bold text-white mb-8">
+ Ready to Start <span className="text-violet-400">Tracking?</span>
+ </h2>
+ <div className="flex flex-col sm:flex-row items-center justify-center gap-4">
+ <Link
+ to="/register"
+ className="bg-violet-600 hover:bg-violet-700 text-white px-8 py-3 rounded-md text-lg font-medium transition-colors min-w-[200px] shadow-lg shadow-violet-600/25"
+ >
+ Create Account
+ </Link>
+ <Link
+ to="/login"
+ className="border border-slate-600 hover:border-violet-500 text-slate-300 hover:text-white px-8 py-3 rounded-md text-lg font-medium transition-colors min-w-[200px]"
+ >
+ Log In
+ </Link>
+ </div>
+ </div>
+ </div>
+ </section>
+
+ {/* Footer */}
+ <footer className="border-t border-slate-800 py-12 bg-slate-950">
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
+ <div className="text-center text-slate-400">
+ <a className="hover:underline" href="">GitHub</a>
+ </div>
+ </div>
+ </footer>
+ </div>
+ );
+};
+
+export default Landing;
diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx
new file mode 100644
index 0000000..5bbdefc
--- /dev/null
+++ b/frontend/src/pages/Login.tsx
@@ -0,0 +1,207 @@
+import { useState } from "react";
+import { Link, useNavigate } from "react-router";
+import { useAuth } from "../contexts/AuthContext";
+
+const Login = () => {
+ const [formData, setFormData] = useState({
+ username: "",
+ password: "",
+ });
+ const [errors, setErrors] = useState<Record<string, string>>({});
+ const [isLoading, setIsLoading] = useState(false);
+
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ const { name, value } = e.target;
+ setFormData((prev) => ({
+ ...prev,
+ [name]: value,
+ }));
+ // Clear error when user starts typing
+ if (errors[name]) {
+ setErrors((prev) => ({
+ ...prev,
+ [name]: "",
+ }));
+ }
+ };
+
+ const validateForm = () => {
+ const newErrors: Record<string, string> = {};
+
+ if (!formData.username.trim()) {
+ newErrors.username = "Username is required";
+ } else if (formData.username.length < 3) {
+ newErrors.username = "Username must be at least 3 characters";
+ }
+
+ if (!formData.password) {
+ newErrors.password = "Password is required";
+ }
+
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ };
+
+ const { login } = useAuth();
+ const navigate = useNavigate();
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ if (!validateForm()) return;
+
+ setIsLoading(true);
+ try {
+ const result = await login(formData.username, formData.password);
+
+ if (!result.success) {
+ setErrors({
+ general: result.error || "Login failed. Please try again.",
+ });
+ return;
+ }
+
+ // Redirect to home page on successful login
+ navigate("/home");
+ } catch (error) {
+ console.error("Login failed:", error);
+ setErrors({ general: "Network error. Please try again." });
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+ <div className="min-h-screen bg-slate-950 flex items-center justify-center px-4 sm:px-6 lg:px-8">
+ <div className="max-w-md w-full space-y-8">
+ {/* Header */}
+ <div className="text-center">
+ <Link to="/" className="inline-flex items-center space-x-3 mb-8">
+ <div className="w-10 h-10 bg-violet-600 rounded-md flex items-center justify-center">
+ <span className="text-white font-bold text-lg">M</span>
+ </div>
+ <span className="text-white font-semibold text-2xl">Mirage</span>
+ </Link>
+ <h2 className="text-3xl font-bold text-white">
+ Sign in to your account
+ </h2>
+ <p className="mt-2 text-sm text-slate-400">
+ Or{" "}
+ <Link
+ to="/register"
+ className="font-medium text-violet-400 hover:text-violet-300 transition-colors"
+ >
+ create a new account
+ </Link>
+ </p>
+ </div>
+
+ {/* Form */}
+ <div className="bg-slate-900 rounded-lg p-8 border border-slate-700">
+ {errors.general && (
+ <div className="mb-6 bg-red-900/50 border border-red-700 rounded-md p-4">
+ <div className="flex">
+ <div className="flex-shrink-0">
+ <svg
+ className="h-5 w-5 text-red-400"
+ viewBox="0 0 20 20"
+ fill="currentColor"
+ >
+ <path
+ fillRule="evenodd"
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
+ clipRule="evenodd"
+ />
+ </svg>
+ </div>
+ <div className="ml-3">
+ <p className="text-sm text-red-300">{errors.general}</p>
+ </div>
+ </div>
+ </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
+ type="text"
+ id="username"
+ name="username"
+ value={formData.username}
+ onChange={handleChange}
+ className="block w-full px-3 py-2 border border-slate-600 rounded-md shadow-sm bg-slate-800 text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-500 focus:border-violet-500 transition-colors"
+ placeholder="Enter your username"
+ />
+ {errors.username && (
+ <p className="mt-1 text-sm text-red-400">{errors.username}</p>
+ )}
+ </div>
+
+ <div>
+ <label
+ htmlFor="password"
+ className="block text-sm font-medium text-slate-300 mb-2"
+ >
+ Password
+ </label>
+ <input
+ type="password"
+ id="password"
+ name="password"
+ value={formData.password}
+ onChange={handleChange}
+ className="block w-full px-3 py-2 border border-slate-600 rounded-md shadow-sm bg-slate-800 text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-500 focus:border-violet-500 transition-colors"
+ placeholder="Enter your password"
+ />
+ {errors.password && (
+ <p className="mt-1 text-sm text-red-400">{errors.password}</p>
+ )}
+ </div>
+
+ <button
+ type="submit"
+ disabled={isLoading}
+ className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-violet-600 hover:bg-violet-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-violet-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
+ >
+ {isLoading ? (
+ <div className="flex items-center">
+ <svg
+ className="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ >
+ <circle
+ className="opacity-25"
+ cx="12"
+ cy="12"
+ r="10"
+ stroke="currentColor"
+ strokeWidth="4"
+ ></circle>
+ <path
+ className="opacity-75"
+ fill="currentColor"
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
+ ></path>
+ </svg>
+ Signing in...
+ </div>
+ ) : (
+ "Sign in"
+ )}
+ </button>
+ </form>
+ </div>
+ </div>
+ </div>
+ );
+};
+
+export default Login;
diff --git a/frontend/src/pages/Register.tsx b/frontend/src/pages/Register.tsx
new file mode 100644
index 0000000..1f8dde8
--- /dev/null
+++ b/frontend/src/pages/Register.tsx
@@ -0,0 +1,230 @@
+import { useState } from 'react';
+import { Link, useNavigate } from 'react-router';
+import { useAuth } from '../contexts/AuthContext';
+
+const Register = () => {
+ const [formData, setFormData] = useState({
+ username: '',
+ email: '',
+ password: '',
+ confirmPassword: ''
+ });
+ const [errors, setErrors] = useState<Record<string, string>>({});
+ const [isLoading, setIsLoading] = useState(false);
+
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ const { name, value } = e.target;
+ setFormData(prev => ({
+ ...prev,
+ [name]: value
+ }));
+ if (errors[name]) {
+ setErrors(prev => ({
+ ...prev,
+ [name]: ''
+ }));
+ }
+ };
+
+ const validateForm = () => {
+ const newErrors: Record<string, string> = {};
+
+ if (!formData.username.trim()) {
+ newErrors.username = 'Username is required';
+ } else if (formData.username.length < 3) {
+ newErrors.username = 'Username must be at least 3 characters';
+ }
+
+ if (!formData.email.trim()) {
+ newErrors.email = 'Email is required';
+ } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
+ newErrors.email = 'Please enter a valid email address';
+ }
+
+ if (!formData.password) {
+ newErrors.password = 'Password is required';
+ } else if (formData.password.length < 6) {
+ newErrors.password = 'Password must be at least 6 characters';
+ }
+
+ if (!formData.confirmPassword) {
+ newErrors.confirmPassword = 'Please confirm your password';
+ } else if (formData.password !== formData.confirmPassword) {
+ newErrors.confirmPassword = 'Passwords do not match';
+ }
+
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ };
+
+ const { register } = useAuth();
+ const navigate = useNavigate();
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ if (!validateForm()) return;
+
+ setIsLoading(true);
+ try {
+ const result = await register({
+ username: formData.username,
+ email: formData.email,
+ password: formData.password,
+ });
+
+ if (!result.success) {
+ setErrors({ general: result.error || 'Registration failed. Please try again.' });
+ return;
+ }
+
+ // Redirect to home page on successful registration
+ navigate('/home');
+ } catch (error) {
+ console.error('Registration failed:', error);
+ setErrors({ general: 'Network error. Please try again.' });
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+ <div className="min-h-screen bg-slate-950 flex items-center justify-center px-4 sm:px-6 lg:px-8">
+ <div className="max-w-md w-full space-y-8">
+ {/* Header */}
+ <div className="text-center">
+ <Link to="/" className="inline-flex items-center space-x-3 mb-8">
+ <div className="w-10 h-10 bg-violet-600 rounded-md flex items-center justify-center">
+ <span className="text-white font-bold text-lg">M</span>
+ </div>
+ <span className="text-white font-semibold text-2xl">Mirage</span>
+ </Link>
+ <h2 className="text-3xl font-bold text-white">
+ Create your account
+ </h2>
+ <p className="mt-2 text-sm text-slate-400">
+ Already have an account?{' '}
+ <Link
+ to="/login"
+ className="font-medium text-violet-400 hover:text-violet-300 transition-colors"
+ >
+ Sign in here
+ </Link>
+ </p>
+ </div>
+
+ {/* Form */}
+ <div className="bg-slate-900 rounded-lg p-8 border border-slate-700">
+ {errors.general && (
+ <div className="mb-6 bg-red-900/50 border border-red-700 rounded-md p-4">
+ <div className="flex">
+ <div className="flex-shrink-0">
+ <svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
+ <path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
+ </svg>
+ </div>
+ <div className="ml-3">
+ <p className="text-sm text-red-300">{errors.general}</p>
+ </div>
+ </div>
+ </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
+ type="text"
+ id="username"
+ name="username"
+ value={formData.username}
+ onChange={handleChange}
+ className="block w-full px-3 py-2 border border-slate-600 rounded-md shadow-sm bg-slate-800 text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-500 focus:border-violet-500 transition-colors"
+ placeholder="Choose a username"
+ />
+ {errors.username && (
+ <p className="mt-1 text-sm text-red-400">{errors.username}</p>
+ )}
+ </div>
+
+ <div>
+ <label htmlFor="email" className="block text-sm font-medium text-slate-300 mb-2">
+ Email address
+ </label>
+ <input
+ type="email"
+ id="email"
+ name="email"
+ value={formData.email}
+ onChange={handleChange}
+ className="block w-full px-3 py-2 border border-slate-600 rounded-md shadow-sm bg-slate-800 text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-500 focus:border-violet-500 transition-colors"
+ placeholder="Enter your email"
+ />
+ {errors.email && (
+ <p className="mt-1 text-sm text-red-400">{errors.email}</p>
+ )}
+ </div>
+
+ <div>
+ <label htmlFor="password" className="block text-sm font-medium text-slate-300 mb-2">
+ Password
+ </label>
+ <input
+ type="password"
+ id="password"
+ name="password"
+ value={formData.password}
+ onChange={handleChange}
+ className="block w-full px-3 py-2 border border-slate-600 rounded-md shadow-sm bg-slate-800 text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-500 focus:border-violet-500 transition-colors"
+ placeholder="Create a password"
+ />
+ {errors.password && (
+ <p className="mt-1 text-sm text-red-400">{errors.password}</p>
+ )}
+ </div>
+
+ <div>
+ <label htmlFor="confirmPassword" className="block text-sm font-medium text-slate-300 mb-2">
+ Confirm password
+ </label>
+ <input
+ type="password"
+ id="confirmPassword"
+ name="confirmPassword"
+ value={formData.confirmPassword}
+ onChange={handleChange}
+ className="block w-full px-3 py-2 border border-slate-600 rounded-md shadow-sm bg-slate-800 text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-500 focus:border-violet-500 transition-colors"
+ placeholder="Confirm your password"
+ />
+ {errors.confirmPassword && (
+ <p className="mt-1 text-sm text-red-400">{errors.confirmPassword}</p>
+ )}
+ </div>
+
+ <button
+ type="submit"
+ disabled={isLoading}
+ className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-violet-600 hover:bg-violet-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-violet-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
+ >
+ {isLoading ? (
+ <div className="flex items-center">
+ <svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
+ </svg>
+ Creating account...
+ </div>
+ ) : (
+ 'Create account'
+ )}
+ </button>
+ </form>
+ </div>
+ </div>
+ </div>
+ );
+};
+
+export default Register;
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage