diff options
| author | Pinapelz <yukais@pinapelz.com> | 2025-06-29 01:28:39 -0700 |
|---|---|---|
| committer | Pinapelz <yukais@pinapelz.com> | 2025-06-29 01:28:39 -0700 |
| commit | ff37cca46430ed714015647469f88ce06781457a (patch) | |
| tree | 63f406a3c7fb463fed7f34efc8d04d58fe96e0cb /frontend/src/pages | |
| parent | e4fa1e69e7ebfb627c7198fd1a9881e9327ec4d4 (diff) | |
scaffold register,login,and auth endpoints
Diffstat (limited to 'frontend/src/pages')
| -rw-r--r-- | frontend/src/pages/Home.tsx | 119 | ||||
| -rw-r--r-- | frontend/src/pages/Landing.tsx | 154 | ||||
| -rw-r--r-- | frontend/src/pages/Login.tsx | 207 | ||||
| -rw-r--r-- | frontend/src/pages/Register.tsx | 230 |
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; |
