Refactor task list example application
This commit is contained in:
@@ -29,12 +29,14 @@
|
|||||||
"chalk": "^5.3.0",
|
"chalk": "^5.3.0",
|
||||||
"concurrently": "^9.0.1",
|
"concurrently": "^9.0.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
"tailwind-merge": "^2.5.2",
|
||||||
"uuid": "^10.0.0",
|
"uuid": "^10.0.0",
|
||||||
"vite": "^5.4.6",
|
"vite": "^5.4.6",
|
||||||
"vitest": "^2.1.1"
|
"vitest": "^2.1.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"body-parser": "^1.20.3",
|
"body-parser": "^1.20.3",
|
||||||
"express": "^4.21.0"
|
"express": "^4.21.0",
|
||||||
|
"lucide-react": "^0.441.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { v4 as id } from 'uuid';
|
import { v4 as id } from 'uuid';
|
||||||
|
|
||||||
/** @typedef {import('../types').Task} Task */
|
/** @typedef {import('../src/types').Task} Task */
|
||||||
|
|
||||||
/** @type {Task[]} tasks - An array to store tasks. */
|
/** @type {Task[]} tasks - An array to store tasks. */
|
||||||
let tasks = [];
|
let tasks = [];
|
||||||
|
|||||||
62
examples/task-list/src/actions.ts
Normal file
62
examples/task-list/src/actions.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { getErrorMessage } from 'utility-belt';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Task,
|
||||||
|
SetTasksAction,
|
||||||
|
AddTaskAction,
|
||||||
|
UpdateTaskAction,
|
||||||
|
RemoveTaskAction,
|
||||||
|
SetLoadingAction,
|
||||||
|
SetErrorAction,
|
||||||
|
TaskAction,
|
||||||
|
TaskData,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
export const setTasks = (tasks: Task[]): SetTasksAction => ({
|
||||||
|
type: 'set-tasks',
|
||||||
|
payload: tasks,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const addTask = (task: Task): AddTaskAction => ({
|
||||||
|
type: 'add-task',
|
||||||
|
payload: task,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateTask = (id: string, task: TaskData): UpdateTaskAction => ({
|
||||||
|
type: 'update-task',
|
||||||
|
payload: { ...task, id },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const removeTask = (id: string): RemoveTaskAction => ({
|
||||||
|
type: 'remove-task',
|
||||||
|
payload: id,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const setLoading = (): SetLoadingAction => ({
|
||||||
|
type: 'set-loading',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const setError = (
|
||||||
|
error: unknown,
|
||||||
|
fallback: string = 'Unknown error',
|
||||||
|
): SetErrorAction => {
|
||||||
|
const message = getErrorMessage(error, fallback);
|
||||||
|
return { type: 'set-error', payload: message };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const bindActionCreators = (dispatch: React.Dispatch<TaskAction>) => {
|
||||||
|
return useMemo(
|
||||||
|
() => ({
|
||||||
|
setTasks: (tasks: Task[]) => dispatch(setTasks(tasks)),
|
||||||
|
addTask: (task: Task) => dispatch(addTask(task)),
|
||||||
|
updateTask: (id: string, task: TaskData) =>
|
||||||
|
dispatch(updateTask(id, task)),
|
||||||
|
removeTask: (id: string) => dispatch(removeTask(id)),
|
||||||
|
setLoading: () => dispatch(setLoading()),
|
||||||
|
setError: (error: unknown, fallback: string) =>
|
||||||
|
dispatch(setError(error, fallback)),
|
||||||
|
}),
|
||||||
|
[dispatch],
|
||||||
|
);
|
||||||
|
};
|
||||||
50
examples/task-list/src/api.ts
Normal file
50
examples/task-list/src/api.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { Task } from './types';
|
||||||
|
|
||||||
|
export const all = async (): Promise<Task[]> => {
|
||||||
|
const response = await fetch('/api/tasks');
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch tasks');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const add = async (title: string): Promise<Task> => {
|
||||||
|
const response = await fetch('/api/tasks', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ title }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to add task');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const update = async (
|
||||||
|
id: string,
|
||||||
|
updatedTask: Partial<Task>,
|
||||||
|
): Promise<void> => {
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const remove = async (id: string): Promise<void> => {
|
||||||
|
const response = await fetch(`/api/tasks/${id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to delete task');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { CreateTask } from './create-task';
|
import { CreateTask } from './create-task';
|
||||||
import { TaskProvider } from './task-context';
|
import { TaskProvider } from '../contexts/task-context';
|
||||||
import { Tasks } from './tasks';
|
import { Tasks } from './tasks';
|
||||||
|
|
||||||
export const Application = () => {
|
export const Application = () => {
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useTaskActions } from './task-context';
|
import { useTaskActions } from '../contexts/task-context';
|
||||||
|
|
||||||
type CreateTaskProps = {
|
type CreateTaskProps = {
|
||||||
onSubmit: (title: string) => void;
|
onSubmit: (title: string) => void;
|
||||||
@@ -23,7 +23,7 @@ export const CreateTask = ({ onSubmit }: CreateTaskProps) => {
|
|||||||
<label htmlFor="new-task-title" className="sr-only">
|
<label htmlFor="new-task-title" className="sr-only">
|
||||||
Title
|
Title
|
||||||
</label>
|
</label>
|
||||||
<div className="flex">
|
<div className="flex flex-col sm:flex-row">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name="title"
|
name="title"
|
||||||
26
examples/task-list/src/components/date-time.tsx
Normal file
26
examples/task-list/src/components/date-time.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import type { ComponentPropsWithoutRef } from 'react';
|
||||||
|
import { twMerge as merge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
const formatDate = new Intl.DateTimeFormat(navigator.language, {
|
||||||
|
dateStyle: 'short',
|
||||||
|
timeStyle: 'short',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const DateTime = ({
|
||||||
|
date,
|
||||||
|
title,
|
||||||
|
className,
|
||||||
|
}: ComponentPropsWithoutRef<'div'> & {
|
||||||
|
date: Date | string;
|
||||||
|
title: string;
|
||||||
|
}) => {
|
||||||
|
if (typeof date === 'string') date = new Date(date);
|
||||||
|
return (
|
||||||
|
<div className={merge('overflow-x-hidden space-x-2 text-xs', className)}>
|
||||||
|
<span className="font-semibold after:content-[':'] after:text-slate-700 text-primary-800 dark:text-primary-200 dark:after:text-slate-300">
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
<span className="whitespace-pre">{formatDate.format(date)}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
45
examples/task-list/src/components/task.tsx
Normal file
45
examples/task-list/src/components/task.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
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 p-4 space-y-2 border-t first:rounded-t-md border-x last:border-b last:rounded-b-md 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 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="button-small button-destructive button-ghost"
|
||||||
|
onClick={() => removeTask(task.id)}
|
||||||
|
>
|
||||||
|
❌
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
<label className="text-xs grid grid-cols-[1.25rem_1fr] gap-1 cursor-pointer rounded-md bg-primary-50 dark:bg-primary-950 border-2 border-primary-100 dark:border-primary-900 p-2">
|
||||||
|
<input type="checkbox" className="sr-only peer" />
|
||||||
|
<ChevronRightCircle className="block w-4 h-4 transition-transform peer-checked:rotate-90" />
|
||||||
|
<h3 className="select-none">Metadata</h3>
|
||||||
|
<div className="hidden col-span-2 gap-2 peer-checked:flex">
|
||||||
|
<DateTime date={task.createdAt} title="Created" />
|
||||||
|
<DateTime date={task.lastModified} title="Modified" />
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
});
|
||||||
14
examples/task-list/src/components/tasks.tsx
Normal file
14
examples/task-list/src/components/tasks.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
125
examples/task-list/src/contexts/task-context.tsx
Normal file
125
examples/task-list/src/contexts/task-context.tsx
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
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 };
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
export const DateTime = ({ date, title }: { date: Date; title: string }) => {
|
|
||||||
return (
|
|
||||||
<div className="flex gap-2 overflow-x-hidden text-xs sm:flex-row">
|
|
||||||
<h3 className="font-semibold sm:after:content-[':'] after:text-gray-900 text-primary-800">
|
|
||||||
{title}
|
|
||||||
</h3>
|
|
||||||
<p className="whitespace-pre">
|
|
||||||
{date.toLocaleString(undefined, {
|
|
||||||
dateStyle: 'short',
|
|
||||||
timeStyle: 'short',
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { StrictMode } from 'react';
|
import { StrictMode } from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import { Application } from './application';
|
import { Application } from './components/application';
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
|
|||||||
45
examples/task-list/src/reducer.ts
Normal file
45
examples/task-list/src/reducer.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import type { Task, TaskAction, TaskState } from './types';
|
||||||
|
|
||||||
|
export const initialState: TaskState = {
|
||||||
|
tasks: [],
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const taskReducer = (
|
||||||
|
state: TaskState,
|
||||||
|
action: TaskAction,
|
||||||
|
): TaskState => {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'set-tasks':
|
||||||
|
return { ...state, tasks: action.payload, loading: false };
|
||||||
|
case 'add-task':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
tasks: [...state.tasks, action.payload],
|
||||||
|
loading: false,
|
||||||
|
};
|
||||||
|
case 'update-task':
|
||||||
|
const { id, ...payload } = action.payload;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
tasks: state.tasks.map((task) =>
|
||||||
|
task.id === id ? { ...task, ...payload } : task,
|
||||||
|
),
|
||||||
|
loading: false,
|
||||||
|
};
|
||||||
|
case 'remove-task':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
tasks: state.tasks.filter((task) => task.id !== action.payload),
|
||||||
|
loading: false,
|
||||||
|
};
|
||||||
|
case 'set-loading':
|
||||||
|
return { ...state, loading: action.payload };
|
||||||
|
case 'set-error':
|
||||||
|
return { ...state, error: action.payload, loading: false };
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
import {
|
|
||||||
createContext,
|
|
||||||
useReducer,
|
|
||||||
useMemo,
|
|
||||||
useEffect,
|
|
||||||
type ReactNode,
|
|
||||||
useContext,
|
|
||||||
useCallback,
|
|
||||||
} 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 = useCallback(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',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
// Add a new task
|
|
||||||
const addTask = useCallback(
|
|
||||||
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',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[dispatch],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update a task
|
|
||||||
const updateTask = useCallback(
|
|
||||||
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',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[dispatch],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Delete a task
|
|
||||||
const deleteTask = useCallback(
|
|
||||||
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',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[dispatch],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchTasks();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TaskContext.Provider
|
|
||||||
value={{
|
|
||||||
tasks: state.tasks,
|
|
||||||
loading: state.loading,
|
|
||||||
error: state.error,
|
|
||||||
addTask,
|
|
||||||
updateTask,
|
|
||||||
deleteTask,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{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,
|
|
||||||
deleteTask: context.deleteTask,
|
|
||||||
}),
|
|
||||||
[context.addTask, context.updateTask, context.deleteTask],
|
|
||||||
);
|
|
||||||
|
|
||||||
return actions;
|
|
||||||
};
|
|
||||||
|
|
||||||
export { TaskContext, TaskProvider, useTaskState };
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import { DateTime } from './date-time';
|
|
||||||
import { useTaskActions } from './task-context';
|
|
||||||
|
|
||||||
type TaskProps = {
|
|
||||||
task: import('../types').Task;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Task = ({ task }: TaskProps) => {
|
|
||||||
const { updateTask, deleteTask } = useTaskActions();
|
|
||||||
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { Task } from './task';
|
|
||||||
import { useTaskState } from './task-context';
|
|
||||||
|
|
||||||
export const Tasks = () => {
|
|
||||||
const tasks = useTaskState();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section>
|
|
||||||
<ul>
|
|
||||||
{tasks.map((task) => (
|
|
||||||
<Task key={task.id} task={task} />
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
62
examples/task-list/src/types.ts
Normal file
62
examples/task-list/src/types.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
export type Task = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
completed: boolean;
|
||||||
|
createdAt: Date;
|
||||||
|
lastModified: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 = {
|
||||||
|
tasks: Task[];
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TaskAction =
|
||||||
|
| SetTasksAction
|
||||||
|
| AddTaskAction
|
||||||
|
| UpdateTaskAction
|
||||||
|
| RemoveTaskAction
|
||||||
|
| SetLoadingAction
|
||||||
|
| SetErrorAction;
|
||||||
|
|
||||||
|
export type SetTasksAction = {
|
||||||
|
type: 'set-tasks';
|
||||||
|
payload: Task[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AddTaskAction = {
|
||||||
|
type: 'add-task';
|
||||||
|
payload: Task;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateTaskAction = {
|
||||||
|
type: 'update-task';
|
||||||
|
payload: TaskData & { id: string };
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RemoveTaskAction = {
|
||||||
|
type: 'remove-task';
|
||||||
|
payload: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SetLoadingAction = {
|
||||||
|
type: 'set-loading';
|
||||||
|
payload?: never;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SetErrorAction = {
|
||||||
|
type: 'set-error';
|
||||||
|
payload: string;
|
||||||
|
};
|
||||||
@@ -3,25 +3,26 @@
|
|||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
|
body {
|
||||||
|
@apply bg-slate-50 text-slate-900 dark:bg-slate-900 dark:text-slate-50;
|
||||||
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
@apply block text-sm font-medium leading-6;
|
@apply block text-sm font-medium leading-6;
|
||||||
&[for]:has(~ input[required]) {
|
|
||||||
@apply after:content-['*'] after:text-red-600;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type='text'],
|
input[type='text'],
|
||||||
input[type='email'],
|
input[type='email'],
|
||||||
input[type='password'] {
|
input[type='password'] {
|
||||||
@apply text-sm sm:text-base block w-full rounded-md border-0 py-1.5 px-2.5 text-slate-900 shadow-sm ring-1 ring-inset ring-primary-700/20 placeholder:text-slate-400 sm:text-sm sm:leading-6 placeholder:text-primary-600;
|
@apply text-sm sm:text-base block w-full rounded-md border-0 py-1.5 px-2.5 shadow-sm ring-1 ring-inset ring-primary-600 placeholder:text-primary-600 sm:text-sm sm:leading-6 dark:bg-slate-800 dark:text-slate-50 dark:placeholder:text-slate-400;
|
||||||
@apply focus:ring-2 focus:ring-primary-600 focus:outline-none;
|
@apply focus:ring-2 focus:outline-none;
|
||||||
&:has(+ button[type='submit']) {
|
&:has(+ button[type='submit']) {
|
||||||
@apply block w-full rounded-r-none;
|
@apply block w-full rounded-r-none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type='checkbox'] {
|
input[type='checkbox'] {
|
||||||
@apply h-4 w-4 rounded border-primary-500 accent-primary-600;
|
@apply h-4 w-4 rounded border-primary-500 accent-primary-600 dark:bg-slate-800 dark:border-slate-700 dark:accent-primary-600;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
@@ -41,13 +42,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.button-primary {
|
.button-primary {
|
||||||
@apply bg-primary-600 text-white cursor-pointer ring-primary-700 transition duration-100 ease-in-out hover:bg-primary-700 active:bg-primary-800 disabled:bg-primary-600/50 disabled:hover:bg-primary-600/50 disabled:active:bg-primary-600/50 disabled:cursor-not-allowed disabled:ring-primary-700/20;
|
@apply bg-primary-600 text-white cursor-pointer ring-primary-600 transition duration-100 ease-in-out hover:bg-primary-700 active:bg-primary-800 disabled:bg-primary-600/50 disabled:hover:bg-primary-600/50 disabled:active:bg-primary-600/50 disabled:cursor-not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-small {
|
||||||
|
@apply py-1 px-1.5 text-xs;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-destructive {
|
.button-destructive {
|
||||||
@apply bg-red-600 text-white cursor-pointer ring-red-700 transition duration-100 ease-in-out hover:bg-red-700 active:bg-red-800 disabled:bg-red-600/50 disabled:hover:bg-red-600/50 disabled:active:bg-red-600/50 disabled:cursor-not-allowed disabled:ring-red-700/20;
|
@apply bg-red-600 text-white cursor-pointer ring-0 transition duration-100 ease-in-out hover:bg-red-700 active:bg-red-800 disabled:bg-red-600/50 disabled:hover:bg-red-600/50 disabled:active:bg-red-600/50 disabled:cursor-not-allowed disabled:ring-red-700/20;
|
||||||
&.button-ghost {
|
&.button-ghost {
|
||||||
@apply bg-transparent text-red-600 ring-red-600 hover:bg-red-600/10;
|
@apply bg-transparent text-red-600 ring-red-600 hover:bg-red-600/10 active:bg-red-600/20 dark:hover:bg-red-400/30 dark:active:bg-red-400/40;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
export interface Task {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
completed: boolean;
|
|
||||||
createdAt: Date;
|
|
||||||
lastModified: Date;
|
|
||||||
}
|
|
||||||
@@ -2,13 +2,15 @@ import { defineConfig } from 'vite';
|
|||||||
import react from '@vitejs/plugin-react';
|
import react from '@vitejs/plugin-react';
|
||||||
import { css } from 'css-configuration';
|
import { css } from 'css-configuration';
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
css,
|
css,
|
||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://localhost:3000',
|
target: `http://localhost:${PORT}`,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
},
|
},
|
||||||
|
|||||||
42
package-lock.json
generated
42
package-lock.json
generated
@@ -17,7 +17,8 @@
|
|||||||
"examples/basic-math",
|
"examples/basic-math",
|
||||||
"examples/characters",
|
"examples/characters",
|
||||||
"examples/accident-counter",
|
"examples/accident-counter",
|
||||||
"packages/css-configuration"
|
"packages/css-configuration",
|
||||||
|
"packages/utilities"
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"prettier": "^3.3.3"
|
"prettier": "^3.3.3"
|
||||||
@@ -111,7 +112,8 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"body-parser": "^1.20.3",
|
"body-parser": "^1.20.3",
|
||||||
"express": "^4.21.0"
|
"express": "^4.21.0",
|
||||||
|
"lucide-react": "^0.441.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/body-parser": "^1.19.5",
|
"@types/body-parser": "^1.19.5",
|
||||||
@@ -122,6 +124,7 @@
|
|||||||
"chalk": "^5.3.0",
|
"chalk": "^5.3.0",
|
||||||
"concurrently": "^9.0.1",
|
"concurrently": "^9.0.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
"tailwind-merge": "^2.5.2",
|
||||||
"uuid": "^10.0.0",
|
"uuid": "^10.0.0",
|
||||||
"vite": "^5.4.6",
|
"vite": "^5.4.6",
|
||||||
"vitest": "^2.1.1"
|
"vitest": "^2.1.1"
|
||||||
@@ -4025,7 +4028,6 @@
|
|||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/jsdom": {
|
"node_modules/jsdom": {
|
||||||
@@ -4125,7 +4127,6 @@
|
|||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||||
@@ -4151,6 +4152,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/lucide-react": {
|
||||||
|
"version": "0.441.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.441.0.tgz",
|
||||||
|
"integrity": "sha512-0vfExYtvSDhkC2lqg0zYVW1Uu9GsI4knuV9GP9by5z0Xhc4Zi5RejTxfz9LsjRmCyWVzHCJvxGKZWcRyvQCWVg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lz-string": {
|
"node_modules/lz-string": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
|
||||||
@@ -4904,7 +4914,6 @@
|
|||||||
"version": "18.3.1",
|
"version": "18.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0"
|
"loose-envify": "^1.1.0"
|
||||||
@@ -5617,6 +5626,17 @@
|
|||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
|
"node_modules/tailwind-merge": {
|
||||||
|
"version": "2.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.2.tgz",
|
||||||
|
"integrity": "sha512-kjEBm+pvD+6eAwzJL2Bi+02/9LFLal1Gs61+QB7HvTfQQ0aXwC5LGT8PEt1gS0CWKktKe6ysPTAy3cBC5MeiIg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/dcastil"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "3.4.12",
|
"version": "3.4.12",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.12.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.12.tgz",
|
||||||
@@ -5979,6 +5999,10 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/utility-belt": {
|
||||||
|
"resolved": "packages/utilities",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/utils-merge": {
|
"node_modules/utils-merge": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||||
@@ -6514,6 +6538,14 @@
|
|||||||
"typescript": "^5.6.2",
|
"typescript": "^5.6.2",
|
||||||
"vite": "^5.4.6"
|
"vite": "^5.4.6"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"packages/utilities": {
|
||||||
|
"name": "utility-belt",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.6.2"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,8 @@
|
|||||||
"examples/basic-math",
|
"examples/basic-math",
|
||||||
"examples/characters",
|
"examples/characters",
|
||||||
"examples/accident-counter",
|
"examples/accident-counter",
|
||||||
"packages/css-configuration"
|
"packages/css-configuration",
|
||||||
|
"packages/utilities"
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"prettier": "^3.3.3"
|
"prettier": "^3.3.3"
|
||||||
|
|||||||
3
packages/utilities/index.d.ts
vendored
Normal file
3
packages/utilities/index.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export type RequireOnly<T, Keys extends keyof T = keyof T> = Partial<T> & Required<Pick<T, Keys>>;
|
||||||
|
export declare const isError: (error: unknown) => error is Error;
|
||||||
|
export declare const getErrorMessage: (error: unknown, defaultMessage: string) => string;
|
||||||
16
packages/utilities/index.js
Normal file
16
packages/utilities/index.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {unknown} error The error object to check.
|
||||||
|
* @returns {error is Error}
|
||||||
|
*/
|
||||||
|
export const isError = (error) => error instanceof Error;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the error message from an error object or return a default message.
|
||||||
|
* @param {unknown} error The error object to check.
|
||||||
|
* @param {string} defaultMessage The default message to return if the error is not an instance of Error.
|
||||||
|
* @returns {string} The error message or the default message.
|
||||||
|
*/
|
||||||
|
export const getErrorMessage = (error, defaultMessage) => {
|
||||||
|
return isError(error) ? error.message : defaultMessage;
|
||||||
|
};
|
||||||
23
packages/utilities/package.json
Normal file
23
packages/utilities/package.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "utility-belt",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Some little utilities",
|
||||||
|
"main": "index.js",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"types": "tsc --emitDeclarationOnly --allowJs --declaration --skipLibCheck index.js"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/stevekinney/testing-javascript.git"
|
||||||
|
},
|
||||||
|
"author": "Steve Kinney <hello@stevekinney.net>",
|
||||||
|
"license": "MIT",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/stevekinney/testing-javascript/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/stevekinney/testing-javascript#readme",
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.6.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user