| 1 |
(function () { |
| 2 |
'use strict'; |
| 3 |
|
| 4 |
var GRADE_SCALE = [ |
| 5 |
{letter:'A+', min:97, dflt:'#059669'}, |
| 6 |
{letter:'A', min:93, dflt:'#10b981'}, |
| 7 |
{letter:'A−', min:90, dflt:'#34d399'}, |
| 8 |
{letter:'B+', min:87, dflt:'#2563eb'}, |
| 9 |
{letter:'B', min:83, dflt:'#3b82f6'}, |
| 10 |
{letter:'B−', min:80, dflt:'#60a5fa'}, |
| 11 |
{letter:'C+', min:77, dflt:'#d97706'}, |
| 12 |
{letter:'C', min:73, dflt:'#f59e0b'}, |
| 13 |
{letter:'C−', min:70, dflt:'#fbbf24'}, |
| 14 |
{letter:'D+', min:67, dflt:'#dc2626'}, |
| 15 |
{letter:'D', min:63, dflt:'#ef4444'}, |
| 16 |
{letter:'D−', min:60, dflt:'#f87171'}, |
| 17 |
{letter:'F', min:0, dflt:'#6b7280'} |
| 18 |
]; |
| 19 |
|
| 20 |
var GRADE_COLOR_KEYS = { |
| 21 |
'A+':'gradeAColor','A':'gradeAColor','A−':'gradeAColor', |
| 22 |
'B+':'gradeBColor','B':'gradeBColor','B−':'gradeBColor', |
| 23 |
'C+':'gradeCColor','C':'gradeCColor','C−':'gradeCColor', |
| 24 |
'D+':'gradeDColor','D':'gradeDColor','D−':'gradeDColor', |
| 25 |
'F':'gradeFColor' |
| 26 |
}; |
| 27 |
|
| 28 |
function getLetter(pct, opts) { |
| 29 |
for (var i = 0; i < GRADE_SCALE.length; i++) { |
| 30 |
if (pct >= GRADE_SCALE[i].min) { |
| 31 |
var key = GRADE_COLOR_KEYS[GRADE_SCALE[i].letter]; |
| 32 |
return {letter: GRADE_SCALE[i].letter, color: (opts[key] || GRADE_SCALE[i].dflt), dflt: GRADE_SCALE[i].dflt}; |
| 33 |
} |
| 34 |
} |
| 35 |
return {letter: 'F', color: opts.gradeFColor || '#6b7280', dflt: '#6b7280'}; |
| 36 |
} |
| 37 |
|
| 38 |
function calcAvg(subjects, mode) { |
| 39 |
if (!subjects.length) return 0; |
| 40 |
if (mode === 'simple') { |
| 41 |
var sum = 0, total = 0; |
| 42 |
subjects.forEach(function(s) { |
| 43 |
sum += parseFloat(s.score) || 0; |
| 44 |
total += parseFloat(s.max) || 100; |
| 45 |
}); |
| 46 |
return total > 0 ? (sum / total) * 100 : 0; |
| 47 |
} |
| 48 |
var totalWeight = 0, weightedSum = 0; |
| 49 |
subjects.forEach(function(s) { |
| 50 |
var sc = parseFloat(s.score) || 0; |
| 51 |
var mx = parseFloat(s.max) || 100; |
| 52 |
var wt = parseFloat(s.weight) || 0; |
| 53 |
var pct = mx > 0 ? (sc / mx) * 100 : 0; |
| 54 |
weightedSum += pct * wt; |
| 55 |
totalWeight += wt; |
| 56 |
}); |
| 57 |
return totalWeight > 0 ? weightedSum / totalWeight : 0; |
| 58 |
} |
| 59 |
|
| 60 |
function setStyle(el, styles) { |
| 61 |
Object.keys(styles).forEach(function(k) { el.style[k] = styles[k]; }); |
| 62 |
} |
| 63 |
|
| 64 |
function typoCssVarsForEl(typo, prefix, el) { |
| 65 |
if (!typo || typeof typo !== 'object') return; |
| 66 |
var map = { |
| 67 |
family:'font-family', weight:'font-weight', style:'font-style', |
| 68 |
decoration:'text-decoration', transform:'text-transform', |
| 69 |
sizeDesktop:'font-size-d', sizeTablet:'font-size-t', sizeMobile:'font-size-m', |
| 70 |
lineHeightDesktop:'line-height-d', lineHeightTablet:'line-height-t', lineHeightMobile:'line-height-m', |
| 71 |
letterSpacingDesktop:'letter-spacing-d', letterSpacingTablet:'letter-spacing-t', letterSpacingMobile:'letter-spacing-m', |
| 72 |
wordSpacingDesktop:'word-spacing-d', wordSpacingTablet:'word-spacing-t', wordSpacingMobile:'word-spacing-m' |
| 73 |
}; |
| 74 |
Object.keys(map).forEach(function(k) { |
| 75 |
if (typo[k] !== undefined && typo[k] !== '') { |
| 76 |
var v = typo[k]; |
| 77 |
if (['sizeDesktop','sizeTablet','sizeMobile'].indexOf(k) !== -1) v = v + (typo.sizeUnit || 'px'); |
| 78 |
else if (['lineHeightDesktop','lineHeightTablet','lineHeightMobile'].indexOf(k) !== -1) v = v + (typo.lineHeightUnit || ''); |
| 79 |
else if (['letterSpacingDesktop','letterSpacingTablet','letterSpacingMobile'].indexOf(k) !== -1) v = v + (typo.letterSpacingUnit || 'px'); |
| 80 |
else if (['wordSpacingDesktop','wordSpacingTablet','wordSpacingMobile'].indexOf(k) !== -1) v = v + (typo.wordSpacingUnit || 'px'); |
| 81 |
el.style.setProperty(prefix + map[k], String(v)); |
| 82 |
} |
| 83 |
}); |
| 84 |
} |
| 85 |
|
| 86 |
function makeEl(tag, attrs, children) { |
| 87 |
var el = document.createElement(tag); |
| 88 |
if (attrs) { |
| 89 |
Object.keys(attrs).forEach(function(k) { |
| 90 |
if (k === 'className') { el.className = attrs[k]; } |
| 91 |
else if (k === 'textContent') { el.textContent = attrs[k]; } |
| 92 |
else if (k === 'style' && typeof attrs[k] === 'object') { setStyle(el, attrs[k]); } |
| 93 |
else { el.setAttribute(k, attrs[k]); } |
| 94 |
}); |
| 95 |
} |
| 96 |
if (children) { |
| 97 |
(Array.isArray(children) ? children : [children]).forEach(function(c) { |
| 98 |
if (c == null) return; |
| 99 |
if (typeof c === 'string') { el.appendChild(document.createTextNode(c)); } |
| 100 |
else { el.appendChild(c); } |
| 101 |
}); |
| 102 |
} |
| 103 |
return el; |
| 104 |
} |
| 105 |
|
| 106 |
function input(type, value, placeholder, onChange) { |
| 107 |
var el = document.createElement('input'); |
| 108 |
el.type = type; |
| 109 |
el.value = value; |
| 110 |
el.className = 'bkbg-grc-input'; |
| 111 |
if (placeholder) el.placeholder = placeholder; |
| 112 |
el.addEventListener('input', function() { onChange(el.value); }); |
| 113 |
return el; |
| 114 |
} |
| 115 |
|
| 116 |
function initApp(app) { |
| 117 |
var opts; |
| 118 |
try { opts = JSON.parse(app.getAttribute('data-opts') || '{}'); } catch(e) { return; } |
| 119 |
|
| 120 |
var accent = opts.accentColor || '#6c3fb5'; |
| 121 |
var cardBg = opts.cardBg || '#fff'; |
| 122 |
var resultBg = opts.resultBg || accent; |
| 123 |
var resultClr = opts.resultColor || '#fff'; |
| 124 |
var cardR = (opts.cardRadius || 16) + 'px'; |
| 125 |
var inpR = (opts.inputRadius || 8) + 'px'; |
| 126 |
var maxW = (opts.maxWidth || 640) + 'px'; |
| 127 |
var ptop = (opts.paddingTop || 60) + 'px'; |
| 128 |
var pbot = (opts.paddingBottom || 60) + 'px'; |
| 129 |
|
| 130 |
var subjects = [ |
| 131 |
{id:1, name:'Math', score:90, max:100, weight:30}, |
| 132 |
{id:2, name:'English', score:85, max:100, weight:30}, |
| 133 |
{id:3, name:'Science', score:78, max:100, weight:20}, |
| 134 |
{id:4, name:'History', score:92, max:100, weight:20} |
| 135 |
]; |
| 136 |
var nextId = 10; |
| 137 |
|
| 138 |
app.style.fontFamily = '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif'; |
| 139 |
app.style.paddingTop = ptop; |
| 140 |
app.style.paddingBottom = pbot; |
| 141 |
if (opts.sectionBg) app.style.background = opts.sectionBg; |
| 142 |
|
| 143 |
typoCssVarsForEl(opts.typoTitle, '--bkbg-grc-tt-', app); |
| 144 |
typoCssVarsForEl(opts.typoResult, '--bkbg-grc-rs-', app); |
| 145 |
typoCssVarsForEl(opts.typoLetter, '--bkbg-grc-lt-', app); |
| 146 |
|
| 147 |
/* ── Card ── */ |
| 148 |
var card = makeEl('div', {className:'bkbg-grc-card', style:{ |
| 149 |
background: cardBg, |
| 150 |
borderRadius: cardR, |
| 151 |
padding: '32px', |
| 152 |
maxWidth: maxW, |
| 153 |
margin: '0 auto', |
| 154 |
boxShadow: '0 4px 24px rgba(0,0,0,.09)' |
| 155 |
}}); |
| 156 |
|
| 157 |
/* ── Header ── */ |
| 158 |
if (opts.showTitle && opts.title) { |
| 159 |
var titleEl = makeEl('div', {className:'bkbg-grc-title', style:{ |
| 160 |
color: opts.titleColor || '#1e1b4b' |
| 161 |
}, textContent: opts.title}); |
| 162 |
card.appendChild(titleEl); |
| 163 |
} |
| 164 |
if (opts.showSubtitle && opts.subtitle) { |
| 165 |
var subEl = makeEl('div', {className:'bkbg-grc-subtitle', style:{ |
| 166 |
color: opts.subtitleColor || '#6b7280' |
| 167 |
}, textContent: opts.subtitle}); |
| 168 |
card.appendChild(subEl); |
| 169 |
} |
| 170 |
|
| 171 |
/* ── Result card ── */ |
| 172 |
var resultCard = makeEl('div', {className:'bkbg-grc-result-card', style:{ |
| 173 |
background: resultBg, |
| 174 |
color: resultClr, |
| 175 |
borderRadius: '12px', |
| 176 |
padding: '20px 24px', |
| 177 |
display: 'flex', |
| 178 |
alignItems: 'center', |
| 179 |
justifyContent: 'center', |
| 180 |
gap: '24px', |
| 181 |
marginBottom: '20px', |
| 182 |
flexWrap: 'wrap' |
| 183 |
}}); |
| 184 |
|
| 185 |
var pctEl = makeEl('div', {className:'bkbg-grc-result-pct'}); |
| 186 |
var modeEl = makeEl('div', {className:'bkbg-grc-result-label', style:{fontSize:'14px',opacity:'0.8',marginTop:'4px'}}); |
| 187 |
modeEl.textContent = opts.gradeMode === 'simple' ? 'Simple Average' : 'Weighted Average'; |
| 188 |
var avgBlock = makeEl('div', {className:'bkbg-grc-result-avg'}, [pctEl, modeEl]); |
| 189 |
resultCard.appendChild(avgBlock); |
| 190 |
|
| 191 |
var letterEl; |
| 192 |
if (opts.showLetterGrade !== false) { |
| 193 |
var divider = makeEl('div', {className:'bkbg-grc-result-divider', style:{width:'1px',height:'60px',background:'rgba(255,255,255,.25)',flexShrink:'0'}}); |
| 194 |
resultCard.appendChild(divider); |
| 195 |
letterEl = makeEl('div', {className:'bkbg-grc-result-letter'}); |
| 196 |
var letterLabel = makeEl('div',{className:'bkbg-grc-result-letter-label',style:{fontSize:'13px',opacity:'0.8',marginTop:'4px'},textContent:'Letter Grade'}); |
| 197 |
var letterBlock = makeEl('div',{className:'bkbg-grc-result-letter-block',style:{textAlign:'center'}},[letterEl,letterLabel]); |
| 198 |
resultCard.appendChild(letterBlock); |
| 199 |
} |
| 200 |
card.appendChild(resultCard); |
| 201 |
|
| 202 |
/* ── Subjects table ── */ |
| 203 |
var tableWrap = makeEl('div', {className:'bkbg-grc-breakdown'}); |
| 204 |
if (opts.showBreakdown !== false) { |
| 205 |
var tHead = makeEl('div',{className:'bkbg-grc-table-header',style:{ |
| 206 |
display:'grid', |
| 207 |
gridTemplateColumns:'2fr 1fr 1fr 1fr 80px', |
| 208 |
gap:'6px', |
| 209 |
padding:'6px 10px', |
| 210 |
marginBottom:'4px' |
| 211 |
}}); |
| 212 |
['Subject','Score','Max','Weight %',''].forEach(function(txt){ |
| 213 |
tHead.appendChild(makeEl('div',{className:'bkbg-grc-table-header-cell',style:{fontSize:'12px',fontWeight:'600',color:opts.labelColor||'#374151'},textContent:txt})); |
| 214 |
}); |
| 215 |
tableWrap.appendChild(tHead); |
| 216 |
card.appendChild(tableWrap); |
| 217 |
} |
| 218 |
|
| 219 |
/* ── Add Subject button ── */ |
| 220 |
var addBtn = makeEl('button',{className:'bkbg-grc-add-btn',style:{ |
| 221 |
marginTop:'8px', |
| 222 |
padding:'9px 18px', |
| 223 |
background:accent, |
| 224 |
color:'#fff', |
| 225 |
border:'none', |
| 226 |
borderRadius:inpR, |
| 227 |
fontWeight:'700', |
| 228 |
cursor:'pointer', |
| 229 |
fontSize:'14px' |
| 230 |
},textContent:'+ Add Subject'}); |
| 231 |
|
| 232 |
/* ── Grade scale ── */ |
| 233 |
var scaleSection = makeEl('div',{style:{display: opts.showGradeScale !== false ? 'block' : 'none'}}); |
| 234 |
|
| 235 |
/* ── Render ── */ |
| 236 |
function renderSubjects() { |
| 237 |
// Remove old rows (all except header) |
| 238 |
var rows = tableWrap.querySelectorAll('.bkbg-grc-subject-row'); |
| 239 |
rows.forEach(function(r) { r.parentNode.removeChild(r); }); |
| 240 |
|
| 241 |
subjects.forEach(function(s) { |
| 242 |
var spct = (parseFloat(s.max)||100) > 0 ? ((parseFloat(s.score)||0)/(parseFloat(s.max)||100))*100 : 0; |
| 243 |
var sg = getLetter(spct, opts); |
| 244 |
|
| 245 |
var row = makeEl('div',{className:'bkbg-grc-subject-row', style:{ |
| 246 |
display:'grid', |
| 247 |
gridTemplateColumns:'2fr 1fr 1fr 1fr 80px', |
| 248 |
gap:'6px', |
| 249 |
alignItems:'center', |
| 250 |
background:opts.rowBg||'#f9fafb', |
| 251 |
border:'1px solid '+(opts.rowBorder||'#e5e7eb'), |
| 252 |
borderRadius:'8px', |
| 253 |
padding:'8px 10px', |
| 254 |
marginBottom:'6px' |
| 255 |
}}); |
| 256 |
|
| 257 |
var nameInp = input('text', s.name, 'Subject', function(v){ s.name=v; updateResult(); }); |
| 258 |
nameInp.style.borderRadius = inpR; |
| 259 |
nameInp.style.borderColor = opts.inputBorder || '#e5e7eb'; |
| 260 |
var scoreInp = input('number', s.score, '', function(v){ s.score=parseFloat(v)||0; renderSubjects(); updateResult(); }); |
| 261 |
scoreInp.style.borderRadius = inpR; |
| 262 |
scoreInp.style.borderColor = opts.inputBorder || '#e5e7eb'; |
| 263 |
var maxInp = input('number', s.max, '', function(v){ s.max=parseFloat(v)||100; renderSubjects(); updateResult(); }); |
| 264 |
maxInp.style.borderRadius = inpR; |
| 265 |
maxInp.style.borderColor = opts.inputBorder || '#e5e7eb'; |
| 266 |
var wInp = input('number', s.weight, '', function(v){ s.weight=parseFloat(v)||0; updateResult(); }); |
| 267 |
wInp.style.borderRadius = inpR; |
| 268 |
wInp.style.borderColor = opts.inputBorder || '#e5e7eb'; |
| 269 |
|
| 270 |
var badge = makeEl('span',{className:'bkbg-grc-grade-badge',style:{fontWeight:'700',fontSize:'13px',color:sg.color,minWidth:'28px',textAlign:'center'},textContent:sg.letter}); |
| 271 |
var removeBtn = makeEl('button',{className:'bkbg-grc-remove-btn',textContent:'×'}); |
| 272 |
removeBtn.addEventListener('click', function(){ |
| 273 |
subjects = subjects.filter(function(x){ return x.id !== s.id; }); |
| 274 |
renderSubjects(); |
| 275 |
updateResult(); |
| 276 |
}); |
| 277 |
|
| 278 |
var actions = makeEl('div',{style:{display:'flex',alignItems:'center',gap:'6px',justifyContent:'flex-end'}},[badge,removeBtn]); |
| 279 |
row.appendChild(nameInp); |
| 280 |
row.appendChild(scoreInp); |
| 281 |
row.appendChild(maxInp); |
| 282 |
row.appendChild(wInp); |
| 283 |
row.appendChild(actions); |
| 284 |
tableWrap.appendChild(row); |
| 285 |
}); |
| 286 |
} |
| 287 |
|
| 288 |
function updateResult() { |
| 289 |
var avg = calcAvg(subjects, opts.gradeMode || 'weighted'); |
| 290 |
pctEl.textContent = avg.toFixed(1) + '%'; |
| 291 |
if (letterEl) { |
| 292 |
var info = getLetter(avg, opts); |
| 293 |
letterEl.textContent = info.letter; |
| 294 |
} |
| 295 |
renderScale(); |
| 296 |
} |
| 297 |
|
| 298 |
/* ── Grade scale rendering ── */ |
| 299 |
function renderScale() { |
| 300 |
scaleSection.innerHTML = ''; |
| 301 |
if (opts.showGradeScale === false) return; |
| 302 |
var avg = calcAvg(subjects, opts.gradeMode || 'weighted'); |
| 303 |
|
| 304 |
scaleSection.appendChild(makeEl('div',{className:'bkbg-grc-scale-title',style:{ |
| 305 |
fontSize:'12px',fontWeight:'700',color:opts.labelColor||'#374151', |
| 306 |
textTransform:'uppercase',letterSpacing:'.05em',marginBottom:'10px' |
| 307 |
},textContent:'Grade Scale'})); |
| 308 |
|
| 309 |
var grid = makeEl('div',{style:{display:'flex',flexWrap:'wrap',gap:'6px'}}); |
| 310 |
GRADE_SCALE.forEach(function(gs) { |
| 311 |
var key = GRADE_COLOR_KEYS[gs.letter]; |
| 312 |
var clr = opts[key] || gs.dflt; |
| 313 |
var active = avg >= gs.min && (gs.min === 0 || avg < (GRADE_SCALE[GRADE_SCALE.indexOf(gs)-1]||{min:101}).min); |
| 314 |
var item = makeEl('div',{style:{ |
| 315 |
background: active ? clr+'22' : (opts.scaleBg||'#f3f4f6'), |
| 316 |
border: '2px solid ' + clr, |
| 317 |
borderRadius:'6px', |
| 318 |
padding:'5px 10px', |
| 319 |
textAlign:'center', |
| 320 |
minWidth:'52px', |
| 321 |
transition:'background .2s' |
| 322 |
}}); |
| 323 |
item.appendChild(makeEl('div',{style:{fontWeight:'800',fontSize:'14px',color:clr},textContent:gs.letter})); |
| 324 |
item.appendChild(makeEl('div',{style:{fontSize:'11px',color:'#6b7280'},textContent:gs.min+'%+'})); |
| 325 |
grid.appendChild(item); |
| 326 |
}); |
| 327 |
scaleSection.appendChild(grid); |
| 328 |
} |
| 329 |
|
| 330 |
addBtn.addEventListener('click', function(){ |
| 331 |
nextId++; |
| 332 |
subjects.push({id:nextId, name:'', score:0, max:100, weight:0}); |
| 333 |
renderSubjects(); |
| 334 |
updateResult(); |
| 335 |
}); |
| 336 |
|
| 337 |
var scaleWrap = makeEl('div',{style:{ |
| 338 |
marginTop:'20px', |
| 339 |
background: opts.scaleBg||'#f3f4f6', |
| 340 |
border:'1px solid '+(opts.scaleBorder||'#e5e7eb'), |
| 341 |
borderRadius:'10px', |
| 342 |
padding:'14px 16px' |
| 343 |
}}); |
| 344 |
scaleWrap.appendChild(scaleSection); |
| 345 |
|
| 346 |
if (opts.showBreakdown !== false) { |
| 347 |
tableWrap.appendChild(addBtn); |
| 348 |
} |
| 349 |
card.appendChild(scaleWrap); |
| 350 |
app.appendChild(card); |
| 351 |
|
| 352 |
renderSubjects(); |
| 353 |
updateResult(); |
| 354 |
} |
| 355 |
|
| 356 |
document.querySelectorAll('.bkbg-grc-app').forEach(initApp); |
| 357 |
})(); |
| 358 |
|