├── 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 | 9 | 10 | 11 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/hooks/store.ts: -------------------------------------------------------------------------------- 1 | import { useDispatch, useSelector } from 'react-redux'; 2 | import type { TypedUseSelectorHook } from 'react-redux'; 3 | import type { RootState, AppDispatch } from '@/stores/store'; 4 | 5 | // Use throughout your app instead of plain `useDispatch` and `useSelector` 6 | export const useAppDispatch: () => AppDispatch = useDispatch; 7 | export const useAppSelector: TypedUseSelectorHook = useSelector; 8 | -------------------------------------------------------------------------------- /src/features/post/components/PostsList.tsx: -------------------------------------------------------------------------------- 1 | import { PostForPreview } from '../types'; 2 | import { PostPreview } from './PostPreview'; 3 | 4 | type PostsListProps = { 5 | posts: PostForPreview[]; 6 | }; 7 | 8 | export const PostsList = ({ posts }: PostsListProps) => { 9 | return ( 10 | <> 11 | {posts.map((post) => ( 12 | 13 | ))} 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /src/layouts/MainLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Header } from '@/components/Header/Header'; 3 | import { Footer } from '@/components/Footer/Footer'; 4 | 5 | type MainLayoutProps = { 6 | children: React.ReactNode; 7 | }; 8 | 9 | export const MainLayout = ({ children }: MainLayoutProps) => { 10 | return ( 11 | <> 12 |
13 | {children} 14 |