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
62 changes: 62 additions & 0 deletions 55-productivity-dashboard/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Productivity Dashboard</title>
<!-- Bootstrap Icons -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<link rel="stylesheet" href="style.css">
</head>
<body>

<div class="dashboard-container">
<!-- Left Panel: Timer & Notes -->
<div class="panel-left">
<!-- Timer Card -->
<div class="card p-3 mb-3 text-center">
<div class="card-title text-muted text-uppercase small font-weight-bold">Focus Timer</div>
<div class="timer-display" id="timer">25:00</div>
<div class="d-flex justify-content-center gap-2">
<button class="btn btn-primary btn-sm" id="startBtn">
<i class="bi bi-play-fill"></i> Start
</button>
<button class="btn btn-outline-secondary btn-sm" id="resetBtn">
<i class="bi bi-arrow-counterclockwise"></i> Reset
</button>
</div>
</div>

<!-- Quick Note Card -->
<div class="card p-3 flex-grow-1 d-flex flex-column">
<div class="card-title text-muted text-uppercase small font-weight-bold mb-2">Quick Notes</div>
<textarea class="form-control text-textarea" id="quickNote" placeholder="Type your thoughts here..."></textarea>
</div>
</div>

<!-- Right Panel: Task List -->
<div class="panel-right card p-3 d-flex flex-column">
<div class="card-title text-muted text-uppercase small font-weight-bold mb-3">Today's Tasks</div>

<!-- Add Task Form -->
<div class="input-group mb-3">
<input type="text" class="form-control" id="taskInput" placeholder="Add a new task...">
<button class="btn btn-primary" id="addTaskBtn">
<i class="bi bi-plus-lg"></i>
</button>
</div>

<!-- Task List (Max 4 items) -->
<ul class="task-list" id="taskList">
<!-- Dynamically filled by JS -->
</ul>

<div class="text-muted small text-center mt-auto">
Maximum 4 active tasks allowed to fit the screen.
</div>
</div>
</div>

<script src="script.js"></script>
</body>
</html>
98 changes: 98 additions & 0 deletions 55-productivity-dashboard/script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// --- TIMER LOGIC ---
let timeLeft = 25 * 60;
let timerId = null;
const timerDisplay = document.getElementById('timer');
const startBtn = document.getElementById('startBtn');
const resetBtn = document.getElementById('resetBtn');

function updateTimerDisplay() {
let minutes = Math.floor(timeLeft / 60);
let seconds = timeLeft % 60;
timerDisplay.textContent = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
}

startBtn.addEventListener('click', () => {
if (timerId === null) {
timerId = setInterval(() => {
if (timeLeft > 0) {
timeLeft--;
updateTimerDisplay();
} else {
clearInterval(timerId);
timerId = null;
startBtn.innerHTML = '<i class="bi bi-play-fill"></i> Start';
alert('Focus time is up! Take a short break.');
timeLeft = 25 * 60;
updateTimerDisplay();
}
}, 1000);
startBtn.innerHTML = '<i class="bi bi-pause-fill"></i> Pause';
} else {
clearInterval(timerId);
timerId = null;
startBtn.innerHTML = '<i class="bi bi-play-fill"></i> Resume';
}
});

resetBtn.addEventListener('click', () => {
clearInterval(timerId);
timerId = null;
timeLeft = 25 * 60;
updateTimerDisplay();
startBtn.innerHTML = '<i class="bi bi-play-fill"></i> Start';
});


// --- TASK LIST LOGIC ---
const taskInput = document.getElementById('taskInput');
const addTaskBtn = document.getElementById('addTaskBtn');
const taskList = document.getElementById('taskList');

addTaskBtn.addEventListener('click', () => {
const taskText = taskInput.value.trim();

// Prevent scrolling by limiting to 4 items max
if (taskList.children.length >= 4) {
alert("Maximum 4 tasks allowed to keep the interface compact!");
return;
}

if (taskText !== "") {
const li = document.createElement('li');
li.className = 'task-item';

li.innerHTML = `
<span>${taskText}</span>
<button class="delete-task-btn"><i class="bi bi-trash"></i></button>
`;
Comment on lines +60 to +67

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Empty-input path hits the max-task alert unnecessarily.

The 4-item cap check (Lines 55-58) runs before the empty-string check (Line 60). If the list already has 4 tasks and the user presses Enter/Add with an empty input, they get a spurious "Maximum 4 tasks allowed" alert instead of a silent no-op.

🐛 Proposed fix
 addTaskBtn.addEventListener('click', () => {
     const taskText = taskInput.value.trim();
-    
-    // Prevent scrolling by limiting to 4 items max
-    if (taskList.children.length >= 4) {
-        alert("Maximum 4 tasks allowed to keep the interface compact!");
-        return;
-    }
-
-    if (taskText !== "") {
+    if (taskText === "") return;
+
+    // Prevent scrolling by limiting to 4 items max
+    if (taskList.children.length >= 4) {
+        alert("Maximum 4 tasks allowed to keep the interface compact!");
+        return;
+    }
+
+    {
         const li = document.createElement('li');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (taskText !== "") {
const li = document.createElement('li');
li.className = 'task-item';
li.innerHTML = `
<span>${taskText}</span>
<button class="delete-task-btn"><i class="bi bi-trash"></i></button>
`;
addTaskBtn.addEventListener('click', () => {
const taskText = taskInput.value.trim();
if (taskText === "") return;
// Prevent scrolling by limiting to 4 items max
if (taskList.children.length >= 4) {
alert("Maximum 4 tasks allowed to keep the interface compact!");
return;
}
{
const li = document.createElement('li');
li.className = 'task-item';
li.innerHTML = `
<span>${taskText}</span>
<button class="delete-task-btn"><i class="bi bi-trash"></i></button>
`;
🧰 Tools
🪛 ast-grep (0.44.0)

[warning] 63-66: Avoid assigning untrusted data to innerHTML/outerHTML or document.write
Context: li.innerHTML = <span>${taskText}</span> <button class="delete-task-btn"><i class="bi bi-trash"></i></button>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').

(inner-outer-html)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@55-productivity-dashboard/script.js` around lines 60 - 67, The add-task flow
in script.js checks the 4-task limit before validating empty input, so an empty
submission can trigger the maximum-tasks alert. In the task creation handler
around the taskText check and the max-item guard, move the empty-string
validation ahead of the cap check so blank input becomes a silent no-op. Keep
the logic in the same add-task path where li is created and appended, but ensure
the alert only fires for non-empty submissions that actually exceed the limit.

Comment on lines +61 to +67

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Untrusted input written via innerHTML (CWE-79).

taskText (from Line 52, user's own taskInput) is interpolated directly into li.innerHTML. Flagged by static analysis for CWE-79 improper neutralization. Even on a single-user local page, building markup via textContent/createElement avoids the anti-pattern entirely.

🛡️ Proposed fix
-        li.innerHTML = `
-            <span>${taskText}</span>
-            <button class="delete-task-btn"><i class="bi bi-trash"></i></button>
-        `;
+        const span = document.createElement('span');
+        span.textContent = taskText;
+        const delBtn = document.createElement('button');
+        delBtn.className = 'delete-task-btn';
+        delBtn.innerHTML = '<i class="bi bi-trash"></i>';
+        li.append(span, delBtn);

Then update the selectors to use span/delBtn directly instead of li.querySelector(...).

🧰 Tools
🪛 ast-grep (0.44.0)

[warning] 63-66: Avoid assigning untrusted data to innerHTML/outerHTML or document.write
Context: li.innerHTML = <span>${taskText}</span> <button class="delete-task-btn"><i class="bi bi-trash"></i></button>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').

(inner-outer-html)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@55-productivity-dashboard/script.js` around lines 61 - 67, The task item
markup in the task list rendering uses untrusted taskText inside innerHTML,
which should be replaced with DOM construction to avoid XSS-style injection.
Update the task creation flow in the function that builds each li so the text is
assigned via textContent on a span element instead of interpolation, then create
the delete button and icon with createElement, and wire the existing delete
handler directly to that button rather than using li.querySelector for lookups.

Source: Linters/SAST tools


// Toggle completed status on click
li.querySelector('span').addEventListener('click', function() {
li.classList.toggle('completed');
});

// Delete button logic
li.querySelector('.delete-task-btn').addEventListener('click', function() {
li.remove();
});

taskList.appendChild(li);
taskInput.value = "";
}
});

taskInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') addTaskBtn.click();
});


// --- LOCALSTORAGE (Saves notes automatically) ---
const quickNote = document.getElementById('quickNote');

if(localStorage.getItem('quick_dashboard_note')) {
quickNote.value = localStorage.getItem('quick_dashboard_note');
}

quickNote.addEventListener('input', () => {
localStorage.setItem('quick_dashboard_note', quickNote.value);
});
201 changes: 201 additions & 0 deletions 55-productivity-dashboard/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
/* Bootstrap-style minimalist Reset & Variables */
:root {
--bs-primary: #0d6efd;
--bs-primary-hover: #0b5ed7;
--bs-body-bg: #f8f9fa;
--bs-card-bg: #ffffff;
--bs-border-color: #dee2e6;
--bs-text-dark: #212529;
--bs-text-muted: #6c757d;
}

* {
box-sizing: border-box;
margin: 0;
padding: 0;
}

body {
background-color: var(--bs-body-bg);
color: var(--bs-text-dark);
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden; /* Strictly no scroll */
padding: 20px;
}

/* Dashboard Grid */
.dashboard-container {
display: flex;
gap: 20px;
width: 680px;
height: 380px; /* Fixed height to fit everything in one view */
}

.panel-left {
flex: 1;
display: flex;
flex-direction: column;
height: 100%;
}

.panel-right {
flex: 1.2;
height: 100%;
}

/* Card Styling */
.card {
background-color: var(--bs-card-bg);
border: 1px solid var(--bs-border-color);
border-radius: 0.375rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
}

.p-3 { padding: 1rem; }
.mb-2 { margin-bottom: 0.5rem; }
.mb-3 { margin-bottom: 1rem; }
.mt-auto { margin-top: auto; }
.text-center { text-align: center; }
.text-muted { color: var(--bs-text-muted) !important; }
.text-uppercase { text-transform: uppercase; }
.small { font-size: 0.875rem; }
.font-weight-bold { font-weight: 700; }
.d-flex { display: flex; }
.flex-column { flex-direction: column; }
.flex-grow-1 { flex-grow: 1; }
.gap-2 { gap: 0.5rem; }
Comment on lines +58 to +70

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

.justify-content-center is used in the markup but never defined here.

index.html line 20 applies d-flex justify-content-center gap-2 to center the Start/Reset buttons. Since .d-flex sets display:flex, the parent's text-align:center (from .text-center on the card) has no effect on flex-item positioning — without a .justify-content-center rule, those buttons will render left-aligned instead of centered as intended.

🐛 Proposed fix
 .gap-2 { gap: 0.5rem; }
+.justify-content-center { justify-content: center; }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.p-3 { padding: 1rem; }
.mb-2 { margin-bottom: 0.5rem; }
.mb-3 { margin-bottom: 1rem; }
.mt-auto { margin-top: auto; }
.text-center { text-align: center; }
.text-muted { color: var(--bs-text-muted) !important; }
.text-uppercase { text-transform: uppercase; }
.small { font-size: 0.875rem; }
.font-weight-bold { font-weight: 700; }
.d-flex { display: flex; }
.flex-column { flex-direction: column; }
.flex-grow-1 { flex-grow: 1; }
.gap-2 { gap: 0.5rem; }
.p-3 { padding: 1rem; }
.mb-2 { margin-bottom: 0.5rem; }
.mb-3 { margin-bottom: 1rem; }
.mt-auto { margin-top: auto; }
.text-center { text-align: center; }
.text-muted { color: var(--bs-text-muted) !important; }
.text-uppercase { text-transform: uppercase; }
.small { font-size: 0.875rem; }
.font-weight-bold { font-weight: 700; }
.d-flex { display: flex; }
.flex-column { flex-direction: column; }
.flex-grow-1 { flex-grow: 1; }
.gap-2 { gap: 0.5rem; }
.justify-content-center { justify-content: center; }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@55-productivity-dashboard/style.css` around lines 58 - 70, The utility class
set in style.css is missing the flex centering helper used by the button
container, so the Start/Reset buttons stay left-aligned. Add a
.justify-content-center rule alongside the existing flex utilities in style.css
so the markup using d-flex justify-content-center gap-2 can center its children
correctly.


/* Timer Display */
.timer-display {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 0.5rem;
color: var(--bs-text-dark);
font-variant-numeric: tabular-nums;
}

/* Buttons */
.btn {
display: inline-block;
font-weight: 400;
line-height: 1.5;
text-align: center;
text-decoration: none;
vertical-align: middle;
cursor: pointer;
user-select: none;
border: 1px solid transparent;
padding: 0.375rem 0.75rem;
font-size: 1rem;
border-radius: 0.375rem;
transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out;
}

.btn-sm {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
border-radius: 0.25rem;
}

.btn-primary {
color: #fff;
background-color: var(--bs-primary);
border-color: var(--bs-primary);
}

.btn-primary:hover {
background-color: var(--bs-primary-hover);
border-color: var(--bs-primary-hover);
}

.btn-outline-secondary {
color: var(--bs-text-muted);
border-color: var(--bs-border-color);
background-color: transparent;
}

.btn-outline-secondary:hover {
background-color: #e9ecef;
}

/* Inputs */
.input-group {
display: flex;
width: 100%;
}

.input-group .form-control {
flex: 1;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}

.input-group .btn {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}

.form-control {
display: block;
width: 100%;
padding: 0.375rem 0.75rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: var(--bs-text-dark);
background-color: #fff;
background-clip: padding-box;
border: 1px solid var(--bs-border-color);
border-radius: 0.375rem;
outline: none;
}

.form-control:focus {
border-color: #86b7fe;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, .25);
}

.text-textarea {
resize: none;
flex-grow: 1;
font-size: 0.9rem;
}

/* Task List */
.task-list {
list-style: none;
display: flex;
flex-direction: column;
gap: 8px;
}

.task-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background-color: #f8f9fa;
border: 1px solid var(--bs-border-color);
border-radius: 0.25rem;
font-size: 0.9rem;
}

.task-item.completed span {
text-decoration: line-through;
color: var(--bs-text-muted);
}

.delete-task-btn {
color: #dc3545;
cursor: pointer;
background: transparent;
border: none;
}

.delete-task-btn:hover {
color: #a71d2a;
}