From 78570c40884c8b18b72428b44e076b46f1d3a93f Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Tue, 18 Aug 2026 17:09:54 +0800 Subject: [PATCH] feat: popup UI for profiles and study load --- popup.css | 64 ++++++++++++++++ popup.html | 63 ++++++++++++++++ popup.js | 212 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 339 insertions(+) create mode 100644 popup.css create mode 100644 popup.html create mode 100644 popup.js diff --git a/popup.css b/popup.css new file mode 100644 index 0000000..fa33166 --- /dev/null +++ b/popup.css @@ -0,0 +1,64 @@ +* { box-sizing: border-box; } +body { + width: 360px; + margin: 0; + font-family: system-ui, sans-serif; + font-size: 13px; + color: #222; +} +.top { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 12px; + background: #006699; + color: #fff; +} +.top h1 { margin: 0; font-size: 16px; } +.master { display: flex; align-items: center; gap: 6px; } +.switch { position: relative; display: inline-block; width: 40px; height: 22px; } +.switch input { display: none; } +.slider { + position: absolute; cursor: pointer; inset: 0; + background: #ccc; border-radius: 22px; transition: 0.2s; +} +.slider::before { + content: ""; position: absolute; height: 18px; width: 18px; + left: 2px; top: 2px; background: #fff; border-radius: 50%; transition: 0.2s; +} +.switch input:checked + .slider { background: #4caf50; } +.switch input:checked + .slider::before { transform: translateX(18px); } +.tabs { display: flex; border-bottom: 1px solid #ddd; } +.tabs button { + flex: 1; padding: 8px; border: none; background: #f5f5f5; cursor: pointer; +} +.tabs button.active { background: #fff; font-weight: bold; border-bottom: 2px solid #006699; } +.tab { padding: 12px; } +.tab h2 { margin: 0 0 8px; font-size: 14px; } +.tab h3 { margin: 12px 0 6px; font-size: 12px; color: #555; } +.profile-list { list-style: none; margin: 0 0 8px; padding: 0; } +.profile-list li { + display: flex; align-items: center; gap: 6px; + padding: 6px; border: 1px solid #ddd; border-radius: 4px; margin-bottom: 6px; +} +.profile-list .name { flex: 1; } +.profile-list .user { color: #777; font-size: 11px; } +.btn { + padding: 6px 10px; border: 1px solid #ccc; border-radius: 4px; + background: #fff; cursor: pointer; +} +.btn.primary { background: #006699; color: #fff; border-color: #006699; } +.btn-row { display: flex; gap: 6px; margin-top: 6px; } +.form input { + width: 100%; padding: 6px; margin-bottom: 6px; + border: 1px solid #ccc; border-radius: 4px; +} +.form .row { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; } +.course-row { + border: 1px solid #ddd; border-radius: 4px; padding: 6px; margin-bottom: 6px; +} +.course-row .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 4px; } +.course-row input { margin-bottom: 4px; } +.status { margin-top: 8px; font-size: 12px; } +.status.error { color: #c00; } +.status.ok { color: #080; } diff --git a/popup.html b/popup.html new file mode 100644 index 0000000..6d3f939 --- /dev/null +++ b/popup.html @@ -0,0 +1,63 @@ + + + + + + + +
+

ISMIS Helper

+
+ + Enabled +
+
+ + + +
+

Profiles

+ + + +
+ + + + + + diff --git a/popup.js b/popup.js new file mode 100644 index 0000000..6b75100 --- /dev/null +++ b/popup.js @@ -0,0 +1,212 @@ +'use strict'; + +const $ = (sel) => document.querySelector(sel); + +const MASTER_KEY = 'enabled'; +const PROFILES_KEY = 'profiles'; + +let editingProfileId = null; + +// ---- Master toggle ---- +async function loadMaster() { + const { enabled } = await browser.storage.local.get(MASTER_KEY); + $('#masterToggle').checked = enabled !== false; + $('#masterLabel').textContent = enabled === false ? 'Disabled' : 'Enabled'; +} +$('#masterToggle').addEventListener('change', async (e) => { + await browser.storage.local.set({ [MASTER_KEY]: e.target.checked }); + $('#masterLabel').textContent = e.target.checked ? 'Enabled' : 'Disabled'; +}); + +// ---- Profiles ---- +async function getProfiles() { + const { profiles } = await browser.storage.local.get(PROFILES_KEY); + return profiles || []; +} +async function saveProfiles(profiles) { + await browser.storage.local.set({ [PROFILES_KEY]: profiles }); +} + +function renderProfiles(profiles) { + const list = $('#profileList'); + list.innerHTML = ''; + for (const p of profiles) { + const li = document.createElement('li'); + const name = document.createElement('div'); + name.className = 'name'; + name.innerHTML = '' + escapeHtml(p.name) + '
' + escapeHtml(p.username) + ''; + const toggle = document.createElement('input'); + toggle.type = 'checkbox'; + toggle.checked = !!p.enabled; + toggle.title = 'Enable/disable'; + toggle.addEventListener('change', async () => { + p.enabled = toggle.checked; + await saveProfiles(profiles); + }); + const edit = document.createElement('button'); + edit.className = 'btn'; + edit.textContent = 'Edit'; + edit.addEventListener('click', () => openProfileForm(p)); + const del = document.createElement('button'); + del.className = 'btn'; + del.textContent = 'Delete'; + del.addEventListener('click', async () => { + await saveProfiles(profiles.filter((x) => x.id !== p.id)); + renderProfiles(await getProfiles()); + }); + li.append(toggle, name, edit, del); + list.appendChild(li); + } +} + +function escapeHtml(s) { + return String(s).replace(/[&<>"']/g, (c) => ({ + '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' + }[c])); +} + +function openProfileForm(p) { + editingProfileId = p ? p.id : null; + $('#pName').value = p ? p.name : ''; + $('#pUsername').value = p ? p.username : ''; + $('#pPassword').value = p ? p.password : ''; + $('#pEnabled').checked = p ? !!p.enabled : true; + $('#profileForm').hidden = false; +} +$('#addProfile').addEventListener('click', () => openProfileForm(null)); +$('#cancelProfile').addEventListener('click', () => { $('#profileForm').hidden = true; }); +$('#saveProfile').addEventListener('click', async () => { + const profiles = await getProfiles(); + const data = { + id: editingProfileId || Date.now().toString(), + name: $('#pName').value.trim(), + username: $('#pUsername').value.trim(), + password: $('#pPassword').value, + enabled: $('#pEnabled').checked + }; + if (!data.name || !data.username) return; + if (editingProfileId) { + const i = profiles.findIndex((x) => x.id === editingProfileId); + if (i >= 0) profiles[i] = data; + } else { + profiles.push(data); + } + await saveProfiles(profiles); + $('#profileForm').hidden = true; + renderProfiles(profiles); +}); + +// ---- Tabs ---- +document.querySelectorAll('.tabs button').forEach((btn) => { + btn.addEventListener('click', () => { + document.querySelectorAll('.tabs button').forEach((b) => b.classList.remove('active')); + btn.classList.add('active'); + $('#tab-login').hidden = btn.dataset.tab !== 'login'; + $('#tab-studyload').hidden = btn.dataset.tab !== 'studyload'; + }); +}); + +// ---- Study Load ---- +function setStatus(msg, cls) { + const el = $('#status'); + el.textContent = msg; + el.className = 'status ' + (cls || ''); +} + +function renderCourses(courses) { + const box = $('#coursesEditor'); + box.innerHTML = ''; + courses.forEach((c, i) => { + const row = document.createElement('div'); + row.className = 'course-row'; + row.innerHTML = + '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '
' + + '' + + ''; + box.appendChild(row); + }); + box.querySelectorAll('input').forEach((inp) => { + inp.addEventListener('input', () => { + const i = +inp.dataset.i; + const f = inp.dataset.f; + studyLoadCourses[i][f] = inp.value; + }); + }); + box.querySelectorAll('.remove-course').forEach((btn) => { + btn.addEventListener('click', () => { + studyLoadCourses.splice(+btn.dataset.i, 1); + renderCourses(studyLoadCourses); + }); + }); +} + +let studyLoadCourses = []; + +$('#loadFromPortal').addEventListener('click', async () => { + setStatus('Loading from portal...'); + try { + const res = await browser.runtime.sendMessage({ type: 'scrape-study-load' }); + if (!res || !res.ok) throw new Error((res && res.error) || 'Scrape failed'); + const d = res.data; + $('#hFullName').value = d.header.fullName || ''; + $('#hStudentId').value = d.header.studentId || ''; + $('#hProgram').value = d.header.program || ''; + $('#hYearLevel').value = d.header.yearLevel || ''; + $('#hSemester').value = d.header.semester || ''; + $('#hDateEnrolled').value = d.header.dateEnrolled || ''; + $('#hDateRange').value = d.header.dateRange || ''; + $('#totalUnits').value = d.totalUnits || ''; + studyLoadCourses = d.courses || []; + renderCourses(studyLoadCourses); + $('#studyLoadForm').hidden = false; + setStatus('Loaded. Review and generate.', 'ok'); + } catch (err) { + setStatus('Could not load from portal: ' + err.message + ' — enter manually below.', 'error'); + studyLoadCourses = []; + renderCourses(studyLoadCourses); + $('#studyLoadForm').hidden = false; + } +}); + +$('#addCourse').addEventListener('click', () => { + studyLoadCourses.push({ group: '', courseCode: '', description: '', units: '', teacher: '', schedule: '', status: '' }); + renderCourses(studyLoadCourses); +}); + +$('#generatePdf').addEventListener('click', async () => { + const data = { + header: { + fullName: $('#hFullName').value.trim(), + studentId: $('#hStudentId').value.trim(), + program: $('#hProgram').value.trim(), + yearLevel: $('#hYearLevel').value.trim(), + semester: $('#hSemester').value.trim(), + dateEnrolled: $('#hDateEnrolled').value.trim(), + dateRange: $('#hDateRange').value.trim() + }, + courses: studyLoadCourses, + totalUnits: $('#totalUnits').value.trim() + }; + if (!data.courses.length) { setStatus('Add at least one course.', 'error'); return; } + setStatus('Generating PDF...'); + try { + const res = await browser.runtime.sendMessage({ type: 'generate-pdf', data }); + if (!res || !res.ok) throw new Error((res && res.error) || 'Generation failed'); + setStatus('PDF downloaded.', 'ok'); + } catch (err) { + setStatus('Error: ' + err.message, 'error'); + } +}); + +// ---- Init ---- +(async function init() { + await loadMaster(); + renderProfiles(await getProfiles()); +})();