repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
Pan-African Youth Internship,…
stars
latest
clone command
git clone gitlawb://did:key:z6MkmNFz...XP9B/pan-african-you...git clone gitlawb://did:key:z6MkmNFz.../pan-african-you...0b401a18sync from playground3h ago| #1 | import { useState, type ChangeEvent, type FormEvent } from "react"; |
| #2 | import { supabase } from "./supabaseClient"; |
| #3 | |
| #4 | interface LoginPageProps { |
| #5 | onBack: () => void; |
| #6 | onYouthLogin: (userId: string) => void; |
| #7 | onOrgLogin: (userId: string) => void; |
| #8 | onAdminLogin: (userId: string) => void; |
| #9 | onYouthRegister: () => void; |
| #10 | onOrgRegister: () => void; |
| #11 | adminOnly?: boolean; |
| #12 | } |
| #13 | |
| #14 | export default function LoginPage({ onBack, onYouthLogin, onOrgLogin, onAdminLogin, onYouthRegister, onOrgRegister, adminOnly }: LoginPageProps) { |
| #15 | const [loginType, setLoginType] = useState<"youth" | "organisation" | "admin">(adminOnly ? "admin" : "youth"); |
| #16 | const [email, setEmail] = useState(""); |
| #17 | const [password, setPassword] = useState(""); |
| #18 | const [error, setError] = useState(""); |
| #19 | const [showPassword, setShowPassword] = useState(false); |
| #20 | const [loading, setLoading] = useState(false); |
| #21 | |
| #22 | const handleSubmit = async (e: FormEvent) => { |
| #23 | e.preventDefault(); |
| #24 | setError(""); |
| #25 | |
| #26 | if (!email || !password) { |
| #27 | setError("Please fill in all fields."); |
| #28 | return; |
| #29 | } |
| #30 | |
| #31 | if (!email.includes("@")) { |
| #32 | setError("Please enter a valid email address."); |
| #33 | return; |
| #34 | } |
| #35 | |
| #36 | if (password.length < 8) { |
| #37 | setError("Password must be at least 8 characters."); |
| #38 | return; |
| #39 | } |
| #40 | |
| #41 | setLoading(true); |
| #42 | |
| #43 | try { |
| #44 | const { data, error: authError } = await supabase.auth.signInWithPassword({ |
| #45 | email, |
| #46 | password, |
| #47 | }); |
| #48 | |
| #49 | if (authError) { |
| #50 | setError(authError.message); |
| #51 | setLoading(false); |
| #52 | return; |
| #53 | } |
| #54 | |
| #55 | const userId = data.user?.id; |
| #56 | if (!userId) { |
| #57 | setError("Login failed — no user ID returned."); |
| #58 | setLoading(false); |
| #59 | return; |
| #60 | } |
| #61 | |
| #62 | // Determine user type by checking which profile table has their user_id |
| #63 | if (loginType === "admin") { |
| #64 | const { data: adminProfile } = await supabase |
| #65 | .from("admin_users") |
| #66 | .select("user_id") |
| #67 | .eq("user_id", userId) |
| #68 | .single(); |
| #69 | |
| #70 | if (adminProfile) { |
| #71 | sessionStorage.setItem("fursalink_user_type", "admin"); |
| #72 | sessionStorage.setItem("fursalink_user_id", userId); |
| #73 | onAdminLogin(userId); |
| #74 | } else { |
| #75 | setError("Unauthorized: You are not an admin"); |
| #76 | setLoading(false); |
| #77 | } |
| #78 | } else if (loginType === "youth") { |
| #79 | const { data: youthProfile } = await supabase |
| #80 | .from("youth_profiles") |
| #81 | .select("user_id") |
| #82 | .eq("user_id", userId) |
| #83 | .single(); |
| #84 | |
| #85 | if (youthProfile) { |
| #86 | sessionStorage.setItem("fursalink_user_type", "youth"); |
| #87 | sessionStorage.setItem("fursalink_user_id", userId); |
| #88 | onYouthLogin(userId); |
| #89 | } else { |
| #90 | setError("No youth profile found for this account. Try logging in as an organisation."); |
| #91 | setLoading(false); |
| #92 | } |
| #93 | } else { |
| #94 | const { data: orgProfile } = await supabase |
| #95 | .from("organisation_profiles") |
| #96 | .select("user_id") |
| #97 | .eq("user_id", userId) |
| #98 | .single(); |
| #99 | |
| #100 | if (orgProfile) { |
| #101 | sessionStorage.setItem("fursalink_user_type", "organisation"); |
| #102 | sessionStorage.setItem("fursalink_user_id", userId); |
| #103 | onOrgLogin(userId); |
| #104 | } else { |
| #105 | setError("No organisation profile found for this account. Try logging in as a youth user."); |
| #106 | setLoading(false); |
| #107 | } |
| #108 | } |
| #109 | } catch (err) { |
| #110 | const message = err instanceof Error ? err.message : "Login failed. Please try again."; |
| #111 | setError(message); |
| #112 | setLoading(false); |
| #113 | } |
| #114 | }; |
| #115 | |
| #116 | const handleGoogleLogin = async () => { |
| #117 | const { error: googleError } = await supabase.auth.signInWithOAuth({ |
| #118 | provider: "google", |
| #119 | options: { |
| #120 | redirectTo: window.location.origin, |
| #121 | }, |
| #122 | }); |
| #123 | if (googleError) { |
| #124 | console.error("Google login error:", googleError); |
| #125 | setError(googleError.message); |
| #126 | } |
| #127 | }; |
| #128 | |
| #129 | return ( |
| #130 | <div className="login-page"> |
| #131 | <nav className="nav"> |
| #132 | <div className="nav-inner"> |
| #133 | <a href="#" className="logo" onClick={(e) => { e.preventDefault(); onBack(); }}> |
| #134 | <span className="logo-icon"><img src="https://i.imgur.com/FT8aHGw.png" alt="FursaLink" /></span> |
| #135 | <span className="logo-text"> |
| #136 | Fursa<span className="logo-highlight">Link</span> |
| #137 | </span> |
| #138 | </a> |
| #139 | <div className="nav-actions"> |
| #140 | <button type="button" className="btn btn-outline btn-sm" onClick={onBack}> |
| #141 | ← Back to Home |
| #142 | </button> |
| #143 | </div> |
| #144 | </div> |
| #145 | </nav> |
| #146 | |
| #147 | <div className="login-container"> |
| #148 | <div className="login-card"> |
| #149 | <div className="login-header"> |
| #150 | <div className="login-icon"> |
| #151 | {loginType === "youth" ? "👤" : loginType === "admin" ? "🛡️" : "🏢"} |
| #152 | </div> |
| #153 | <h1 className="login-title">Welcome Back</h1> |
| #154 | <p className="login-subtitle"> |
| #155 | Sign in to your {loginType === "youth" ? "youth" : loginType === "admin" ? "admin" : "organisation"} account |
| #156 | </p> |
| #157 | </div> |
| #158 | |
| #159 | {/* Login Type Toggle */} |
| #160 | {!adminOnly && ( |
| #161 | <div className="login-type-toggle"> |
| #162 | <button |
| #163 | type="button" |
| #164 | className={`login-type-btn ${loginType === "youth" ? "login-type-active" : ""}`} |
| #165 | onClick={() => { setLoginType("youth"); setError(""); }} |
| #166 | > |
| #167 | <span className="login-type-icon">👤</span> |
| #168 | Youth User |
| #169 | </button> |
| #170 | <button |
| #171 | type="button" |
| #172 | className={`login-type-btn ${loginType === "organisation" ? "login-type-active" : ""}`} |
| #173 | onClick={() => { setLoginType("organisation"); setError(""); }} |
| #174 | > |
| #175 | <span className="login-type-icon">🏢</span> |
| #176 | Organisation |
| #177 | </button> |
| #178 | </div> |
| #179 | )} |
| #180 | |
| #181 | <form onSubmit={handleSubmit} className="login-form"> |
| #182 | {error && ( |
| #183 | <div className="login-error"> |
| #184 | <span className="login-error-icon">⚠️</span> |
| #185 | {error} |
| #186 | </div> |
| #187 | )} |
| #188 | |
| #189 | <div className="login-field"> |
| #190 | <label className="login-label">Email Address</label> |
| #191 | <div className="login-input-wrap"> |
| #192 | <span className="login-input-icon">✉️</span> |
| #193 | <input |
| #194 | type="email" |
| #195 | className="login-input" |
| #196 | value={email} |
| #197 | onChange={(e: ChangeEvent<HTMLInputElement>) => setEmail(e.target.value)} |
| #198 | placeholder="you@example.com" |
| #199 | autoComplete="email" |
| #200 | /> |
| #201 | </div> |
| #202 | </div> |
| #203 | |
| #204 | <div className="login-field"> |
| #205 | <label className="login-label">Password</label> |
| #206 | <div className="login-input-wrap"> |
| #207 | <span className="login-input-icon">🔒</span> |
| #208 | <input |
| #209 | type={showPassword ? "text" : "password"} |
| #210 | className="login-input" |
| #211 | value={password} |
| #212 | onChange={(e: ChangeEvent<HTMLInputElement>) => setPassword(e.target.value)} |
| #213 | placeholder="Enter your password" |
| #214 | autoComplete="current-password" |
| #215 | /> |
| #216 | <button |
| #217 | type="button" |
| #218 | className="login-password-toggle" |
| #219 | onClick={() => setShowPassword(!showPassword)} |
| #220 | aria-label={showPassword ? "Hide password" : "Show password"} |
| #221 | > |
| #222 | {showPassword ? "🙈" : "👁️"} |
| #223 | </button> |
| #224 | </div> |
| #225 | </div> |
| #226 | |
| #227 | <div className="login-options"> |
| #228 | <label className="login-remember"> |
| #229 | <input type="checkbox" className="login-checkbox" /> |
| #230 | <span>Remember me</span> |
| #231 | </label> |
| #232 | <a href="#" className="login-forgot">Forgot password?</a> |
| #233 | </div> |
| #234 | |
| #235 | <button type="submit" className="btn btn-primary btn-lg login-submit" disabled={loading}> |
| #236 | {loading ? "Signing In..." : "Sign In"} |
| #237 | </button> |
| #238 | </form> |
| #239 | |
| #240 | <div className="login-divider"> |
| #241 | <span>or</span> |
| #242 | </div> |
| #243 | |
| #244 | <div className="login-social"> |
| #245 | <button type="button" className="login-social-btn" title="Sign in with Google" onClick={handleGoogleLogin}> |
| #246 | <span className="login-social-icon">G</span> |
| #247 | |
| #248 | </button> |
| #249 | <button type="button" className="login-social-btn" title="Sign in with LinkedIn"> |
| #250 | <span className="login-social-icon">in</span> |
| #251 | |
| #252 | </button> |
| #253 | </div> |
| #254 | |
| #255 | <p className="login-register-link"> |
| #256 | Don't have an account?{" "} |
| #257 | <a href="#" onClick={(e) => { e.preventDefault(); loginType === "youth" ? onYouthRegister() : onOrgRegister(); }}> |
| #258 | Register here |
| #259 | </a> |
| #260 | </p> |
| #261 | </div> |
| #262 | </div> |
| #263 | </div> |
| #264 | ); |
| #265 | } |
| #266 |