├── .vscode └── settings.json ├── README.md ├── TODO.md ├── app.json ├── generateKeys.js ├── hasura ├── .dockerignore ├── .gitignore ├── Dockerfile ├── config.yaml ├── docker-compose.yml ├── metadata │ ├── actions.graphql │ ├── actions.yaml │ ├── allow_list.yaml │ ├── cron_triggers.yaml │ ├── functions.yaml │ ├── query_collections.yaml │ ├── remote_schemas.yaml │ ├── tables.yaml │ └── version.yaml └── migrations │ ├── 1597498463371_create_table_public_users │ ├── down.sql │ └── up.sql │ ├── 1597498548202_create_table_public_accounts │ ├── down.sql │ └── up.sql │ ├── 1597498609423_create_table_public_sessions │ ├── down.sql │ └── up.sql │ ├── 1597498658823_create_table_public_verification_requests │ ├── down.sql │ └── up.sql │ ├── 1597498709972_create_table_public_feeds │ ├── down.sql │ └── up.sql │ ├── 1597498729906_set_fk_public_feeds_user_id │ ├── down.sql │ └── up.sql │ ├── 1597504190869_create_table_public_boards │ ├── down.sql │ └── up.sql │ ├── 1597505181237_create_table_public_boards_users │ ├── down.sql │ └── up.sql │ ├── 1597505238023_create_table_public_lists │ ├── down.sql │ └── up.sql │ ├── 1597505340738_create_table_public_cards │ ├── down.sql │ └── up.sql │ ├── 1597577533977_alter_table_public_lists_alter_column_position │ ├── down.sql │ └── up.sql │ ├── 1597577544193_alter_table_public_lists_drop_constraint_lists_position_key │ ├── down.sql │ └── up.sql │ ├── 1597577650903_alter_table_public_lists_alter_column_position │ ├── down.sql │ └── up.sql │ ├── 1597577665522_alter_table_public_cards_alter_column_position │ ├── down.sql │ └── up.sql │ ├── 1597601412648_alter_table_public_lists_alter_column_position │ ├── down.sql │ └── up.sql │ ├── 1597601429537_alter_table_public_lists_add_unique_board_id_position │ ├── down.sql │ └── up.sql │ ├── 1599496540352_alter_table_public_boards_add_column_icon │ ├── down.sql │ └── up.sql │ ├── 1599632432195_alter_table_public_lists_drop_constraint_lists_board_id_position_key │ ├── down.sql │ └── up.sql │ ├── 1599670620301_alter_table_public_lists_alter_column_position │ ├── down.sql │ └── up.sql │ ├── 1599670653341_alter_table_public_cards_alter_column_position │ ├── down.sql │ └── up.sql │ ├── 1599915366833_set_fk_public_cards_list_id │ ├── down.sql │ └── up.sql │ ├── 1599915561792_set_fk_public_cards_board_id │ ├── down.sql │ └── up.sql │ └── 1599915623950_set_fk_public_lists_board_id │ ├── down.sql │ └── up.sql ├── heroku.yml ├── license.md └── nextjs ├── .babelrc ├── .env.example ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .prettierignore ├── .prettierrc ├── codegen.yml ├── graphql.schema.json ├── next-env.d.ts ├── next.config.js ├── package.json ├── postcss.config.js ├── public └── images │ ├── bug_fixed.svg │ ├── favicon.ico │ └── logo.png ├── schema.prisma ├── src ├── components │ ├── AccessDeniedIndicator.tsx │ ├── AddInput.tsx │ ├── CustomLink.tsx │ ├── ItemLink.tsx │ ├── Loader.tsx │ ├── icons │ │ ├── BalanceIcon.tsx │ │ ├── ChartIcon.tsx │ │ ├── ClientIcon.tsx │ │ ├── ContactIcon.tsx │ │ ├── DashboardIcon.tsx │ │ ├── FormIcon.tsx │ │ ├── HamburgerIcon.tsx │ │ ├── Logo.tsx │ │ ├── LogoutIcon.tsx │ │ ├── MailIcon.tsx │ │ ├── MoonIcon.tsx │ │ ├── NotificationIcon.tsx │ │ ├── ProfileIcon.tsx │ │ ├── SaleIcon.tsx │ │ ├── SearchIcon.tsx │ │ ├── SettingsIcon.tsx │ │ ├── StarIcon.tsx │ │ └── SunIcon.tsx │ └── pages │ │ ├── account │ │ ├── graphql │ │ │ ├── UpdateUser.graphql │ │ │ └── User.graphql │ │ └── index.tsx │ │ ├── boards │ │ ├── board │ │ │ ├── boardUtils.ts │ │ │ ├── graphql │ │ │ │ └── BoardQuery.graphql │ │ │ └── index.tsx │ │ ├── card │ │ │ ├── NewCard.tsx │ │ │ ├── graphql │ │ │ │ ├── CardFragment.graphql │ │ │ │ ├── DeleteCard.graphql │ │ │ │ ├── InsertCard.graphql │ │ │ │ ├── MoveCard.graphql │ │ │ │ └── UpdateCard.graphql │ │ │ └── index.tsx │ │ ├── graphql │ │ │ ├── BoardFragment.graphql │ │ │ ├── Boards.graphql │ │ │ ├── DeleteBoard.graphql │ │ │ └── InsertBoard.graphql │ │ ├── index.tsx │ │ └── list │ │ │ ├── ActionDropdown.tsx │ │ │ ├── ListHeader.tsx │ │ │ ├── NewList.tsx │ │ │ ├── graphql │ │ │ ├── DeleteList.graphql │ │ │ ├── InsertList.graphql │ │ │ ├── ListFragment.graphql │ │ │ └── UpdateList.graphql │ │ │ └── index.tsx │ │ ├── error │ │ └── index.tsx │ │ ├── feeds │ │ ├── AddNewFeedForm.tsx │ │ ├── feed.tsx │ │ ├── graphql │ │ │ ├── FeedFragment.graphql │ │ │ ├── Feeds.graphql │ │ │ └── InsertFeed.graphql │ │ └── index.tsx │ │ └── index │ │ └── Book.tsx ├── generated │ └── graphql.tsx ├── layouts │ ├── BoardLayout.tsx │ ├── MainLayout.tsx │ └── components │ │ ├── Footer.tsx │ │ ├── Header.tsx │ │ └── main │ │ ├── Sidebar.tsx │ │ └── SidebarMobile.tsx ├── lib │ ├── apolloClient.tsx │ └── cache.ts ├── pages │ ├── 404.tsx │ ├── _app.tsx │ ├── _document.tsx │ ├── _error.tsx │ ├── account.tsx │ ├── api │ │ └── auth │ │ │ └── [...nextauth].ts │ ├── boards │ │ ├── [boardId].tsx │ │ └── index.tsx │ ├── feeds.tsx │ └── index.tsx ├── styles │ ├── bar-of-progress.css │ └── tailwind.css ├── types │ ├── page.ts │ ├── session.ts │ ├── token.ts │ └── user.ts ├── utils │ ├── createCtx.ts │ ├── index.ts │ └── timeFromNow.tsx └── zustands │ └── boards.ts ├── tailwind.config.js ├── tsconfig.json ├── vendor.d.ts └── yarn.lock /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.watcherExclude": { 3 | "**/.git/objects/**": true, 4 | "**/.git/subtree-cache/**": true, 5 | "**/.hg/store/**": true 6 | } 7 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## (Unmaintained status - Take it as a learning resource) 2 |

3 | Logo 4 |

5 | 6 | A fullstack boilerplate which uses Hasura GraphQL Engine and Next.js to develop applications. 7 | 8 |

9 | 10 | Twitter: Son Do Hong 11 | 12 |

13 | 14 | ## Content 15 | 16 | - [Content](#content) 17 | - [TechStack Features](#techstack-features) 18 | - [Requirements](#requirements) 19 | - [Development Step](#development-step) 20 | - [1. **Clone the application**](#1-clone-the-application) 21 | - [2. **Run a generateKeys.js (present in root directory)**](#2-run-a-generatekeysjs-present-in-root-directory) 22 | - [3. **Start `Hasura GraphQL server` with docker-compose**](#3-start-hasura-graphql-server-with-docker-compose) 23 | - [4. **Start `Hasura console`**](#4-start-hasura-console) 24 | - [5. **Open another terminal and install dependencies for NextJs application**](#5-open-another-terminal-and-install-dependencies-for-nextjs-application) 25 | - [6. **Create Google client credentials**](#6-create-google-client-credentials) 26 | - [7. **Start the NextJs application**](#7-start-the-nextjs-application) 27 | - [**Deployment**](#deployment) 28 | - [**Heroku for Hasura application**](#heroku-for-hasura-application) 29 | - [Hasura Config:](#hasura-config) 30 | - [**Get postgres database url at heroku**](#get-postgres-database-url-at-heroku) 31 | - [`](#) 32 | - [**Vercel for NextJs application**](#vercel-for-nextjs-application) 33 | - [NextJs Config (Similar with.env)](#nextjs-config-similar-withenv) 34 | - [Example: With your domain is `domainname.herokuapp.com`](#example-with-your-domain-is-domainnameherokuappcom) 35 | - [Migration flow from development to production.](#migration-flow-from-development-to-production) 36 | - [Prerequisites:](#prerequisites) 37 | - [Custom logic with NextJs API route (serverless)](#custom-logic-with-nextjs-api-route-serverless) 38 | - [License](#license) 39 | 40 | --- 41 | 42 | ## TechStack Features 43 | 44 | This boilerplate is built using the following technologies: 45 | 46 | 1. [NextJs](https://nextjs.org/) (The React Framework 47 | for Production) 48 | 2. [Hasura](https://hasura.io/) (GraphQL engine: supports GraphQL Query, Mutation and Subscription out of the box.) 49 | 3. [GraphQL](https://graphql.org/) (Flexible query language for API layer ~~ REST alternative) 50 | 4. [NextAuth](https://next-auth.js.org/) (Authentication for NextJs power by NextJs [API Routes](https://nextjs.org/docs/api-routes/introduction)) 51 | 5. [Apollo Client](https://www.apollographql.com/docs/react/) (Comprehensive Graphql Client - Automatically update the UI while fetching, caching, and updating the application state.) 52 | 6. [GraphQL Code Generator](https://graphql-code-generator.com/) (Generate react hooks with fully typescript for all your graphql query) 53 | 7. [Tailwindcss](https://tailwindcss.com/) (A utility-first CSS framework) 54 | 8. [Retail UI](https://github.com/sondh0127/retail-ui) (UI libary power by tailwindcss) 55 | 9. [Typescript](https://www.typescriptlang.org/) (Typed JavaScript at Any Scale) 56 | 57 | --- 58 | 59 | ## Requirements 60 | 61 | 1. [Node.js](https://nodejs.org/) Recommended Install via [nvm](https://github.com/nvm-sh/nvm) 62 | 2. [npm](https://www.npmjs.com/) Yeah! for `npm install -g yarn` 63 | 3. [Docker](https://www.docker.com/) and [Docker Compose](https://docs.docker.com/compose/install/) 64 | 4. [Hasura CLI](https://hasura.io/docs/1.0/graphql/core/hasura-cli/index.html) 65 | 66 | --- 67 | 68 | ## Development Step 69 | 70 | ### 1. **Clone the application** 71 | 72 | ```sh 73 | git clone https://github.com/sondh0127/nextjs-hasura-fullstack 74 | ``` 75 | 76 | ### 2. **Run a generateKeys.js (present in root directory)** 77 | 78 | ```sh 79 | node generateKeys.js 80 | ``` 81 | 82 | > It will create `hasura/.env` and `nextjs/.env` files which used to provide authentication and security for the app 83 | 84 | ``` 85 | AUTH_PRIVATE_KEY # Private key 86 | HASURA_GRAPHQL_JWT_SECRET # Public key 87 | HASURA_GRAPHQL_ADMIN_SECRET # Hasura console password 88 | ``` 89 | For Backend: 90 | 91 | ### 3. **Start `Hasura GraphQL server` with docker-compose** 92 | 93 | Starting Docker by using docker-compose which will start our backend app and database services. 94 | 95 | ```sh 96 | cd hasura/ 97 | docker-compose up 98 | ``` 99 | 100 | If everything goes well, it’ll be up and running on http://localhost:8080/v1/graphql. 101 | 102 | ### 4. **Start `Hasura console`** 103 | The `console` will help us automatically create migration and metadata for any change. [Readmore](https://hasura.io/docs/1.0/graphql/core/hasura-cli/hasura_console.html) 104 | 105 | Require [Hasura CLI](#requirements) 106 | 107 | Open console on another terminal 108 | ```sh 109 | cd hasura/ 110 | hasura console --admin-secret 111 | ``` 112 | The console is running on http://localhost:9695. 113 | 114 | For Frontend: 115 | ### 5. **Open another terminal and install dependencies for NextJs application** 116 | 117 | ```sh 118 | cd nextjs/ && yarn 119 | ``` 120 | 121 | ### 6. **Create Google client credentials** 122 | 123 | Create a new [Google OAuth Client](https://console.developers.google.com/apis/credentials/oauthclient) and copy the credentials (Client ID and Client Secret) into `.env` file. 124 | 125 | ```sh 126 | GOOGLE_CLIENT_ID="" 127 | GOOGLE_CLIENT_SECRET="" 128 | ``` 129 | 130 | > Config Authorised redirect URIs: 131 | 132 | ``` 133 | http://localhost:3000/api/auth/callback/google 134 | https://domailname.app/api/auth/callback/google 135 | ``` 136 | 137 | ### 7. **Start the NextJs application** 138 | 139 | ```sh 140 | yarn dev 141 | ``` 142 | 143 | The above command will start the application on [http://localhost:3000/](http://localhost:3000). It also watching for the change of your GraphQL to generate new code by [GraphQL Code Generator](https://graphql-code-generator.com/) 144 | 145 | --- 146 | 147 | ## **Deployment** 148 | 149 | The production ready can be done with Vercel and Heroku platforms by following the instruction. (Free hosting) 150 | 151 | --- 152 | 153 | ### **Heroku for Hasura application** 154 | 155 | To deploy the backend application on Heroku press the button below. You must [register for a free Heroku account](https://signup.heroku.com/). 156 | 157 | [![Deploy to 158 | Heroku](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/sondh0127/nextjs-hasura-fullstack) 159 | 160 | ### Hasura Config: 161 | 162 | > From heroku dashboard: Go to `Settings` => [`Config Vars`] Choose `Reveal Config Vars` => Add the following config 163 | 164 | ```env 165 | HASURA_GRAPHQL_ADMIN_SECRET="secret@123" 166 | 167 | HASURA_GRAPHQL_JWT_SECRET: '' 168 | ``` 169 | 170 | > Calm! You just need to wait Heroku to update config for a while 171 | 172 | ### **Get postgres database url at heroku** 173 | 174 | > From heroku dashboard: Go to `Resources` => `Heroku postgres` addons. In new windows: `Settings` => `View Credentials`... Copy your `DATABASE_URL` from `URI` field with sample format: 175 | > `postgres://postgres:@localhost:5432/postgres` 176 | 177 | ## ` 178 | 179 | ### **Vercel for NextJs application** 180 | 181 | Click on the button below to deploy the frontend application on Vercel. You'll need to [sign up for a free Vercel account](https://vercel.com/signup/). 182 | 183 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/import/git?s=https%3A%2F%2Fgithub.com%2Fsondh0127%2Fnextjs-hasura-fullstack%2Ftree%2Fmaster%2Fnextjs&env=NEXT_PUBLIC_API_URL,NEXT_PUBLIC_WS_URL,DATABASE_URL,NEXTAUTH_URL,GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET,AUTH_PRIVATE_KEY&project-name=nextjs-hasura-fullstack&repo-name=nextjs-hasura-fullstack) 184 | 185 | ### NextJs Config (Similar with.env) 186 | 187 | ### Example: With your domain is `domainname.herokuapp.com` 188 | 189 | Get your [`DATABASE_URL`](#get-postgres-database-url-at-heroku) 190 | 191 | ``` 192 | NEXT_PUBLIC_API_URL=https://domainname.herokuapp.com/v1/graphql 193 | 194 | NEXT_PUBLIC_WS_URL=wss://domainname.herokuapp.com/v1/graphql 195 | 196 | DATABASE_URL=postgres://postgres:@localhost:5432/postgres 197 | 198 | NEXTAUTH_URL=http://localhost:3000 199 | 200 | GOOGLE_CLIENT_ID="" 201 | 202 | GOOGLE_CLIENT_SECRET="" 203 | 204 | AUTH_PRIVATE_KEY="" 205 | 206 | ``` 207 | 208 | --- 209 | 210 | ## Migration flow from development to production. 211 | 212 | ### Prerequisites: 213 | 214 | - [Hasura CLI](https://hasura.io/docs/1.0/graphql/manual/hasura-cli/install-hasura-cli.html) 215 | 216 | Open console: 217 | 218 | ``` 219 | hasura console --admin-secret 220 | ``` 221 | 222 | Run migration with metadata: 223 | 224 | ```sh 225 | hasura migrate apply --endpoint --admin-secret 226 | 227 | hasura metadata apply --endpoint --admin-secret 228 | ``` 229 | 230 | Example: 231 | 232 | ```sh 233 | hasura migrate apply --endpoint https://nextjs-hasura-fullstack-trello.herokuapp.com --admin-secret secret@123 234 | 235 | hasura metadata apply --endpoint https://nextjs-hasura-fullstack-trello.herokuapp.com --admin-secret secret@123 236 | ``` 237 | 238 | --- 239 | 240 | ## Custom logic with NextJs API route (serverless) 241 | 242 | > TODO 243 | 244 | --- 245 | 246 | heroku login 247 | heroku container:login 248 | heroku container:push web -a appname 249 | heroku container:release web -a appname 250 | heroku open 251 | 252 | vercel 253 | 254 | ## License 255 | 256 | This project is licensed under the [MIT License](https://opensource.org/licenses/MIT). 257 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # Init variable for heroku 2 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "NextJs Hasura Fullstack Backend", 3 | "description": "Blazing fast, instant realtime GraphQL APIs on Postgres with fine grained access control and webhook triggers for async business logic.", 4 | "logo": "https://storage.googleapis.com/hasura-graphql-engine/console/assets/favicon.png", 5 | "keywords": ["database", "api", "graphql", "heroku", "postgres", "hasura"], 6 | "success_url": "/console", 7 | "website": "https://hasura.io", 8 | "repository": "https://github.com/sondh0127/nextjs-hasura-fullstack/hasura", 9 | "formation": { 10 | "web": { 11 | "quantity": 1 12 | } 13 | }, 14 | "stack": "container", 15 | "addons": [ 16 | { 17 | "plan": "heroku-postgresql:hobby-dev" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /generateKeys.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const fs = require('fs') 3 | const util = require('util') 4 | const exec = util.promisify(require('child_process').exec) 5 | 6 | async function generateKeys() { 7 | try { 8 | /* -------------------------- Copy env.example ------------------------- */ 9 | 10 | const frontendExample = fs.readFileSync('nextjs/.env.example', 'utf8') 11 | console.log( 12 | `Please create a Google OAuth Client( https://console.developers.google.com/apis/credentials/oauthclient) and copy the credentials to GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET in your .env\n`, 13 | ) 14 | 15 | /* -------------------------- Generate keys --------------------------- */ 16 | 17 | const { stdout: privateKey } = await exec('openssl genrsa 2048') 18 | var randomfn = './' + Math.random().toString(36).substring(7) 19 | fs.writeFileSync(randomfn, privateKey) 20 | const { stdout: publicKey } = await exec( 21 | 'openssl rsa -in ' + randomfn + ' -pubout', 22 | ) 23 | fs.unlinkSync(randomfn) 24 | /* --------------------------------- Config ----------------------------- */ 25 | const AUTH_PRIVATE_KEY = { 26 | type: 'RS512', 27 | key: privateKey, 28 | } 29 | 30 | const HASURA_GRAPHQL_JWT_SECRET = { 31 | type: 'RS512', 32 | key: publicKey, 33 | } 34 | 35 | const frontendEnv = frontendExample + `\n` + 36 | `AUTH_PRIVATE_KEY='${JSON.stringify(AUTH_PRIVATE_KEY)}'\n` + 37 | `HASURA_GRAPHQL_ADMIN_SECRET=secret@123\n` 38 | 39 | const backendEnv = 40 | `HASURA_GRAPHQL_ADMIN_SECRET=secret@123\n` + 41 | `HASURA_GRAPHQL_JWT_SECRET='${JSON.stringify(HASURA_GRAPHQL_JWT_SECRET,)}'\n` 42 | 43 | fs.writeFileSync('nextjs/.env', frontendEnv) 44 | fs.writeFileSync('hasura/.env', backendEnv) 45 | 46 | const logString = 47 | 'Secret keys was generated in nextjs/.env and hasura/.env\n' 48 | console.log(logString) 49 | } catch (err) { 50 | console.error(err) 51 | } 52 | } 53 | generateKeys() 54 | -------------------------------------------------------------------------------- /hasura/.dockerignore: -------------------------------------------------------------------------------- 1 | # https://docs.docker.com/engine/reference/builder/#dockerignore-file 2 | # Don't send the following files to the docker daemon as the build context. 3 | 4 | .git 5 | .gitattributes 6 | .travis.yml 7 | README.md 8 | docker-compose.yml 9 | -------------------------------------------------------------------------------- /hasura/.gitignore: -------------------------------------------------------------------------------- 1 | .env -------------------------------------------------------------------------------- /hasura/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM hasura/graphql-engine:v1.3.2.cli-migrations-v2 2 | 3 | # Copy migrations directory 4 | COPY ./migrations /hasura-migrations 5 | 6 | # Copy metadata directory 7 | COPY ./metadata /hasura-metadata 8 | 9 | # Enable the console 10 | ENV HASURA_GRAPHQL_ENABLE_CONSOLE=true 11 | 12 | # Enable debugging mode. It should be disabled in production. 13 | ENV HASURA_GRAPHQL_DEV_MODE=true 14 | 15 | # Heroku hobby tier PG has few limitations including 20 max connections 16 | # https://devcenter.heroku.com/articles/heroku-postgres-plans#hobby-tier 17 | ENV HASURA_GRAPHQL_PG_CONNECTIONS=15 18 | 19 | # https://github.com/hasura/graphql-engine/issues/4651#issuecomment-623414531 20 | ENV HASURA_GRAPHQL_CLI_ENVIRONMENT=default 21 | 22 | # https://github.com/hasura/graphql-engine/issues/5172#issuecomment-653774367 23 | ENV HASURA_GRAPHQL_MIGRATIONS_DATABASE_ENV_VAR=DATABASE_URL 24 | 25 | # Enable JWT 26 | ENV HASURA_GRAPHQL_JWT_SECRET=HASURA_GRAPHQL_JWT_SECRET 27 | 28 | # Secure the GraphQL endpoint 29 | ENV HASURA_GRAPHQL_ADMIN_SECRET=HASURA_GRAPHQL_ADMIN_SECRET 30 | 31 | # Change $DATABASE_URL to your heroku postgres URL if you're not using 32 | # the primary postgres instance in your app 33 | CMD graphql-engine \ 34 | --database-url $DATABASE_URL \ 35 | serve \ 36 | --server-port $PORT -------------------------------------------------------------------------------- /hasura/config.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | endpoint: http://localhost:8080 3 | metadata_directory: metadata 4 | actions: 5 | kind: synchronous 6 | handler_webhook_baseurl: http://localhost:3000 7 | -------------------------------------------------------------------------------- /hasura/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.6" 2 | services: 3 | db: 4 | container_name: nextjs-hasura-fullstack-db 5 | image: postgres:11.3-alpine 6 | ports: 7 | - "5432:5432" 8 | volumes: 9 | - db_data:/var/lib/postgresql/data 10 | restart: unless-stopped 11 | pgadmin: 12 | container_name: nextjs-hasura-fullstack-db-admin 13 | image: dpage/pgadmin4 14 | restart: always 15 | depends_on: 16 | - db 17 | ports: 18 | - 5050:80 19 | environment: 20 | PGADMIN_DEFAULT_EMAIL: pgadmin@example.com 21 | PGADMIN_DEFAULT_PASSWORD: admin 22 | backend: 23 | container_name: nextjs-hasura-fullstack-backend 24 | image: nextjs-hasura-fullstack-backend 25 | build: 26 | context: . 27 | ports: 28 | - "8080:8080" 29 | depends_on: 30 | - db 31 | restart: on-failure 32 | environment: 33 | DATABASE_URL: postgres://postgres:@db:5432/postgres 34 | PORT: 8080 35 | HASURA_GRAPHQL_ADMIN_SECRET: "${HASURA_GRAPHQL_ADMIN_SECRET}" 36 | HASURA_GRAPHQL_JWT_SECRET: "${HASURA_GRAPHQL_JWT_SECRET}" 37 | volumes: 38 | db_data: 39 | -------------------------------------------------------------------------------- /hasura/metadata/actions.graphql: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /hasura/metadata/actions.yaml: -------------------------------------------------------------------------------- 1 | actions: [] 2 | custom_types: 3 | enums: [] 4 | input_objects: [] 5 | objects: [] 6 | scalars: [] 7 | -------------------------------------------------------------------------------- /hasura/metadata/allow_list.yaml: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /hasura/metadata/cron_triggers.yaml: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /hasura/metadata/functions.yaml: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /hasura/metadata/query_collections.yaml: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /hasura/metadata/remote_schemas.yaml: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /hasura/metadata/tables.yaml: -------------------------------------------------------------------------------- 1 | - table: 2 | schema: public 3 | name: accounts 4 | - table: 5 | schema: public 6 | name: boards 7 | object_relationships: 8 | - name: user 9 | using: 10 | foreign_key_constraint_on: user_id 11 | array_relationships: 12 | - name: boards_users 13 | using: 14 | foreign_key_constraint_on: 15 | column: board_id 16 | table: 17 | schema: public 18 | name: boards_users 19 | - name: cards 20 | using: 21 | foreign_key_constraint_on: 22 | column: board_id 23 | table: 24 | schema: public 25 | name: cards 26 | - name: lists 27 | using: 28 | foreign_key_constraint_on: 29 | column: board_id 30 | table: 31 | schema: public 32 | name: lists 33 | insert_permissions: 34 | - role: user 35 | permission: 36 | check: 37 | user_id: 38 | _eq: X-Hasura-User-Id 39 | set: 40 | user_id: x-hasura-User-Id 41 | columns: 42 | - icon 43 | - name 44 | backend_only: false 45 | select_permissions: 46 | - role: user 47 | permission: 48 | columns: 49 | - created_at 50 | - icon 51 | - id 52 | - name 53 | - updated_at 54 | - user_id 55 | filter: 56 | _or: 57 | - user_id: 58 | _eq: X-Hasura-User-Id 59 | allow_aggregations: true 60 | update_permissions: 61 | - role: user 62 | permission: 63 | columns: 64 | - icon 65 | - name 66 | filter: 67 | user_id: 68 | _eq: X-Hasura-User-Id 69 | check: null 70 | delete_permissions: 71 | - role: user 72 | permission: 73 | filter: 74 | user_id: 75 | _eq: X-Hasura-User-Id 76 | - table: 77 | schema: public 78 | name: boards_users 79 | object_relationships: 80 | - name: board 81 | using: 82 | foreign_key_constraint_on: board_id 83 | - name: user 84 | using: 85 | foreign_key_constraint_on: user_id 86 | insert_permissions: 87 | - role: user 88 | permission: 89 | check: 90 | board: 91 | user_id: 92 | _eq: X-Hasura-User-Id 93 | columns: 94 | - board_id 95 | - user_id 96 | backend_only: false 97 | select_permissions: 98 | - role: user 99 | permission: 100 | columns: 101 | - id 102 | - board_id 103 | - user_id 104 | - created_at 105 | - updated_at 106 | filter: 107 | user_id: 108 | _eq: X-Hasura-User-Id 109 | allow_aggregations: true 110 | - table: 111 | schema: public 112 | name: cards 113 | object_relationships: 114 | - name: board 115 | using: 116 | foreign_key_constraint_on: board_id 117 | - name: list 118 | using: 119 | foreign_key_constraint_on: list_id 120 | insert_permissions: 121 | - role: user 122 | permission: 123 | check: 124 | board: 125 | user_id: 126 | _eq: X-Hasura-User-Id 127 | columns: 128 | - board_id 129 | - description 130 | - list_id 131 | - position 132 | - title 133 | backend_only: false 134 | select_permissions: 135 | - role: user 136 | permission: 137 | columns: 138 | - id 139 | - description 140 | - list_id 141 | - board_id 142 | - position 143 | - title 144 | - created_at 145 | - updated_at 146 | filter: 147 | board: 148 | user_id: 149 | _eq: X-Hasura-User-Id 150 | allow_aggregations: true 151 | update_permissions: 152 | - role: user 153 | permission: 154 | columns: 155 | - description 156 | - list_id 157 | - position 158 | - title 159 | filter: 160 | board: 161 | user_id: 162 | _eq: X-Hasura-User-Id 163 | check: null 164 | delete_permissions: 165 | - role: user 166 | permission: 167 | filter: 168 | board: 169 | user_id: 170 | _eq: X-Hasura-User-Id 171 | - table: 172 | schema: public 173 | name: feeds 174 | object_relationships: 175 | - name: user 176 | using: 177 | foreign_key_constraint_on: user_id 178 | insert_permissions: 179 | - role: user 180 | permission: 181 | check: 182 | user_id: 183 | _eq: X-Hasura-User-Id 184 | set: 185 | user_id: x-hasura-user-id 186 | columns: 187 | - body 188 | backend_only: false 189 | select_permissions: 190 | - role: user 191 | permission: 192 | columns: 193 | - id 194 | - user_id 195 | - body 196 | - created_at 197 | - updated_at 198 | filter: {} 199 | update_permissions: 200 | - role: user 201 | permission: 202 | columns: 203 | - body 204 | filter: 205 | user_id: 206 | _eq: X-Hasura-User-Id 207 | check: null 208 | delete_permissions: 209 | - role: user 210 | permission: 211 | filter: 212 | user_id: 213 | _eq: X-Hasura-User-Id 214 | - table: 215 | schema: public 216 | name: lists 217 | object_relationships: 218 | - name: board 219 | using: 220 | foreign_key_constraint_on: board_id 221 | array_relationships: 222 | - name: cards 223 | using: 224 | foreign_key_constraint_on: 225 | column: list_id 226 | table: 227 | schema: public 228 | name: cards 229 | insert_permissions: 230 | - role: user 231 | permission: 232 | check: 233 | _or: 234 | - board: 235 | user_id: 236 | _eq: X-Hasura-User-Id 237 | columns: 238 | - board_id 239 | - id 240 | - name 241 | - position 242 | backend_only: false 243 | select_permissions: 244 | - role: user 245 | permission: 246 | columns: 247 | - board_id 248 | - created_at 249 | - id 250 | - name 251 | - position 252 | - updated_at 253 | filter: 254 | board: 255 | user_id: 256 | _eq: X-Hasura-User-Id 257 | allow_aggregations: true 258 | update_permissions: 259 | - role: user 260 | permission: 261 | columns: 262 | - name 263 | - position 264 | filter: 265 | _or: 266 | - board: 267 | user_id: 268 | _eq: X-Hasura-User-Id 269 | check: null 270 | delete_permissions: 271 | - role: user 272 | permission: 273 | filter: 274 | _or: 275 | - board: 276 | user_id: 277 | _eq: X-Hasura-User-Id 278 | - table: 279 | schema: public 280 | name: sessions 281 | - table: 282 | schema: public 283 | name: users 284 | array_relationships: 285 | - name: boards 286 | using: 287 | foreign_key_constraint_on: 288 | column: user_id 289 | table: 290 | schema: public 291 | name: boards 292 | - name: boards_users 293 | using: 294 | foreign_key_constraint_on: 295 | column: user_id 296 | table: 297 | schema: public 298 | name: boards_users 299 | - name: feeds 300 | using: 301 | foreign_key_constraint_on: 302 | column: user_id 303 | table: 304 | schema: public 305 | name: feeds 306 | insert_permissions: 307 | - role: user 308 | permission: 309 | check: 310 | id: 311 | _eq: X-Hasura-User-Id 312 | columns: 313 | - image 314 | - name 315 | backend_only: false 316 | select_permissions: 317 | - role: user 318 | permission: 319 | columns: 320 | - id 321 | - name 322 | - email 323 | - email_verified 324 | - image 325 | - created_at 326 | - updated_at 327 | filter: {} 328 | allow_aggregations: true 329 | - table: 330 | schema: public 331 | name: verification_requests 332 | -------------------------------------------------------------------------------- /hasura/metadata/version.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | -------------------------------------------------------------------------------- /hasura/migrations/1597498463371_create_table_public_users/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE "public"."users"; 2 | -------------------------------------------------------------------------------- /hasura/migrations/1597498463371_create_table_public_users/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE "public"."users"("id" serial NOT NULL, "name" text, "email" text NOT NULL, "email_verified" timestamptz, "image" text, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id") ); 2 | CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"() 3 | RETURNS TRIGGER AS $$ 4 | DECLARE 5 | _new record; 6 | BEGIN 7 | _new := NEW; 8 | _new."updated_at" = NOW(); 9 | RETURN _new; 10 | END; 11 | $$ LANGUAGE plpgsql; 12 | CREATE TRIGGER "set_public_users_updated_at" 13 | BEFORE UPDATE ON "public"."users" 14 | FOR EACH ROW 15 | EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"(); 16 | COMMENT ON TRIGGER "set_public_users_updated_at" ON "public"."users" 17 | IS 'trigger to set value of column "updated_at" to current timestamp on row update'; 18 | -------------------------------------------------------------------------------- /hasura/migrations/1597498548202_create_table_public_accounts/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE "public"."accounts"; 2 | -------------------------------------------------------------------------------- /hasura/migrations/1597498548202_create_table_public_accounts/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE "public"."accounts"("id" serial NOT NULL, "compound_id" text NOT NULL, "user_id" integer NOT NULL, "provider_type" text NOT NULL, "provider_id" text NOT NULL, "provider_account_id" text NOT NULL, "refresh_token" text, "access_token" text, "access_token_expires" timestamptz, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id") ); 2 | CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"() 3 | RETURNS TRIGGER AS $$ 4 | DECLARE 5 | _new record; 6 | BEGIN 7 | _new := NEW; 8 | _new."updated_at" = NOW(); 9 | RETURN _new; 10 | END; 11 | $$ LANGUAGE plpgsql; 12 | CREATE TRIGGER "set_public_accounts_updated_at" 13 | BEFORE UPDATE ON "public"."accounts" 14 | FOR EACH ROW 15 | EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"(); 16 | COMMENT ON TRIGGER "set_public_accounts_updated_at" ON "public"."accounts" 17 | IS 'trigger to set value of column "updated_at" to current timestamp on row update'; 18 | -------------------------------------------------------------------------------- /hasura/migrations/1597498609423_create_table_public_sessions/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE "public"."sessions"; 2 | -------------------------------------------------------------------------------- /hasura/migrations/1597498609423_create_table_public_sessions/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE "public"."sessions"("id" serial NOT NULL, "user_id" text NOT NULL, "expires" timestamptz NOT NULL, "session_token" text NOT NULL, "access_token" text NOT NULL, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id") ); 2 | CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"() 3 | RETURNS TRIGGER AS $$ 4 | DECLARE 5 | _new record; 6 | BEGIN 7 | _new := NEW; 8 | _new."updated_at" = NOW(); 9 | RETURN _new; 10 | END; 11 | $$ LANGUAGE plpgsql; 12 | CREATE TRIGGER "set_public_sessions_updated_at" 13 | BEFORE UPDATE ON "public"."sessions" 14 | FOR EACH ROW 15 | EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"(); 16 | COMMENT ON TRIGGER "set_public_sessions_updated_at" ON "public"."sessions" 17 | IS 'trigger to set value of column "updated_at" to current timestamp on row update'; 18 | -------------------------------------------------------------------------------- /hasura/migrations/1597498658823_create_table_public_verification_requests/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE "public"."verification_requests"; 2 | -------------------------------------------------------------------------------- /hasura/migrations/1597498658823_create_table_public_verification_requests/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE "public"."verification_requests"("id" serial NOT NULL, "identifier" text NOT NULL, "token" text NOT NULL, "expires" timestamptz NOT NULL, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id") ); 2 | CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"() 3 | RETURNS TRIGGER AS $$ 4 | DECLARE 5 | _new record; 6 | BEGIN 7 | _new := NEW; 8 | _new."updated_at" = NOW(); 9 | RETURN _new; 10 | END; 11 | $$ LANGUAGE plpgsql; 12 | CREATE TRIGGER "set_public_verification_requests_updated_at" 13 | BEFORE UPDATE ON "public"."verification_requests" 14 | FOR EACH ROW 15 | EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"(); 16 | COMMENT ON TRIGGER "set_public_verification_requests_updated_at" ON "public"."verification_requests" 17 | IS 'trigger to set value of column "updated_at" to current timestamp on row update'; 18 | -------------------------------------------------------------------------------- /hasura/migrations/1597498709972_create_table_public_feeds/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE "public"."feeds"; 2 | -------------------------------------------------------------------------------- /hasura/migrations/1597498709972_create_table_public_feeds/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE "public"."feeds"("id" serial NOT NULL, "user_id" integer NOT NULL, "body" text NOT NULL, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id") ); 2 | CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"() 3 | RETURNS TRIGGER AS $$ 4 | DECLARE 5 | _new record; 6 | BEGIN 7 | _new := NEW; 8 | _new."updated_at" = NOW(); 9 | RETURN _new; 10 | END; 11 | $$ LANGUAGE plpgsql; 12 | CREATE TRIGGER "set_public_feeds_updated_at" 13 | BEFORE UPDATE ON "public"."feeds" 14 | FOR EACH ROW 15 | EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"(); 16 | COMMENT ON TRIGGER "set_public_feeds_updated_at" ON "public"."feeds" 17 | IS 'trigger to set value of column "updated_at" to current timestamp on row update'; 18 | -------------------------------------------------------------------------------- /hasura/migrations/1597498729906_set_fk_public_feeds_user_id/down.sql: -------------------------------------------------------------------------------- 1 | alter table "public"."feeds" drop constraint "feeds_user_id_fkey"; 2 | -------------------------------------------------------------------------------- /hasura/migrations/1597498729906_set_fk_public_feeds_user_id/up.sql: -------------------------------------------------------------------------------- 1 | alter table "public"."feeds" 2 | add constraint "feeds_user_id_fkey" 3 | foreign key ("user_id") 4 | references "public"."users" 5 | ("id") on update restrict on delete restrict; 6 | -------------------------------------------------------------------------------- /hasura/migrations/1597504190869_create_table_public_boards/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE "public"."boards"; 2 | -------------------------------------------------------------------------------- /hasura/migrations/1597504190869_create_table_public_boards/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE "public"."boards"("id" serial NOT NULL, "name" text NOT NULL, "user_id" integer NOT NULL, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id") , FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON UPDATE restrict ON DELETE restrict); 2 | CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"() 3 | RETURNS TRIGGER AS $$ 4 | DECLARE 5 | _new record; 6 | BEGIN 7 | _new := NEW; 8 | _new."updated_at" = NOW(); 9 | RETURN _new; 10 | END; 11 | $$ LANGUAGE plpgsql; 12 | CREATE TRIGGER "set_public_boards_updated_at" 13 | BEFORE UPDATE ON "public"."boards" 14 | FOR EACH ROW 15 | EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"(); 16 | COMMENT ON TRIGGER "set_public_boards_updated_at" ON "public"."boards" 17 | IS 'trigger to set value of column "updated_at" to current timestamp on row update'; 18 | -------------------------------------------------------------------------------- /hasura/migrations/1597505181237_create_table_public_boards_users/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE "public"."boards_users"; 2 | -------------------------------------------------------------------------------- /hasura/migrations/1597505181237_create_table_public_boards_users/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE "public"."boards_users"("id" serial NOT NULL, "board_id" integer NOT NULL, "user_id" integer NOT NULL, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id") , FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON UPDATE restrict ON DELETE restrict, FOREIGN KEY ("board_id") REFERENCES "public"."boards"("id") ON UPDATE restrict ON DELETE restrict); 2 | CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"() 3 | RETURNS TRIGGER AS $$ 4 | DECLARE 5 | _new record; 6 | BEGIN 7 | _new := NEW; 8 | _new."updated_at" = NOW(); 9 | RETURN _new; 10 | END; 11 | $$ LANGUAGE plpgsql; 12 | CREATE TRIGGER "set_public_boards_users_updated_at" 13 | BEFORE UPDATE ON "public"."boards_users" 14 | FOR EACH ROW 15 | EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"(); 16 | COMMENT ON TRIGGER "set_public_boards_users_updated_at" ON "public"."boards_users" 17 | IS 'trigger to set value of column "updated_at" to current timestamp on row update'; 18 | -------------------------------------------------------------------------------- /hasura/migrations/1597505238023_create_table_public_lists/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE "public"."lists"; 2 | -------------------------------------------------------------------------------- /hasura/migrations/1597505238023_create_table_public_lists/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE "public"."lists"("id" serial NOT NULL, "name" text NOT NULL, "position" integer NOT NULL, "board_id" integer NOT NULL, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id") , FOREIGN KEY ("board_id") REFERENCES "public"."boards"("id") ON UPDATE restrict ON DELETE restrict); 2 | CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"() 3 | RETURNS TRIGGER AS $$ 4 | DECLARE 5 | _new record; 6 | BEGIN 7 | _new := NEW; 8 | _new."updated_at" = NOW(); 9 | RETURN _new; 10 | END; 11 | $$ LANGUAGE plpgsql; 12 | CREATE TRIGGER "set_public_lists_updated_at" 13 | BEFORE UPDATE ON "public"."lists" 14 | FOR EACH ROW 15 | EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"(); 16 | COMMENT ON TRIGGER "set_public_lists_updated_at" ON "public"."lists" 17 | IS 'trigger to set value of column "updated_at" to current timestamp on row update'; 18 | -------------------------------------------------------------------------------- /hasura/migrations/1597505340738_create_table_public_cards/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE "public"."cards"; 2 | -------------------------------------------------------------------------------- /hasura/migrations/1597505340738_create_table_public_cards/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE "public"."cards"("id" serial NOT NULL, "description" text NOT NULL, "list_id" integer NOT NULL, "board_id" integer NOT NULL, "position" integer NOT NULL, "title" text NOT NULL, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id") , FOREIGN KEY ("list_id") REFERENCES "public"."lists"("id") ON UPDATE restrict ON DELETE restrict, FOREIGN KEY ("board_id") REFERENCES "public"."boards"("id") ON UPDATE restrict ON DELETE restrict); 2 | CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"() 3 | RETURNS TRIGGER AS $$ 4 | DECLARE 5 | _new record; 6 | BEGIN 7 | _new := NEW; 8 | _new."updated_at" = NOW(); 9 | RETURN _new; 10 | END; 11 | $$ LANGUAGE plpgsql; 12 | CREATE TRIGGER "set_public_cards_updated_at" 13 | BEFORE UPDATE ON "public"."cards" 14 | FOR EACH ROW 15 | EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"(); 16 | COMMENT ON TRIGGER "set_public_cards_updated_at" ON "public"."cards" 17 | IS 'trigger to set value of column "updated_at" to current timestamp on row update'; 18 | -------------------------------------------------------------------------------- /hasura/migrations/1597577533977_alter_table_public_lists_alter_column_position/down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "public"."lists" DROP CONSTRAINT "lists_position_key"; 2 | -------------------------------------------------------------------------------- /hasura/migrations/1597577533977_alter_table_public_lists_alter_column_position/up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "public"."lists" ADD CONSTRAINT "lists_position_key" UNIQUE ("position"); 2 | -------------------------------------------------------------------------------- /hasura/migrations/1597577544193_alter_table_public_lists_drop_constraint_lists_position_key/down.sql: -------------------------------------------------------------------------------- 1 | alter table "public"."lists" add constraint "lists_position_key" unique ("position"); 2 | -------------------------------------------------------------------------------- /hasura/migrations/1597577544193_alter_table_public_lists_drop_constraint_lists_position_key/up.sql: -------------------------------------------------------------------------------- 1 | alter table "public"."lists" drop constraint "lists_position_key"; 2 | -------------------------------------------------------------------------------- /hasura/migrations/1597577650903_alter_table_public_lists_alter_column_position/down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "public"."lists" DROP CONSTRAINT "lists_position_key"; 2 | -------------------------------------------------------------------------------- /hasura/migrations/1597577650903_alter_table_public_lists_alter_column_position/up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "public"."lists" ADD CONSTRAINT "lists_position_key" UNIQUE ("position"); 2 | -------------------------------------------------------------------------------- /hasura/migrations/1597577665522_alter_table_public_cards_alter_column_position/down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "public"."cards" DROP CONSTRAINT "cards_position_key"; 2 | -------------------------------------------------------------------------------- /hasura/migrations/1597577665522_alter_table_public_cards_alter_column_position/up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "public"."cards" ADD CONSTRAINT "cards_position_key" UNIQUE ("position"); 2 | -------------------------------------------------------------------------------- /hasura/migrations/1597601412648_alter_table_public_lists_alter_column_position/down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "public"."lists" ADD CONSTRAINT "lists_position_key" UNIQUE ("position"); 2 | -------------------------------------------------------------------------------- /hasura/migrations/1597601412648_alter_table_public_lists_alter_column_position/up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "public"."lists" DROP CONSTRAINT "lists_position_key"; 2 | -------------------------------------------------------------------------------- /hasura/migrations/1597601429537_alter_table_public_lists_add_unique_board_id_position/down.sql: -------------------------------------------------------------------------------- 1 | alter table "public"."lists" drop constraint "lists_board_id_position_key"; 2 | -------------------------------------------------------------------------------- /hasura/migrations/1597601429537_alter_table_public_lists_add_unique_board_id_position/up.sql: -------------------------------------------------------------------------------- 1 | alter table "public"."lists" add constraint "lists_board_id_position_key" unique ("board_id", "position"); 2 | -------------------------------------------------------------------------------- /hasura/migrations/1599496540352_alter_table_public_boards_add_column_icon/down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "public"."boards" DROP COLUMN "icon"; 2 | -------------------------------------------------------------------------------- /hasura/migrations/1599496540352_alter_table_public_boards_add_column_icon/up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "public"."boards" ADD COLUMN "icon" text NOT NULL DEFAULT '📑'; 2 | -------------------------------------------------------------------------------- /hasura/migrations/1599632432195_alter_table_public_lists_drop_constraint_lists_board_id_position_key/down.sql: -------------------------------------------------------------------------------- 1 | alter table "public"."lists" add constraint "lists_board_id_position_key" unique ("board_id", "position"); 2 | -------------------------------------------------------------------------------- /hasura/migrations/1599632432195_alter_table_public_lists_drop_constraint_lists_board_id_position_key/up.sql: -------------------------------------------------------------------------------- 1 | alter table "public"."lists" drop constraint "lists_board_id_position_key"; 2 | -------------------------------------------------------------------------------- /hasura/migrations/1599670620301_alter_table_public_lists_alter_column_position/down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "public"."lists" ALTER COLUMN "position" TYPE integer; 2 | -------------------------------------------------------------------------------- /hasura/migrations/1599670620301_alter_table_public_lists_alter_column_position/up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "public"."lists" ALTER COLUMN "position" TYPE numeric; 2 | -------------------------------------------------------------------------------- /hasura/migrations/1599670653341_alter_table_public_cards_alter_column_position/down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "public"."cards" ALTER COLUMN "position" TYPE integer; 2 | ALTER TABLE "public"."cards" ADD CONSTRAINT "cards_position_key" UNIQUE ("position"); 3 | -------------------------------------------------------------------------------- /hasura/migrations/1599670653341_alter_table_public_cards_alter_column_position/up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "public"."cards" ALTER COLUMN "position" TYPE numeric; 2 | ALTER TABLE "public"."cards" DROP CONSTRAINT "cards_position_key"; 3 | -------------------------------------------------------------------------------- /hasura/migrations/1599915366833_set_fk_public_cards_list_id/down.sql: -------------------------------------------------------------------------------- 1 | alter table "public"."cards" drop constraint "cards_list_id_fkey", 2 | add constraint "cards_list_id_fkey" 3 | foreign key ("list_id") 4 | references "public"."lists" 5 | ("id") 6 | on update restrict 7 | on delete restrict; 8 | -------------------------------------------------------------------------------- /hasura/migrations/1599915366833_set_fk_public_cards_list_id/up.sql: -------------------------------------------------------------------------------- 1 | alter table "public"."cards" drop constraint "cards_list_id_fkey", 2 | add constraint "cards_list_id_fkey" 3 | foreign key ("list_id") 4 | references "public"."lists" 5 | ("id") on update restrict on delete cascade; 6 | -------------------------------------------------------------------------------- /hasura/migrations/1599915561792_set_fk_public_cards_board_id/down.sql: -------------------------------------------------------------------------------- 1 | alter table "public"."cards" drop constraint "cards_board_id_fkey", 2 | add constraint "cards_board_id_fkey" 3 | foreign key ("board_id") 4 | references "public"."boards" 5 | ("id") 6 | on update restrict 7 | on delete restrict; 8 | -------------------------------------------------------------------------------- /hasura/migrations/1599915561792_set_fk_public_cards_board_id/up.sql: -------------------------------------------------------------------------------- 1 | alter table "public"."cards" drop constraint "cards_board_id_fkey", 2 | add constraint "cards_board_id_fkey" 3 | foreign key ("board_id") 4 | references "public"."boards" 5 | ("id") on update restrict on delete cascade; 6 | -------------------------------------------------------------------------------- /hasura/migrations/1599915623950_set_fk_public_lists_board_id/down.sql: -------------------------------------------------------------------------------- 1 | alter table "public"."lists" drop constraint "lists_board_id_fkey", 2 | add constraint "lists_board_id_fkey" 3 | foreign key ("board_id") 4 | references "public"."boards" 5 | ("id") 6 | on update restrict 7 | on delete restrict; 8 | -------------------------------------------------------------------------------- /hasura/migrations/1599915623950_set_fk_public_lists_board_id/up.sql: -------------------------------------------------------------------------------- 1 | alter table "public"."lists" drop constraint "lists_board_id_fkey", 2 | add constraint "lists_board_id_fkey" 3 | foreign key ("board_id") 4 | references "public"."boards" 5 | ("id") on update restrict on delete cascade; 6 | -------------------------------------------------------------------------------- /heroku.yml: -------------------------------------------------------------------------------- 1 | build: 2 | docker: 3 | web: ./hasura/Dockerfile 4 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Son Do Hong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /nextjs/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["next/babel"], 3 | "plugins": [] 4 | } 5 | -------------------------------------------------------------------------------- /nextjs/.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_API_URL=http://localhost:8080/v1/graphql 2 | NEXT_PUBLIC_WS_URL=ws://localhost:8080/v1/graphql 3 | DATABASE_URL=postgres://postgres:@localhost:5432/postgres 4 | EMAIL_SERVER="" 5 | EMAIL_FROM=noreply@example.com 6 | NEXTAUTH_URL=http://localhost:3000 7 | GOOGLE_CLIENT_ID="" 8 | GOOGLE_CLIENT_SECRET="" -------------------------------------------------------------------------------- /nextjs/.eslintignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.next/ -------------------------------------------------------------------------------- /nextjs/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, // Make sure eslint picks up the config at the root of the directory 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 2020, // Use the latest ecmascript standard 6 | "sourceType": "module", // Allows using import/export statements 7 | "ecmaFeatures": { 8 | "jsx": true // Enable JSX since we're using React 9 | } 10 | }, 11 | "settings": { 12 | "react": { 13 | "version": "detect" // Automatically detect the react version 14 | } 15 | }, 16 | "env": { 17 | "browser": true, // Enables browser globals like window and document 18 | "amd": true, // Enables require() and define() as global variables as per the amd spec. 19 | "node": true // Enables Node.js global variables and Node.js scoping. 20 | }, 21 | /* Guest: If we want to customize a plugin: should input that in the plugin list */ 22 | "plugins": ["simple-import-sort", "@typescript-eslint"], 23 | "extends": [ 24 | /* Eslint typescript */ 25 | "eslint:recommended", 26 | "prettier", 27 | "prettier/react", 28 | "prettier/@typescript-eslint", 29 | "plugin:@typescript-eslint/eslint-recommended", 30 | "plugin:@typescript-eslint/recommended", 31 | "plugin:prettier/recommended", 32 | "plugin:react/recommended", 33 | "plugin:react-hooks/recommended", 34 | "plugin:jsx-a11y/recommended" 35 | ], 36 | "rules": { 37 | // Include .prettierrc.js rules 38 | "prettier/prettier": ["error", {}, { "usePrettierrc": true }], // Use our .prettierrc file as source 39 | 40 | "simple-import-sort/sort": "error", 41 | 42 | // Targeting Next.js 43 | "react/prop-types": "off", 44 | "react/react-in-jsx-scope": "off", 45 | 46 | /* @typescript-eslint */ 47 | "@typescript-eslint/ban-types": "warn", 48 | "@typescript-eslint/explicit-module-boundary-types": "off", 49 | "@typescript-eslint/ban-ts-comment": "off" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /nextjs/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | .env 33 | .env.key 34 | 35 | # vercel 36 | .vercel -------------------------------------------------------------------------------- /nextjs/.prettierignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.next/ 3 | -------------------------------------------------------------------------------- /nextjs/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "singleQuote": true, 4 | "semi": false, 5 | "tabWidth": 2, 6 | "trailingComma": "all", 7 | "bracketSpacing": true 8 | } 9 | -------------------------------------------------------------------------------- /nextjs/codegen.yml: -------------------------------------------------------------------------------- 1 | schema: 2 | - "http://localhost:8080/v1/graphql": 3 | headers: 4 | x-hasura-admin-secret: ${HASURA_GRAPHQL_ADMIN_SECRET} 5 | documents: 6 | - "./src/**/*.graphql" 7 | watch: true 8 | config: 9 | scalars: 10 | DateTime: Date 11 | JSON: "{ [key: string]: any }" 12 | timestamptz: string 13 | numeric: number 14 | overwrite: true 15 | generates: 16 | ./src/generated/graphql.tsx: 17 | plugins: 18 | - "typescript" 19 | - "typescript-operations" 20 | - "typescript-react-apollo" 21 | config: 22 | maybeValue: T | undefined 23 | withHooks: true 24 | withComponent: false 25 | withHOC: false 26 | hooks: 27 | afterOneFileWrite: 28 | - prettier --write 29 | ./graphql.schema.json: 30 | plugins: 31 | - "introspection" 32 | -------------------------------------------------------------------------------- /nextjs/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /nextjs/next.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const withPlugins = require('next-compose-plugins') 3 | 4 | // next.js configuration 5 | const nextConfig = { 6 | // https://nextjs.org/docs/api-reference/next.config.js/react-strict-mode 7 | reactStrictMode: true, 8 | } 9 | 10 | module.exports = withPlugins([], nextConfig) 11 | -------------------------------------------------------------------------------- /nextjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-hasura-fullstack-frontend", 3 | "version": "0.2.0", 4 | "author": "sondh0127@gmail.com", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "concurrently \"next dev\" \"yarn generate --watch\"", 8 | "build:analyze": "ANALYZE=true next build && tsc --project tsconfig.server.json", 9 | "build": "next build", 10 | "export": "next export", 11 | "start": "next start", 12 | "lint": "eslint --fix .", 13 | "format": "prettier --write './**/*.{js,jsx,ts,tsx,css,md,json}' --config ./.prettierrc", 14 | "generate": "graphql-codegen --config codegen.yml --require dotenv/config", 15 | "postinstall": "npx @prisma/cli generate" 16 | }, 17 | "husky": { 18 | "hooks": { 19 | "pre-commit": "lint-staged" 20 | } 21 | }, 22 | "lint-staged": { 23 | "./**/*.{js,jsx,ts,tsx}": [ 24 | "eslint --fix" 25 | ] 26 | }, 27 | "dependencies": { 28 | "@apollo/client": "^3.1.5", 29 | "@babel/core": "^7.11.6", 30 | "@badrap/bar-of-progress": "^0.1.1", 31 | "@graphql-codegen/typescript-react-apollo": "^2.0.6", 32 | "@prisma/client": "^2.6.2", 33 | "@retail-ui/core": "^0.6.11", 34 | "@tailwindcss/ui": "^0.6.0", 35 | "@types/react-beautiful-dnd": "^13.0.0", 36 | "clsx": "^1.1.1", 37 | "dayjs": "^1.8.35", 38 | "dotenv": "^8.2.0", 39 | "esm": "^3.2.25", 40 | "graphql": "^15.3.0", 41 | "immer": "^7.0.9", 42 | "isomorphic-unfetch": "^3.0.0", 43 | "isomorphic-ws": "^4.0.1", 44 | "jsonwebtoken": "^8.5.1", 45 | "next": "^9.5.3", 46 | "next-auth": "^3.1.0", 47 | "pg": "^8.3.3", 48 | "postcss-flexbugs-fixes": "^4.2.1", 49 | "postcss-preset-env": "^6.7.0", 50 | "react": "16.13.1", 51 | "react-beautiful-dnd": "^13.0.0", 52 | "react-dom": "16.13.1", 53 | "react-scrollbars-custom": "^4.0.25", 54 | "react-use": "^15.3.4", 55 | "subscriptions-transport-ws": "^0.9.18", 56 | "tailwindcss": "^1.8.8", 57 | "zustand": "^3.1.1" 58 | }, 59 | "devDependencies": { 60 | "@graphql-codegen/cli": "^1.17.8", 61 | "@graphql-codegen/introspection": "^1.17.8", 62 | "@graphql-codegen/typescript": "^1.17.9", 63 | "@graphql-codegen/typescript-operations": "^1.17.8", 64 | "@prisma/cli": "^2.6.2", 65 | "@types/faker": "^4.1.12", 66 | "@types/jsonwebtoken": "^8.5.0", 67 | "@types/next-auth": "^3.1.4", 68 | "@types/node": "^14.10.1", 69 | "@types/pg": "^7.14.4", 70 | "@types/react": "^16.9.49", 71 | "@types/react-dom": "^16.9.8", 72 | "@typescript-eslint/eslint-plugin": "^4.1.0", 73 | "@typescript-eslint/parser": "^4.1.0", 74 | "@urql/devtools": "^2.0.2", 75 | "concurrently": "^5.3.0", 76 | "eslint": "^7.8.1", 77 | "eslint-config-prettier": "^6.11.0", 78 | "eslint-plugin-jsx-a11y": "^6.3.1", 79 | "eslint-plugin-prettier": "^3.1.4", 80 | "eslint-plugin-react": "^7.20.6", 81 | "eslint-plugin-react-hooks": "^4.1.2", 82 | "eslint-plugin-simple-import-sort": "^5.0.3", 83 | "faker": "^5.1.0", 84 | "husky": "^4.3.0", 85 | "lint-staged": "^10.3.0", 86 | "next-compose-plugins": "^2.2.0", 87 | "prettier": "^2.1.1", 88 | "typescript": "^4.0.2" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /nextjs/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | 'tailwindcss', 4 | 'postcss-flexbugs-fixes', 5 | [ 6 | 'postcss-preset-env', 7 | { 8 | autoprefixer: { 9 | flexbox: 'no-2009', 10 | }, 11 | stage: 3, 12 | features: { 13 | 'custom-properties': false, 14 | }, 15 | }, 16 | ], 17 | ], 18 | } 19 | -------------------------------------------------------------------------------- /nextjs/public/images/bug_fixed.svg: -------------------------------------------------------------------------------- 1 | #29 bug fixed -------------------------------------------------------------------------------- /nextjs/public/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sondh0127/nextjs-hasura-fullstack/b8a99e0d7799938f6fae5ffec5b0dd14258f1c1c/nextjs/public/images/favicon.ico -------------------------------------------------------------------------------- /nextjs/public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sondh0127/nextjs-hasura-fullstack/b8a99e0d7799938f6fae5ffec5b0dd14258f1c1c/nextjs/public/images/logo.png -------------------------------------------------------------------------------- /nextjs/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | } 4 | 5 | datasource db { 6 | provider = "postgresql" 7 | url = env("DATABASE_URL") 8 | } 9 | 10 | model Account { 11 | id Int @id @default(autoincrement()) 12 | compoundId String @unique @map(name: "compound_id") 13 | userId Int @map(name: "user_id") 14 | providerType String @map(name: "provider_type") 15 | providerId String @map(name: "provider_id") 16 | providerAccountId String @map(name: "provider_account_id") 17 | refreshToken String? @map(name: "refresh_token") 18 | accessToken String? @map(name: "access_token") 19 | accessTokenExpires DateTime? @map(name: "access_token_expires") 20 | createdAt DateTime @default(now()) @map(name: "created_at") 21 | updatedAt DateTime @default(now()) @map(name: "updated_at") 22 | 23 | @@index([providerAccountId], name: "providerAccountId") 24 | @@index([providerId], name: "providerId") 25 | @@index([userId], name: "userId") 26 | 27 | @@map(name: "accounts") 28 | } 29 | 30 | model Session { 31 | id Int @id @default(autoincrement()) 32 | userId String @map(name: "user_id") 33 | expires DateTime 34 | sessionToken String @unique @map(name: "session_token") 35 | accessToken String @unique @map(name: "access_token") 36 | createdAt DateTime @default(now()) @map(name: "created_at") 37 | updatedAt DateTime @default(now()) @map(name: "updated_at") 38 | 39 | @@map(name: "sessions") 40 | } 41 | 42 | model User { 43 | id Int @id @default(autoincrement()) 44 | name String? 45 | email String? @unique 46 | emailVerified DateTime? @map(name: "email_verified") 47 | image String? 48 | createdAt DateTime @default(now()) @map(name: "created_at") 49 | updatedAt DateTime @default(now()) @map(name: "updated_at") 50 | 51 | @@map(name: "users") 52 | } 53 | 54 | model VerificationRequest { 55 | id Int @id @default(autoincrement()) 56 | identifier String 57 | token String @unique 58 | expires DateTime 59 | createdAt DateTime @default(now()) @map(name: "created_at") 60 | updatedAt DateTime @default(now()) @map(name: "updated_at") 61 | 62 | @@map(name: "verification_requests") 63 | } 64 | -------------------------------------------------------------------------------- /nextjs/src/components/AccessDeniedIndicator.tsx: -------------------------------------------------------------------------------- 1 | import { Button, ExclamationCircleSolid } from '@retail-ui/core' 2 | import { signIn } from 'next-auth/client' 3 | import Link from 'next/link' 4 | import React from 'react' 5 | 6 | interface AccessDeniedIndicatorProps { 7 | message?: string 8 | } 9 | 10 | const AccessDeniedIndicator: React.FC = ({ 11 | message = 'You need to Sign In to view this content!', 12 | }) => { 13 | const iconNode = () => { 14 | return 15 | } 16 | 17 | const signInButtonNode = () => { 18 | return ( 19 | 20 | 29 | 30 | ) 31 | } 32 | 33 | return ( 34 |
35 |
38 |
{iconNode()}
39 |
40 | {signInButtonNode()} 41 |
42 |
43 |
44 | ) 45 | } 46 | 47 | export default AccessDeniedIndicator 48 | -------------------------------------------------------------------------------- /nextjs/src/components/AddInput.tsx: -------------------------------------------------------------------------------- 1 | import { Button, ButtonIcon, Input } from '@retail-ui/core' 2 | import clsx from 'clsx' 3 | import * as React from 'react' 4 | import { useClickAway, useKey } from 'react-use' 5 | 6 | interface AddInputProps { 7 | isCreating: boolean 8 | setIsCreating: (isCreating: boolean) => void 9 | loading: boolean 10 | onSubmit: (value: string) => void 11 | className?: string 12 | label: string 13 | placeholder?: string 14 | onClickAway: (value: string) => void 15 | } 16 | 17 | export const AddInput: React.FC = (props) => { 18 | const { 19 | onSubmit, 20 | isCreating, 21 | setIsCreating, 22 | loading, 23 | className, 24 | label, 25 | placeholder, 26 | onClickAway, 27 | } = props 28 | 29 | const [value, setValue] = React.useState('') 30 | 31 | const reset = React.useCallback(() => { 32 | setIsCreating(false) 33 | setValue('') 34 | }, []) 35 | 36 | const handleClickAway = React.useCallback(() => { 37 | onClickAway(value) 38 | reset() 39 | }, [onClickAway, reset, value]) 40 | 41 | const ref = React.useRef(null) 42 | 43 | useClickAway(ref, () => handleClickAway()) 44 | useKey('Escape', () => handleClickAway()) 45 | 46 | const handleSubmit = React.useCallback(() => { 47 | onSubmit(value) 48 | reset() 49 | }, [onSubmit, reset, value]) 50 | 51 | const cls = clsx(className, `flex w-full h-10`) 52 | 53 | return ( 54 |
55 | {!isCreating && ( 56 | 80 | )} 81 | {isCreating && ( 82 |
83 | { 89 | setValue(e.target.value) 90 | }} 91 | className={``} 92 | onKeyDown={(e) => { 93 | if (e.key === 'Enter') { 94 | handleSubmit() 95 | } 96 | }} 97 | /> 98 | { 102 | e.preventDefault() 103 | handleSubmit() 104 | }} 105 | icon={ 106 | 112 | 118 | 119 | } 120 | /> 121 |
122 | )} 123 |
124 | ) 125 | } 126 | 127 | export default AddInput 128 | -------------------------------------------------------------------------------- /nextjs/src/components/CustomLink.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import { LinkProps } from 'next/link' 3 | import { useRouter } from 'next/router' 4 | 5 | type CustomLinkChildren = (props: { 6 | isActive: boolean 7 | href: string 8 | }) => React.ReactNode 9 | 10 | export type CustomLinkProps = LinkProps & { 11 | children: CustomLinkChildren 12 | } 13 | 14 | const CustomLink: React.FC = ({ children, ...rest }) => { 15 | const router = useRouter() 16 | 17 | const isActive = rest.as 18 | ? router.asPath === rest.as 19 | : router.pathname === rest.href 20 | const href = rest.as ? router.asPath : router.pathname 21 | 22 | return {children({ isActive, href })} 23 | } 24 | 25 | export default CustomLink 26 | -------------------------------------------------------------------------------- /nextjs/src/components/ItemLink.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | interface ItemLinkProps { 4 | href: string 5 | } 6 | 7 | export const ItemLink: React.FC = (props) => { 8 | const { children, href } = props 9 | 10 | return ( 11 | 17 | {children} 18 | 19 | ) 20 | } 21 | 22 | export default ItemLink 23 | -------------------------------------------------------------------------------- /nextjs/src/components/Loader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | interface Props { 4 | fontSize?: string 5 | } 6 | 7 | const Loader: React.FC = () => { 8 | return ( 9 |
10 |
Spinner
11 |
12 | ) 13 | } 14 | 15 | export default Loader 16 | -------------------------------------------------------------------------------- /nextjs/src/components/icons/BalanceIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const BalanceIcon: React.FC = () => { 4 | return ( 5 | 6 | 11 | 12 | ) 13 | } 14 | 15 | export default BalanceIcon 16 | -------------------------------------------------------------------------------- /nextjs/src/components/icons/ChartIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const ChartIcon: React.FC = () => { 4 | return ( 5 | 18 | ) 19 | } 20 | 21 | export default ChartIcon 22 | -------------------------------------------------------------------------------- /nextjs/src/components/icons/ClientIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const ClientIcon: React.FC = () => { 4 | return ( 5 | 6 | 7 | 8 | ) 9 | } 10 | 11 | export default ClientIcon 12 | -------------------------------------------------------------------------------- /nextjs/src/components/icons/ContactIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const ContactIcon: React.FC = () => { 4 | return ( 5 | 6 | 11 | 12 | ) 13 | } 14 | 15 | export default ContactIcon 16 | -------------------------------------------------------------------------------- /nextjs/src/components/icons/DashboardIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const DashboardIcon: React.FC = () => { 4 | return ( 5 | 17 | ) 18 | } 19 | 20 | export default DashboardIcon 21 | -------------------------------------------------------------------------------- /nextjs/src/components/icons/FormIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const FormIcon: React.FC = () => { 4 | return ( 5 | 17 | ) 18 | } 19 | 20 | export default FormIcon 21 | -------------------------------------------------------------------------------- /nextjs/src/components/icons/HamburgerIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const HamburgerIcon: React.FC = () => { 4 | return ( 5 | 17 | ) 18 | } 19 | 20 | export default HamburgerIcon 21 | -------------------------------------------------------------------------------- /nextjs/src/components/icons/Logo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Logo: React.FC = () => { 4 | return ( 5 |
6 | 12 | 16 | 17 | 18 | 19 | 26 | 35 | 36 | 37 | 38 | 39 | 43 | 47 | 53 | 59 | 60 |

NextHasura

61 |
62 | ) 63 | } 64 | 65 | export default Logo 66 | -------------------------------------------------------------------------------- /nextjs/src/components/icons/LogoutIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const LogoutIcon: React.FC = () => { 4 | return ( 5 | 17 | ) 18 | } 19 | 20 | export default LogoutIcon 21 | -------------------------------------------------------------------------------- /nextjs/src/components/icons/MailIcon.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useMemo, useState } from 'react' 2 | 3 | const MailIcon: React.FC = ({ children }) => { 4 | return ( 5 | 17 | ) 18 | } 19 | 20 | export default MailIcon 21 | -------------------------------------------------------------------------------- /nextjs/src/components/icons/MoonIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const MoonIcon: React.FC = () => { 4 | return ( 5 | 13 | ) 14 | } 15 | 16 | export default MoonIcon 17 | -------------------------------------------------------------------------------- /nextjs/src/components/icons/NotificationIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const NotificationIcon: React.FC = () => { 4 | return ( 5 | 13 | ) 14 | } 15 | 16 | export default NotificationIcon 17 | -------------------------------------------------------------------------------- /nextjs/src/components/icons/ProfileIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const ProfileIcon: React.FC = () => { 4 | return ( 5 | 17 | ) 18 | } 19 | 20 | export default ProfileIcon 21 | -------------------------------------------------------------------------------- /nextjs/src/components/icons/SaleIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const SaleIcon: React.FC = () => { 4 | return ( 5 | 6 | 7 | 8 | ) 9 | } 10 | 11 | export default SaleIcon 12 | -------------------------------------------------------------------------------- /nextjs/src/components/icons/SearchIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const SearchIcon: React.FC = () => { 4 | return ( 5 | 17 | ) 18 | } 19 | 20 | export default SearchIcon 21 | -------------------------------------------------------------------------------- /nextjs/src/components/icons/SettingsIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const SettingsIcon: React.FC = () => { 4 | return ( 5 | 18 | ) 19 | } 20 | 21 | export default SettingsIcon 22 | -------------------------------------------------------------------------------- /nextjs/src/components/icons/StarIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const StarIcon: React.FC = () => { 4 | return ( 5 | 6 | 7 | 8 | ) 9 | } 10 | 11 | export default StarIcon 12 | -------------------------------------------------------------------------------- /nextjs/src/components/icons/SunIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const SunIcon: React.FC = () => { 4 | return ( 5 | 17 | ) 18 | } 19 | 20 | export default SunIcon 21 | -------------------------------------------------------------------------------- /nextjs/src/components/pages/account/graphql/UpdateUser.graphql: -------------------------------------------------------------------------------- 1 | mutation UpdateUser($userId: Int!, $name: String) { 2 | update_users(where: { id: { _eq: $userId } }, _set: { name: $name }) { 3 | returning { 4 | id 5 | name 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /nextjs/src/components/pages/account/graphql/User.graphql: -------------------------------------------------------------------------------- 1 | query User($userId: Int!) { 2 | users_by_pk(id: $userId) { 3 | id 4 | name 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /nextjs/src/components/pages/account/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Alert, 3 | Button, 4 | Card, 5 | CardBody, 6 | CardFooter, 7 | Input, 8 | InputGroup, 9 | } from '@retail-ui/core' 10 | import { useSession } from 'next-auth/client' 11 | import React, { useEffect, useState } from 'react' 12 | 13 | import { useUpdateUserMutation, useUserQuery } from '@/generated/graphql' 14 | 15 | import Loader from '../../../layouts/components/Footer' 16 | 17 | const Account = () => { 18 | const [name, setName] = useState('') 19 | const [session] = useSession() 20 | 21 | /* Fetching */ 22 | const { 23 | data: fetchUserData, 24 | loading: fetchUserLoading, 25 | error: fetchUserError, 26 | } = useUserQuery({ 27 | // @ts-ignore 28 | variables: { userId: session.id }, 29 | }) 30 | 31 | useEffect(() => { 32 | if (fetchUserData) { 33 | setName(fetchUserData.users_by_pk?.name || '') 34 | } 35 | }, [fetchUserData]) 36 | 37 | /* Update */ 38 | 39 | const [errorMessage, setErrorMessage] = React.useState('') 40 | const [updateUser, { loading: updateUserLoading }] = useUpdateUserMutation({ 41 | onError: (error) => setErrorMessage(error.message), 42 | }) 43 | 44 | const handleSubmit = async () => { 45 | await updateUser({ 46 | variables: { 47 | // @ts-ignore 48 | userId: Number(session.id), 49 | name, 50 | }, 51 | }) 52 | } 53 | 54 | const errorNode = () => { 55 | if (!errorMessage) { 56 | return false 57 | } 58 | 59 | return ( 60 | setErrorMessage('')} 66 | > 67 | ) 68 | } 69 | 70 | /* Render */ 71 | if (fetchUserLoading) { 72 | return 73 | } 74 | 75 | if (fetchUserError) { 76 | return

Error: {fetchUserError.message}

77 | } 78 | 79 | return ( 80 |
81 |

My Account

82 | 83 | 84 | 85 | {errorNode()} 86 |
87 | 88 | setName(e.currentTarget.value)} 93 | disabled={updateUserLoading} 94 | /> 95 | 96 | 97 |
98 | 101 |
102 |
103 |
104 |
105 |
106 | ) 107 | } 108 | 109 | export default Account 110 | -------------------------------------------------------------------------------- /nextjs/src/components/pages/boards/board/boardUtils.ts: -------------------------------------------------------------------------------- 1 | import produce from 'immer' 2 | 3 | export interface DraggableLocation { 4 | droppableId: string 5 | index: number 6 | } 7 | export const reorder = ( 8 | list: T[], 9 | sourceIndex: number, 10 | destinationIndex: number, 11 | ) => { 12 | const result = produce(list, (draftState) => { 13 | const [removed] = draftState.splice(sourceIndex, 1) 14 | draftState.splice(destinationIndex, 0, removed) 15 | }) 16 | 17 | return result 18 | } 19 | 20 | export const move = ( 21 | source: T[], 22 | destination: T[], 23 | sourceLocation: DraggableLocation, 24 | destinationLocation: DraggableLocation, 25 | ) => { 26 | const { source: nextSource, destination: nextDestination } = produce( 27 | { source, destination }, 28 | (draftState) => { 29 | const [removed] = draftState.source.splice(sourceLocation.index, 1) 30 | draftState.destination.splice(destinationLocation.index, 0, removed) 31 | }, 32 | ) 33 | return [nextSource, nextDestination] 34 | } 35 | 36 | interface PositionArray { 37 | position: number 38 | } 39 | 40 | export const getUpdatePositionReorder = ( 41 | sourceArray: T[], 42 | sourceIndex: number, 43 | destinationIndex: number, 44 | ) => { 45 | const sourceItem = sourceArray[sourceIndex] 46 | 47 | const destinationItem = sourceArray[destinationIndex] 48 | const destinationMinus1Item = sourceArray[destinationIndex - 1] 49 | const destinationPlus1Item = sourceArray[destinationIndex + 1] 50 | 51 | let updatedPositionOfSourceItem: number = sourceItem.position 52 | 53 | if (sourceItem.position > destinationItem.position) { 54 | if (destinationMinus1Item) { 55 | updatedPositionOfSourceItem = 56 | (destinationItem.position + destinationMinus1Item.position) / 2 57 | } else { 58 | updatedPositionOfSourceItem = destinationItem.position / 2 59 | } 60 | } else { 61 | if (destinationPlus1Item) { 62 | updatedPositionOfSourceItem = 63 | (destinationItem.position + destinationPlus1Item.position) / 2 64 | } else { 65 | updatedPositionOfSourceItem = destinationItem.position + 1024 66 | } 67 | } 68 | return updatedPositionOfSourceItem 69 | } 70 | 71 | export const getUpdatePositionMove = ( 72 | sourceArray: T[], 73 | destinationArray: T[], 74 | sourceIndex: number, 75 | destinationIndex: number, 76 | ) => { 77 | const sourceItem = sourceArray[sourceIndex] 78 | const destinationItem = destinationArray[destinationIndex] 79 | const destinationMinus1Item = destinationArray[destinationIndex - 1] 80 | 81 | const lastDestinationItem = destinationArray[destinationArray.length - 1] 82 | 83 | let updatedPositionOfSourceItem: number = sourceItem.position 84 | if (!destinationItem) { 85 | /* destinationArray Empty push to end */ 86 | console.log(`🇻🇳 [LOG]: destinationItem`, `Not destinationItem`) 87 | if (lastDestinationItem) { 88 | updatedPositionOfSourceItem = lastDestinationItem.position + 1024 89 | } else { 90 | updatedPositionOfSourceItem = 1024 91 | } 92 | } else if (destinationMinus1Item) { 93 | updatedPositionOfSourceItem = 94 | (destinationItem.position + destinationMinus1Item.position) / 2 95 | } else { 96 | updatedPositionOfSourceItem = destinationItem.position / 2 97 | } 98 | 99 | return updatedPositionOfSourceItem 100 | } 101 | -------------------------------------------------------------------------------- /nextjs/src/components/pages/boards/board/graphql/BoardQuery.graphql: -------------------------------------------------------------------------------- 1 | query Board($id: Int!) { 2 | boards_by_pk(id: $id) { 3 | ...Board 4 | lists(order_by: { position: asc }) { 5 | ...List 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /nextjs/src/components/pages/boards/board/index.tsx: -------------------------------------------------------------------------------- 1 | import produce from 'immer' 2 | import { useRouter } from 'next/router' 3 | import React from 'react' 4 | import { 5 | DragDropContext, 6 | DragStart, 7 | DragUpdate, 8 | Droppable, 9 | DropResult, 10 | ResponderProvided, 11 | } from 'react-beautiful-dnd' 12 | import Scrollbar from 'react-scrollbars-custom' 13 | 14 | import { 15 | useBoardQuery, 16 | useMoveCardMutation, 17 | useUpdateCardMutation, 18 | useUpdateListMutation, 19 | } from '@/generated/graphql' 20 | import { useLastPositionNumber } from '@/zustands/boards' 21 | 22 | import ListBoard from '../list' 23 | import NewList from '../list/NewList' 24 | import { 25 | getUpdatePositionMove, 26 | getUpdatePositionReorder, 27 | reorder, 28 | } from './boardUtils' 29 | 30 | const BoardPage: React.FC = () => { 31 | const router = useRouter() 32 | const { setLastPosition } = useLastPositionNumber() 33 | 34 | const { data, loading } = useBoardQuery({ 35 | variables: { 36 | id: Number(router.query.boardId), 37 | }, 38 | skip: !router.query.boardId, 39 | }) 40 | 41 | const [updateList] = useUpdateListMutation() 42 | const [updateCard] = useUpdateCardMutation() 43 | const [moveCard] = useMoveCardMutation() 44 | 45 | const renderLists = React.useMemo(() => { 46 | return data?.boards_by_pk?.lists || [] 47 | }, [data]) 48 | 49 | React.useEffect(() => { 50 | setLastPosition(renderLists[renderLists.length - 1]?.position || 1) 51 | }, [renderLists]) 52 | 53 | const onDragEnd = async (result: DropResult, provided: ResponderProvided) => { 54 | const { source, destination, type } = result 55 | 56 | // dropped outside the list 57 | if (!destination) { 58 | return 59 | } 60 | 61 | if ( 62 | source.droppableId === destination.droppableId && 63 | source.index === destination.index 64 | ) { 65 | return 66 | } 67 | 68 | // DragDrop a "List" 69 | if (type === 'LIST') { 70 | const updatedPosition = getUpdatePositionReorder( 71 | renderLists, 72 | source.index, 73 | destination.index, 74 | ) 75 | const sourceList = renderLists[source.index] 76 | 77 | updateList({ 78 | variables: { 79 | id: sourceList.id, 80 | name: sourceList.name, 81 | position: updatedPosition, 82 | }, 83 | update: (cache, { data }) => { 84 | if (data?.update_lists_by_pk) { 85 | const cacheId = cache.identify({ 86 | __typename: 'boards', 87 | id: sourceList.board_id, 88 | }) 89 | if (cacheId) { 90 | cache.modify({ 91 | id: cacheId, 92 | fields: { 93 | lists(existingListRefs = []) { 94 | return reorder( 95 | existingListRefs, 96 | source.index, 97 | destination.index, 98 | ) 99 | }, 100 | }, 101 | }) 102 | } 103 | } 104 | }, 105 | optimisticResponse: (variables) => { 106 | return { 107 | __typename: 'mutation_root', 108 | update_lists_by_pk: { 109 | ...sourceList, 110 | position: updatedPosition, 111 | }, 112 | } 113 | }, 114 | }) 115 | } 116 | 117 | if (type === 'CARD') { 118 | /* same list: reorder card */ 119 | if (source.droppableId === destination.droppableId) { 120 | const listCards = renderLists.find( 121 | (item) => item.id === Number(source.droppableId), 122 | )?.cards 123 | 124 | if (!listCards) { 125 | return 126 | } 127 | 128 | const updatedPosition = getUpdatePositionReorder( 129 | listCards, 130 | source.index, 131 | destination.index, 132 | ) 133 | 134 | const sourceCard = listCards[source.index] 135 | 136 | await updateCard({ 137 | variables: { 138 | id: sourceCard.id, 139 | title: sourceCard.title, 140 | position: updatedPosition, 141 | }, 142 | update: (cache, { data }) => { 143 | if (data?.update_cards_by_pk) { 144 | const cacheId = cache.identify({ 145 | __typename: 'lists', 146 | id: sourceCard.list_id, 147 | }) 148 | if (cacheId) { 149 | cache.modify({ 150 | id: cacheId, 151 | fields: { 152 | cards(existingCardRefs = []) { 153 | return reorder( 154 | existingCardRefs, 155 | source.index, 156 | destination.index, 157 | ) 158 | }, 159 | }, 160 | }) 161 | } 162 | } 163 | }, 164 | optimisticResponse: (variables) => { 165 | return { 166 | __typename: 'mutation_root', 167 | update_cards_by_pk: { 168 | ...sourceCard, 169 | position: updatedPosition, 170 | }, 171 | } 172 | }, 173 | }) 174 | } else { 175 | /** 176 | * Diferent list: move card 177 | */ 178 | 179 | const sourceListCards = renderLists.find( 180 | (item) => item.id === Number(source.droppableId), 181 | )?.cards 182 | 183 | const destinationListCards = renderLists.find( 184 | (item) => item.id === Number(destination.droppableId), 185 | )?.cards 186 | 187 | if (!sourceListCards || !destinationListCards) { 188 | return 189 | } 190 | 191 | const updatedPosition = getUpdatePositionMove( 192 | sourceListCards, 193 | destinationListCards, 194 | source.index, 195 | destination.index, 196 | ) 197 | 198 | const sourceCard = sourceListCards[source.index] 199 | 200 | await moveCard({ 201 | variables: { 202 | id: sourceCard.id, 203 | list_id: Number(destination.droppableId), 204 | title: sourceCard.title, 205 | position: updatedPosition, 206 | }, 207 | update: (cache, { data }) => { 208 | if (data?.update_cards_by_pk) { 209 | const cardCacheId = cache.identify(data.update_cards_by_pk) 210 | 211 | if (!cardCacheId) { 212 | return 213 | } 214 | 215 | const cacheId = cache.identify({ 216 | __typename: 'lists', 217 | id: source.droppableId, 218 | }) 219 | 220 | if (cacheId) { 221 | cache.modify({ 222 | id: cacheId, 223 | fields: { 224 | cards(existingRefs = []) { 225 | const next = existingRefs.filter( 226 | (listRef: { __ref: string }) => 227 | listRef.__ref !== cardCacheId, 228 | ) 229 | return next 230 | }, 231 | }, 232 | }) 233 | } 234 | 235 | const cacheIdDestination = cache.identify({ 236 | __typename: 'lists', 237 | id: destination.droppableId, 238 | }) 239 | 240 | if (cacheIdDestination) { 241 | cache.modify({ 242 | id: cacheIdDestination, 243 | fields: { 244 | cards(existingCardRefs = [], { toReference }) { 245 | const moveRef = toReference(cardCacheId) 246 | if (moveRef) { 247 | const next = produce( 248 | existingCardRefs as any[], 249 | (draftState) => { 250 | draftState.splice(destination.index, 0, moveRef) 251 | }, 252 | ) 253 | return next 254 | } 255 | }, 256 | }, 257 | }) 258 | } 259 | } 260 | }, 261 | optimisticResponse: () => ({ 262 | __typename: 'mutation_root', 263 | update_cards_by_pk: { 264 | ...sourceCard, 265 | position: updatedPosition, 266 | }, 267 | }), 268 | }) 269 | } 270 | } 271 | } 272 | 273 | let listRender 274 | if (loading) { 275 | listRender = ( 276 |
277 | {[...Array(3)].map((item, idx) => ( 278 |
282 |
283 |
284 |
285 | {/*
286 |
287 |
288 |
*/} 289 |
290 |
291 | {[...Array(4)].map((item, index) => ( 292 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 | ))} 304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 | ))} 313 | {/* */} 314 |
315 | ) 316 | } else { 317 | listRender = ( 318 |
319 | { 321 | console.log(`🇻🇳 [LOG]: source`, source) 322 | }} 323 | // onDragUpdate={onDragUpdate} 324 | onDragEnd={onDragEnd} 325 | > 326 | {/* List */} 327 | 333 | {( 334 | { innerRef, droppableProps, placeholder }, 335 | { isDraggingOver }, 336 | ) => { 337 | return ( 338 |
346 | {renderLists.map((list, index) => ( 347 | 348 | ))} 349 | {placeholder} 350 |
351 | ) 352 | }} 353 |
354 |
355 |
356 | 357 |
358 |
359 | ) 360 | } 361 | 362 | return ( 363 |
364 | 365 |
366 |
367 |

370 | {data?.boards_by_pk?.icon} {data?.boards_by_pk?.name} 371 |

372 |
373 | {listRender} 374 |
375 |
376 |
377 | ) 378 | } 379 | 380 | export default BoardPage 381 | -------------------------------------------------------------------------------- /nextjs/src/components/pages/boards/card/NewCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import AddInput from '@/components/AddInput' 4 | import { 5 | CardFragment, 6 | ListFragment, 7 | useInsertCardMutation, 8 | } from '@/generated/graphql' 9 | 10 | interface NewCardProps { 11 | lastCard?: CardFragment 12 | list: ListFragment 13 | } 14 | const NewCard: React.FC = (props) => { 15 | const { lastCard, list } = props 16 | const [isCreating, setIsCreating] = React.useState(false) 17 | 18 | const [insertCard, { loading }] = useInsertCardMutation() 19 | 20 | const handleAddCard = React.useCallback( 21 | async (value: string) => { 22 | try { 23 | const lastPosition = lastCard?.position || 0 24 | const getNewPosition = (): number => { 25 | const bufferPosition = 1024 26 | return lastPosition + bufferPosition 27 | } 28 | 29 | await insertCard({ 30 | variables: { 31 | list_id: list.id, 32 | board_id: list.board_id, 33 | title: value, 34 | description: ``, 35 | position: getNewPosition(), 36 | }, 37 | update: (cache, { data }) => { 38 | if (data?.insert_cards_one) { 39 | const cacheId = cache.identify(data?.insert_cards_one) 40 | if (cacheId) { 41 | cache.modify({ 42 | id: cache.identify({ 43 | __typename: 'lists', 44 | id: list.id, 45 | }), 46 | fields: { 47 | cards: (existingCards = [], { toReference }) => { 48 | return [...existingCards, toReference(cacheId)] 49 | }, 50 | }, 51 | }) 52 | } 53 | } 54 | }, 55 | optimisticResponse: (variables) => { 56 | return { 57 | __typename: 'mutation_root', 58 | insert_cards_one: { 59 | __typename: 'cards', 60 | ...variables, 61 | id: (lastCard?.id || 0) + 1, 62 | created_at: Date.now().toString(), 63 | updated_at: Date.now().toString(), 64 | }, 65 | } 66 | }, 67 | }) 68 | } catch (err) { 69 | console.log(`🇻🇳 [LOG]: onSubmit -> err`, Object.values(err)) 70 | } 71 | }, 72 | [lastCard, list], 73 | ) 74 | 75 | const handleClickAway = (value: string) => { 76 | if (value) { 77 | handleAddCard(value) 78 | } else { 79 | setIsCreating(false) 80 | } 81 | } 82 | 83 | return ( 84 | 93 | ) 94 | } 95 | 96 | export default NewCard 97 | -------------------------------------------------------------------------------- /nextjs/src/components/pages/boards/card/graphql/CardFragment.graphql: -------------------------------------------------------------------------------- 1 | fragment Card on cards { 2 | id 3 | updated_at 4 | created_at 5 | 6 | title 7 | description 8 | position 9 | list_id 10 | board_id 11 | } 12 | -------------------------------------------------------------------------------- /nextjs/src/components/pages/boards/card/graphql/DeleteCard.graphql: -------------------------------------------------------------------------------- 1 | mutation DeleteCard($id: Int!) { 2 | delete_cards_by_pk(id: $id) { 3 | ...Card 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /nextjs/src/components/pages/boards/card/graphql/InsertCard.graphql: -------------------------------------------------------------------------------- 1 | mutation InsertCard( 2 | $list_id: Int! 3 | $board_id: Int! 4 | $description: String! 5 | $title: String! 6 | $position: numeric! 7 | ) { 8 | insert_cards_one( 9 | object: { 10 | description: $description 11 | title: $title 12 | position: $position 13 | list_id: $list_id 14 | board_id: $board_id 15 | } 16 | ) { 17 | ...Card 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /nextjs/src/components/pages/boards/card/graphql/MoveCard.graphql: -------------------------------------------------------------------------------- 1 | # Move card to other list 2 | 3 | mutation MoveCard( 4 | $id: Int! 5 | $title: String! 6 | $position: numeric! 7 | $list_id: Int! 8 | ) { 9 | update_cards_by_pk( 10 | pk_columns: { id: $id } 11 | _set: { title: $title, position: $position, list_id: $list_id } 12 | ) { 13 | ...Card 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /nextjs/src/components/pages/boards/card/graphql/UpdateCard.graphql: -------------------------------------------------------------------------------- 1 | mutation UpdateCard($id: Int!, $title: String!, $position: numeric!) { 2 | update_cards_by_pk( 3 | pk_columns: { id: $id } 4 | _set: { title: $title, position: $position } 5 | ) { 6 | ...Card 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /nextjs/src/components/pages/boards/card/index.tsx: -------------------------------------------------------------------------------- 1 | import { ButtonIcon, Card, CardBody, XSolid } from '@retail-ui/core' 2 | import React from 'react' 3 | import { Draggable } from 'react-beautiful-dnd' 4 | 5 | import { CardFragment, useDeleteCardMutation } from '@/generated/graphql' 6 | 7 | interface CardTaskProps { 8 | card: CardFragment 9 | index: number 10 | } 11 | 12 | const CardTask: React.FC = (props) => { 13 | const { card, index } = props 14 | 15 | const [deleteCard, { loading }] = useDeleteCardMutation() 16 | 17 | const handleDeleteCard = React.useCallback(async () => { 18 | try { 19 | await deleteCard({ 20 | variables: { id: card.id }, 21 | update: (cache, { data }) => { 22 | if (data?.delete_cards_by_pk) { 23 | const cacheId = cache.identify(data?.delete_cards_by_pk) 24 | if (cacheId) { 25 | cache.modify({ 26 | id: cache.identify({ __typename: 'lists', id: card.list_id }), 27 | fields: { 28 | cards(existingCardRefs = [], { readField, toReference }) { 29 | return existingCardRefs.filter( 30 | //@ts-ignore 31 | (ref) => ref.__ref !== cacheId, 32 | ) 33 | }, 34 | }, 35 | }) 36 | cache.evict({ id: cacheId }) 37 | } 38 | } 39 | }, 40 | optimisticResponse: { 41 | __typename: 'mutation_root', 42 | delete_cards_by_pk: { 43 | ...card, 44 | }, 45 | }, 46 | }) 47 | } catch (err) { 48 | console.log(`🇻🇳 [LOG]: removeAction -> err`, err) 49 | } 50 | }, []) 51 | 52 | return ( 53 | 54 | {( 55 | { draggableProps, dragHandleProps, innerRef }, 56 | { isDragging, isDropAnimating }, 57 | ) => ( 58 | 65 | 66 |
67 |
{card.title}
68 |
69 | } 74 | onClick={(e) => { 75 | e.preventDefault() 76 | handleDeleteCard() 77 | }} 78 | /> 79 |
80 |
81 |
82 |

{card.description}

83 |
84 |
85 |
86 | )} 87 |
88 | ) 89 | } 90 | 91 | export default CardTask 92 | -------------------------------------------------------------------------------- /nextjs/src/components/pages/boards/graphql/BoardFragment.graphql: -------------------------------------------------------------------------------- 1 | fragment Board on boards { 2 | id 3 | name 4 | icon 5 | created_at 6 | } 7 | -------------------------------------------------------------------------------- /nextjs/src/components/pages/boards/graphql/Boards.graphql: -------------------------------------------------------------------------------- 1 | query Boards { 2 | boards(order_by: { id: asc }) { 3 | ...Board 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /nextjs/src/components/pages/boards/graphql/DeleteBoard.graphql: -------------------------------------------------------------------------------- 1 | mutation DeleteBoard($id: Int!) { 2 | delete_boards_by_pk(id: $id) { 3 | ...Board 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /nextjs/src/components/pages/boards/graphql/InsertBoard.graphql: -------------------------------------------------------------------------------- 1 | mutation InsertBoard($name: String) { 2 | insert_boards_one(object: { name: $name }) { 3 | ...Board 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /nextjs/src/components/pages/boards/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | ButtonIcon, 4 | Card, 5 | CardBody, 6 | CardFooter, 7 | CardHeader, 8 | Input, 9 | StarSolid, 10 | } from '@retail-ui/core' 11 | import NextLink from 'next/link' 12 | import React from 'react' 13 | 14 | import { 15 | BoardFragmentDoc, 16 | useBoardsQuery, 17 | useInsertBoardMutation, 18 | } from '@/generated/graphql' 19 | import { timeFromNow } from '@/utils' 20 | 21 | const BoardsPage: React.FC = () => { 22 | const { data, loading } = useBoardsQuery() 23 | const [name, setName] = React.useState('') 24 | 25 | const [insert, { loading: insertLoading }] = useInsertBoardMutation({ 26 | update: (cache, { data }) => { 27 | cache.modify({ 28 | fields: { 29 | boards: (existingBoards = []) => { 30 | const newTodoRef = cache.writeFragment({ 31 | data: data?.insert_boards_one, 32 | fragment: BoardFragmentDoc, 33 | }) 34 | return [...existingBoards, newTodoRef] 35 | }, 36 | }, 37 | }) 38 | }, 39 | }) 40 | 41 | const handleInsert = async () => { 42 | try { 43 | if (name) { 44 | await insert({ 45 | variables: { name }, 46 | }) 47 | } 48 | } catch (error) { 49 | // console.log('Error: ', Object.entries(error)) 50 | } 51 | } 52 | 53 | return ( 54 |
55 |
56 |
57 | setName(e.target.value)} 62 | /> 63 |
64 | 71 |
72 | 73 |
76 | {(data?.boards || []).map((board) => { 77 | return ( 78 | 79 | 86 | } 90 | /> 91 | 92 | } 93 | > 94 |
95 |
{board.icon}
96 |
99 | {board.name} 100 |
101 |
102 |
103 | {/* {board.name} */} 104 | 105 | {timeFromNow(board.created_at)} 106 | 107 |
108 | ) 109 | })} 110 |
111 |
112 | ) 113 | } 114 | 115 | export default BoardsPage 116 | -------------------------------------------------------------------------------- /nextjs/src/components/pages/boards/list/ActionDropdown.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ButtonIcon, 3 | DotsVerticalSolid, 4 | Dropdown, 5 | DropdownButton, 6 | DropdownItem, 7 | DropdownList, 8 | } from '@retail-ui/core' 9 | import * as React from 'react' 10 | 11 | import { ListFragment, useDeleteListMutation } from '@/generated/graphql' 12 | 13 | interface ActionDropdownProps { 14 | list: ListFragment 15 | } 16 | 17 | export const ActionDropdown: React.FC = (props) => { 18 | const { list } = props 19 | 20 | const [deleteList, { loading }] = useDeleteListMutation() 21 | 22 | const removeAction = async () => { 23 | try { 24 | await deleteList({ 25 | variables: { id: list.id }, 26 | update: (cache, { data }) => { 27 | if (data?.delete_lists_by_pk) { 28 | const cacheId = cache.identify(data?.delete_lists_by_pk) 29 | if (cacheId) { 30 | cache.modify({ 31 | id: cache.identify({ __typename: 'boards', id: list.board_id }), 32 | fields: { 33 | lists(existingListRefs = [], { readField, toReference }) { 34 | return existingListRefs.filter( 35 | (listRef: { __ref: string }) => listRef.__ref !== cacheId, 36 | ) 37 | }, 38 | }, 39 | }) 40 | cache.evict({ id: cacheId }) 41 | } 42 | } 43 | }, 44 | optimisticResponse: { 45 | __typename: 'mutation_root', 46 | delete_lists_by_pk: { 47 | ...list, 48 | }, 49 | }, 50 | }) 51 | } catch (err) { 52 | console.log(`🇻🇳 [LOG]: removeAction -> err`, err) 53 | } 54 | } 55 | return ( 56 | 57 | 58 | {({ toggleOpen }) => ( 59 | } 63 | onClick={() => toggleOpen()} 64 | /> 65 | )} 66 | 67 | 68 | removeAction()}> 69 | Delete 70 | 71 | 72 | 73 | ) 74 | } 75 | 76 | export default ActionDropdown 77 | -------------------------------------------------------------------------------- /nextjs/src/components/pages/boards/list/ListHeader.tsx: -------------------------------------------------------------------------------- 1 | import { Badge, Button, Input } from '@retail-ui/core' 2 | import React from 'react' 3 | import { useClickAway } from 'react-use' 4 | 5 | import { ListFragment, useUpdateListMutation } from '@/generated/graphql' 6 | 7 | interface ListHeaderProps { 8 | list: ListFragment 9 | } 10 | 11 | const ListHeader: React.FC = (props) => { 12 | const { 13 | list: { id, name, position }, 14 | } = props 15 | 16 | const [updateList] = useUpdateListMutation() 17 | 18 | const onSubmit = async (newName: string) => { 19 | try { 20 | await updateList({ 21 | variables: { id, name: newName, position }, 22 | }) 23 | } catch (err) { 24 | console.log(`🇻🇳 [Error]: onSubmit`, Object.values(err)) 25 | } 26 | } 27 | 28 | const [isEditting, setIsEditting] = React.useState(!name) 29 | const [value, setValue] = React.useState(name.toUpperCase()) 30 | 31 | const ref = React.useRef(null) 32 | 33 | useClickAway(ref, () => { 34 | if (isEditting) { 35 | if (value && value !== name) { 36 | onSubmit(value) 37 | } else { 38 | setValue(name) 39 | } 40 | setIsEditting(false) 41 | } 42 | }) 43 | 44 | return ( 45 |
46 | {!isEditting && ( 47 | 58 | )} 59 | 60 | {isEditting && ( 61 | { 67 | setValue(e.target.value.toUpperCase()) 68 | }} 69 | onKeyDown={(e) => { 70 | if (e.key === 'Enter') { 71 | onSubmit(value) 72 | setIsEditting(false) 73 | } 74 | }} 75 | /> 76 | )} 77 | 78 | {/* {!!cards.length && {cards.length}} */} 79 |
80 | ) 81 | } 82 | 83 | export default ListHeader 84 | -------------------------------------------------------------------------------- /nextjs/src/components/pages/boards/list/NewList.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardHeader } from '@retail-ui/core' 2 | import { useRouter } from 'next/router' 3 | import React, { useCallback } from 'react' 4 | 5 | import AddInput from '@/components/AddInput' 6 | import { useInsertListMutation } from '@/generated/graphql' 7 | import { useLastPositionNumber } from '@/zustands/boards' 8 | 9 | interface NewListProps { 10 | lastId: number 11 | } 12 | 13 | const NewList: React.FC = (props) => { 14 | const { lastId } = props 15 | const router = useRouter() 16 | const [isCreating, setIsCreating] = React.useState(false) 17 | 18 | const { lastPosition } = useLastPositionNumber() 19 | 20 | const [insertList, { loading }] = useInsertListMutation() 21 | 22 | const handleNewList = useCallback( 23 | async (value: string) => { 24 | try { 25 | const getPositionOfNewList = (): number => { 26 | const bufferPosition = 1024 27 | return lastPosition + bufferPosition 28 | } 29 | 30 | await insertList({ 31 | variables: { 32 | name: value, 33 | position: getPositionOfNewList(), 34 | board_id: Number(router.query.boardId), 35 | }, 36 | update: (cache, { data }) => { 37 | if (data?.insert_lists_one) { 38 | const cacheId = cache.identify(data?.insert_lists_one) 39 | if (cacheId) { 40 | cache.modify({ 41 | id: cache.identify({ 42 | __typename: 'boards', 43 | id: Number(router.query.boardId), 44 | }), 45 | fields: { 46 | lists: (existingLists = [], { toReference }) => { 47 | return [...existingLists, toReference(cacheId)] 48 | }, 49 | }, 50 | optimistic: true, 51 | }) 52 | } 53 | } 54 | }, 55 | optimisticResponse: (variables) => { 56 | return { 57 | __typename: 'mutation_root', 58 | insert_lists_one: { 59 | __typename: 'lists', 60 | ...variables, 61 | id: lastId + 1, 62 | cards: [], 63 | }, 64 | } 65 | }, 66 | }) 67 | } catch (err) { 68 | console.log(`🇻🇳 [LOG]: onSubmit -> err`, Object.values(err)) 69 | } 70 | }, 71 | [lastPosition, router.query.boardId], 72 | ) 73 | 74 | const handleClickAway = (value: string) => { 75 | if (value) { 76 | handleNewList(value) 77 | } else { 78 | setIsCreating(false) 79 | } 80 | } 81 | 82 | return ( 83 | 84 | 85 | 94 | 95 | 96 | ) 97 | } 98 | 99 | export default NewList 100 | -------------------------------------------------------------------------------- /nextjs/src/components/pages/boards/list/graphql/DeleteList.graphql: -------------------------------------------------------------------------------- 1 | mutation DeleteList($id: Int!) { 2 | delete_lists_by_pk(id: $id) { 3 | ...List 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /nextjs/src/components/pages/boards/list/graphql/InsertList.graphql: -------------------------------------------------------------------------------- 1 | mutation InsertList($name: String!, $position: numeric!, $board_id: Int!) { 2 | insert_lists_one( 3 | object: { name: $name, position: $position, board_id: $board_id } 4 | ) { 5 | ...List 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /nextjs/src/components/pages/boards/list/graphql/ListFragment.graphql: -------------------------------------------------------------------------------- 1 | fragment List on lists { 2 | id 3 | position 4 | name 5 | board_id 6 | cards(order_by: { position: asc }) { 7 | ...Card 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /nextjs/src/components/pages/boards/list/graphql/UpdateList.graphql: -------------------------------------------------------------------------------- 1 | mutation UpdateList($id: Int!, $name: String!, $position: numeric!) { 2 | update_lists_by_pk( 3 | pk_columns: { id: $id } 4 | _set: { name: $name, position: $position } 5 | ) { 6 | ...List 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /nextjs/src/components/pages/boards/list/index.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardBody, CardFooter, CardHeader } from '@retail-ui/core' 2 | import React from 'react' 3 | import { Draggable, Droppable } from 'react-beautiful-dnd' 4 | 5 | import { ListFragment } from '@/generated/graphql' 6 | 7 | import CardTask from '../card' 8 | import NewCard from '../card/NewCard' 9 | import ActionDropdown from './ActionDropdown' 10 | import ListHeader from './ListHeader' 11 | 12 | interface ListBoardProps { 13 | list: ListFragment 14 | index: number 15 | } 16 | 17 | const ListBoard: React.FC = ({ list, index }) => { 18 | return ( 19 | 20 | {({ innerRef, dragHandleProps, draggableProps }, { isDragging }) => { 21 | return ( 22 |
27 | 32 | } 34 | {...dragHandleProps} 35 | className={`hover:bg-purple-50`} 36 | > 37 | 38 | 39 | 45 | {( 46 | { innerRef, droppableProps, placeholder }, 47 | { isDraggingOver }, 48 | ) => ( 49 | 54 | {list.cards.map((card, index) => ( 55 | 56 | ))} 57 | {placeholder} 58 | 59 | )} 60 | 61 | 62 | 63 | 67 | 68 | 69 |
70 | ) 71 | }} 72 |
73 | ) 74 | } 75 | 76 | export default ListBoard 77 | -------------------------------------------------------------------------------- /nextjs/src/components/pages/error/index.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import React from 'react' 3 | 4 | interface IndexProps { 5 | statusCode: number 6 | } 7 | 8 | const IndexPageComponent: React.FC = ({ statusCode }) => { 9 | const signOutButtonNode = () => { 10 | return ( 11 |
12 | 13 | 14 | 15 |
16 | ) 17 | } 18 | 19 | return ( 20 |
21 |
22 |
23 |

Nextjs Hasura Boilerplate

24 |

25 | {statusCode 26 | ? `An error ${statusCode} occurred on server` 27 | : 'An error occurred on client'} 28 |

29 |
30 |
31 | {signOutButtonNode()} 32 |
33 |
34 |
35 |
36 |
37 | ) 38 | } 39 | 40 | export default IndexPageComponent 41 | -------------------------------------------------------------------------------- /nextjs/src/components/pages/feeds/AddNewFeedForm.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Alert, 3 | Button, 4 | Card, 5 | CardBody, 6 | CardFooter, 7 | CardHeader, 8 | Input, 9 | } from '@retail-ui/core' 10 | import { useSession } from 'next-auth/client' 11 | import React, { useState } from 'react' 12 | 13 | import AccessDeniedIndicator from '@/components/AccessDeniedIndicator' 14 | import { useInsertFeedMutation } from '@/generated/graphql' 15 | 16 | const AddNewFeedForm = () => { 17 | const [body, setBody] = useState('') 18 | const [session] = useSession() 19 | const [errorMessage, setErrorMessage] = React.useState('') 20 | 21 | const [insertFeed, { loading, error }] = useInsertFeedMutation() 22 | 23 | React.useEffect(() => { 24 | setErrorMessage(error?.message || '') 25 | }, [error]) 26 | 27 | React.useEffect(() => { 28 | setErrorMessage('') 29 | }, [body]) 30 | 31 | if (!session) { 32 | return ( 33 | 34 | ) 35 | } 36 | 37 | const handleSubmit = async () => { 38 | try { 39 | await insertFeed({ variables: { body } }) 40 | setBody('') 41 | } catch (error) { 42 | console.log('Error: ', Object.entries(error)) 43 | } 44 | } 45 | 46 | const errorNode = () => { 47 | if (!errorMessage) { 48 | return null 49 | } 50 | return ( 51 | setErrorMessage('')} 57 | > 58 | ) 59 | } 60 | 61 | return ( 62 | 63 | 64 | 65 | {errorNode()} 66 | setBody(e.currentTarget.value)} 71 | onKeyDown={(e) => e.key === 'Enter' && handleSubmit()} 72 | /> 73 | 74 | 75 |
76 | 83 |
84 |
85 |
86 | ) 87 | } 88 | 89 | export default AddNewFeedForm 90 | -------------------------------------------------------------------------------- /nextjs/src/components/pages/feeds/feed.tsx: -------------------------------------------------------------------------------- 1 | import { Avatar, Card, CardBody } from '@retail-ui/core' 2 | import React from 'react' 3 | 4 | import { FeedFragment } from '@/generated/graphql' 5 | import { timeFromNow } from '@/utils' 6 | 7 | interface FeedPageProps { 8 | feed: FeedFragment 9 | } 10 | 11 | const FeedPage: React.FC = ({ feed }) => { 12 | return ( 13 | 14 | 15 |
16 |
17 | {feed.user && } 18 |
19 |
20 |
21 |

{feed.user.name}

22 |
23 |
24 |

{timeFromNow(feed.created_at)}

25 |
26 |
27 |
28 | {/* */} 29 |
30 |

{feed.body}

31 |
32 |
33 |
34 | ) 35 | } 36 | 37 | export default FeedPage 38 | -------------------------------------------------------------------------------- /nextjs/src/components/pages/feeds/graphql/FeedFragment.graphql: -------------------------------------------------------------------------------- 1 | fragment Feed on feeds { 2 | id 3 | created_at 4 | body 5 | user { 6 | id 7 | name 8 | image 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /nextjs/src/components/pages/feeds/graphql/Feeds.graphql: -------------------------------------------------------------------------------- 1 | subscription Feeds { 2 | feeds(order_by: { created_at: desc }) { 3 | ...Feed 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /nextjs/src/components/pages/feeds/graphql/InsertFeed.graphql: -------------------------------------------------------------------------------- 1 | mutation InsertFeed($body: String) { 2 | insert_feeds_one(object: { body: $body }) { 3 | ...Feed 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /nextjs/src/components/pages/feeds/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { useFeedsSubscription } from '@/generated/graphql' 4 | 5 | import AddNewFeedForm from './AddNewFeedForm' 6 | import Feed from './feed' 7 | 8 | const FeedsPage = () => { 9 | const { data, loading } = useFeedsSubscription() 10 | console.log(`🇻🇳 [LOG]: FeedsPage -> data`, data) 11 | 12 | return ( 13 |
14 |
15 | 16 |
17 |
18 | {(data?.feeds || []).map((feed) => { 19 | return ( 20 |
21 | 22 |
23 | ) 24 | })} 25 |
26 |
27 | ) 28 | } 29 | 30 | export default FeedsPage 31 | -------------------------------------------------------------------------------- /nextjs/src/components/pages/index/Book.tsx: -------------------------------------------------------------------------------- 1 | import { Badge } from '@retail-ui/core' 2 | import React from 'react' 3 | 4 | const Book: React.FC = () => { 5 | return ( 6 |
9 | room hinh 14 |
17 |
18 |

19 | The Local Hostel 20 |

21 | 22 | Great for solo travellers 23 | 24 |

25 | 31 | 36 | 37 | 6km from city center 38 |

39 |
40 |

41 | from $35 a night 42 |

43 |
44 |
45 | ) 46 | } 47 | 48 | export default Book 49 | -------------------------------------------------------------------------------- /nextjs/src/layouts/BoardLayout.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | import { useRouter } from 'next/router' 3 | import React, { useState } from 'react' 4 | 5 | import { NextPageWithLayout } from '@/types/page' 6 | 7 | // import links from '@/utils/uiLinks' 8 | import Header from './components/Header' 9 | import SidebarMobile from './components/main/SidebarMobile' 10 | // import Sidebar from './components/main/Sidebar' 11 | // import SidebarMobile from './components/SidebarMobile' 12 | 13 | interface BoardLayoutProps { 14 | title: string 15 | } 16 | 17 | const BoardLayout: React.FC = (props) => { 18 | const { children, title } = props 19 | const { query } = useRouter() 20 | const [isSideMenuOpen, setIsSideMenuOpen] = useState(false) 21 | 22 | React.useEffect(() => { 23 | setIsSideMenuOpen(false) 24 | }, [query]) 25 | 26 | return ( 27 | 28 | 29 | {`${title} ${query.boardId}` || 'sondh0127'} 30 | 31 | 32 | 33 | 34 |
35 |
setIsSideMenuOpen(!isSideMenuOpen)} /> 36 |
37 | {/* */} 38 | { 41 | console.log(`onClose`) 42 | 43 | setIsSideMenuOpen(false) 44 | }} 45 | /> 46 |
{children}
47 |
48 |
49 |
50 | ) 51 | } 52 | export default BoardLayout 53 | 54 | export const getBoardLayout = (title: string) => { 55 | const getLayout: NextPageWithLayout['getLayout'] = (page) => ( 56 | {page} 57 | ) 58 | return getLayout 59 | } 60 | -------------------------------------------------------------------------------- /nextjs/src/layouts/MainLayout.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | import React from 'react' 3 | 4 | import { NextPageWithLayout } from '@/types/page' 5 | 6 | import Header from './components/Header' 7 | 8 | interface MainLayoutProps { 9 | title: string 10 | footer?: () => React.ReactNode 11 | } 12 | 13 | const MainLayout: React.FC = (props) => { 14 | const { title, children } = props 15 | 16 | return ( 17 | 18 | 19 | {title || 'sondh0127'} 20 | 21 | 22 | 23 | 24 |
25 |
26 |
27 |
{children}
28 |
29 |
30 |
31 | ) 32 | } 33 | 34 | export default MainLayout 35 | 36 | export const getMainLayout = (title: string) => { 37 | const getLayout: NextPageWithLayout['getLayout'] = (page) => ( 38 | {page} 39 | ) 40 | return getLayout 41 | } 42 | -------------------------------------------------------------------------------- /nextjs/src/layouts/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import React from 'react' 3 | 4 | const Footer: React.FC = () => { 5 | return ( 6 |
7 |

8 | Copyright © 2020 9 | 10 | 11 | @sondh0127 12 | 13 | 14 |

15 |
16 | ) 17 | } 18 | 19 | export default Footer 20 | -------------------------------------------------------------------------------- /nextjs/src/layouts/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Dropdown, 3 | DropdownButton, 4 | DropdownItem, 5 | DropdownList, 6 | Input, 7 | InputAddon, 8 | InputGroup, 9 | } from '@retail-ui/core' 10 | import { useThemeCtx } from '@retail-ui/core' 11 | import { Transition } from '@tailwindui/react' 12 | import { useToggle } from 'ahooks' 13 | import { signOut, useSession } from 'next-auth/client' 14 | import Link from 'next/link' 15 | import { useRouter } from 'next/router' 16 | import React from 'react' 17 | 18 | import CustomLink from '@/components/CustomLink' 19 | import HamburgerIcon from '@/components/icons/HamburgerIcon' 20 | import Logo from '@/components/icons/Logo' 21 | import LogoutIcon from '@/components/icons/LogoutIcon' 22 | import MoonIcon from '@/components/icons/MoonIcon' 23 | import NotificationIcon from '@/components/icons/NotificationIcon' 24 | import ProfileIcon from '@/components/icons/ProfileIcon' 25 | import SearchIcon from '@/components/icons/SearchIcon' 26 | import SettingsIcon from '@/components/icons/SettingsIcon' 27 | import SunIcon from '@/components/icons/SunIcon' 28 | 29 | const routes = [ 30 | { 31 | name: 'Home', 32 | href: '/', 33 | }, 34 | { 35 | name: 'Feeds', 36 | href: '/feeds', 37 | }, 38 | { 39 | name: 'Account', 40 | href: '/account', 41 | }, 42 | { 43 | name: 'Boards', 44 | href: '/boards', 45 | }, 46 | ] 47 | 48 | const SessionItem: React.FC = () => { 49 | const [session] = useSession() 50 | 51 | if (session) { 52 | return ( 53 | 54 | 55 | {({ toggleOpen }) => ( 56 | 65 | )} 66 | 67 | 68 | 69 | 70 | 71 | Profile 72 | 73 | 74 | 75 | Settings 76 | 77 | signOut()}> 78 | 79 | Log out 80 | 81 | 82 | 83 | ) 84 | } 85 | 86 | return ( 87 | 88 | 89 | Login 90 | 91 | 92 | ) 93 | } 94 | 95 | interface HeaderProps { 96 | onMenuClick?: () => void 97 | } 98 | 99 | const Header: React.FC = (props) => { 100 | const { onMenuClick } = props 101 | const router = useRouter() 102 | const { theme, toggleTheme } = useThemeCtx() 103 | const [isMenuOpen, { toggle: toggleMenu }] = useToggle(false) 104 | 105 | React.useEffect(() => { 106 | toggleMenu(false) 107 | }, [router.pathname]) 108 | 109 | return ( 110 |
111 |
112 | {/* Mobile hamburger */} 113 |
114 | {onMenuClick && ( 115 | 124 | )} 125 | 126 | 127 |
128 | {(routes || []).map((item, index) => ( 129 | 130 | {({ isActive, href }) => ( 131 | 137 | 140 | {item.name} 141 | 142 | 143 | )} 144 | 145 | ))} 146 |
147 |
148 | 149 | {/* Right bar */} 150 |
151 | 165 |
166 | 167 | 168 | 169 | 170 | 176 | 177 |
178 | 186 | {/* Notifications menu */} 187 | 188 | 189 | {({ toggleOpen }) => ( 190 | 200 | )} 201 | 202 | 203 | 204 | Messages 205 | 206 | 13 207 | 208 | 209 | 210 | Sales 211 | 212 | 2 213 | 214 | 215 | 216 | Alerts 217 | 218 | 219 | 220 | {/* Profile menu */} 221 | 222 | 223 |
224 |
225 | {/* Menu link */} 226 | 237 |
238 |
239 | 240 | 241 | 242 | 243 | 249 | 250 |
251 | {(routes || []).map((item, index) => ( 252 | 253 | {({ isActive, href }) => ( 254 | 260 | 263 | {item.name} 264 | 265 | 266 | )} 267 | 268 | ))} 269 |
270 |
271 |
272 | ) 273 | } 274 | 275 | export default Header 276 | -------------------------------------------------------------------------------- /nextjs/src/layouts/components/main/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export interface SidebarProps { 4 | className?: string 5 | } 6 | 7 | const Sidebar: React.FC = () => { 8 | return ( 9 | 22 | ) 23 | } 24 | 25 | export default Sidebar 26 | -------------------------------------------------------------------------------- /nextjs/src/layouts/components/main/SidebarMobile.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ButtonIcon, 3 | Drawer, 4 | DrawerHeader, 5 | Input, 6 | XSolid, 7 | } from '@retail-ui/core' 8 | import Link from 'next/link' 9 | import { useRouter } from 'next/router' 10 | import React, { Fragment } from 'react' 11 | import { useClickAway, useKey } from 'react-use' 12 | 13 | import AddInput from '@/components/AddInput' 14 | import Logo from '@/components/icons/Logo' 15 | import { 16 | BoardFragmentDoc, 17 | BoardsQuery, 18 | InsertBoardMutation, 19 | useBoardsQuery, 20 | useDeleteBoardMutation, 21 | useInsertBoardMutation, 22 | } from '@/generated/graphql' 23 | 24 | interface SidebarMobileProps { 25 | isSideMenuOpen: boolean 26 | onClose: () => void 27 | } 28 | 29 | const SidebarMobile: React.FC = ({ 30 | isSideMenuOpen, 31 | onClose, 32 | }) => { 33 | const { data, loading } = useBoardsQuery() 34 | const [isOnNewBoard, setIsOnNewBoard] = React.useState(false) 35 | const router = useRouter() 36 | 37 | /* Insert new board */ 38 | const [insertBoard, { loading: insertLoading }] = useInsertBoardMutation() 39 | 40 | /* Delete a board */ 41 | const [deleteBoard, { loading: deleteLoading }] = useDeleteBoardMutation() 42 | 43 | const handleInsertBoard = React.useCallback(async (boardName: string) => { 44 | try { 45 | const { data } = await insertBoard({ 46 | variables: { 47 | name: boardName, 48 | }, 49 | update: (cache, { data }) => { 50 | if (data?.insert_boards_one) { 51 | const cacheId = cache.identify(data.insert_boards_one) 52 | if (cacheId) { 53 | cache.modify({ 54 | fields: { 55 | boards: (existingBoards = [], { toReference }) => { 56 | // const newBoardRef = cache.writeFragment({ 57 | // data: data?.insert_boards_one, 58 | // fragment: BoardFragmentDoc, 59 | // }) 60 | // return [...existingBoards, newBoardRef] 61 | return [...existingBoards, toReference(cacheId)] 62 | }, 63 | }, 64 | }) 65 | } 66 | } 67 | }, 68 | }) 69 | router.push(`/boards/${data?.insert_boards_one?.id}`) 70 | } catch (error) { 71 | console.log('Error: ', Object.entries(error)) 72 | } 73 | }, []) 74 | 75 | const handleDeleteBoard = async (id: number) => { 76 | // add popup confirm 77 | await deleteBoard({ 78 | variables: { id }, 79 | update: (cache) => { 80 | cache.evict({ id: `boards:${id}` }) 81 | cache.gc() 82 | }, 83 | }) 84 | console.log({ id }) 85 | } 86 | const handleClickAway = (value: string) => { 87 | if (!value) { 88 | setIsOnNewBoard(false) 89 | } 90 | } 91 | 92 | return ( 93 | 94 | 95 | 96 | 97 | 98 | 151 | 152 | 153 | ) 154 | } 155 | 156 | export default SidebarMobile 157 | -------------------------------------------------------------------------------- /nextjs/src/lib/apolloClient.tsx: -------------------------------------------------------------------------------- 1 | import { ApolloClient, HttpLink, split } from '@apollo/client' 2 | import { NormalizedCacheObject } from '@apollo/client/cache' 3 | import { WebSocketLink } from '@apollo/client/link/ws' 4 | import { getMainDefinition } from '@apollo/client/utilities' 5 | import fetch from 'isomorphic-unfetch' 6 | import ws from 'isomorphic-ws' 7 | import React from 'react' 8 | import { SubscriptionClient } from 'subscriptions-transport-ws' 9 | 10 | import { cache } from './cache' 11 | 12 | const createHttpLink = (token: string) => { 13 | const httpLink = new HttpLink({ 14 | uri: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080/v1/graphql', 15 | credentials: 'include', 16 | headers: { Authorization: `Bearer ${token}` }, 17 | fetch, 18 | }) 19 | return httpLink 20 | } 21 | 22 | const createWSLink = (token: string) => { 23 | return new WebSocketLink( 24 | new SubscriptionClient( 25 | process.env.NEXT_PUBLIC_WS_URL || 'ws://localhost:8080/v1/graphql', 26 | { 27 | lazy: true, 28 | reconnect: true, 29 | connectionParams: async () => { 30 | return { 31 | headers: { Authorization: `Bearer ${token}` }, 32 | } 33 | }, 34 | }, 35 | ws, 36 | ), 37 | ) 38 | } 39 | /* ApolloClient */ 40 | let apolloClient: ApolloClient 41 | 42 | export const createApolloClient = (token: string) => { 43 | const ssrMode = typeof window === 'undefined' 44 | 45 | const link = !ssrMode 46 | ? split( 47 | //only create the split in the browser 48 | // split based on operation type 49 | ({ query }) => { 50 | const definition = getMainDefinition(query) 51 | return ( 52 | definition.kind === 'OperationDefinition' && 53 | definition.operation === 'subscription' 54 | ) 55 | }, 56 | createWSLink(token), 57 | createHttpLink(token), 58 | ) 59 | : createHttpLink(token) 60 | 61 | return new ApolloClient({ ssrMode, link, cache }) 62 | } 63 | 64 | export const initializeApollo = (initialState = {}, token: string) => { 65 | const _apolloClient = apolloClient ?? createApolloClient(token) 66 | 67 | // If your page has Next.js data fetching methods that use Apollo Client 68 | // the initial state gets hydrated here 69 | if (initialState) { 70 | // Get existing cache, loaded during client side data fetching 71 | const existingCache = _apolloClient.extract() 72 | // Restore the cache using the data passed from getStaticProps/getServerSideProps 73 | // combined with the existing cached data 74 | _apolloClient.cache.restore({ ...existingCache, ...initialState }) 75 | } 76 | // For SSG and SSR always create a new Apollo Client 77 | if (typeof window === 'undefined') return _apolloClient 78 | // Create the Apollo Client once in the client 79 | if (!apolloClient) apolloClient = _apolloClient 80 | 81 | return _apolloClient 82 | } 83 | 84 | export function useApollo(initialState: any, token: string) { 85 | const store = React.useMemo(() => initializeApollo(initialState, token), [ 86 | initialState, 87 | token, 88 | ]) 89 | return store 90 | } 91 | -------------------------------------------------------------------------------- /nextjs/src/lib/cache.ts: -------------------------------------------------------------------------------- 1 | import { InMemoryCache } from '@apollo/client' 2 | import { concatPagination } from '@apollo/client/utilities' 3 | 4 | export const cache = new InMemoryCache({ 5 | typePolicies: { 6 | Query: { 7 | fields: { 8 | feeds: concatPagination(), 9 | // boards: { 10 | // merge(existing = [], incoming: any[]) { 11 | // return [...existing, ...incoming] 12 | // }, 13 | // }, 14 | }, 15 | }, 16 | }, 17 | }) 18 | -------------------------------------------------------------------------------- /nextjs/src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import { NextPage } from 'next' 2 | import Head from 'next/head' 3 | import React from 'react' 4 | 5 | import Page from '../components/pages/error' 6 | 7 | const Custom404Page: NextPage = () => { 8 | return ( 9 | <> 10 | 11 | Error Page 12 | 13 | 14 | 15 | ) 16 | } 17 | 18 | export default Custom404Page 19 | -------------------------------------------------------------------------------- /nextjs/src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '@/styles/tailwind.css' 2 | 3 | import { ApolloProvider } from '@apollo/client' 4 | import ProgressBar from '@badrap/bar-of-progress' 5 | import { ThemeProvider } from '@retail-ui/core' 6 | import { Provider as NextAuthProvider } from 'next-auth/client' 7 | import Router from 'next/router' 8 | import React from 'react' 9 | 10 | import { useApollo } from '@/lib/apolloClient' 11 | import { AppPropsWithLayout } from '@/types/page' 12 | 13 | const progress = new ProgressBar({ 14 | size: 2, 15 | color: '#7e3af2', 16 | className: 'bar-of-progress', 17 | delay: 100, 18 | }) 19 | 20 | Router.events.on('routeChangeStart', progress.start) 21 | Router.events.on('routeChangeComplete', progress.finish) 22 | Router.events.on('routeChangeError', progress.finish) 23 | 24 | const App = ({ Component, pageProps }: AppPropsWithLayout) => { 25 | const { session } = pageProps 26 | const apolloClient = useApollo(pageProps.initialApolloState, session?.token) 27 | const getLayout = Component.getLayout || ((page) => page) 28 | 29 | return ( 30 | <> 31 | 32 | 33 | 34 | {getLayout()} 35 | 36 | 37 | 38 | 39 | ) 40 | } 41 | 42 | export default App 43 | -------------------------------------------------------------------------------- /nextjs/src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import NextDocument, { 2 | DocumentContext, 3 | Head, 4 | Html, 5 | Main, 6 | NextScript, 7 | } from 'next/document' 8 | import React from 'react' 9 | import { resetServerContext } from 'react-beautiful-dnd' 10 | 11 | export default class MyDocument extends NextDocument { 12 | static async getInitialProps(ctx: DocumentContext) { 13 | resetServerContext() 14 | try { 15 | const initialProps = await NextDocument.getInitialProps(ctx) 16 | return { 17 | ...initialProps, 18 | styles: <>{initialProps.styles}, 19 | } 20 | // eslint-disable-next-line no-empty 21 | } finally { 22 | } 23 | } 24 | render() { 25 | return ( 26 | 27 | 28 | 29 |
30 | 31 | 32 | 33 | ) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /nextjs/src/pages/_error.tsx: -------------------------------------------------------------------------------- 1 | import { NextPage } from 'next' 2 | import Head from 'next/head' 3 | import React from 'react' 4 | 5 | import Page from '../components/pages/error' 6 | 7 | interface ErrorProps { 8 | statusCode: number 9 | } 10 | 11 | const Error: NextPage = ({ statusCode }) => { 12 | return ( 13 | <> 14 | 15 | Error Page 16 | 17 | 18 | 19 | ) 20 | } 21 | 22 | Error.getInitialProps = ({ res, err }) => { 23 | const statusCode = res?.statusCode || err?.statusCode || 404 24 | 25 | return { statusCode } 26 | } 27 | 28 | export default Error 29 | -------------------------------------------------------------------------------- /nextjs/src/pages/account.tsx: -------------------------------------------------------------------------------- 1 | import { GetServerSideProps } from 'next' 2 | import { getSession } from 'next-auth/client' 3 | import React from 'react' 4 | 5 | import AccessDeniedIndicator from '@/components/AccessDeniedIndicator' 6 | import Account from '@/components/pages/account' 7 | import { getMainLayout } from '@/layouts/MainLayout' 8 | import { NextPageWithLayout } from '@/types/page' 9 | import { SessionProp } from '@/types/session' 10 | 11 | const AccountPage: NextPageWithLayout = ({ session }) => { 12 | if (!session) { 13 | return 14 | } 15 | return 16 | } 17 | 18 | export const getServerSideProps: GetServerSideProps = async ( 19 | ctx, 20 | ) => { 21 | const session = await getSession(ctx as any) 22 | 23 | return { 24 | props: { session }, 25 | } 26 | } 27 | AccountPage.getLayout = getMainLayout('My account') 28 | 29 | export default AccountPage 30 | -------------------------------------------------------------------------------- /nextjs/src/pages/api/auth/[...nextauth].ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client' 2 | import jwt from 'jsonwebtoken' 3 | import { NextApiRequest, NextApiResponse } from 'next' 4 | import NextAuth from 'next-auth' 5 | import Adapters from 'next-auth/adapters' 6 | import Providers from 'next-auth/providers' 7 | 8 | import Session from '@/types/session' 9 | import Token from '@/types/token' 10 | import User from '@/types/user' 11 | 12 | const jwtSecret = JSON.parse(process.env.AUTH_PRIVATE_KEY || ``) 13 | 14 | const prisma = new PrismaClient() 15 | 16 | const options = { 17 | providers: [ 18 | Providers.Google({ 19 | clientId: process.env.GOOGLE_CLIENT_ID || '', 20 | clientSecret: process.env.GOOGLE_CLIENT_SECRET || '', 21 | }), 22 | ], 23 | database: process.env.DATABASE_URL, 24 | // @ts-ignore 25 | adapter: Adapters.Prisma.Adapter({ prisma }), 26 | session: { 27 | jwt: true, 28 | }, 29 | jwt: { 30 | encode: async ({ token, secret }: { token: Token; secret: string }) => { 31 | const tokenContents = { 32 | id: `${token.id}`, 33 | name: token.name, 34 | email: token.email, 35 | picture: token.picture, 36 | 'https://hasura.io/jwt/claims': { 37 | 'x-hasura-allowed-roles': ['admin', 'user'], 38 | 'x-hasura-default-role': 'user', 39 | 'x-hasura-role': 'user', 40 | 'x-hasura-user-id': `${token.id}`, 41 | }, 42 | iat: Date.now() / 1000, 43 | exp: Math.floor(Date.now() / 1000) + 72 * 60 * 60, 44 | sub: `${token.id}`, 45 | } 46 | 47 | const encodedToken = jwt.sign(tokenContents, jwtSecret.key, { 48 | algorithm: jwtSecret.type, 49 | }) 50 | 51 | return encodedToken 52 | }, 53 | decode: async ({ token, secret }: { token: string; secret: string }) => { 54 | const decodedToken = jwt.verify(token, jwtSecret.key, { 55 | algorithms: jwtSecret.type, 56 | }) 57 | 58 | return decodedToken 59 | }, 60 | }, 61 | debug: true, 62 | callbacks: { 63 | session: async (session: Session, user: User) => { 64 | const encodedToken = jwt.sign(user, jwtSecret.key, { 65 | algorithm: jwtSecret.type, 66 | }) 67 | 68 | session.id = user.id 69 | session.token = encodedToken 70 | 71 | return Promise.resolve(session) 72 | }, 73 | jwt: async ( 74 | token: Token, 75 | user: User, 76 | account: any, 77 | profile: any, 78 | isNewUser: any, 79 | ) => { 80 | const isSignIn = user ? true : false 81 | 82 | if (isSignIn) { 83 | token.id = user.id 84 | } 85 | 86 | return Promise.resolve(token) 87 | }, 88 | }, 89 | } 90 | 91 | const Auth = (req: NextApiRequest, res: NextApiResponse) => 92 | // @ts-ignore 93 | NextAuth(req, res, options) 94 | 95 | export default Auth 96 | -------------------------------------------------------------------------------- /nextjs/src/pages/boards/[boardId].tsx: -------------------------------------------------------------------------------- 1 | import { GetServerSideProps } from 'next' 2 | import { getSession } from 'next-auth/client' 3 | import React from 'react' 4 | 5 | import AccessDeniedIndicator from '@/components/AccessDeniedIndicator' 6 | import BoardPage from '@/components/pages/boards/board' 7 | import { getBoardLayout } from '@/layouts/BoardLayout' 8 | import { NextPageWithLayout } from '@/types/page' 9 | import { SessionProp } from '@/types/session' 10 | 11 | const Board: NextPageWithLayout = ({ session }) => { 12 | if (!session) { 13 | return 14 | } 15 | 16 | return 17 | } 18 | 19 | export const getServerSideProps: GetServerSideProps = async ( 20 | ctx, 21 | ) => { 22 | const session = await getSession(ctx as any) 23 | 24 | return { 25 | props: { session }, 26 | } 27 | } 28 | Board.getLayout = getBoardLayout('Board') 29 | 30 | export default Board 31 | -------------------------------------------------------------------------------- /nextjs/src/pages/boards/index.tsx: -------------------------------------------------------------------------------- 1 | import { GetServerSideProps } from 'next' 2 | import { getSession } from 'next-auth/client' 3 | import React from 'react' 4 | 5 | import AccessDeniedIndicator from '@/components/AccessDeniedIndicator' 6 | import BoardsPage from '@/components/pages/boards' 7 | import { getMainLayout } from '@/layouts/MainLayout' 8 | import { NextPageWithLayout } from '@/types/page' 9 | import { SessionProp } from '@/types/session' 10 | 11 | const Boards: NextPageWithLayout = ({ session }) => { 12 | if (!session) { 13 | return 14 | } 15 | return 16 | } 17 | 18 | export const getServerSideProps: GetServerSideProps = async ( 19 | ctx, 20 | ) => { 21 | const session = await getSession(ctx as any) 22 | return { props: { session } } 23 | } 24 | 25 | Boards.getLayout = getMainLayout('Boards') 26 | export default Boards 27 | -------------------------------------------------------------------------------- /nextjs/src/pages/feeds.tsx: -------------------------------------------------------------------------------- 1 | import { GetServerSideProps } from 'next' 2 | import { getSession } from 'next-auth/client' 3 | import React from 'react' 4 | 5 | import AccessDeniedIndicator from '@/components/AccessDeniedIndicator' 6 | import FeedsPage from '@/components/pages/feeds' 7 | import { getMainLayout } from '@/layouts/MainLayout' 8 | import { NextPageWithLayout } from '@/types/page' 9 | import { SessionProp } from '@/types/session' 10 | 11 | type FeedsProps = SessionProp 12 | 13 | const Feeds: NextPageWithLayout = ({ session }) => { 14 | if (!session) { 15 | return 16 | } 17 | 18 | return 19 | } 20 | 21 | export const getServerSideProps: GetServerSideProps = async ( 22 | ctx, 23 | ) => { 24 | const session = await getSession(ctx as any) 25 | 26 | return { props: { session } } 27 | } 28 | 29 | Feeds.getLayout = getMainLayout('Feeds') 30 | 31 | export default Feeds 32 | -------------------------------------------------------------------------------- /nextjs/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { useApolloClient } from '@apollo/client' 2 | import { Button } from '@retail-ui/core' 3 | import { GetServerSideProps } from 'next' 4 | import { getSession, signIn, signOut } from 'next-auth/client' 5 | import Link from 'next/link' 6 | import React from 'react' 7 | 8 | import { ItemLink } from '@/components/ItemLink' 9 | import Book from '@/components/pages/index/Book' 10 | import { getMainLayout } from '@/layouts/MainLayout' 11 | import { NextPageWithLayout } from '@/types/page' 12 | import Session, { SessionProp } from '@/types/session' 13 | 14 | interface IndexPageProps extends SessionProp { 15 | session: Session | null 16 | } 17 | 18 | const IndexPage: NextPageWithLayout = ({ session }) => { 19 | const apolloClient = useApolloClient() 20 | 21 | const authButtonNode = () => { 22 | if (session) { 23 | apolloClient.resetStore() 24 | return ( 25 | 26 | 35 | 36 | ) 37 | } 38 | 39 | return ( 40 | 41 | 50 | 51 | ) 52 | } 53 | 54 | return ( 55 |
56 |
57 |

58 | Nextjs Hasura Fullstack 59 |

60 |

61 | A boilerplate that uses{' '} 62 | Hasura and{' '} 63 | Next.js to develop web 64 | applications This demo has been built using{' '} 65 | Tailwindcss,{' '} 66 | NextAuth.js and{' '} 67 | 68 | ApolloClient 69 | 70 |

71 |
72 |
73 | 74 |
75 |
{authButtonNode()}
76 |
77 | ) 78 | } 79 | 80 | export const getServerSideProps: GetServerSideProps = async ( 81 | ctx, 82 | ) => { 83 | const session = await getSession(ctx as any) 84 | 85 | return { 86 | props: { 87 | session, 88 | }, 89 | } 90 | } 91 | 92 | IndexPage.getLayout = getMainLayout('Homepage') 93 | 94 | export default IndexPage 95 | -------------------------------------------------------------------------------- /nextjs/src/styles/bar-of-progress.css: -------------------------------------------------------------------------------- 1 | .bar-of-progress { 2 | &::after { 3 | content: ''; 4 | display: block; 5 | position: absolute; 6 | right: 0; 7 | width: 100px; 8 | height: 100%; 9 | box-shadow: 0 0 10px currentColor, 0 0 5px currentColor; 10 | transform: rotate(3deg) translate(0, -4px); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /nextjs/src/styles/tailwind.css: -------------------------------------------------------------------------------- 1 | @import url('https://rsms.me/inter/inter.css'); 2 | @tailwind base; 3 | 4 | /* Write your own custom base styles here */ 5 | 6 | /* Start purging... */ 7 | @tailwind components; 8 | /* Stop purging. */ 9 | 10 | /* Write you own custom component styles here */ 11 | /*! purgecss start ignore */ 12 | @import './bar-of-progress.css'; 13 | 14 | /*! purgecss end ignore */ 15 | 16 | /* Start purging... */ 17 | @tailwind utilities; 18 | /* Stop purging. */ 19 | 20 | /* Your own custom utilities */ 21 | -------------------------------------------------------------------------------- /nextjs/src/types/page.ts: -------------------------------------------------------------------------------- 1 | import { NextPage } from 'next' 2 | import { AppProps } from 'next/app' 3 | 4 | export type NextPageWithLayout

= NextPage & { 5 | getLayout?: (component: JSX.Element) => JSX.Element 6 | } 7 | 8 | export type AppPropsWithLayout

= AppProps & { 9 | Component: NextPageWithLayout 10 | } 11 | -------------------------------------------------------------------------------- /nextjs/src/types/session.ts: -------------------------------------------------------------------------------- 1 | import { Session as AuthSession } from 'next-auth/client' 2 | 3 | export default interface Session extends AuthSession { 4 | id?: number 5 | token?: string 6 | } 7 | 8 | export type SessionProp = { 9 | session: Session | null 10 | } 11 | -------------------------------------------------------------------------------- /nextjs/src/types/token.ts: -------------------------------------------------------------------------------- 1 | export default interface Token { 2 | id: number 3 | email: string 4 | name: string 5 | picture: string 6 | } 7 | -------------------------------------------------------------------------------- /nextjs/src/types/user.ts: -------------------------------------------------------------------------------- 1 | export default interface User { 2 | id: number 3 | name: string 4 | image: string 5 | email: string 6 | } 7 | -------------------------------------------------------------------------------- /nextjs/src/utils/createCtx.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | export const createCtx = | null>() => { 4 | const ctx = React.createContext(undefined) 5 | 6 | const useCtx = () => { 7 | const c = React.useContext(ctx) 8 | if (c === undefined) 9 | throw new Error('useCtx must be inside a Provider with a value') 10 | return c 11 | } 12 | 13 | return [useCtx, ctx.Provider] as const // 'as const' makes TypeScript infer a tuple 14 | } 15 | -------------------------------------------------------------------------------- /nextjs/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './timeFromNow' 2 | -------------------------------------------------------------------------------- /nextjs/src/utils/timeFromNow.tsx: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs' 2 | import relativeTime from 'dayjs/plugin/relativeTime' 3 | 4 | dayjs.extend(relativeTime) 5 | 6 | export const timeFromNow = (time: string): string => { 7 | return dayjs(time).fromNow() 8 | } 9 | -------------------------------------------------------------------------------- /nextjs/src/zustands/boards.ts: -------------------------------------------------------------------------------- 1 | import create from 'zustand' 2 | 3 | export const useLastPositionNumber = create<{ 4 | lastPosition: number 5 | setLastPosition: (lastPosition: number) => void 6 | }>((set) => ({ 7 | lastPosition: 0, 8 | setLastPosition: (lastPosition) => set(() => ({ lastPosition })), 9 | })) 10 | -------------------------------------------------------------------------------- /nextjs/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const defaultTheme = require('tailwindcss/defaultTheme') 3 | const { resolveConfig } = require('@retail-ui/core') 4 | 5 | module.exports = resolveConfig({ 6 | experimental: { 7 | applyComplexClasses: true, 8 | uniformColorPalette: true, 9 | extendedSpacingScale: true, 10 | defaultLineHeights: true, 11 | extendedFontSizeScale: true, 12 | removeDeprecatedGapUtilities: true, 13 | purgeLayersByDefault: true, 14 | }, 15 | purge: ['./src/**/*.{js,ts,jsx,tsx}'], 16 | theme: { 17 | extend: { 18 | fontFamily: { 19 | sans: ['Inter', ...defaultTheme.fontFamily.sans], 20 | }, 21 | }, 22 | }, 23 | variants: { 24 | display: ['responsive', 'dark', 'last', 'group-hover'], 25 | }, 26 | plugins: [require('@tailwindcss/ui')], 27 | }) 28 | -------------------------------------------------------------------------------- /nextjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "alwaysStrict": true, 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "esModuleInterop": true, 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "jsx": "preserve", 18 | "baseUrl": ".", 19 | "paths": { 20 | "@/*": ["src/*"] 21 | } 22 | }, 23 | "exclude": ["node_modules", "tailwind.config.js"], 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"] 25 | } 26 | -------------------------------------------------------------------------------- /nextjs/vendor.d.ts: -------------------------------------------------------------------------------- 1 | // declare module 'next-auth/client' 2 | // declare module 'next-auth' 3 | // declare module 'next-auth/providers' 4 | --------------------------------------------------------------------------------