diff --git a/examples/task-list/package.json b/examples/task-list/package.json index e6993fb..939640f 100644 --- a/examples/task-list/package.json +++ b/examples/task-list/package.json @@ -29,12 +29,14 @@ "chalk": "^5.3.0", "concurrently": "^9.0.1", "cors": "^2.8.5", + "tailwind-merge": "^2.5.2", "uuid": "^10.0.0", "vite": "^5.4.6", "vitest": "^2.1.1" }, "dependencies": { "body-parser": "^1.20.3", - "express": "^4.21.0" + "express": "^4.21.0", + "lucide-react": "^0.441.0" } } diff --git a/examples/task-list/server/tasks.js b/examples/task-list/server/tasks.js index ef28e0c..06d150b 100644 --- a/examples/task-list/server/tasks.js +++ b/examples/task-list/server/tasks.js @@ -1,6 +1,6 @@ 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. */ let tasks = []; diff --git a/examples/task-list/src/actions.ts b/examples/task-list/src/actions.ts new file mode 100644 index 0000000..ba41bb2 --- /dev/null +++ b/examples/task-list/src/actions.ts @@ -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) => { + 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], + ); +}; diff --git a/examples/task-list/src/api.ts b/examples/task-list/src/api.ts new file mode 100644 index 0000000..779cd6d --- /dev/null +++ b/examples/task-list/src/api.ts @@ -0,0 +1,50 @@ +import { Task } from './types'; + +export const all = async (): Promise => { + 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 => { + 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, +): Promise => { + 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 => { + const response = await fetch(`/api/tasks/${id}`, { + method: 'DELETE', + }); + + if (!response.ok) { + throw new Error('Failed to delete task'); + } +}; diff --git a/examples/task-list/src/application.tsx b/examples/task-list/src/components/application.tsx similarity index 85% rename from examples/task-list/src/application.tsx rename to examples/task-list/src/components/application.tsx index 3b444b4..185701a 100644 --- a/examples/task-list/src/application.tsx +++ b/examples/task-list/src/components/application.tsx @@ -1,5 +1,5 @@ import { CreateTask } from './create-task'; -import { TaskProvider } from './task-context'; +import { TaskProvider } from '../contexts/task-context'; import { Tasks } from './tasks'; export const Application = () => { diff --git a/examples/task-list/src/create-task.tsx b/examples/task-list/src/components/create-task.tsx similarity index 89% rename from examples/task-list/src/create-task.tsx rename to examples/task-list/src/components/create-task.tsx index bd80370..98b90d6 100644 --- a/examples/task-list/src/create-task.tsx +++ b/examples/task-list/src/components/create-task.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { useTaskActions } from './task-context'; +import { useTaskActions } from '../contexts/task-context'; type CreateTaskProps = { onSubmit: (title: string) => void; @@ -23,7 +23,7 @@ export const CreateTask = ({ onSubmit }: CreateTaskProps) => { -
+
& { + date: Date | string; + title: string; +}) => { + if (typeof date === 'string') date = new Date(date); + return ( +
+ + {title} + + {formatDate.format(date)} +
+ ); +}; diff --git a/examples/task-list/src/components/task.tsx b/examples/task-list/src/components/task.tsx new file mode 100644 index 0000000..7d02e17 --- /dev/null +++ b/examples/task-list/src/components/task.tsx @@ -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 ( +
  • +
    + + updateTask(task.id, { completed: !task.completed })} + /> +

    {task.title}

    + +
    + +
  • + ); +}); diff --git a/examples/task-list/src/components/tasks.tsx b/examples/task-list/src/components/tasks.tsx new file mode 100644 index 0000000..6fbc293 --- /dev/null +++ b/examples/task-list/src/components/tasks.tsx @@ -0,0 +1,14 @@ +import { Task } from './task'; +import { useTaskState } from '../contexts/task-context'; + +export const Tasks = () => { + const tasks = useTaskState(); + + return ( +
      + {tasks.map((task) => ( + + ))} +
    + ); +}; diff --git a/examples/task-list/src/contexts/task-context.tsx b/examples/task-list/src/contexts/task-context.tsx new file mode 100644 index 0000000..41ae02b --- /dev/null +++ b/examples/task-list/src/contexts/task-context.tsx @@ -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(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) => { + 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 ( + + {children} + + ); +}; + +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 }; diff --git a/examples/task-list/src/date-time.tsx b/examples/task-list/src/date-time.tsx deleted file mode 100644 index ca37067..0000000 --- a/examples/task-list/src/date-time.tsx +++ /dev/null @@ -1,15 +0,0 @@ -export const DateTime = ({ date, title }: { date: Date; title: string }) => { - return ( -
    -

    - {title} -

    -

    - {date.toLocaleString(undefined, { - dateStyle: 'short', - timeStyle: 'short', - })} -

    -
    - ); -}; diff --git a/examples/task-list/src/index.tsx b/examples/task-list/src/index.tsx index 3b207e6..baf1ffe 100644 --- a/examples/task-list/src/index.tsx +++ b/examples/task-list/src/index.tsx @@ -1,6 +1,6 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; -import { Application } from './application'; +import { Application } from './components/application'; createRoot(document.getElementById('root')!).render( diff --git a/examples/task-list/src/reducer.ts b/examples/task-list/src/reducer.ts new file mode 100644 index 0000000..51f7ac2 --- /dev/null +++ b/examples/task-list/src/reducer.ts @@ -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; + } +}; diff --git a/examples/task-list/src/task-context.tsx b/examples/task-list/src/task-context.tsx deleted file mode 100644 index 8993252..0000000 --- a/examples/task-list/src/task-context.tsx +++ /dev/null @@ -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) => void; - deleteTask: (id: string) => void; -} - -const TaskContext = createContext(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) => { - 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 ( - - {children} - - ); -}; - -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 }; diff --git a/examples/task-list/src/task-reducer.ts b/examples/task-list/src/task-reducer.ts deleted file mode 100644 index 9924547..0000000 --- a/examples/task-list/src/task-reducer.ts +++ /dev/null @@ -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; - } -}; diff --git a/examples/task-list/src/task.tsx b/examples/task-list/src/task.tsx deleted file mode 100644 index e26e10a..0000000 --- a/examples/task-list/src/task.tsx +++ /dev/null @@ -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 ( -
  • -
    - - updateTask(task.id, { completed: !task.completed })} - /> -

    {task.title}

    - -
    -
    - - -
    -
  • - ); -}; diff --git a/examples/task-list/src/tasks.tsx b/examples/task-list/src/tasks.tsx deleted file mode 100644 index 32670ba..0000000 --- a/examples/task-list/src/tasks.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { Task } from './task'; -import { useTaskState } from './task-context'; - -export const Tasks = () => { - const tasks = useTaskState(); - - return ( -
    -
      - {tasks.map((task) => ( - - ))} -
    -
    - ); -}; diff --git a/examples/task-list/src/types.ts b/examples/task-list/src/types.ts new file mode 100644 index 0000000..96ebb5a --- /dev/null +++ b/examples/task-list/src/types.ts @@ -0,0 +1,62 @@ +export type Task = { + id: string; + title: string; + completed: boolean; + createdAt: Date; + lastModified: Date; +}; + +export type TaskData = Partial>; + +export interface TaskContextProps { + tasks: Task[]; + loading: boolean; + error: string | null; + addTask: (title: string) => void; + updateTask: (id: string, updatedTask: Partial>) => 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; +}; diff --git a/examples/task-list/styles.css b/examples/task-list/styles.css index 8608921..cf30495 100644 --- a/examples/task-list/styles.css +++ b/examples/task-list/styles.css @@ -3,25 +3,26 @@ @tailwind utilities; @layer base { + body { + @apply bg-slate-50 text-slate-900 dark:bg-slate-900 dark:text-slate-50; + } + label { @apply block text-sm font-medium leading-6; - &[for]:has(~ input[required]) { - @apply after:content-['*'] after:text-red-600; - } } input[type='text'], input[type='email'], 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 focus:ring-2 focus:ring-primary-600 focus:outline-none; + @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:outline-none; &:has(+ button[type='submit']) { @apply block w-full rounded-r-none; } } 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 { @@ -41,13 +42,17 @@ } .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 { - @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 { - @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; } } } diff --git a/examples/task-list/types.ts b/examples/task-list/types.ts deleted file mode 100644 index 89759b2..0000000 --- a/examples/task-list/types.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface Task { - id: string; - title: string; - completed: boolean; - createdAt: Date; - lastModified: Date; -} diff --git a/examples/task-list/vite.config.ts b/examples/task-list/vite.config.ts index 747d49b..2530d4c 100644 --- a/examples/task-list/vite.config.ts +++ b/examples/task-list/vite.config.ts @@ -2,13 +2,15 @@ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import { css } from 'css-configuration'; +const PORT = process.env.PORT || 3000; + export default defineConfig({ plugins: [react()], css, server: { proxy: { '/api': { - target: 'http://localhost:3000', + target: `http://localhost:${PORT}`, changeOrigin: true, secure: false, }, diff --git a/package-lock.json b/package-lock.json index 97640aa..217b4da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,8 @@ "examples/basic-math", "examples/characters", "examples/accident-counter", - "packages/css-configuration" + "packages/css-configuration", + "packages/utilities" ], "devDependencies": { "prettier": "^3.3.3" @@ -111,7 +112,8 @@ "license": "MIT", "dependencies": { "body-parser": "^1.20.3", - "express": "^4.21.0" + "express": "^4.21.0", + "lucide-react": "^0.441.0" }, "devDependencies": { "@types/body-parser": "^1.19.5", @@ -122,6 +124,7 @@ "chalk": "^5.3.0", "concurrently": "^9.0.1", "cors": "^2.8.5", + "tailwind-merge": "^2.5.2", "uuid": "^10.0.0", "vite": "^5.4.6", "vitest": "^2.1.1" @@ -4025,7 +4028,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/jsdom": { @@ -4125,7 +4127,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" @@ -4151,6 +4152,15 @@ "dev": true, "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": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", @@ -4904,7 +4914,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" @@ -5617,6 +5626,17 @@ "optional": 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": { "version": "3.4.12", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.12.tgz", @@ -5979,6 +5999,10 @@ "dev": true, "license": "MIT" }, + "node_modules/utility-belt": { + "resolved": "packages/utilities", + "link": true + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -6514,6 +6538,14 @@ "typescript": "^5.6.2", "vite": "^5.4.6" } + }, + "packages/utilities": { + "name": "utility-belt", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "typescript": "^5.6.2" + } } } } diff --git a/package.json b/package.json index 8d30b74..8f2d5a0 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,8 @@ "examples/basic-math", "examples/characters", "examples/accident-counter", - "packages/css-configuration" + "packages/css-configuration", + "packages/utilities" ], "devDependencies": { "prettier": "^3.3.3" diff --git a/packages/utilities/index.d.ts b/packages/utilities/index.d.ts new file mode 100644 index 0000000..51ad903 --- /dev/null +++ b/packages/utilities/index.d.ts @@ -0,0 +1,3 @@ +export type RequireOnly = Partial & Required>; +export declare const isError: (error: unknown) => error is Error; +export declare const getErrorMessage: (error: unknown, defaultMessage: string) => string; diff --git a/packages/utilities/index.js b/packages/utilities/index.js new file mode 100644 index 0000000..07f50f6 --- /dev/null +++ b/packages/utilities/index.js @@ -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; +}; diff --git a/packages/utilities/package.json b/packages/utilities/package.json new file mode 100644 index 0000000..fe4971b --- /dev/null +++ b/packages/utilities/package.json @@ -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 ", + "license": "MIT", + "bugs": { + "url": "https://github.com/stevekinney/testing-javascript/issues" + }, + "homepage": "https://github.com/stevekinney/testing-javascript#readme", + "devDependencies": { + "typescript": "^5.6.2" + } +}