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
43 changes: 37 additions & 6 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { PlusIcon } from '@heroicons/react/24/outline';
import { PlusIcon, MoonIcon, SunIcon } from '@heroicons/react/24/outline';

import { TaskProvider } from './context/TaskContext';
import { TagProvider } from './context/TagContext';
Expand All @@ -11,23 +11,54 @@ import TaskBoard from './features/lists/components/TaskBoard';

function App() {
const [showInput, setShowInput] = useState(false);
const [isDarkMode, setIsDarkMode] = useState(() => {
if (typeof window === 'undefined') {
return false;
}

const storedTheme = window.localStorage.getItem('task-dashboard.theme');
if (storedTheme) {
return storedTheme === 'dark';
}

return window.matchMedia('(prefers-color-scheme: dark)').matches;
});

useEffect(() => {
if (typeof document !== 'undefined') {
document.documentElement.classList.toggle('dark', isDarkMode);
}

if (typeof window !== 'undefined') {
window.localStorage.setItem('task-dashboard.theme', isDarkMode ? 'dark' : 'light');
}
}, [isDarkMode]);

return (
<TaskProvider>
<TagProvider>
<ListProvider>
<div className="App min-h-screen bg-gradient-to-br from-primary-50 to-secondary-50 flex flex-col items-center py-12 px-4" data-testid="app">
<div className="App min-h-screen bg-gradient-to-br from-primary-50 to-secondary-50 dark:from-neutral-950 dark:via-slate-950 dark:to-neutral-900 flex flex-col items-center py-12 px-4 transition-colors duration-300" data-testid="app">
<div className="w-full max-w-6xl">
<motion.div
className="mb-6 bg-white rounded-2xl shadow-soft p-6"
className="mb-6 bg-white dark:bg-neutral-900 dark:border dark:border-neutral-800 rounded-2xl shadow-soft p-6 transition-colors duration-300"
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
data-testid="app-header"
>
<div className="flex justify-between items-center mb-6">
<h1 className="text-3xl font-bold text-neutral-800 tracking-tight">Task Dashboard</h1>
{/* Stats will be displayed from TaskContext */}
<h1 className="text-3xl font-bold text-neutral-800 dark:text-neutral-100 tracking-tight">Task Dashboard</h1>
<button
type="button"
className="inline-flex items-center gap-2 rounded-xl border border-neutral-200 dark:border-neutral-700 bg-white/70 dark:bg-neutral-800 px-3 py-2 text-sm font-medium text-neutral-700 dark:text-neutral-200 hover:bg-neutral-50 dark:hover:bg-neutral-700 transition-colors"
onClick={() => setIsDarkMode(prev => !prev)}
data-testid="theme-toggle-button"
aria-label={isDarkMode ? 'Switch to light mode' : 'Switch to dark mode'}
>
{isDarkMode ? <SunIcon className="h-4 w-4" /> : <MoonIcon className="h-4 w-4" />}
{isDarkMode ? 'Light' : 'Dark'}
</button>
</div>

<AnimatePresence>
Expand Down
8 changes: 4 additions & 4 deletions src/features/lists/components/ListAddTask.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,15 @@ function ListAddTask({ onCancel, listFilters }) {
placeholder="Add a task to this list..."
value={text}
onChange={(e) => setText(e.target.value)}
className="w-full py-2 px-4 pr-20 text-sm text-neutral-800 rounded-lg border border-neutral-200 focus:border-primary-400 focus:ring-1 focus:ring-primary-200 outline-hidden transition-all"
className="w-full py-2 px-4 pr-20 text-sm text-neutral-800 dark:text-neutral-100 dark:bg-neutral-800 rounded-lg border border-neutral-200 dark:border-neutral-700 focus:border-primary-400 focus:ring-1 focus:ring-primary-200 outline-hidden transition-all"
autoComplete="off"
data-testid="list-task-input"
/>
<div className="absolute right-1.5 top-1/2 -translate-y-1/2 flex gap-1">
<motion.button
type="button"
onClick={onCancel}
className="p-1.5 text-neutral-500 hover:text-neutral-700 rounded-full hover:bg-neutral-100 transition-colors"
className="p-1.5 text-neutral-500 dark:text-neutral-300 hover:text-neutral-700 dark:hover:text-neutral-100 rounded-full hover:bg-neutral-100 dark:hover:bg-neutral-700 transition-colors"
whileTap={{ scale: 0.9 }}
data-testid="list-cancel-button"
>
Expand All @@ -81,7 +81,7 @@ function ListAddTask({ onCancel, listFilters }) {
{/* Show tags that will be automatically applied */}
{getTagFilters().length > 0 && (
<div className="mt-2 mb-1 px-1" data-testid="auto-applied-tags">
<p className="text-xs text-neutral-500">
<p className="text-xs text-neutral-500 dark:text-neutral-400">
Will be tagged with:
<span className="font-medium ml-1 text-primary-600">
{getTagFilters().join(', ')}
Expand All @@ -93,4 +93,4 @@ function ListAddTask({ onCancel, listFilters }) {
);
}

export default ListAddTask;
export default ListAddTask;
28 changes: 14 additions & 14 deletions src/features/lists/components/TaskBoard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,10 @@ function TaskBoard() {
return (
<div className="task-board" data-testid="task-board">
<div className="mb-4 flex justify-between items-center">
<h2 className="font-semibold text-lg text-neutral-700">Task Lists</h2>
<h2 className="font-semibold text-lg text-neutral-700 dark:text-neutral-200">Task Lists</h2>
<button
type="button"
className="flex items-center text-sm font-medium text-primary-600 hover:text-primary-800 transition-colors px-3 py-1.5 hover:bg-primary-50 rounded-lg"
className="flex items-center text-sm font-medium text-primary-600 hover:text-primary-800 dark:text-primary-300 dark:hover:text-primary-200 transition-colors px-3 py-1.5 hover:bg-primary-50 dark:hover:bg-neutral-800 rounded-lg"
onClick={handleManageTags}
data-testid="manage-tags-button"
>
Expand Down Expand Up @@ -120,7 +120,7 @@ function TaskBoard() {
return (
<div
key={list.id}
className="task-list-container bg-white rounded-xl shadow-soft w-full flex flex-col"
className="task-list-container bg-white dark:bg-neutral-900 dark:border dark:border-neutral-800 rounded-xl shadow-soft w-full flex flex-col transition-colors duration-300"
data-testid={`task-list-${list.id}`}
>
{editingListId === list.id ? (
Expand All @@ -131,18 +131,18 @@ function TaskBoard() {
/>
) : (
<>
<div className="list-header p-4 border-b border-neutral-100 flex justify-between items-center">
<h2 className="font-medium text-lg" data-testid={`list-title-${list.id}`}>{list.title}</h2>
<div className="list-header p-4 border-b border-neutral-100 dark:border-neutral-800 flex justify-between items-center">
<h2 className="font-medium text-lg text-neutral-800 dark:text-neutral-100" data-testid={`list-title-${list.id}`}>{list.title}</h2>
<div className="flex items-center gap-2">
<span
className="text-xs font-medium text-neutral-500 bg-neutral-100 px-2 py-0.5 rounded-xs"
className="text-xs font-medium text-neutral-500 dark:text-neutral-300 bg-neutral-100 dark:bg-neutral-800 px-2 py-0.5 rounded-xs"
data-testid={`task-count-${list.id}`}
>
{completedTasksCount}/{filteredTasks.length}
</span>
<button
type="button"
className="text-sm text-neutral-500 hover:text-neutral-700 px-2 py-1 hover:bg-neutral-100 rounded-xs"
className="text-sm text-neutral-500 dark:text-neutral-300 hover:text-neutral-700 dark:hover:text-neutral-100 px-2 py-1 hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-xs"
onClick={() => handleEditTaskList(list.id)}
data-testid={`edit-list-${list.id}`}
>
Expand All @@ -151,7 +151,7 @@ function TaskBoard() {
{list.id !== 'default' && (
<button
type="button"
className="text-sm text-rose-500 hover:text-rose-700 px-2 py-1 hover:bg-rose-50 rounded-xs"
className="text-sm text-rose-500 hover:text-rose-700 px-2 py-1 hover:bg-rose-50 dark:hover:bg-rose-950/40 rounded-xs"
onClick={() => deleteTaskList(list.id)}
data-testid={`delete-list-${list.id}`}
>
Expand All @@ -174,7 +174,7 @@ function TaskBoard() {
<button
type="button"
onClick={() => handleAddTaskToList(list.id)}
className="mb-3 w-full py-2 px-3 flex items-center justify-center text-sm text-neutral-600 hover:text-primary-600 bg-neutral-50 hover:bg-neutral-100 rounded-lg border border-dashed border-neutral-300 hover:border-primary-300 transition-colors"
className="mb-3 w-full py-2 px-3 flex items-center justify-center text-sm text-neutral-600 dark:text-neutral-300 hover:text-primary-600 dark:hover:text-primary-300 bg-neutral-50 dark:bg-neutral-800 hover:bg-neutral-100 dark:hover:bg-neutral-700 rounded-lg border border-dashed border-neutral-300 dark:border-neutral-700 hover:border-primary-300 transition-colors"
data-testid={`add-task-to-list-${list.id}`}
>
<PlusIcon className="h-4 w-4 mr-1.5" />
Expand All @@ -187,11 +187,11 @@ function TaskBoard() {

{/* List action buttons */}
{filteredTasks.length > 0 && (
<div className="list-actions p-3 border-t border-neutral-100 flex justify-between">
<div className="list-actions p-3 border-t border-neutral-100 dark:border-neutral-800 flex justify-between">
<motion.button
type="button"
onClick={() => handleCompleteListTasks(list.id)}
className="flex items-center text-xs font-medium text-primary-600 hover:text-primary-800 transition-colors px-2 py-1 hover:bg-primary-50 rounded-lg"
className="flex items-center text-xs font-medium text-primary-600 hover:text-primary-800 dark:text-primary-300 dark:hover:text-primary-200 transition-colors px-2 py-1 hover:bg-primary-50 dark:hover:bg-neutral-800 rounded-lg"
disabled={allTasksCompleted}
whileHover={{ scale: allTasksCompleted ? 1 : 1.02 }}
whileTap={{ scale: allTasksCompleted ? 1 : 0.98 }}
Expand All @@ -204,7 +204,7 @@ function TaskBoard() {
<motion.button
type="button"
onClick={() => handleDeleteListCompletedTasks(list.id)}
className="flex items-center text-xs font-medium text-rose-500 hover:text-rose-700 transition-colors px-2 py-1 hover:bg-rose-50 rounded-lg"
className="flex items-center text-xs font-medium text-rose-500 hover:text-rose-700 transition-colors px-2 py-1 hover:bg-rose-50 dark:hover:bg-rose-950/40 rounded-lg"
disabled={!hasCompletedTasks}
whileHover={{ scale: !hasCompletedTasks ? 1 : 1.02 }}
whileTap={{ scale: !hasCompletedTasks ? 1 : 0.98 }}
Expand All @@ -224,7 +224,7 @@ function TaskBoard() {
{/* Add new task list button */}
<motion.button
type="button"
className="add-list-button h-48 rounded-xl border-2 border-dashed border-neutral-200 flex flex-col items-center justify-center text-neutral-400 hover:text-primary-600 hover:border-primary-300 transition-colors"
className="add-list-button h-48 rounded-xl border-2 border-dashed border-neutral-200 dark:border-neutral-700 flex flex-col items-center justify-center text-neutral-400 dark:text-neutral-500 hover:text-primary-600 dark:hover:text-primary-300 hover:border-primary-300 transition-colors"
onClick={addTaskList}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
Expand All @@ -238,4 +238,4 @@ function TaskBoard() {
);
}

export default TaskBoard;
export default TaskBoard;
24 changes: 12 additions & 12 deletions src/features/lists/components/TaskListConfig.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,32 +36,32 @@ function TaskListConfig({ taskList, onSave, onCancel }) {

return (
<div className="task-list-config p-4" data-testid="task-list-config">
<h3 className="font-medium text-lg mb-4">Configure List</h3>
<h3 className="font-medium text-lg mb-4 text-neutral-800 dark:text-neutral-100">Configure List</h3>

{/* List title input */}
<div className="mb-4">
<label className="block text-sm font-medium text-neutral-700 mb-1" htmlFor="list-title">List Title</label>
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1" htmlFor="list-title">List Title</label>
<input
id="list-title"
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="w-full px-3 py-2 border border-neutral-300 rounded-md focus:outline-hidden focus:ring-1 focus:ring-primary-500"
className="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-100 rounded-md focus:outline-hidden focus:ring-1 focus:ring-primary-500"
placeholder="Enter list title"
data-testid="list-title-input"
/>
</div>

{/* Current filters */}
<div className="mb-4">
<label className="block text-sm font-medium text-neutral-700 mb-1">Current Filters</label>
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">Current Filters</label>

{filters.length === 0 ? (
<p className="text-sm text-neutral-500" data-testid="no-filters-message">No filters applied. This list will show all tasks.</p>
<p className="text-sm text-neutral-500 dark:text-neutral-400" data-testid="no-filters-message">No filters applied. This list will show all tasks.</p>
) : (
<div className="flex flex-wrap gap-2" data-testid="filter-list">
{filters.map((filter, index) => (
<div key={index} className="inline-flex items-center bg-neutral-100 px-3 py-1 rounded-full text-sm" data-testid={`filter-item-${index}`}>
<div key={index} className="inline-flex items-center bg-neutral-100 dark:bg-neutral-800 dark:text-neutral-200 px-3 py-1 rounded-full text-sm" data-testid={`filter-item-${index}`}>
{filter.type === 'tag' && (
<span>Tag: {filter.value}</span>
)}
Expand All @@ -71,7 +71,7 @@ function TaskListConfig({ taskList, onSave, onCancel }) {
<button
type="button"
onClick={() => removeFilter(index)}
className="ml-1 text-neutral-500 hover:text-neutral-700"
className="ml-1 text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-200"
data-testid={`remove-filter-${index}`}
>
<XMarkIcon className="h-4 w-4" />
Expand All @@ -84,9 +84,9 @@ function TaskListConfig({ taskList, onSave, onCancel }) {

{/* Add tag filter */}
<div className="mb-4">
<label className="block text-sm font-medium text-neutral-700 mb-1">Filter by Tag</label>
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">Filter by Tag</label>
{tags.length === 0 ? (
<p className="text-sm text-neutral-500" data-testid="no-tags-available-message">No tags available. Add tags to tasks first.</p>
<p className="text-sm text-neutral-500 dark:text-neutral-400" data-testid="no-tags-available-message">No tags available. Add tags to tasks first.</p>
) : (
<div className="flex flex-wrap gap-2" data-testid="available-tags">
{tags.map((tag) => (
Expand All @@ -112,7 +112,7 @@ function TaskListConfig({ taskList, onSave, onCancel }) {

{/* Add completion filter */}
<div className="mb-6">
<label className="block text-sm font-medium text-neutral-700 mb-1">Filter by Status</label>
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">Filter by Status</label>
<div className="flex gap-2">
<button
type="button"
Expand Down Expand Up @@ -148,7 +148,7 @@ function TaskListConfig({ taskList, onSave, onCancel }) {
<button
type="button"
onClick={onCancel}
className="px-4 py-2 border border-neutral-300 text-neutral-700 rounded-md hover:bg-neutral-50"
className="px-4 py-2 border border-neutral-300 dark:border-neutral-700 text-neutral-700 dark:text-neutral-200 rounded-md hover:bg-neutral-50 dark:hover:bg-neutral-800"
data-testid="cancel-config"
>
Cancel
Expand All @@ -166,4 +166,4 @@ function TaskListConfig({ taskList, onSave, onCancel }) {
);
}

export default TaskListConfig;
export default TaskListConfig;
Loading