/* global React, window, Icon, Mascot, useT, useEyad, PORTAL_USER, PORTAL_SESSIONS, PORTAL_PROGRESS, PORTAL_BADGES, PORTAL_CERTS, PORTAL_LEADERS, PORTAL_REPORTS, PORTAL_INSIGHTS_AR, PORTAL_INSIGHTS_EN, SUBJECTS, ThemeIcon, CourseCover, TeacherPortrait */ // Eyad Academy — Student portal shell + Dashboard + Sessions views const { useState, useEffect, useMemo, useRef } = React; // ───────────────────────────────────────────────────────────── // Utilities // ───────────────────────────────────────────────────────────── function pickName(rec, lang) { if (rec && typeof rec === "object" && "ar" in rec && "en" in rec) return rec[lang]; return rec; } function subjectOf(id) { return (window.SUBJECTS || []).find(s => s.id === id) || {}; } function subjectAccent(id) { return ({ quran: "#8B7AD9", arabic: "#D9A441", math: "#6FA8DC", science: "#E89B8B", })[id] || "#D9A441"; } function formatHourLabel(h, lang) { // h is 24-hr int — show "5:00 م" or "5:00 PM" const period = h >= 12 ? (lang === "ar" ? "م" : "PM") : (lang === "ar" ? "ص" : "AM"); const hh = ((h + 11) % 12) + 1; return `${hh}:00 ${period}`; } function daysFromNowLabel(offset, lang) { if (offset === 0) return lang === "ar" ? "اليوم" : "Today"; if (offset === 1) return lang === "ar" ? "غدًا" : "Tomorrow"; if (offset === -1) return lang === "ar" ? "أمس" : "Yesterday"; if (offset > 0) return (lang === "ar" ? "بعد " : "in ") + offset + (lang === "ar" ? " أيام" : " days"); return (lang === "ar" ? "قبل " : "") + Math.abs(offset) + (lang === "ar" ? " أيام" : " days ago"); } function greetingKey() { const h = new Date().getHours(); if (h < 12) return "morning"; if (h < 18) return "afternoon"; return "evening"; } // ───────────────────────────────────────────────────────────── // PORTAL SHELL // ───────────────────────────────────────────────────────────── function Portal({ onExitToSite, onLogout }) { const t = useT(); const { lang, theme, setLang, setTheme } = useEyad(); const [view, setView] = useState("dashboard"); // current view key const [mobileNavOpen, setMobileNavOpen] = useState(false); const [openSession, setOpenSession] = useState(null); const [logoutConfirm, setLogoutConfirm] = useState(false); // Esc closes mobile nav / session detail useEffect(() => { const onKey = (e) => { if (e.key !== "Escape") return; if (openSession) setOpenSession(null); else if (mobileNavOpen) setMobileNavOpen(false); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [mobileNavOpen, openSession]); const navItems = [ { id: "dashboard", icon: "dashIcon", label: t.portal.nav.dashboard }, { id: "sessions", icon: "playIcon", label: t.portal.nav.sessions }, { id: "timetable", icon: "calIcon", label: t.portal.nav.timetable }, { id: "progress", icon: "chartIcon", label: t.portal.nav.progress }, { id: "certificates", icon: "certIcon", label: t.portal.nav.certificates }, { id: "gamification", icon: "starIcon", label: t.portal.nav.gamification }, { id: "reports", icon: "reportIcon", label: t.portal.nav.reports }, { id: "settings", icon: "cogIcon", label: t.portal.nav.settings }, ]; const goView = (v) => { setView(v); setMobileNavOpen(false); window.scrollTo({ top: 0, behavior: "smooth" }); }; return (
{mobileNavOpen &&
setMobileNavOpen(false)}/>}
{t.portal.topbar.level} {PORTAL_USER.level} {PORTAL_USER.xp.toLocaleString()}
{view === "dashboard" && } {view === "sessions" && } {view === "timetable" && } {view === "progress" && } {view === "certificates" && } {view === "gamification" && } {view === "reports" && } {view === "settings" && }
{openSession && setOpenSession(null)}/>} {logoutConfirm && setLogoutConfirm(false)}/>}
); } // ───────────────────────────────────────────────────────────── // DASHBOARD // ───────────────────────────────────────────────────────────── function DashboardView({ onOpenSession, onGo }) { const t = useT(); const { lang } = useEyad(); const greeting = t.portal.greeting[greetingKey()]; const insights = lang === "ar" ? PORTAL_INSIGHTS_AR : PORTAL_INSIGHTS_EN; // Next session = upcoming with smallest dayOffset, else first const upcoming = PORTAL_SESSIONS .filter(s => s.dayOffset >= 0) .sort((a, b) => a.dayOffset - b.dayOffset || a.startHour - b.startHour); const next = upcoming[0]; const todayTomorrow = upcoming.filter(s => s.dayOffset <= 1).slice(0, 4); return ( <>
{greeting}

{pickName(PORTAL_USER.name, lang).split(" ")[0]} ✨

{t.portal.dashboard.sub}

{/* Next session — featured */} {next && (
)} {/* Streak */}
{t.portal.dashboard.streak.kicker}
{PORTAL_USER.streak}{t.portal.dashboard.streak.days}
{t.portal.dashboard.streak.motivate}
{/* XP */}
{t.portal.dashboard.xp.kicker}
{t.portal.dashboard.xp.level} {PORTAL_USER.level}
{PORTAL_USER.xp.toLocaleString()}/{PORTAL_USER.xpNext.toLocaleString()}
{(PORTAL_USER.xpNext - PORTAL_USER.xp).toLocaleString()} {t.portal.topbar.xp} {t.portal.dashboard.xp.next}
{/* This week */}
{t.portal.dashboard.week.kicker}
5 {t.portal.dashboard.week.attended} 5 {t.portal.dashboard.week.lessons}
6.5 {t.portal.dashboard.week.h}
{/* Insights */}
{t.portal.dashboard.insights.h}
    {insights.map((it, i) => (
  • {it.text}
  • ))}
{/* Upcoming list */}
{t.portal.dashboard.upcoming.h}
{todayTomorrow.length === 0 ?
{t.portal.dashboard.upcoming.empty}
: (
    {todayTomorrow.map((s) => (
  • onOpenSession(s)}>
    {lang === "ar" ? s.titleAr : s.titleEn}
    {daysFromNowLabel(s.dayOffset, lang)} · {formatHourLabel(s.startHour, lang)} · {pickName({ ar: s.teacher.name_ar, en: s.teacher.name_en }, lang)}
  • ))}
)}
{/* Progress glance */}
{t.portal.dashboard.progressGlance.h}
    {PORTAL_PROGRESS.subjects.map((p) => { const subj = subjectOf(p.id); const pct = Math.round((p.completed / p.total) * 100); return (
  • {t.subjects.items[p.id]?.name} {pct}%
  • ); })}
{/* Badges glance */}
{t.portal.dashboard.badges.h}
{PORTAL_BADGES.filter(b => b.earned).slice(0, 4).map((b) => (
{lang === "ar" ? b.name_ar : b.name_en}
))}
{/* Motivation */}
{t.portal.dashboard.motivate.h}

{t.portal.dashboard.motivate.body}

); } function NextSessionCard({ session, onOpen }) { const t = useT(); const { lang } = useEyad(); const subj = subjectOf(session.subject); const accent = subjectAccent(session.subject); const isNow = session.dayOffset === 0; return (
{t.portal.dashboard.next.kicker}

{lang === "ar" ? session.titleAr : session.titleEn}

{t.portal.dashboard.next.with} {pickName({ ar: session.teacher.name_ar, en: session.teacher.name_en }, lang)} {daysFromNowLabel(session.dayOffset, lang)} · {formatHourLabel(session.startHour, lang)}
{t.portal.dashboard.next.meetingId} {session.zoomMeetingId}
); } // ───────────────────────────────────────────────────────────── // SESSIONS LIST // ───────────────────────────────────────────────────────────── function SessionsView({ onOpenSession }) { const t = useT(); const { lang } = useEyad(); const [tab, setTab] = useState("upcoming"); const list = useMemo(() => PORTAL_SESSIONS .filter(s => tab === "upcoming" ? s.dayOffset >= 0 : s.dayOffset < 0) .sort((a, b) => tab === "upcoming" ? a.dayOffset - b.dayOffset || a.startHour - b.startHour : b.dayOffset - a.dayOffset), [tab]); return ( <>
{list.length === 0 ?
{t.portal.sessions.empty[tab]}
: (
{list.map((s) => ( ))}
)} ); } function SessionCard({ session, onOpen }) { const t = useT(); const { lang } = useEyad(); const accent = subjectAccent(session.subject); const subj = subjectOf(session.subject); const isUpcoming = session.dayOffset >= 0; return (
onOpen(session)} style={{ "--accent": accent }}>
{t.subjects.items[session.subject]?.name}

{lang === "ar" ? session.titleAr : session.titleEn}

{pickName({ ar: session.teacher.name_ar, en: session.teacher.name_en }, lang)} {daysFromNowLabel(session.dayOffset, lang)} · {formatHourLabel(session.startHour, lang)} · {session.durationMin} {lang === "ar" ? "د" : "min"}
{isUpcoming ? ( ) : (
{t.portal.sessions.grade} {lang === "ar" ? session.grade : session.grade_en || "—"}
)}
); } // ───────────────────────────────────────────────────────────── // SESSION DETAIL MODAL — with Zoom join confirm + file upload // ───────────────────────────────────────────────────────────── function SessionDetailModal({ session, onClose }) { const t = useT(); const { lang } = useEyad(); const accent = subjectAccent(session.subject); const [confirmJoin, setConfirmJoin] = useState(false); const [files, setFiles] = useState(session.materials || []); const fileInputRef = useRef(null); useEffect(() => { document.body.style.overflow = "hidden"; return () => { document.body.style.overflow = ""; }; }, []); const onFileChosen = (e) => { const f = e.target.files?.[0]; if (!f) return; const ext = (f.name.split(".").pop() || "").toLowerCase(); const kind = ["pdf"].includes(ext) ? "pdf" : ["mp3", "wav", "m4a"].includes(ext) ? "audio" : ["jpg", "jpeg", "png", "gif", "webp"].includes(ext) ? "image" : "file"; const sizeMb = (f.size / (1024 * 1024)).toFixed(1) + " MB"; setFiles([...files, { id: `up-${Date.now()}`, name_ar: f.name, name_en: f.name, kind, size: sizeMb, uploaded_by: "you" }]); e.target.value = ""; }; const removeFile = (id) => setFiles(files.filter(f => f.id !== id)); return (
e.stopPropagation()} style={{ "--accent": accent }}>
{t.subjects.items[session.subject]?.name} · {daysFromNowLabel(session.dayOffset, lang)} · {formatHourLabel(session.startHour, lang)}

{lang === "ar" ? session.titleAr : session.titleEn}

{t.portal.sessions.with} {pickName({ ar: session.teacher.name_ar, en: session.teacher.name_en }, lang)}
{t.portal.sessions.time} {formatHourLabel(session.startHour, lang)} · {session.durationMin} {lang === "ar" ? "دقيقة" : "min"}
{t.portal.sessions.date} {daysFromNowLabel(session.dayOffset, lang)}
{t.portal.sessions.zoomId} {session.zoomMeetingId}
{session.status !== "completed" && ( )} {session.status === "completed" && (
{t.portal.sessions.grade}: {lang === "ar" ? session.grade : session.grade_en}
)}

{t.portal.sessions.materials}

{files.length === 0 &&
{lang === "ar" ? "لا توجد ملفات بعد." : "No materials yet."}
}
    {files.map((f) => (
  • {lang === "ar" ? f.name_ar : f.name_en}
    {f.size} · {f.uploaded_by === "you" ? t.portal.sessions.uploadedYou : t.portal.sessions.uploadedTeacher}
    {f.uploaded_by === "you" && ( )}
  • ))}
{t.portal.sessions.addHint}
{session.notes_ar && session.notes_ar !== "—" && session.notes_ar !== "" && (

{t.portal.sessions.notes}

{lang === "ar" ? session.notes_ar : session.notes_en}

)} {session.status !== "completed" && (

{t.portal.sessions.chat}

{t.portal.sessions.chatHint}
)}
{confirmJoin && (
e.stopPropagation()}>

{t.portal.sessions.confirmJoin.h}

{t.portal.sessions.confirmJoin.body}

{session.zoomMeetingId}
)}
); } // ───────────────────────────────────────────────────────────── // LOGOUT CONFIRM // ───────────────────────────────────────────────────────────── function LogoutConfirm({ onConfirm, onCancel }) { const t = useT(); return (
e.stopPropagation()}>

{t.portal.logoutModal.h}

{t.portal.logoutModal.body}

); } // ───────────────────────────────────────────────────────────── // Shared atoms // ───────────────────────────────────────────────────────────── function PageHead({ h, sub }) { return (

{h}

{sub}

); } function MiniSparkline({ data }) { const max = Math.max(...data); const w = 220, hh = 56, pad = 4; const step = (w - pad * 2) / (data.length - 1); const pts = data.map((v, i) => [pad + i * step, pad + (1 - v / max) * (hh - pad * 2)]); const d = pts.map((p, i) => `${i === 0 ? "M" : "L"} ${p[0]} ${p[1]}`).join(" "); return ( {pts.map((p, i) => )} ); } // ───────────────────────────────────────────────────────────── // Portal-specific icons // ───────────────────────────────────────────────────────────── function PortalIcon({ name }) { const s = { width: 18, height: 18, display: "inline-block", flexShrink: 0 }; const sk = { fill: "none", stroke: "currentColor", strokeWidth: 1.7, strokeLinecap: "round", strokeLinejoin: "round" }; switch (name) { case "dashIcon": return ; case "playIcon": return ; case "calIcon": return ; case "chartIcon": return ; case "certIcon": return ; case "starIcon": return ; case "reportIcon": return ; case "cogIcon": return ; case "homeIcon": return ; case "logoutIcon": return ; default: return null; } } function SearchIcon() { return (); } function BellIcon() { return (); } function FlameIcon() { return ( ); } function ZoomGlyph({ small = false }) { const sz = small ? 16 : 18; return ( ); } function ZoomBigIcon() { return (
); } function DownloadIcon() { return (); } function PaperclipIcon() { return (); } function ChatIcon() { return (); } function FileTypeIcon({ kind }) { const bg = kind === "pdf" ? "#E89B8B" : kind === "audio" ? "#8B7AD9" : kind === "image" ? "#6FA8DC" : "#D9A441"; const lbl = kind === "pdf" ? "PDF" : kind === "audio" ? "♪" : kind === "image" ? "IMG" : "FILE"; return (
{lbl}
); } Object.assign(window, { Portal, DashboardView, SessionsView, SessionDetailModal, NextSessionCard, pickName, subjectOf, subjectAccent, formatHourLabel, daysFromNowLabel, PageHead });