From 0cdcac6a3177f775fcec57bb9f97c7f041232be0 Mon Sep 17 00:00:00 2001 From: karrisanthoshigayatri Date: Tue, 9 Jun 2026 17:18:28 +0530 Subject: [PATCH 1/4] 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 93befe3..599fd73 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 543be76..e0c921c 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 f69f7f6..c87ad17 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 3c72ceac11c8e94a912d4182e91f3a6a3430a4ab Mon Sep 17 00:00:00 2001 From: karrisanthoshigayatri Date: Tue, 9 Jun 2026 17:27:15 +0530 Subject: [PATCH 2/4] focused statistics dashboard --- css/index.css | 40 +++++++++++++++++++++++++++++++++++ database.js | 9 ++++++++ index.html | 15 +++++++++++++ js/app.js | 54 +++++++++++++++++++++++++++++++++++++++++++++++ js/store.js | 31 +++++++++++++++++++++++++++ server.js | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 207 insertions(+) diff --git a/css/index.css b/css/index.css index 599fd73..42433fb 100644 --- a/css/index.css +++ b/css/index.css @@ -1276,6 +1276,44 @@ body { gap: 24px; } +.focus-stats-panel { + width: 100%; + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 14px; +} + +.focus-stat-card { + background: var(--color-background-secondary); + border: 1px solid var(--color-border-secondary); + border-radius: var(--border-radius-md); + padding: 18px 16px; + display: flex; + flex-direction: column; + gap: 8px; + min-width: 0; +} + +.focus-stat-label { + font-size: 12px; + font-weight: 700; + color: var(--color-text-tertiary); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.focus-stat-value { + font-size: 24px; + font-weight: 700; + color: var(--color-text-primary); +} + +@media (max-width: 820px) { + .focus-stats-panel { + grid-template-columns: 1fr; + } +} + .timer-container { position: relative; width: 240px; @@ -5074,3 +5112,5 @@ body { @keyframes slideDown { to { opacity: 0; transform: translateY(10px); } } + + diff --git a/database.js b/database.js index 33a1b17..311bf7f 100644 --- a/database.js +++ b/database.js @@ -33,6 +33,15 @@ function initDb() { FOREIGN KEY (subject_id) REFERENCES subjects(id) )`); + // Focus Sessions Table + db.run(`CREATE TABLE IF NOT EXISTS focus_sessions ( + id TEXT PRIMARY KEY, + task_id TEXT, + duration_seconds INTEGER NOT NULL, + completed_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (task_id) REFERENCES tasks(id) + )`); + db.all("PRAGMA table_info(tasks)", (err, rows) => { if (err) return; diff --git a/index.html b/index.html index e0c921c..8d8f846 100644 --- a/index.html +++ b/index.html @@ -191,6 +191,21 @@

StudyPlan

+ +
+
+
Sessions completed
+
0
+
+
+
Total focus time
+
0m
+
+
+
Current streak
+
0 days
+
+
Current Focus Task
diff --git a/js/app.js b/js/app.js index c87ad17..105ae31 100644 --- a/js/app.js +++ b/js/app.js @@ -99,6 +99,10 @@ 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 focusStatsPanel = document.getElementById('focus-stats-panel'); +const focusTotalSessions = document.getElementById('focus-total-sessions'); +const focusTotalTime = document.getElementById('focus-total-time'); +const focusCurrentStreak = document.getElementById('focus-current-streak'); if (labelFilterSelect) { labelFilterSelect.addEventListener('change', (e) => { @@ -286,6 +290,53 @@ function showBrowserNotification() { } } +async function recordCompletedFocusSession(durationSeconds) { + await store.addFocusSession({ + task_id: activeFocusTaskId || null, + duration_seconds: durationSeconds, + completed_at: new Date().toISOString() + }); +} + +function formatSessionDuration(seconds) { + const mins = Math.floor(seconds / 60); + const hrs = Math.floor(mins / 60); + const remainder = mins % 60; + if (hrs > 0) { + return `${hrs}h ${remainder}m`; + } + return `${remainder}m`; +} + +function calculateFocusStreak(sessions) { + const daySet = new Set( + (sessions || []).map(s => new Date(s.completed_at).toISOString().substring(0, 10)) + ); + let streak = 0; + const today = new Date(); + today.setHours(0, 0, 0, 0); + let cursor = new Date(today); + + while (daySet.has(cursor.toISOString().substring(0, 10))) { + streak += 1; + cursor.setDate(cursor.getDate() - 1); + } + + return streak; +} + +function renderFocusStats() { + if (!focusStatsPanel) return; + const sessions = store.focusSessions || []; + const totalSessions = sessions.length; + const totalSeconds = sessions.reduce((acc, session) => acc + Number(session.duration_seconds || 0), 0); + const streak = calculateFocusStreak(sessions); + + focusTotalSessions.textContent = totalSessions; + focusTotalTime.textContent = formatSessionDuration(totalSeconds); + focusCurrentStreak.textContent = `${streak} day${streak === 1 ? '' : 's'}`; +} + function startTimer() { if (timerInterval) return; TIME_LIMIT = getTimerDuration(); @@ -308,6 +359,7 @@ function startTimer() { playCompletionSound(); showBrowserNotification(); Toast.show('Focus session complete!', 'success'); + recordCompletedFocusSession(TIME_LIMIT).then(() => renderFocusStats()); resetTimer(); } }, 1000); @@ -1178,6 +1230,7 @@ store.subscribe(renderTasks); store.subscribe(renderExtraction); store.subscribe(renderCalendar); store.subscribe(renderFocusTasks); +store.subscribe(renderFocusStats); store.subscribe(renderProfileSection); store.subscribe(renderSidebarSubjects); @@ -1237,6 +1290,7 @@ document.addEventListener('DOMContentLoaded', () => { } store.fetchInitialData(); + store.fetchFocusSessions(); const calendarBtn = document.getElementById('calendar-btn'); const allTasksBtn = document.getElementById('all-tasks-btn'); diff --git a/js/store.js b/js/store.js index d561435..c10e152 100644 --- a/js/store.js +++ b/js/store.js @@ -5,6 +5,7 @@ export const store = { subjects: [], tasks: [], currentPaste: null, + focusSessions: [], listeners: [], isSameCalendarDate(dateA, dateB) { @@ -314,6 +315,36 @@ export const store = { } }, + async fetchFocusSessions() { + try { + const res = await fetch('/api/focus-sessions'); + if (!res.ok) throw new Error('Failed to fetch focus sessions'); + this.focusSessions = await res.json(); + this.notify(); + } catch (e) { + console.error('Failed to load focus sessions', e); + } + }, + + async addFocusSession(sessionData) { + try { + const res = await fetch('/api/focus-sessions', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(sessionData) + }); + + if (!res.ok) { + const body = await res.json().catch(() => ({})); + throw new Error(body.error || 'Failed to record focus session'); + } + + await this.fetchFocusSessions(); + } catch (e) { + console.error('Failed to save focus session', e); + } + }, + clearExtracted() { this.currentPaste = null; this.notify(); diff --git a/server.js b/server.js index c09dbc7..81fd3de 100644 --- a/server.js +++ b/server.js @@ -319,6 +319,45 @@ app.get('/api/tasks', (req, res) => { }); }); +app.get('/api/focus-sessions', (req, res) => { + const query = ` + SELECT fs.*, t.title AS task_title + FROM focus_sessions fs + LEFT JOIN tasks t ON t.id = fs.task_id + ORDER BY datetime(fs.completed_at) DESC + `; + db.all(query, (err, rows) => { + if (err) return res.status(500).json({ error: err.message }); + res.json(rows); + }); +}); + +app.get('/api/focus-stats', (req, res) => { + db.all('SELECT duration_seconds, completed_at FROM focus_sessions ORDER BY datetime(completed_at) ASC', (err, rows) => { + if (err) return res.status(500).json({ error: err.message }); + + const totalSessions = rows.length; + const totalSeconds = rows.reduce((sum, row) => sum + Number(row.duration_seconds || 0), 0); + const daySet = new Set(rows.map(row => new Date(row.completed_at).toISOString().substring(0, 10))); + + let streak = 0; + const today = new Date(); + today.setHours(0, 0, 0, 0); + let cursor = new Date(today); + + while (daySet.has(cursor.toISOString().substring(0, 10))) { + streak += 1; + cursor.setDate(cursor.getDate() - 1); + } + + res.json({ + totalSessions, + totalSeconds, + currentStreakDays: streak, + }); + }); +}); + // ================= ADD TASKS ================= app.post('/api/tasks', (req, res) => { try { @@ -434,6 +473,25 @@ app.post('/api/tasks', (req, res) => { } }); +app.post('/api/focus-sessions', (req, res) => { + const { task_id, duration_seconds, completed_at } = req.body; + if (!Number.isFinite(Number(duration_seconds)) || Number(duration_seconds) <= 0) { + return res.status(400).json({ error: 'duration_seconds must be a positive number' }); + } + + const id = `focus_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; + const timestamp = completed_at ? completed_at : new Date().toISOString(); + + db.run( + 'INSERT INTO focus_sessions (id, task_id, duration_seconds, completed_at) VALUES (?, ?, ?, ?)', + [id, task_id || null, Number(duration_seconds), timestamp], + function (err) { + if (err) return res.status(500).json({ error: err.message }); + res.status(201).json({ success: true, id, task_id: task_id || null, duration_seconds: Number(duration_seconds), completed_at: timestamp }); + } + ); +}); + // ================= UPDATE ================= app.put('/api/tasks/:id', (req, res) => { From d635d5954726e5166d528fb1d20c8f348237f253 Mon Sep 17 00:00:00 2001 From: karrisanthoshigayatri Date: Wed, 10 Jun 2026 07:11:50 +0530 Subject: [PATCH 3/4] sssssw --- css/index.css | 189 ++++++++++++++++++++------------------------------ js/app.js | 170 ++++++++++++++++++++++++++++++++++++++------- js/store.js | 44 ++++++++++++ server.js | 37 ++++++++++ 4 files changed, 302 insertions(+), 138 deletions(-) diff --git a/css/index.css b/css/index.css index 42433fb..6c8a61a 100644 --- a/css/index.css +++ b/css/index.css @@ -621,6 +621,66 @@ body { .task-check:hover { border-color: var(--color-text-primary); } .task-check.done { background: var(--color-text-success); border-color: var(--color-text-success); } .task-check.done::after { content: ''; position: absolute; left: 4px; top: 1px; width: 4px; height: 8px; border: solid white; border-width: 0 2px 2px 0; transform: rotate(45deg); } + +/* Completion animations: subtle pop for the checkbox and a gentle bloom for the whole task */ +@keyframes check-pop { + 0% { transform: scale(0.6); opacity: 0; } + 60% { transform: scale(1.12); opacity: 1; } + 100% { transform: scale(1); } +} + +@keyframes checkmark-draw { + 0% { transform: rotate(45deg) scaleY(0); opacity: 0; } + 60% { transform: rotate(45deg) scaleY(1.05); opacity: 1; } + 100% { transform: rotate(45deg) scaleY(1); opacity: 1; } +} + +@keyframes completed-bloom { + 0% { + box-shadow: 0 0 0 0 rgba(22, 101, 52, 0.18); + transform: translateY(0); + } + 60% { + box-shadow: 0 8px 30px 6px rgba(22, 101, 52, 0.06); + transform: translateY(-2px); + } + 100% { + box-shadow: none; + transform: translateY(0); + } +} + +/* Apply the animations when a task is rendered as done. These are subtle and short. */ +.task-check.done { + animation: check-pop 360ms cubic-bezier(0.2, 0.9, 0.29, 1) both; +} +.task-check.done::after { + animation: checkmark-draw 260ms ease-out 90ms both; + transform-origin: center center; +} + +.task-item.done { + /* stronger border & clearer muted text to distinguish from pending tasks */ + background: linear-gradient(135deg, rgba(var(--color-success-rgb), 0.08) 0%, var(--color-background-primary) 100%); + border-color: var(--color-border-success); + color: var(--color-text-tertiary); + animation: completed-bloom 520ms cubic-bezier(0.2, 0.9, 0.29, 1) both; +} + +/* Make completed tasks slightly more visually distinct: lighter name color, reduced weight */ +.task-item.done .task-name { + color: var(--color-text-tertiary); + font-weight: 500; + opacity: 0.95; +} + +/* Respect users who prefer reduced motion: disable the animations and rely on transitions */ +@media (prefers-reduced-motion: reduce) { + .task-check, .task-check.done, .task-check.done::after, .task-item.done { + animation: none !important; + transition-duration: 0.01ms !important; + } +} .task-info { flex: 1; min-width: 0; } .task-name { font-size: 14px; font-weight: 500; color: var(--color-text-primary); line-height: 1.4; transition: color 0.2s; margin-bottom: 6px; } .task-item.done .task-name { text-decoration: line-through; color: var(--color-text-tertiary); } @@ -4982,135 +5042,36 @@ body { font-weight: 600; } -.paste-zone { - transition: border-color 0.2s ease, background-color 0.2s ease; -} - -.paste-zone--dragover { - border-color: #4f46e5 !important; - background-color: rgba(79, 70, 229, 0.05) !important; - box-shadow: inset 0 0 8px rgba(79, 70, 229, 0.1); -} -/* ========================================================================== - Modern Toast Notifications - ========================================================================== */ - -.toast-container { - position: fixed; - top: 24px; - right: 24px; - z-index: 10000; - display: flex; - flex-direction: column; - gap: 12px; - pointer-events: none; -} - -.toast-notification { - pointer-events: auto; +.subject-sidebar-item { display: flex; align-items: center; - gap: 12px; - background: var(--color-background-primary); - color: var(--color-text-primary); - padding: 12px 16px; - border-radius: 12px; - box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2); - border: 1px solid var(--color-border-secondary); - font-family: 'Inter', sans-serif; - font-size: 14px; - font-weight: 500; - animation: toastSlideIn 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards; - max-width: 350px; - transform-origin: top right; -} - -.toast-notification.toast-hiding { - animation: toastSlideOut 0.2s ease-in forwards; -} - -.toast-icon { - font-size: 18px; - flex-shrink: 0; -} - -.toast-message { - flex-grow: 1; - line-height: 1.4; + justify-content: space-between; } -.toast-close { - background: none; - border: none; - color: var(--color-text-tertiary); - font-size: 18px; - cursor: pointer; - padding: 0; - margin-left: 8px; - flex-shrink: 0; +.subject-sidebar-content { display: flex; align-items: center; - justify-content: center; - transition: color 0.2s; -} - -.toast-close:hover { - color: var(--color-text-primary); -} - -/* Toast Themes */ -.toast-success { border-left: 4px solid var(--color-text-success); } -.toast-error { border-left: 4px solid var(--color-text-danger); } -.toast-warning { border-left: 4px solid var(--color-text-warning); } -.toast-info { border-left: 4px solid var(--color-text-info); } - -@keyframes toastSlideIn { - from { opacity: 0; transform: translateY(-20px) scale(0.95); } - to { opacity: 1; transform: translateY(0) scale(1); } -} - -@keyframes toastSlideOut { - from { opacity: 1; transform: scale(1); } - to { opacity: 0; transform: scale(0.9); } + gap: 10px; } -/* ========================================================================== - Modern Confirm Modal - ========================================================================== */ -.custom-confirm-backdrop { - position: fixed; - inset: 0; - background: rgba(0,0,0,0.5); - z-index: 10001; +.subject-sidebar-actions { display: flex; align-items: center; - justify-content: center; - backdrop-filter: blur(4px); - opacity: 0; + gap: 8px; } -.custom-confirm-modal { - background: var(--color-background-primary); - border: 1px solid var(--color-border-tertiary); - color: var(--color-text-primary); +.delete-subject-btn { opacity: 0; - transform: translateY(10px); -} - -@keyframes fadeIn { - to { opacity: 1; } -} - -@keyframes fadeOut { - to { opacity: 0; } -} - -@keyframes slideUp { - to { opacity: 1; transform: translateY(0); } + border: none; + background: transparent; + color: var(--color-text-danger); + cursor: pointer; + font-size: 14px; + transition: opacity 0.2s ease; } -@keyframes slideDown { - to { opacity: 0; transform: translateY(10px); } +.subject-sidebar-item:hover .delete-subject-btn { + opacity: 1; } diff --git a/js/app.js b/js/app.js index 105ae31..e2dae92 100644 --- a/js/app.js +++ b/js/app.js @@ -174,10 +174,43 @@ function renderSidebarSubjects() { listEl.innerHTML = subjects.map(s => { const n = countBySubject[s.id] ?? 0; const safeColor = s.color ? escapeHtml(s.color) : 'var(--color-text-info)'; - return ``; +return ` + +`; }).join(''); + + document.querySelectorAll('.delete-subject-btn') + .forEach(btn => { + btn.addEventListener('click', (e) => { + e.stopPropagation(); + + const subjectId = btn.dataset.subjectId; + + store.deleteSubject(subjectId); + }); + }); } const newTaskModal = document.getElementById('new-task-modal'); @@ -240,6 +273,80 @@ function setCircleDasharray() { timerPathRemaining.setAttribute("stroke-dasharray", circleDasharray); } +let startTime; + +function saveTimerState() { + localStorage.setItem('focusTimerState', JSON.stringify({ + TIME_LIMIT, + timePassed, + isRunning: !!timerInterval, + startTime: timerInterval ? startTime : null, + durationInput: timerDurationInput.value + })); +} + +function loadTimerState() { + const saved = localStorage.getItem('focusTimerState'); + if (!saved) return; + + try { + const state = JSON.parse(saved); + + TIME_LIMIT = state.TIME_LIMIT || (25 * 60); + + if (state.isRunning && state.startTime) { + const elapsed = Math.floor((Date.now() - state.startTime) / 1000); + timePassed = elapsed; + } else { + timePassed = state.timePassed || 0; + } + + timeLeft = TIME_LIMIT - timePassed; + + if (timeLeft < 0) timeLeft = 0; + + if (state.durationInput) { + timerDurationInput.value = state.durationInput; + } + + timerText.innerHTML = formatTimeLeft(timeLeft); + setCircleDasharray(); + updateTimerColor(); + + if (state.isRunning && timeLeft > 0) { + startTime = Date.now() - (timePassed * 1000); + + timerInterval = setInterval(() => { + timePassed = Math.floor((Date.now() - startTime) / 1000); + timeLeft = TIME_LIMIT - timePassed; + + timerText.innerHTML = formatTimeLeft(timeLeft); + setCircleDasharray(); + updateTimerColor(); + + saveTimerState(); + + if (timeLeft <= 0) { + clearInterval(timerInterval); + timerInterval = null; + localStorage.removeItem('focusTimerState'); + + playCompletionSound(); + showBrowserNotification(); + Toast.show('Focus session complete!', 'success'); + + resetTimer(); +} + }, 250); + + timerPauseBtn.classList.remove('hidden'); + timerStartBtn.classList.add('hidden'); + timerDurationInput.disabled = true; + } + } catch (err) { + console.error('Failed to load timer state', err); + } +} function getTimerColor(timeLeft, totalTime) { const fraction = timeLeft / totalTime; if (fraction <= 0.1) return '#ef4444'; @@ -339,23 +446,29 @@ function renderFocusStats() { function startTimer() { if (timerInterval) return; + TIME_LIMIT = getTimerDuration(); - if (timePassed === 0) timeLeft = TIME_LIMIT; timerDurationInput.disabled = true; + timerStartBtn.classList.add('hidden'); timerPauseBtn.classList.remove('hidden'); requestNotificationPermission(); - - timerInterval = setInterval(() => { - timePassed += 1; + + startTime = Date.now() - (timePassed * 1000); + saveTimerState(); + timerInterval = setInterval(() => { + timePassed = Math.floor((Date.now() - startTime) / 1000); timeLeft = TIME_LIMIT - timePassed; + timerText.innerHTML = formatTimeLeft(timeLeft); setCircleDasharray(); updateTimerColor(); + saveTimerState(); - if (timeLeft === 0) { + if (timeLeft <= 0) { clearInterval(timerInterval); timerInterval = null; + localStorage.removeItem('focusTimerState'); playCompletionSound(); showBrowserNotification(); Toast.show('Focus session complete!', 'success'); @@ -363,11 +476,17 @@ function startTimer() { resetTimer(); } }, 1000); + + timerPauseBtn.classList.remove('hidden'); + timerStartBtn.classList.add('hidden'); } function pauseTimer() { clearInterval(timerInterval); timerInterval = null; + + saveTimerState(); + timerPauseBtn.classList.add('hidden'); timerStartBtn.classList.remove('hidden'); } @@ -375,13 +494,18 @@ function pauseTimer() { function resetTimer() { clearInterval(timerInterval); timerInterval = null; + + localStorage.removeItem('focusTimerState'); + timePassed = 0; TIME_LIMIT = getTimerDuration(); timeLeft = TIME_LIMIT; + timerDurationInput.disabled = false; + timerText.innerHTML = formatTimeLeft(timeLeft); timerPathRemaining.setAttribute("stroke-dasharray", "283 283"); - timerPathRemaining.style.stroke = 'var(--color-text-primary)'; + updateTimerColor(); timerPauseBtn.classList.add('hidden'); timerStartBtn.classList.remove('hidden'); } @@ -1290,7 +1414,11 @@ document.addEventListener('DOMContentLoaded', () => { } store.fetchInitialData(); +<<<<<<< HEAD store.fetchFocusSessions(); +======= + loadTimerState(); +>>>>>>> upstream/main const calendarBtn = document.getElementById('calendar-btn'); const allTasksBtn = document.getElementById('all-tasks-btn'); @@ -1344,6 +1472,7 @@ document.addEventListener('DOMContentLoaded', () => { }); } +<<<<<<< HEAD if (profileBtn) { profileBtn.addEventListener('click', () => { showProfileSection(); @@ -1355,6 +1484,8 @@ document.addEventListener('DOMContentLoaded', () => { renderCalendar(); }); +======= +>>>>>>> upstream/main document.getElementById('cal-next').addEventListener('click', () => { currentMonthDate.setMonth(currentMonthDate.getMonth() + 1); renderCalendar(); @@ -1478,21 +1609,6 @@ extractBtn.addEventListener('click', async () => { extractBtn.innerHTML = ''; extractBtn.disabled = true; - - // Show loading skeleton - extractPreview.innerHTML = ` -
Extracting tasks...
-
-
-
-
-
-
-
-
-
-
- `; const items = await extractTasksFromText(text); @@ -1632,3 +1748,9 @@ if (quoteEl) { quoteEl.textContent = quotes[index]; } + +if (calendarDownloadBtn) { + calendarDownloadBtn.addEventListener('click', () => { + downloadCalendar(); + }); +} \ No newline at end of file diff --git a/js/store.js b/js/store.js index c10e152..b5e0ef1 100644 --- a/js/store.js +++ b/js/store.js @@ -66,6 +66,50 @@ export const store = { } }, + // ================= DELETE SUBJECT FUNCTION ================= + + async deleteSubject(subjectId) { + const subject = this.subjects.find( + s => String(s.id) === String(subjectId) + ); + + if (!subject) return; + + const confirmed = confirm( + `Are you sure you want to delete "${subject.name}"?\n\nThis will also remove related tasks.` + ); + + if (!confirmed) return; + + const originalSubjects = [...this.subjects]; + const originalTasks = [...this.tasks]; + + // optimistic update + this.subjects = this.subjects.filter( + s => String(s.id) !== String(subjectId) + ); + + this.tasks = this.tasks.filter( + t => String(t.subject_id) !== String(subjectId) + ); + + this.notify(); + + try { + await fetch(`/api/subjects/${subjectId}`, { + method: 'DELETE' + }); + } catch (e) { + this.subjects = originalSubjects; + this.tasks = originalTasks; + this.notify(); + + console.error('Failed to delete subject', e); + alert('❌ Failed to delete subject'); + } +}, + + // ================= UPDATED FUNCTION ================= async addTasks(newTasks) { try { diff --git a/server.js b/server.js index 81fd3de..e7b2fe8 100644 --- a/server.js +++ b/server.js @@ -304,6 +304,43 @@ app.post('/api/subjects', (req, res) => { ) }); +// ================= DELETE SUBJECT ================= +app.delete('/api/subjects/:id', (req, res) => { + + const { id } = req.params; + + db.run( + 'DELETE FROM tasks WHERE subject_id = ?', + [id], + function(taskErr) { + + if (taskErr) { + return res.status(500).json({ + error: taskErr.message + }); + } + + db.run( + 'DELETE FROM subjects WHERE id = ?', + [id], + function(subjectErr) { + + if (subjectErr) { + return res.status(500).json({ + error: subjectErr.message + }); + } + + res.json({ + success: true, + deleted: this.changes + }); + } + ); + } + ); +}); + // ================= TASKS ================= app.get('/api/tasks', (req, res) => { db.all('SELECT * FROM tasks ORDER BY due_at ASC', (err, rows) => { From a561c0096e7e58ca6810b5fbd93c31fac4b0099b Mon Sep 17 00:00:00 2001 From: karrisanthoshigayatri Date: Wed, 10 Jun 2026 07:13:08 +0530 Subject: [PATCH 4/4] added dark mode toggle --- index.html | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/index.html b/index.html index 8d8f846..0bfbb85 100644 --- a/index.html +++ b/index.html @@ -541,7 +541,9 @@

{ - if (e.target.checked) { + const isCompact = e.target.checked; + localStorage.setItem('studyplan_compact_view', isCompact); + if (isCompact) { document.body.classList.add('compact-view'); } else { document.body.classList.remove('compact-view');