| 1 |
(function () { |
| 2 |
'use strict'; |
| 3 |
|
| 4 |
var GRADE_POINTS = { |
| 5 |
'A+': 4.0, 'A': 4.0, 'A-': 3.7, |
| 6 |
'B+': 3.3, 'B': 3.0, 'B-': 2.7, |
| 7 |
'C+': 2.3, 'C': 2.0, 'C-': 1.7, |
| 8 |
'D+': 1.3, 'D': 1.0, 'D-': 0.7, |
| 9 |
'F': 0.0 |
| 10 |
}; |
| 11 |
var GRADE_KEYS = Object.keys(GRADE_POINTS); |
| 12 |
|
| 13 |
function calcGPA(courses) { |
| 14 |
var totalCredits = 0, totalPoints = 0; |
| 15 |
courses.forEach(function (c) { |
| 16 |
var pts = GRADE_POINTS[c.grade]; |
| 17 |
var cr = parseFloat(c.credits) || 0; |
| 18 |
if (pts !== undefined && cr > 0) { |
| 19 |
totalCredits += cr; |
| 20 |
totalPoints += pts * cr; |
| 21 |
} |
| 22 |
}); |
| 23 |
return { |
| 24 |
gpa: totalCredits > 0 ? totalPoints / totalCredits : 0, |
| 25 |
totalCredits: totalCredits, |
| 26 |
totalPoints: totalPoints |
| 27 |
}; |
| 28 |
} |
| 29 |
|
| 30 |
function gradeClass(gpa) { |
| 31 |
if (gpa >= 3.9) return 'Summa Cum Laude'; |
| 32 |
if (gpa >= 3.7) return 'Magna Cum Laude'; |
| 33 |
if (gpa >= 3.5) return 'Cum Laude'; |
| 34 |
if (gpa >= 3.0) return 'Good Standing'; |
| 35 |
if (gpa >= 2.0) return 'Satisfactory'; |
| 36 |
if (gpa > 0) return 'Probation Risk'; |
| 37 |
return '—'; |
| 38 |
} |
| 39 |
|
| 40 |
function gradeClassColor(gpa, accent) { |
| 41 |
if (gpa >= 3.5) return '#15803d'; |
| 42 |
if (gpa >= 3.0) return accent; |
| 43 |
if (gpa >= 2.0) return '#d97706'; |
| 44 |
return '#dc2626'; |
| 45 |
} |
| 46 |
|
| 47 |
function esc(s) { |
| 48 |
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); |
| 49 |
} |
| 50 |
|
| 51 |
function typoCssVarsForEl(typo, prefix, el) { |
| 52 |
if (!typo || typeof typo !== 'object') return; |
| 53 |
var map = { |
| 54 |
family: 'font-family', weight: 'font-weight', style: 'font-style', |
| 55 |
decoration: 'text-decoration', transform: 'text-transform', |
| 56 |
sizeDesktop: 'font-size-d', sizeTablet: 'font-size-t', sizeMobile: 'font-size-m', |
| 57 |
lineHeightDesktop: 'line-height-d', lineHeightTablet: 'line-height-t', lineHeightMobile: 'line-height-m', |
| 58 |
letterSpacingDesktop: 'letter-spacing-d', letterSpacingTablet: 'letter-spacing-t', letterSpacingMobile: 'letter-spacing-m', |
| 59 |
wordSpacingDesktop: 'word-spacing-d', wordSpacingTablet: 'word-spacing-t', wordSpacingMobile: 'word-spacing-m' |
| 60 |
}; |
| 61 |
Object.keys(map).forEach(function (k) { |
| 62 |
if (typo[k] !== undefined && typo[k] !== '') { |
| 63 |
var v = typo[k]; |
| 64 |
if (['sizeDesktop','sizeTablet','sizeMobile'].indexOf(k) !== -1) { |
| 65 |
v = v + (typo.sizeUnit || 'px'); |
| 66 |
} else if (['lineHeightDesktop','lineHeightTablet','lineHeightMobile'].indexOf(k) !== -1) { |
| 67 |
v = v + (typo.lineHeightUnit || ''); |
| 68 |
} else if (['letterSpacingDesktop','letterSpacingTablet','letterSpacingMobile'].indexOf(k) !== -1) { |
| 69 |
v = v + (typo.letterSpacingUnit || 'px'); |
| 70 |
} else if (['wordSpacingDesktop','wordSpacingTablet','wordSpacingMobile'].indexOf(k) !== -1) { |
| 71 |
v = v + (typo.wordSpacingUnit || 'px'); |
| 72 |
} |
| 73 |
el.style.setProperty(prefix + map[k], String(v)); |
| 74 |
} |
| 75 |
}); |
| 76 |
} |
| 77 |
|
| 78 |
function buildApp(app) { |
| 79 |
var opts; |
| 80 |
try { opts = JSON.parse(app.getAttribute('data-opts') || '{}'); } catch (e) { return; } |
| 81 |
var o = opts; |
| 82 |
var accent = o.accentColor || '#6c3fb5'; |
| 83 |
|
| 84 |
/* State */ |
| 85 |
var courses = (o.defaultCourses || []).map(function (c) { |
| 86 |
return { name: c.name, grade: c.grade, credits: c.credits }; |
| 87 |
}); |
| 88 |
|
| 89 |
app.style.paddingTop = (o.paddingTop || 60) + 'px'; |
| 90 |
app.style.paddingBottom = (o.paddingBottom || 60) + 'px'; |
| 91 |
if (o.sectionBg) app.style.background = o.sectionBg; |
| 92 |
|
| 93 |
typoCssVarsForEl(o.typoTitle, '--bkbg-gpa-tt-', app); |
| 94 |
typoCssVarsForEl(o.typoGpa, '--bkbg-gpa-gp-', app); |
| 95 |
|
| 96 |
/* Header */ |
| 97 |
var maxW = (o.maxWidth || 680) + 'px'; |
| 98 |
if (o.showTitle || o.showSubtitle) { |
| 99 |
var hdr = document.createElement('div'); |
| 100 |
hdr.style.cssText = 'text-align:center;margin-bottom:32px;max-width:' + maxW + ';margin-left:auto;margin-right:auto'; |
| 101 |
if (o.showTitle && o.title) { |
| 102 |
var ht = document.createElement('h3'); |
| 103 |
ht.className = 'bkbg-gpa-title'; |
| 104 |
ht.textContent = o.title; |
| 105 |
ht.style.cssText = 'color:' + (o.titleColor || '#1e1b4b') + ';margin:0 0 8px'; |
| 106 |
hdr.appendChild(ht); |
| 107 |
} |
| 108 |
if (o.showSubtitle && o.subtitle) { |
| 109 |
var hs = document.createElement('p'); |
| 110 |
hs.textContent = o.subtitle; |
| 111 |
hs.style.cssText = 'color:' + (o.subtitleColor || '#6b7280') + ';margin:0'; |
| 112 |
hdr.appendChild(hs); |
| 113 |
} |
| 114 |
app.appendChild(hdr); |
| 115 |
} |
| 116 |
|
| 117 |
/* Card */ |
| 118 |
var card = document.createElement('div'); |
| 119 |
card.className = 'bkbg-gpa-card'; |
| 120 |
card.style.cssText = 'background:' + (o.cardBg || '#fff') + ';border-radius:' + (o.cardRadius || 16) + 'px;padding:32px;max-width:' + maxW + ';margin:0 auto;box-shadow:0 4px 24px rgba(0,0,0,0.06)'; |
| 121 |
app.appendChild(card); |
| 122 |
|
| 123 |
/* Result area */ |
| 124 |
var resultArea = document.createElement('div'); |
| 125 |
resultArea.className = 'bkbg-gpa-result'; |
| 126 |
resultArea.style.cssText = 'background:' + (o.resultBg || '#f5f3ff') + ';border:2px solid ' + (o.resultBorder || '#ede9fe') + ';border-radius:' + (o.cardRadius || 16) + 'px;padding:24px 32px;text-align:center;margin-bottom:24px'; |
| 127 |
card.appendChild(resultArea); |
| 128 |
|
| 129 |
/* Table header */ |
| 130 |
var tHead = document.createElement('div'); |
| 131 |
tHead.style.cssText = 'display:grid;grid-template-columns:1fr 100px 80px 40px;background:' + (o.tableHeaderBg || '#f9fafb') + ';border-radius:8px 8px 0 0;padding:8px 12px;font-weight:600;font-size:13px;color:' + (o.subtitleColor || '#6b7280') + ';gap:8px'; |
| 132 |
tHead.innerHTML = '<span>Course</span><span style="text-align:center">Grade</span><span style="text-align:center">Credits</span><span></span>'; |
| 133 |
card.appendChild(tHead); |
| 134 |
|
| 135 |
/* Rows container */ |
| 136 |
var tbody = document.createElement('div'); |
| 137 |
tbody.className = 'bkbg-gpa-tbody'; |
| 138 |
card.appendChild(tbody); |
| 139 |
|
| 140 |
/* Add button */ |
| 141 |
var addBtn = document.createElement('button'); |
| 142 |
addBtn.className = 'bkbg-gpa-add-btn'; |
| 143 |
addBtn.textContent = '+ Add Course'; |
| 144 |
addBtn.style.cssText = 'background:' + accent + ';color:#fff;margin-top:12px;border:none;border-radius:' + (o.inputRadius || 8) + 'px;padding:8px 18px;cursor:pointer;font-size:14px;font-weight:600'; |
| 145 |
addBtn.addEventListener('click', function () { |
| 146 |
courses.push({ name: 'New Course', grade: 'B', credits: 3 }); |
| 147 |
render(); |
| 148 |
}); |
| 149 |
card.appendChild(addBtn); |
| 150 |
|
| 151 |
/* Grade scale help */ |
| 152 |
if (o.showHelp !== false) { |
| 153 |
var help = document.createElement('div'); |
| 154 |
help.className = 'bkbg-gpa-scale-help'; |
| 155 |
['A/A+ = 4.0', 'A- = 3.7', 'B+ = 3.3', 'B = 3.0', 'B- = 2.7', 'C+ = 2.3', 'C = 2.0', 'D = 1.0'].forEach(function (t) { |
| 156 |
var sp = document.createElement('span'); |
| 157 |
sp.textContent = t; |
| 158 |
help.appendChild(sp); |
| 159 |
}); |
| 160 |
card.appendChild(help); |
| 161 |
} |
| 162 |
|
| 163 |
function renderResult() { |
| 164 |
var res = calcGPA(courses); |
| 165 |
var gpa = res.gpa.toFixed(2); |
| 166 |
var html = '<div class="bkbg-gpa-number" style="color:' + (o.gpaColor || accent) + '">' + esc(gpa) + '</div>'; |
| 167 |
html += '<div style="color:' + (o.subtitleColor || '#6b7280') + ';margin-top:4px">GPA (4.0 scale)</div>'; |
| 168 |
if (o.showGradeClass !== false) { |
| 169 |
html += '<div class="bkbg-gpa-classification" style="color:' + gradeClassColor(res.gpa, accent) + '">' + esc(gradeClass(res.gpa)) + '</div>'; |
| 170 |
} |
| 171 |
if (o.showTotals !== false) { |
| 172 |
html += '<div style="display:flex;justify-content:center;gap:32px;margin-top:16px">'; |
| 173 |
html += '<div style="text-align:center"><div style="font-size:22px;font-weight:700;color:' + (o.labelColor || '#374151') + '">' + res.totalCredits + '</div><div style="font-size:12px;color:' + (o.subtitleColor || '#6b7280') + '">Credits</div></div>'; |
| 174 |
html += '<div style="text-align:center"><div style="font-size:22px;font-weight:700;color:' + (o.labelColor || '#374151') + '">' + res.totalPoints.toFixed(1) + '</div><div style="font-size:12px;color:' + (o.subtitleColor || '#6b7280') + '">Grade Points</div></div>'; |
| 175 |
html += '</div>'; |
| 176 |
} |
| 177 |
resultArea.innerHTML = html; |
| 178 |
} |
| 179 |
|
| 180 |
function render() { |
| 181 |
tbody.innerHTML = ''; |
| 182 |
courses.forEach(function (c, i) { |
| 183 |
var row = document.createElement('div'); |
| 184 |
row.className = 'bkbg-gpa-row'; |
| 185 |
row.style.background = i % 2 === 0 ? '#fff' : '#fafafa'; |
| 186 |
|
| 187 |
/* Name input */ |
| 188 |
var nameIn = document.createElement('input'); |
| 189 |
nameIn.type = 'text'; nameIn.value = c.name; nameIn.placeholder = 'Course name'; |
| 190 |
nameIn.style.borderRadius = (o.inputRadius || 8) + 'px'; |
| 191 |
nameIn.addEventListener('input', function () { courses[i].name = nameIn.value; }); |
| 192 |
|
| 193 |
/* Grade select */ |
| 194 |
var gradeEl = document.createElement('select'); |
| 195 |
gradeEl.style.borderRadius = (o.inputRadius || 8) + 'px'; |
| 196 |
gradeEl.style.textAlign = 'center'; |
| 197 |
GRADE_KEYS.forEach(function (g) { |
| 198 |
var opt = document.createElement('option'); |
| 199 |
opt.value = g; opt.textContent = g; |
| 200 |
if (g === c.grade) opt.selected = true; |
| 201 |
gradeEl.appendChild(opt); |
| 202 |
}); |
| 203 |
gradeEl.addEventListener('change', function () { courses[i].grade = gradeEl.value; renderResult(); }); |
| 204 |
|
| 205 |
/* Credits input */ |
| 206 |
var credIn = document.createElement('input'); |
| 207 |
credIn.type = 'number'; credIn.min = '0'; credIn.step = '0.5'; credIn.value = c.credits; |
| 208 |
credIn.style.borderRadius = (o.inputRadius || 8) + 'px'; |
| 209 |
credIn.style.textAlign = 'center'; |
| 210 |
credIn.addEventListener('input', function () { courses[i].credits = parseFloat(credIn.value) || 0; renderResult(); }); |
| 211 |
|
| 212 |
/* Remove btn */ |
| 213 |
var rmBtn = document.createElement('button'); |
| 214 |
rmBtn.className = 'bkbg-gpa-remove-btn'; |
| 215 |
rmBtn.textContent = '✕'; |
| 216 |
rmBtn.addEventListener('click', function () { courses.splice(i, 1); render(); }); |
| 217 |
|
| 218 |
row.appendChild(nameIn); |
| 219 |
row.appendChild(gradeEl); |
| 220 |
row.appendChild(credIn); |
| 221 |
row.appendChild(rmBtn); |
| 222 |
tbody.appendChild(row); |
| 223 |
}); |
| 224 |
renderResult(); |
| 225 |
} |
| 226 |
|
| 227 |
render(); |
| 228 |
} |
| 229 |
|
| 230 |
document.querySelectorAll('.bkbg-gpa-app').forEach(buildApp); |
| 231 |
})(); |
| 232 |
|