Online Portfolio updated .txt
1
100%
Steven Barizo, MIT — Activity Portfolio
Steven Barizo, MIT
TEACHER MODE
Loading...
Reload Viewer
Fullscreen
Exit Teacher Mode
Select an activity with a Photopea or YouTube link to view it here.
Loading activity...
Live Grade Panel
Student —
Student ID —
Overall % —
College Grade —
Status —
Midterm Lab —
Finals Lab —
Completion —
© Steven Barizo, MIT — Authorized Use Only
/* ============================================
STUDENTS ONLY EDIT THIS PART
============================================ */
const STUDENT_NAME = "Medrano, L. Junmer";
const SECTION = "IT1H";
const SUBMISSION_ID = "26187054";
/* Official scores released by the instructor (raw values).
Quizzes are out of 50. Leave blank ("") if not yet released. */
const MIDTERM_SCORES = {
attendance: "", // 0-100
quiz1: "", // 0-50
quiz2: "",
quiz3: "",
quiz4: "",
exam: "" // 0-100
};
const FINALS_SCORES = {
attendance: "",
quiz1: "",
quiz2: "",
quiz3: "",
quiz4: "",
exam: ""
};
/* Paste Photopea / YouTube links. Example: "1":"https://www.photopea.com/#i7BUCWZ5F"
Midterm: Activities 1-15 | Finals: Activities 16-33 */
const ACTIVITY_LINKS = {
"1":"https://www.canva.com/design/DAHQ7yQYDU8/gVh-d8JecWyY25latn8XCg/view",
"2":"",
"3":"",
"4":"",
"5":"",
"6":"",
"7":"",
"8":"",
"9":"",
"10":"",
"11":"",
"12":"",
"13":"",
"14":"",
"15":"",
"16":"",
"17":"",
"18":"",
"19":"",
"20":"",
"21":"",
"22":"",
"23":"",
"24":"",
"25":"",
"26":"",
"27":"",
"28":"",
"29":"",
"30":"",
"31":"",
"32":"",
"33":"",
};
/* ============================================
DO NOT EDIT BELOW THIS LINE
============================================ */
const PROFESSOR_NAME = "Steven Barizo, MIT";
/* Runtime checks (do not edit) */
const QUIZ_MAX = 50;
/* Category weights (each period computed independently) */
const GRADE_WEIGHTS = {
attendance: 0.15,
cp: 0.15,
quizzes: 0.10,
laboratory: 0.40,
exam: 0.20
};
const RUBRIC_CRITERIA = [
{id:"completion", label:"Completion / Required Output", maxScore:40},
{id:"accuracy", label:"Accuracy / Tool Use", maxScore:40},
{id:"organization", label:"Organization / Effort", maxScore:20}
];
/* Midterm: Activity 1-15 | Finals: Activity 16-33 */
const ACTIVITY_DEFS = Array.from({length:33}, (_, i)=>{
const n = String(i + 1);
const period = i < 15 ? "midterm" : "finals";
return [n, n, `Activity ${n}`, period];
});
function getRubricMaxScore(){
return RUBRIC_CRITERIA.reduce((s,c)=>s+Number(c.maxScore||0),0);
}
const REQUIRED_ACTIVITIES = ACTIVITY_DEFS.map(([id,activityNumber,title,period])=>({
id, activityNumber, title, period,
maxScore:getRubricMaxScore(),
activityUrl:ACTIVITY_LINKS[id]||""
}));
const ACTIVITIES = REQUIRED_ACTIVITIES;
/* Base 60 College Grade Equivalent (percentage → college grade) */
function percentageToCollegeGrade(pct){
const p = Number(pct);
if(!Number.isFinite(p) || p < 75) return "5.00";
if(p >= 99) return "1.00";
if(p >= 97) return "1.00";
if(p >= 96) return "1.00";
if(p >= 95) return "1.25";
if(p >= 94) return "1.25";
if(p >= 93) return "1.25";
if(p >= 92) return "1.50";
if(p >= 91) return "1.50";
if(p >= 90) return "1.50";
if(p >= 89) return "1.75";
if(p >= 88) return "1.75";
if(p >= 87) return "1.75";
if(p >= 86) return "1.75";
if(p >= 85) return "1.75";
if(p >= 84) return "2.00";
if(p >= 83) return "2.00";
if(p >= 82) return "2.00";
if(p >= 81) return "2.00";
if(p >= 80) return "2.00";
if(p >= 79) return "2.25";
if(p >= 78) return "2.25";
if(p >= 77) return "2.50";
if(p >= 76) return "2.50";
if(p >= 75) return "3.00";
return "5.00";
}
/* DepEd-style Base 60: raw 60 → 75, raw 100 → 100 (lookup + formula blend) */
function base60Transmute(rawPercent){
const raw = Math.max(0, Math.min(100, Number(rawPercent)||0));
// Linear Base 60 mapping used in many PH institutions:
// transmuted = (raw / 100) * 40 + 60 → then scaled so 60 raw = 75
// Official DepEd-style: for raw >= 60: ((raw-60)/40)*25 + 75
let t;
if(raw >= 60){
t = ((raw - 60) / 40) * 25 + 75;
} else {
t = (raw / 60) * 15 + 60; // 0→60, 60→75 floor band
}
return Math.max(60, Math.min(100, Math.round(t)));
}
const siteTitle = document.getElementById("siteTitle");
const statusText = document.getElementById("status");
const portfolioTitle = document.getElementById("portfolioTitle");
const studentNameText = document.getElementById("studentNameText");
const studentIdText = document.getElementById("studentIdText");
const sectionText = document.getElementById("sectionText");
const professorNameText = document.getElementById("professorNameText");
const submittedCountText = document.getElementById("submittedCountText");
const missingCountText = document.getElementById("missingCountText");
const completionBadge = document.getElementById("completionBadge");
const completionPanel = document.getElementById("completionPanel");
const midtermScoreDisplay = document.getElementById("midtermScoreDisplay");
const finalsScoreDisplay = document.getElementById("finalsScoreDisplay");
const activityList = document.getElementById("activityList");
const viewerWrap = document.getElementById("viewerWrap");
const emptyMessage = document.getElementById("emptyMessage");
const loadingMessage = document.getElementById("loadingMessage");
const reloadButton = document.getElementById("reloadButton");
const fullscreenButton = document.getElementById("fullscreenButton");
const teacherLogoutButton = document.getElementById("teacherLogoutButton");
const exportJsonButton = document.getElementById("exportJsonButton");
const exportCsvButton = document.getElementById("exportCsvButton");
const exportHtmlButton = document.getElementById("exportHtmlButton");
const exportExcelButton = document.getElementById("exportExcelButton");
const importJsonButton = document.getElementById("importJsonButton");
const importFileInput = document.getElementById("importFileInput");
const importReferenceCsvButton = document.getElementById("importReferenceCsvButton");
const importReferenceCsvInput = document.getElementById("importReferenceCsvInput");
const resetGradesButton = document.getElementById("resetGradesButton");
const crossCheckPanel = document.getElementById("crossCheckPanel");
let currentActivityIndex = -1;
let mediaFrame = null;
let loadingTimer = null;
let teacherModeEnabled = false;
let teacherComboReady = true;
let gradeData = createEmptyGradeData();
let storageKey = "";
const gradeStore = window.sessionStorage; // per-tab; does not leak teacher scores across students/days like localStorage
init();
function createEmptyGradeData(){
return {
rubric:{},
notes:{},
midterm:{cp:"", attendance:"", quiz1:"", quiz2:"", quiz3:"", quiz4:"", exam:"", deduction:0, remarks:""},
finals:{cp:"", attendance:"", quiz1:"", quiz2:"", quiz3:"", quiz4:"", exam:"", deduction:0, remarks:""},
crossCheck:{importedAt:null, studentId:"", mismatches:[], matches:[], reference:null},
updatedAt:null
};
}
function init(){
document.title = `${PROFESSOR_NAME} - ${STUDENT_NAME}`;
siteTitle.textContent = PROFESSOR_NAME;
portfolioTitle.textContent = "Activity Portfolio";
studentNameText.textContent = STUDENT_NAME;
studentIdText.textContent = SUBMISSION_ID;
sectionText.textContent = SECTION;
professorNameText.textContent = PROFESSOR_NAME;
// One-time cleanup of old shared localStorage keys that mixed student grades
try{
Object.keys(localStorage).forEach(k=>{
if(k.startsWith("photopea-portfolio-grades-v2-")) localStorage.removeItem(k);
});
}catch(_){}
loadGradeData();
applyAutoLinkScores();
setupButtons();
setupTeacherHotkeys();
setupTeacherPanel();
renderStudentScoreDisplay();
renderActivityList();
updateSummary();
updateTeacherMode();
statusText.textContent = `${ACTIVITIES.length} activities loaded.`;
const firstWithLink = ACTIVITIES.findIndex(a=>hasValidLink(a.activityUrl));
if(firstWithLink >= 0) openActivity(firstWithLink);
}
function isFilled(v){
return v !== "" && v !== null && v !== undefined && String(v).trim() !== "";
}
function hasValidStudentIdentity(){
const nameOk = isFilled(STUDENT_NAME) && !/Student Name Here/i.test(STUDENT_NAME) && STUDENT_NAME.trim().length >= 5;
const idOk = isFilled(SUBMISSION_ID) && SUBMISSION_ID.trim() !== "STUDENT ID" && SUBMISSION_ID.trim().length >= 3;
const sectionOk = isFilled(SECTION) && String(SECTION).trim().length >= 2;
return nameOk && idOk && sectionOk;
}
function toNum(v){
if(!isFilled(v)) return null;
const n = Number(v);
return Number.isFinite(n) ? n : null;
}
function convertQuiz(raw){
const n = toNum(raw);
if(n === null) return null;
return Math.min(100, Math.round((n / QUIZ_MAX) * 100));
}
function getStudentPeriodScores(period){
return period === "finals" ? FINALS_SCORES : MIDTERM_SCORES;
}
function getEffectivePeriodScores(period){
const student = getStudentPeriodScores(period);
const teacher = gradeData[period] || {};
const pick = (key)=> isFilled(teacher[key]) ? teacher[key] : student[key];
return {
attendance: pick("attendance"),
quiz1: pick("quiz1"),
quiz2: pick("quiz2"),
quiz3: pick("quiz3"),
quiz4: pick("quiz4"),
exam: pick("exam"),
cp: teacher.cp,
deduction: Number(teacher.deduction || 0),
remarks: teacher.remarks || ""
};
}
function getMissingRequirements(){
const missing = [];
if(!isFilled(STUDENT_NAME) || /Student Name Here/i.test(STUDENT_NAME)) missing.push("Student name not entered correctly");
if(!isFilled(SUBMISSION_ID) || SUBMISSION_ID === "STUDENT ID") missing.push("Student number not entered correctly");
if(!SECTION || !String(SECTION).trim()) missing.push("Section not entered");
const checkPeriod = (label, scores)=>{
if(!isFilled(scores.attendance)) missing.push(`${label} Attendance score not entered`);
if(!isFilled(scores.quiz1)) missing.push(`${label} Quiz 1 score missing`);
if(!isFilled(scores.quiz2)) missing.push(`${label} Quiz 2 score missing`);
if(!isFilled(scores.quiz3)) missing.push(`${label} Quiz 3 score missing`);
if(!isFilled(scores.quiz4)) missing.push(`${label} Quiz 4 score missing`);
if(!isFilled(scores.exam)) missing.push(`${label} Exam score missing`);
};
checkPeriod("Midterm", MIDTERM_SCORES);
checkPeriod("Finals", FINALS_SCORES);
ACTIVITIES.forEach(a=>{
if(!hasValidLink(a.activityUrl)){
missing.push(`Activity ${a.activityNumber} link missing`);
}
});
return missing;
}
function renderCompletion(){
const missing = getMissingRequirements();
const complete = missing.length === 0;
completionBadge.className = "completion-badge " + (complete ? "ok" : "bad");
completionBadge.textContent = complete ? "✅ Completed" : "❌ Not Completed";
if(complete){
completionPanel.className = "notice-box ready";
completionPanel.innerHTML = "✅ All required requirements have been completed. Your submission is ready. ";
} else {
completionPanel.className = "notice-box missing";
completionPanel.innerHTML = "Missing Requirements " +
missing.map(m=>`${escapeHtml(m)} `).join("") + " ";
}
return {complete, missing};
}
function renderStudentScoreDisplay(){
const mismatchMap = {};
(gradeData.crossCheck?.mismatches || []).forEach(m=>{
mismatchMap[`${m.period}:${m.key}`] = m;
});
const matchSet = new Set((gradeData.crossCheck?.matches || []).map(m=>`${m.period}:${m.key}`));
const render = (el, scores, prefix, period)=>{
const items = [
["Attendance", "attendance", scores.attendance],
["Quiz 1 (/50)", "quiz1", scores.quiz1],
["Quiz 2 (/50)", "quiz2", scores.quiz2],
["Quiz 3 (/50)", "quiz3", scores.quiz3],
["Quiz 4 (/50)", "quiz4", scores.quiz4],
[`${prefix} Exam`, "exam", scores.exam]
];
el.innerHTML = items.map(([label,key,val])=>{
const id = `${period}:${key}`;
const mismatch = mismatchMap[id];
const matched = matchSet.has(id);
const cls = mismatch ? "score-item mismatch" : (matched ? "score-item matched" : "score-item");
const note = mismatch
? `Entered ${escapeHtml(mismatch.studentValue)} → Official ${escapeHtml(mismatch.referenceValue)} (corrected) `
: "";
return `
${escapeHtml(label)}
${isFilled(val) ? escapeHtml(val) : "—"}
${note}
`;
}).join("");
};
render(midtermScoreDisplay, MIDTERM_SCORES, "Midterm", "midterm");
render(finalsScoreDisplay, FINALS_SCORES, "Finals", "finals");
renderCrossCheckPanel();
}
function renderCrossCheckPanel(){
if(!crossCheckPanel) return;
const cc = gradeData.crossCheck || {};
if(!cc.importedAt){
crossCheckPanel.className = "notice-box crosscheck teacher-only";
crossCheckPanel.innerHTML = `Reference Score Cross-Check
Import your official Student ID Reference Scores.csv to compare quizzes, attendance, and major exams against this student's entered scores.
Mismatches are flagged and corrected for grading using the official reference values. The student's original entries remain visible for audit.
`;
return;
}
const mismatches = cc.mismatches || [];
if(!mismatches.length){
crossCheckPanel.className = "notice-box crosscheck teacher-only ready";
crossCheckPanel.innerHTML = `✅ Cross-Check Complete
Student ID ${escapeHtml(cc.studentId || SUBMISSION_ID)} matched the reference file.
All compared Attendance / Quiz / Exam scores match the official records. No corrections needed.
Imported: ${escapeHtml(new Date(cc.importedAt).toLocaleString())}
`;
return;
}
crossCheckPanel.className = "notice-box crosscheck teacher-only warn";
crossCheckPanel.innerHTML = `⚠️ Score Mismatch Detected — Corrected from Reference
Student ID ${escapeHtml(cc.studentId || SUBMISSION_ID)} has ${mismatches.length} mismatched score(s). Official reference values were applied for grading.
${mismatches.map(m=>`${escapeHtml(m.label)} : student entered ${escapeHtml(m.studentValue)} , official is ${escapeHtml(m.referenceValue)} — corrected `).join("")}
Imported: ${escapeHtml(new Date(cc.importedAt).toLocaleString())}
`;
}
function isRecognizableLink(url){
if(!hasValidLink(url)) return false;
const type = detectResourceType(url);
return type === "photopea" || type === "youtube" || type === "canva" || type === "link";
}
function activityHasSubmittableLink(activity){
return Boolean(activity && hasValidLink(activity.activityUrl) && isRecognizableLink(activity.activityUrl));
}
function applyAutoLinkScores(){
let changed = false;
ACTIVITIES.forEach(activity=>{
const hasLink = activityHasSubmittableLink(activity);
if(!hasLink){
// Never keep scores on activities with no student link
if(gradeData.rubric[activity.id]){
delete gradeData.rubric[activity.id];
changed = true;
}
return;
}
const alreadyGraded = RUBRIC_CRITERIA.every(c=>isFilled(getRubricScore(activity.id, c.id)));
if(alreadyGraded) return;
RUBRIC_CRITERIA.forEach(c=>setRubricScore(activity.id, c.id, c.maxScore));
changed = true;
});
if(changed) saveGradeData();
}
function getActivityRubricPercent(activityId){
const activity = ACTIVITIES.find(a=>a.id === activityId);
// Missing / invalid links must never count as scored (including never auto-100)
if(!activityHasSubmittableLink(activity)) return null;
const max = getRubricMaxScore();
if(max <= 0) return null;
const graded = RUBRIC_CRITERIA.every(c=>isFilled(getRubricScore(activityId, c.id)));
if(graded){
return Math.min(100, Math.round((getActivityRubricTotal(activityId) / max) * 100));
}
return 100;
}
function computeLaboratoryGrade(period){
const list = ACTIVITIES.filter(a=>a.period === period);
const percents = list.map(a=>getActivityRubricPercent(a.id)).filter(v=>v !== null);
if(!percents.length) return null;
const avg = percents.reduce((s,v)=>s+v,0) / percents.length;
return Math.min(100, Math.round(avg));
}
function computeQuizAverage(scores){
const converted = [scores.quiz1, scores.quiz2, scores.quiz3, scores.quiz4]
.map(convertQuiz)
.filter(v=>v !== null);
if(!converted.length) return null;
return Math.min(100, Math.round(converted.reduce((s,v)=>s+v,0) / converted.length));
}
function computePeriodGrade(period){
const s = getEffectivePeriodScores(period);
const attendance = toNum(s.attendance);
const cp = toNum(s.cp);
const quizzes = computeQuizAverage(s);
const laboratory = computeLaboratoryGrade(period);
const exam = toNum(s.exam);
const parts = [
{key:"attendance", value:attendance, weight:GRADE_WEIGHTS.attendance},
{key:"cp", value:cp, weight:GRADE_WEIGHTS.cp},
{key:"quizzes", value:quizzes, weight:GRADE_WEIGHTS.quizzes},
{key:"laboratory", value:laboratory, weight:GRADE_WEIGHTS.laboratory},
{key:"exam", value:exam, weight:GRADE_WEIGHTS.exam}
];
const available = parts.filter(p=>p.value !== null);
if(!available.length){
return {
period, attendance, cp, quizzes, laboratory, exam,
percentage:null, base60:null, college:null, pass:null, deduction:s.deduction, remarks:s.remarks,
rawWeighted:null
};
}
const weightSum = available.reduce((s,p)=>s+p.weight,0);
const rawWeighted = available.reduce((sum,p)=>sum + (p.value * (p.weight / weightSum)), 0);
const afterDeduction = Math.max(0, rawWeighted - Number(s.deduction || 0));
const percentage = Math.min(100, Math.round(afterDeduction));
const base60 = base60Transmute(percentage);
const college = percentageToCollegeGrade(percentage);
const pass = percentage >= 75;
return {
period, attendance, cp, quizzes, laboratory, exam,
percentage, base60, college, pass, deduction:s.deduction, remarks:s.remarks,
rawWeighted
};
}
function computeOverallStanding(){
const midterm = computePeriodGrade("midterm");
const finals = computePeriodGrade("finals");
const identityOk = hasValidStudentIdentity();
const vals = [midterm.percentage, finals.percentage].filter(v=>v !== null);
let overall = null;
if(vals.length){
overall = Math.min(100, Math.round(vals.reduce((s,v)=>s+v,0) / vals.length));
}
// Incorrect / missing student identity forces FAIL in the grading system
if(!identityOk){
return {
midterm, finals,
overall: overall,
college: "5.00",
base60: overall === null ? null : base60Transmute(overall),
pass: false,
identityFail: true
};
}
const college = overall === null ? null : percentageToCollegeGrade(overall);
const base60 = overall === null ? null : base60Transmute(overall);
const pass = overall === null ? null : overall >= 75;
return {midterm, finals, overall, college, base60, pass, identityFail:false};
}
function setupTeacherHotkeys(){
window.addEventListener("keydown", e=>{
const combo = e.ctrlKey && e.shiftKey;
if(combo && teacherComboReady){
e.preventDefault();
teacherComboReady = false;
if(teacherModeEnabled){
teacherModeEnabled = false;
updateTeacherMode();
updateSummary();
statusText.textContent = "Teacher Mode closed.";
} else {
promptTeacherLogin();
}
}
});
window.addEventListener("keyup", e=>{
if(!(e.ctrlKey && e.shiftKey)) teacherComboReady = true;
});
window.addEventListener("blur", ()=>{ teacherComboReady = true; });
}
function promptTeacherLogin(){
const pw = prompt("Access code:");
if(pw === null) return;
if(__ok(pw)){
teacherModeEnabled = true;
applyAutoLinkScores();
updateTeacherMode();
syncTeacherPanelFromData();
renderStudentScoreDisplay();
renderActivityList();
updateSummary();
statusText.textContent = "Teacher Mode enabled. Grades update in real time.";
} else {
alert("Access denied.");
}
}
/* discreet access verification — not for student editing */
function __ok(v){
const a = [0x70,0x6c,0x6d,0x75,0x6e,0x2d,0x74,0x65,0x61,0x63,0x68,0x65,0x72];
if(typeof v !== "string" || v.length !== a.length) return false;
for(let i=0;i{
syncTeacherPanelFromData();
};
fields.forEach(([id, key])=>{
const el = document.getElementById(id);
el.oninput = ()=>{
const period = periodSelect.value;
if(!gradeData[period]) gradeData[period] = {};
gradeData[period][key] = el.value;
if(key === "deduction") gradeData[period].deduction = Number(el.value || 0);
saveGradeData();
updateSummary();
};
});
document.querySelectorAll("[data-deduct]").forEach(btn=>{
btn.onclick = ()=>{
const period = periodSelect.value;
const add = Number(btn.getAttribute("data-deduct"));
const current = Number(gradeData[period].deduction || 0);
gradeData[period].deduction = Math.min(100, current + add);
document.getElementById("teacherDeductionInput").value = gradeData[period].deduction;
saveGradeData();
updateSummary();
};
});
document.getElementById("clearDeductionBtn").onclick = ()=>{
const period = periodSelect.value;
gradeData[period].deduction = 0;
document.getElementById("teacherDeductionInput").value = 0;
saveGradeData();
updateSummary();
};
teacherLogoutButton.onclick = ()=>{
teacherModeEnabled = false;
updateTeacherMode();
statusText.textContent = "Teacher Mode closed.";
};
}
function syncTeacherPanelFromData(){
const period = document.getElementById("teacherPeriodSelect").value;
const d = gradeData[period] || {};
document.getElementById("teacherCpInput").value = d.cp ?? "";
document.getElementById("teacherAttInput").value = d.attendance ?? "";
document.getElementById("teacherExamInput").value = d.exam ?? "";
document.getElementById("teacherQ1Input").value = d.quiz1 ?? "";
document.getElementById("teacherQ2Input").value = d.quiz2 ?? "";
document.getElementById("teacherQ3Input").value = d.quiz3 ?? "";
document.getElementById("teacherQ4Input").value = d.quiz4 ?? "";
document.getElementById("teacherDeductionInput").value = d.deduction ?? 0;
document.getElementById("teacherRemarksInput").value = d.remarks ?? "";
}
function setupButtons(){
reloadButton.onclick = ()=>{
const a = getCurrentActivity();
if(!a || !hasValidLink(a.activityUrl)){
alert("No activity link is currently open.");
return;
}
loadMediaFresh(a.activityUrl);
};
fullscreenButton.onclick = async ()=>{
if(!mediaFrame){ alert("Open an activity first."); return; }
try{ await mediaFrame.requestFullscreen(); }
catch{ alert("Fullscreen is not supported or was blocked."); }
};
exportJsonButton.onclick = exportJson;
exportCsvButton.onclick = exportCsv;
exportHtmlButton.onclick = exportGradedHtml;
exportExcelButton.onclick = exportExcel;
importJsonButton.onclick = ()=>importFileInput.click();
importFileInput.onchange = importJson;
importReferenceCsvButton.onclick = ()=>importReferenceCsvInput.click();
importReferenceCsvInput.onchange = importReferenceCsv;
resetGradesButton.onclick = ()=>{
if(!confirm("Reset all teacher grading data (CP, deductions, remarks, rubrics, cross-check) for this submission?")) return;
gradeData = createEmptyGradeData();
gradeData.studentFingerprint = getStudentFingerprint();
storageKey = buildStorageKey();
gradeStore.removeItem(storageKey);
try{ localStorage.removeItem(storageKey); }catch(_){}
// also clear old v2 keys that may still leak previous student scores
try{
Object.keys(localStorage).forEach(k=>{
if(k.startsWith("photopea-portfolio-grades-v2-") || k.startsWith("photopea-portfolio-grades-v3-")){
localStorage.removeItem(k);
}
});
}catch(_){}
syncTeacherPanelFromData();
renderStudentScoreDisplay();
renderActivityList();
updateSummary();
};
}
function renderActivityList(){
activityList.innerHTML = "";
ACTIVITIES.forEach((activity, index)=>{
const hasLink = hasValidLink(activity.activityUrl);
const label = getResourceLabel(activity.activityUrl);
const card = document.createElement("div");
card.className = "activity-card " + (hasLink ? "has-link" : "missing") + (index === currentActivityIndex ? " active" : "");
const periodClass = activity.period === "finals" ? "period-tag finals" : "period-tag";
card.innerHTML = `
${escapeHtml(activity.activityNumber)}
${escapeHtml(activity.period)}
${escapeHtml(activity.title)}
${hasLink ? escapeHtml(label + " link submitted") : "Missing activity link"}
${hasLink ? escapeHtml(shortenUrl(activity.activityUrl)) : "No link provided"}
`;
card.appendChild(createRubricBlock(activity));
const actions = document.createElement("div");
actions.className = "card-actions";
const viewButton = document.createElement("button");
viewButton.type = "button";
viewButton.textContent = `View in ${label}`;
viewButton.onclick = ()=>openActivity(index);
actions.appendChild(viewButton);
card.appendChild(actions);
activityList.appendChild(card);
});
}
function createRubricBlock(activity){
const gradeBlock = document.createElement("div");
gradeBlock.className = "grade-block teacher-only";
const totalBox = document.createElement("div");
totalBox.className = "rubric-total";
totalBox.innerHTML = 'Rubric Total
';
const totalValue = totalBox.querySelector(".rubric-total-value");
gradeBlock.appendChild(totalBox);
const controls = [];
RUBRIC_CRITERIA.forEach(criterion=>{
const box = document.createElement("div");
box.className = "criterion";
const hasLink = activityHasSubmittableLink(activity);
if(!hasLink && gradeData.rubric[activity.id]){
delete gradeData.rubric[activity.id];
}
let saved = hasLink ? getRubricScore(activity.id, criterion.id) : "";
const autoLinked = hasLink && saved === "";
if(autoLinked){
setRubricScore(activity.id, criterion.id, criterion.maxScore);
saved = criterion.maxScore;
}
const initial = saved === "" ? 0 : Number(saved);
const valueLabel = !hasLink
? `No link / ${criterion.maxScore}`
: (saved === "" ? `Not graded / ${criterion.maxScore}` : `${initial}/${criterion.maxScore}`);
box.innerHTML = `${escapeHtml(criterion.label)}
${escapeHtml(valueLabel)}
`;
const value = box.querySelector(".criterion-value");
const slider = document.createElement("input");
slider.className = "grade-slider";
slider.type = "range";
slider.min = "0";
slider.max = String(criterion.maxScore);
slider.step = "5";
slider.value = String(hasLink ? initial : 0);
slider.disabled = !hasLink;
slider.oninput = ()=>{
if(!activityHasSubmittableLink(activity)) return;
setRubricScore(activity.id, criterion.id, Number(slider.value));
value.textContent = `${slider.value}/${criterion.maxScore}`;
updateRubricTotalDisplay(activity.id, totalValue);
saveGradeData();
updateSummary();
};
const scale = document.createElement("div");
scale.className = "grade-scale";
scale.innerHTML = `0 ${criterion.maxScore} `;
box.appendChild(slider);
box.appendChild(scale);
gradeBlock.appendChild(box);
controls.push({criterion, slider, value});
});
const noteArea = document.createElement("textarea");
noteArea.className = "note-area";
noteArea.placeholder = "Teacher notes for this activity...";
noteArea.value = gradeData.notes[activity.id] || "";
noteArea.oninput = ()=>{
gradeData.notes[activity.id] = noteArea.value;
saveGradeData();
};
const rubricActions = document.createElement("div");
rubricActions.className = "rubric-actions";
const autoButton = document.createElement("button");
autoButton.type = "button";
autoButton.className = "good";
autoButton.textContent = "Auto 100";
autoButton.disabled = !activityHasSubmittableLink(activity);
autoButton.onclick = ()=>{
if(!activityHasSubmittableLink(activity)){
alert("This activity has no link. Auto 100 is only for submitted links.");
return;
}
controls.forEach(c=>{
setRubricScore(activity.id, c.criterion.id, c.criterion.maxScore);
c.slider.value = String(c.criterion.maxScore);
c.value.textContent = `${c.criterion.maxScore}/${c.criterion.maxScore}`;
});
updateRubricTotalDisplay(activity.id, totalValue);
saveGradeData();
updateSummary();
};
const clearButton = document.createElement("button");
clearButton.type = "button";
clearButton.textContent = "Clear Rubric";
clearButton.onclick = ()=>{
delete gradeData.rubric[activity.id];
controls.forEach(c=>{
c.slider.value = "0";
c.value.textContent = activityHasSubmittableLink(activity)
? `Not graded / ${c.criterion.maxScore}`
: `No link / ${c.criterion.maxScore}`;
});
updateRubricTotalDisplay(activity.id, totalValue);
saveGradeData();
updateSummary();
};
rubricActions.appendChild(autoButton);
rubricActions.appendChild(clearButton);
gradeBlock.appendChild(noteArea);
gradeBlock.appendChild(rubricActions);
updateRubricTotalDisplay(activity.id, totalValue);
return gradeBlock;
}
function setRubricScore(activityId, criterionId, value){
if(!gradeData.rubric[activityId]) gradeData.rubric[activityId] = {};
gradeData.rubric[activityId][criterionId] = Number(value);
}
function getRubricScore(activityId, criterionId){
if(!gradeData.rubric[activityId]) return "";
const v = gradeData.rubric[activityId][criterionId];
return v === undefined || v === null ? "" : v;
}
function getActivityRubricTotal(activityId){
return RUBRIC_CRITERIA.reduce((s,c)=>s + Number(getRubricScore(activityId, c.id) || 0), 0);
}
function updateRubricTotalDisplay(activityId, el){
el.textContent = `${getActivityRubricTotal(activityId)}/${getRubricMaxScore()}`;
}
function openActivity(index){
const activity = ACTIVITIES[index];
if(!activity) return;
currentActivityIndex = index;
renderActivityList();
if(!hasValidLink(activity.activityUrl)){
statusText.textContent = `Activity ${activity.activityNumber}: missing activity link.`;
emptyMessage.style.display = "flex";
emptyMessage.textContent = "This activity has no Photopea or YouTube link.";
loadingMessage.style.display = "none";
removeMediaFrame();
return;
}
statusText.textContent = `Viewing ${getResourceLabel(activity.activityUrl)} Activity ${activity.activityNumber}: ${activity.title}`;
emptyMessage.style.display = "none";
loadMediaFresh(activity.activityUrl);
}
function loadMediaFresh(rawUrl){
const resourceType = detectResourceType(rawUrl);
const label = getResourceLabel(rawUrl);
emptyMessage.style.display = "none";
loadingMessage.textContent = `Loading ${label}...`;
loadingMessage.style.display = "flex";
if(loadingTimer) clearTimeout(loadingTimer);
removeMediaFrame();
if(resourceType === "youtube"){
loadYoutubeViewer(rawUrl);
return;
}
if(resourceType === "canva"){
loadCanvaViewer(rawUrl);
return;
}
const finalUrl = normalizeViewerUrl(rawUrl);
if(!finalUrl){ alert("Invalid activity link."); loadingMessage.style.display = "none"; return; }
mediaFrame = document.createElement("iframe");
mediaFrame.className = "media-frame";
mediaFrame.setAttribute("referrerpolicy", "strict-origin-when-cross-origin");
mediaFrame.referrerPolicy = "strict-origin-when-cross-origin";
mediaFrame.name = `activity_viewer_${Date.now()}`;
mediaFrame.allowFullscreen = true;
mediaFrame.allow = "clipboard-read; clipboard-write; fullscreen";
mediaFrame.onload = ()=>setTimeout(()=>loadingMessage.style.display = "none", 1200);
viewerWrap.appendChild(mediaFrame);
mediaFrame.src = finalUrl;
loadingTimer = setTimeout(()=>loadingMessage.style.display = "none", 6000);
}
function loadYoutubeViewer(rawUrl){
const videoId = getYoutubeVideoId(rawUrl);
if(!videoId){ alert("Invalid YouTube link."); loadingMessage.style.display = "none"; return; }
const watchUrl = `https://www.youtube.com/watch?v=${encodeURIComponent(videoId)}`;
const embedUrl = `https://www.youtube-nocookie.com/embed/${encodeURIComponent(videoId)}?rel=0&modestbranding=1&playsinline=1&enablejsapi=1`;
mediaFrame = document.createElement("iframe");
mediaFrame.className = "media-frame";
mediaFrame.setAttribute("referrerpolicy", "strict-origin-when-cross-origin");
mediaFrame.referrerPolicy = "strict-origin-when-cross-origin";
mediaFrame.title = "YouTube video player";
mediaFrame.name = `yt_viewer_${Date.now()}`;
mediaFrame.allowFullscreen = true;
mediaFrame.setAttribute("allowfullscreen", "true");
mediaFrame.setAttribute("frameborder", "0");
mediaFrame.setAttribute("allow", "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen");
mediaFrame.allow = "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen";
const bar = document.createElement("div");
bar.className = "viewer-fallback-bar";
bar.id = "viewerFallbackBar";
bar.innerHTML = "If the player shows Error 153, use Open on YouTube (common when opening this HTML as a local file). ";
const openBtn = document.createElement("a");
openBtn.href = watchUrl;
openBtn.target = "_blank";
openBtn.rel = "noopener noreferrer";
openBtn.textContent = "Open on YouTube";
bar.appendChild(openBtn);
if(location.protocol === "file:"){
const shell = document.createElement("div");
shell.className = "youtube-shell";
shell.id = "youtubeShell";
shell.innerHTML = `▶ `;
const startEmbed = ()=>{
shell.remove();
mediaFrame.src = embedUrl;
viewerWrap.appendChild(mediaFrame);
loadingMessage.style.display = "flex";
setTimeout(()=>loadingMessage.style.display = "none", 1500);
};
shell.onclick = startEmbed;
viewerWrap.appendChild(shell);
viewerWrap.appendChild(bar);
loadingMessage.style.display = "none";
statusText.textContent = "YouTube ready. Click play to load the video.";
return;
}
mediaFrame.onload = ()=>setTimeout(()=>loadingMessage.style.display = "none", 1000);
viewerWrap.appendChild(mediaFrame);
viewerWrap.appendChild(bar);
mediaFrame.src = embedUrl;
loadingTimer = setTimeout(()=>loadingMessage.style.display = "none", 6000);
}
function loadCanvaViewer(rawUrl){
const finalUrl = normalizeCanvaEmbedUrl(rawUrl);
if(!finalUrl){
alert("Invalid Canva link. Use a public view/embed link from Share → Embed.");
loadingMessage.style.display = "none";
return;
}
mediaFrame = document.createElement("iframe");
mediaFrame.className = "media-frame";
mediaFrame.setAttribute("referrerpolicy", "strict-origin-when-cross-origin");
mediaFrame.referrerPolicy = "strict-origin-when-cross-origin";
mediaFrame.title = "Canva design";
mediaFrame.name = `canva_viewer_${Date.now()}`;
mediaFrame.allowFullscreen = true;
mediaFrame.setAttribute("allowfullscreen", "true");
mediaFrame.allow = "fullscreen";
mediaFrame.setAttribute("allow", "fullscreen");
mediaFrame.style.border = "0";
mediaFrame.onload = ()=>setTimeout(()=>loadingMessage.style.display = "none", 1200);
const bar = document.createElement("div");
bar.className = "viewer-fallback-bar";
bar.id = "viewerFallbackBar";
bar.innerHTML = "Canva public embed. If blank, open the design and use Share → Embed. ";
const openBtn = document.createElement("a");
openBtn.href = finalUrl.replace("?embed", "").replace("&embed", "");
openBtn.target = "_blank";
openBtn.rel = "noopener noreferrer";
openBtn.textContent = "Open in Canva";
bar.appendChild(openBtn);
viewerWrap.appendChild(mediaFrame);
viewerWrap.appendChild(bar);
mediaFrame.src = finalUrl;
loadingTimer = setTimeout(()=>loadingMessage.style.display = "none", 8000);
}
function removeMediaFrame(){
if(mediaFrame){ mediaFrame.remove(); mediaFrame = null; }
const bar = document.getElementById("viewerFallbackBar");
if(bar) bar.remove();
const shell = document.getElementById("youtubeShell");
if(shell) shell.remove();
}
function detectResourceType(url){
if(!url || typeof url !== "string") return "missing";
const text = url.trim();
if(!text) return "missing";
if(getYoutubeVideoId(text)) return "youtube";
if(isCanvaUrl(text)) return "canva";
if(/photopea\.com/i.test(text) || text.startsWith("#")) return "photopea";
if(/^https?:\/\//i.test(text) || /^www\./i.test(text)) return "link";
return "photopea";
}
function getResourceLabel(url){
const type = detectResourceType(url);
if(type === "youtube") return "YouTube";
if(type === "canva") return "Canva";
if(type === "photopea") return "Photopea";
if(type === "link") return "Link";
return "Activity";
}
function normalizeViewerUrl(url){
const type = detectResourceType(url);
if(type === "youtube") return normalizeYoutubeEmbedUrl(url);
if(type === "canva") return normalizeCanvaEmbedUrl(url);
if(type === "photopea") return normalizePhotopeaUrl(url);
if(type === "link") return normalizeGenericUrl(url);
return "";
}
function normalizeGenericUrl(url){
if(!url || typeof url !== "string") return "";
let text = url.trim();
if(!text) return "";
if(/^https?:\/\//i.test(text)) return text;
if(/^www\./i.test(text)) return "https://" + text;
return text;
}
function normalizeYoutubeEmbedUrl(url){
const videoId = getYoutubeVideoId(url);
if(!videoId) return "";
return `https://www.youtube-nocookie.com/embed/${encodeURIComponent(videoId)}?rel=0&modestbranding=1&playsinline=1`;
}
function isCanvaUrl(rawUrl){
if(!rawUrl || typeof rawUrl !== "string") return false;
const text = rawUrl.trim().toLowerCase();
return /canva\.com\/design\//.test(text) || /canva\.me\//.test(text);
}
function normalizeCanvaEmbedUrl(url){
if(!url || typeof url !== "string") return "";
let text = url.trim();
if(!text) return "";
if(!/^https?:\/\//i.test(text)){
if(/^www\./i.test(text) || /canva\./i.test(text)) text = "https://" + text.replace(/^\/\//,"");
else return "";
}
try{
const parsed = new URL(text);
const host = parsed.hostname.toLowerCase().replace(/^www\./,"");
if(host !== "canva.com" && host !== "canva.me") return "";
if(/canva\.com$/i.test(host)){
let path = parsed.pathname.replace(/\/+$/,"");
if(/\/edit$/i.test(path)) path = path.replace(/\/edit$/i, "/view");
if(!/\/view$/i.test(path)) path = path + "/view";
parsed.pathname = path;
return `${parsed.origin}${parsed.pathname}?embed`;
}
return parsed.toString();
} catch {
return "";
}
}
function getYoutubeVideoId(rawUrl){
if(!rawUrl || typeof rawUrl !== "string") return "";
let text = rawUrl.trim();
if(!text) return "";
if(!/^https?:\/\//i.test(text)){
if(/^www\./i.test(text) || /youtube\.com/i.test(text) || /youtu\.be/i.test(text) || /youtube-nocookie\.com/i.test(text)){
text = "https://" + text.replace(/^\/\//,"");
} else return "";
}
try{
const parsed = new URL(text);
const host = parsed.hostname.toLowerCase().replace(/^www\./,"");
const pathParts = parsed.pathname.split("/").filter(Boolean);
if(host === "youtu.be") return pathParts[0] || "";
if(host === "youtube.com" || host === "m.youtube.com" || host === "music.youtube.com" || host === "youtube-nocookie.com"){
if(parsed.pathname === "/watch") return parsed.searchParams.get("v") || "";
if(pathParts[0] === "embed" && pathParts[1]) return pathParts[1];
if(pathParts[0] === "shorts" && pathParts[1]) return pathParts[1];
if(pathParts[0] === "live" && pathParts[1]) return pathParts[1];
}
return "";
} catch { return ""; }
}
function normalizePhotopeaUrl(url){
if(!url || typeof url !== "string") return "";
let t = url.trim();
if(!t) return "";
t = t.replace(/&/g, "&");
if(t.startsWith("https://www.photopea.com/#")) return t.replace("https://www.photopea.com/#", "https://www.photopea.com#");
if(t.startsWith("https://www.photopea.com#")) return t;
if(t.startsWith("http://www.photopea.com/#")) return t.replace("http://www.photopea.com/#", "https://www.photopea.com#");
if(t.startsWith("http://www.photopea.com#")) return t.replace("http://", "https://");
if(t.startsWith("www.photopea.com/#")) return t.replace("www.photopea.com/#", "https://www.photopea.com#");
if(t.startsWith("www.photopea.com#")) return "https://" + t;
if(t.startsWith("photopea.com/#")) return t.replace("photopea.com/#", "https://www.photopea.com#");
if(t.startsWith("photopea.com#")) return "https://www." + t;
if(t.startsWith("#")) return "https://www.photopea.com" + t;
return "https://www.photopea.com#" + t;
}
function hasValidLink(url){ return Boolean(url && String(url).trim().length > 0); }
function getCurrentActivity(){ return currentActivityIndex < 0 ? null : ACTIVITIES[currentActivityIndex]; }
function getStudentFingerprint(){
return [
String(STUDENT_NAME || "").trim(),
String(SUBMISSION_ID || "").trim(),
String(SECTION || "").trim(),
String(PROFESSOR_NAME || "").trim()
].join("||");
}
function buildStorageKey(){
return "photopea-portfolio-grades-v3-" + slugify(getStudentFingerprint() || "unknown-student");
}
function loadGradeData(){
storageKey = buildStorageKey();
const fingerprint = getStudentFingerprint();
try{
const saved = gradeStore.getItem(storageKey);
if(!saved){
gradeData = createEmptyGradeData();
gradeData.studentFingerprint = fingerprint;
return;
}
const p = JSON.parse(saved);
// Reject data that belongs to another student (or old unscoped saves)
if(!p.studentFingerprint || p.studentFingerprint !== fingerprint){
gradeStore.removeItem(storageKey);
gradeData = createEmptyGradeData();
gradeData.studentFingerprint = fingerprint;
return;
}
if(p.crossCheck && p.crossCheck.studentId && String(p.crossCheck.studentId).trim() !== String(SUBMISSION_ID || "").trim()){
gradeStore.removeItem(storageKey);
gradeData = createEmptyGradeData();
gradeData.studentFingerprint = fingerprint;
return;
}
const empty = createEmptyGradeData();
gradeData = {
rubric: p.rubric || {},
notes: p.notes || {},
midterm: Object.assign({}, empty.midterm, p.midterm || {}),
finals: Object.assign({}, empty.finals, p.finals || {}),
crossCheck: Object.assign({}, empty.crossCheck, p.crossCheck || {}),
studentFingerprint: fingerprint,
updatedAt: p.updatedAt || null
};
} catch {
gradeData = createEmptyGradeData();
gradeData.studentFingerprint = fingerprint;
}
}
function saveGradeData(){
storageKey = buildStorageKey();
gradeData.studentFingerprint = getStudentFingerprint();
gradeData.updatedAt = new Date().toISOString();
gradeStore.setItem(storageKey, JSON.stringify(gradeData));
}
function fmt(v, suffix=""){
return v === null || v === undefined || v === "" ? "—" : `${v}${suffix}`;
}
function updateSummary(){
const totalActivities = ACTIVITIES.length;
const submittedActivities = ACTIVITIES.filter(a=>hasValidLink(a.activityUrl)).length;
const missingActivities = totalActivities - submittedActivities;
submittedCountText.textContent = `${submittedActivities}/${totalActivities}`;
missingCountText.textContent = String(missingActivities);
const completion = renderCompletion();
const standing = computeOverallStanding();
if(teacherModeEnabled){
document.getElementById("midtermPctText").textContent = fmt(standing.midterm.percentage, "%");
document.getElementById("finalsPctText").textContent = fmt(standing.finals.percentage, "%");
document.getElementById("computedGradeText").textContent = fmt(standing.overall, "%");
document.getElementById("collegeGradeText").textContent = fmt(standing.college);
document.getElementById("midtermLabText").textContent = fmt(standing.midterm.laboratory);
document.getElementById("finalsLabText").textContent = fmt(standing.finals.laboratory);
document.getElementById("base60Text").textContent = fmt(standing.base60);
document.getElementById("passFailText").textContent = standing.identityFail
? "FAIL (Identity)"
: (standing.pass === null ? "—" : (standing.pass ? "PASS" : "FAIL"));
document.getElementById("passFailText").style.color = standing.pass === true ? "var(--accent)" : "#f87171";
document.getElementById("floatName").textContent = STUDENT_NAME;
document.getElementById("floatId").textContent = SUBMISSION_ID;
document.getElementById("floatOverall").textContent = fmt(standing.overall, "%");
document.getElementById("floatCollege").textContent = fmt(standing.college);
document.getElementById("floatPass").textContent = standing.identityFail
? "FAIL (Identity)"
: (standing.pass === null ? "—" : (standing.pass ? "PASS" : "FAIL"));
document.getElementById("floatPassRow").className = "float-row " + (standing.pass === true ? "pass" : "fail");
document.getElementById("floatMidLab").textContent = fmt(standing.midterm.laboratory);
document.getElementById("floatFinLab").textContent = fmt(standing.finals.laboratory);
document.getElementById("floatCompletion").textContent = completion.complete ? "✅ Completed" : "❌ Not Completed";
}
}
function buildExportPayload(){
const standing = computeOverallStanding();
const completion = getMissingRequirements();
return {
exportedAt: new Date().toISOString(),
storageKey,
studentName: STUDENT_NAME,
section: SECTION,
professorName: PROFESSOR_NAME,
submissionId: SUBMISSION_ID,
pageUrl: window.location.href,
rubricCriteria: RUBRIC_CRITERIA,
gradeWeights: GRADE_WEIGHTS,
midtermScores: getEffectivePeriodScores("midterm"),
finalsScores: getEffectivePeriodScores("finals"),
standing,
completion:{ complete: completion.length === 0, missing: completion },
activities: ACTIVITIES.map(activity=>{
const rubric = {};
RUBRIC_CRITERIA.forEach(c=>{
const score = getRubricScore(activity.id, c.id);
rubric[c.id] = {label:c.label, score: score === "" ? "" : Number(score), maxScore:c.maxScore};
});
return {
id: activity.id,
activityNumber: activity.activityNumber,
title: activity.title,
period: activity.period,
hasLink: hasValidLink(activity.activityUrl),
activityUrl: activity.activityUrl,
resourceType: detectResourceType(activity.activityUrl),
resourceLabel: getResourceLabel(activity.activityUrl),
viewerUrl: hasValidLink(activity.activityUrl) ? normalizeViewerUrl(activity.activityUrl) : "",
rubric,
totalScore: getActivityRubricTotal(activity.id),
maxScore: getRubricMaxScore(),
labPercent: getActivityRubricPercent(activity.id),
notes: gradeData.notes[activity.id] || ""
};
})
};
}
function buildCombinedGradeRow(){
const standing = computeOverallStanding();
const mid = standing.midterm;
const fin = standing.finals;
const midScores = getEffectivePeriodScores("midterm");
const finScores = getEffectivePeriodScores("finals");
const headers = [
"Student ID","Student Name","Section",
"Midterm CP","Midterm Attendance","Midterm Quiz 1","Midterm Quiz 2","Midterm Quiz 3","Midterm Quiz 4",
"Midterm Exam Score","Midterm Laboratory Grade","Midterm Percentage","Midterm College Grade",
"",
"Finals CP","Finals Attendance","Finals Quiz 1","Finals Quiz 2","Finals Quiz 3","Finals Quiz 4",
"Finals Exam Score","Finals Laboratory Grade","Finals Percentage","Finals College Grade"
];
const values = [
SUBMISSION_ID, STUDENT_NAME, SECTION,
mid.cp ?? "", mid.attendance ?? "", midScores.quiz1 ?? "", midScores.quiz2 ?? "", midScores.quiz3 ?? "", midScores.quiz4 ?? "",
mid.exam ?? "", mid.laboratory ?? "", mid.percentage ?? "", mid.college ?? "",
"",
fin.cp ?? "", fin.attendance ?? "", finScores.quiz1 ?? "", finScores.quiz2 ?? "", finScores.quiz3 ?? "", finScores.quiz4 ?? "",
fin.exam ?? "", fin.laboratory ?? "", fin.percentage ?? "", fin.college ?? ""
];
return {headers, values, standing};
}
function parseCsvText(text){
const rows = [];
let row = [];
let cell = "";
let inQuotes = false;
const src = String(text || "").replace(/^\uFEFF/, "");
for(let i=0;ir.some(c=>String(c||"").trim() !== ""));
}
function normalizeHeaderKey(h){
return String(h || "").trim().toLowerCase().replace(/\s+/g, " ");
}
function findReferenceRowForStudent(rows){
if(!rows.length) return null;
const headers = rows[0].map(normalizeHeaderKey);
const idIdx = headers.findIndex(h=>h === "student id" || h === "studentid" || h === "id");
if(idIdx < 0) throw new Error("CSV must include a Student ID column.");
const target = String(SUBMISSION_ID || "").trim();
for(let i=1;i{ obj[h] = rows[i][idx] ?? ""; });
return obj;
}
}
return null;
}
function scoresEqual(a, b){
const na = toNum(a);
const nb = toNum(b);
if(na === null && nb === null) return true;
if(na === null || nb === null) return false;
return na === nb;
}
function importReferenceCsv(event){
const file = event.target.files[0];
if(!file) return;
const reader = new FileReader();
reader.onload = ()=>{
try{
const rows = parseCsvText(reader.result);
if(rows.length < 2) throw new Error("Reference CSV has no student data rows.");
const ref = findReferenceRowForStudent(rows);
if(!ref){
alert(`No row found for Student ID "${SUBMISSION_ID}" in the reference CSV.\n\nMake sure the Student ID in this portfolio matches the CSV.`);
return;
}
const fieldMap = [
{period:"midterm", key:"cp", header:"midterm cp", label:"Midterm CP", studentSide:false},
{period:"midterm", key:"attendance", header:"midterm attendance", label:"Midterm Attendance", studentSide:true},
{period:"midterm", key:"quiz1", header:"midterm quiz 1", label:"Midterm Quiz 1", studentSide:true},
{period:"midterm", key:"quiz2", header:"midterm quiz 2", label:"Midterm Quiz 2", studentSide:true},
{period:"midterm", key:"quiz3", header:"midterm quiz 3", label:"Midterm Quiz 3", studentSide:true},
{period:"midterm", key:"quiz4", header:"midterm quiz 4", label:"Midterm Quiz 4", studentSide:true},
{period:"midterm", key:"exam", header:"midterm exam score", label:"Midterm Exam Score", studentSide:true},
{period:"finals", key:"cp", header:"finals cp", label:"Finals CP", studentSide:false},
{period:"finals", key:"attendance", header:"finals attendance", label:"Finals Attendance", studentSide:true},
{period:"finals", key:"quiz1", header:"finals quiz 1", label:"Finals Quiz 1", studentSide:true},
{period:"finals", key:"quiz2", header:"finals quiz 2", label:"Finals Quiz 2", studentSide:true},
{period:"finals", key:"quiz3", header:"finals quiz 3", label:"Finals Quiz 3", studentSide:true},
{period:"finals", key:"quiz4", header:"finals quiz 4", label:"Finals Quiz 4", studentSide:true},
{period:"finals", key:"exam", header:"finals exam score", label:"Finals Exam Score", studentSide:true}
];
const mismatches = [];
const matches = [];
fieldMap.forEach(field=>{
const refVal = ref[field.header];
if(!isFilled(refVal)) return;
const studentScores = getStudentPeriodScores(field.period);
const studentVal = field.studentSide ? studentScores[field.key] : (gradeData[field.period][field.key] || "");
const compareVal = field.studentSide ? studentVal : (isFilled(gradeData[field.period][field.key]) ? gradeData[field.period][field.key] : studentVal);
if(field.studentSide){
if(!isFilled(studentVal)){
// Student left blank — apply official value quietly as correction fill
gradeData[field.period][field.key] = String(refVal).trim();
mismatches.push({
period: field.period,
key: field.key,
label: field.label,
studentValue: "(blank)",
referenceValue: String(refVal).trim()
});
return;
}
if(!scoresEqual(studentVal, refVal)){
mismatches.push({
period: field.period,
key: field.key,
label: field.label,
studentValue: String(studentVal),
referenceValue: String(refVal).trim()
});
// Correct for grading using teacher override (student entry stays visible)
gradeData[field.period][field.key] = String(refVal).trim();
} else {
matches.push({period: field.period, key: field.key, label: field.label});
// Keep student value; clear override if it was only for this field? leave as-is if already matching
}
} else {
// CP: set from reference; flag if teacher already had a different CP
if(isFilled(gradeData[field.period][field.key]) && !scoresEqual(gradeData[field.period][field.key], refVal)){
mismatches.push({
period: field.period,
key: field.key,
label: field.label,
studentValue: String(gradeData[field.period][field.key]),
referenceValue: String(refVal).trim()
});
} else if(isFilled(gradeData[field.period][field.key]) && scoresEqual(gradeData[field.period][field.key], refVal)){
matches.push({period: field.period, key: field.key, label: field.label});
}
gradeData[field.period][field.key] = String(refVal).trim();
}
});
gradeData.crossCheck = {
importedAt: new Date().toISOString(),
studentId: String(SUBMISSION_ID || "").trim(),
mismatches,
matches,
reference: ref
};
// Append audit remark
const remarkNote = mismatches.length
? `Cross-check ${new Date().toLocaleString()}: ${mismatches.length} mismatch(es) corrected from reference CSV.`
: `Cross-check ${new Date().toLocaleString()}: all compared scores matched reference CSV.`;
["midterm","finals"].forEach(period=>{
const prev = gradeData[period].remarks || "";
if(!prev.includes("Cross-check")){
gradeData[period].remarks = prev ? `${prev}\n${remarkNote}` : remarkNote;
} else {
gradeData[period].remarks = `${prev}\n${remarkNote}`;
}
});
saveGradeData();
syncTeacherPanelFromData();
renderStudentScoreDisplay();
updateSummary();
if(mismatches.length){
alert(`⚠️ SCORE MISMATCH DETECTED\n\nStudent ID: ${SUBMISSION_ID}\n${mismatches.length} score(s) did not match the official reference.\n\nDetails:\n` +
mismatches.map(m=>`• ${m.label}: entered ${m.studentValue} → official ${m.referenceValue}`).join("\n") +
`\n\nOfficial reference values have been applied for grading.\nStudent-entered values remain visible for audit.`);
} else {
alert(`✅ Cross-check passed\n\nStudent ID ${SUBMISSION_ID} matches the official reference scores.\nNo corrections needed.`);
}
statusText.textContent = mismatches.length
? `Cross-check: ${mismatches.length} mismatch(es) corrected from reference.`
: "Cross-check: all scores matched the reference CSV.";
} catch(e){
console.error(e);
alert("Could not import reference CSV.\n\n" + (e.message || e));
} finally {
importReferenceCsvInput.value = "";
}
};
reader.readAsText(file);
}
function exportExcel(){
if(typeof XLSX === "undefined"){
alert("Excel library failed to load. Check your internet connection, or use Export CSV.");
return;
}
const {headers, values} = buildCombinedGradeRow();
const aoa = [headers, values];
const wb = XLSX.utils.book_new();
const ws = XLSX.utils.aoa_to_sheet(aoa);
XLSX.utils.book_append_sheet(wb, ws, "Grades");
XLSX.writeFile(wb, `${makeExportBaseName()}-grades.xlsx`);
}
function exportJson(){
downloadTextFile(JSON.stringify(buildExportPayload(), null, 2), `${makeExportBaseName()}-grades.json`, "application/json");
}
function exportCsv(){
const {headers, values} = buildCombinedGradeRow();
const lines = [
headers.map(escapeCsv).join(","),
values.map(escapeCsv).join(",")
];
downloadTextFile(lines.join("\n"), `${makeExportBaseName()}-grades.csv`, "text/csv");
}
function importJson(event){
const file = event.target.files[0];
if(!file) return;
const reader = new FileReader();
reader.onload = ()=>{
try{
const imported = JSON.parse(reader.result);
if(!imported) throw new Error("Invalid JSON.");
if(imported.midterm) Object.assign(gradeData.midterm, imported.midterm);
if(imported.finals) Object.assign(gradeData.finals, imported.finals);
if(imported.midtermScores){
["cp","attendance","quiz1","quiz2","quiz3","quiz4","exam","deduction","remarks"].forEach(k=>{
if(imported.midtermScores[k] !== undefined) gradeData.midterm[k] = imported.midtermScores[k];
});
}
if(imported.finalsScores){
["cp","attendance","quiz1","quiz2","quiz3","quiz4","exam","deduction","remarks"].forEach(k=>{
if(imported.finalsScores[k] !== undefined) gradeData.finals[k] = imported.finalsScores[k];
});
}
if(Array.isArray(imported.activities)){
for(const activity of imported.activities){
if(!activity.id) continue;
if(!gradeData.rubric[activity.id]) gradeData.rubric[activity.id] = {};
if(activity.rubric){
for(const c of RUBRIC_CRITERIA){
const item = activity.rubric[c.id];
if(item && item.score !== "" && item.score !== null && item.score !== undefined){
gradeData.rubric[activity.id][c.id] = Number(item.score);
}
}
}
gradeData.notes[activity.id] = activity.notes || "";
}
}
saveGradeData();
syncTeacherPanelFromData();
renderActivityList();
updateSummary();
alert("Grades imported successfully.");
} catch(e){
console.error(e);
alert("Could not import this JSON file.");
} finally {
importFileInput.value = "";
}
};
reader.readAsText(file);
}
function exportGradedHtml(){
const payload = buildExportPayload();
downloadTextFile(buildGradedHtml(payload), `${makeExportBaseName()}-graded-feedback.html`, "text/html");
}
function buildGradedHtml(payload){
const s = payload.standing;
return `${escapeHtml(payload.studentName)} - Graded Portfolio
Graded Portfolio Feedback
Overall %
${s.overall ?? "—"}
College Grade
${s.college ?? "—"}
Midterm %
${s.midterm.percentage ?? "—"}
Finals %
${s.finals.percentage ?? "—"}
Midterm Lab
${s.midterm.laboratory ?? "—"}
Finals Lab
${s.finals.laboratory ?? "—"}
Activities
${payload.activities.map(a=>`
${escapeHtml(a.activityNumber)}. ${escapeHtml(a.title)}
${a.hasLink?"Link submitted":"Missing link"} · ${escapeHtml(a.period)} · Score: ${a.totalScore}/${a.maxScore}
${a.notes?`
${escapeHtml(a.notes)}
`:""}
`).join("")}
`;
}
function shortenUrl(url){
if(!url) return "";
const clean = url.trim();
return clean.length <= 60 ? clean : clean.slice(0,38) + "..." + clean.slice(-18);
}
function downloadTextFile(content, fileName, mimeType){
const blob = new Blob([content], {type:mimeType});
const link = document.createElement("a");
const objectUrl = URL.createObjectURL(blob);
link.href = objectUrl;
link.download = fileName;
document.body.appendChild(link);
link.click();
link.remove();
URL.revokeObjectURL(objectUrl);
}
function escapeCsv(value){
const text = String(value ?? "");
return `"${text.replace(/"/g,'""')}"`;
}
function escapeHtml(value){
return String(value ?? "").replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'");
}
function slugify(value){
return String(value).toLowerCase().trim().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"");
}
function makeExportBaseName(){
return slugify(`${SECTION}-${STUDENT_NAME}-${PROFESSOR_NAME}`) || "activity-portfolio";
}
/* ORANGES — rare / occasional only */
const ORANGE_MIN_DELAY = 60000;
const ORANGE_MAX_DELAY = 180000;
const ORANGE_MESSAGES = ["Uy, Kaya Mo Yan","Proud Ako Sayo","Ang Galing Mo","Nakaka-Proud Ka","May Progress Ka","Ang Layo Mo Na","Tuloy-Tuloy Lang","One Step Closer","You Got This","You're Doing Great","Keep Going","Keep Growing","Keep Winning","Keep Learning","Achievement Unlocked","Main Character Energy","Future Graduate Energy","Trust The Process","Believe In Yourself","Small Wins Matter","Progress Is Progress","Your Effort Shows","Good Things Are Coming","You're Closer Than You Think"];
const GOLDEN_ORANGE_MESSAGES = ["Future You Is Proud","Malayo Na Narating Mo","Achievement Loading","Main Character Arc","Success Is Brewing","Level Up Incoming","Deserve Mo 'To","Your Time Is Coming","Keep The Fire Alive","Trust Your Journey"];
function startRandomOrangeEvents(){
// First possible appearance after ~1.5–3 minutes, then infrequently
setTimeout(()=>spawnOrangeBatch(), randomNumber(90000, 180000));
scheduleNextOrange();
}
function scheduleNextOrange(){
const delay = randomNumber(ORANGE_MIN_DELAY, ORANGE_MAX_DELAY);
setTimeout(()=>{ spawnOrangeBatch(); scheduleNextOrange(); }, delay);
}
function spawnOrangeBatch(){
// Most of the time, skip entirely so oranges feel rare
if(Math.random() > 0.28) return;
const roll = Math.random();
let count = 1;
if(roll > .95) count = 3;
else if(roll > .82) count = 2;
for(let i=0;ispawnFallingOrange(), i*randomNumber(250,900));
}
function spawnFallingOrange(){
const orange = document.createElement("div");
const isGolden = Math.random() < 0.08;
orange.className = "falling-orange" + (isGolden ? " golden" : "");
orange.innerHTML = '
';
const size = randomNumber(34,88), left = randomNumber(2,94), duration = randomNumber(3500,17000), rotate = randomNumber(260,900);
orange.style.setProperty("--orange-size", size + "px");
orange.style.setProperty("--orange-rotate", rotate + "deg");
orange.style.left = left + "vw";
orange.style.animationDuration = duration + "ms";
let popped = false;
orange.onclick = event=>{
event.stopPropagation();
if(popped) return;
popped = true;
const rect = orange.getBoundingClientRect();
orange.remove();
showOrangeMessage(rect.left + rect.width/2, rect.top + rect.height/2, isGolden);
};
orange.onanimationend = ()=>orange.remove();
document.body.appendChild(orange);
}
function showOrangeMessage(x,y,isGolden){
const message = document.createElement("div");
message.className = "orange-pop-message" + (isGolden ? " golden" : "");
message.textContent = isGolden ? randomFromArray(GOLDEN_ORANGE_MESSAGES) : randomFromArray(ORANGE_MESSAGES);
message.style.left = x + "px";
message.style.top = y + "px";
document.body.appendChild(message);
setTimeout(()=>message.remove(), 3400);
}
function randomFromArray(array){ return array[Math.floor(Math.random()*array.length)]; }
function randomNumber(min,max){ return Math.floor(Math.random()*(max-min+1))+min; }
startRandomOrangeEvents();