From 0cdcac6a3177f775fcec57bb9f97c7f041232be0 Mon Sep 17 00:00:00 2001 From: karrisanthoshigayatri Date: Tue, 9 Jun 2026 17:18:28 +0530 Subject: [PATCH 1/3] implemented profile page --- css/index.css | 13 +++++++ index.html | 57 ++++++++++++++++++++++++++++++- js/app.js | 94 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 163 insertions(+), 1 deletion(-) diff --git a/css/index.css b/css/index.css index 93befe3f..599fd736 100644 --- a/css/index.css +++ b/css/index.css @@ -4101,6 +4101,19 @@ body { .tasks-section { flex: 1; overflow-y: auto; padding: 0 24px 24px; scroll-behavior: smooth; } .tasks-section::-webkit-scrollbar { width: 6px; } .tasks-section::-webkit-scrollbar-thumb { background: var(--color-border-secondary); border-radius: 10px; } +.profile-section { flex: 1; overflow-y: auto; padding: 24px; display: flex; flex-direction: column; gap: 24px; } +.profile-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 20px; } +.profile-page-title { font-size: 24px; font-weight: 700; color: var(--color-text-primary); } +.profile-page-subtitle { margin: 8px 0 0; color: var(--color-text-secondary); max-width: 640px; line-height: 1.6; } +.profile-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px; } +.profile-card { background: var(--color-background-primary); border: 1px solid var(--color-border-tertiary); border-radius: var(--border-radius-md); padding: 24px; box-shadow: var(--shadow-sm); } +.profile-card h2 { margin: 0 0 16px; font-size: 16px; font-weight: 700; color: var(--color-text-primary); } +.profile-field { display: flex; justify-content: space-between; gap: 16px; margin-bottom: 14px; font-size: 14px; color: var(--color-text-secondary); } +.profile-field-label { color: var(--color-text-tertiary); font-weight: 600; } +.profile-stats { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; } +.profile-stat-value { display: block; font-size: 22px; font-weight: 700; color: var(--color-text-primary); margin-bottom: 4px; } +.profile-summary-card p { margin: 0; color: var(--color-text-secondary); line-height: 1.8; } +@media (max-width: 900px) { .profile-grid { grid-template-columns: 1fr; } } .tasks-actions-bar { margin-top: 18px; display: flex; diff --git a/index.html b/index.html index 543be764..e0c921c6 100644 --- a/index.html +++ b/index.html @@ -62,7 +62,7 @@

StudyPlan

- +
@@ -207,6 +207,61 @@

StudyPlan

+ + + diff --git a/js/app.js b/js/app.js index f69f7f6c..c87ad17c 100644 --- a/js/app.js +++ b/js/app.js @@ -454,6 +454,89 @@ function renderFocusTasks() { } } +function renderProfileSection() { + if (!profileSection) return; + + const tasks = store.tasks || []; + const subjects = store.subjects || []; + const completedCount = tasks.filter(t => t.status === 'Done').length; + const pendingCount = tasks.filter(t => t.status !== 'Done' && !t.archived).length; + const archivedCount = tasks.filter(t => t.archived).length; + const subjectsCount = subjects.length; + const username = localStorage.getItem('studyplan_username') || 'StudyPlan User'; + const email = localStorage.getItem('studyplan_email') || 'user@studyplan.app'; + const joinedDate = localStorage.getItem('studyplan_joined') || 'June 2026'; + + profileSection.innerHTML = ` +
+
+
Profile
+

View your account summary, study stats, and future account settings in one place.

+
+
+ +
+
+

Account details

+
+ Username + ${escapeHtml(username)} +
+
+ Email + ${escapeHtml(email)} +
+
+ Member since + ${escapeHtml(joinedDate)} +
+
+ +
+

Study statistics

+
+
+ ${completedCount} + Completed +
+
+ ${pendingCount} + Pending +
+
+ ${archivedCount} + Archived +
+
+ ${subjectsCount} + Subjects +
+
+
+
+ +
+

Account overview

+

Your profile information and study statistics will update automatically as you use StudyPlan.

+
+ `; +} + +function showProfileSection() { + currentView = 'profile'; + document.querySelector('.cal-section')?.classList.add('hidden'); + document.getElementById('tasks-section')?.classList.add('hidden'); + document.getElementById('focus-section')?.classList.add('hidden'); + profileSection?.classList.remove('hidden'); + topbar?.classList.add('hidden'); + renderProfileSection(); +} + +function hideProfileSection() { + profileSection?.classList.add('hidden'); + topbar?.classList.remove('hidden'); +} + function formatDate(dateStr) { if (!dateStr) return 'No Date'; const d = new Date(dateStr); @@ -1095,6 +1178,7 @@ store.subscribe(renderTasks); store.subscribe(renderExtraction); store.subscribe(renderCalendar); store.subscribe(renderFocusTasks); +store.subscribe(renderProfileSection); store.subscribe(renderSidebarSubjects); document.addEventListener('DOMContentLoaded', () => { @@ -1166,6 +1250,7 @@ document.addEventListener('DOMContentLoaded', () => { calendarBtn.addEventListener('click', () => { currentView = 'calendar'; + hideProfileSection(); document.querySelector('.cal-section').classList.remove('hidden'); document.getElementById('tasks-section').classList.remove('hidden'); document.getElementById('focus-section').classList.add('hidden'); @@ -1175,6 +1260,7 @@ document.addEventListener('DOMContentLoaded', () => { allTasksBtn.addEventListener('click', () => { currentView = 'all-tasks'; + hideProfileSection(); document.querySelector('.cal-section').classList.add('hidden'); document.getElementById('tasks-section').classList.remove('hidden'); document.getElementById('focus-section').classList.add('hidden'); @@ -1184,6 +1270,7 @@ document.addEventListener('DOMContentLoaded', () => { archivedTasksBtn.addEventListener('click', () => { currentView = 'archived'; + hideProfileSection(); document.querySelector('.cal-section').classList.add('hidden'); document.getElementById('tasks-section').classList.remove('hidden'); document.getElementById('focus-section').classList.add('hidden'); @@ -1194,6 +1281,7 @@ document.addEventListener('DOMContentLoaded', () => { if(focusModeBtn) { focusModeBtn.addEventListener('click', () => { currentView = 'focus'; + hideProfileSection(); document.querySelector('.cal-section').classList.add('hidden'); document.getElementById('tasks-section').classList.add('hidden'); document.getElementById('focus-section').classList.remove('hidden'); @@ -1202,6 +1290,12 @@ document.addEventListener('DOMContentLoaded', () => { }); } + if (profileBtn) { + profileBtn.addEventListener('click', () => { + showProfileSection(); + }); + } + document.getElementById('cal-prev').addEventListener('click', () => { currentMonthDate.setMonth(currentMonthDate.getMonth() - 1); renderCalendar(); From 4d1273ed3ac8523476ec93e74d019f9bf7f18cf2 Mon Sep 17 00:00:00 2001 From: karrisanthoshigayatri Date: Wed, 10 Jun 2026 06:55:59 +0530 Subject: [PATCH 2/3] feature/profile page --- js/app.js | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/js/app.js b/js/app.js index 0122aab5..9167e2e0 100644 --- a/js/app.js +++ b/js/app.js @@ -99,6 +99,9 @@ const downloadBtn = document.getElementById('download-btn'); const calendarDownloadBtn = document.getElementById('calendar-download-btn'); const newTaskBtn = document.getElementById('add-task-btn'); const labelFilterSelect = document.getElementById('label-filter'); +const profileSection = document.getElementById('profile-section'); +const profileBtn = document.getElementById('profile-btn'); +const topbar = document.querySelector('.topbar'); if (labelFilterSelect) { labelFilterSelect.addEventListener('change', (e) => { @@ -1415,24 +1418,27 @@ document.addEventListener('DOMContentLoaded', () => { }); } -<<<<<<< HEAD if (profileBtn) { profileBtn.addEventListener('click', () => { showProfileSection(); }); } - document.getElementById('cal-prev').addEventListener('click', () => { - currentMonthDate.setMonth(currentMonthDate.getMonth() - 1); - renderCalendar(); - }); + const calPrev = document.getElementById('cal-prev'); + if (calPrev) { + calPrev.addEventListener('click', () => { + currentMonthDate.setMonth(currentMonthDate.getMonth() - 1); + renderCalendar(); + }); + } -======= ->>>>>>> upstream/main - document.getElementById('cal-next').addEventListener('click', () => { - currentMonthDate.setMonth(currentMonthDate.getMonth() + 1); - renderCalendar(); - }); + const calNext = document.getElementById('cal-next'); + if (calNext) { + calNext.addEventListener('click', () => { + currentMonthDate.setMonth(currentMonthDate.getMonth() + 1); + renderCalendar(); + }); + } //NEw Task addition event listeners From f198c39f59235e07ddec1484a55a933c1e1c7d17 Mon Sep 17 00:00:00 2001 From: karrisanthoshigayatri Date: Fri, 26 Jun 2026 06:45:29 +0530 Subject: [PATCH 3/3] feat: add dark mode persistence with profile and statistics updates --- css/index.css | 169 ++++++++++- database.js | 37 ++- index.html | 65 ++++- js/app.js | 469 +++++++++++++++++++++++-------- public/crown-30andmorestreak.svg | 3 + public/medal-3daystreak.svg | 3 + public/rocket-7daystreak.svg | 3 + public/streak-icon.svg | 3 + server.js | 83 +++++- 9 files changed, 693 insertions(+), 142 deletions(-) create mode 100644 public/crown-30andmorestreak.svg create mode 100644 public/medal-3daystreak.svg create mode 100644 public/rocket-7daystreak.svg create mode 100644 public/streak-icon.svg diff --git a/css/index.css b/css/index.css index c0bfadda..1db3b97a 100644 --- a/css/index.css +++ b/css/index.css @@ -150,9 +150,61 @@ body { /* Sidebar */ .sidebar { - background: var(--color-background-secondary); - border-right: 1px solid var(--color-border-tertiary); - padding: 20px 16px; + background: var(--color-background-secondary); border-right: 1px solid var(--color-border-tertiary); + padding: 20px 16px; display: flex; flex-direction: column; gap: 4px; +} +.sidebar-header { font-size: 11px; font-weight: 700; color: var(--color-text-tertiary); padding: 8px 12px; margin-bottom: 2px; letter-spacing: .06em; text-transform: uppercase; } +.nav-item { display: flex; align-items: center; gap: 12px; padding: 10px 12px; border-radius: var(--border-radius-md); cursor: pointer; font-size: 14px; font-weight: 500; color: var(--color-text-secondary); transition: all 0.2s ease; position: relative; } +.nav-item:hover { background: var(--color-background-primary); color: var(--color-text-primary); transform: translateX(2px); } +.nav-item.active { background: var(--color-background-primary); color: var(--color-text-primary); box-shadow: var(--shadow-sm); } +.nav-item.active::before { content: ''; position: absolute; left: 0; top: 15%; height: 70%; width: 3px; background: var(--color-text-primary); border-radius: 4px; } +.nav-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; box-shadow: inset 0 0 0 1px rgba(0,0,0,0.1); } +.badge { margin-left: auto; font-size: 12px; font-weight: 600; background: var(--color-background-tertiary); color: var(--color-text-secondary); padding: 2px 8px; border-radius: 12px; transition: all 0.2s; } +.nav-item:hover .badge { background: var(--color-background-primary); border: 1px solid var(--color-border-tertiary); } +.sidebar-divider { height: 1px; background: var(--color-border-tertiary); margin: 12px 0; } +.add-subject { font-size: 13px; font-weight: 500; color: var(--color-text-tertiary); padding: 8px 12px; cursor: pointer; transition: color 0.2s; display: flex; align-items: center; gap: 8px; } +.add-subject:hover { color: var(--color-text-primary); } + +/* Main */ +.main { display: flex; flex-direction: column; overflow: hidden; position: relative; } +.topbar { display: flex; align-items: center; gap: 12px; padding: 16px 24px; border-bottom: 1px solid var(--color-border-tertiary); flex-shrink: 0; background: rgba(var(--color-background-primary), 0.8); backdrop-filter: blur(10px); z-index: 10; } +.topbar-title { font-size: 18px; font-weight: 600; color: var(--color-text-primary); flex: 1; } +.btn { font-family: inherit; font-size: 13px; font-weight: 600; padding: 8px 16px; border: 1px solid var(--color-border-secondary); border-radius: var(--border-radius-sm); background: var(--color-background-primary); color: var(--color-text-primary); cursor: pointer; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); box-shadow: var(--shadow-sm); display: inline-flex; align-items: center; justify-content: center; gap: 6px; } +.btn:hover { background: var(--color-background-secondary); transform: translateY(-1px); box-shadow: var(--shadow-md); } +.btn:active { transform: translateY(0); box-shadow: 0 0 0; } +.btn-primary { background: var(--color-text-primary); color: var(--color-background-primary); border-color: transparent; } +.btn-primary:hover { background: var(--color-text-secondary); } + +/* Calendar */ +.cal-section { padding: 16px 24px; border-bottom: 1px solid var(--color-border-tertiary); flex-shrink: 0; } +.cal-header { display: flex; align-items: center; margin-bottom: 16px; justify-content:space-between; } +.cal-title { font-size: 15px; font-weight: 600; flex: 1; color: var(--color-text-primary); } +.cal-nav { font-size: 18px; color: var(--color-text-secondary); cursor: pointer; padding: 4px 8px; border-radius: 4px; transition: background 0.2s; } +.cal-nav:hover { background: var(--color-background-secondary); color: var(--color-text-primary); } +.cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; text-align: center; } +.cal-day-label { font-size: 11px; color: var(--color-text-tertiary); padding-bottom: 8px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; } +.cal-day { font-size: 13px; font-weight: 500; height: 32px; display: flex; align-items: center; justify-content: center; border-radius: var(--border-radius-md); cursor: pointer; color: var(--color-text-secondary); position: relative; transition: all 0.2s; } +.cal-day:hover { background: var(--color-background-secondary); color: var(--color-text-primary); transform: scale(1.1); z-index: 1; } +.cal-day.today { background: var(--color-text-primary); color: var(--color-background-primary); box-shadow: var(--shadow-sm); } +.cal-day-indicators { position: absolute; bottom: 3px; left: 0; right: 0; display: flex; justify-content: center; gap: 3px; } +.cal-day-indicator { width: 5px; height: 5px; border-radius: 50%; } +.cal-day.muted { color: var(--color-border-secondary); } +.cal-legend { display: flex; gap: 16px; margin-top: 16px; font-size: 11px; font-weight: 500; color: var(--color-text-tertiary); justify-content: flex-end; } +.cal-legend span { display: flex; align-items: center; gap: 6px; } +.legend-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; } + +/*Streak*/ +.streak-display { display: flex; align-items: center; gap: 6px; padding: 8px 10px; border-radius: 12px; cursor: pointer; transition: all 0.2s ease; } +.streak-display:hover { background: rgba(0, 0, 0, 0.04); } +.streak-icon { width: 22px; height: 22px; object-fit: contain; } +#streak-count { font-size: 20px; font-weight: 700; color: var(--color-text-primary); } + +/* Tasks */ +.tasks-section { flex: 1; overflow-y: auto; padding: 0 24px 24px; scroll-behavior: smooth; } +.tasks-section::-webkit-scrollbar { width: 6px; } +.tasks-section::-webkit-scrollbar-thumb { background: var(--color-border-secondary); border-radius: 10px; } +.tasks-actions-bar { + margin-top: 18px; display: flex; flex-direction: column; gap: 4px; @@ -2932,6 +2984,112 @@ body { align-items: center; gap: 12px; } +/* Header right & Badges */ + +.header-right { + display: flex; + align-items: center; + gap: 10px; +} + +.streak-badges { + display: flex; + align-items: center; + gap: 8px; + + margin-right: 6px; + + transform: translateY(1px); +} + +.badge-wrapper { + width: 45px; + height: 45px; + + display: flex; + align-items: center; + justify-content: center; + + border-radius: 999px; + + background: rgba(0, 0, 0, 0.025); + + transition: all 0.2s ease; +} + +.badge-wrapper:hover { + background: rgba(0, 0, 0, 0.05); +} + +.streak-badge { + width: 30px; + height: 30px; + + object-fit: contain; + + transition: all 0.2s ease; +} + +.streak-badge.locked { + filter: grayscale(1); + opacity: 0.32; +} + +.streak-badge.unlocked { + opacity: 0.92; +} + +.badge-wrapper:hover .streak-badge { + transform: scale(1.08); +} + +.streak-display { + position: relative; +} + +.streak-tooltip { + position: absolute; + + top: 36px; + left: 50%; + + transform: translateX(-50%) translateY(4px); + + min-width: 220px; + + padding: 10px 12px; + + border-radius: 12px; + + background: #111; + color: white; + + font-size: 12px; + font-weight: 500; + line-height: 1.5; + + text-align: center; + + opacity: 0; + visibility: hidden; + + transition: all 0.2s ease; + + pointer-events: none; + + z-index: 100; +} + +.streak-display:hover .streak-tooltip { + opacity: 1; + visibility: visible; + + transform: translateX(-50%) translateY(0); +} + +.hidden { + display: none; +} .logo { width: 40px; @@ -2947,7 +3105,7 @@ body { .header-nav { display: flex; - gap: 24px; + gap: 20px; } .header-nav a { @@ -2962,6 +3120,7 @@ body { } .profile-btn { + height:40px; padding: 8px 16px; border: none; border-radius: var(--border-radius-md); @@ -3934,6 +4093,7 @@ body { min-width: 48px; } .summary-box { + position:relative; margin-top: 16px; padding: 14px; background: var(--color-background-secondary); @@ -4713,6 +4873,7 @@ body { font-weight: 600; } + /* Footer */ .site-footer { width: 100%; diff --git a/database.js b/database.js index 33a1b17d..d2a34f89 100644 --- a/database.js +++ b/database.js @@ -5,6 +5,7 @@ const db = new sqlite3.Database(path.join(__dirname, 'studyplan.db')); function initDb() { db.serialize(() => { + // Subjects Table db.run(`CREATE TABLE IF NOT EXISTS subjects ( id TEXT PRIMARY KEY, @@ -14,9 +15,18 @@ function initDb() { created_at DATETIME DEFAULT CURRENT_TIMESTAMP )`); - // Tasks Table + // ================= USERS TABLE ================= + db.run(`CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY, + email TEXT UNIQUE NOT NULL, + password TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + )`); + + // ================= TASKS TABLE ================= db.run(`CREATE TABLE IF NOT EXISTS tasks ( id TEXT PRIMARY KEY, + user_email TEXT, subject_id TEXT, title TEXT NOT NULL, description TEXT, @@ -33,6 +43,21 @@ function initDb() { FOREIGN KEY (subject_id) REFERENCES subjects(id) )`); + // Add labels column to existing tasks table if it doesn't exist + db.all("PRAGMA table_info(tasks)", (err, rows) => { + if (err) return; + + const hasLabels = rows.some(r => r.name === 'labels'); + if (!hasLabels) { + db.run("ALTER TABLE tasks ADD COLUMN labels TEXT DEFAULT '[]'"); + } + + // ================= ADD USER EMAIL COLUMN ================= + const hasUserEmail = rows.some(r => r.name === 'user_email'); + if (!hasUserEmail) { + db.run("ALTER TABLE tasks ADD COLUMN user_email TEXT"); + } + }); db.all("PRAGMA table_info(tasks)", (err, rows) => { if (err) return; @@ -59,15 +84,21 @@ db.all("PRAGMA table_info(tasks)", (err, rows) => { db.get('SELECT COUNT(*) as count FROM subjects', (err, row) => { if (row && row.count === 0) { console.log("Seeding subjects..."); - const stmt = db.prepare("INSERT INTO subjects (id, name, short_code, color) VALUES (?, ?, ?, ?)"); + + const stmt = db.prepare( + "INSERT INTO subjects (id, name, short_code, color) VALUES (?, ?, ?, ?)" + ); + stmt.run('sub_1', 'Computer Science', 'CS', 'var(--color-text-info)'); stmt.run('sub_2', 'Mathematics', 'Maths', 'var(--color-text-success)'); stmt.run('sub_3', 'English Lit', 'English', 'var(--color-text-purple)'); stmt.run('sub_4', 'Physics', 'Physics', 'var(--color-text-warning)'); + stmt.finalize(); } }); + }); } -module.exports = { db, initDb }; +module.exports = { db, initDb }; \ No newline at end of file diff --git a/index.html b/index.html index e0c921c6..9c950bd6 100644 --- a/index.html +++ b/index.html @@ -55,14 +55,54 @@

StudyPlan

+ Dashboard + Tasks + Calendar + Settings +
+<<<<<<< HEAD +======= + +
+ + + +
+ +>>>>>>> upstream/main
@@ -115,6 +155,21 @@

StudyPlan

April 2026
+ +
+ Streak + 0 + +
+ Complete tasks to build streak & earn cool badges +
+ +
diff --git a/js/app.js b/js/app.js index 9167e2e0..f462681b 100644 --- a/js/app.js +++ b/js/app.js @@ -59,8 +59,8 @@ function generateSummary(tasks, subjects) { const topSubject = Object.keys(subjectCount).length ? Object.keys(subjectCount).reduce((a, b) => - subjectCount[a] > subjectCount[b] ? a : b - ) + subjectCount[a] > subjectCount[b] ? a : b + ) : 'no specific subject'; return ` @@ -487,39 +487,39 @@ if (panelToggleBtn) { }); } -if(timerStartBtn) timerStartBtn.addEventListener('click', startTimer); -if(timerPauseBtn) timerPauseBtn.addEventListener('click', pauseTimer); -if(timerResetBtn) timerResetBtn.addEventListener('click', resetTimer); +if (timerStartBtn) timerStartBtn.addEventListener('click', startTimer); +if (timerPauseBtn) timerPauseBtn.addEventListener('click', pauseTimer); +if (timerResetBtn) timerResetBtn.addEventListener('click', resetTimer); function renderFocusTasks() { - if(!focusTaskList || !activeFocusTask) return; + if (!focusTaskList || !activeFocusTask) return; const tasks = store.tasks; const subjects = store.subjects; - + const activeTasks = tasks.filter(t => !t.archived && t.status !== 'Done'); const now = new Date(); - + const dueSoon = []; activeTasks.forEach(t => { - if(!t.due_at) return; + if (!t.due_at) return; const d = new Date(t.due_at); const diffDays = (d - now) / (1000 * 60 * 60 * 24); if (diffDays <= 3) dueSoon.push(t); }); - - dueSoon.sort((a,b) => new Date(a.due_at) - new Date(b.due_at)); - + + dueSoon.sort((a, b) => new Date(a.due_at) - new Date(b.due_at)); + if (dueSoon.length === 0) { focusTaskList.innerHTML = '
No tasks due soon to focus on.
'; } else { focusTaskList.innerHTML = dueSoon.map(t => { const sub = subjects.find(s => s.id === t.subject_id) || subjects[0] || { short_code: 'Gen' }; let pillClass = ''; - if(sub.short_code === 'CS') pillClass = 'pill-blue'; - else if(sub.short_code === 'Maths') pillClass = 'pill-green'; - else if(sub.short_code === 'English') pillClass = 'pill-purple'; + if (sub.short_code === 'CS') pillClass = 'pill-blue'; + else if (sub.short_code === 'Maths') pillClass = 'pill-green'; + else if (sub.short_code === 'English') pillClass = 'pill-purple'; else pillClass = 'pill-amber'; - + return `
${t.title}
@@ -529,7 +529,7 @@ function renderFocusTasks() {
`; }).join(''); - + document.querySelectorAll('.focus-task-item').forEach(el => { el.addEventListener('click', () => { activeFocusTaskId = el.dataset.id; @@ -537,7 +537,7 @@ function renderFocusTasks() { }); }); } - + if (activeFocusTaskId) { const activeT = store.tasks.find(t => t.id === activeFocusTaskId); if (activeT) { @@ -555,7 +555,7 @@ function renderFocusTasks() {
`; - + const completeBtn = activeFocusTask.querySelector('.complete-focus-task-btn'); if (completeBtn) { completeBtn.addEventListener('click', () => { @@ -564,7 +564,7 @@ function renderFocusTasks() { renderFocusTasks(); }); } - + const clearBtn = activeFocusTask.querySelector('.clear-focus-task-btn'); if (clearBtn) { clearBtn.addEventListener('click', () => { @@ -667,31 +667,31 @@ function hideProfileSection() { function formatDate(dateStr) { if (!dateStr) return 'No Date'; const d = new Date(dateStr); - return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute:'2-digit' }); + return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); } async function downloadData() { - try { - const response = await fetch('/api/download'); - - if (!response.ok) { - throw new Error('Failed to download data'); - } - - const blob = await response.blob(); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = 'study_data.csv'; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - setTimeout(() => URL.revokeObjectURL(url), 100); - - } catch (error) { + try { + const response = await fetch('/api/download'); + + if (!response.ok) { + throw new Error('Failed to download data'); + } + + const blob = await response.blob(); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'study_data.csv'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + setTimeout(() => URL.revokeObjectURL(url), 100); + + } catch (error) { console.error(error); Toast.show('Failed to download data', 'error'); - } + } } async function downloadCalendar() { @@ -721,13 +721,13 @@ async function downloadCalendar() { function renderTasks() { const tasks = store.tasks; const subjects = store.subjects; - + if (subjects.length === 0) return; // Wait for subjects to load - + // Filter based on archived status const activeTasks = tasks.filter(t => !t.archived); const archivedTasks = tasks.filter(t => t.archived); - + // Update badges const allTasksBadge = document.querySelector('#all-tasks-btn .badge'); if (allTasksBadge) { @@ -737,12 +737,14 @@ function renderTasks() { if (archivedBadge) { archivedBadge.textContent = archivedTasks.length; } + + const displayTasksRaw = currentView === 'archived' ? archivedTasks : activeTasks; const displayTasks = activeLabelFilter ? displayTasksRaw.filter(t => t.labels && t.labels.includes(activeLabelFilter)) : displayTasksRaw; - + // Extract unique labels to populate the filter dropdown if (labelFilterSelect) { const uniqueLabels = new Set(); @@ -769,7 +771,7 @@ function renderTasks() { const thisWeek = []; const completed = []; const pending = []; - + if (currentView === 'calendar' && selectedDate) { sorted.forEach(t => { const d = new Date(t.due_at); @@ -794,14 +796,14 @@ function renderTasks() { else thisWeek.push(t); }); } - + const renderGroup = (title, items, titleColor, showConflict = false) => { if (items.length === 0) return ''; let html = `
${title}
`; - + if (showConflict) { const workloadSuggestions = analyzeWorkload(items); workloadSuggestions.forEach(workload => { @@ -812,26 +814,27 @@ function renderTasks() {
`; }); } - - + + items.forEach(t => { const sub = subjects.find(s => s.id === t.subject_id) || subjects[0]; const isDone = t.status === 'Done'; + const isHighPriority = t.priority === 'high'; const isOverdue = !isDone && t.due_at && new Date(t.due_at) < now; const isUrgent = isHighPriority && title === '⚠ Due soon'; let pillClass = ''; - if(sub.short_code === 'CS') pillClass = 'pill-blue'; - else if(sub.short_code === 'Maths') pillClass = 'pill-green'; - else if(sub.short_code === 'English') pillClass = 'pill-purple'; + if (sub.short_code === 'CS') pillClass = 'pill-blue'; + else if (sub.short_code === 'Maths') pillClass = 'pill-green'; + else if (sub.short_code === 'English') pillClass = 'pill-purple'; else pillClass = 'pill-amber'; - + if (t._isEditing) { - let subjectOptions = subjects.map(s => + let subjectOptions = subjects.map(s => `` ).join(''); - + const localDate = t.due_at ? new Date(t.due_at).toISOString().substring(0, 16) : ''; const isHighPriority = t.priority === 'high'; const editDurationUnit = t.is_estimated_duration_min === 0 ? 'hours' : 'minutes'; @@ -912,9 +915,9 @@ function renderTasks() { html += ``; return html; }; - + if (currentView === 'calendar' && selectedDate) { - const selStr = selectedDate.toLocaleDateString('en-US', {month:'short', day:'numeric'}); + const selStr = selectedDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); const actionBar = `
@@ -948,9 +951,9 @@ function renderTasks() { } tasksSection.innerHTML = actionBar + - renderGroup(`Tasks for ${selStr}`, dueSoon, 'var(--color-text-primary)') + - renderGroup('Completed', completed, 'var(--color-text-tertiary)') + - emptyState; + renderGroup(`Tasks for ${selStr}`, dueSoon, 'var(--color-text-primary)') + + renderGroup('Completed', completed, 'var(--color-text-tertiary)') + + emptyState; } else { console.log('[Daily Study Time] Hiding banner - currentView:', currentView, 'selectedDate:', selectedDate); const studyTimeEl = document.getElementById('daily-study-time'); @@ -1000,11 +1003,11 @@ function renderTasks() { document.querySelectorAll('.task-item').forEach(el => { el.addEventListener('click', (e) => { if (e.target.closest('.task-actions') || e.target.closest('.task-check')) return; - + const taskId = el.dataset.id; const task = store.tasks.find(t => String(t.id) === String(taskId)); if (task && task._isEditing) return; - + store.toggleTaskStatus(taskId); }); }); @@ -1040,6 +1043,7 @@ function renderTasks() { e.stopPropagation(); const taskId = el.dataset.id; const itemEl = el.closest('.task-item'); + const rawTitle = itemEl.querySelector('.board-edit-title').value; const subject_id = itemEl.querySelector('.board-edit-subject').value; @@ -1123,32 +1127,32 @@ function renderCalendar() { const calTitle = document.getElementById('cal-month-title'); const calGrid = document.getElementById('cal-grid'); if (!calGrid) return; - + const year = currentMonthDate.getFullYear(); const month = currentMonthDate.getMonth(); - + const monthNames = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; calTitle.textContent = `${monthNames[month]} ${year}`; - + const topbarTitle = document.querySelector('.topbar-title'); - if(topbarTitle) topbarTitle.textContent = `${monthNames[month]} ${year}`; + if (topbarTitle) topbarTitle.textContent = `${monthNames[month]} ${year}`; const firstDay = new Date(year, month, 1).getDay(); const daysInMonth = new Date(year, month + 1, 0).getDate(); const prevMonthDays = new Date(year, month, 0).getDate(); - + const today = new Date(); - + let html = `
Su
Mo
Tu
We
Th
Fr
Sa
`; - + for (let i = 0; i < firstDay; i++) { html += `
${prevMonthDays - firstDay + i + 1}
`; } - + for (let i = 1; i <= daysInMonth; i++) { const isToday = i === today.getDate() && month === today.getMonth() && year === today.getFullYear(); const isSelected = selectedDate && i === selectedDate.getDate() && month === selectedDate.getMonth() && year === selectedDate.getFullYear(); - + // Find tasks for this day const dayTasks = store.tasks.filter(t => { if (t.archived) return false; @@ -1162,9 +1166,9 @@ function renderCalendar() { if (dayTasks.length > 0) { indicatorHtml = `
`; dayTasks.forEach((t, idx) => { - if (idx > 2) return; - const sub = store.subjects.find(s => s.id === t.subject_id) || store.subjects[0]; - indicatorHtml += `
`; + if (idx > 2) return; + const sub = store.subjects.find(s => s.id === t.subject_id) || store.subjects[0]; + indicatorHtml += `
`; }); indicatorHtml += `
`; } @@ -1176,13 +1180,13 @@ function renderCalendar() { ${indicatorHtml}
`; } - + const totalCells = firstDay + daysInMonth; const nextDays = (7 - (totalCells % 7)) % 7; for (let i = 1; i <= nextDays; i++) { html += `
${i}
`; } - + calGrid.innerHTML = html; // Bind day clicks @@ -1190,7 +1194,7 @@ function renderCalendar() { el.addEventListener('click', (e) => { const d = parseInt(e.currentTarget.getAttribute('data-day')); const clickedDate = new Date(year, month, d); - + if (selectedDate && clickedDate.getTime() === selectedDate.getTime()) { selectedDate = null; } else { @@ -1210,24 +1214,24 @@ function renderExtraction() { addItemsBtn.textContent = 'Add items to planner'; return; } - + addItemsBtn.disabled = false; addItemsBtn.textContent = `Add ${pasteItems.length} items to planner`; - + let html = `
Extracted — ${pasteItems.length} items
`; pasteItems.forEach((item, index) => { // try to match subject name const sub = store.subjects.find(s => s.name.toLowerCase().includes((item.subject_name || '').toLowerCase())) || store.subjects[3]; // Attach subject id to item so Add will work item.subject_id = sub.id; - + if (item._isEditing) { - let subjectOptions = store.subjects.map(s => + let subjectOptions = store.subjects.map(s => `` ).join(''); - + const localDate = item.due_at ? new Date(item.due_at).toISOString().substring(0, 16) : ''; - + html += `
@@ -1262,15 +1266,15 @@ function renderExtraction() { `; } }); - + extractPreview.innerHTML = html; - + setTimeout(() => { document.querySelectorAll('.conf-fill').forEach(el => { el.style.width = el.getAttribute('data-width') + '%'; }); }, 100); - + document.querySelectorAll('.conf-edit').forEach(btn => { btn.addEventListener('click', (e) => { const idx = e.target.getAttribute('data-index'); @@ -1286,9 +1290,9 @@ function renderExtraction() { const title = card.querySelector('.edit-title-input').value; let dateVal = card.querySelector('.edit-date-input').value; const notes = card.querySelector('.edit-notes-input').value; - + const newSubject = store.subjects.find(s => s.id === subjectId); - + store.updateExtractedItem(idx, { subject_id: subjectId, subject_name: newSubject ? newSubject.name : 'General', @@ -1301,12 +1305,90 @@ function renderExtraction() { }); } +function calculateStreak(tasks) { + const completedTasks = tasks.filter(t => t.status === 'Done' && t.due_at && !t.archived); + + const dates = new Set(); + completedTasks.forEach(t => { + const d = new Date(t.due_at); + if (!isNaN(d.getTime())) { + dates.add(`${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`); + } + }); + + if (dates.size === 0) return 0; + + const today = new Date(); + const todayStr = `${today.getFullYear()}-${today.getMonth()}-${today.getDate()}`; + + const yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); + const yesterdayStr = `${yesterday.getFullYear()}-${yesterday.getMonth()}-${yesterday.getDate()}`; + + let streak = 0; + let checkDate = new Date(); + let checkStr = todayStr; + + if (dates.has(todayStr)) { + streak = 1; + } else if (dates.has(yesterdayStr)) { + streak = 1; + checkDate = yesterday; + checkStr = yesterdayStr; + } else { + return 0; + } + + while (true) { + checkDate.setDate(checkDate.getDate() - 1); + checkStr = `${checkDate.getFullYear()}-${checkDate.getMonth()}-${checkDate.getDate()}`; + if (dates.has(checkStr)) { + streak++; + } else { + break; + } + } + + return streak; +} + +function renderStreak() { + const streakCount = calculateStreak(store.tasks); + + const streakCountEl = document.getElementById('streak-count'); + if (streakCountEl) { + streakCountEl.textContent = streakCount; + } + + const badge3 = document.getElementById('badge-3-wrapper'); + const badge7 = document.getElementById('badge-7-wrapper'); + const badge30 = document.getElementById('badge-30-wrapper'); + + if (badge3) badge3.classList.toggle('hidden', streakCount < 3); + if (badge7) badge7.classList.toggle('hidden', streakCount < 7); + if (badge30) badge30.classList.toggle('hidden', streakCount < 30); + + const tooltip = document.getElementById('streak-tooltip'); + if (tooltip) { + if (streakCount >= 30) { + tooltip.textContent = '30 day badge unlocked'; + } else if (streakCount >= 7) { + tooltip.textContent = '7 day badge unlocked'; + } else if (streakCount >= 3) { + tooltip.textContent = '3 day badge unlocked'; + } else { + tooltip.textContent = 'Complete tasks to build streak & earn cool badges'; + } + } +} + store.subscribe(renderTasks); store.subscribe(renderExtraction); store.subscribe(renderCalendar); store.subscribe(renderFocusTasks); store.subscribe(renderProfileSection); store.subscribe(renderSidebarSubjects); +store.subscribe(renderStreak); document.addEventListener('DOMContentLoaded', () => { if (newSubjectColorsEl) { @@ -1406,7 +1488,7 @@ document.addEventListener('DOMContentLoaded', () => { renderTasks(); }); - if(focusModeBtn) { + if (focusModeBtn) { focusModeBtn.addEventListener('click', () => { currentView = 'focus'; hideProfileSection(); @@ -1440,6 +1522,35 @@ document.addEventListener('DOMContentLoaded', () => { }); } + document.getElementById('nav-dashboard').addEventListener('click', (e) => { + e.preventDefault(); + currentView = 'calendar'; + document.querySelector('.cal-section').classList.remove('hidden'); + document.getElementById('tasks-section').classList.remove('hidden'); + document.getElementById('focus-section').classList.add('hidden'); + updateSidebarActive('calendar-btn'); + renderTasks(); + }); + + document.getElementById('nav-tasks').addEventListener('click', (e) => { + e.preventDefault(); + currentView = 'all-tasks'; + document.querySelector('.cal-section').classList.add('hidden'); + document.getElementById('tasks-section').classList.remove('hidden'); + document.getElementById('focus-section').classList.add('hidden'); + updateSidebarActive('all-tasks-btn'); + renderTasks(); + }); + + document.getElementById('nav-calendar').addEventListener('click', (e) => { + e.preventDefault(); + currentView = 'calendar'; + document.querySelector('.cal-section').classList.remove('hidden'); + document.getElementById('tasks-section').classList.remove('hidden'); + document.getElementById('focus-section').classList.add('hidden'); + updateSidebarActive('calendar-btn'); + renderTasks(); + }); //NEw Task addition event listeners newTaskBtn.addEventListener('click', () => { @@ -1449,37 +1560,35 @@ newTaskBtn.addEventListener('click', () => { return; } - newTaskSubject.innerHTML = store.subjects - .map(s => ``) - .join(''); - + newTaskSubject.innerHTML = store.subjects + .map(s => ``) + .join(''); - if (selectedDate) { - const d = new Date(selectedDate); - d.setHours(18, 0, 0, 0); - newTaskDate.value = d.toISOString().substring(0, 16); - } else { - newTaskDate.value = ''; - } + if (selectedDate) { + const d = new Date(selectedDate); + d.setHours(18, 0, 0, 0); + newTaskDate.value = d.toISOString().substring(0, 16); + } else { + newTaskDate.value = ''; + } newTaskTitle.value = ''; newTaskNotes.value = ''; if (newTaskEstimatedDuration) newTaskEstimatedDuration.value = ''; setNewTaskDurationUnit('minutes'); + newTaskModal.style.display = 'flex'; + }); - newTaskModal.style.display = 'flex'; -}); - -newTaskCancel.addEventListener('click', () => { - newTaskModal.style.display = 'none'; -}); - -newTaskModal.addEventListener('click', (e) => { - if (e.target === newTaskModal) { + newTaskCancel.addEventListener('click', () => { newTaskModal.style.display = 'none'; - } -}); + }); +newTaskModal.addEventListener('click', (e) => { + if (e.target === newTaskModal) { + newTaskModal.style.display = 'none'; + } + }); + function setNewTaskDurationUnit(unit) { selectedTaskDurationUnit = unit; newTaskDurationSwitch?.setAttribute('data-unit', unit); @@ -1530,9 +1639,10 @@ if (!subject_id) { labels }; - await store.addTasks([newTask]); - newTaskModal.style.display = 'none'; -}); + await store.addTasks([newTask]); + newTaskModal.style.display = 'none'; + }); + addItemsBtn.addEventListener('click', () => { if (store.currentPaste) { @@ -1555,15 +1665,15 @@ if (pasteInput.value.trim() === "") { extractBtn.addEventListener('click', async () => { const text = pasteInput.value; if (!text.trim()) return; - + extractBtn.innerHTML = ''; extractBtn.disabled = true; const items = await extractTasksFromText(text); - + extractBtn.innerHTML = 'Extract with AI →'; extractBtn.disabled = false; - + store.setExtracted(items); }); @@ -1696,6 +1806,131 @@ if (quoteEl) { quoteEl.textContent = quotes[index]; } +calendarDownloadBtn.addEventListener('click', () => { + downloadCalendar(); +}); + +// ================= AUTH FRONTEND ================= + +const authModal = document.getElementById('auth-modal'); +const authTitle = document.getElementById('auth-title'); +const authSubtitle = document.getElementById('auth-subtitle'); +const authSubmitBtn = document.getElementById('auth-submit-btn'); +const authToggleBtn = document.getElementById('auth-toggle-btn'); +const authToggleText = document.getElementById('auth-toggle-text'); +const authError = document.getElementById('auth-error'); + +const emailInput = document.getElementById('auth-email'); +const passwordInput = document.getElementById('auth-password'); + +const logoutBtn = document.getElementById('logout-btn'); + +let isLoginMode = true; + +// ================= CHECK LOGIN ================= + +const savedUser = localStorage.getItem('studyplan_user'); + +if (savedUser) { + authModal.style.display = 'none'; +} else { + authModal.style.display = 'flex'; +} + +// ================= TOGGLE LOGIN/SIGNUP ================= + +authToggleBtn.addEventListener('click', (e) => { + e.preventDefault(); + + isLoginMode = !isLoginMode; + + authError.style.display = 'none'; + + if (isLoginMode) { + authTitle.textContent = 'Welcome back'; + authSubtitle.textContent = 'Sign in to your StudyPlan account'; + authSubmitBtn.textContent = 'Sign In'; + authToggleText.textContent = "Don't have an account?"; + authToggleBtn.textContent = 'Sign Up'; + } else { + authTitle.textContent = 'Create account'; + authSubtitle.textContent = 'Sign up for StudyPlan'; + authSubmitBtn.textContent = 'Sign Up'; + authToggleText.textContent = 'Already have an account?'; + authToggleBtn.textContent = 'Sign In'; + } +}); + +// ================= LOGIN / SIGNUP ================= + +authSubmitBtn.addEventListener('click', async () => { + + const email = emailInput.value.trim(); + const password = passwordInput.value.trim(); + + if (!email || !password) { + authError.textContent = 'Please fill all fields'; + authError.style.display = 'block'; + return; + } + + const endpoint = isLoginMode + ? '/api/auth/login' + : '/api/auth/signup'; + + try { + + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + email, + password + }) + }); + + const data = await response.json(); + + if (!response.ok) { + authError.textContent = data.error || 'Authentication failed'; + authError.style.display = 'block'; + return; + } + + // Save logged-in user + localStorage.setItem('studyplan_user', email); + + authModal.style.display = 'none'; + + location.reload(); + + } catch (err) { + + authError.textContent = 'Server error'; + authError.style.display = 'block'; + } +}); + +// ================= LOGOUT ================= + +logoutBtn.addEventListener('click', async () => { + + try { + + await fetch('/api/auth/logout', { + method: 'POST' + }); + + localStorage.removeItem('studyplan_user'); + + location.reload(); + + } catch (err) { + console.error(err); + } +}); if (calendarDownloadBtn) { diff --git a/public/crown-30andmorestreak.svg b/public/crown-30andmorestreak.svg new file mode 100644 index 00000000..0566ca5e --- /dev/null +++ b/public/crown-30andmorestreak.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/medal-3daystreak.svg b/public/medal-3daystreak.svg new file mode 100644 index 00000000..09db6f76 --- /dev/null +++ b/public/medal-3daystreak.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/rocket-7daystreak.svg b/public/rocket-7daystreak.svg new file mode 100644 index 00000000..1282ab9d --- /dev/null +++ b/public/rocket-7daystreak.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/streak-icon.svg b/public/streak-icon.svg new file mode 100644 index 00000000..5c3f2535 --- /dev/null +++ b/public/streak-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/server.js b/server.js index 68dadff2..13aab53f 100644 --- a/server.js +++ b/server.js @@ -553,30 +553,87 @@ Text: "${text}" return res.json(tasks); }); // ================= AUTH ================= -const users = {}; // Simple in-memory user store +// SIGNUP app.post('/api/auth/signup', (req, res) => { const { email, password } = req.body; + if (!email || !password) { - return res.status(400).json({ error: 'Email and password required' }); - } - if (users[email]) { - return res.status(400).json({ error: 'User already exists' }); + return res.status(400).json({ + error: 'Email and password required' + }); } - users[email] = { email, password }; - res.json({ success: true, message: 'Account created successfully' }); + + const id = 'user_' + Date.now(); + + db.run( + `INSERT INTO users (id, email, password) + VALUES (?, ?, ?)`, + [id, email, password], + function(err) { + + if (err) { + + if (err.message.includes('UNIQUE')) { + return res.status(400).json({ + error: 'User already exists' + }); + } + + return res.status(500).json({ + error: err.message + }); + } + + res.json({ + success: true, + message: 'Account created successfully' + }); + } + ); }); +// LOGIN app.post('/api/auth/login', (req, res) => { const { email, password } = req.body; + if (!email || !password) { - return res.status(400).json({ error: 'Email and password required' }); - } - const user = users[email]; - if (!user || user.password !== password) { - return res.status(401).json({ error: 'Invalid email or password' }); + return res.status(400).json({ + error: 'Email and password required' + }); } - res.json({ success: true, email: user.email }); + + db.get( + `SELECT * FROM users WHERE email = ?`, + [email], + (err, user) => { + + if (err) { + return res.status(500).json({ + error: err.message + }); + } + + if (!user || user.password !== password) { + return res.status(401).json({ + error: 'Invalid email or password' + }); + } + + res.json({ + success: true, + email: user.email + }); + } + ); +}); + +// LOGOUT +app.post('/api/auth/logout', (req, res) => { + res.json({ + success: true, + message: 'Logged out successfully' + }); }); // Intentional test route for verifying server error page behavior.