Add task date and reminder feature
This commit is contained in:
156
src/App.css
156
src/App.css
@@ -1,7 +1,7 @@
|
|||||||
.todo-container {
|
.todo-container {
|
||||||
max-width: 540px;
|
max-width: 560px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 60px auto;
|
margin: 50px auto;
|
||||||
padding: 32px;
|
padding: 32px;
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
@@ -21,88 +21,178 @@
|
|||||||
|
|
||||||
.input-section {
|
.input-section {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
flex-direction: column;
|
||||||
margin-bottom: 24px;
|
gap: 16px;
|
||||||
|
margin-bottom: 28px;
|
||||||
|
background: var(--code-bg);
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-section input {
|
.task-input-wrapper {
|
||||||
flex: 1;
|
width: 100%;
|
||||||
padding: 12px 16px;
|
}
|
||||||
|
|
||||||
|
.task-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px 16px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: var(--code-bg);
|
background: var(--bg);
|
||||||
color: var(--text-h);
|
color: var(--text-h);
|
||||||
outline: none;
|
outline: none;
|
||||||
|
box-sizing: border-box;
|
||||||
transition: border-color 0.2s, box-shadow 0.2s;
|
transition: border-color 0.2s, box-shadow 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-section input:focus {
|
.task-input:focus,
|
||||||
|
.reminder-input:focus {
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
box-shadow: 0 0 0 3px var(--accent-bg);
|
box-shadow: 0 0 0 3px var(--accent-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-section button {
|
.task-input.input-error {
|
||||||
padding: 12px 20px;
|
border-color: #ef4444;
|
||||||
font-size: 16px;
|
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reminder-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reminder-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-h);
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-icon {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reminder-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reminder-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px 14px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: inherit;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text-h);
|
||||||
|
outline: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: border-color 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-btn {
|
||||||
|
padding: 12px 24px;
|
||||||
|
font-size: 15px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
transition: opacity 0.2s, transform 0.1s;
|
transition: opacity 0.2s, transform 0.1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-section button:hover {
|
.add-btn:hover {
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-section button:active {
|
.add-btn:active {
|
||||||
transform: scale(0.98);
|
transform: scale(0.98);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
margin: 0;
|
||||||
|
color: #ef4444;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
.todo-container ul {
|
.todo-container ul {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.todo-container li {
|
.todo-container li {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 12px 16px;
|
padding: 14px 16px;
|
||||||
background: var(--code-bg);
|
background: var(--code-bg);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 8px;
|
border-radius: 10px;
|
||||||
transition: background-color 0.2s, border-color 0.2s;
|
transition: background-color 0.2s, border-color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.todo-container li span {
|
.task-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--text-h);
|
|
||||||
font-size: 16px;
|
|
||||||
user-select: none;
|
user-select: none;
|
||||||
transition: color 0.2s, text-decoration 0.2s;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.todo-container li span:hover {
|
.task-text {
|
||||||
|
color: var(--text-h);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
word-break: break-word;
|
||||||
|
transition: color 0.2s, text-decoration 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-content:hover .task-text {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.todo-container li.completed span {
|
.task-timestamps {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text);
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-created,
|
||||||
|
.task-reminder {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-container li.completed .task-text {
|
||||||
text-decoration: line-through;
|
text-decoration: line-through;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.todo-container li button {
|
.todo-container li.completed .task-timestamps {
|
||||||
|
text-decoration: line-through;
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn {
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@@ -114,8 +204,22 @@
|
|||||||
transition: background-color 0.2s, color 0.2s, border-color 0.2s;
|
transition: background-color 0.2s, color 0.2s, border-color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.todo-container li button:hover {
|
.delete-btn:hover {
|
||||||
background: #ef4444;
|
background: #ef4444;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
border-color: #ef4444;
|
border-color: #ef4444;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 520px) {
|
||||||
|
.reminder-controls {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
.add-btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.task-timestamps {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
169
src/App.jsx
169
src/App.jsx
@@ -1,56 +1,179 @@
|
|||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [task, setTask] = useState("");
|
const [task, setTask] = useState("");
|
||||||
|
const [reminder, setReminder] = useState("");
|
||||||
const [tasks, setTasks] = useState([]);
|
const [tasks, setTasks] = useState([]);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
const addTask = () => {
|
const addTask = async () => {
|
||||||
if (task.trim() === "") return;
|
if (task.trim() === "") {
|
||||||
|
setError("Please enter a task name.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let reminderIso = null;
|
||||||
|
if (reminder) {
|
||||||
|
const selectedDate = new Date(reminder);
|
||||||
|
if (!isNaN(selectedDate.getTime())) {
|
||||||
|
reminderIso = selectedDate.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Requirement: Ask for notification permission only when setting a reminder
|
||||||
|
if ("Notification" in window && Notification.permission === "default") {
|
||||||
|
try {
|
||||||
|
await Notification.requestPermission();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Notification permission error:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const newTask = {
|
const newTask = {
|
||||||
text: task,
|
id: Date.now() + Math.random(),
|
||||||
|
text: task.trim(),
|
||||||
completed: false,
|
completed: false,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
reminder: reminderIso,
|
||||||
|
reminderFired: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
setTasks([...tasks, newTask]);
|
setTasks((prev) => [...prev, newTask]);
|
||||||
setTask("");
|
setTask("");
|
||||||
|
setReminder("");
|
||||||
|
setError("");
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteTask = (index) => {
|
const deleteTask = (id) => {
|
||||||
setTasks(tasks.filter((_, i) => i !== index));
|
setTasks((prev) => prev.filter((item) => item.id !== id));
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleTask = (index) => {
|
const toggleTask = (id) => {
|
||||||
setTasks(
|
setTasks((prev) =>
|
||||||
tasks.map((item, i) =>
|
prev.map((item) =>
|
||||||
i === index ? { ...item, completed: !item.completed } : item
|
item.id === id ? { ...item, completed: !item.completed } : item
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
const now = new Date().getTime();
|
||||||
|
|
||||||
|
setTasks((prevTasks) => {
|
||||||
|
let updated = false;
|
||||||
|
const nextTasks = prevTasks.map((item) => {
|
||||||
|
if (
|
||||||
|
item.reminder &&
|
||||||
|
!item.reminderFired &&
|
||||||
|
!item.completed &&
|
||||||
|
new Date(item.reminder).getTime() <= now
|
||||||
|
) {
|
||||||
|
updated = true;
|
||||||
|
|
||||||
|
if ("Notification" in window && Notification.permission === "granted") {
|
||||||
|
try {
|
||||||
|
new Notification("Task Reminder", {
|
||||||
|
body: `Reminder: ${item.text}`,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Notification failed:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
alert(`Reminder: ${item.text}`);
|
||||||
|
|
||||||
|
return { ...item, reminderFired: true };
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
|
||||||
|
return updated ? nextTasks : prevTasks;
|
||||||
|
});
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const formatDate = (isoString) => {
|
||||||
|
if (!isoString) return "";
|
||||||
|
const date = new Date(isoString);
|
||||||
|
if (isNaN(date.getTime())) return isoString;
|
||||||
|
|
||||||
|
const day = date.getDate();
|
||||||
|
const month = date.toLocaleString("en-US", { month: "short" });
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const time = date.toLocaleString("en-US", {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
hour12: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return `${day} ${month} ${year}, ${time}`;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="todo-container">
|
<div className="todo-container">
|
||||||
<h1>To-Do List</h1>
|
<h1>To-Do List</h1>
|
||||||
|
|
||||||
<div className="input-section">
|
<div className="input-section">
|
||||||
<input
|
<div className="task-input-wrapper">
|
||||||
type="text"
|
<input
|
||||||
placeholder="Enter a task..."
|
type="text"
|
||||||
value={task}
|
className={`task-input ${error ? "input-error" : ""}`}
|
||||||
onChange={(e) => setTask(e.target.value)}
|
placeholder="Enter a task..."
|
||||||
onKeyDown={(e) => e.key === "Enter" && addTask()}
|
value={task}
|
||||||
/>
|
onChange={(e) => {
|
||||||
|
setTask(e.target.value);
|
||||||
|
if (error) setError("");
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => e.key === "Enter" && addTask()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button onClick={addTask}>Add</button>
|
<div className="reminder-section">
|
||||||
|
<label className="reminder-label">
|
||||||
|
<span className="label-icon">📅</span> Reminder (Optional)
|
||||||
|
</label>
|
||||||
|
<div className="reminder-controls">
|
||||||
|
<input
|
||||||
|
type="datetime-local"
|
||||||
|
className="reminder-input"
|
||||||
|
value={reminder}
|
||||||
|
onChange={(e) => setReminder(e.target.value)}
|
||||||
|
onKeyDown={(e) => e.key === "Enter" && addTask()}
|
||||||
|
/>
|
||||||
|
<button className="add-btn" onClick={addTask}>
|
||||||
|
Add Task
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <p className="error-message">{error}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
{tasks.map((item, index) => (
|
{tasks.map((item) => (
|
||||||
<li key={index} className={item.completed ? "completed" : ""}>
|
<li key={item.id} className={item.completed ? "completed" : ""}>
|
||||||
<span onClick={() => toggleTask(index)}>{item.text}</span>
|
<div className="task-content" onClick={() => toggleTask(item.id)}>
|
||||||
|
<span className="task-text">{item.text}</span>
|
||||||
|
|
||||||
<button onClick={() => deleteTask(index)}>Delete</button>
|
<div className="task-timestamps">
|
||||||
|
<span className="task-created">
|
||||||
|
Added: {formatDate(item.createdAt)}
|
||||||
|
</span>
|
||||||
|
<span className="task-reminder">
|
||||||
|
{item.reminder
|
||||||
|
? `🔔 Reminder: ${formatDate(item.reminder)}`
|
||||||
|
: "No reminder set"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button className="delete-btn" onClick={() => deleteTask(item.id)}>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
Reference in New Issue
Block a user