Created
May 15, 2025 14:33
-
-
Save MimiGapa/06586305fd3599107095d5add804fc50 to your computer and use it in GitHub Desktop.
PROJECT FILES
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | |
); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"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" | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** @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: [], | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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