├── src
├── react-app-env.d.ts
├── lib
│ └── useForm.ts
├── pages
│ ├── Posts
│ │ └── Posts.tsx
│ ├── NotFound
│ │ └── NotFound.tsx
│ ├── Home
│ │ └── Home.tsx
│ ├── ErrorFallback
│ │ └── ErrorFallback.tsx
│ ├── Settings
│ │ └── Settings.tsx
│ ├── Login
│ │ └── Login.tsx
│ ├── Register
│ │ └── Register.tsx
│ └── Profile
│ │ └── Profile.tsx
├── test
│ ├── server
│ │ ├── browser.ts
│ │ ├── server.ts
│ │ ├── handlers
│ │ │ ├── index.ts
│ │ │ ├── users.ts
│ │ │ ├── auth.ts
│ │ │ ├── comments.ts
│ │ │ └── posts.ts
│ │ ├── index.ts
│ │ ├── utils.ts
│ │ └── db.ts
│ ├── data-generators.ts
│ └── test-utils.tsx
├── components
│ ├── Form
│ │ ├── index.ts
│ │ ├── FieldWrapper.tsx
│ │ ├── TextareaField.tsx
│ │ ├── InputField.tsx
│ │ └── DebouncedInputField.tsx
│ ├── Elements
│ │ └── Spinner
│ │ │ └── PageSpinner.tsx
│ ├── Head
│ │ └── Head.tsx
│ ├── NavItem
│ │ └── NavItem.tsx
│ ├── Header
│ │ ├── __tests__
│ │ │ └── Header.test.tsx
│ │ └── Header.tsx
│ ├── Pagination
│ │ └── Pagination.tsx
│ └── Footer
│ │ └── Footer.tsx
├── config
│ └── index.ts
├── App.tsx
├── features
│ ├── auth
│ │ ├── hooks
│ │ │ ├── useIsCurrentUser.ts
│ │ │ ├── useRedirectAfterLogin.ts
│ │ │ ├── useAuth.ts
│ │ │ ├── useInitAuth.ts
│ │ │ ├── __tests__
│ │ │ │ ├── useRedirectAfterLogin.ts
│ │ │ │ ├── useIsCurrentUser.test.ts
│ │ │ │ ├── useAuth.test.tsx
│ │ │ │ └── useInitAuth.test.ts
│ │ │ ├── useLoginUser.ts
│ │ │ └── useRegisterUser.ts
│ │ ├── providers
│ │ │ └── AuthProvider.tsx
│ │ ├── api
│ │ │ ├── registerApi.ts
│ │ │ └── loginApi.ts
│ │ ├── index.ts
│ │ ├── types
│ │ │ └── auth.ts
│ │ ├── components
│ │ │ ├── __tests__
│ │ │ │ ├── LoginForm.test.tsx
│ │ │ │ └── RegisterForm.test.tsx
│ │ │ ├── LoginForm.tsx
│ │ │ └── RegisterForm.tsx
│ │ └── stores
│ │ │ └── authSlice.ts
│ ├── users
│ │ ├── types
│ │ │ ├── user.ts
│ │ │ └── settings.ts
│ │ ├── index.ts
│ │ ├── hooks
│ │ │ ├── useProfileDetails.ts
│ │ │ └── useUpdateSettings.ts
│ │ ├── components
│ │ │ ├── UserPosts.tsx
│ │ │ ├── __tests__
│ │ │ │ ├── ProfileDetails.test.tsx
│ │ │ │ ├── UserPosts.test.tsx
│ │ │ │ └── SettingsForm.test.tsx
│ │ │ ├── ProfileDetails.tsx
│ │ │ └── SettingsForm.tsx
│ │ └── api
│ │ │ └── userApi.ts
│ ├── post
│ │ ├── components
│ │ │ ├── TagList.tsx
│ │ │ ├── PostsList.tsx
│ │ │ ├── PostPreview.tsx
│ │ │ ├── PostDetails.tsx
│ │ │ └── PostEditor.tsx
│ │ ├── pages
│ │ │ ├── CreatePost.tsx
│ │ │ ├── UpdatePost.tsx
│ │ │ ├── __tests__
│ │ │ │ ├── CreatePost.test.tsx
│ │ │ │ ├── UpdatePost.test.tsx
│ │ │ │ ├── PostPage.test.tsx
│ │ │ │ └── PostsListPage.test.tsx
│ │ │ ├── PostsListPage.tsx
│ │ │ └── PostPage.tsx
│ │ ├── index.ts
│ │ ├── hooks
│ │ │ ├── useDeletePost.ts
│ │ │ ├── usePostEditor.ts
│ │ │ └── useSearchPostsWithPagination.ts
│ │ ├── types
│ │ │ └── index.ts
│ │ ├── routes
│ │ │ └── PostRoutes.tsx
│ │ └── api
│ │ │ └── postApi.ts
│ └── comments
│ │ ├── hooks
│ │ ├── useUpdateFormVisibility.ts
│ │ ├── useDeleteComment.ts
│ │ ├── useUpdateComment.ts
│ │ └── useCreateComponent.ts
│ │ ├── index.ts
│ │ ├── components
│ │ ├── Comments.tsx
│ │ ├── CommentsList.tsx
│ │ ├── UpdateCommentForm.tsx
│ │ ├── CreateComment.tsx
│ │ ├── CommentCard.tsx
│ │ └── __tests__
│ │ │ └── Comments.test.tsx
│ │ ├── types
│ │ └── index.ts
│ │ └── api
│ │ └── commentsApi.ts
├── hooks
│ ├── store.ts
│ └── useDebounce.ts
├── layouts
│ ├── MainLayout.tsx
│ ├── ContentLayout.tsx
│ └── ErrorPageLayout.tsx
├── utils
│ └── storage.ts
├── reportWebVitals.ts
├── routes
│ ├── index.tsx
│ ├── public.tsx
│ ├── common.tsx
│ └── protected.tsx
├── setupTests.ts
├── api
│ ├── api.ts
│ └── utils.ts
├── index.tsx
├── stores
│ └── store.ts
├── providers
│ └── AppProvider.tsx
└── assets
│ └── logo.svg
├── public
├── robots.txt
├── favicon.ico
├── logo192.png
├── logo512.png
├── manifest.json
├── index.html
└── mockServiceWorker.js
├── .env.example
├── .prettierrc.json
├── tsconfig.paths.json
├── cypress
├── .eslintrc.js
├── fixtures
│ └── example.json
├── tsconfig.json
├── global.d.ts
├── support
│ ├── e2e.ts
│ └── commands.ts
└── e2e
│ └── smoke.cy.ts
├── cypress.config.ts
├── .gitignore
├── tsconfig.json
├── craco.config.js
├── .eslintrc.js
├── LICENSE
├── docs
├── application-overview.md
├── testing.md
├── performance.md
├── project-configuration.md
├── project-architecture.md
└── state-management.md
├── .github
└── workflows
│ └── ci.yml
├── README.md
└── package.json
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luismarques-io/best-practices-app-react/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luismarques-io/best-practices-app-react/HEAD/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luismarques-io/best-practices-app-react/HEAD/public/logo512.png
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | REACT_APP_API_URL=https://dummyjson.com/
2 | REACT_APP_API_MOCKING=true
3 | REACT_APP_API_MOCKING_DB_SEED=true
4 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 2,
3 | "printWidth": 120,
4 | "singleQuote": true,
5 | "jsxSingleQuote": true
6 | }
7 |
--------------------------------------------------------------------------------
/src/lib/useForm.ts:
--------------------------------------------------------------------------------
1 | import { useForm as useReactHookForm } from 'react-hook-form';
2 |
3 | export const useForm = useReactHookForm;
4 |
--------------------------------------------------------------------------------
/src/pages/Posts/Posts.tsx:
--------------------------------------------------------------------------------
1 | import { PostRoutes } from '@/features/post';
2 |
3 | export function Posts() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/tsconfig.paths.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "paths": {
5 | "@/*": ["./src/*"]
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/test/server/browser.ts:
--------------------------------------------------------------------------------
1 | import { setupWorker } from 'msw';
2 |
3 | import { handlers } from './handlers';
4 |
5 | export const worker = setupWorker(...handlers);
6 |
--------------------------------------------------------------------------------
/src/components/Form/index.ts:
--------------------------------------------------------------------------------
1 | export * from './InputField';
2 | export * from './DebouncedInputField';
3 | export * from './TextareaField';
4 | export * from './FieldWrapper';
5 |
--------------------------------------------------------------------------------
/src/test/server/server.ts:
--------------------------------------------------------------------------------
1 | import { setupServer } from 'msw/node';
2 |
3 | import { handlers } from './handlers';
4 |
5 | export const server = setupServer(...handlers);
6 |
--------------------------------------------------------------------------------
/cypress/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | plugins: ['eslint-plugin-cypress'],
4 | parser: '@typescript-eslint/parser',
5 | env: { 'cypress/globals': true },
6 | };
7 |
--------------------------------------------------------------------------------
/cypress/fixtures/example.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Using fixtures to represent data",
3 | "email": "hello@cypress.io",
4 | "body": "Fixtures are a great way to mock data for responses to routes"
5 | }
6 |
--------------------------------------------------------------------------------
/cypress.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'cypress';
2 |
3 | export default defineConfig({
4 | e2e: {
5 | setupNodeEvents(on, config) {
6 | // implement node event listeners here
7 | },
8 | },
9 | });
10 |
--------------------------------------------------------------------------------
/src/config/index.ts:
--------------------------------------------------------------------------------
1 | export const API_URL = process.env.REACT_APP_API_URL as string;
2 | export const STORAGE_PREFIX = 'best_practices_app_';
3 | export const APP_TITLE = 'Best Practices App';
4 | export const JWT_SECRET = '123456';
5 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { AppRoutes } from '@/routes';
2 | import { MainLayout } from '@/layouts/MainLayout';
3 |
4 | export function App() {
5 | return (
6 |
7 |
8 |
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/cypress/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "esModuleInterop": true,
4 | "target": "es5",
5 | "lib": ["es5", "dom"],
6 | "types": ["node", "cypress", "@testing-library/cypress"]
7 | },
8 | "include": ["**/*.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/src/features/auth/hooks/useIsCurrentUser.ts:
--------------------------------------------------------------------------------
1 | import { useAuth } from './useAuth';
2 |
3 | export const useIsCurrentUser = (userId: string | undefined) => {
4 | const { user } = useAuth();
5 | return Boolean(userId && user && user.id === userId);
6 | };
7 |
--------------------------------------------------------------------------------
/cypress/global.d.ts:
--------------------------------------------------------------------------------
1 | // add new command to the existing Cypress interface
2 | declare global {
3 | namespace Cypress {
4 | interface Chainable {
5 | checkAndDismissNotification: (matcher: RegExp | string) => void;
6 | }
7 | }
8 | }
9 |
10 | export {};
11 |
--------------------------------------------------------------------------------
/src/test/server/handlers/index.ts:
--------------------------------------------------------------------------------
1 | import { authHandlers } from './auth';
2 | import { usersHandlers } from './users';
3 | import { commentsHandlers } from './comments';
4 | import { postsHandlers } from './posts';
5 |
6 | export const handlers = [...authHandlers, ...usersHandlers, ...commentsHandlers, ...postsHandlers];
7 |
--------------------------------------------------------------------------------
/src/features/users/types/user.ts:
--------------------------------------------------------------------------------
1 | export type GetUserByIdDTO = {
2 | userId: string;
3 | };
4 |
5 | export type UserResponse = {
6 | id: string;
7 | username: string;
8 | email: string;
9 | firstName: string;
10 | lastName: string;
11 | gender: string;
12 | image: string;
13 | password?: string;
14 | };
15 |
--------------------------------------------------------------------------------
/src/pages/NotFound/NotFound.tsx:
--------------------------------------------------------------------------------
1 | import { ErrorPageLayout } from '@/layouts/ErrorPageLayout';
2 |
3 | export function NotFound() {
4 | return (
5 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/Elements/Spinner/PageSpinner.tsx:
--------------------------------------------------------------------------------
1 | export const PageSpinner = () => {
2 | return (
3 |
4 |
5 | Loading...
6 |
7 |
8 | );
9 | };
10 |
--------------------------------------------------------------------------------
/src/features/users/index.ts:
--------------------------------------------------------------------------------
1 | export * from './api/userApi';
2 |
3 | export * from './components/SettingsForm';
4 | export * from './components/ProfileDetails';
5 | export * from './components/UserPosts';
6 |
7 | export * from './hooks/useUpdateSettings';
8 | export * from './hooks/useProfileDetails';
9 |
10 | export * from './types/settings';
11 | export * from './types/user';
12 |
--------------------------------------------------------------------------------
/src/features/post/components/TagList.tsx:
--------------------------------------------------------------------------------
1 | export function TagList({ tags }: Readonly<{ tags: string[] }>) {
2 | // Remove duplicate tags
3 | tags = Array.from(new Set(tags));
4 | return (
5 | <>
6 | {tags.map((tag) => (
7 |
8 | {tag}
9 |
10 | ))}
11 | >
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/src/pages/Home/Home.tsx:
--------------------------------------------------------------------------------
1 | import { Navigate } from 'react-router-dom';
2 | import { Head } from '@/components/Head/Head';
3 | import { ContentLayout } from '@/layouts/ContentLayout';
4 |
5 | export function Home() {
6 | return (
7 | <>
8 |