/* 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)}/>}
{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" && (
)}
))}
{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 (
);
}
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 (
);
}
// ─────────────────────────────────────────────────────────────
// 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 });