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 {
|
||||
max-width: 540px;
|
||||
max-width: 560px;
|
||||
width: 100%;
|
||||
margin: 60px auto;
|
||||
margin: 50px auto;
|
||||
padding: 32px;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
@@ -21,88 +21,178 @@
|
||||
|
||||
.input-section {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
margin-bottom: 28px;
|
||||
background: var(--code-bg);
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.input-section input {
|
||||
flex: 1;
|
||||
padding: 12px 16px;
|
||||
.task-input-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.task-input {
|
||||
width: 100%;
|
||||
padding: 14px 16px;
|
||||
font-size: 16px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: var(--code-bg);
|
||||
background: var(--bg);
|
||||
color: var(--text-h);
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.input-section input:focus {
|
||||
.task-input:focus,
|
||||
.reminder-input:focus {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 3px var(--accent-bg);
|
||||
}
|
||||
|
||||
.input-section button {
|
||||
padding: 12px 20px;
|
||||
font-size: 16px;
|
||||
.task-input.input-error {
|
||||
border-color: #ef4444;
|
||||
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;
|
||||
background: var(--accent);
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: opacity 0.2s, transform 0.1s;
|
||||
}
|
||||
|
||||
.input-section button:hover {
|
||||
.add-btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.input-section button:active {
|
||||
.add-btn:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.error-message {
|
||||
margin: 0;
|
||||
color: #ef4444;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.todo-container ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.todo-container li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
padding: 14px 16px;
|
||||
background: var(--code-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
border-radius: 10px;
|
||||
transition: background-color 0.2s, border-color 0.2s;
|
||||
}
|
||||
|
||||
.todo-container li span {
|
||||
.task-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
color: var(--text-h);
|
||||
font-size: 16px;
|
||||
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);
|
||||
}
|
||||
|
||||
.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;
|
||||
color: var(--text);
|
||||
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;
|
||||
font-size: 14px;
|
||||
background: transparent;
|
||||
@@ -114,8 +204,22 @@
|
||||
transition: background-color 0.2s, color 0.2s, border-color 0.2s;
|
||||
}
|
||||
|
||||
.todo-container li button:hover {
|
||||
.delete-btn:hover {
|
||||
background: #ef4444;
|
||||
color: #ffffff;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
157
src/App.jsx
157
src/App.jsx
@@ -1,56 +1,179 @@
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import "./App.css";
|
||||
|
||||
function App() {
|
||||
const [task, setTask] = useState("");
|
||||
const [reminder, setReminder] = useState("");
|
||||
const [tasks, setTasks] = useState([]);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const addTask = () => {
|
||||
if (task.trim() === "") return;
|
||||
const addTask = async () => {
|
||||
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 = {
|
||||
text: task,
|
||||
id: Date.now() + Math.random(),
|
||||
text: task.trim(),
|
||||
completed: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
reminder: reminderIso,
|
||||
reminderFired: false,
|
||||
};
|
||||
|
||||
setTasks([...tasks, newTask]);
|
||||
setTasks((prev) => [...prev, newTask]);
|
||||
setTask("");
|
||||
setReminder("");
|
||||
setError("");
|
||||
};
|
||||
|
||||
const deleteTask = (index) => {
|
||||
setTasks(tasks.filter((_, i) => i !== index));
|
||||
const deleteTask = (id) => {
|
||||
setTasks((prev) => prev.filter((item) => item.id !== id));
|
||||
};
|
||||
|
||||
const toggleTask = (index) => {
|
||||
setTasks(
|
||||
tasks.map((item, i) =>
|
||||
i === index ? { ...item, completed: !item.completed } : item
|
||||
const toggleTask = (id) => {
|
||||
setTasks((prev) =>
|
||||
prev.map((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 (
|
||||
<div className="todo-container">
|
||||
<h1>To-Do List</h1>
|
||||
|
||||
<div className="input-section">
|
||||
<div className="task-input-wrapper">
|
||||
<input
|
||||
type="text"
|
||||
className={`task-input ${error ? "input-error" : ""}`}
|
||||
placeholder="Enter a task..."
|
||||
value={task}
|
||||
onChange={(e) => setTask(e.target.value)}
|
||||
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>
|
||||
|
||||
<ul>
|
||||
{tasks.map((item, index) => (
|
||||
<li key={index} className={item.completed ? "completed" : ""}>
|
||||
<span onClick={() => toggleTask(index)}>{item.text}</span>
|
||||
{tasks.map((item) => (
|
||||
<li key={item.id} className={item.completed ? "completed" : ""}>
|
||||
<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>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
Reference in New Issue
Block a user