repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
certificates
stars
latest
clone command
git clone gitlawb://did:key:z6Mkqhmm...XL9c/certificatesgit clone gitlawb://did:key:z6Mkqhmm.../certificates019974a8sync from playground15h ago| #1 | import { useState } from "react"; |
| #2 | import { useNavigate, Link } from "react-router-dom"; |
| #3 | import { useAuth } from "../contexts/AuthContext"; |
| #4 | import { useToast } from "../components/Toast"; |
| #5 | import { ShieldCheck, LogIn, UserPlus, ArrowLeft } from "lucide-react"; |
| #6 | |
| #7 | type Mode = "login" | "signup"; |
| #8 | |
| #9 | export default function AdminLogin() { |
| #10 | const [mode, setMode] = useState<Mode>("login"); |
| #11 | const [email, setEmail] = useState(""); |
| #12 | const [password, setPassword] = useState(""); |
| #13 | const [confirmPassword, setConfirmPassword] = useState(""); |
| #14 | const [loading, setLoading] = useState(false); |
| #15 | const { login, signup, user } = useAuth(); |
| #16 | const { addToast } = useToast(); |
| #17 | const navigate = useNavigate(); |
| #18 | |
| #19 | if (user) { |
| #20 | navigate("/admin/dashboard", { replace: true }); |
| #21 | return null; |
| #22 | } |
| #23 | |
| #24 | const handleSubmit = async (e: React.FormEvent) => { |
| #25 | e.preventDefault(); |
| #26 | setLoading(true); |
| #27 | |
| #28 | try { |
| #29 | if (mode === "signup") { |
| #30 | if (password !== confirmPassword) { |
| #31 | addToast("Passwords do not match.", "error"); |
| #32 | setLoading(false); |
| #33 | return; |
| #34 | } |
| #35 | if (password.length < 6) { |
| #36 | addToast("Password must be at least 6 characters.", "error"); |
| #37 | setLoading(false); |
| #38 | return; |
| #39 | } |
| #40 | await signup(email, password); |
| #41 | addToast("Account created! You're now signed in.", "success"); |
| #42 | } else { |
| #43 | await login(email, password); |
| #44 | addToast("Logged in successfully!", "success"); |
| #45 | } |
| #46 | navigate("/admin/dashboard"); |
| #47 | } catch (err: unknown) { |
| #48 | const msg = err instanceof Error ? err.message : "Authentication failed"; |
| #49 | if (msg.includes("auth/email-already-in-use")) { |
| #50 | addToast("An account with this email already exists. Try signing in.", "error"); |
| #51 | } else if (msg.includes("auth/invalid-credential") || msg.includes("auth/wrong-password") || msg.includes("auth/user-not-found")) { |
| #52 | addToast("Invalid email or password.", "error"); |
| #53 | } else if (msg.includes("auth/weak-password")) { |
| #54 | addToast("Password is too weak. Use at least 6 characters.", "error"); |
| #55 | } else if (msg.includes("auth/invalid-email")) { |
| #56 | addToast("Please enter a valid email address.", "error"); |
| #57 | } else if (msg.includes("permission-denied")) { |
| #58 | addToast("Access denied — check Firestore security rules.", "error"); |
| #59 | } else { |
| #60 | addToast(msg, "error"); |
| #61 | } |
| #62 | } finally { |
| #63 | setLoading(false); |
| #64 | } |
| #65 | }; |
| #66 | |
| #67 | const switchMode = () => { |
| #68 | setMode((m) => (m === "login" ? "signup" : "login")); |
| #69 | setConfirmPassword(""); |
| #70 | }; |
| #71 | |
| #72 | return ( |
| #73 | <div className="min-h-screen flex items-center justify-center bg-gray-100 px-4"> |
| #74 | <div className="w-full max-w-md"> |
| #75 | {/* Back to home */} |
| #76 | <Link |
| #77 | to="/" |
| #78 | className="inline-flex items-center gap-1.5 text-sm text-gray-500 hover:text-gray-700 mb-6 transition-colors" |
| #79 | > |
| #80 | <ArrowLeft className="w-3.5 h-3.5" /> |
| #81 | Back to Home |
| #82 | </Link> |
| #83 | |
| #84 | <div className="text-center mb-8"> |
| #85 | <div className="inline-flex items-center justify-center w-14 h-14 rounded-2xl bg-emerald-100 mb-4"> |
| #86 | <ShieldCheck className="w-7 h-7 text-emerald-600" /> |
| #87 | </div> |
| #88 | <h1 className="text-2xl font-bold text-gray-900"> |
| #89 | {mode === "login" ? "Admin Login" : "Create Admin Account"} |
| #90 | </h1> |
| #91 | <p className="text-sm text-gray-500 mt-1"> |
| #92 | {mode === "login" |
| #93 | ? "Sign in to manage certificates" |
| #94 | : "Set up your admin account to start issuing certificates"} |
| #95 | </p> |
| #96 | </div> |
| #97 | |
| #98 | <form onSubmit={handleSubmit} className="bg-white rounded-2xl shadow-sm border border-gray-200 p-8 space-y-5"> |
| #99 | <div> |
| #100 | <label className="block text-sm font-medium text-gray-700 mb-1.5">Email</label> |
| #101 | <input |
| #102 | type="email" |
| #103 | value={email} |
| #104 | onChange={(e) => setEmail(e.target.value)} |
| #105 | required |
| #106 | className="w-full px-4 py-2.5 border border-gray-300 rounded-lg text-sm focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500" |
| #107 | placeholder="admin@example.com" |
| #108 | /> |
| #109 | </div> |
| #110 | <div> |
| #111 | <label className="block text-sm font-medium text-gray-700 mb-1.5">Password</label> |
| #112 | <input |
| #113 | type="password" |
| #114 | value={password} |
| #115 | onChange={(e) => setPassword(e.target.value)} |
| #116 | required |
| #117 | className="w-full px-4 py-2.5 border border-gray-300 rounded-lg text-sm focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500" |
| #118 | placeholder={mode === "signup" ? "At least 6 characters" : "••••••••"} |
| #119 | /> |
| #120 | </div> |
| #121 | {mode === "signup" && ( |
| #122 | <div> |
| #123 | <label className="block text-sm font-medium text-gray-700 mb-1.5">Confirm Password</label> |
| #124 | <input |
| #125 | type="password" |
| #126 | value={confirmPassword} |
| #127 | onChange={(e) => setConfirmPassword(e.target.value)} |
| #128 | required |
| #129 | className="w-full px-4 py-2.5 border border-gray-300 rounded-lg text-sm focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500" |
| #130 | placeholder="Re-enter password" |
| #131 | /> |
| #132 | </div> |
| #133 | )} |
| #134 | <button |
| #135 | type="submit" |
| #136 | disabled={loading} |
| #137 | className="w-full flex items-center justify-center gap-2 px-4 py-3 bg-emerald-600 hover:bg-emerald-700 disabled:opacity-50 text-white font-semibold rounded-lg transition-colors text-sm" |
| #138 | > |
| #139 | {loading ? ( |
| #140 | <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white" /> |
| #141 | ) : mode === "login" ? ( |
| #142 | <> |
| #143 | <LogIn className="w-4 h-4" /> Sign In |
| #144 | </> |
| #145 | ) : ( |
| #146 | <> |
| #147 | <UserPlus className="w-4 h-4" /> Create Account |
| #148 | </> |
| #149 | )} |
| #150 | </button> |
| #151 | |
| #152 | <div className="text-center pt-2 border-t border-gray-100"> |
| #153 | <button |
| #154 | type="button" |
| #155 | onClick={switchMode} |
| #156 | className="text-sm text-emerald-600 hover:text-emerald-700 font-medium transition-colors" |
| #157 | > |
| #158 | {mode === "login" |
| #159 | ? "Don't have an account? Sign up" |
| #160 | : "Already have an account? Sign in"} |
| #161 | </button> |
| #162 | </div> |
| #163 | </form> |
| #164 | </div> |
| #165 | </div> |
| #166 | ); |
| #167 | } |
| #168 |