Spaces:
Sleeping
Sleeping
| import { useState, useEffect, ChangeEvent, CSSProperties, ReactNode } from "react"; | |
| import { useNavigate } from "react-router-dom"; | |
| import { useAuth } from "../../lib/authContext"; | |
| import { getApiBaseUrl } from "../../lib/api/config"; | |
| // ββ Types βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| type StatusType = "success" | "error" | "loading"; | |
| interface Status { | |
| text: string; | |
| type: StatusType; | |
| } | |
| interface StudentProfileForm { | |
| birthDate: string; | |
| birthTime: string; | |
| country: string; | |
| gender: string; | |
| phone: string; | |
| quranLevel: string; | |
| preferredLanguage: string; | |
| preferredAccent: string; | |
| profilePhotoUrl: string; | |
| } | |
| interface StudentProfileBody { | |
| birthDateTime: string; | |
| country: string; | |
| gender: string; | |
| phone: string | null; | |
| quranLevel: string | null; | |
| preferredLanguage: string | null; | |
| preferredAccent: string | null; | |
| profilePhotoUrl: string | null; | |
| } | |
| interface FormGroupProps { | |
| label: string; | |
| required?: boolean; | |
| children: ReactNode; | |
| } | |
| // ββ Constants βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const BACKEND_URL = `${getApiBaseUrl()}/api/auth/complete-google-profile`; | |
| const COUNTRIES: string[] = [ | |
| "Egypt", "Saudi Arabia", "UAE", "Jordan", "Kuwait", "Qatar", | |
| "Bahrain", "Oman", "Libya", "Tunisia", "Morocco", "Algeria", "Sudan", "Other", | |
| ]; | |
| const STATUS_COLORS: Record<StatusType, { bg: string; color: string }> = { | |
| success: { bg: "#d4edda", color: "#155724" }, | |
| error: { bg: "#f8d7da", color: "#721c24" }, | |
| loading: { bg: "#fff3cd", color: "#856404" }, | |
| }; | |
| // ββ Sub-component βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function FormGroup({ label, required = false, children }: FormGroupProps) { | |
| return ( | |
| <div style={{ marginBottom: 16, flex: 1 }}> | |
| <label style={styles.label}> | |
| {label} {required && <span style={{ color: "#e53935" }}>*</span>} | |
| </label> | |
| {children} | |
| </div> | |
| ); | |
| } | |
| // ββ Component βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| export default function CompleteStudentProfile() { | |
| const navigate = useNavigate(); | |
| const { updateProfile } = useAuth(); | |
| const [hasToken, setHasToken] = useState<boolean>(true); | |
| const [loading, setLoading] = useState<boolean>(false); | |
| const [status, setStatus] = useState<Status | null>(null); | |
| const [response, setResponse] = useState<string | null>(null); | |
| const [form, setForm] = useState<StudentProfileForm>({ | |
| birthDate: "", | |
| birthTime: "00:00", | |
| country: "", | |
| gender: "", | |
| phone: "", | |
| quranLevel: "", | |
| preferredLanguage: "", | |
| preferredAccent: "", | |
| profilePhotoUrl: "", | |
| }); | |
| useEffect(() => { | |
| setHasToken(!!localStorage.getItem("authToken")); | |
| }, []); | |
| function handleChange(e: ChangeEvent<HTMLInputElement | HTMLSelectElement>): void { | |
| const { id, value } = e.target; | |
| setForm((prev) => ({ ...prev, [id]: value })); | |
| } | |
| async function submitProfile(): Promise<void> { | |
| const jwt = localStorage.getItem("authToken"); | |
| if (!jwt) { setStatus({ text: "β No authentication token found. Please sign in first.", type: "error" }); return; } | |
| if (!form.birthDate){ setStatus({ text: "β Birth date is required", type: "error" }); return; } | |
| if (!form.country) { setStatus({ text: "β Country is required", type: "error" }); return; } | |
| if (!form.gender) { setStatus({ text: "β Gender is required", type: "error" }); return; } | |
| const birthDateTime = `${form.birthDate}T${form.birthTime || "00:00"}:00`; | |
| const body: StudentProfileBody = { | |
| birthDateTime, | |
| country: form.country, | |
| gender: form.gender, | |
| phone: form.phone || null, | |
| quranLevel: form.quranLevel || null, | |
| preferredLanguage: form.preferredLanguage || null, | |
| preferredAccent: form.preferredAccent || null, | |
| profilePhotoUrl: form.profilePhotoUrl || null, | |
| }; | |
| setLoading(true); | |
| setStatus({ text: "β³ Saving profile...", type: "loading" }); | |
| try { | |
| const res = await fetch(BACKEND_URL, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json", Authorization: "Bearer " + jwt }, | |
| body: JSON.stringify(body), | |
| }); | |
| const text = await res.text(); | |
| setResponse(text); | |
| if (res.ok) { | |
| setStatus({ text: "β Profile completed successfully!", type: "success" }); | |
| updateProfile({ profileCompleted: true }); | |
| setTimeout(() => navigate("/student/dashboard", { replace: true }), 1000); | |
| } else { | |
| setStatus({ text: "β Error: " + text, type: "error" }); | |
| setLoading(false); | |
| } | |
| } catch (err: unknown) { | |
| const message = err instanceof Error ? err.message : "Unknown error"; | |
| setStatus({ text: "β Failed: " + message, type: "error" }); | |
| setLoading(false); | |
| } | |
| } | |
| return ( | |
| <div style={styles.page}> | |
| <div style={styles.container}> | |
| {/* Header */} | |
| <div style={styles.header}> | |
| <div style={styles.icon}>π</div> | |
| <h1 style={styles.h1}>Complete Student Profile</h1> | |
| <p style={styles.subtitle}>Fill in the remaining details to get started</p> | |
| </div> | |
| <div style={{ textAlign: "center", marginBottom: 16 }}> | |
| <span style={styles.roleBadgeStudent}>STUDENT</span> | |
| </div> | |
| {/* No token warning */} | |
| {!hasToken && ( | |
| <div style={styles.warning}> | |
| β οΈ No JWT found. Please{" "} | |
| <a href="/auth/google" style={styles.link}>sign in with Google first</a>. | |
| </div> | |
| )} | |
| {/* Personal Info */} | |
| <div style={styles.sectionTitle}>π€ Personal Information</div> | |
| <FormGroup label="Birth Date" required> | |
| <input type="date" id="birthDate" value={form.birthDate} onChange={handleChange} style={styles.input} /> | |
| </FormGroup> | |
| <div style={styles.formRow}> | |
| <FormGroup label="Country" required> | |
| <select id="country" value={form.country} onChange={handleChange} style={styles.input}> | |
| <option value="">Select country...</option> | |
| {COUNTRIES.map((c) => <option key={c} value={c}>{c}</option>)} | |
| </select> | |
| </FormGroup> | |
| <FormGroup label="Gender" required> | |
| <select id="gender" value={form.gender} onChange={handleChange} style={styles.input}> | |
| <option value="">Select gender...</option> | |
| <option value="Male">Male</option> | |
| <option value="Female">Female</option> | |
| </select> | |
| </FormGroup> | |
| </div> | |
| <FormGroup label="Phone Number"> | |
| <input type="tel" id="phone" value={form.phone} onChange={handleChange} placeholder="e.g. 01012345678" style={styles.input} /> | |
| </FormGroup> | |
| {/* Quran Profile */} | |
| <div style={styles.sectionTitle}>π Quran Profile</div> | |
| <FormGroup label="Quran Level"> | |
| <select id="quranLevel" value={form.quranLevel} onChange={handleChange} style={styles.input}> | |
| <option value="">Select level...</option> | |
| <option value="BEGINNER">Beginner β Just starting</option> | |
| <option value="INTERMEDIATE">Intermediate β Can read with help</option> | |
| <option value="ADVANCED">Advanced β Reads fluently</option> | |
| <option value="HAFIZ">Hafiz β Memorized Quran</option> | |
| </select> | |
| </FormGroup> | |
| <div style={styles.formRow}> | |
| <FormGroup label="Preferred Language"> | |
| <select id="preferredLanguage" value={form.preferredLanguage} onChange={handleChange} style={styles.input}> | |
| <option value="">Select language...</option> | |
| {["Arabic", "English", "French", "Urdu", "Turkish", "Indonesian"].map((l) => ( | |
| <option key={l} value={l}>{l}</option> | |
| ))} | |
| </select> | |
| </FormGroup> | |
| <FormGroup label="Preferred Accent"> | |
| <select id="preferredAccent" value={form.preferredAccent} onChange={handleChange} style={styles.input}> | |
| <option value="">Select accent...</option> | |
| {["Egyptian", "Saudi", "Khaleeji", "Levantine", "Moroccan"].map((a) => ( | |
| <option key={a} value={a}>{a}</option> | |
| ))} | |
| </select> | |
| </FormGroup> | |
| </div> | |
| <FormGroup label="Profile Photo URL"> | |
| <input type="url" id="profilePhotoUrl" value={form.profilePhotoUrl} onChange={handleChange} placeholder="https://... (leave empty to keep Google photo)" style={styles.input} /> | |
| </FormGroup> | |
| <button | |
| style={{ ...styles.submitBtn, ...(loading ? styles.submitBtnDisabled : {}) }} | |
| onClick={submitProfile} | |
| disabled={loading} | |
| > | |
| β Complete Profile | |
| </button> | |
| {status && ( | |
| <div style={{ ...styles.statusBadge, background: STATUS_COLORS[status.type].bg, color: STATUS_COLORS[status.type].color }}> | |
| {status.text} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } | |
| // ββ Styles ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const styles: Record<string, CSSProperties> = { | |
| page: { fontFamily: "'Segoe UI', sans-serif", background: "#f0f4f8", minHeight: "100vh", display: "flex", alignItems: "center", justifyContent: "center", padding: "40px 20px" }, | |
| container: { background: "white", borderRadius: 16, padding: 40, width: 600, boxShadow: "0 4px 24px rgba(0,0,0,0.10)" }, | |
| header: { textAlign: "center", marginBottom: 32 }, | |
| icon: { fontSize: 48, marginBottom: 12 }, | |
| h1: { color: "#1a6b3c", fontSize: 24, marginBottom: 6 }, | |
| subtitle: { color: "#777", fontSize: 14 }, | |
| roleBadgeStudent: { display: "inline-block", background: "#e8f5e9", color: "#1a6b3c", padding: "4px 12px", borderRadius: 20, fontSize: 12, fontWeight: 700, marginBottom: 16 }, | |
| warning: { background: "#fff3cd", border: "1px solid #ffc107", borderRadius: 8, padding: "12px 16px", fontSize: 13, color: "#856404", marginBottom: 20 }, | |
| link: { color: "#1a6b3c", fontWeight: 600 }, | |
| sectionTitle: { fontSize: 13, fontWeight: 700, color: "#1a6b3c", textTransform: "uppercase", letterSpacing: 1, margin: "24px 0 12px", paddingBottom: 6, borderBottom: "2px solid #e8f5e9" }, | |
| formRow: { display: "flex", gap: 16 }, | |
| label: { display: "block", fontSize: 13, fontWeight: 600, color: "#444", marginBottom: 6 }, | |
| input: { width: "100%", padding: "11px 14px", border: "1.5px solid #ddd", borderRadius: 8, fontSize: 14, color: "#333", outline: "none", background: "white", fontFamily: "inherit", boxSizing: "border-box" }, | |
| submitBtn: { width: "100%", padding: 14, background: "#1a6b3c", color: "white", border: "none", borderRadius: 10, fontSize: 16, fontWeight: 600, cursor: "pointer", marginTop: 24 }, | |
| submitBtnDisabled: { background: "#aaa", cursor: "not-allowed" }, | |
| statusBadge: { padding: "12px 16px", borderRadius: 8, fontSize: 14, fontWeight: 500, marginTop: 16, textAlign: "center" }, | |
| responseBox: { background: "#1e1e1e", borderRadius: 10, padding: 16, marginTop: 16 }, | |
| responseTitle: { color: "#4ec9b0", fontSize: 12, marginBottom: 8, textTransform: "uppercase" }, | |
| responsePre: { color: "#d4d4d4", fontSize: 12, fontFamily: "'Courier New', monospace", whiteSpace: "pre-wrap", wordBreak: "break-all" }, | |
| }; | |