├── .babelrc ├── .devcontainer └── devcontainer.json ├── .dockerignore ├── .env.development ├── .env.development.local.example ├── .env.production ├── .env.production.local.example ├── .env.test ├── .env.test.local.example ├── .eslintignore ├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ ├── build-docker-image.yml │ ├── deploy.yml │ └── tests.yml ├── .gitignore ├── .gitpod.yml ├── .prettierignore ├── .prettierrc ├── .vscode ├── extensions.json └── settings.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile.dev ├── Dockerfile.e2e ├── Dockerfile.prod ├── Dockerfile.test ├── LICENSE ├── README.md ├── components ├── Alert │ ├── Alert.scss │ ├── Alert.tsx │ └── index.ts ├── Button │ ├── Button.scss │ ├── Button.tsx │ └── index.ts ├── CustomHead │ ├── CustomHead.tsx │ └── index.ts ├── Dropdown │ ├── Dropdown.scss │ ├── Dropdown.tsx │ └── index.ts ├── DropzoneSingle │ ├── DropzoneSingle.scss │ ├── DropzoneSingle.tsx │ └── index.ts ├── Error │ ├── ErrorCard.scss │ ├── ErrorCard.tsx │ ├── ErrorFallback.scss │ ├── ErrorFallback.tsx │ └── index.ts ├── Footer │ ├── Footer.scss │ ├── Footer.test.tsx │ ├── Footer.tsx │ └── index.ts ├── Loading │ ├── Loading.scss │ ├── Loading.tsx │ ├── Spinner.scss │ ├── Spinner.tsx │ └── index.ts ├── Navbar │ ├── NavLink.scss │ ├── NavLink.tsx │ ├── Navbar.scss │ ├── Navbar.tsx │ └── index.ts ├── NoItems │ ├── NoItems.scss │ ├── NoItems.tsx │ └── index.ts ├── Pagination │ ├── Pagination.scss │ ├── Pagination.tsx │ └── index.ts ├── PostItem │ ├── PostItem.scss │ ├── PostItem.test.tsx │ ├── PostItem.tsx │ └── index.ts ├── PreviewTheme │ ├── PreviewTheme.scss │ ├── PreviewTheme.tsx │ └── index.ts ├── ProgressBar │ ├── ProgressBar.scss │ ├── ProgressBar.tsx │ └── index.ts ├── SearchInput │ ├── SearchInput.scss │ ├── SearchInput.test.tsx │ ├── SearchInput.tsx │ └── index.ts ├── ThemeChanger │ ├── ThemeChanger.scss │ ├── ThemeChanger.test.tsx │ ├── ThemeChanger.tsx │ └── index.ts ├── UserItem │ ├── UserItem.scss │ ├── UserItem.test.tsx │ ├── UserItem.tsx │ └── index.ts └── hooks │ ├── index.ts │ ├── tests │ └── useViewport.test.ts │ ├── useDecrementPage.ts │ ├── useDetectOutsideClick.ts │ ├── useIsMounted.ts │ ├── usePrevious.ts │ └── useViewport.ts ├── docker-compose.dev.yml ├── docker-compose.e2e.yml ├── docker-compose.live.yml ├── docker-compose.prod.yml ├── docker-compose.test.yml ├── docs ├── api.md ├── cypress.md ├── demo-environments.md ├── docker.md ├── error-handling.md ├── github-actions.md ├── next-auth.md ├── nextjs.md ├── postgres.md ├── prisma.md ├── react-hook-form.md ├── react-query.md ├── readme-assets │ ├── banner-1280x640-200kb.png │ ├── banner.png │ ├── demo.mp4 │ ├── lighthouse-score.png │ └── mobile-screens │ │ ├── Screenshot1.png │ │ ├── Screenshot10.png │ │ ├── Screenshot11.png │ │ ├── Screenshot12.png │ │ ├── Screenshot13.png │ │ ├── Screenshot14.png │ │ ├── Screenshot15.png │ │ ├── Screenshot2.png │ │ ├── Screenshot3.png │ │ ├── Screenshot4.png │ │ ├── Screenshot5.png │ │ ├── Screenshot6.png │ │ ├── Screenshot7.png │ │ ├── Screenshot8.png │ │ └── Screenshot9.png ├── tailwind.md ├── tests.md ├── themes.md ├── todo.md ├── traefik.md ├── vs-code.md └── workflow.md ├── envs ├── development-docker │ ├── .env.development.docker │ └── .env.development.docker.local.example ├── development-gitpod │ ├── .env.development.gitpod │ └── .env.development.gitpod.local.example ├── production-docker │ ├── .env.production.docker │ └── .env.production.docker.local.example ├── production-live │ ├── .env.example │ ├── .env.production.live │ ├── .env.production.live.build.local.example │ ├── .env.production.live.dc │ └── .env.production.live.local.example └── test-docker │ ├── .env.test.docker │ └── .env.test.docker.local.example ├── jest.config.base.js ├── jest.config.js ├── jest.env.setup.ts ├── layouts ├── AuthLayout │ ├── AuthLayout.scss │ ├── AuthLayout.tsx │ └── index.ts └── PageLayout │ ├── PageLayout.scss │ ├── PageLayout.tsx │ └── index.ts ├── lib-client ├── constants.ts ├── imageLoaders.ts ├── permissions.ts ├── providers │ ├── ErrorBoundaryWrapper.tsx │ ├── Me.tsx │ └── SuspenseWrapper.tsx └── react-query │ ├── auth │ ├── tests │ │ ├── useCreateUser.test.ts │ │ └── useMe.test.ts │ ├── useCreateUser.ts │ └── useMe.ts │ ├── axios.ts │ ├── posts │ ├── tests │ │ ├── useCreatePost.test.ts │ │ ├── useDeletePost.test.ts │ │ ├── usePost.test.ts │ │ ├── usePosts.test.ts │ │ └── useUpdatePost.test.ts │ ├── useCreatePost.ts │ ├── useDeletePost.ts │ ├── usePost.ts │ ├── usePosts.ts │ └── useUpdatePost.ts │ ├── queryClientConfig.ts │ ├── queryKeys.ts │ ├── seed │ └── useCreateSeed.ts │ ├── useCalcIsFetching.ts │ └── users │ ├── tests │ ├── useDeleteUser.test.ts │ ├── useUpdateUser.test.ts │ ├── useUser.test.ts │ └── useUsers.test.ts │ ├── useDeleteUser.ts │ ├── useImage.ts │ ├── useUpdateUser.ts │ ├── useUser.ts │ └── useUsers.ts ├── lib-server ├── constants.ts ├── error.ts ├── middleware │ ├── auth.ts │ └── upload.ts ├── nc.ts ├── prisma.ts ├── services │ ├── auth.ts │ ├── posts.ts │ └── users.ts └── validation.ts ├── next-env.d.ts ├── next.config.js ├── nodemon.json ├── notes ├── Dockerfile.dev.simple ├── certificate.png ├── code │ ├── Backup_SuspenseWrapper.tsx │ ├── CustomHead.tsx │ ├── docker-compose.test.yml │ ├── errorHandler500.ts │ ├── hooks.tsx │ ├── jest.config.js │ ├── jest.setup.ts │ ├── package.back.json │ └── useMe.ts ├── compressImage.ts ├── docker dev with args │ ├── .env.development │ ├── Dockerfile.dev │ └── docker-compose.dev.yml ├── envs │ ├── .stackblitzrc │ ├── backup_package.json │ ├── development-codesandbox │ │ ├── .env.development.codesandbox │ │ └── .env.development.codesandbox.local.example │ ├── development-replit │ │ ├── .env.development.replit │ │ └── .env.development.replit.local.example │ ├── replit.nix │ └── sandbox.config.json ├── md │ ├── README.backup.md │ ├── README.main.old.md │ └── README.md ├── pg-config │ ├── pg_hba.conf │ └── postgresql.conf └── test │ ├── __tests__ │ ├── Footer.tsx │ └── hello.ts │ ├── hello.ts │ ├── main.test.ts │ ├── profile.test.ts │ └── todo.spec.js ├── package.json ├── pages ├── 404.tsx ├── 500.tsx ├── [username] │ ├── index.tsx │ └── post │ │ └── [id].tsx ├── _app.tsx ├── _document.tsx ├── api │ ├── auth │ │ └── [...nextauth].ts │ ├── posts │ │ ├── [id].ts │ │ └── index.ts │ ├── seed │ │ └── index.ts │ └── users │ │ ├── [id].ts │ │ ├── index.ts │ │ └── profile.ts ├── auth │ ├── login.tsx │ └── register.tsx ├── index.tsx ├── post │ ├── create │ │ └── [[...id]].tsx │ └── drafts.tsx ├── settings │ └── [[...username]].tsx └── users.tsx ├── postcss.config.js ├── prisma ├── Dockerfile.migrate ├── migrations │ ├── 20220502084226_ │ │ └── migration.sql │ └── migration_lock.toml ├── package.json ├── pg-config │ ├── pg_hba.conf │ └── postgresql.conf ├── pg-data │ └── .gitkeep ├── schema.prisma ├── seed-run.js └── seed.js ├── public ├── favicons │ ├── android-chrome-192x192.png │ ├── android-chrome-384x384.png │ ├── apple-touch-icon.png │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── mstile-150x150.png │ ├── safari-pinned-tab.svg │ └── site.webmanifest └── images │ └── banner-1280x640-200kb.png ├── scripts └── scripts.sh ├── server ├── index.ts └── utils.ts ├── stylelintrc.json ├── styles ├── components.scss ├── globals.scss ├── index.scss ├── layouts.scss ├── tw-base.scss ├── tw-components.scss ├── tw-utilities.scss └── views.scss ├── tailwind.config.js ├── test-client ├── HookWrapper.tsx ├── Wrapper.tsx ├── config │ ├── hook-form-resolver-fix.js │ ├── jest.config.js │ └── jest.setup.ts ├── server │ ├── fake-data.ts │ ├── fixtures │ │ └── image.jpg │ ├── handlers │ │ ├── auth.ts │ │ ├── error.ts │ │ ├── posts.ts │ │ └── users.ts │ └── index.ts └── test-utils.tsx ├── test-server ├── config │ ├── jest.config.integration.js │ └── jest.config.unit.js ├── singleton.ts └── test-client.ts ├── tests-api ├── integration │ └── posts.test.ts └── unit │ ├── controllers │ └── posts.test.ts │ └── services │ └── users.test.ts ├── tests-e2e ├── .eslintrc.json ├── cypress.json ├── cypress │ ├── fixtures │ │ ├── example.json │ │ └── fakeUser.json │ ├── global.d.ts │ ├── integration │ │ ├── home.test.ts │ │ ├── navbar.test.ts │ │ ├── post.test.ts │ │ ├── register.test.ts │ │ └── settings.test.ts │ ├── plugins │ │ └── index.js │ └── support │ │ ├── commands.js │ │ └── index.js ├── package.json ├── tsconfig.json └── yarn.lock ├── themes ├── color-names.js ├── hex-to-rgb.js ├── index.js ├── themes.js └── with-opacity.js ├── tsconfig.jest.json ├── tsconfig.json ├── tsconfig.server.json ├── types ├── blob-polyfill.d.ts ├── environment.d.ts ├── global.d.ts ├── index.ts ├── models │ ├── Post.ts │ └── User.ts └── next-auth.d.ts ├── uploads ├── avatars │ ├── avatar0.jpg │ ├── avatar1.jpg │ ├── avatar2.jpg │ ├── avatar3.jpg │ └── placeholder-avatar.jpg └── headers │ ├── header0.jpg │ ├── header1.jpg │ ├── header2.jpg │ ├── header3.jpg │ ├── header4.jpg │ └── placeholder-header.jpg ├── utils ├── bem.ts ├── index.ts └── tw-config.js ├── views ├── Auth │ ├── Auth.scss │ ├── Auth.test.tsx │ ├── Auth.tsx │ └── index.ts ├── Create │ ├── Create.scss │ ├── Create.test.tsx │ ├── Create.tsx │ └── index.ts ├── Custom500 │ ├── Custom500.scss │ ├── Custom500.tsx │ └── index.ts ├── Drafts │ ├── Drafts.scss │ ├── Drafts.test.tsx │ ├── Drafts.tsx │ └── index.ts ├── Home │ ├── Home.scss │ ├── Home.test.tsx │ ├── Home.tsx │ └── index.ts ├── NotFound │ ├── NotFound.scss │ ├── NotFound.tsx │ └── index.ts ├── Post │ ├── Post.scss │ ├── Post.test.tsx │ ├── Post.tsx │ └── index.ts ├── Profile │ ├── Profile.scss │ ├── Profile.test.tsx │ ├── Profile.tsx │ └── index.ts ├── Settings │ ├── Settings.scss │ ├── Settings.test.tsx │ ├── Settings.tsx │ └── index.ts └── Users │ ├── Users.scss │ ├── Users.test.tsx │ ├── Users.tsx │ └── index.ts └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["next/babel"], 3 | "plugins": ["superjson-next", "preval"] 4 | } 5 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | .gitignore 3 | **/.gitkeep 4 | **/.DS_Store 5 | .dockerignore 6 | Dockerfile* 7 | docker-compose*.yml 8 | README.md 9 | certs/ 10 | docs/ 11 | notes/ 12 | 13 | # pass env vars through docker-compose 14 | # or mount .env* in dev 15 | .env* 16 | envs/ 17 | dist/ 18 | 19 | # needed for sqlite 20 | # prisma/db.dev 21 | # postgres 22 | prisma/pg-data/data-* 23 | 24 | # named volumes 25 | .next/ 26 | node_modules/ 27 | 28 | # cypress 29 | tests-e2e/node_modules/ 30 | 31 | # jest coverage 32 | coverage/ -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # development local default variables 2 | # public, versioned, same for all users 3 | 4 | # possible values: local | docker | gitpod | replit 5 | APP_ENV=local 6 | 7 | # ---------------------------------- 8 | # local 9 | 10 | # set just these 3 11 | # http - just works 12 | # https - needed only for Facebook oauth 13 | # you need: certs/localhost-key.pem and certs/localhost.pem 14 | SITE_PROTOCOL=https 15 | SITE_HOSTNAME=localhost 16 | PORT=3001 17 | 18 | # expand it immediately 19 | NEXTAUTH_URL=${SITE_PROTOCOL}://${SITE_HOSTNAME}:${PORT} 20 | 21 | -------------------------------------------------------------------------------- /.env.development.local.example: -------------------------------------------------------------------------------- 1 | # development private vars, secrets, server only, not versioned 2 | # or public vars specific for a developer, to override defaults from .env.development 3 | 4 | # template for all .env.development.*.local files 5 | 6 | # ------- 7 | # db-1 8 | 9 | # db in docker container 10 | # localhost when app runs on host os - APP_ENV=local 11 | POSTGRES_HOSTNAME=localhost 12 | POSTGRES_PORT=5432 13 | POSTGRES_USER=postgres_user 14 | POSTGRES_PASSWORD=password 15 | POSTGRES_DB=npb-db-dev 16 | 17 | 18 | # ------- 19 | # db-2 20 | 21 | # second optional remote db for development 22 | #POSTGRES_HOSTNAME= 23 | #POSTGRES_PORT= 24 | #POSTGRES_USER= 25 | #POSTGRES_PASSWORD= 26 | #POSTGRES_DB= 27 | 28 | # ------- 29 | 30 | # used in schema.prisma 31 | # expand it immediately 32 | DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOSTNAME}:${POSTGRES_PORT}/${POSTGRES_DB}?schema=public 33 | 34 | # ------- 35 | # next-auth vars common for all APP_ENVs 36 | 37 | # jwt secret 38 | SECRET= 39 | 40 | # Facebook 41 | FACEBOOK_CLIENT_ID= 42 | FACEBOOK_CLIENT_SECRET= 43 | 44 | # Google 45 | GOOGLE_CLIENT_ID= 46 | GOOGLE_CLIENT_SECRET= 47 | 48 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # production local default variables 2 | # public, versioned, same for all users 3 | 4 | # possible values: local | docker | live 5 | APP_ENV=local 6 | 7 | # ---------------------------------- 8 | # local production for testing 9 | 10 | # locally - http | https 11 | SITE_PROTOCOL=http 12 | SITE_HOSTNAME=localhost 13 | PORT=3001 14 | 15 | NEXTAUTH_URL=${SITE_PROTOCOL}://${SITE_HOSTNAME}:${PORT} 16 | -------------------------------------------------------------------------------- /.env.production.local.example: -------------------------------------------------------------------------------- 1 | # local production (staging-local) 2 | # private vars, secrets, server only, not versioned 3 | # or public vars specific for a developer, to override defaults from .env.production 4 | 5 | # template for all .env.*.local files 6 | 7 | # ------- 8 | # db-1 9 | 10 | # db in docker container 11 | # localhost when app runs on host os - APP_ENV=local 12 | POSTGRES_HOSTNAME=localhost 13 | # only difference from dev 14 | POSTGRES_DB=npb-db-prod 15 | POSTGRES_PORT=5432 16 | POSTGRES_USER=postgres_user 17 | POSTGRES_PASSWORD=password 18 | 19 | 20 | # ------- 21 | # db-2 22 | 23 | # second optional remote db for production 24 | #POSTGRES_HOSTNAME= 25 | #POSTGRES_PORT= 26 | #POSTGRES_USER= 27 | #POSTGRES_PASSWORD= 28 | #POSTGRES_DB= 29 | 30 | # ------- 31 | 32 | # used in schema.prisma 33 | # expand it immediately 34 | DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOSTNAME}:${POSTGRES_PORT}/${POSTGRES_DB}?schema=public 35 | 36 | # ------- 37 | # next-auth vars common for all APP_ENVs 38 | 39 | # jwt secret 40 | SECRET= 41 | 42 | # Facebook 43 | FACEBOOK_CLIENT_ID= 44 | FACEBOOK_CLIENT_SECRET= 45 | 46 | # Google 47 | GOOGLE_CLIENT_ID= 48 | GOOGLE_CLIENT_SECRET= 49 | 50 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | 2 | APP_ENV=local 3 | 4 | SITE_PROTOCOL=http 5 | SITE_HOSTNAME=localhost 6 | PORT=3001 7 | 8 | NEXTAUTH_URL=${SITE_PROTOCOL}://${SITE_HOSTNAME}:${PORT} 9 | -------------------------------------------------------------------------------- /.env.test.local.example: -------------------------------------------------------------------------------- 1 | 2 | POSTGRES_HOSTNAME=localhost 3 | POSTGRES_PORT=5435 4 | POSTGRES_USER=postgres_user 5 | POSTGRES_PASSWORD=password 6 | POSTGRES_DB=npb-db-test 7 | 8 | # used in schema.prisma, expand pg vars 9 | DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOSTNAME}:${POSTGRES_PORT}/${POSTGRES_DB}?schema=public 10 | 11 | # ------- 12 | # next-auth vars common for all APP_ENVs 13 | 14 | # jwt secret 15 | SECRET=RANDOM_STRING 16 | 17 | # Facebook 18 | FACEBOOK_CLIENT_ID= 19 | FACEBOOK_CLIENT_SECRET= 20 | 21 | # Google 22 | GOOGLE_CLIENT_ID= 23 | GOOGLE_CLIENT_SECRET= 24 | 25 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .next/ 3 | dist/ 4 | notes/ 5 | prisma/ 6 | tests-e2e/ 7 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "warnOnUnsupportedTypeScriptVersion": false 5 | // "project": "./tsconfig.json" 6 | }, 7 | "plugins": ["@typescript-eslint", "react-hooks"], 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:react/recommended", 11 | "plugin:@typescript-eslint/recommended", 12 | // "plugin:@typescript-eslint/recommended-requiring-type-checking", 13 | "plugin:jest/recommended", 14 | "plugin:jest/style", 15 | "plugin:testing-library/react", 16 | "prettier" 17 | ], 18 | "env": { 19 | "es6": true, 20 | "browser": true, 21 | "jest": true, 22 | "node": true 23 | }, 24 | "settings": { 25 | "react": { 26 | "version": "detect" 27 | } 28 | }, 29 | "rules": { 30 | "react-hooks/rules-of-hooks": "error", 31 | "react/react-in-jsx-scope": 0, 32 | "react/display-name": 0, 33 | "react/prop-types": 0, 34 | "@typescript-eslint/explicit-function-return-type": 0, 35 | "@typescript-eslint/explicit-member-accessibility": 0, 36 | "@typescript-eslint/indent": 0, 37 | "@typescript-eslint/member-delimiter-style": 0, 38 | "@typescript-eslint/no-explicit-any": 0, 39 | "@typescript-eslint/no-var-requires": 0, 40 | "@typescript-eslint/no-use-before-define": 0, 41 | "@typescript-eslint/no-unused-vars": "warn", 42 | "@typescript-eslint/no-empty-interface": "off", 43 | "no-console": "warn", 44 | // tests 45 | "testing-library/no-node-access": "warn", 46 | "testing-library/no-unnecessary-act": "warn" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | --- 5 | 6 | **Describe the bug** 7 | A clear and concise description of what the bug is. 8 | 9 | **To Reproduce** 10 | Please include steps to reproduce the behavior; 11 | or an URL to Reproduction repository; 12 | 13 | **Expected behavior** 14 | A clear and concise description of what you expected to happen. 15 | 16 | **Screenshots** 17 | If applicable, add screenshots to help explain your problem. 18 | 19 | **Environment (please complete the following information):** 20 | 21 | - Development or production app environment 22 | - Browser [e.g. chrome v60.12.0, safari 10.1] 23 | - OS: [e.g. iOS, Windows x64] (if applicable) 24 | - node version (if applicable) 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | --- 5 | 6 | **Is your feature request related to a problem? Please describe.** 7 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 8 | 9 | **Describe the solution you'd like** 10 | A clear and concise description of what you want to happen. 11 | 12 | **Describe alternatives you've considered** 13 | A clear and concise description of any alternative solutions or features you've considered. 14 | 15 | **Additional context** 16 | Add any other context or screenshots about the feature request here. 17 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## What does it do? 2 | 3 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. 4 | 5 | ## Fixes # (issue) 6 | 7 | Please mention in the format "Fixes #issueNumber" or "Closes #issueNumber". 8 | This is important for semantic-release to correctly generate release tags and update issues. 9 | 10 | ## Type of change 11 | 12 | Please delete options that are not relevant. 13 | 14 | - [ ] Bug fix (non-breaking change which fixes an issue) 15 | - [ ] New feature (non-breaking change which adds functionality) 16 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 17 | - [ ] This change requires a documentation update 18 | 19 | ## Checklist: 20 | 21 | - [ ] I have performed a self-review of my own code 22 | - [ ] I have commented my code, particularly in hard-to-understand areas 23 | - [ ] Updated documentation (if applicable) 24 | - [ ] Added tests that prove my fix is effective or that my feature works 25 | - [ ] Make sure app compiles in both dev and prod mode by running `yarn dev` and `yarn build` 26 | - [ ] New and existing unit, integration and e2e tests pass locally with my changes 27 | - [ ] My changes generate no new warnings (browser console and Node.js terminal) 28 | - [ ] There are no new linting `yarn lint` and typing `yarn types` errors 29 | - [ ] Make sure code is formatted by running `yarn format` 30 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: deploy 2 | 3 | # trigger on docker build workflow 4 | # only works on a default branch 5 | on: 6 | workflow_run: 7 | workflows: ['docker build - disabled'] 8 | types: 9 | - completed 10 | 11 | workflow_dispatch: 12 | 13 | jobs: 14 | deploy: 15 | name: Deploy image from Dockerhub to VPS with ssh 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Deploy latest image 20 | uses: appleboy/ssh-action@master 21 | with: 22 | host: ${{ secrets.VPS_HOST }} 23 | username: ${{ secrets.VPS_USERNAME }} 24 | key: ${{ secrets.VPS_KEY_ED25519 }} 25 | port: ${{ secrets.VPS_PORT }} 26 | script_stop: true 27 | script: | 28 | cd /home/ubuntu/traefik-proxy/apps/nextjs-prisma-boilerplate 29 | echo 'Old image id:' 30 | docker inspect --format='{{index .RepoDigests 0}}' nemanjamitic/nextjs-prisma-boilerplate:latest 31 | docker-compose down 32 | docker image rm nemanjamitic/nextjs-prisma-boilerplate:latest 33 | docker-compose up -d 34 | echo 'New image id:' 35 | docker inspect --format='{{index .RepoDigests 0}}' nemanjamitic/nextjs-prisma-boilerplate:latest 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .next/ 3 | prisma/dev.db 4 | certs/ 5 | dist/ 6 | /**/tsconfig.tsbuildinfo 7 | yarn-error.log 8 | 9 | # postgres 10 | prisma/pg-data/data-* 11 | 12 | # very important 13 | /**/.env*.local 14 | /**/.env 15 | 16 | !uploads/avatars 17 | uploads/avatars/* 18 | !uploads/avatars/avatar0.jpg 19 | !uploads/avatars/avatar1.jpg 20 | !uploads/avatars/avatar2.jpg 21 | !uploads/avatars/avatar3.jpg 22 | !uploads/avatars/placeholder-avatar.jpg 23 | 24 | !uploads/headers 25 | uploads/headers/* 26 | !uploads/headers/placeholder-header.jpg 27 | !uploads/headers/header0.jpg 28 | !uploads/headers/header1.jpg 29 | !uploads/headers/header2.jpg 30 | !uploads/headers/header3.jpg 31 | !uploads/headers/header4.jpg 32 | 33 | # cypress 34 | tests-e2e/node_modules/ 35 | tests-e2e/cypress/screenshots/ 36 | 37 | # jest coverage 38 | coverage/ -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | # This configuration file was automatically generated by Gitpod. 2 | # Please adjust to your needs (see https://www.gitpod.io/docs/config-gitpod-file) 3 | # and commit this file to your remote git repository to share the goodness with others. 4 | 5 | # Commands to start on workspace startup 6 | tasks: 7 | - name: Terminal 8 | init: yarn install && yarn gitpod:push:env 9 | command: yarn gitpod:dev:env 10 | 11 | # Ports to expose on workspace startup 12 | ports: 13 | - name: Website 14 | port: 3001 15 | description: api http server listens on this port 16 | onOpen: open-browser 17 | visibility: public 18 | 19 | # Extensions 20 | vscode: 21 | extensions: 22 | - dsznajder.es7-react-js-snippets 23 | - esbenp.prettier-vscode 24 | - Prisma.prisma 25 | - bradlc.vscode-tailwindcss 26 | - stylelint.vscode-stylelint 27 | - csstools.postcss 28 | - orta.vscode-jest 29 | - mikestead.dotenv 30 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | yarn.lock 4 | package-lock.json 5 | public 6 | prisma/pg-data-dev 7 | prisma/pg-data-prod 8 | prisma/pg-data-test 9 | notes 10 | dist 11 | certs 12 | tsconfig.tsbuildinfo 13 | uploads 14 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 90, 3 | "trailingComma": "es5", 4 | "tabWidth": 2, 5 | "semi": true, 6 | "singleQuote": true 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dsznajder.es7-react-js-snippets", 4 | "esbenp.prettier-vscode", 5 | "Prisma.prisma", 6 | "bradlc.vscode-tailwindcss", 7 | "stylelint.vscode-stylelint", 8 | "csstools.postcss", 9 | "ms-vscode-remote.remote-containers", 10 | "orta.vscode-jest", 11 | "mikestead.dotenv" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": "explicit", 4 | "source.fixAll.stylelint": "explicit" 5 | }, 6 | // Add those two lines: 7 | "editor.formatOnSave": true, // Tell VSCode to format files on save 8 | "editor.defaultFormatter": "esbenp.prettier-vscode", // Tell VSCode to use Prettier as default file formatter 9 | // Recommended config for the extension 10 | "css.validate": false, 11 | "less.validate": false, 12 | "scss.validate": false, 13 | "typescript.tsdk": "node_modules/typescript/lib", 14 | // disable run tests on startup 15 | "jest.autoRun": "off" 16 | } 17 | -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | 2 | # pick image with git and bash 3 | # ARG BASE=mcr.microsoft.com/vscode/devcontainers/javascript-node:0-16-bullseye 4 | ARG BASE=node:16-bullseye 5 | 6 | # no ENV vars required buildtime 7 | 8 | #--------------------- 9 | 10 | FROM ${BASE} AS dependencies 11 | WORKDIR /app 12 | ENV NODE_ENV development 13 | 14 | COPY package.json yarn.lock ./ 15 | COPY prisma ./prisma 16 | 17 | RUN yarn install 18 | RUN npx prisma generate 19 | RUN rm -rf prisma 20 | 21 | # don't copy source, mount it via volume 22 | 23 | # volumes folders must be created and chowned before docker-compose creates them as root 24 | # create them during docker build 25 | RUN mkdir -p .next 26 | RUN chown node:node . node_modules .next 27 | RUN chown -R node:node node_modules/.prisma 28 | 29 | USER node 30 | 31 | # debug 32 | # RUN ls -la node_modules/.prisma/client 33 | 34 | EXPOSE 3001 35 | ENV PORT 3001 36 | 37 | CMD [ "yarn", "cmd:start:dev" ] 38 | -------------------------------------------------------------------------------- /Dockerfile.e2e: -------------------------------------------------------------------------------- 1 | # this Dockerfile is needed because of @testing-library/cypress 2 | # import in tests-e2e/cypress/support/commands.js 3 | # must be in root to include prisma 4 | # must include all imports from seed: prisma, bcryptjs, faker, next.js... 5 | 6 | # 2x smaller size than 16.3 which is in project 7 | FROM cypress/base:16.14.2 8 | 9 | WORKDIR /app 10 | 11 | # important: 12 | # MUST repeat everything from host 13 | # /app/package.json 14 | # /app/tsconfig.json 15 | # /app/tests-e2e/cypress.json 16 | # /app/tests-e2e/tsconfig.json 17 | COPY tests-e2e/package.json tests-e2e/yarn.lock ./ 18 | 19 | # important: tsconfig.json parent/child same like on host 20 | COPY tsconfig.json ./ 21 | COPY tests-e2e/tsconfig.json ./tests-e2e/tsconfig.json 22 | 23 | # add prisma and generate client for seed 24 | COPY prisma ./prisma 25 | 26 | 27 | # by setting CI environment variable we switch the Cypress install messages 28 | # to small "started / finished" and avoid 1000s of lines of progress messages 29 | # https://github.com/cypress-io/cypress/issues/1243 30 | ENV CI=1 31 | RUN yarn install --frozen-lockfile 32 | 33 | # dont clean yarn cache 34 | 35 | RUN npx prisma generate 36 | RUN rm -rf prisma 37 | 38 | # copy imported files in Cypress tests from next.js app 39 | COPY prisma/seed.js ./prisma/seed.js 40 | COPY lib-client/constants.ts ./lib-client/constants.ts 41 | 42 | 43 | # verify that Cypress has been installed correctly. 44 | # running this command separately from "cypress run" will also cache its result 45 | # to avoid verifying again when running the tests 46 | RUN npx cypress verify 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /Dockerfile.test: -------------------------------------------------------------------------------- 1 | 2 | # use alpine for tests like prod 3 | ARG BASE=node:16-alpine 4 | 5 | # simplified prod build 6 | # migrate, seed, build 7 | #--------------------- 8 | 9 | FROM ${BASE} AS dependencies 10 | 11 | RUN whoami && id 12 | 13 | # openssl for prisma client, bash for jest-preview 14 | RUN apk update && apk add --no-cache openssl libc6-compat bash 15 | 16 | WORKDIR /app 17 | ENV NODE_ENV development 18 | 19 | # prepare only node_modules and prisma client 20 | # src will be mounted via volume 21 | # build at runtime, not here 22 | COPY package.json yarn.lock ./ 23 | COPY prisma ./prisma 24 | 25 | # dev dependencies for api integration testing 26 | RUN yarn install 27 | RUN npx prisma generate 28 | 29 | RUN rm -rf prisma 30 | 31 | ENV NODE_ENV test 32 | 33 | # volumes folders must be created and chowned before docker-compose creates them as root 34 | # create them during docker build 35 | RUN mkdir -p .next dist 36 | RUN chown node:node . node_modules .next dist 37 | RUN chown -R node:node node_modules/.prisma 38 | 39 | USER node 40 | 41 | # on container 42 | EXPOSE 3001 43 | # env for app 44 | ENV PORT 3001 45 | 46 | # build app at runtime, not here 47 | # api integration tests dont need built prod app, Jest runs code 48 | 49 | # 1. migrate prod, 2. build app, 3. start prod 50 | # default: just 1. migrate and shut down - for integration tests 51 | # for e2e: 1, 2 and 3 52 | CMD [ "yarn", "prisma:migrate:prod" ] 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Nemanja Mitic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /components/Alert/Alert.scss: -------------------------------------------------------------------------------- 1 | .alert { 2 | @apply p-2 border; 3 | 4 | &--info { 5 | @apply text-th-info border-th-info bg-th-info/20; 6 | } 7 | 8 | &--success { 9 | @apply text-th-success border-th-success bg-th-success/20; 10 | } 11 | 12 | &--warning { 13 | @apply text-th-warning border-th-warning bg-th-warning/20; 14 | } 15 | 16 | &--error { 17 | @apply text-th-error border-th-error bg-th-error/20; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /components/Alert/Alert.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { withBem } from 'utils/bem'; 3 | 4 | type Props = { 5 | message: string; 6 | variant?: 'info' | 'success' | 'warning' | 'error'; 7 | className?: string; 8 | }; 9 | 10 | const Alert: FC = ({ message, variant = 'info', className }) => { 11 | const b = withBem('alert'); 12 | 13 | const modifiers = { 14 | info: variant === 'info', 15 | success: variant === 'success', 16 | warning: variant === 'warning', 17 | error: variant === 'error', 18 | }; 19 | 20 | const _className = className ? ` ${className}` : ''; 21 | 22 | return ( 23 |
24 | {message} 25 |
26 | ); 27 | }; 28 | 29 | export default Alert; 30 | -------------------------------------------------------------------------------- /components/Alert/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from 'components/Alert/Alert'; 2 | -------------------------------------------------------------------------------- /components/Button/Button.scss: -------------------------------------------------------------------------------- 1 | .button { 2 | @apply inline-block focus:outline-none transition ease-in-out duration-200 cursor-pointer; 3 | 4 | &--pill { 5 | @apply rounded-full; 6 | } 7 | 8 | &--size-sm { 9 | @apply px-2 py-1 text-sm; 10 | } 11 | 12 | &--size-base { 13 | @apply px-4 py-2; 14 | } 15 | 16 | &--size-lg { 17 | @apply px-8 py-3 text-lg; 18 | } 19 | 20 | &--primary { 21 | @apply focus:ring-2 focus:ring-opacity-50; 22 | @apply bg-th-primary hover:bg-th-primary-focus focus:ring-th-primary text-th-primary-content; 23 | } 24 | 25 | &--secondary { 26 | @apply focus:ring-2 focus:ring-opacity-50; 27 | @apply bg-th-secondary hover:bg-th-secondary-focus focus:ring-th-secondary text-th-secondary-content; 28 | } 29 | 30 | &--neutral { 31 | @apply bg-th-neutral text-th-neutral-content; 32 | &:hover { 33 | @apply bg-th-neutral-focus; 34 | } 35 | &:focus { 36 | @apply ring-2 ring-opacity-50 ring-th-neutral; 37 | } 38 | } 39 | 40 | &--danger { 41 | // @apply bg-red-500 hover:bg-red-800 focus:ring-2 focus:ring-red-500 focus:ring-opacity-50 text-white; 42 | @apply bg-th-error text-th-base-content; 43 | } 44 | 45 | &--transparent { 46 | @apply bg-transparent text-current; 47 | &:hover { 48 | @apply bg-th-base-content bg-opacity-20 border-opacity-0; 49 | } 50 | } 51 | 52 | &--disabled, 53 | &--disabled:hover { 54 | @apply cursor-not-allowed; 55 | @apply bg-th-neutral border-opacity-0 bg-opacity-20 text-th-base-content text-opacity-20; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /components/Button/Button.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | ReactNode, 3 | forwardRef, 4 | ForwardRefRenderFunction, 5 | HTMLAttributes, 6 | } from 'react'; 7 | import { withBem } from 'utils/bem'; 8 | 9 | type Props = { 10 | children: ReactNode | string; 11 | className?: string; 12 | pill?: boolean; 13 | disabled?: boolean; 14 | tagName?: 'button' | 'a' | 'span'; 15 | type?: 'submit' | 'button'; 16 | variant?: 'primary' | 'secondary' | 'danger' | 'transparent' | 'neutral' | 'blank'; // add link button 17 | size?: 'sm' | 'base' | 'lg'; 18 | } & HTMLAttributes; 19 | 20 | const Button: ForwardRefRenderFunction = ( 21 | { 22 | children, 23 | className, 24 | pill, 25 | disabled = false, 26 | tagName = 'button', 27 | type = 'button', 28 | variant = 'primary', 29 | size = 'base', 30 | ...props 31 | }, 32 | ref 33 | ) => { 34 | const b = withBem('button'); 35 | 36 | const Tag = tagName; 37 | const modifiers = { 38 | 'size-sm': size === 'sm', 39 | 'size-base': size === 'base', 40 | 'size-lg': size === 'lg', 41 | primary: variant === 'primary', 42 | secondary: variant === 'secondary', 43 | neutral: variant === 'neutral', 44 | danger: variant === 'danger', 45 | transparent: variant === 'transparent', 46 | pill, 47 | disabled, 48 | }; 49 | const _className = className ? ` ${className}` : ''; 50 | 51 | return ( 52 | 59 | {children} 60 | 61 | ); 62 | }; 63 | 64 | export default forwardRef(Button); 65 | -------------------------------------------------------------------------------- /components/Button/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from 'components/Button/Button'; 2 | -------------------------------------------------------------------------------- /components/CustomHead/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from 'components/CustomHead/CustomHead'; 2 | -------------------------------------------------------------------------------- /components/Dropdown/Dropdown.scss: -------------------------------------------------------------------------------- 1 | .dropdown { 2 | @apply shrink-0; 3 | 4 | &__container { 5 | @apply relative flex justify-center items-center; 6 | } 7 | 8 | &__anchor { 9 | @apply flex justify-between items-center p-1 ml-auto cursor-pointer; 10 | } 11 | 12 | &__menu { 13 | @apply absolute invisible opacity-0 top-14 right-0 w-64 rounded-lg shadow-md overflow-hidden p-2; 14 | @apply transition-opacity ease-in duration-300; 15 | @apply bg-th-base-100 text-th-base-content; 16 | } 17 | 18 | &__menu--active { 19 | @apply visible opacity-100; 20 | } 21 | 22 | &__list { 23 | // 24 | } 25 | 26 | &__list-item { 27 | & > * { 28 | @apply block; 29 | } 30 | 31 | &:not(:last-child) { 32 | @apply border-b border-th-base-200; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /components/Dropdown/Dropdown.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode, FC } from 'react'; 2 | import { useDetectOutsideClick } from 'components/hooks'; 3 | import { withBem } from 'utils/bem'; 4 | 5 | type Props = { 6 | children: ReactNode; 7 | items: ReactNode[]; 8 | }; 9 | 10 | const Dropdown: FC = ({ children, items }) => { 11 | const { menuRef, anchorRef, isActive, setIsActive } = useDetectOutsideClick(); 12 | const b = withBem('dropdown'); 13 | 14 | const handleAvatarClick = () => { 15 | // both closes and opens 16 | setIsActive((prevIsActive) => !prevIsActive); 17 | }; 18 | 19 | return ( 20 |
21 |
22 | 23 | {children} 24 | 25 | 26 | 36 |
37 |
38 | ); 39 | }; 40 | 41 | export default Dropdown; 42 | -------------------------------------------------------------------------------- /components/Dropdown/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from 'components/Dropdown/Dropdown'; 2 | -------------------------------------------------------------------------------- /components/DropzoneSingle/DropzoneSingle.scss: -------------------------------------------------------------------------------- 1 | .dropzone-single { 2 | &__preview { 3 | @apply relative bg-gray-200; 4 | 5 | &--active { 6 | @apply bg-gray-400; 7 | } 8 | } 9 | 10 | &__image { 11 | @apply w-full h-44 object-cover; 12 | } 13 | 14 | &__placeholder { 15 | @apply bg-th-neutral w-full h-44; 16 | } 17 | 18 | &__overlay { 19 | @apply absolute top-0 left-0 flex justify-center items-center h-full w-full; 20 | @apply bg-gray-200 bg-opacity-0; 21 | @apply transition-all ease-in; 22 | 23 | & > span { 24 | @apply text-transparent font-bold whitespace-pre-line; 25 | @apply transition-all ease-in; 26 | text-align-last: center; 27 | } 28 | 29 | // hover and style same transition 30 | &--active { 31 | @apply bg-opacity-30; 32 | 33 | & > span { 34 | @apply text-white; 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /components/DropzoneSingle/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from 'components/DropzoneSingle/DropzoneSingle'; 2 | -------------------------------------------------------------------------------- /components/Error/ErrorCard.scss: -------------------------------------------------------------------------------- 1 | .error-card { 2 | @apply flex flex-col gap-2 xs:gap-4 p-4 rounded-md; 3 | @apply w-full max-w-xs xs:max-w-md mx-4 xs:mx-0; 4 | @apply bg-th-base-200; 5 | 6 | &__content { 7 | @apply flex flex-col gap-4 items-center; 8 | @apply xs:flex-row; 9 | } 10 | 11 | &__icon { 12 | & > svg { 13 | @apply h-16 w-16 xs:h-20 xs:w-20 text-th-accent shrink-0; 14 | } 15 | } 16 | 17 | &__message { 18 | @apply self-start xs:self-center; 19 | } 20 | 21 | &__link { 22 | & > a { 23 | @apply link-primary; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /components/Error/ErrorCard.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, ReactNode } from 'react'; 2 | import { withBem } from 'utils/bem'; 3 | 4 | type Props = { 5 | title: string; 6 | icon: ReactNode; 7 | message: ReactNode; 8 | link: ReactNode; 9 | }; 10 | 11 | const ErrorCard: FC = ({ title, icon, message, link }) => { 12 | const b = withBem('error-card'); 13 | 14 | return ( 15 |
16 |

{title}

17 |
18 |
{icon}
19 |
{message}
20 |
21 |
{link}
22 |
23 | ); 24 | }; 25 | 26 | export default ErrorCard; 27 | -------------------------------------------------------------------------------- /components/Error/ErrorFallback.scss: -------------------------------------------------------------------------------- 1 | .error-fallback { 2 | @apply flex justify-center items-center; 3 | 4 | &--screen { 5 | @apply h-screen w-screen; 6 | } 7 | 8 | &--page { 9 | @apply min-h-[var(--available-content-min-h)] sm:min-h-[var(--available-content-min-h-sm)]; 10 | } 11 | 12 | &--item { 13 | @apply py-2 xs:py-0 xs:h-56 border border-th-base-200; 14 | 15 | &:not(:last-child) { 16 | @apply mb-4; 17 | } 18 | } 19 | 20 | &__message { 21 | & > span { 22 | @apply mr-2; 23 | } 24 | } 25 | 26 | &__label { 27 | @apply font-bold; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /components/Error/ErrorFallback.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { FallbackProps } from 'react-error-boundary'; 3 | import { withBem } from 'utils/bem'; 4 | import ErrorCard from 'components/Error/ErrorCard'; 5 | import { BiError } from 'react-icons/bi'; 6 | import { FallbackType } from 'types'; 7 | 8 | export type ErrorFallbackProps = { 9 | fallbackType: FallbackType; 10 | } & FallbackProps; 11 | 12 | // screen, page, item, same as loading 13 | const ErrorFallback: FC = ({ 14 | error, 15 | resetErrorBoundary, 16 | fallbackType, 17 | }) => { 18 | const b = withBem('error-fallback'); 19 | 20 | const modifiers = { 21 | item: fallbackType === 'item', 22 | page: fallbackType === 'page', 23 | screen: fallbackType === 'screen', 24 | }; 25 | 26 | return ( 27 |
28 | } 31 | message={ 32 |
33 | UI: 34 | {fallbackType} 35 | Message: 36 | 37 | {error.message} 38 | 39 |
40 | } 41 | link={ 42 | { 45 | e.preventDefault(); 46 | resetErrorBoundary(); 47 | }} 48 | > 49 | Try again 50 | 51 | } 52 | /> 53 |
54 | ); 55 | }; 56 | 57 | export default ErrorFallback; 58 | -------------------------------------------------------------------------------- /components/Error/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from 'components/Error/ErrorFallback'; 2 | -------------------------------------------------------------------------------- /components/Footer/Footer.scss: -------------------------------------------------------------------------------- 1 | .footer { 2 | @apply h-[var(--footer-height)]; 3 | @apply bg-th-base-200 text-th-base-content; 4 | 5 | @apply flex justify-between items-center px-4; 6 | 7 | &__left-empty { 8 | @apply inline-block w-10; 9 | } 10 | 11 | &__links { 12 | @apply flex justify-between items-center gap-2; 13 | } 14 | 15 | &__author { 16 | @apply inline-block w-36 sm:w-40 text-right font-bold; 17 | } 18 | 19 | &__seed-link { 20 | @apply link-primary text-sm; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /components/Footer/Footer.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen } from '@testing-library/react'; 2 | import { customRender } from 'test-client/test-utils'; 3 | import Footer from 'components/Footer'; 4 | 5 | // trivial component test example 6 | describe('Footer', () => { 7 | test('renders', async () => { 8 | customRender(