Online Work Session Focus Enhancer
Use a structured timer and ambient sounds to boost your concentration.
25:00
Time to Focus
Daily Goal Progress
0 / 4 Sessions Completed
Your Focus Statistics
Focus Hours This Week
Session Log
Configure Your Focus Sessions
Timer Durations (minutes)
Goals & Sounds
No sessions logged yet.
`; return; } container.innerHTML = sessionLogs.map(log => { const d = new Date(log.date); return `
${d.toLocaleDateString()} ${d.toLocaleTimeString()} - ${log.duration} min focus
`;
}).join('');
}
// --- CHART LOGIC ---
function updateWeeklyChart() {
const ctx = document.getElementById('weeklyFocusChart')?.getContext('2d');
if (!ctx) return;
const weeklyData = [0, 0, 0, 0, 0, 0, 0]; // Sun - Sat
const today = new Date();
const firstDayOfWeek = new Date(today.setDate(today.getDate() - today.getDay()));
sessionLogs.forEach(log => {
const logDate = new Date(log.date);
const diffDays = Math.floor((logDate - firstDayOfWeek) / (1000 * 60 * 60 * 24));
if (diffDays >= 0 && diffDays < 7) {
weeklyData[logDate.getDay()] += log.duration / 60; // in hours
}
});
if (weeklyFocusChartInstance) weeklyFocusChartInstance.destroy();
weeklyFocusChartInstance = new Chart(ctx, {
type: 'bar',
data: {
labels: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
datasets: [{
label: 'Focus Hours',
data: weeklyData,
backgroundColor: '#14b8a6',
borderRadius: 4
}]
},
options: {
responsive: true,
plugins: { legend: { display: false } },
scales: { y: { beginAtZero: true, title: { display: true, text: 'Hours' } } }
}
});
}
// --- SOUND GENERATION (WEB AUDIO API) ---
function initAudio() {
if (!audioCtx) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
}
function playNotificationSound() {
initAudio();
const oscillator = audioCtx.createOscillator();
const gainNode = audioCtx.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioCtx.destination);
oscillator.type = 'sine';
oscillator.frequency.setValueAtTime(880, audioCtx.currentTime);
gainNode.gain.setValueAtTime(0.2, audioCtx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.0001, audioCtx.currentTime + 0.5);
oscillator.start();
oscillator.stop(audioCtx.currentTime + 0.5);
}
function startAmbientSound() {
const soundType = ambientSoundSelect.value;
if (soundType === 'none' || soundNode) return;
initAudio();
if (soundType === 'whiteNoise') {
const bufferSize = 2 * audioCtx.sampleRate;
const noiseBuffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
const output = noiseBuffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
output[i] = Math.random() * 2 - 1;
}
soundNode = audioCtx.createBufferSource();
soundNode.buffer = noiseBuffer;
soundNode.loop = true;
} else if (soundType === 'rain') {
// Simplified rain sound - more complex synthesis is possible
const bufferSize = audioCtx.sampleRate * 2;
const noiseBuffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
const output = noiseBuffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
output[i] = Math.random() * 2 - 1;
}
const source = audioCtx.createBufferSource();
source.buffer = noiseBuffer;
source.loop = true;
const filter = audioCtx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.value = 800;
source.connect(filter);
soundNode = filter; // The filter is now the node to connect
source.start(0);
}
const gainNode = audioCtx.createGain();
gainNode.gain.setValueAtTime(0.05, audioCtx.currentTime);
soundNode.connect(gainNode).connect(audioCtx.destination);
if (soundNode.start) soundNode.start(0);
}
function stopAmbientSound() {
if (soundNode) {
if (soundNode.stop) soundNode.stop(0);
soundNode.disconnect();
soundNode = null;
}
}
// --- PDF GENERATION ---
async function generatePdf() {
const { jsPDF } = window.jspdf;
const doc = new jsPDF({ orientation: 'portrait', unit: 'pt', format: 'a4' });
doc.setFont("helvetica", "bold");
doc.setFontSize(22);
doc.setTextColor("#0f766e");
doc.text("Focus Session Report", doc.internal.pageSize.width / 2, 60, { align: 'center' });
const totalHours = sessionLogs.reduce((sum, log) => sum + log.duration, 0) / 60;
doc.setFont("helvetica", "normal");
doc.setFontSize(12);
doc.setTextColor("#111827");
doc.text(`Total Focus Time Logged: ${totalHours.toFixed(2)} hours`, 40, 100);
const chartCanvas = document.getElementById('weeklyFocusChart');
const chartImg = chartCanvas.toDataURL('image/png', 1.0);
doc.setFontSize(16);
doc.text("Weekly Focus Breakdown", 40, 140);
doc.addImage(chartImg, 'PNG', 40, 160, 500, 250);
const tableData = sessionLogs.map(log => {
const d = new Date(log.date);
return [
d.toLocaleDateString(),
d.toLocaleTimeString(),
`${log.duration} minutes`
];
});
doc.autoTable({
startY: 440,
head: [['Date', 'Time', 'Duration']],
body: tableData,
theme: 'grid',
headStyles: { fillColor: '#0d9488' }
});
doc.save(`Focus-Report_${new Date().toISOString().split('T')[0]}.pdf`);
}
// --- TAB NAVIGATION LOGIC ---
function switchTab(tabIndex) {
currentTabIndex = tabIndex;
tabsContainer.querySelectorAll('button').forEach((btn, index) => {
btn.classList.toggle('tab-active', index === tabIndex);
btn.classList.toggle('tab-inactive', index !== tabIndex);
});
tabContents.forEach((content, index) => {
content.classList.toggle('hidden', index !== tabIndex);
});
updateButtonStates();
// Refresh UI elements on tab switch
if (tabs[tabIndex] === 'sessionLog') {
renderSessionLog();
updateWeeklyChart();
}
}
function updateButtonStates() {
prevBtn.disabled = currentTabIndex === 0;
nextBtn.disabled = currentTabIndex === tabs.length - 1;
prevBtn.classList.toggle('opacity-50', prevBtn.disabled);
nextBtn.classList.toggle('opacity-50', nextBtn.disabled);
}
// --- EVENT LISTENERS ---
startPauseBtn.addEventListener('click', toggleTimer);
resetBtn.addEventListener('click', () => resetTimer());
saveSettingsBtn.addEventListener('click', saveSettings);
downloadPdfBtn.addEventListener('click', generatePdf);
ambientSoundSelect.addEventListener('change', () => {
stopAmbientSound();
if (state.isRunning) startAmbientSound();
});
tabsContainer.querySelectorAll('button').forEach((btn, index) => {
btn.addEventListener('click', () => switchTab(index));
});
prevBtn.addEventListener('click', () => {
if (currentTabIndex > 0) switchTab(currentTabIndex - 1);
});
nextBtn.addEventListener('click', () => {
if (currentTabIndex < tabs.length - 1) switchTab(currentTabIndex + 1);
});
// --- RUN INITIALIZATION ---
initialize();
});
