Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions database.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@ function initDb() {
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`);

// Subject Details Table (Topics, Resources)
db.run(`CREATE TABLE IF NOT EXISTS subject_details (
id TEXT PRIMARY KEY,
subject_id TEXT,
type TEXT NOT NULL, -- 'topic', 'link', 'note'
content TEXT NOT NULL,
is_completed INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (subject_id) REFERENCES subjects(id) ON DELETE CASCADE
)`);

// Tasks Table
db.run(`CREATE TABLE IF NOT EXISTS tasks (
id TEXT PRIMARY KEY,
Expand Down
28 changes: 27 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,7 @@ <h3 style="font-size:12px; font-weight:700; text-transform:uppercase; color:var(

<div id="new-subject-modal" class="modal-backdrop" style="display:none; position:fixed; inset:0; z-index:9998; align-items:center; justify-content:center;">
<div class="modal-card" style="border-radius:12px; padding:20px; width:320px; box-shadow:0 12px 40px rgba(0,0,0,0.25);">
<h3 style="margin:0 0 12px; font-size:18px; font-weight:600;">New subject</h3>
<h3 id="subject-modal-title" style="margin:0 0 12px; font-size:18px; font-weight:600;">New subject</h3>
<label style="display:block; font-size:12px; margin-bottom:4px;">Subject name</label>
<input id="new-subject-name" type="text" style="width:100%; padding:6px; margin-bottom:12px;" placeholder="e.g. History">
<label style="display:block; font-size:12px; margin-bottom:6px;">Color</label>
Expand All @@ -429,6 +429,32 @@ <h3 style="margin:0 0 12px; font-size:18px; font-weight:600;">New subject</h3>
</div>
</div>

<div id="subject-details-modal" class="modal-backdrop" style="display:none; position:fixed; inset:0; z-index:9998; align-items:center; justify-content:center;">
<div class="modal-card" style="border-radius:12px; padding:24px; width:480px; max-height: 80vh; overflow-y: auto; box-shadow:0 12px 40px rgba(0,0,0,0.25);">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
<h3 id="detail-modal-subject-name" style="margin:0; font-size:20px; font-weight:700;">Subject Details</h3>
<button id="subject-details-close" style="background:none; border:none; font-size:24px; cursor:pointer;">&times;</button>
</div>

<div style="margin-bottom: 24px;">
<h4 style="font-size:12px; font-weight:700; text-transform:uppercase; color:var(--color-text-tertiary); margin-bottom:12px;">Add Topic or Resource</h4>
<div style="display:flex; gap:8px;">
<select id="detail-type-select" style="padding:6px; border-radius:4px; border:1px solid var(--color-border-secondary);">
<option value="topic">Topic</option>
<option value="link">Resource Link</option>
<option value="note">Note</option>
</select>
<input id="detail-content-input" type="text" style="flex:1; padding:6px; border-radius:4px; border:1px solid var(--color-border-secondary);" placeholder="Enter topic, link or note...">
<button id="add-detail-btn" class="btn btn-primary" style="padding:6px 12px;">Add</button>
</div>
</div>

<div id="subject-details-list">
<!-- Details will be injected here -->
</div>
</div>
</div>

<div id="new-task-modal" class="modal-backdrop" style="display:none; position:fixed; inset:0; z-index:9998; align-items:center; justify-content:center;">
<div class="modal-card" style=" border-radius:12px; padding:20px; width:320px; box-shadow:0 12px 40px rgba(0,0,0,0.25);">
<h3 style="margin:0 0 12px; font-size:18px; font-weight:600;">New task</h3>
Expand Down
146 changes: 141 additions & 5 deletions js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ const SUBJECT_COLORS = [
];

let selectedNewSubjectColor = SUBJECT_COLORS[0];
let editingSubjectId = null;

function escapeHtml(s) {
return String(s)
Expand All @@ -87,6 +88,7 @@ function escapeHtml(s) {
}

const newSubjectModal = document.getElementById('new-subject-modal');
const subjectModalTitle = document.getElementById('subject-modal-title');
const newSubjectName = document.getElementById('new-subject-name');
const newSubjectColorsEl = document.getElementById('new-subject-colors');
const newSubjectCancel = document.getElementById('new-subject-cancel');
Expand All @@ -102,15 +104,88 @@ function syncNewSubjectColorSwatches() {
});
}

function openNewSubjectModal() {
function openNewSubjectModal(subject = null) {
if (!newSubjectModal || !newSubjectName) return;
newSubjectName.value = '';
selectedNewSubjectColor = SUBJECT_COLORS[0];

if (subject) {
editingSubjectId = subject.id;
subjectModalTitle.textContent = 'Edit Subject';
newSubjectName.value = subject.name;
selectedNewSubjectColor = subject.color || SUBJECT_COLORS[0];
} else {
editingSubjectId = null;
subjectModalTitle.textContent = 'New Subject';
newSubjectName.value = '';
selectedNewSubjectColor = SUBJECT_COLORS[0];
}

syncNewSubjectColorSwatches();
newSubjectModal.style.display = 'flex';
newSubjectName.focus();
}

const subjectDetailsModal = document.getElementById('subject-details-modal');
const detailModalSubjectName = document.getElementById('detail-modal-subject-name');
const subjectDetailsList = document.getElementById('subject-details-list');
const detailTypeSelect = document.getElementById('detail-type-select');
const detailContentInput = document.getElementById('detail-content-input');
const addDetailBtn = document.getElementById('add-detail-btn');
const subjectDetailsClose = document.getElementById('subject-details-close');
let activeDetailSubjectId = null;

async function openSubjectDetails(subjectId) {
const subject = store.subjects.find(s => s.id === subjectId);
if (!subject) return;

activeDetailSubjectId = subjectId;
detailModalSubjectName.textContent = subject.name;
subjectDetailsModal.style.display = 'flex';
renderSubjectDetails();
}

async function renderSubjectDetails() {
if (!activeDetailSubjectId) return;
const details = await store.fetchSubjectDetails(activeDetailSubjectId);

if (details.length === 0) {
subjectDetailsList.innerHTML = '<div style="text-align:center; color:var(--color-text-tertiary); padding:20px;">No topics or resources yet.</div>';
} else {
subjectDetailsList.innerHTML = details.map(d => {
const icon = d.type === 'topic' ? '📚' : (d.type === 'link' ? '🔗' : '📝');
const isCompleted = d.is_completed === 1;
return `
<div class="detail-item" style="display:flex; align-items:center; gap:12px; padding:10px; border-bottom:1px solid var(--color-border-tertiary);">
<span style="font-size:16px;">${icon}</span>
<div style="flex:1; ${isCompleted ? 'text-decoration:line-through; opacity:0.6;' : ''}">
${d.type === 'link' ? `<a href="${escapeHtml(d.content)}" target="_blank" style="color:var(--color-text-info);">${escapeHtml(d.content)}</a>` : escapeHtml(d.content)}
</div>
<div style="display:flex; gap:8px;">
${d.type === 'topic' ? `<input type="checkbox" ${isCompleted ? 'checked' : ''} class="toggle-detail-completion" data-id="${d.id}">` : ''}
<button class="delete-detail-btn" data-id="${d.id}" style="background:none; border:none; color:var(--color-text-danger); cursor:pointer;">&times;</button>
</div>
</div>
`;
}).join('');

// Attach listeners
subjectDetailsList.querySelectorAll('.toggle-detail-completion').forEach(cb => {
cb.addEventListener('change', async (e) => {
await store.updateSubjectDetail(e.target.dataset.id, { is_completed: e.target.checked ? 1 : 0 });
renderSubjectDetails();
});
});

subjectDetailsList.querySelectorAll('.delete-detail-btn').forEach(btn => {
btn.addEventListener('click', async (e) => {
if (confirm('Delete this item?')) {
await store.deleteSubjectDetail(e.target.dataset.id);
renderSubjectDetails();
}
});
});
}
}

function renderSidebarSubjects() {
const listEl = document.getElementById('subjects-sidebar-list');
if (!listEl) return;
Expand All @@ -131,9 +206,43 @@ function renderSidebarSubjects() {
const n = countBySubject[s.id] ?? 0;
const safeColor = s.color ? escapeHtml(s.color) : 'var(--color-text-info)';
return `<div class="nav-item subject-sidebar-item" data-subject-id="${escapeHtml(s.id)}">
<span class="nav-dot" style="background:${safeColor}"></span>${escapeHtml(s.name)}<span class="badge">${n}</span>
<span class="nav-dot" style="background:${safeColor}"></span>
<span class="subject-name" style="flex:1;">${escapeHtml(s.name)}</span>
<span class="badge">${n}</span>
<div class="subject-actions" style="display:none; margin-left:8px; gap:4px;">
<button class="edit-subject-btn" title="Edit" style="background:none; border:none; font-size:12px; cursor:pointer;">✏️</button>
<button class="delete-subject-btn" title="Delete" style="background:none; border:none; font-size:12px; cursor:pointer;">🗑️</button>
</div>
</div>`;
}).join('');

// Attach sidebar listeners
listEl.querySelectorAll('.subject-sidebar-item').forEach(el => {
const subjectId = el.dataset.subjectId;

el.addEventListener('mouseenter', () => {
el.querySelector('.subject-actions').style.display = 'flex';
});
el.addEventListener('mouseleave', () => {
el.querySelector('.subject-actions').style.display = 'none';
});

el.addEventListener('click', (e) => {
if (e.target.closest('.subject-actions')) return;
openSubjectDetails(subjectId);
});

el.querySelector('.edit-subject-btn').addEventListener('click', (e) => {
e.stopPropagation();
const subject = store.subjects.find(s => s.id === subjectId);
openNewSubjectModal(subject);
});

el.querySelector('.delete-subject-btn').addEventListener('click', (e) => {
e.stopPropagation();
store.deleteSubject(subjectId);
});
});
}

const newTaskModal = document.getElementById('new-task-modal');
Expand Down Expand Up @@ -897,11 +1006,38 @@ document.addEventListener('DOMContentLoaded', () => {

if (newSubjectSave) {
newSubjectSave.addEventListener('click', async () => {
const ok = await store.addSubject({ name: newSubjectName.value, color: selectedNewSubjectColor });
const payload = { name: newSubjectName.value, color: selectedNewSubjectColor };
let ok = false;
if (editingSubjectId) {
ok = await store.updateSubject(editingSubjectId, payload);
} else {
ok = await store.addSubject(payload);
}
if (ok && newSubjectModal) newSubjectModal.style.display = 'none';
});
}

if (subjectDetailsClose) {
subjectDetailsClose.addEventListener('click', () => {
subjectDetailsModal.style.display = 'none';
activeDetailSubjectId = null;
});
}

if (addDetailBtn) {
addDetailBtn.addEventListener('click', async () => {
const type = detailTypeSelect.value;
const content = detailContentInput.value.trim();
if (!content) return;

const ok = await store.addSubjectDetail(activeDetailSubjectId, { type, content });
if (ok) {
detailContentInput.value = '';
renderSubjectDetails();
}
});
}

if (newSubjectName) {
newSubjectName.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
Expand Down
97 changes: 95 additions & 2 deletions js/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,18 @@ export const store = {

async addSubject({ name, color }) {
const trimmed = String(name || '').trim();
if (!trimmed) {
alert('Please enter a subject name');
if (!trimmed || trimmed.length < 2) {
alert('Subject name must be at least 2 characters');
return false;
}

// Check for local duplicates before network request
const exists = this.subjects.some(s => s.name.toLowerCase() === trimmed.toLowerCase());
if (exists) {
alert('Subject already exists');
return false;
}

try {
const res = await fetch('/api/subjects', {
method: 'POST',
Expand All @@ -62,6 +70,91 @@ export const store = {
}
},

async updateSubject(id, { name, color }) {
try {
const res = await fetch(`/api/subjects/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, color })
});
if (!res.ok) throw new Error('Update failed');
const subsRes = await fetch('/api/subjects');
this.subjects = await subsRes.json();
this.notify();
return true;
} catch (e) {
console.error('Failed to update subject', e);
return false;
}
},

async deleteSubject(id) {
if (!confirm('Are you sure? This will delete all tasks and resources for this subject.')) return;
try {
const res = await fetch(`/api/subjects/${id}`, { method: 'DELETE' });
if (!res.ok) throw new Error('Delete failed');

const [subsRes, tasksRes] = await Promise.all([
fetch('/api/subjects'),
fetch('/api/tasks')
]);
this.subjects = await subsRes.json();
this.tasks = await tasksRes.json();
this.notify();
} catch (e) {
console.error('Failed to delete subject', e);
}
},

async fetchSubjectDetails(id) {
try {
const res = await fetch(`/api/subjects/${id}/details`);
return await res.json();
} catch (e) {
console.error('Failed to fetch subject details', e);
return [];
}
},

async addSubjectDetail(subjectId, { type, content }) {
try {
const res = await fetch(`/api/subjects/${subjectId}/details`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type, content })
});
if (!res.ok) throw new Error('Add detail failed');
return await res.json();
} catch (e) {
console.error('Failed to add subject detail', e);
return null;
}
},

async updateSubjectDetail(detailId, fields) {
try {
const res = await fetch(`/api/subjects/details/${detailId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(fields)
});
return res.ok;
} catch (e) {
console.error('Failed to update detail', e);
return false;
}
},

async deleteSubjectDetail(detailId) {
try {
const res = await fetch(`/api/subjects/details/${detailId}`, { method: 'DELETE' });
return res.ok;
} catch (e) {
console.error('Failed to delete detail', e);
return false;
}
},

// ================= UPDATED FUNCTION =================
async addTasks(newTasks) {
try {
Expand Down
Loading