├── backend ├── metadata │ ├── functions.yaml │ ├── actions.graphql │ ├── allow_list.yaml │ ├── cron_triggers.yaml │ ├── remote_schemas.yaml │ ├── query_collections.yaml │ ├── version.yaml │ ├── actions.yaml │ └── tables.yaml ├── migrations │ ├── 1594912494982_create_table_public_users │ │ ├── down.sql │ │ └── up.sql │ ├── 1595435360739_create_table_public_boards │ │ ├── down.sql │ │ └── up.sql │ ├── 1595435462203_create_table_public_cards │ │ ├── down.sql │ │ └── up.sql │ ├── 1595435581334_create_table_public_lists │ │ ├── down.sql │ │ └── up.sql │ ├── 1594912624761_create_table_public_accounts │ │ ├── down.sql │ │ └── up.sql │ ├── 1594912673190_create_table_public_sessions │ │ ├── down.sql │ │ └── up.sql │ ├── 1595435754795_create_table_public_boards_users │ │ ├── down.sql │ │ └── up.sql │ ├── 1594912735062_create_table_public_verification_requests │ │ ├── down.sql │ │ └── up.sql │ ├── 1595435097518_alter_table_public_users_add_column_theme │ │ ├── down.sql │ │ └── up.sql │ ├── 1595670601846_alter_table_public_users_alter_column_name │ │ ├── down.sql │ │ └── up.sql │ └── metadata.yaml ├── config.yaml ├── .env.example ├── .dockerignore ├── Dockerfile └── docker-compose.yml ├── frontend ├── next-env.d.ts ├── types │ ├── user.ts │ └── session.ts ├── vendor.d.ts ├── public │ └── images │ │ ├── favicon.ico │ │ └── bug_fixed.svg ├── tsconfig.test.json ├── components │ ├── pages │ │ ├── index │ │ │ └── index.tsx │ │ ├── cards │ │ │ └── show │ │ │ │ ├── settings.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── details-form.tsx │ │ └── boards │ │ │ ├── show │ │ │ ├── card.tsx │ │ │ ├── invite-users.tsx │ │ │ ├── list.tsx │ │ │ └── index.tsx │ │ │ └── index │ │ │ └── index.tsx │ ├── layout │ │ ├── index.tsx │ │ └── container.tsx │ ├── loader │ │ └── index.tsx │ ├── access-denied-indicator │ │ └── index.tsx │ └── navbar │ │ └── index.tsx ├── tsconfig.server.json ├── .env.example ├── pages │ ├── index.tsx │ ├── cards │ │ └── [cardId] │ │ │ └── index.tsx │ ├── boards │ │ ├── [boardId] │ │ │ └── index.tsx │ │ └── index.tsx │ ├── _app.tsx │ └── api │ │ └── auth │ │ └── [...nextauth].ts ├── tsconfig.build.json ├── tsconfig.base.json ├── tsconfig.json ├── lib │ └── with-graphql.tsx ├── package.json └── tslint.json ├── yarn.lock ├── .gitignore ├── .vscode └── settings.json ├── license.md └── readme.md /backend/metadata/functions.yaml: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /backend/metadata/actions.graphql: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /backend/metadata/allow_list.yaml: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /backend/metadata/cron_triggers.yaml: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /backend/metadata/remote_schemas.yaml: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /backend/metadata/query_collections.yaml: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /backend/metadata/version.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | -------------------------------------------------------------------------------- /backend/migrations/1594912494982_create_table_public_users/down.sql: -------------------------------------------------------------------------------- 1 | 2 | DROP TABLE "public"."users"; -------------------------------------------------------------------------------- /backend/migrations/1595435360739_create_table_public_boards/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE "public"."boards"; 2 | -------------------------------------------------------------------------------- /backend/migrations/1595435462203_create_table_public_cards/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE "public"."cards"; 2 | -------------------------------------------------------------------------------- /backend/migrations/1595435581334_create_table_public_lists/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE "public"."lists"; 2 | -------------------------------------------------------------------------------- /backend/migrations/1594912624761_create_table_public_accounts/down.sql: -------------------------------------------------------------------------------- 1 | 2 | DROP TABLE "public"."accounts"; -------------------------------------------------------------------------------- /backend/migrations/1594912673190_create_table_public_sessions/down.sql: -------------------------------------------------------------------------------- 1 | 2 | DROP TABLE "public"."sessions"; -------------------------------------------------------------------------------- /frontend/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/.DS_STORE 2 | **/node_modules 3 | **/.next 4 | **/.env 5 | **/out 6 | 7 | **/*.pem 8 | **/.vercel 9 | -------------------------------------------------------------------------------- /backend/migrations/1595435754795_create_table_public_boards_users/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE "public"."boards_users"; 2 | -------------------------------------------------------------------------------- /frontend/types/user.ts: -------------------------------------------------------------------------------- 1 | export default interface IUser { 2 | id: number; 3 | name: string; 4 | image: string; 5 | } 6 | -------------------------------------------------------------------------------- /backend/migrations/1594912735062_create_table_public_verification_requests/down.sql: -------------------------------------------------------------------------------- 1 | 2 | DROP TABLE "public"."verification_requests"; -------------------------------------------------------------------------------- /frontend/vendor.d.ts: -------------------------------------------------------------------------------- 1 | declare module "next-auth/client"; 2 | declare module "next-auth"; 3 | declare module "next-auth/providers"; 4 | -------------------------------------------------------------------------------- /backend/migrations/1595435097518_alter_table_public_users_add_column_theme/down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "public"."users" DROP COLUMN "theme"; 2 | -------------------------------------------------------------------------------- /backend/metadata/actions.yaml: -------------------------------------------------------------------------------- 1 | actions: [] 2 | custom_types: 3 | enums: [] 4 | input_objects: [] 5 | objects: [] 6 | scalars: [] 7 | -------------------------------------------------------------------------------- /frontend/public/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghoshnirmalya/nextjs-hasura-trello-clone/HEAD/frontend/public/images/favicon.ico -------------------------------------------------------------------------------- /frontend/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.build.json", 3 | "compilerOptions": { 4 | "module": "commonjs" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /backend/migrations/1595670601846_alter_table_public_users_alter_column_name/down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "public"."users" ALTER COLUMN "name" SET NOT NULL; 2 | -------------------------------------------------------------------------------- /backend/migrations/1595670601846_alter_table_public_users_alter_column_name/up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "public"."users" ALTER COLUMN "name" DROP NOT NULL; 2 | -------------------------------------------------------------------------------- /backend/migrations/1595435097518_alter_table_public_users_add_column_theme/up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "public"."users" ADD COLUMN "theme" bpchar NOT NULL DEFAULT 'dark'; 2 | -------------------------------------------------------------------------------- /frontend/types/session.ts: -------------------------------------------------------------------------------- 1 | export default interface ISession { 2 | user: { 3 | name: string; 4 | email: string; 5 | image: string; 6 | }; 7 | expires: string; 8 | } 9 | -------------------------------------------------------------------------------- /backend/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 | -------------------------------------------------------------------------------- /frontend/components/pages/index/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Box } from "@chakra-ui/core"; 3 | 4 | const Index = () => { 5 | return Hello; 6 | }; 7 | 8 | export default Index; 9 | -------------------------------------------------------------------------------- /backend/.env.example: -------------------------------------------------------------------------------- 1 | HASURA_GRAPHQL_DATABASE_URL=postgres://postgres:@db:5432/postgres 2 | HASURA_GRAPHQL_ADMIN_SECRET=secret 3 | HASURA_GRAPHQL_MIGRATIONS_DIR=./migrations 4 | HASURA_GRAPHQL_METADATA_DIR=./metadata 5 | -------------------------------------------------------------------------------- /backend/.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 | -------------------------------------------------------------------------------- /frontend/tsconfig.server.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "dist", 6 | "target": "es2017", 7 | "isolatedModules": false, 8 | "noEmit": false 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM hasura/graphql-engine:v1.3.0-beta.4.cli-migrations-v2 2 | 3 | # Set working directory 4 | RUN mkdir -p /usr/src/app 5 | WORKDIR /usr/src/app 6 | 7 | COPY . /usr/src/app 8 | 9 | CMD graphql-engine \ 10 | --database-url $HASURA_GRAPHQL_DATABASE_URL \ 11 | serve 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/node_modules": true, // this excludes all folders 4 | "**/dist": true 5 | }, 6 | "files.autoSave": "onFocusChange", 7 | "breadcrumbs.enabled": true, 8 | "eslint.alwaysShowStatus": true, 9 | "editor.formatOnSave": true 10 | } 11 | -------------------------------------------------------------------------------- /backend/migrations/metadata.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | tables: 3 | - table: 4 | schema: public 5 | name: accounts 6 | - table: 7 | schema: public 8 | name: sessions 9 | - table: 10 | schema: public 11 | name: users 12 | - table: 13 | schema: public 14 | name: verification_requests 15 | -------------------------------------------------------------------------------- /frontend/.env.example: -------------------------------------------------------------------------------- 1 | API_URL=http://localhost:8080/v1/graphql 2 | WS_URL=ws://localhost:8080/v1/graphql 3 | AUTH_URL=http://localhost:3000/api/authentication/routes/v1 4 | HASURA_ADMIN_SECRET=secret 5 | DATABASE_URL=postgres://postgres:@localhost:5432/postgres 6 | AUTH_PRIVATE_KEY="" 7 | AUTH_PUBLIC_KEY="" 8 | NEXTAUTH_URL=http://localhost:3000 9 | EMAIL_SERVER="" 10 | EMAIL_FROM=noreply@example.com 11 | -------------------------------------------------------------------------------- /frontend/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Head from "next/head"; 3 | import Page from "components/pages/index"; 4 | import { NextPage } from "next"; 5 | 6 | const IndexPage: NextPage = () => { 7 | return ( 8 | <> 9 | 10 | Index Page 11 | 12 | 13 | 14 | ); 15 | }; 16 | 17 | export default IndexPage; 18 | -------------------------------------------------------------------------------- /frontend/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "build", 6 | "declaration": true, 7 | "jsx": "react", 8 | "target": "es5", 9 | "module": "commonjs", 10 | "moduleResolution": "node" 11 | }, 12 | "exclude": [ 13 | "build", 14 | "node_modules", 15 | "coverage", 16 | "config", 17 | "src/**/*.test.ts", 18 | "src/**/*.test.tsx", 19 | "src/**/*.story.tsx" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /backend/metadata/tables.yaml: -------------------------------------------------------------------------------- 1 | - table: 2 | schema: public 3 | name: accounts 4 | - table: 5 | schema: public 6 | name: boards 7 | - table: 8 | schema: public 9 | name: boards_users 10 | - table: 11 | schema: public 12 | name: cards 13 | - table: 14 | schema: public 15 | name: lists 16 | - table: 17 | schema: public 18 | name: sessions 19 | - table: 20 | schema: public 21 | name: users 22 | - table: 23 | schema: public 24 | name: verification_requests 25 | -------------------------------------------------------------------------------- /frontend/components/layout/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import { ColorModeProvider, LightMode } from "@chakra-ui/core"; 3 | import Container from "components/layout/container"; 4 | import Navbar from "components/navbar"; 5 | 6 | const Layout: FC = ({ children }) => { 7 | return ( 8 | 9 | 10 | 11 | {children} 12 | 13 | 14 | ); 15 | }; 16 | 17 | export default Layout; 18 | -------------------------------------------------------------------------------- /frontend/components/loader/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import { Box, CircularProgress } from "@chakra-ui/core"; 3 | 4 | interface Props { 5 | size?: string; 6 | thickness?: number; 7 | } 8 | 9 | const Loader: FC = ({ size = "50px", thickness = 0.15 }) => { 10 | return ( 11 | 12 | 18 | 19 | ); 20 | }; 21 | 22 | export default Loader; 23 | -------------------------------------------------------------------------------- /frontend/tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowUnreachableCode": false, 4 | "noFallthroughCasesInSwitch": true, 5 | "allowSyntheticDefaultImports": true, 6 | "downlevelIteration": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "noImplicitAny": true, 9 | "noImplicitReturns": true, 10 | "noImplicitThis": true, 11 | "noUnusedLocals": true, 12 | "sourceMap": true, 13 | "strictNullChecks": true, 14 | "strict": true, 15 | "pretty": true, 16 | "jsx": "react", 17 | "suppressImplicitAnyIndexErrors": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /frontend/components/layout/container.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import { Box, useColorMode } from "@chakra-ui/core"; 3 | 4 | const Container: FC = ({ children }) => { 5 | const { colorMode } = useColorMode(); 6 | const bgColor = { light: "gray.100", dark: "gray.900" }; 7 | const heightOfNavbar: string = "74px"; 8 | 9 | return ( 10 | 16 | 17 | {children} 18 | 19 | 20 | ); 21 | }; 22 | 23 | export default Container; 24 | -------------------------------------------------------------------------------- /backend/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | backend: 5 | container_name: nextjs-hasura-trello-clone-backend 6 | image: nextjs-hasura-trello-clone-backend 7 | build: 8 | context: . 9 | depends_on: 10 | - db 11 | env_file: ./.env 12 | ports: 13 | - "8080:8080" 14 | volumes: 15 | - .:/usr/src/app 16 | restart: on-failure 17 | 18 | db: 19 | container_name: nextjs-hasura-trello-clone-db 20 | image: postgres:11.3-alpine 21 | ports: 22 | - "5432:5432" 23 | volumes: 24 | - db_data:/var/lib/postgresql/data 25 | restart: unless-stopped 26 | 27 | volumes: 28 | db_data: 29 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "node", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "baseUrl": ".", 15 | "paths": { 16 | "*": ["/*"], 17 | "components/*": ["components/*"], 18 | "lib/*": ["lib/*"], 19 | "pages/*": ["pages/*"], 20 | "types/*": ["types/*"], 21 | "configs/*": ["configs/*"] 22 | }, 23 | "strict": false, 24 | "forceConsistentCasingInFileNames": true 25 | }, 26 | "exclude": ["node_modules"], 27 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"] 28 | } 29 | -------------------------------------------------------------------------------- /frontend/pages/cards/[cardId]/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Head from "next/head"; 3 | import Page from "components/pages/cards/show"; 4 | import { NextPage } from "next"; 5 | import Loader from "components/loader"; 6 | import AccessDeniedIndicator from "components/access-denied-indicator"; 7 | import { useSession } from "next-auth/client"; 8 | import WithGraphQL from "lib/with-graphql"; 9 | 10 | const CardsShowPage: NextPage = () => { 11 | const [session, loading] = useSession(); 12 | 13 | if (loading) { 14 | return ; 15 | } 16 | 17 | if (!session) { 18 | return ; 19 | } 20 | 21 | return ( 22 | 23 | 24 | Cards Page 25 | 26 | 27 | 28 | ); 29 | }; 30 | 31 | export default CardsShowPage; 32 | -------------------------------------------------------------------------------- /frontend/pages/boards/[boardId]/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Head from "next/head"; 3 | import Page from "components/pages/boards/show"; 4 | import { NextPage } from "next"; 5 | import Loader from "components/loader"; 6 | import AccessDeniedIndicator from "components/access-denied-indicator"; 7 | import { useSession } from "next-auth/client"; 8 | import WithGraphQL from "lib/with-graphql"; 9 | 10 | const BoardsShowPage: NextPage = () => { 11 | const [session, loading] = useSession(); 12 | 13 | if (loading) { 14 | return ; 15 | } 16 | 17 | if (!session) { 18 | return ; 19 | } 20 | 21 | return ( 22 | 23 | 24 | Boards Page 25 | 26 | 27 | 28 | ); 29 | }; 30 | 31 | export default BoardsShowPage; 32 | -------------------------------------------------------------------------------- /frontend/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { AppProps } from "next/app"; 3 | import Head from "next/head"; 4 | import { Provider as NextAuthProvider } from "next-auth/client"; 5 | import { ThemeProvider, CSSReset, theme } from "@chakra-ui/core"; 6 | import Layout from "components/layout"; 7 | 8 | const App = ({ Component, pageProps }: AppProps) => { 9 | const { session } = pageProps; 10 | 11 | return ( 12 | <> 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | }; 27 | 28 | export default App; 29 | -------------------------------------------------------------------------------- /frontend/components/pages/cards/show/settings.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import { Box, Button, Link as _Link, Stack, Heading } from "@chakra-ui/core"; 3 | 4 | const Settings: FC = () => { 5 | return ( 6 | 7 | 8 | 9 | Settings 10 | 11 | 12 | 13 | 21 | 22 | 23 | 31 | 32 | 33 | ); 34 | }; 35 | 36 | export default Settings; 37 | -------------------------------------------------------------------------------- /backend/migrations/1595435360739_create_table_public_boards/up.sql: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION IF NOT EXISTS pgcrypto; 2 | CREATE TABLE "public"."boards"("id" uuid NOT NULL DEFAULT gen_random_uuid(), "name" varchar NOT NULL, "user_id" uuid NOT NULL, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id") , UNIQUE ("id")); 3 | CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"() 4 | RETURNS TRIGGER AS $$ 5 | DECLARE 6 | _new record; 7 | BEGIN 8 | _new := NEW; 9 | _new."updated_at" = NOW(); 10 | RETURN _new; 11 | END; 12 | $$ LANGUAGE plpgsql; 13 | CREATE TRIGGER "set_public_boards_updated_at" 14 | BEFORE UPDATE ON "public"."boards" 15 | FOR EACH ROW 16 | EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"(); 17 | COMMENT ON TRIGGER "set_public_boards_updated_at" ON "public"."boards" 18 | IS 'trigger to set value of column "updated_at" to current timestamp on row update'; 19 | -------------------------------------------------------------------------------- /frontend/pages/boards/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Head from "next/head"; 3 | import Page from "components/pages/boards/index"; 4 | import { Box } from "@chakra-ui/core"; 5 | import { NextPage } from "next"; 6 | import Loader from "components/loader"; 7 | import AccessDeniedIndicator from "components/access-denied-indicator"; 8 | import { useSession } from "next-auth/client"; 9 | import WithGraphQL from "lib/with-graphql"; 10 | 11 | const BoardsIndexPage: NextPage = () => { 12 | const [session, loading] = useSession(); 13 | 14 | if (loading) { 15 | return ; 16 | } 17 | 18 | if (!session) { 19 | return ; 20 | } 21 | 22 | return ( 23 | 24 | 25 | Boards Page 26 | 27 | 28 | 29 | 30 | 31 | ); 32 | }; 33 | 34 | export default BoardsIndexPage; 35 | -------------------------------------------------------------------------------- /backend/migrations/1595435581334_create_table_public_lists/up.sql: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION IF NOT EXISTS pgcrypto; 2 | CREATE TABLE "public"."lists"("id" uuid NOT NULL DEFAULT gen_random_uuid(), "name" varchar NOT NULL, "position" numeric NOT NULL, "board_id" uuid NOT NULL, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id") , UNIQUE ("id")); 3 | CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"() 4 | RETURNS TRIGGER AS $$ 5 | DECLARE 6 | _new record; 7 | BEGIN 8 | _new := NEW; 9 | _new."updated_at" = NOW(); 10 | RETURN _new; 11 | END; 12 | $$ LANGUAGE plpgsql; 13 | CREATE TRIGGER "set_public_lists_updated_at" 14 | BEFORE UPDATE ON "public"."lists" 15 | FOR EACH ROW 16 | EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"(); 17 | COMMENT ON TRIGGER "set_public_lists_updated_at" ON "public"."lists" 18 | IS 'trigger to set value of column "updated_at" to current timestamp on row update'; 19 | -------------------------------------------------------------------------------- /backend/migrations/1594912494982_create_table_public_users/up.sql: -------------------------------------------------------------------------------- 1 | 2 | CREATE EXTENSION IF NOT EXISTS pgcrypto; 3 | CREATE TABLE "public"."users"("id" uuid NOT NULL DEFAULT gen_random_uuid(), "name" varchar, "email" varchar NOT NULL, "email_verified" timestamptz, "image" bpchar, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id") , UNIQUE ("id")); 4 | CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"() 5 | RETURNS TRIGGER AS $$ 6 | DECLARE 7 | _new record; 8 | BEGIN 9 | _new := NEW; 10 | _new."updated_at" = NOW(); 11 | RETURN _new; 12 | END; 13 | $$ LANGUAGE plpgsql; 14 | CREATE TRIGGER "set_public_users_updated_at" 15 | BEFORE UPDATE ON "public"."users" 16 | FOR EACH ROW 17 | EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"(); 18 | COMMENT ON TRIGGER "set_public_users_updated_at" ON "public"."users" 19 | IS 'trigger to set value of column "updated_at" to current timestamp on row update'; 20 | -------------------------------------------------------------------------------- /backend/migrations/1595435754795_create_table_public_boards_users/up.sql: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION IF NOT EXISTS pgcrypto; 2 | CREATE TABLE "public"."boards_users"("id" uuid NOT NULL DEFAULT gen_random_uuid(), "board_id" uuid NOT NULL, "user_id" uuid NOT NULL, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id") , UNIQUE ("id")); 3 | CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"() 4 | RETURNS TRIGGER AS $$ 5 | DECLARE 6 | _new record; 7 | BEGIN 8 | _new := NEW; 9 | _new."updated_at" = NOW(); 10 | RETURN _new; 11 | END; 12 | $$ LANGUAGE plpgsql; 13 | CREATE TRIGGER "set_public_boards_users_updated_at" 14 | BEFORE UPDATE ON "public"."boards_users" 15 | FOR EACH ROW 16 | EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"(); 17 | COMMENT ON TRIGGER "set_public_boards_users_updated_at" ON "public"."boards_users" 18 | IS 'trigger to set value of column "updated_at" to current timestamp on row update'; 19 | -------------------------------------------------------------------------------- /backend/migrations/1594912673190_create_table_public_sessions/up.sql: -------------------------------------------------------------------------------- 1 | 2 | CREATE EXTENSION IF NOT EXISTS pgcrypto; 3 | CREATE TABLE "public"."sessions"("id" uuid NOT NULL DEFAULT gen_random_uuid(), "user_id" uuid NOT NULL, "expires" timestamptz NOT NULL, "session_token" varchar NOT NULL, "access_token" varchar NOT NULL, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id") ); 4 | CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"() 5 | RETURNS TRIGGER AS $$ 6 | DECLARE 7 | _new record; 8 | BEGIN 9 | _new := NEW; 10 | _new."updated_at" = NOW(); 11 | RETURN _new; 12 | END; 13 | $$ LANGUAGE plpgsql; 14 | CREATE TRIGGER "set_public_sessions_updated_at" 15 | BEFORE UPDATE ON "public"."sessions" 16 | FOR EACH ROW 17 | EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"(); 18 | COMMENT ON TRIGGER "set_public_sessions_updated_at" ON "public"."sessions" 19 | IS 'trigger to set value of column "updated_at" to current timestamp on row update'; -------------------------------------------------------------------------------- /backend/migrations/1595435462203_create_table_public_cards/up.sql: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION IF NOT EXISTS pgcrypto; 2 | CREATE TABLE "public"."cards"("id" uuid NOT NULL DEFAULT gen_random_uuid(), "description" varchar, "list_id" uuid NOT NULL, "board_id" uuid NOT NULL, "position" numeric NOT NULL, "title" varchar NOT NULL, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id") , UNIQUE ("id")); 3 | CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"() 4 | RETURNS TRIGGER AS $$ 5 | DECLARE 6 | _new record; 7 | BEGIN 8 | _new := NEW; 9 | _new."updated_at" = NOW(); 10 | RETURN _new; 11 | END; 12 | $$ LANGUAGE plpgsql; 13 | CREATE TRIGGER "set_public_cards_updated_at" 14 | BEFORE UPDATE ON "public"."cards" 15 | FOR EACH ROW 16 | EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"(); 17 | COMMENT ON TRIGGER "set_public_cards_updated_at" ON "public"."cards" 18 | IS 'trigger to set value of column "updated_at" to current timestamp on row update'; 19 | -------------------------------------------------------------------------------- /backend/migrations/1594912735062_create_table_public_verification_requests/up.sql: -------------------------------------------------------------------------------- 1 | 2 | CREATE EXTENSION IF NOT EXISTS pgcrypto; 3 | CREATE TABLE "public"."verification_requests"("id" uuid NOT NULL DEFAULT gen_random_uuid(), "identifier" varchar NOT NULL, "token" varchar NOT NULL, "expires" timestamptz NOT NULL, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id") , UNIQUE ("id")); 4 | CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"() 5 | RETURNS TRIGGER AS $$ 6 | DECLARE 7 | _new record; 8 | BEGIN 9 | _new := NEW; 10 | _new."updated_at" = NOW(); 11 | RETURN _new; 12 | END; 13 | $$ LANGUAGE plpgsql; 14 | CREATE TRIGGER "set_public_verification_requests_updated_at" 15 | BEFORE UPDATE ON "public"."verification_requests" 16 | FOR EACH ROW 17 | EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"(); 18 | COMMENT ON TRIGGER "set_public_verification_requests_updated_at" ON "public"."verification_requests" 19 | IS 'trigger to set value of column "updated_at" to current timestamp on row update'; -------------------------------------------------------------------------------- /frontend/components/access-denied-indicator/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import { Icon, Flex, Button, Stack, Box } from "@chakra-ui/core"; 3 | import Link from "next/link"; 4 | import { signIn } from "next-auth/client"; 5 | 6 | const AccessDeniedIndicator: FC = () => { 7 | const iconNode = () => { 8 | return ; 9 | }; 10 | 11 | const signInButtonNode = () => { 12 | return ( 13 | 14 | 23 | 24 | ); 25 | }; 26 | 27 | return ( 28 | 29 | 30 | {iconNode()} 31 | {signInButtonNode()} 32 | 33 | 34 | ); 35 | }; 36 | 37 | export default AccessDeniedIndicator; 38 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Nirmalya Ghosh 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 | -------------------------------------------------------------------------------- /backend/migrations/1594912624761_create_table_public_accounts/up.sql: -------------------------------------------------------------------------------- 1 | 2 | CREATE EXTENSION IF NOT EXISTS pgcrypto; 3 | CREATE TABLE "public"."accounts"("id" uuid NOT NULL DEFAULT gen_random_uuid(), "compound_id" varchar NOT NULL, "user_id" uuid NOT NULL, "provider_type" varchar NOT NULL, "provider_id" varchar NOT NULL, "provider_account_id" varchar 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") ); 4 | CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"() 5 | RETURNS TRIGGER AS $$ 6 | DECLARE 7 | _new record; 8 | BEGIN 9 | _new := NEW; 10 | _new."updated_at" = NOW(); 11 | RETURN _new; 12 | END; 13 | $$ LANGUAGE plpgsql; 14 | CREATE TRIGGER "set_public_accounts_updated_at" 15 | BEFORE UPDATE ON "public"."accounts" 16 | FOR EACH ROW 17 | EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"(); 18 | COMMENT ON TRIGGER "set_public_accounts_updated_at" ON "public"."accounts" 19 | IS 'trigger to set value of column "updated_at" to current timestamp on row update'; -------------------------------------------------------------------------------- /frontend/lib/with-graphql.tsx: -------------------------------------------------------------------------------- 1 | import fetch from "isomorphic-unfetch"; 2 | import { Client, defaultExchanges, subscriptionExchange, Provider } from "urql"; 3 | import { SubscriptionClient } from "subscriptions-transport-ws"; 4 | import ws from "isomorphic-ws"; 5 | 6 | const subscriptionClient = new SubscriptionClient( 7 | process.env.WS_URL || "ws://localhost:8080/v1/graphql", 8 | { 9 | reconnect: true, 10 | connectionParams: { 11 | headers: { "X-Hasura-Admin-Secret": "secret" }, 12 | }, 13 | }, 14 | ws 15 | ); 16 | 17 | const client = new Client({ 18 | url: process.env.API_URL || "http://localhost:8080/v1/graphql", 19 | fetch, 20 | fetchOptions: { 21 | headers: { "X-Hasura-Admin-Secret": "secret" }, 22 | }, 23 | requestPolicy: "cache-and-network", 24 | exchanges: [ 25 | ...defaultExchanges, 26 | subscriptionExchange({ 27 | forwardSubscription(operation) { 28 | return subscriptionClient.request(operation); 29 | }, 30 | }), 31 | ], 32 | }); 33 | 34 | const WithGraphQL = ({ children }: any) => ( 35 | {children} 36 | ); 37 | 38 | export default WithGraphQL; 39 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-hasura-trello-clone-frontend", 3 | "version": "0.0.1", 4 | "author": "nirmalya.email@gmail.com", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "next dev", 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 | "type-check": "tsc" 13 | }, 14 | "dependencies": { 15 | "@chakra-ui/core": "^0.8.0", 16 | "@emotion/core": "^10.0.28", 17 | "@emotion/styled": "^10.0.27", 18 | "emotion-theming": "^10.0.27", 19 | "graphql": "^15.3.0", 20 | "graphql-tag": "^2.10.4", 21 | "isomorphic-unfetch": "^3.0.0", 22 | "isomorphic-ws": "^4.0.1", 23 | "jsonwebtoken": "^8.5.1", 24 | "next": "^9.4.4", 25 | "next-auth": "^3.29.10", 26 | "next-urql": "^1.0.2", 27 | "pg": "^8.3.0", 28 | "react": "^16.13.1", 29 | "react-beautiful-dnd": "^13.0.0", 30 | "react-dom": "^16.13.1", 31 | "react-is": "^16.13.1", 32 | "react-scrollbars-custom": "^4.0.25", 33 | "subscriptions-transport-ws": "^0.9.17", 34 | "urql": "^1.9.8", 35 | "ws": "^7.4.6" 36 | }, 37 | "devDependencies": { 38 | "@types/jsonwebtoken": "^8.5.0", 39 | "@types/lodash": "^4.14.158", 40 | "@types/next": "^9.0.0", 41 | "@types/node": "^14.0.23", 42 | "@types/pg": "^7.14.4", 43 | "@types/react": "^16.9.43", 44 | "@types/react-beautiful-dnd": "^13.0.0", 45 | "@types/react-dom": "^16.9.8", 46 | "husky": "^4.2.5", 47 | "lint-staged": "^10.2.11", 48 | "prettier": "^2.0.5", 49 | "tslint": "^6.1.2", 50 | "tslint-react": "^5.0.0", 51 | "typescript": "^3.9.7" 52 | }, 53 | "husky": { 54 | "hooks": { 55 | "pre-commit": "lint-staged" 56 | } 57 | }, 58 | "lint-staged": { 59 | "*.{js,json,css,md,tsx,ts}": [ 60 | "prettier --write" 61 | ] 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /frontend/components/pages/boards/show/card.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Draggable } from "react-beautiful-dnd"; 3 | import Link from "next/link"; 4 | import { PseudoBox, useColorMode, Box, Heading, Stack } from "@chakra-ui/core"; 5 | 6 | const _Card = ({ cards }: { cards: any }) => { 7 | const { colorMode } = useColorMode(); 8 | const bgColor = { light: "gray.100", dark: "gray.900" }; 9 | const borderColor = { light: "gray.300", dark: "gray.700" }; 10 | const color = { light: "gray.900", dark: "gray.100" }; 11 | 12 | return ( 13 | 14 | {cards.map((card: any, index: number) => { 15 | return ( 16 | 17 | 22 | 23 | 24 | {(provided, snapshot) => ( 25 | 32 | 41 | 42 | 43 | 44 | {card.title} 45 | 46 | 47 | 48 | 49 | 50 | )} 51 | 52 | 53 | 54 | 55 | ); 56 | })} 57 | 58 | ); 59 | }; 60 | 61 | export default _Card; 62 | -------------------------------------------------------------------------------- /frontend/pages/api/auth/[...nextauth].ts: -------------------------------------------------------------------------------- 1 | import NextAuth from "next-auth"; 2 | import Providers from "next-auth/providers"; 3 | import jwt from "jsonwebtoken"; 4 | import { NextApiRequest, NextApiResponse } from "next"; 5 | 6 | interface iToken { 7 | id: string; 8 | email: string; 9 | name?: string; 10 | picture?: string; 11 | } 12 | 13 | const options = { 14 | providers: [ 15 | Providers.Email({ 16 | server: process.env.EMAIL_SERVER, 17 | from: process.env.EMAIL_FROM, 18 | }), 19 | ], 20 | database: process.env.DATABASE_URL, 21 | session: { 22 | jwt: true, 23 | }, 24 | jwt: { 25 | encode: async ({ token, secret }: { token: iToken; secret: string }) => { 26 | const tokenContents = { 27 | id: token.id, 28 | name: token.name, 29 | email: token.email, 30 | picture: token.picture, 31 | "https://hasura.io/jwt/claims": { 32 | "x-hasura-allowed-roles": ["admin", "user"], 33 | "x-hasura-default-role": "user", 34 | "x-hasura-user-id": token.id, 35 | }, 36 | iat: Date.now() / 1000, 37 | exp: Math.floor(Date.now() / 1000) + 24 * 60 * 60, 38 | sub: token.id, 39 | }; 40 | 41 | const signOptions = { 42 | algorithm: "RS256", 43 | }; 44 | 45 | const encodedToken = jwt.sign( 46 | tokenContents, 47 | process.env.AUTH_PRIVATE_KEY || secret, 48 | // @ts-ignore 49 | signOptions 50 | ); 51 | 52 | return encodedToken; 53 | }, 54 | decode: async ({ token, secret }: { token: string; secret: string }) => { 55 | const signOptions = { 56 | algorithms: ["RS256"], 57 | }; 58 | 59 | const decodedToken = jwt.verify( 60 | token, 61 | process.env.AUTH_PRIVATE_KEY || secret, 62 | // @ts-ignore 63 | signOptions 64 | ); 65 | 66 | return decodedToken; 67 | }, 68 | }, 69 | debug: true, 70 | callbacks: { 71 | session: async (session, user) => { 72 | session.id = user.id; 73 | 74 | return Promise.resolve(session); 75 | }, 76 | jwt: async (token, user) => { 77 | const isSignIn = user ? true : false; 78 | 79 | if (isSignIn) { 80 | token.id = user.id; 81 | } 82 | 83 | return Promise.resolve(token); 84 | }, 85 | }, 86 | }; 87 | 88 | const Auth = (req: NextApiRequest, res: NextApiResponse) => 89 | NextAuth(req, res, options); 90 | 91 | export default Auth; 92 | -------------------------------------------------------------------------------- /frontend/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint-react"], 3 | "rules": { 4 | "align": [true, "parameters", "arguments", "statements"], 5 | "ban": false, 6 | "class-name": true, 7 | "comment-format": [true, "check-space"], 8 | "curly": true, 9 | "eofline": false, 10 | "forin": true, 11 | "indent": [true, "spaces"], 12 | "interface-name": [true, "never-prefix"], 13 | "jsdoc-format": true, 14 | "jsx-no-lambda": false, 15 | "jsx-no-multiline-js": false, 16 | "jsx-wrap-multiline": false, 17 | "label-position": true, 18 | "max-line-length": [true, 300], 19 | "member-ordering": [ 20 | true, 21 | "public-before-private", 22 | "static-before-instance", 23 | "variables-before-functions" 24 | ], 25 | "no-any": false, 26 | "no-arg": true, 27 | "no-bitwise": true, 28 | "no-console": [false], 29 | "no-consecutive-blank-lines": true, 30 | "no-construct": true, 31 | "no-debugger": true, 32 | "no-duplicate-variable": true, 33 | "no-empty": true, 34 | "no-eval": true, 35 | "no-shadowed-variable": true, 36 | "no-string-literal": true, 37 | "no-switch-case-fall-through": true, 38 | "no-trailing-whitespace": false, 39 | "no-unused-expression": true, 40 | "no-use-before-declare": true, 41 | "one-line": [ 42 | true, 43 | "check-catch", 44 | "check-else", 45 | "check-open-brace", 46 | "check-whitespace" 47 | ], 48 | "quotemark": [true, "single", "jsx-double"], 49 | "radix": true, 50 | "semicolon": [true, "always", "ignore-bound-class-methods"], 51 | "switch-default": true, 52 | "trailing-comma": false, 53 | "triple-equals": [true, "allow-null-check"], 54 | "typedef": [true, "parameter", "property-declaration"], 55 | "typedef-whitespace": [ 56 | true, 57 | { 58 | "call-signature": "nospace", 59 | "index-signature": "nospace", 60 | "parameter": "nospace", 61 | "property-declaration": "nospace", 62 | "variable-declaration": "nospace" 63 | } 64 | ], 65 | "variable-name": [ 66 | true, 67 | "ban-keywords", 68 | "check-format", 69 | "allow-leading-underscore", 70 | "allow-pascal-case" 71 | ], 72 | "whitespace": [ 73 | true, 74 | "check-branch", 75 | "check-decl", 76 | "check-module", 77 | "check-operator", 78 | "check-separator", 79 | "check-type" 80 | ] 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /frontend/components/pages/cards/show/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import { 3 | Box, 4 | Link as _Link, 5 | Drawer, 6 | DrawerBody, 7 | DrawerHeader, 8 | DrawerOverlay, 9 | DrawerContent, 10 | DrawerCloseButton, 11 | useColorMode, 12 | Grid, 13 | } from "@chakra-ui/core"; 14 | import gql from "graphql-tag"; 15 | import { useSubscription } from "urql"; 16 | import Loader from "components/loader"; 17 | import { useRouter } from "next/router"; 18 | import Board from "components/pages/boards/show"; 19 | import DetailsForm from "components/pages/cards/show/details-form"; 20 | import Settings from "components/pages/cards/show/settings"; 21 | 22 | const FETCH_CARD_SUBSCRIPTION = gql` 23 | subscription fetchCard($id: uuid!) { 24 | cards_by_pk(id: $id) { 25 | id 26 | title 27 | description 28 | board_id 29 | } 30 | } 31 | `; 32 | 33 | const Card: FC = () => { 34 | const { colorMode } = useColorMode(); 35 | const bgColor = { light: "white", dark: "gray.800" }; 36 | const color = { light: "gray.900", dark: "gray.100" }; 37 | const router = useRouter(); 38 | const currentCardId = router.query.cardId; 39 | 40 | const [{ data: fetchCardData, error: fetchCardError }] = useSubscription({ 41 | query: FETCH_CARD_SUBSCRIPTION, 42 | variables: { id: currentCardId }, 43 | }); 44 | 45 | if (!fetchCardData) { 46 | return ; 47 | } 48 | 49 | if (fetchCardError) { 50 | return

Error: {fetchCardError.message}

; 51 | } 52 | 53 | const { board_id } = fetchCardData.cards_by_pk; 54 | 55 | return ( 56 | <> 57 | 62 | router.push( 63 | `/boards/[boardId]?boardId=${board_id}`, 64 | `/boards/${board_id}` 65 | ) 66 | } 67 | > 68 | 69 | 74 | 75 | Update Card 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | ); 91 | }; 92 | 93 | export default Card; 94 | -------------------------------------------------------------------------------- /frontend/components/pages/boards/show/invite-users.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Button, 4 | useColorMode, 5 | Menu, 6 | MenuButton, 7 | MenuList, 8 | MenuOptionGroup, 9 | MenuItemOption, 10 | Icon, 11 | } from "@chakra-ui/core"; 12 | import gql from "graphql-tag"; 13 | import { useSubscription, useMutation } from "urql"; 14 | import xor from "lodash/xor"; 15 | import Loader from "components/loader"; 16 | 17 | interface user { 18 | id: number; 19 | email: string; 20 | } 21 | 22 | const FETCH_USERS_SUBSCRIPTION = gql` 23 | subscription fetchUsers { 24 | users { 25 | id 26 | email 27 | name 28 | } 29 | } 30 | `; 31 | 32 | const ADD_USER_MUTATION = gql` 33 | mutation addUser($board_id: uuid!, $user_id: uuid!) { 34 | insert_boards_users(objects: { user_id: $user_id, board_id: $board_id }) { 35 | returning { 36 | board_id 37 | user_id 38 | } 39 | } 40 | } 41 | `; 42 | 43 | const InviteUsersButton = ({ 44 | boardId, 45 | users, 46 | }: { 47 | boardId: string | string[] | undefined; 48 | users: string[]; 49 | }) => { 50 | const { colorMode } = useColorMode(); 51 | const color = { light: "gray.900", dark: "gray.100" }; 52 | const borderColor = { light: "gray.300", dark: "gray.700" }; 53 | const [{ data }] = useSubscription({ 54 | query: FETCH_USERS_SUBSCRIPTION, 55 | }); 56 | const [{ fetching: mutationFetching }, addUserMutation] = useMutation( 57 | ADD_USER_MUTATION 58 | ); 59 | 60 | if (!data) { 61 | return ; 62 | } 63 | 64 | const handleSubmit = (values: any) => { 65 | const usersToBeInvited: any[] = xor(values, selectedUserIds); 66 | 67 | usersToBeInvited.map(async (id: number) => { 68 | await addUserMutation({ 69 | user_id: id, 70 | board_id: boardId, 71 | }); 72 | }); 73 | }; 74 | 75 | if (mutationFetching) { 76 | return ( 77 | 80 | ); 81 | } 82 | 83 | const selectedUserIds = users.map((user: any) => user.user_id); 84 | 85 | return ( 86 | <> 87 | 88 | 93 | Invite 94 | 95 | 104 | { 108 | handleSubmit(value); 109 | }} 110 | > 111 | {data.users.map((user: user) => { 112 | return ( 113 | 118 | {user.email} 119 | 120 | ); 121 | })} 122 | 123 | 124 | 125 | 126 | ); 127 | }; 128 | 129 | export default InviteUsersButton; 130 | -------------------------------------------------------------------------------- /frontend/components/navbar/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { ChangeEvent } from "react"; 2 | import { NextComponentType } from "next"; 3 | import Link from "next/link"; 4 | import { signIn, signOut, useSession } from "next-auth/client"; 5 | import { 6 | Box, 7 | Stack, 8 | Link as _Link, 9 | Button, 10 | useColorMode, 11 | Menu, 12 | MenuButton, 13 | Icon, 14 | MenuList, 15 | MenuGroup, 16 | MenuItem, 17 | Switch, 18 | MenuDivider, 19 | } from "@chakra-ui/core"; 20 | 21 | const Navbar: NextComponentType = () => { 22 | const [session] = useSession(); 23 | const { colorMode, toggleColorMode } = useColorMode(); 24 | const bgColor = { light: "white", dark: "gray.800" }; 25 | const borderColor = { light: "gray.300", dark: "gray.700" }; 26 | const color = { light: "gray.800", dark: "gray.100" }; 27 | 28 | const handleToggleTheme = async (e: ChangeEvent) => { 29 | const theme: string = !!e.target.checked ? "dark" : "light"; 30 | 31 | console.log(theme); 32 | 33 | toggleColorMode(); 34 | }; 35 | 36 | const profileDropDown = () => { 37 | if (!session) { 38 | return false; 39 | } 40 | 41 | return ( 42 | 43 | 44 | 49 | Profile 50 | 51 | 56 | 57 | 58 | 59 | <_Link>My Account 60 | 61 | 62 | 63 | 64 | Dark Theme 65 | 66 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | Docs 77 | FAQ 78 | 79 | 80 | 81 | 82 | ); 83 | }; 84 | 85 | const signInButtonNode = () => { 86 | if (session) { 87 | return false; 88 | } 89 | 90 | return ( 91 | 92 | 93 | 102 | 103 | 104 | ); 105 | }; 106 | 107 | const signOutButtonNode = () => { 108 | if (!session) { 109 | return false; 110 | } 111 | 112 | return ( 113 | 114 | 115 | 124 | 125 | 126 | ); 127 | }; 128 | 129 | return ( 130 | 131 | 141 | 148 | 149 | 150 | 151 | 152 | <_Link>Home 153 | 154 | 155 | 156 | 157 | <_Link>Boards 158 | 159 | 160 | 161 | 162 | 163 | 164 | {profileDropDown()} 165 | {signInButtonNode()} 166 | {signOutButtonNode()} 167 | 168 | 169 | 170 | 171 | 172 | ); 173 | }; 174 | 175 | export default Navbar; 176 | -------------------------------------------------------------------------------- /frontend/components/pages/cards/show/details-form.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, FormEvent, useState, useEffect } from "react"; 2 | import { 3 | Box, 4 | Button, 5 | FormControl, 6 | FormLabel, 7 | Input, 8 | Link as _Link, 9 | Stack, 10 | Textarea, 11 | Heading, 12 | AlertIcon, 13 | Alert, 14 | } from "@chakra-ui/core"; 15 | import gql from "graphql-tag"; 16 | import { useQuery, useMutation } from "urql"; 17 | import { useRouter } from "next/router"; 18 | import Loader from "components/loader"; 19 | 20 | const FETCH_CARD_QUERY = gql` 21 | query fetchCard($id: uuid!) { 22 | cards_by_pk(id: $id) { 23 | id 24 | title 25 | description 26 | board_id 27 | } 28 | } 29 | `; 30 | 31 | const UPDATE_CARD_MUTATION = gql` 32 | mutation updateCard($id: uuid!, $description: String, $title: String) { 33 | update_cards( 34 | where: { id: { _eq: $id } } 35 | _set: { description: $description, title: $title } 36 | ) { 37 | returning { 38 | id 39 | title 40 | description 41 | } 42 | } 43 | } 44 | `; 45 | 46 | const DetailsForm: FC = () => { 47 | const [title, setTitle] = useState(""); 48 | const [description, setDescription] = useState(""); 49 | const router = useRouter(); 50 | const currentCardId = router.query.cardId; 51 | 52 | const [ 53 | { data: fetchCardData, fetching: fetchCardFetching, error: fetchCardError }, 54 | ] = useQuery({ 55 | query: FETCH_CARD_QUERY, 56 | variables: { id: currentCardId }, 57 | }); 58 | 59 | useEffect(() => { 60 | if (fetchCardData) { 61 | const { title, description } = fetchCardData.cards_by_pk; 62 | 63 | setTitle(title || ""); 64 | setDescription(description || ""); 65 | } 66 | }, [fetchCardData]); 67 | 68 | const [ 69 | { fetching: cardMutationFetching, error: cardMutationError }, 70 | updateCard, 71 | ] = useMutation(UPDATE_CARD_MUTATION); 72 | 73 | if (fetchCardFetching) { 74 | return ; 75 | } 76 | 77 | if (fetchCardError) { 78 | return

Error: {fetchCardError.message}

; 79 | } 80 | 81 | const { board_id: boardId } = fetchCardData.cards_by_pk; 82 | 83 | const handleSubmit = async () => { 84 | await updateCard({ 85 | id: currentCardId, 86 | title, 87 | description, 88 | }); 89 | 90 | if (!cardMutationError) { 91 | router.push(`/boards/[boardId]?boardId=${boardId}`, `/boards/${boardId}`); 92 | } 93 | }; 94 | 95 | return ( 96 | 97 | 98 | {cardMutationError ? ( 99 | 100 | 101 | There was an error processing your request. Please try again! 102 | 103 | ) : null} 104 | 105 | 106 | 107 | Details 108 | 109 | 110 | 111 | 112 | 113 | Title 114 | ) => 121 | setTitle(e.currentTarget.value) 122 | } 123 | /> 124 | 125 | 126 | Description 127 |