Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ module.exports = {
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
'@typescript-eslint/no-empty-object-type': ['error', { allowWithName: 'Props$' }],
},
}
};
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v20.14.0
v20.18.0
2,378 changes: 1,123 additions & 1,255 deletions package-lock.json

Large diffs are not rendered by default.

70 changes: 35 additions & 35 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"url": "https://github.com/leanstacks/skeleton-ui-react"
},
"scripts": {
"dev": "vite --open",
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview",
Expand All @@ -22,51 +22,51 @@
"test:ci": "vitest run --coverage --silent"
},
"dependencies": {
"@codesandbox/sandpack-react": "2.14.2",
"@codesandbox/sandpack-react": "2.19.9",
"@leanstacks/react-common": "1.0.0",
"@react-spring/web": "9.7.3",
"@tanstack/react-query": "5.45.0",
"@tanstack/react-query-devtools": "5.45.0",
"@tanstack/react-table": "8.17.3",
"axios": "1.7.2",
"@react-spring/web": "9.7.5",
"@tanstack/react-query": "5.59.15",
"@tanstack/react-query-devtools": "5.59.15",
"@tanstack/react-table": "8.20.5",
"axios": "1.7.7",
"classnames": "2.5.1",
"dayjs": "1.11.11",
"dayjs": "1.11.13",
"formik": "2.4.6",
"i18next": "23.11.5",
"i18next": "23.16.2",
"i18next-browser-languagedetector": "8.0.0",
"lodash": "4.17.21",
"qs": "6.12.1",
"qs": "6.13.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-i18next": "14.1.2",
"react-router-dom": "6.23.1",
"tailwindcss": "3.4.4",
"react-i18next": "15.1.0",
"react-router-dom": "6.27.0",
"tailwindcss": "3.4.14",
"uuid": "10.0.0",
"yup": "1.4.0"
},
"devDependencies": {
"@testing-library/react": "16.0.0",
"@testing-library/react": "16.0.1",
"@testing-library/user-event": "14.5.2",
"@types/lodash": "4.17.5",
"@types/qs": "6.9.15",
"@types/react": "18.3.3",
"@types/react-dom": "18.3.0",
"@types/uuid": "9.0.8",
"@typescript-eslint/eslint-plugin": "7.13.0",
"@typescript-eslint/parser": "7.13.0",
"@vitejs/plugin-react": "4.3.1",
"@vitest/coverage-v8": "1.6.0",
"autoprefixer": "10.4.19",
"eslint": "8.57.0",
"eslint-plugin-react-hooks": "4.6.2",
"eslint-plugin-react-refresh": "0.4.7",
"jsdom": "24.1.0",
"msw": "2.3.1",
"postcss": "8.4.38",
"prettier": "3.3.2",
"prettier-plugin-tailwindcss": "0.6.4",
"typescript": "5.4.5",
"vite": "5.3.1",
"vitest": "1.6.0"
"@types/lodash": "4.17.12",
"@types/qs": "6.9.16",
"@types/react": "18.3.12",
"@types/react-dom": "18.3.1",
"@types/uuid": "10.0.0",
"@typescript-eslint/eslint-plugin": "8.11.0",
"@typescript-eslint/parser": "8.11.0",
"@vitejs/plugin-react": "4.3.3",
"@vitest/coverage-v8": "2.1.3",
"autoprefixer": "10.4.20",
"eslint": "8.57.1",
"eslint-plugin-react-hooks": "5.0.0",
"eslint-plugin-react-refresh": "0.4.13",
"jsdom": "25.0.1",
"msw": "2.5.0",
"postcss": "8.4.47",
"prettier": "3.3.3",
"prettier-plugin-tailwindcss": "0.6.8",
"typescript": "5.6.3",
"vite": "5.4.10",
"vitest": "2.1.3"
}
}
7 changes: 4 additions & 3 deletions src/api/useGetUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,16 @@ export const useGetUser = ({ userId }: UseGetUserProps): UseQueryResult<User, Er
const axios = useAxios();
const config = useConfig();

const getUser = async (id: number): Promise<User | null> => {
const getUser = async (): Promise<User | null> => {
const response = await axios.request({
url: `${config.VITE_BASE_URL_API}/users/${id}`,
url: `${config.VITE_BASE_URL_API}/users/${userId}`,
});
return response.data;
};

return useQuery({
queryKey: [QueryKeys.Users, userId],
queryFn: () => getUser(userId),
queryFn: () => getUser(),
enabled: !!userId,
});
};
9 changes: 7 additions & 2 deletions src/components/Router/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,17 @@ import DashboardPage from 'pages/DashboardPage/DashboardPage';
import SettingsPage from 'pages/SettingsPage/SettingsPage';
import AppearanceSettings from 'pages/SettingsPage/components/AppearanceSettings';
import ComponentsPage from 'pages/ComponentsPage/ComponentsPage';
import AvatarComponents from 'pages/ComponentsPage/components/AvatarComponents';
import TextComponents from 'pages/ComponentsPage/components/TextComponents';
import ButtonComponents from 'pages/ComponentsPage/components/ButtonComponents';
import BadgeComponents from 'pages/ComponentsPage/components/BadgeComponents';
import CardComponents from 'pages/ComponentsPage/components/CardComponents';
import UsersPage from 'pages/UsersPage/UsersPage';
import UserDetailLayout from 'pages/UsersPage/components/UserDetailLayout';
import UserDetail from 'pages/UsersPage/components/UserDetail';
import UserTaskList from 'pages/UsersPage/components/UserTaskList';
import UserDetailEmpty from 'pages/UsersPage/components/UserDetailEmpty';
import AvatarComponents from 'pages/ComponentsPage/components/AvatarComponents';
import UserTaskList from 'pages/UsersPage/components/UserTaskList';
import TaskDetail from 'pages/UsersPage/Tasks/components/TaskDetail';

/**
* The React Router configuration. An array of `RouteObject`.
Expand Down Expand Up @@ -109,6 +110,10 @@ export const routes: RouteObject[] = [
path: 'tasks',
element: <UserTaskList />,
},
{
path: 'tasks/:taskId',
element: <TaskDetail />,
},
],
},
],
Expand Down
19 changes: 19 additions & 0 deletions src/pages/UsersPage/Tasks/api/__tests__/useGetTask.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { describe, expect, it } from 'vitest';

import { renderHook, waitFor } from 'test/test-utils';

import { useGetTask } from '../useGetTask';

describe('useGetTask', () => {
it('should render hook successfully', async () => {
// ARRANGE
const { result } = renderHook(() => useGetTask({ taskId: 1 }));
await waitFor(() => expect(result.current.isSuccess).toBe(true));

// ASSERT
expect(result.current.isSuccess).toBe(true);
expect(result.current.isError).toBe(false);
expect(result.current.data).toBeDefined();
expect(result.current.data?.id).toEqual(1);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { renderHook, waitFor } from 'test/test-utils';
import { queryClient } from 'test/query-client';
import { todosFixture } from '__fixtures__/todos';
import { QueryKeys } from 'utils/constants';
import { Task } from '../useGetUserTasks';
import { Task } from '../../../api/useGetUserTasks';

import { useUpdateTask } from '../useUpdateTask';

Expand Down
38 changes: 38 additions & 0 deletions src/pages/UsersPage/Tasks/api/useGetTask.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { UseQueryResult, useQuery } from '@tanstack/react-query';

import { useAxios } from 'hooks/useAxios';
import { useConfig } from 'hooks/useConfig';
import { Task } from 'pages/UsersPage/api/useGetUserTasks';
import { QueryKeys } from 'utils/constants';

/**
* Properties for the `useGetTask` hook.
* @param {number} taskId - A `Task` identifier.
*/
interface UseGetTaskProps {
taskId: number;
}

/**
* An API hook which fetches a single `Task` object by the identifier attribute.
* @param {UseGetTaskProps} props - Hook properties.
* @returns Returns a `UseQueryResult` with `Task` data.
*/
export const useGetTask = ({ taskId }: UseGetTaskProps): UseQueryResult<Task> => {
const axios = useAxios();
const config = useConfig();

const getTask = async (): Promise<Task> => {
const response = await axios.request({
url: `${config.VITE_BASE_URL_API}/todos/${taskId}`,
});

return response.data;
};

return useQuery({
queryKey: [QueryKeys.Tasks, taskId],
queryFn: () => getTask(),
enabled: !!taskId,
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import reject from 'lodash/reject';

import { QueryKeys } from 'utils/constants';
import { Task } from './useGetUserTasks';
import { Task } from 'pages/UsersPage/api/useGetUserTasks';
import { useConfig } from 'hooks/useConfig';
import { useAxios } from 'hooks/useAxios';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import {
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';

import { Task } from '../api/useGetUserTasks';
import { Task } from 'pages/UsersPage/api/useGetUserTasks';
import { useUpdateTask } from '../api/useUpdateTask';
import { useToasts } from 'hooks/useToasts';
import Icon from 'components/Icon/Icon';

/**
* Propeties for the`TaskCompleteToggle` component.
* Propeties for the `TaskCompleteToggle` component.
* @param {Task} task - A Task object.
* @see {@link PropsWithClassName}
* @see {@link PropsWithTestId}
Expand Down
140 changes: 140 additions & 0 deletions src/pages/UsersPage/Tasks/components/TaskDetail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import {
Alert,
AlertVariant,
Button,
ButtonVariant,
PropsWithClassName,
PropsWithTestId,
} from '@leanstacks/react-common';
import { useNavigate, useParams } from 'react-router-dom';
import classNames from 'classnames';

import Icon from 'components/Icon/Icon';
import Text from 'components/Text/Text';
import { useGetTask } from '../api/useGetTask';
import LoaderSkeleton from 'components/Loader/LoaderSkeleton';
import { useGetUser } from 'api/useGetUser';
import LoaderSpinner from 'components/Loader/LoaderSpinner';
import Badge from 'components/Badge/Badge';

/**
* Properties for the `TaskDetail` component.
* @see {@link PropsWithClassName}
* @see {@link PropsWithTestId}
*/
interface TaskDetailProps extends PropsWithClassName, PropsWithTestId {}

/**
* The `TaskDetail` component displays the attributes of a single `Task`.
* Provides buttons and navigation to perform actions on the Task.
* @param {TaskDetailProps} props - Component properties.
* @returns {JSX.Element} JSX
*/
const TaskDetail = ({ className, testId = 'task-detail' }: TaskDetailProps): JSX.Element => {
const navigate = useNavigate();
const { taskId } = useParams();
const {
data: task,
error: taskError,
isLoading: isLoadingTask,
} = useGetTask({ taskId: Number(taskId) });
const {
data: user,
error: userError,
isLoading: isLoadingUser,
} = useGetUser({ userId: Number(task?.userId) });

return (
<div className={className} data-testid={testId}>
<div className="mb-1 flex items-center gap-1 border-b border-neutral-500/10 pb-1">
<Icon name="checklist" />
<Text variant="heading3">Task</Text>
{isLoadingTask && <LoaderSpinner iconClassName="text-xl" />}
{!!task && <div className="text-xl">{`#${task.id}`}</div>}
<div className="ms-auto">
<Icon name="edit" fill={0} className="me-2 text-xl" opticalSize={20} />
<Icon name="delete" fill={0} className="me-2 text-xl" opticalSize={20} />
<Button
variant={ButtonVariant.Text}
className="!m-0 !p-0"
title="Close"
onClick={() => navigate(-1)}
testId={`${testId}-button-close`}
>
<Icon name="close" className="text-xl" opticalSize={20} />
</Button>
</div>
</div>

{taskError && (
<Alert
variant={AlertVariant.Error}
className="mb-4 flex items-center gap-2 rounded-none"
testId={`${testId}-alert-taskError`}
>
<Icon name="error" />
{taskError.message}
</Alert>
)}

{userError && (
<Alert
variant={AlertVariant.Error}
className="mb-4 flex items-center gap-2 rounded-none"
testId={`${testId}-alert-userError`}
>
<Icon name="error" />
{userError.message}
</Alert>
)}

{isLoadingTask && (
<div data-testid={`${testId}-loader`}>
<div className="mt-4">
<LoaderSkeleton className="mb-2 h-4 w-12" />
<LoaderSkeleton className="h-5 w-80" />
</div>
<div className="mt-4">
<LoaderSkeleton className="mb-2 h-4 w-12" />
<LoaderSkeleton className="h-5 w-80" />
</div>
<div className="mt-4">
<LoaderSkeleton className="mb-2 h-4 w-12" />
<LoaderSkeleton className="h-5 w-80" />
</div>
</div>
)}

{task && (
<div data-testid={`${testId}-task`}>
<div className="mt-4">
<div className="text-xs font-bold uppercase">Title</div>
<div className="text-lg" data-testid={`${testId}-task-title`}>
{task.title}
</div>
</div>

<div className="mt-4">
<div className="text-xs font-bold uppercase">Assignee</div>
<div>
{isLoadingUser && <LoaderSkeleton className="h-4 w-40" testId="loader-user" />}
{user && <span data-testid={`${testId}-task-user-name`}>{user.name}</span>}
</div>
</div>

<div className="mt-4">
<div className="text-xs font-bold uppercase">Status</div>
<Badge
className={classNames('inline', { '!bg-blue-600': task.completed })}
testId={`${testId}-task-status`}
>
{task.completed ? 'COMPLETE' : 'INCOMPLETE'}
</Badge>
</div>
</div>
)}
</div>
);
};

export default TaskDetail;
Loading