Skip to content

Instantly share code, notes, and snippets.

@MimiGapa
Created May 15, 2025 14:33
Show Gist options
  • Save MimiGapa/06586305fd3599107095d5add804fc50 to your computer and use it in GitHub Desktop.
Save MimiGapa/06586305fd3599107095d5add804fc50 to your computer and use it in GitHub Desktop.
PROJECT FILES
import { useState } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { TasksProvider } from './context/TasksContext';
import Header from './components/Header';
import MatrixPage from './pages/MatrixPage';
import TasksPage from './pages/TasksPage';
import ChartsPage from './pages/ChartsPage';
import SettingsPage from './pages/SettingsPage';
import TaskForm from './components/TaskForm';
import { useTasks } from './context/TasksContext';
function AppContent() {
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
const { addTask } = useTasks();
return (
<>
<Header onAddClick={() => setIsAddModalOpen(true)} />
<div className="container mx-auto p-4">
<Routes>
<Route path="/" element={<MatrixPage />} />
<Route path="/tasks" element={<TasksPage />} />
<Route path="/charts" element={<ChartsPage />} />
<Route path="/settings" element={<SettingsPage />} />
</Routes>
</div>
{isAddModalOpen && (
<TaskForm isOpen={true} onClose={() => setIsAddModalOpen(false)} onSave={addTask} initialTask={null} />
)}
</>
);
}
function App() {
return (
<TasksProvider>
<Router>
<AppContent />
</Router>
</TasksProvider>
);
}
export default App;
import { useState } from 'react';
import { useTasks } from '../context/TasksContext';
import { LineChart, Line, PieChart, Pie, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend } from 'recharts';
import { DateTime } from 'luxon';
function ChartsPage() {
const { tasks } = useTasks();
const [period, setPeriod] = useState('week');
const completedTasks = tasks.filter((t) => t.completedAt);
const earliestDate = completedTasks.length ? DateTime.fromJSDate(new Date(Math.min(...completedTasks.map((t) => t.completedAt.getTime())))) : DateTime.now();
const now = DateTime.now();
const lineData = [];
let current = earliestDate.startOf(period);
while (current <= now) {
const end = current.endOf(period);
const count = completedTasks.filter((t) => {
const d = DateTime.fromJSDate(t.completedAt);
return d >= current && d < end;
}).length;
lineData.push({ name: current.toFormat(period === 'day' ? 'MMM d' : period === 'week' ? 'MMM d' : period === 'month' ? 'MMM yyyy' : 'QQQ yyyy'), count });
current = current.plus({ [period === 'quarter' ? 'quarter' : period]: 1 });
}
const pieData = [
{ name: 'Do', value: tasks.filter((t) => t.urgent && t.important).length },
{ name: 'Decide', value: tasks.filter((t) => !t.urgent && t.important).length },
{ name: 'Delegate', value: tasks.filter((t) => t.urgent && !t.important).length },
{ name: 'Delete', value: tasks.filter((t) => !t.urgent && !t.important).length },
].filter((d) => d.value > 0);
const tagCounts = tasks.reduce((acc, task) => {
task.tags.forEach((tag) => { acc[tag] = (acc[tag] || 0) + 1; });
return acc;
}, {});
const barData = Object.entries(tagCounts).map(([name, value]) => ({ name, value }));
return (
<div>
<div className="mb-4">
<label className="mr-2">Period:</label>
<select value={period} onChange={(e) => setPeriod(e.target.value)} className="border rounded p-1">
<option value="day">Day</option>
<option value="week">Week</option>
<option value="month">Month</option>
<option value="quarter">Quarter</option>
</select>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div>
<h3 className="font-semibold mb-2">Tasks Completed Over Time</h3>
<LineChart width={500} height={300} data={lineData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Line type="monotone" dataKey="count" stroke="#8884d8" />
</LineChart>
</div>
<div>
<h3 className="font-semibold mb-2">Quadrant Distribution</h3>
<PieChart width={400} height={400}>
<Pie dataKey="value" data={pieData} cx="50%" cy="50%" outerRadius={80} fill="#8884d8" label />
<Tooltip />
</PieChart>
</div>
<div>
<h3 className="font-semibold mb-2">Tasks by Tag</h3>
<BarChart width={500} height={300} data={barData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Bar dataKey="value" fill="#82ca9d" />
</BarChart>
</div>
</div>
</div>
);
}
export default ChartsPage;
import { Link } from 'react-router-dom';
import { Plus, List, BarChart, Settings } from 'lucide-react';
const EisenhowerGrid = ({ className }) => (
<svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect rx="1" x="3" y="3" width="7" height="7" />
<rect rx="1" x="14" y="3" width="7" height="7" />
<rect rx="1" x="14" y="14" width="7" height="7" />
<rect rx="1" x="3" y="14" width="7" height="7" />
</svg>
);
function Header({ onAddClick }) {
return (
<header className="border-b bg-white/80 backdrop-blur py-3 px-4 flex items-center justify-between">
<h1 className="text-lg font-semibold">Eisenhower To-Do</h1>
<nav className="flex items-center gap-4">
<Link to="/" aria-label="Matrix" className="flex items-center gap-2 text-gray-600 hover:text-gray-900">
<EisenhowerGrid className="h-4 w-4" />
<span className="hidden sm:inline">Matrix</span>
</Link>
<Link to="/tasks" aria-label="Tasks" className="flex items-center gap-2 text-gray-600 hover:text-gray-900">
<List className="h-4 w-4" />
<span className="hidden sm:inline">Tasks</span>
</Link>
<Link to="/charts" aria-label="Charts" className="flex items-center gap-2 text-gray-600 hover:text-gray-900">
<BarChart className="h-4 w-4" />
<span className="hidden sm:inline">Charts</span>
</Link>
<Link to="/settings" aria-label="Settings" className="flex items-center gap-2 text-gray-600 hover:text-gray-900">
<Settings className="h-4 w-4" />
<span className="hidden sm:inline">Settings</span>
</Link>
<button onClick={onAddClick} aria-label="New Task" className="flex items-center gap-2 text-gray-600 hover:text-gray-900">
<Plus className="h-4 w-4" />
<span className="hidden sm:inline">New Task</span>
</button>
</nav>
</header>
);
}
export default Header;
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Eisenhower To-Do</title>
<!-- Load fonts directly here if not loaded in index.css -->
<link href="https://fonts.googleapis.com/css2?family=Geist:wght@100;900&family=Geist+Mono:wght@100;900&display=swap" rel="stylesheet">
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
import './styles/ref_index.css';
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
import { useState } from 'react';
import { useTasks } from '../context/TasksContext';
import TaskForm from '../components/TaskForm';
function QuadrantCard({ title, tasks, onEdit, onDelete, onToggleComplete }) {
const tagColors = ['bg-red-500', 'bg-blue-500', 'bg-green-500', 'bg-yellow-500', 'bg-purple-500'];
const getTagColor = (tag) => {
const index = tag.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) % tagColors.length;
return tagColors[index];
};
return (
<div className="p-4 border rounded-md shadow-sm bg-white">
<h2 className="text-lg font-semibold mb-2">{title}</h2>
<div className="space-y-2">
{tasks.length === 0 ? (
<p className="text-gray-500 text-sm">No tasks here.</p>
) : (
tasks.map((task) => (
<div key={task.id} className="flex items-center justify-between p-2 border rounded">
<div>
<input
type="checkbox"
checked={task.completed}
onChange={() => onToggleComplete(task.id)}
className="mr-2"
/>
<span className={task.completed ? 'line-through text-gray-500' : ''}>{task.name}</span>
<div className="flex gap-1 mt-1">
{task.tags.map((tag) => (
<span key={tag} className={`px-1 text-xs text-white rounded ${getTagColor(tag)}`}>{tag}</span>
))}
</div>
</div>
<div className="flex gap-2">
<button onClick={() => onEdit(task)} className="text-blue-600">Edit</button>
<button onClick={() => onDelete(task.id)} className="text-red-600">Delete</button>
</div>
</div>
))
)}
</div>
</div>
);
}
function MatrixPage() {
const { tasks, updateTask, deleteTask, toggleComplete } = useTasks();
const [editingTask, setEditingTask] = useState(null);
const doTasks = tasks.filter((t) => t.urgent && t.important);
const decideTasks = tasks.filter((t) => !t.urgent && t.important);
const delegateTasks = tasks.filter((t) => t.urgent && !t.important);
const deleteTasks = tasks.filter((t) => !t.urgent && !t.important);
return (
<div>
<div className="grid grid-cols-2 gap-4">
<QuadrantCard title="Do" tasks={doTasks} onEdit={setEditingTask} onDelete={deleteTask} onToggleComplete={toggleComplete} />
<QuadrantCard title="Decide" tasks={decideTasks} onEdit={setEditingTask} onDelete={deleteTask} onToggleComplete={toggleComplete} />
<QuadrantCard title="Delegate" tasks={delegateTasks} onEdit={setEditingTask} onDelete={deleteTask} onToggleComplete={toggleComplete} />
<QuadrantCard title="Delete" tasks={deleteTasks} onEdit={setEditingTask} onDelete={deleteTask} onToggleComplete={toggleComplete} />
</div>
{editingTask && (
<TaskForm isOpen={true} onClose={() => setEditingTask(null)} onSave={updateTask} initialTask={editingTask} />
)}
</div>
);
}
export default MatrixPage;
{
"name": "matrix_to-do",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"@heroicons/react": "^2.2.0",
"dexie": "^4.0.11",
"lucide-react": "^0.510.0",
"luxon": "^3.6.1",
"react": "^19.1.0",
"react-datepicker": "^8.3.0",
"react-dom": "^19.1.0",
"react-modal": "^3.16.3",
"react-router-dom": "^7.6.0",
"recharts": "^2.15.3",
"uuid": "^11.1.0"
},
"devDependencies": {
"@tailwindcss/vite": "^4.1.6",
"@vitejs/plugin-react": "^4.4.1",
"autoprefixer": "^10.4.21",
"postcss": "^8.5.3",
"tailwindcss": "^4.1.6",
"vite": "^6.3.5"
}
}
import { useState } from 'react';
import { useTasks } from '../context/TasksContext';
// Custom ConfirmDialog component
function ConfirmDialog({ isOpen, onClose, onConfirm, title, message }) {
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg">
<div className="flex flex-col gap-2">
<div className="flex justify-between items-center">
<h2 className="text-lg font-semibold">{title}</h2>
<button
onClick={onClose}
className="text-muted-foreground hover:text-foreground"
>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
<span className="sr-only">Close</span>
</button>
</div>
<p className="text-sm text-muted-foreground">{message}</p>
</div>
<div className="flex-col-reverse sm:flex-row sm:justify-end flex gap-2 justify-end">
<button
onClick={onConfirm}
className="cursor-pointer inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive bg-destructive !text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 h-9 px-4 py-2 has-[>svg]:px-3"
>
Yes, delete everything
</button>
</div>
</div>
</div>
);
}
function SettingsPage() {
const { tasks, setTasks } = useTasks();
const [isModalOpen, setIsModalOpen] = useState(false);
// Export data as JSON file
const exportData = () => {
const dataStr = JSON.stringify(tasks);
const blob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'tasks.json';
a.click();
URL.revokeObjectURL(url);
};
// Import data from JSON file
const importData = (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (event) => {
try {
setTasks(JSON.parse(event.target.result));
alert('Data imported successfully');
} catch (err) {
alert('Invalid file format');
}
};
reader.readAsText(file);
}
};
// Handle deletion confirmation
const handleDeleteConfirm = () => {
setTasks([]);
setIsModalOpen(false);
};
return (
<div className="container mx-auto p-4 space-y-8 max-w-full sm:max-w-lg">
<h1 className="text-2xl font-bold">Settings</h1>
<div className="space-y-6">
{/* Data Backup & Restore Card */}
<div className="bg-card text-card-foreground flex flex-col gap-6 rounded-xl p-6 shadow-sm">
<div className="flex flex-col gap-1.5">
<h2 className="text-lg font-semibold">Data Backup & Restore</h2>
<p className="text-sm text-muted-foreground">
Backup & restore your data to a file. You can use this to move data between devices.
</p>
</div>
<div className="flex flex-wrap gap-4 justify-center">
<button
onClick={exportData}
className="cursor-pointer inline-flex items-center justify-center rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-9 px-4 py-2"
>
Export DB
</button>
<label
className="cursor-pointer inline-flex items-center justify-center rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-9 px-4 py-2"
>
Import DB
<input
type="file"
accept=".json"
onChange={importData}
className="hidden"
/>
</label>
</div>
</div>
{/* Delete All Data (Dangerous) Card */}
<div className="bg-card text-card-foreground flex flex-col gap-6 rounded-xl p-6 shadow-sm">
<div className="flex flex-col gap-1.5">
<h2 className="text-lg font-semibold">Delete All Data (Dangerous)</h2>
<p className="text-sm text-muted-foreground">
This will delete all data from the database. This action is irreversible.
</p>
</div>
<div className="flex justify-center">
<button
onClick={() => setIsModalOpen(true)}
className="cursor-pointer inline-flex items-center justify-center rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 bg-destructive text-destructive-foreground hover:bg-destructive/90 h-9 px-4 py-2"
>
Clear All Data
</button>
</div>
</div>
</div>
{/* Custom Confirmation Dialog */}
<ConfirmDialog
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
onConfirm={handleDeleteConfirm}
title="Are you absolutely sure?"
message="This action cannot be undone. This will permanently delete all your habits, logs, and conditions from the database."
/>
</div>
);
}
export default SettingsPage;
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
fontFamily: {
sans: ['Geist', 'sans-serif'],
mono: ['Geist Mono', 'monospace'],
},
},
},
plugins: [],
}
import { useState, useEffect } from 'react';
import Modal from 'react-modal';
import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';
Modal.setAppElement('#root');
function TaskForm({ isOpen, onClose, onSave, initialTask }) {
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [tags, setTags] = useState('');
const [important, setImportant] = useState(false);
const [urgent, setUrgent] = useState(false);
const [deadline, setDeadline] = useState(null);
useEffect(() => {
if (initialTask) {
setName(initialTask.name);
setDescription(initialTask.description || '');
setTags(initialTask.tags.join(', '));
setImportant(initialTask.important);
setUrgent(initialTask.urgent);
setDeadline(initialTask.deadline ? new Date(initialTask.deadline) : null);
} else {
setName('');
setDescription('');
setTags('');
setImportant(false);
setUrgent(false);
setDeadline(null);
}
}, [initialTask]);
const handleSubmit = (e) => {
e.preventDefault();
if (!name.trim()) return;
const tagsArray = tags.split(',').map(tag => tag.trim()).filter(tag => tag);
const taskData = {
...initialTask,
name,
description,
tags: tagsArray,
important,
urgent,
deadline: deadline ? deadline.toISOString() : null,
};
onSave(taskData);
onClose();
};
return (
<Modal
isOpen={isOpen}
onRequestClose={onClose}
className="bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg"
overlayClassName="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50"
>
<h2 className="text-lg font-semibold mb-4">{initialTask ? 'Edit Task' : 'Add Task'}</h2>
<button
onClick={onClose}
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
<span className="sr-only">Close</span>
</button>
<form onSubmit={handleSubmit} className="space-y-5">
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Task name"
className="w-full border rounded-md p-2"
/>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Description"
className="w-full border rounded-md p-2"
rows={3}
/>
<input
value={tags}
onChange={(e) => setTags(e.target.value)}
placeholder="Tags (comma-separated)"
className="w-full border rounded-md p-2"
/>
<div className="flex gap-4">
<label><input type="checkbox" checked={important} onChange={(e) => setImportant(e.target.checked)} /> Important</label>
<label><input type="checkbox" checked={urgent} onChange={(e) => setUrgent(e.target.checked)} /> Urgent</label>
</div>
<DatePicker
selected={deadline}
onChange={setDeadline}
placeholderText="Deadline"
className="w-full border rounded-md p-2"
/>
<div className="flex justify-end gap-2">
<button type="submit" className="cursor-pointer inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive bg-primary text-primary-foreground shadow-xs hover:bg-primary/90 h-9 px-4 py-2 has-[>svg]:px-3 w-full">
{initialTask ? 'Save' : 'Create Task'}
</button>
</div>
</form>
</Modal>
);
}
export default TaskForm;
import { createContext, useContext, useState, useEffect } from 'react';
import { v4 as uuidv4 } from 'uuid';
const TasksContext = createContext();
export function TasksProvider({ children }) {
const [tasks, setTasks] = useState(() => {
const saved = localStorage.getItem('tasks');
return saved ? JSON.parse(saved) : [];
});
useEffect(() => {
localStorage.setItem('tasks', JSON.stringify(tasks));
}, [tasks]);
const addTask = (newTask) => {
setTasks((prev) => [
...prev,
{ id: uuidv4(), ...newTask, completed: false, createdAt: new Date(), completedAt: null },
]);
};
const updateTask = (updatedTask) => {
setTasks((prev) =>
prev.map((task) =>
task.id === updatedTask.id ? { ...updatedTask, updatedAt: new Date() } : task
)
);
};
const deleteTask = (id) => {
setTasks((prev) => prev.filter((task) => task.id !== id));
};
const toggleComplete = (id) => {
setTasks((prev) =>
prev.map((task) =>
task.id === id
? { ...task, completed: !task.completed, completedAt: !task.completed ? new Date() : null }
: task
)
);
};
const getAllTags = () => Array.from(new Set(tasks.flatMap((task) => task.tags)));
return (
<TasksContext.Provider value={{ tasks, addTask, updateTask, deleteTask, toggleComplete, getAllTags }}>
{children}
</TasksContext.Provider>
);
}
export const useTasks = () => useContext(TasksContext);
import { useState } from 'react';
import { useTasks } from '../context/TasksContext';
import TaskForm from '../components/TaskForm';
import { DateTime } from 'luxon';
function TaskItem({ task, onEdit, onDelete, onToggleComplete }) {
const tagColors = ['bg-red-500', 'bg-blue-500', 'bg-green-500', 'bg-yellow-500', 'bg-purple-500'];
const getTagColor = (tag) => {
const index = tag.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) % tagColors.length;
return tagColors[index];
};
return (
<div className="flex items-center justify-between p-2 border rounded">
<div>
<input
type="checkbox"
checked={task.completed}
onChange={() => onToggleComplete(task.id)}
className="mr-2"
/>
<span className={task.completed ? 'line-through text-gray-500' : ''}>{task.name}</span>
{task.deadline && (
<span className="text-gray-500 text-sm ml-2">
{DateTime.fromISO(task.deadline).toFormat('MMM d, yyyy')}
</span>
)}
<div className="flex gap-1 mt-1">
{task.tags.map((tag) => (
<span key={tag} className={`px-1 text-xs text-white rounded ${getTagColor(tag)}`}>{tag}</span>
))}
</div>
</div>
<div className="flex gap-2">
<button onClick={() => onEdit(task)} className="text-blue-600">Edit</button>
<button onClick={() => onDelete(task.id)} className="text-red-600">Delete</button>
</div>
</div>
);
}
function TasksPage() {
const { tasks, getAllTags, updateTask, deleteTask, toggleComplete } = useTasks();
const [viewMode, setViewMode] = useState('list');
const [groupingPeriod, setGroupingPeriod] = useState('week');
const [selectedTags, setSelectedTags] = useState([]);
const [sortBy, setSortBy] = useState('deadline');
const [editingTask, setEditingTask] = useState(null);
const allTags = getAllTags();
const filteredTasks = selectedTags.length > 0
? tasks.filter((task) => task.tags.some((tag) => selectedTags.includes(tag)))
: tasks;
const groupTasksByPeriod = (tasks, period) => {
const groups = {};
tasks.forEach((task) => {
const key = task.deadline
? DateTime.fromISO(task.deadline).startOf(period === 'day' ? 'day' : period === 'week' ? 'week' : period === 'month' ? 'month' : 'quarter').toISODate()
: 'No Deadline';
groups[key] = groups[key] || [];
groups[key].push(task);
});
return Object.entries(groups).sort((a, b) => (a[0] === 'No Deadline' ? 1 : b[0] === 'No Deadline' ? -1 : a[0].localeCompare(b[0])));
};
const formatGroupKey = (key, period) => {
if (key === 'No Deadline') return 'No Deadline';
const date = DateTime.fromISO(key);
return period === 'day' ? date.toFormat('MMMM d, yyyy') :
period === 'week' ? `Week of ${date.toFormat('MMMM d, yyyy')}` :
period === 'month' ? date.toFormat('MMMM yyyy') :
date.toFormat('QQQ yyyy');
};
const displayedTasks = viewMode === 'list'
? [...filteredTasks].sort((a, b) => sortBy === 'deadline' ? ((a.deadline || '9999-12-31') > (b.deadline || '9999-12-31') ? 1 : -1) : a.name.localeCompare(b.name))
: groupTasksByPeriod(filteredTasks, groupingPeriod);
return (
<div>
<div className="flex flex-wrap gap-4 mb-4">
<div>
<label className="mr-2">View:</label>
<select value={viewMode} onChange={(e) => setViewMode(e.target.value)} className="border rounded p-1">
<option value="list">List</option>
<option value="grouped">Grouped</option>
</select>
</div>
{viewMode === 'grouped' && (
<div>
<label className="mr-2">Group By:</label>
<select value={groupingPeriod} onChange={(e) => setGroupingPeriod(e.target.value)} className="border rounded p-1">
<option value="day">Day</option>
<option value="week">Week</option>
<option value="month">Month</option>
<option value="quarter">Quarter</option>
</select>
</div>
)}
{viewMode === 'list' && (
<div>
<label className="mr-2">Sort By:</label>
<select value={sortBy} onChange={(e) => setSortBy(e.target.value)} className="border rounded p-1">
<option value="deadline">Deadline</option>
<option value="name">Name</option>
</select>
</div>
)}
</div>
<div className="mb-4">
<h3 className="font-semibold">Filter by Tags</h3>
<div className="flex gap-2 flex-wrap">
{allTags.map((tag) => (
<button
key={tag}
className={`px-2 py-1 rounded ${selectedTags.includes(tag) ? 'bg-blue-500 text-white' : 'bg-gray-200'}`}
onClick={() => setSelectedTags((prev) => prev.includes(tag) ? prev.filter(t => t !== tag) : [...prev, tag])}
>
{tag}
</button>
))}
</div>
</div>
{viewMode === 'list' ? (
<div className="space-y-2">
{displayedTasks.map((task) => (
<TaskItem key={task.id} task={task} onEdit={setEditingTask} onDelete={deleteTask} onToggleComplete={toggleComplete} />
))}
</div>
) : (
<div className="space-y-4">
{displayedTasks.map(([key, groupTasks]) => (
<div key={key}>
<h3 className="font-semibold">{formatGroupKey(key, groupingPeriod)}</h3>
<div className="space-y-2">
{groupTasks.map((task) => (
<TaskItem key={task.id} task={task} onEdit={setEditingTask} onDelete={deleteTask} onToggleComplete={toggleComplete} />
))}
</div>
</div>
))}
</div>
)}
{editingTask && (
<TaskForm isOpen={true} onClose={() => setEditingTask(null)} onSave={updateTask} initialTask={editingTask} />
)}
</div>
);
}
export default TasksPage;
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [react(), tailwindcss()],
base: './'
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment