Add task list demo application
This commit is contained in:
14
examples/task-list/src/application.tsx
Normal file
14
examples/task-list/src/application.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { CreateTask } from './create-task';
|
||||
import { TaskProvider } from './task-context';
|
||||
import { Tasks } from './tasks';
|
||||
|
||||
export const Application = () => {
|
||||
return (
|
||||
<TaskProvider>
|
||||
<main className="container max-w-xl my-10 space-y-8">
|
||||
<CreateTask onSubmit={(title) => console.log(title)} />
|
||||
<Tasks />
|
||||
</main>
|
||||
</TaskProvider>
|
||||
);
|
||||
};
|
||||
43
examples/task-list/src/create-task.tsx
Normal file
43
examples/task-list/src/create-task.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { useState } from 'react';
|
||||
import { useTaskContext } from './task-context';
|
||||
|
||||
type CreateTaskProps = {
|
||||
onSubmit: (title: string) => void;
|
||||
};
|
||||
|
||||
export const CreateTask = ({ onSubmit }: CreateTaskProps) => {
|
||||
const { addTask } = useTaskContext();
|
||||
const [title, setTitle] = useState('');
|
||||
|
||||
return (
|
||||
<form
|
||||
method="POST"
|
||||
action="/api/tasks"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
addTask(title);
|
||||
setTitle('');
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<label htmlFor="new-task-title" className="sr-only">
|
||||
Title
|
||||
</label>
|
||||
<div className="flex">
|
||||
<input
|
||||
type="text"
|
||||
name="title"
|
||||
id="new-task-title"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="What do you need to get done?"
|
||||
required
|
||||
/>
|
||||
<button type="submit" disabled={!title}>
|
||||
Create Task
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
15
examples/task-list/src/date-time.tsx
Normal file
15
examples/task-list/src/date-time.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
export const DateTime = ({ date, title }: { date: Date; title: string }) => {
|
||||
return (
|
||||
<div className="flex gap-2 text-xs sm:flex-row">
|
||||
<h3 className="font-semibold sm:after:content-[':'] after:text-gray-900 text-primary-800">
|
||||
{title}
|
||||
</h3>
|
||||
<p>
|
||||
{date.toLocaleString(undefined, {
|
||||
dateStyle: 'short',
|
||||
timeStyle: 'short',
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
9
examples/task-list/src/index.tsx
Normal file
9
examples/task-list/src/index.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { Application } from './application';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<Application />
|
||||
</StrictMode>,
|
||||
);
|
||||
131
examples/task-list/src/task-context.tsx
Normal file
131
examples/task-list/src/task-context.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import {
|
||||
createContext,
|
||||
useReducer,
|
||||
useEffect,
|
||||
type ReactNode,
|
||||
useContext,
|
||||
} from 'react';
|
||||
import { TasksActions, taskReducer, initialState } from './task-reducer';
|
||||
import type { Task } from '../types';
|
||||
|
||||
interface TaskContextProps {
|
||||
tasks: Task[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
addTask: (title: string) => void;
|
||||
updateTask: (id: string, updatedTask: Partial<Task>) => void;
|
||||
deleteTask: (id: string) => void;
|
||||
}
|
||||
|
||||
const TaskContext = createContext<TaskContextProps | undefined>(undefined);
|
||||
|
||||
interface TaskProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const TaskProvider = ({ children }: TaskProviderProps) => {
|
||||
const [state, dispatch] = useReducer(taskReducer, initialState);
|
||||
|
||||
// Fetch all tasks
|
||||
const fetchTasks = async () => {
|
||||
dispatch({ type: TasksActions.SET_LOADING });
|
||||
try {
|
||||
const response = await fetch('/api/tasks');
|
||||
const data = await response.json();
|
||||
dispatch({ type: TasksActions.FETCH_TASKS, payload: data });
|
||||
} catch (error) {
|
||||
dispatch({
|
||||
type: TasksActions.SET_ERROR,
|
||||
payload: 'Failed to fetch tasks',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Add a new task
|
||||
const addTask = async (title: string) => {
|
||||
dispatch({ type: TasksActions.SET_LOADING });
|
||||
try {
|
||||
const response = await fetch('/api/tasks', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ title }),
|
||||
});
|
||||
const data = await response.json();
|
||||
dispatch({ type: TasksActions.ADD_TASK, payload: data });
|
||||
} catch (error) {
|
||||
dispatch({ type: TasksActions.SET_ERROR, payload: 'Failed to add task' });
|
||||
}
|
||||
};
|
||||
|
||||
// Update a task
|
||||
const updateTask = async (id: string, updatedTask: Partial<Task>) => {
|
||||
dispatch({ type: TasksActions.SET_LOADING });
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/tasks/${id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(updatedTask),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to update task');
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: TasksActions.UPDATE_TASK,
|
||||
payload: { id, ...updatedTask },
|
||||
});
|
||||
} catch (error) {
|
||||
dispatch({
|
||||
type: TasksActions.SET_ERROR,
|
||||
payload: 'Failed to update task',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Delete a task
|
||||
const deleteTask = async (id: string) => {
|
||||
dispatch({ type: TasksActions.SET_LOADING });
|
||||
try {
|
||||
await fetch(`/api/tasks/${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
dispatch({ type: TasksActions.DELETE_TASK, payload: id });
|
||||
} catch (error) {
|
||||
dispatch({
|
||||
type: TasksActions.SET_ERROR,
|
||||
payload: 'Failed to delete task',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchTasks();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<TaskContext.Provider
|
||||
value={{
|
||||
tasks: state.tasks,
|
||||
loading: state.loading,
|
||||
error: state.error,
|
||||
addTask,
|
||||
updateTask,
|
||||
deleteTask,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TaskContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const useTaskContext = () => {
|
||||
const context = useContext(TaskContext);
|
||||
if (!context) {
|
||||
throw new Error('useTaskContext must be used within a TaskProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export { TaskContext, TaskProvider, useTaskContext };
|
||||
60
examples/task-list/src/task-reducer.ts
Normal file
60
examples/task-list/src/task-reducer.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import type { Task } from '../types';
|
||||
|
||||
export enum TasksActions {
|
||||
FETCH_TASKS = 'fetch-tasks',
|
||||
ADD_TASK = 'add-task',
|
||||
UPDATE_TASK = 'update-task',
|
||||
DELETE_TASK = 'delete-task',
|
||||
SET_LOADING = 'set-loading',
|
||||
SET_ERROR = 'set-error',
|
||||
}
|
||||
|
||||
export interface TaskState {
|
||||
tasks: Task[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export interface Action {
|
||||
type: TasksActions;
|
||||
payload?: any;
|
||||
}
|
||||
|
||||
export const initialState: TaskState = {
|
||||
tasks: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
};
|
||||
|
||||
export const taskReducer = (state: TaskState, action: Action): TaskState => {
|
||||
switch (action.type) {
|
||||
case TasksActions.FETCH_TASKS:
|
||||
return { ...state, tasks: action.payload, loading: false };
|
||||
case TasksActions.ADD_TASK:
|
||||
return {
|
||||
...state,
|
||||
tasks: [...state.tasks, action.payload],
|
||||
loading: false,
|
||||
};
|
||||
case TasksActions.UPDATE_TASK:
|
||||
return {
|
||||
...state,
|
||||
tasks: state.tasks.map((task) =>
|
||||
task.id === action.payload.id ? { ...task, ...action.payload } : task,
|
||||
),
|
||||
loading: false,
|
||||
};
|
||||
case TasksActions.DELETE_TASK:
|
||||
return {
|
||||
...state,
|
||||
tasks: state.tasks.filter((task) => task.id !== action.payload),
|
||||
loading: false,
|
||||
};
|
||||
case TasksActions.SET_LOADING:
|
||||
return { ...state, loading: true };
|
||||
case TasksActions.SET_ERROR:
|
||||
return { ...state, error: action.payload, loading: false };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
40
examples/task-list/src/task.tsx
Normal file
40
examples/task-list/src/task.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { DateTime } from './date-time';
|
||||
import { useTaskContext } from './task-context';
|
||||
|
||||
type TaskProps = {
|
||||
task: import('../types').Task;
|
||||
};
|
||||
|
||||
export const Task = ({ task }: TaskProps) => {
|
||||
const { updateTask, deleteTask } = useTaskContext();
|
||||
|
||||
return (
|
||||
<li className="block p-4 space-y-2 border-t-2 border-x-2 last:border-b-2">
|
||||
<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 w-6 h-6"
|
||||
checked={task.completed}
|
||||
onChange={() => updateTask(task.id, { completed: !task.completed })}
|
||||
/>
|
||||
<h2 className="w-full font-semibold">{task.title}</h2>
|
||||
<button
|
||||
className="py-1 px-1.5 text-xs button-destructive button-ghost"
|
||||
onClick={() => {
|
||||
deleteTask(task.id);
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</header>
|
||||
<div className="flex flex-col md:gap-2 md:flex-row">
|
||||
<DateTime date={task.createdAt} title="Created" />
|
||||
<DateTime date={task.lastModified} title="Modified" />
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
16
examples/task-list/src/tasks.tsx
Normal file
16
examples/task-list/src/tasks.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Task } from './task';
|
||||
import { useTaskContext } from './task-context';
|
||||
|
||||
export const Tasks = () => {
|
||||
const { tasks } = useTaskContext();
|
||||
|
||||
return (
|
||||
<section>
|
||||
<ul>
|
||||
{tasks.map((task) => (
|
||||
<Task key={task.id} task={task} />
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user