Quran_Tech_Server / F_Pro /src /components /auth /CompleteStudentProfile.tsx
aboalaa147's picture
Initial deployment
eb6a2f9
Raw
History Blame Contribute Delete
12.2 kB
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" },
};