Untitled
unknown
plain_text
10 months ago
22 kB
15
Indexable
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Academic Performance Tracker</title>
<style>
:root{
--bg:#0f172a; --panel:#111827; --muted:#94a3b8; --text:#e5e7eb;
--accent:#22c55e; --warn:#f59e0b; --danger:#ef4444; --card:#0b1220;
--border:#1f2937; --blue:#3b82f6; --purple:#8b5cf6;
}
*{box-sizing:border-box}
body{margin:0; background:linear-gradient(180deg,#0b1020, #0f172a 40%, #0b1020);
color:var(--text); font-family:system-ui,-apple-system,Segoe UI,Roboto,Inter,Arial,sans-serif}
header{padding:20px; border-bottom:1px solid var(--border); background:rgba(17,24,39,.6); backdrop-filter: blur(8px); position:sticky; top:0; z-index:10}
header h1{margin:0; font-size:20px}
main{max-width:1100px; margin:20px auto; padding:0 16px 40px}
.grid{display:grid; gap:16px}
@media(min-width:900px){ .grid{grid-template-columns: 1.1fr 1fr} }
.card{background:linear-gradient(180deg, #0d1528, #0a1120); border:1px solid var(--border); border-radius:14px; padding:16px}
.card h2{margin:0 0 12px; font-size:16px; color:#e2e8f0}
label{display:block; font-size:12px; color:var(--muted); margin-bottom:6px}
input, select, button, textarea{
width:100%; padding:10px 12px; border-radius:10px; border:1px solid var(--border); background:#0a0f1e; color:var(--text);
}
input:focus, select:focus, button:focus{outline:1px solid var(--blue)}
.row{display:grid; gap:10px; grid-template-columns:1fr}
@media(min-width:640px){ .row.two{grid-template-columns:1fr 1fr} .row.three{grid-template-columns:1fr 1fr 1fr} }
button{cursor:pointer; border:1px solid #1f2b46; background:linear-gradient(180deg,#14213d,#0f1b34); color:#e5e7eb}
button.primary{background:linear-gradient(180deg,#1e40af,#1b235a); border-color:#243b7b}
button.success{background:linear-gradient(180deg,#166534,#064e3b); border-color:#115e38}
button.warn{background:linear-gradient(180deg,#92400e,#7c2d12); border-color:#4a1d09}
.flex{display:flex; gap:10px; align-items:center; flex-wrap:wrap}
.badge{font-size:11px; padding:3px 8px; border-radius:999px; border:1px solid var(--border); background:#0a0f1e; color:var(--muted)}
.kpi{display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:10px}
.kpi .box{background:#0a0f1e; border:1px solid var(--border); border-radius:12px; padding:12px}
.kpi .value{font-size:22px; font-weight:600}
table{width:100%; border-collapse:collapse; font-size:14px; margin-top:8px}
th, td{padding:10px; border-bottom:1px solid var(--border); vertical-align:middle}
th{text-align:left; color:#cbd5e1}
tr:hover td{background:#0b1326}
.progress{height:10px; background:#0b1020; border:1px solid var(--border); border-radius:999px; overflow:hidden}
.bar{height:100%; background:linear-gradient(90deg, #22c55e, #3b82f6); width:0%}
.status{font-size:12px}
.status.ok{color:var(--accent)}
.status.warn{color:var(--warn)}
.status.bad{color:var(--danger)}
.pill{padding:2px 8px; border-radius:999px; border:1px solid var(--border); color:#cbd5e1; font-size:12px}
.right{margin-left:auto}
.muted{color:var(--muted)}
.small{font-size:12px}
.spacer{height:8px}
.divider{border-top:1px solid var(--border); margin:10px 0}
.footer{color:var(--muted); font-size:12px; text-align:center; margin-top:20px}
</style>
</head>
<body>
<header>
<div class="flex">
<h1>Academic Performance Tracker</h1>
<span class="pill">Local save</span>
<span class="pill">GPA 4.0 scale</span>
<span class="pill">Weighted assessments</span>
<span class="right small muted">Tip: export a backup regularly</span>
</div>
</header>
<main>
<div class="grid">
<section class="card">
<h2>Setup: semesters and courses</h2>
<div class="row two">
<div>
<label>Semester name</label>
<input id="semesterName" placeholder="e.g., 2025 S1" />
</div>
<div>
<label> </label>
<button class="primary" id="addSemesterBtn">Add semester</button>
</div>
</div>
<div class="divider"></div>
<div class="row three">
<div>
<label>Semester</label>
<select id="semesterSelect"></select>
</div>
<div>
<label>Course name</label>
<input id="courseName" placeholder="e.g., General Chemistry" />
</div>
<div>
<label>Course code</label>
<input id="courseCode" placeholder="e.g., CHM-101" />
</div>
</div>
<div class="row three">
<div>
<label>Credits</label>
<input id="courseCredits" type="number" min="0" step="0.5" placeholder="e.g., 3" />
</div>
<div>
<label>Target grade (percent)</label>
<input id="targetPercent" type="number" min="0" max="100" step="1" placeholder="e.g., 75" />
</div>
<div>
<label> </label>
<button class="success" id="addCourseBtn">Add course</button>
</div>
</div>
<div class="divider"></div>
<div class="flex">
<button id="exportBtn">Export backup (JSON)</button>
<input id="importFile" type="file" accept="application/json" />
<button class="warn" id="importBtn">Import</button>
<span class="small muted">Import replaces current data after confirmation.</span>
</div>
</section>
<section class="card">
<h2>GPA and progress</h2>
<div class="kpi">
<div class="box">
<div class="small muted">Semester GPA</div>
<div class="value" id="semesterGpa">—</div>
<div class="small muted" id="semesterGpaNote"></div>
</div>
<div class="box">
<div class="small muted">Cumulative GPA</div>
<div class="value" id="cumulativeGpa">—</div>
<div class="small muted" id="cumulativeGpaNote"></div>
</div>
<div class="box">
<div class="small muted">On-track courses</div>
<div class="value" id="onTrackCount">—</div>
<div class="small muted" id="onTrackNote"></div>
</div>
</div>
<div class="spacer"></div>
<div class="small muted">GPA mapping (editable in code): A=85+ → 4.0, A-=80 → 3.7, B+=75 → 3.3, B=70 → 3.0, B-=65 → 2.7, C+=60 → 2.3, C=55 → 2.0, D=50 → 1.0, F<50 → 0.0</div>
</section>
</div>
<section class="card">
<h2>Add assessments</h2>
<div class="row three">
<div>
<label>Course</label>
<select id="courseSelect"></select>
</div>
<div>
<label>Assessment type</label>
<select id="assessType">
<option>Mid-sem</option>
<option>Continuous assessment</option>
<option>Assignment</option>
<option>Quiz</option>
<option>End-sem</option>
<option>Other</option>
</select>
</div>
<div>
<label>Weight (%) of course</label>
<input id="assessWeight" type="number" min="0" max="100" step="0.5" placeholder="e.g., 30" />
</div>
</div>
<div class="row three">
<div>
<label>Score achieved</label>
<input id="assessScore" type="number" min="0" step="0.1" placeholder="e.g., 24" />
</div>
<div>
<label>Out of</label>
<input id="assessOutOf" type="number" min="1" step="0.1" placeholder="e.g., 30" />
</div>
<div>
<label> </label>
<button class="primary" id="addAssessBtn">Add assessment</button>
</div>
</div>
<div class="small muted">Weights should sum to 100% across all assessments for each course. The tracker will warn you if not.</div>
</section>
<section class="card">
<h2>Courses dashboard</h2>
<div id="coursesContainer"></div>
</section>
<div class="footer">Built for clarity and honest feedback — keep learning sharp.</div>
</main>
<script>
// Data model in localStorage
const storeKey = 'apt-data-v1';
let data = JSON.parse(localStorage.getItem(storeKey) || '{"semesters":[]}');
const $ = (id)=>document.getElementById(id);
function save(){ localStorage.setItem(storeKey, JSON.stringify(data)); }
// GPA mapping: percentage -> grade points (editable)
function pctToGpa(p){
if (p >= 85) return 4.0;
if (p >= 80) return 3.7;
if (p >= 75) return 3.3;
if (p >= 70) return 3.0;
if (p >= 65) return 2.7;
if (p >= 60) return 2.3;
if (p >= 55) return 2.0;
if (p >= 50) return 1.0;
return 0.0;
}
function number(v, d=2){ return isFinite(v) ? Number(v).toFixed(d) : '—'; }
function ensureSelections(){
// Semester dropdowns
const semSel = $('semesterSelect');
const courseSel = $('courseSelect');
semSel.innerHTML = '';
courseSel.innerHTML = '';
data.semesters.forEach((s, i)=>{
const o = document.createElement('option'); o.value=i; o.textContent=s.name; semSel.appendChild(o);
});
// Course dropdown uses current semester (first by default)
const si = semSel.value || 0;
const semester = data.semesters[si] || null;
if (semester){
semester.courses.forEach((c, j)=>{
const o = document.createElement('option');
o.value = `${si}:${j}`;
o.textContent = `${c.code} — ${c.name}`;
courseSel.appendChild(o);
});
}
}
function renderCourses(){
const container = $('coursesContainer');
container.innerHTML = '';
data.semesters.forEach((sem, si)=>{
const semDiv = document.createElement('div');
semDiv.className='card';
semDiv.innerHTML = `
<div class="flex">
<h2>${sem.name}</h2>
<span class="pill right">${sem.courses.length} course(s)</span>
<button class="warn" data-del-sem="${si}">Delete semester</button>
</div>
<div class="spacer"></div>
<table>
<thead>
<tr>
<th>Course</th>
<th>Credits</th>
<th>Weights</th>
<th>Current %</th>
<th>GPA</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody id="tbody-${si}"></tbody>
</table>
`;
container.appendChild(semDiv);
const tbody = semDiv.querySelector(`#tbody-${si}`);
sem.courses.forEach((c, ci)=>{
const { percent, weightSum } = coursePercent(c);
const gpa = pctToGpa(percent);
const onTrack = (c.targetPercent ?? 0) <= percent;
const statusClass = onTrack ? 'ok' : (Math.abs((c.targetPercent||0)-percent) <= 5 ? 'warn' : 'bad');
const statusText = onTrack ? 'On track' : (statusClass==='warn' ? 'Slightly behind' : 'At risk');
const tr = document.createElement('tr');
tr.innerHTML = `
<td>
<div>${c.code} — ${c.name}</div>
<div class="small muted">Target: ${c.targetPercent || 0}%</div>
<div class="progress" style="margin-top:6px"><div class="bar" style="width:${Math.min(100, percent)}%"></div></div>
</td>
<td>${c.credits}</td>
<td>${number(weightSum,0)}%</td>
<td>${number(percent)}%</td>
<td>${number(gpa,2)}</td>
<td><span class="status ${statusClass}">${statusText}</span></td>
<td>
<div class="flex">
<button data-view="${si}:${ci}">View</button>
<button data-del="${si}:${ci}" class="warn">Delete</button>
</div>
</td>
`;
tbody.appendChild(tr);
});
// Semester summary GPA
const { semGpa, credits } = semesterGpa(sem);
// Update top KPI if this is the currently selected semester
const currentIndex = Number(($('semesterSelect').value || 0));
if (currentIndex === si){
$('semesterGpa').textContent = number(semGpa,2);
$('semesterGpaNote').textContent = credits ? `${credits} total credits` : '';
}
});
// Cumulative GPA
const { cumGpa, credits } = cumulativeGpa();
$('cumulativeGpa').textContent = number(cumGpa,2);
$('cumulativeGpaNote').textContent = credits ? `${credits} total credits` : '';
// On-track count
const { onTrack, total } = onTrackSummary();
$('onTrackCount').textContent = `${onTrack}/${total}`;
$('onTrackNote').textContent = total ? `${Math.round(onTrack*100/total)}% on track` : '';
}
function coursePercent(course){
// Weighted percentage = sum( (score/outOf)*100 * weight/100 )
let total = 0, wsum = 0;
(course.assessments||[]).forEach(a=>{
if (a.outOf > 0 && a.weight > 0){
total += (a.score / a.outOf) * 100 * (a.weight/100);
wsum += a.weight;
}
});
return { percent: wsum>0 ? total : 0, weightSum: wsum };
}
function semesterGpa(sem){
let totalPts = 0, totalCred = 0;
sem.courses.forEach(c=>{
const { percent, weightSum } = coursePercent(c);
// If weights < 100, still compute current standing; GPA uses current percent estimate
const gp = pctToGpa(percent);
totalPts += gp * Number(c.credits || 0);
totalCred += Number(c.credits || 0);
});
return { semGpa: totalCred ? totalPts/totalCred : 0, credits: totalCred };
}
function cumulativeGpa(){
let totalPts = 0, totalCred = 0;
data.semesters.forEach(sem=>{
const s = semesterGpa(sem);
totalPts += s.semGpa * s.credits;
totalCred += s.credits;
});
return { cumGpa: totalCred ? totalPts/totalCred : 0, credits: totalCred };
}
function onTrackSummary(){
let on = 0, total = 0;
data.semesters.forEach(sem=>{
sem.courses.forEach(c=>{
const { percent } = coursePercent(c);
total++;
if ((c.targetPercent||0) <= percent) on++;
});
});
return { onTrack:on, total };
}
function refreshAll(){
ensureSelections();
renderCourses();
}
// Events
$('addSemesterBtn').addEventListener('click', ()=>{
const name = $('semesterName').value.trim();
if (!name) return alert('Enter a semester name.');
data.semesters.push({ name, courses: [] });
save(); $('semesterName').value=''; refreshAll();
});
$('addCourseBtn').addEventListener('click', ()=>{
const si = Number($('semesterSelect').value || 0);
if (!data.semesters[si]) return alert('Add a semester first.');
const name = $('courseName').value.trim();
const code = $('courseCode').value.trim();
const credits = Number($('courseCredits').value || 0);
const targetPercent = Number($('targetPercent').value || 0);
if (!name || !code || credits<=0) return alert('Provide course name, code, and positive credits.');
data.semesters[si].courses.push({
name, code, credits, targetPercent, assessments:[]
});
save();
$('courseName').value=''; $('courseCode').value=''; $('courseCredits').value=''; $('targetPercent').value='';
refreshAll();
});
$('addAssessBtn').addEventListener('click', ()=>{
const sel = $('courseSelect').value;
if (!sel) return alert('Add a course first.');
const [si, ci] = sel.split(':').map(Number);
const type = $('assessType').value;
const weight = Number($('assessWeight').value || 0);
const score = Number($('assessScore').value || 0);
const outOf = Number($('assessOutOf').value || 0);
if (weight<=0 || outOf<=0) return alert('Enter positive weight and out-of.');
const course = data.semesters[si].courses[ci];
course.assessments.push({ type, weight, score, outOf, ts: Date.now() });
// Warn if weights exceed 100
const { weightSum } = coursePercent(course);
if (weightSum > 100 + 1e-6){
if (!confirm(`Weights now sum to ${weightSum}%. Continue?`)){
course.assessments.pop(); // revert
return;
}
}
save();
$('assessWeight').value=''; $('assessScore').value=''; $('assessOutOf').value='';
refreshAll();
});
// Delegated actions: delete semester/course, view course detail
document.body.addEventListener('click', (e)=>{
const delSem = e.target.getAttribute('data-del-sem');
if (delSem !== null){
const si = Number(delSem);
if (confirm(`Delete semester "${data.semesters[si].name}"?`)){
data.semesters.splice(si,1); save(); refreshAll();
}
return;
}
const del = e.target.getAttribute('data-del');
if (del){
const [si, ci] = del.split(':').map(Number);
const c = data.semesters[si].courses[ci];
if (confirm(`Delete course "${c.code} — ${c.name}"?`)){
data.semesters[si].courses.splice(ci,1); save(); refreshAll();
}
return;
}
const view = e.target.getAttribute('data-view');
if (view){
const [si, ci] = view.split(':').map(Number);
openCourseModal(si, ci);
}
});
// Export/Import
$('exportBtn').addEventListener('click', ()=>{
const blob = new Blob([JSON.stringify(data,null,2)], {type:'application/json'});
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'apt-backup.json';
a.click();
URL.revokeObjectURL(a.href);
});
$('importBtn').addEventListener('click', ()=>{
const f = $('importFile').files[0];
if (!f) return alert('Choose a file.');
const reader = new FileReader();
reader.onload = ()=>{
if (!confirm('Import will replace current data. Continue?')) return;
try{
const obj = JSON.parse(reader.result);
if (!obj || !Array.isArray(obj.semesters)) throw new Error('Invalid file.');
data = obj; save(); refreshAll();
}catch(err){ alert('Invalid JSON file.'); }
};
reader.readAsText(f);
});
// Course detail modal (simple inline panel)
function openCourseModal(si, ci){
const sem = data.semesters[si];
const c = sem.courses[ci];
const { percent, weightSum } = coursePercent(c);
const container = document.createElement('div');
container.style.position='fixed';
container.style.inset='0';
container.style.background='rgba(0,0,0,.6)';
container.style.zIndex='999';
container.innerHTML = `
<div style="max-width:900px;margin:60px auto;background:#0b1220;border:1px solid #1f2937;border-radius:14px;padding:16px">
<div class="flex">
<h2>${c.code} — ${c.name}</h2>
<span class="pill">Credits: ${c.credits}</span>
<span class="pill">Target: ${c.targetPercent || 0}%</span>
<button class="right" id="closeModalBtn">Close</button>
</div>
<div class="divider"></div>
<div class="row two">
<div class="box" style="background:#0a0f1e;border:1px solid #1f2937;border-radius:12px;padding:12px">
<div class="small muted">Current percentage</div>
<div class="value">${number(percent)}%</div>
<div class="small muted">Weights used: ${number(weightSum,0)}%</div>
</div>
<div class="box" style="background:#0a0f1e;border:1px solid #1f2937;border-radius:12px;padding:12px">
<div class="small muted">Estimated GPA</div>
<div class="value">${number(pctToGpa(percent),2)}</div>
<div class="small muted">Mapping editable in code</div>
</div>
</div>
<div class="spacer"></div>
<table>
<thead>
<tr>
<th>Type</th><th>Weight %</th><th>Score</th><th>%</th><th>Added</th><th></th>
</tr>
</thead>
<tbody id="alist"></tbody>
</table>
</div>
`;
document.body.appendChild(container);
const tbody = container.querySelector('#alist');
c.assessments.forEach((a, idx)=>{
const pct = a.outOf>0 ? (a.score/a.outOf)*100 : 0;
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${a.type}</td>
<td>${number(a.weight,1)}</td>
<td>${a.score}/${a.outOf}</td>
<td>${number(pct,1)}%</td>
<td>${new Date(a.ts).toLocaleDateString()}</td>
<td><button data-del-ass="${si}:${ci}:${idx}" class="warn">Delete</button></td>
`;
tbody.appendChild(tr);
});
container.addEventListener('click', (e)=>{
if (e.target.id==='closeModalBtn' || e.target===container) container.remove();
const delA = e.target.getAttribute('data-del-ass');
if (delA){
const [s, cidx, aidx] = delA.split(':').map(Number);
if (confirm('Delete this assessment?')){
data.semesters[s].courses[cidx].assessments.splice(aidx,1);
save();
container.remove();
refreshAll();
}
}
});
}
// Initial render with a helpful default semester if empty
if (data.semesters.length===0){
data.semesters.push({
name:'Sample Semester',
courses:[
{ name:'General Chemistry', code:'CHM-101', credits:3, targetPercent:75, assessments:[
{type:'Mid-sem', weight:30, score:24, outOf:30, ts: Date.now()-86400000*30},
{type:'Continuous assessment', weight:20, score:16, outOf:20, ts: Date.now()-86400000*20},
]},
{ name:'Calculus I', code:'MTH-111', credits:4, targetPercent:70, assessments:[
{type:'Quiz', weight:10, score:8, outOf:10, ts: Date.now()-86400000*15},
{type:'Assignment', weight:10, score:7.5, outOf:10, ts: Date.now()-86400000*10},
]},
]
});
save();
}
refreshAll();
</script>
</body>
</html>
Editor is loading...
Leave a Comment