Skip to content

Commit 6ef4c2e

Browse files
committed
chore: bump version to 0.3.6
1 parent 771b118 commit 6ef4c2e

File tree

5 files changed

+150
-31
lines changed

5 files changed

+150
-31
lines changed

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.3.5
1+
0.3.6

server.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -451,14 +451,28 @@ def get_library():
451451
@app.route('/api/library', methods=['POST'])
452452
def add_library_item():
453453
data = request.json
454+
name = data.get('name')
455+
category_id = data.get('categoryId')
456+
artist_id = data.get('artistId')
457+
454458
conn = get_db()
455459
cursor = conn.cursor()
456460

461+
# Check for duplicates (case-insensitive)
462+
cursor.execute('''
463+
SELECT id FROM library_items
464+
WHERE LOWER(name) = LOWER(?) AND category_id = ? AND (artist_id = ? OR (artist_id IS NULL AND ? IS NULL))
465+
''', (name, category_id, artist_id, artist_id))
466+
467+
if cursor.fetchone():
468+
conn.close()
469+
return jsonify({'error': 'An item with this name already exists in this category.'}), 409
470+
457471
item_id = generate_id()
458472
cursor.execute('''
459473
INSERT INTO library_items (id, name, category_id, artist_id, star_rating, notes, created_at)
460474
VALUES (?, ?, ?, ?, ?, ?, ?)
461-
''', (item_id, data.get('name'), data.get('categoryId'), data.get('artistId'),
475+
''', (item_id, name, category_id, artist_id,
462476
data.get('starRating', 0), data.get('notes', ''), datetime.now().isoformat()))
463477

464478
conn.commit()
@@ -472,6 +486,28 @@ def update_library_item(item_id):
472486
data = request.json
473487
conn = get_db()
474488
cursor = conn.cursor()
489+
490+
# Check for duplicates if name/category/artist are being changed
491+
if any(k in data for k in ['name', 'categoryId', 'artistId']):
492+
cursor.execute('SELECT name, category_id, artist_id FROM library_items WHERE id=?', (item_id,))
493+
current = cursor.fetchone()
494+
if not current:
495+
conn.close()
496+
return jsonify({'error': 'Item not found'}), 404
497+
498+
new_name = data.get('name', current[0])
499+
new_cat = data.get('categoryId', current[1])
500+
new_art = data.get('artistId', current[2]) if 'artistId' in data else current[2]
501+
502+
cursor.execute('''
503+
SELECT id FROM library_items
504+
WHERE LOWER(name) = LOWER(?) AND category_id = ? AND (artist_id = ? OR (artist_id IS NULL AND ? IS NULL))
505+
AND id != ?
506+
''', (new_name, new_cat, new_art, new_art, item_id))
507+
508+
if cursor.fetchone():
509+
conn.close()
510+
return jsonify({'error': 'An item with this name already exists in this category.'}), 409
475511

476512
# Map frontend camelCase to backend snake_case
477513
field_map = {

templates/library.html

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,8 @@ <h3 class="modal-title" id="item-modal-title">Add Library Item</h3>
357357
<label class="form-label" id="name-label">Name *</label>
358358
<input type="text" class="form-input" id="item-name" placeholder="Enter name...">
359359
<span class="form-error hidden" id="name-error">Name is required</span>
360+
<span class="form-error hidden" id="duplicate-error">This item already exists in the selected
361+
category</span>
360362
</div>
361363
<div class="form-group">
362364
<label class="form-label">Progress Rating</label>
@@ -737,11 +739,43 @@ <h4 style="font-size: 1rem; line-height: 1.2; white-space: nowrap; overflow: hid
737739

738740
async function saveItem() {
739741
const id = document.getElementById('edit-item-id').value;
740-
const name = document.getElementById('item-name').value.trim();
741-
if (!name) return;
742-
const data = { categoryId: document.getElementById('item-category').value, artistId: document.getElementById('item-artist').value || null, name, starRating: selectedRating };
743-
if (id) await FretLogData.updateLibraryItem(id, data); else await FretLogData.addLibraryItem(data);
744-
closeModal('item-modal'); filterItems(!!id);
742+
const nameInput = document.getElementById('item-name');
743+
const name = nameInput.value.trim();
744+
const nameError = document.getElementById('name-error');
745+
const dupError = document.getElementById('duplicate-error');
746+
747+
// Reset errors
748+
nameError.classList.add('hidden');
749+
dupError.classList.add('hidden');
750+
751+
if (!name) {
752+
nameError.classList.remove('hidden');
753+
return;
754+
}
755+
756+
const data = {
757+
categoryId: document.getElementById('item-category').value,
758+
artistId: document.getElementById('item-artist').value || null,
759+
name,
760+
starRating: selectedRating
761+
};
762+
763+
try {
764+
if (id) {
765+
await FretLogData.updateLibraryItem(id, data);
766+
} else {
767+
await FretLogData.addLibraryItem(data);
768+
}
769+
closeModal('item-modal');
770+
filterItems(!!id);
771+
} catch (error) {
772+
if (error.message.includes('409')) {
773+
dupError.classList.remove('hidden');
774+
} else {
775+
console.error('Failed to save library item:', error);
776+
alert('An error occurred while saving. Please try again.');
777+
}
778+
}
745779
}
746780

747781
function editItem(id) { const item = FretLogData.getLibraryItems().find(i => i.id === id); if (item) openItemModal(item); }

templates/sessions.html

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -388,9 +388,9 @@ <h3 class="modal-title">Delete Session?</h3>
388388
// Sessions Page Logic
389389
// ==========================================
390390

391-
let currentPage = 1;
391+
window.currentPage = 1;
392392
const itemsPerPage = 10;
393-
let filteredSessions = [];
393+
window.filteredSessions = [];
394394
let tempSessionItems = [];
395395
let viewingSessionId = null;
396396
let deletingSessionId = null;
@@ -477,6 +477,7 @@ <h3 class="modal-title">Delete Session?</h3>
477477

478478
// Filter and sort sessions
479479
function filterSessions() {
480+
window.currentPage = 1;
480481
let sessions = [...FretLogData.getSessions()];
481482

482483
// Add current session if valid
@@ -520,7 +521,7 @@ <h3 class="modal-title">Delete Session?</h3>
520521

521522
sessions.sort((a, b) => new Date(b.endTime || b.date) - new Date(a.endTime || a.date));
522523

523-
filteredSessions = sessions;
524+
window.filteredSessions = sessions;
524525
renderSessions();
525526
}
526527

@@ -530,7 +531,7 @@ <h3 class="modal-title">Delete Session?</h3>
530531
const emptyState = document.getElementById('no-sessions');
531532
const pagination = document.getElementById('pagination');
532533

533-
if (filteredSessions.length === 0) {
534+
if (window.filteredSessions.length === 0) {
534535
container.classList.add('hidden');
535536
emptyState.classList.remove('hidden');
536537
pagination.classList.add('hidden');
@@ -540,10 +541,10 @@ <h3 class="modal-title">Delete Session?</h3>
540541
container.classList.remove('hidden');
541542
emptyState.classList.add('hidden');
542543

543-
const totalPages = Math.ceil(filteredSessions.length / itemsPerPage);
544-
const start = (currentPage - 1) * itemsPerPage;
544+
const totalPages = Math.ceil(window.filteredSessions.length / itemsPerPage);
545+
const start = (window.currentPage - 1) * itemsPerPage;
545546
const end = start + itemsPerPage;
546-
const paginated = filteredSessions.slice(start, end);
547+
const paginated = window.filteredSessions.slice(start, end);
547548

548549
const instruments = FretLogData.getInstruments();
549550

@@ -611,11 +612,11 @@ <h3 class="modal-title">Delete Session?</h3>
611612
</div>`;
612613
}).join('');
613614

614-
if (filteredSessions.length > itemsPerPage) {
615+
if (window.filteredSessions.length > itemsPerPage) {
615616
pagination.classList.remove('hidden');
616-
document.getElementById('pagination-info').textContent = `Showing ${start + 1}-${Math.min(end, filteredSessions.length)} of ${filteredSessions.length}`;
617-
document.getElementById('prev-page').disabled = currentPage === 1;
618-
document.getElementById('next-page').disabled = currentPage === totalPages;
617+
document.getElementById('pagination-info').textContent = `Showing ${start + 1}-${Math.min(end, window.filteredSessions.length)} of ${window.filteredSessions.length}`;
618+
document.getElementById('prev-page').disabled = window.currentPage === 1;
619+
document.getElementById('next-page').disabled = window.currentPage === totalPages;
619620
} else {
620621
pagination.classList.add('hidden');
621622
}
@@ -746,6 +747,9 @@ <h3 class="modal-title">Delete Session?</h3>
746747
const existingIds = new Set(tempSessionItems.map(i => i.libraryItemId || i.library_item_id));
747748
items = items.filter(i => !existingIds.has(i.id));
748749

750+
// Sort items alphabetically
751+
items.sort((a, b) => a.name.localeCompare(b.name));
752+
749753
document.getElementById('item-select').innerHTML = items.length ?
750754
'<option value="">Select...</option>' + items.map(i => `<option value="${i.id}">${i.name}</option>`).join('') :
751755
'<option value="">No items</option>';
@@ -809,6 +813,24 @@ <h3 class="modal-title">Delete Session?</h3>
809813
await initPromise;
810814
await render();
811815

816+
// Pagination listeners
817+
document.getElementById('prev-page').addEventListener('click', () => {
818+
if (window.currentPage > 1) {
819+
window.currentPage--;
820+
renderSessions();
821+
window.scrollTo({ top: 0, behavior: 'smooth' });
822+
}
823+
});
824+
825+
document.getElementById('next-page').addEventListener('click', () => {
826+
const totalPages = Math.ceil(window.filteredSessions.length / itemsPerPage);
827+
if (window.currentPage < totalPages) {
828+
window.currentPage++;
829+
renderSessions();
830+
window.scrollTo({ top: 0, behavior: 'smooth' });
831+
}
832+
});
833+
812834
flatpickr('#filter-date-start', { altInput: true, altFormat: 'M j, Y', onChange: filterSessions });
813835
flatpickr('#filter-date-end', { altInput: true, altFormat: 'M j, Y', onChange: filterSessions });
814836
flatpickr('#session-date', { altInput: true, altFormat: 'M j, Y' });

templates/statistics.html

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -272,28 +272,42 @@ <h3 class="card-title mb-md">Top Practiced Items</h3>
272272
const dailyTotals = FretLogData.getDailyTotals();
273273

274274
if (period === 'week') {
275-
const weekStart = new Date(now); weekStart.setDate(now.getDate() - (now.getDay() === 0 ? 6 : now.getDay() - 1));
275+
const weekStart = new Date(now);
276+
weekStart.setDate(now.getDate() - (now.getDay() === 0 ? 6 : now.getDay() - 1));
276277
weekStart.setHours(0, 0, 0, 0);
277278
for (let i = 0; i < 7; i++) {
278279
const d = new Date(weekStart); d.setDate(weekStart.getDate() + i);
279280
labels.push(d.toLocaleDateString('en-US', { weekday: 'short' }));
280281
data.push((dailyTotals[FretLogData.formatDateKey(d)] || 0) / 60000);
281282
}
282283
} else if (period === 'month') {
284+
const baseDate = new Date(now);
285+
baseDate.setHours(12, 0, 0, 0); // Mid-day for safe date math
283286
for (let i = 29; i >= 0; i--) {
284-
const d = new Date(now); d.setDate(now.getDate() - i);
285-
labels.push(d.getDate().toString());
287+
const d = new Date(baseDate);
288+
d.setDate(baseDate.getDate() - i);
289+
const dayNum = d.getDate();
290+
const monthName = d.toLocaleDateString('en-US', { month: 'short' });
291+
// Label with month name on the 1st day or the first day of the window
292+
labels.push(dayNum === 1 || i === 29 ? `${monthName} ${dayNum}` : dayNum.toString());
286293
data.push((dailyTotals[FretLogData.formatDateKey(d)] || 0) / 60000);
287294
}
288295
} else if (period === 'year') {
289-
for (let i = 51; i >= 0; i--) {
290-
const ws = new Date(now); ws.setDate(now.getDate() - (i * 7));
291-
let total = 0;
292-
for (let j = 0; j < 7; j++) {
293-
const d = new Date(ws); d.setDate(ws.getDate() + j);
294-
total += dailyTotals[FretLogData.formatDateKey(d)] || 0;
296+
// Last 12 months
297+
for (let i = 11; i >= 0; i--) {
298+
const d = new Date(now.getFullYear(), now.getMonth() - i, 1);
299+
const monthKey = d.toLocaleDateString('en-US', { month: 'short' });
300+
labels.push(monthKey);
301+
302+
let monthTotal = 0;
303+
const m = d.getMonth();
304+
const y = d.getFullYear();
305+
const lastDay = new Date(y, m + 1, 0).getDate();
306+
for (let day = 1; day <= lastDay; day++) {
307+
const check = new Date(y, m, day);
308+
monthTotal += dailyTotals[FretLogData.formatDateKey(check)] || 0;
295309
}
296-
labels.push(`W${52 - i}`); data.push(total / 60000);
310+
data.push(monthTotal / 60000);
297311
}
298312
}
299313

@@ -310,7 +324,17 @@ <h3 class="card-title mb-md">Top Practiced Items</h3>
310324
plugins: { legend: { display: false }, tooltip: { callbacks: { label: c => `${Math.round(c.raw)}m` } } },
311325
scales: {
312326
y: { beginAtZero: true, grid: { color: isDark ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)' }, ticks: { color: isDark ? '#9CA3AF' : '#4A4A4A' } },
313-
x: { grid: { display: false }, ticks: { color: isDark ? '#9CA3AF' : '#4A4A4A', autoSkip: true, maxTicksLimit: period === 'year' ? 12 : 10 } }
327+
x: {
328+
grid: { display: false },
329+
ticks: {
330+
color: isDark ? '#9CA3AF' : '#4A4A4A',
331+
autoSkip: true,
332+
maxTicksLimit: period === 'month' ? 15 : 12,
333+
align: 'center',
334+
maxRotation: 0,
335+
font: { size: 10 }
336+
}
337+
}
314338
}
315339
}
316340
});
@@ -352,10 +376,13 @@ <h3 class="card-title mb-md">Top Practiced Items</h3>
352376
}
353377

354378
labelsContainer.innerHTML = '<span>M</span><span>T</span><span>W</span><span>T</span><span>F</span><span>S</span><span>S</span>';
355-
let monthsH = '<div class="heatmap-months-inner">';
379+
let monthsH = '<div class="heatmap-months-inner" style="display: flex; gap: 2px;">';
356380
monthPos.forEach((p, i) => {
357381
const span = monthPos[i + 1] ? monthPos[i + 1].idx - p.idx : weeks.length - p.idx;
358-
monthsH += `<span style="flex:${span}">${p.name}</span>`;
382+
// Column width is 12px + 2px gap.
383+
// Width should be (span * 14) - 2 (to account for the gap after the last column in span)
384+
const width = (span * 14) - 2;
385+
monthsH += `<span style="width:${width}px; flex: none; text-align: left;">${p.name}</span>`;
359386
});
360387
monthsContainer.innerHTML = monthsH + '</div>';
361388

0 commit comments

Comments
 (0)