Simplify Task List example
This commit is contained in:
43
examples/task-list/src/components/application.jsx
Normal file
43
examples/task-list/src/components/application.jsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { useEffect, useReducer } from 'react';
|
||||||
|
|
||||||
|
import * as api from '../api';
|
||||||
|
import { initialState, taskReducer } from '../reducer';
|
||||||
|
|
||||||
|
import { CreateTask } from './create-task';
|
||||||
|
import { Tasks } from './tasks';
|
||||||
|
|
||||||
|
export const Application = () => {
|
||||||
|
const [state, dispatch] = useReducer(taskReducer, initialState);
|
||||||
|
const { tasks } = state;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
api.all().then((payload) => {
|
||||||
|
dispatch({ type: 'set-tasks', payload });
|
||||||
|
});
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const addTask = (title) => {
|
||||||
|
api.add(title).then((payload) => {
|
||||||
|
dispatch({ type: 'add-task', payload });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateTask = (id, updatedTask) => {
|
||||||
|
api.update(id, updatedTask).then(() => {
|
||||||
|
dispatch({ type: 'update-task', payload: { id, ...updatedTask } });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeTask = (id) => {
|
||||||
|
api.remove(id).then(() => {
|
||||||
|
dispatch({ type: 'remove-task', payload: id });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="container my-10 max-w-xl space-y-8">
|
||||||
|
<CreateTask onSubmit={addTask} />
|
||||||
|
<Tasks tasks={tasks} updateTask={updateTask} removeTask={removeTask} />
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { CreateTask } from './create-task';
|
|
||||||
import { TaskProvider } from '../contexts/task-context';
|
|
||||||
import { Tasks } from './tasks';
|
|
||||||
|
|
||||||
export const Application = () => {
|
|
||||||
return (
|
|
||||||
<TaskProvider>
|
|
||||||
<main className="container my-10 max-w-xl space-y-8">
|
|
||||||
<CreateTask onSubmit={(title) => console.log(title)} />
|
|
||||||
<Tasks />
|
|
||||||
</main>
|
|
||||||
</TaskProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useTaskActions } from '../contexts/task-context';
|
|
||||||
|
|
||||||
export const CreateTask = () => {
|
export const CreateTask = ({ onSubmit }) => {
|
||||||
const { addTask } = useTaskActions();
|
|
||||||
const [title, setTitle] = useState('');
|
const [title, setTitle] = useState('');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -11,7 +9,7 @@ export const CreateTask = () => {
|
|||||||
action="/api/tasks"
|
action="/api/tasks"
|
||||||
onSubmit={(event) => {
|
onSubmit={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
addTask(title);
|
onSubmit(title);
|
||||||
setTitle('');
|
setTitle('');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -19,7 +17,7 @@ export const CreateTask = () => {
|
|||||||
<label htmlFor="new-task-title" className="sr-only">
|
<label htmlFor="new-task-title" className="sr-only">
|
||||||
Title
|
Title
|
||||||
</label>
|
</label>
|
||||||
<div className="flex flex-col sm:flex-row">
|
<div className="flex flex-row">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name="title"
|
name="title"
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import type { ComponentPropsWithoutRef } from 'react';
|
|
||||||
import { twMerge as merge } from 'tailwind-merge';
|
import { twMerge as merge } from 'tailwind-merge';
|
||||||
|
|
||||||
const formatDate = new Intl.DateTimeFormat(navigator.language, {
|
const formatDate = new Intl.DateTimeFormat(navigator.language, {
|
||||||
@@ -10,11 +9,9 @@ export const DateTime = ({
|
|||||||
date,
|
date,
|
||||||
title,
|
title,
|
||||||
className,
|
className,
|
||||||
}: ComponentPropsWithoutRef<'div'> & {
|
|
||||||
date: Date | string;
|
|
||||||
title: string;
|
|
||||||
}) => {
|
}) => {
|
||||||
if (typeof date === 'string') date = new Date(date);
|
if (typeof date === 'string') date = new Date(date);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={merge('space-x-2 overflow-x-hidden text-xs', className)}>
|
<div className={merge('space-x-2 overflow-x-hidden text-xs', className)}>
|
||||||
<span className="text-primary-800 dark:text-primary-200 font-semibold after:text-slate-700 after:content-[':'] dark:after:text-slate-300">
|
<span className="text-primary-800 dark:text-primary-200 font-semibold after:text-slate-700 after:content-[':'] dark:after:text-slate-300">
|
||||||
33
examples/task-list/src/components/task.jsx
Normal file
33
examples/task-list/src/components/task.jsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { memo } from 'react';
|
||||||
|
import { ChevronRightCircle } from 'lucide-react';
|
||||||
|
import { DateTime } from './date-time';
|
||||||
|
|
||||||
|
export const Task = memo(({ task, updateTask, removeTask }) => {
|
||||||
|
return (
|
||||||
|
<li className="block space-y-2 border-x border-t border-slate-300 bg-white p-4 first:rounded-t-md last:rounded-b-md last:border-b dark:border-slate-700">
|
||||||
|
<header className="flex flex-row items-center gap-4">
|
||||||
|
<label htmlFor={`toggle-${task.id}`} className="sr-only">
|
||||||
|
Mark Task as {task.completed ? 'Incomplete' : 'Complete'}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id={`toggle-${task.id}`}
|
||||||
|
type="checkbox"
|
||||||
|
className="block h-6 w-6"
|
||||||
|
checked={task.completed}
|
||||||
|
onChange={() => updateTask(task.id, { completed: !task.completed })}
|
||||||
|
/>
|
||||||
|
<h2 className="w-full font-semibold">{task.title}</h2>
|
||||||
|
<button
|
||||||
|
className="button-small button-destructive button-ghost"
|
||||||
|
onClick={() => removeTask(task.id)}
|
||||||
|
>
|
||||||
|
❌
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
<div className="flex flex-col sm:flex-row gap-1 sm:gap-4">
|
||||||
|
<DateTime date={task.createdAt} title="Created" />
|
||||||
|
<DateTime date={task.lastModified} title="Modified" />
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import { memo } from 'react';
|
|
||||||
import { ChevronRightCircle } from 'lucide-react';
|
|
||||||
import { DateTime } from './date-time';
|
|
||||||
import { useTaskActions } from '../contexts/task-context';
|
|
||||||
|
|
||||||
type TaskProps = {
|
|
||||||
task: import('../types').Task;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Task = memo(({ task }: TaskProps) => {
|
|
||||||
const { updateTask, removeTask } = useTaskActions();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li className="block space-y-2 border-x border-t border-slate-300 p-4 first:rounded-t-md last:rounded-b-md last:border-b dark:border-slate-700">
|
|
||||||
<header className="flex flex-row items-center gap-4">
|
|
||||||
<label htmlFor={`toggle-${task.id}`} className="sr-only">
|
|
||||||
Mark Task as {task.completed ? 'Incomplete' : 'Complete'}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id={`toggle-${task.id}`}
|
|
||||||
type="checkbox"
|
|
||||||
className="block h-6 w-6"
|
|
||||||
checked={task.completed}
|
|
||||||
onChange={() => updateTask(task.id, { completed: !task.completed })}
|
|
||||||
/>
|
|
||||||
<h2 className="w-full font-semibold">{task.title}</h2>
|
|
||||||
<button
|
|
||||||
className="button-small button-destructive button-ghost"
|
|
||||||
onClick={() => removeTask(task.id)}
|
|
||||||
>
|
|
||||||
❌
|
|
||||||
</button>
|
|
||||||
</header>
|
|
||||||
<label className="bg-primary-50 border-primary-100 grid cursor-pointer grid-cols-[1.25rem_1fr] gap-1 rounded-md border-2 p-2 text-xs dark:border-slate-900 dark:bg-slate-800">
|
|
||||||
<input type="checkbox" className="peer sr-only" />
|
|
||||||
<ChevronRightCircle className="block h-4 w-4 transition-transform peer-checked:rotate-90" />
|
|
||||||
<h3 className="select-none">Metadata</h3>
|
|
||||||
<div className="col-span-2 hidden gap-2 peer-checked:flex">
|
|
||||||
<DateTime date={task.createdAt} title="Created" />
|
|
||||||
<DateTime date={task.lastModified} title="Modified" />
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
16
examples/task-list/src/components/tasks.jsx
Normal file
16
examples/task-list/src/components/tasks.jsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { Task } from './task';
|
||||||
|
|
||||||
|
export const Tasks = ({ tasks, updateTask, removeTask }) => {
|
||||||
|
return (
|
||||||
|
<ul>
|
||||||
|
{tasks.map((task) => (
|
||||||
|
<Task
|
||||||
|
key={task.id}
|
||||||
|
task={task}
|
||||||
|
updateTask={updateTask}
|
||||||
|
removeTask={removeTask}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { Task } from './task';
|
|
||||||
import { useTaskState } from '../contexts/task-context';
|
|
||||||
|
|
||||||
export const Tasks = () => {
|
|
||||||
const tasks = useTaskState();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ul>
|
|
||||||
{tasks.map((task) => (
|
|
||||||
<Task key={task.id} task={task} />
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
import {
|
|
||||||
createContext,
|
|
||||||
useReducer,
|
|
||||||
useMemo,
|
|
||||||
useEffect,
|
|
||||||
type PropsWithChildren,
|
|
||||||
useContext,
|
|
||||||
useCallback,
|
|
||||||
} from 'react';
|
|
||||||
|
|
||||||
import { taskReducer, initialState } from '../reducer';
|
|
||||||
import { bindActionCreators } from '../actions';
|
|
||||||
import * as api from '../api';
|
|
||||||
import type { Task, TaskContextProps } from '../types';
|
|
||||||
|
|
||||||
const TaskContext = createContext<TaskContextProps | undefined>(undefined);
|
|
||||||
|
|
||||||
const TaskProvider = ({ children }: PropsWithChildren) => {
|
|
||||||
const [state, dispatch] = useReducer(taskReducer, initialState);
|
|
||||||
const { setLoading, setError, ...actions } = bindActionCreators(dispatch);
|
|
||||||
|
|
||||||
// Fetch all tasks
|
|
||||||
const getAllTasks = useCallback(async () => {
|
|
||||||
setLoading();
|
|
||||||
try {
|
|
||||||
const tasks = await api.all();
|
|
||||||
actions.setTasks(tasks);
|
|
||||||
} catch (error) {
|
|
||||||
setError(error, 'Failed to fetch tasks');
|
|
||||||
}
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
// Add a new task
|
|
||||||
const addTask = useCallback(
|
|
||||||
async (title: string) => {
|
|
||||||
setLoading();
|
|
||||||
try {
|
|
||||||
const task = await api.add(title);
|
|
||||||
actions.addTask(task);
|
|
||||||
} catch (error) {
|
|
||||||
setError(error, 'Failed to add task');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[dispatch],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update a task
|
|
||||||
const updateTask = useCallback(
|
|
||||||
async (id: string, updatedTask: Partial<Task>) => {
|
|
||||||
setLoading();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await api.update(id, updatedTask);
|
|
||||||
actions.updateTask(id, updatedTask);
|
|
||||||
} catch (error) {
|
|
||||||
setError(error, 'Failed to update task');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[dispatch],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Delete a task
|
|
||||||
const removeTask = useCallback(
|
|
||||||
async (id: string) => {
|
|
||||||
setLoading();
|
|
||||||
try {
|
|
||||||
await api.remove(id);
|
|
||||||
actions.removeTask(id);
|
|
||||||
} catch (error) {
|
|
||||||
setError(error, 'Failed to delete task');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[dispatch],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getAllTasks();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TaskContext.Provider
|
|
||||||
value={{
|
|
||||||
tasks: state.tasks,
|
|
||||||
loading: state.loading,
|
|
||||||
error: state.error,
|
|
||||||
addTask,
|
|
||||||
updateTask,
|
|
||||||
removeTask,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</TaskContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const useTaskState = () => {
|
|
||||||
const context = useContext(TaskContext);
|
|
||||||
|
|
||||||
if (!context) {
|
|
||||||
throw new Error('useTaskContext must be used within a TaskProvider');
|
|
||||||
}
|
|
||||||
|
|
||||||
return context.tasks;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useTaskActions = () => {
|
|
||||||
const context = useContext(TaskContext);
|
|
||||||
|
|
||||||
if (!context) {
|
|
||||||
throw new Error('useTaskContext must be used within a TaskProvider');
|
|
||||||
}
|
|
||||||
|
|
||||||
const actions = useMemo(
|
|
||||||
() => ({
|
|
||||||
addTask: context.addTask,
|
|
||||||
updateTask: context.updateTask,
|
|
||||||
removeTask: context.removeTask,
|
|
||||||
}),
|
|
||||||
[context.addTask, context.updateTask, context.removeTask],
|
|
||||||
);
|
|
||||||
|
|
||||||
return actions;
|
|
||||||
};
|
|
||||||
|
|
||||||
export { TaskContext, TaskProvider, useTaskState };
|
|
||||||
@@ -2,7 +2,7 @@ import { StrictMode } from 'react';
|
|||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import { Application } from './components/application';
|
import { Application } from './components/application';
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<Application />
|
<Application />
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
@@ -8,15 +8,6 @@ export type Task = {
|
|||||||
|
|
||||||
export type TaskData = Partial<Omit<Task, 'id'>>;
|
export type TaskData = Partial<Omit<Task, 'id'>>;
|
||||||
|
|
||||||
export interface TaskContextProps {
|
|
||||||
tasks: Task[];
|
|
||||||
loading: boolean;
|
|
||||||
error: string | null;
|
|
||||||
addTask: (title: string) => void;
|
|
||||||
updateTask: (id: string, updatedTask: Partial<Omit<Task, 'id'>>) => void;
|
|
||||||
removeTask: (id: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TaskState = {
|
export type TaskState = {
|
||||||
tasks: Task[];
|
tasks: Task[];
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
|||||||
Reference in New Issue
Block a user