├── 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 |
19 | Members
20 |
21 |
22 |
23 |
29 | Due date
30 |
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 | {
17 | e.preventDefault();
18 | signIn();
19 | }}
20 | >
21 | Sign In
22 |
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 |
78 | Invite
79 |
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 | {
96 | e.preventDefault();
97 | signIn();
98 | }}
99 | >
100 | Sign In
101 |
102 |
103 |
104 | );
105 | };
106 |
107 | const signOutButtonNode = () => {
108 | if (!session) {
109 | return false;
110 | }
111 |
112 | return (
113 |
114 |
115 | {
118 | e.preventDefault();
119 | signOut();
120 | }}
121 | >
122 | Sign Out
123 |
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 |
139 |
140 |
141 |
142 |
143 |
144 |
148 | router.push(
149 | `/boards/[boardId]?boardId=${boardId}`,
150 | `/boards/${boardId}`
151 | )
152 | }
153 | >
154 | Cancel
155 |
156 |
157 |
158 |
165 | Post
166 |
167 |
168 |
169 |
170 |
171 | );
172 | };
173 |
174 | export default DetailsForm;
175 |
--------------------------------------------------------------------------------
/frontend/components/pages/boards/index/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, FormEvent } from "react";
2 | import {
3 | Box,
4 | PseudoBox,
5 | Link as _Link,
6 | Heading,
7 | Text,
8 | Stack,
9 | Button,
10 | useDisclosure,
11 | Drawer,
12 | DrawerBody,
13 | DrawerFooter,
14 | DrawerHeader,
15 | DrawerOverlay,
16 | DrawerContent,
17 | DrawerCloseButton,
18 | FormControl,
19 | FormLabel,
20 | Input,
21 | Alert,
22 | AlertIcon,
23 | useColorMode,
24 | } from "@chakra-ui/core";
25 | import { NextPage } from "next";
26 | import gql from "graphql-tag";
27 | import { useSubscription, useMutation } from "urql";
28 | import Link from "next/link";
29 | import Loader from "components/loader";
30 |
31 | const FETCH_BOARDS_SUBSCRIPTION = gql`
32 | subscription fetchBoards {
33 | boards(order_by: { created_at: desc }) {
34 | id
35 | name
36 | }
37 | }
38 | `;
39 |
40 | const CREATE_BOARD_MUTATION = gql`
41 | mutation createBoard($name: String!, $user_id: uuid!) {
42 | insert_boards(objects: { name: $name, user_id: $user_id }) {
43 | returning {
44 | id
45 | name
46 | }
47 | }
48 | }
49 | `;
50 |
51 | const Boards: NextPage = () => {
52 | const { colorMode } = useColorMode();
53 | const bgColor = { light: "white", dark: "gray.800" };
54 | const borderColor = { light: "gray.300", dark: "gray.700" };
55 | const color = { light: "gray.900", dark: "gray.100" };
56 | const { isOpen, onOpen, onClose } = useDisclosure();
57 | const [name, setName] = useState("");
58 | const currentUserId = "40989e49-4857-4429-80ad-839633adbe55";
59 | const [
60 | { fetching: mutationFetching, error: mutationError },
61 | createBoardMutation,
62 | ] = useMutation(CREATE_BOARD_MUTATION);
63 | const [{ data }] = useSubscription({
64 | query: FETCH_BOARDS_SUBSCRIPTION,
65 | });
66 |
67 | if (!data) {
68 | return ;
69 | }
70 |
71 | const handleSubmit = async (e: FormEvent) => {
72 | e.preventDefault();
73 |
74 | await createBoardMutation({
75 | user_id: currentUserId,
76 | name,
77 | });
78 |
79 | if (!mutationError) {
80 | onClose();
81 | setName("");
82 | }
83 | };
84 |
85 | const headingNode = () => {
86 | return (
87 |
88 |
89 |
90 | Boards
91 |
92 |
93 |
94 |
95 | ) =>
102 | console.log(e.currentTarget.value)
103 | }
104 | />
105 |
106 |
107 |
108 | Add new board
109 |
110 |
111 |
112 |
113 | );
114 | };
115 |
116 | const drawerNode = () => {
117 | return (
118 |
119 |
120 |
121 |
122 | Create Board
123 |
124 | {mutationError ? (
125 |
126 |
127 | There was an error processing your request. Please try again!
128 |
129 | ) : null}
130 |
131 | Name
132 | ) =>
139 | setName(e.currentTarget.value)
140 | }
141 | />
142 |
143 |
144 |
145 |
146 |
155 | Save
156 |
157 |
158 | Cancel
159 |
160 |
161 |
162 |
163 |
164 | );
165 | };
166 |
167 | return (
168 |
169 | {headingNode()}
170 | {drawerNode()}
171 |
172 | {data.boards.map((board: { id: number; name: string }) => {
173 | return (
174 |
175 |
179 |
180 |
189 |
190 | {board.name}
191 |
192 | {board.id}
193 |
194 |
195 |
196 |
197 | );
198 | })}
199 |
200 |
201 | );
202 | };
203 |
204 | export default Boards;
205 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | This is a clone of Trello application built using Hasura and Next.js. This application has been bootstrapped using [Hasura Next.js Boilerplate](https://github.com/ghoshnirmalya/nextjs-hasura-boilerplate) This mono-repo consists of the following packages:
12 |
13 | 1. [**frontend**](https://github.com/ghoshnirmalya/nextjs-hasura-trello-clone/tree/master/packages/frontend): Next.js application
14 | 2. [**backend**](https://github.com/ghoshnirmalya/nextjs-hasura-trello-clone/tree/master/packages/backend): Dockerized Hasura application
15 |
16 |
17 |
18 |
19 |
20 | - [Overview](#overview)
21 | - [Requirements](#requirements)
22 | - [Packages](#packages)
23 | - [1. **Frontend**: Next.js application](#1-frontend-nextjs-application)
24 | - [2. **Backend**: Dockerized Hasura application](#2-backend-dockerized-hasura-application)
25 | - [Installation](#installation)
26 | - [1. **Clone the application**](#1-clone-the-application)
27 | - [2. **Install Lerna globally**](#2-install-lerna-globally)
28 | - [3. **Bootstrap the packages**](#3-bootstrap-the-packages)
29 | - [4. **Start the packages**](#4-start-the-packages)
30 | - [5. **Go inside the directory of the backend package**](#5-go-inside-the-directory-of-the-backend-package)
31 | - [6. **Create a .env file and copy the contents from .env.example (present in packages/backend directory)**](#6-create-a-env-file-and-copy-the-contents-from-envexample-present-in-packagesbackend-directory)
32 | - [7. **Generate the RSA keys**](#7-generate-the-rsa-keys)
33 | - [8. **Print the keys in the escaped format**](#8-print-the-keys-in-the-escaped-format)
34 | - [9. **Copy the value of the key into the `HASURA_GRAPHQL_JWT_SECRET` key (in the .env file)**](#9-copy-the-value-of-the-key-into-the-hasura_graphql_jwt_secret-key-in-the-env-file)
35 | - [10. **Start docker-compose**](#10-start-docker-compose)
36 | - [Deployment](#deployment)
37 | - [Other interesting repositories](#other-interesting-repositories)
38 | - [License](#license)
39 |
40 |
41 |
42 | ## Overview
43 |
44 | This boilerplate is built using [Lerna](https://lerna.js.org/) for managing all the packages in a simple manner. Because of Lerna, it becomes very easy to install, develop and maintain a mono-repo structure.
45 |
46 |
47 |
48 |
49 |
50 | ## Requirements
51 |
52 | 1. [Node.js](https://nodejs.org/)
53 | 2. [npm](https://www.npmjs.com/)
54 | 3. [Lerna](https://lerna.js.org/)
55 | 4. [Docker](https://www.docker.com/)
56 |
57 | ## Packages
58 |
59 | ### 1. [**Frontend**](https://github.com/ghoshnirmalya/nextjs-hasura-trello-clone/tree/master/packages/frontend): Next.js application
60 |
61 | This application is the primary user-facing application. Once it’s up and running (see Development section), it’s available on http://localhost:3000/.
62 |
63 | 
64 |
65 | To create a new user, we’ll have to Sign Up using Google. [NextAuth](https://next-auth.js.org/) is being used to help us in authentication.
66 |
67 | 
68 |
69 | ### 2. [**Backend**](https://github.com/ghoshnirmalya/nextjs-hasura-trello-clone/tree/master/packages/backend): Dockerized Hasura application
70 |
71 | [Hasura](https://hasura.io/) is an open source engine that connects to our databases & micro-services and auto-generates a production-ready GraphQL backend. It’s very easy to get Hasura up and running on our local system. All the migrations are set up in the [migrations](https://github.com/ghoshnirmalya/nextjs-hasura-trello-clone/tree/master/packages/backend/migrations) directory.
72 |
73 | ## Installation
74 |
75 | ### 1. **Clone the application**
76 |
77 | ```sh
78 | git clone https://github.com/ghoshnirmalya/nextjs-hasura-trello-clone
79 | ```
80 |
81 | ### 2. **Install Lerna globally**
82 |
83 | ```sh
84 | npm install --global lerna
85 | ```
86 |
87 | ### 3. **Bootstrap the packages**
88 |
89 | From the project root, we can run the following command to bootstrap the packages and install all their dependencies and linking any cross-dependencies:
90 |
91 | ```sh
92 | lerna bootstrap
93 | ```
94 |
95 | ### 4. **Start the packages**
96 |
97 | From the project root, we can run the following command to start our Node.js packages:
98 |
99 | ```sh
100 | yarn start
101 | ```
102 |
103 | The above command will start the frontend package on [http://localhost:3000/](http://localhost:3000).
104 |
105 | The backend package doesn’t do anything after we execute the above command.
106 |
107 | ### 5. **Go inside the directory of the backend package**
108 |
109 | ```sh
110 | cd packages/backend
111 | ```
112 |
113 | ### 6. **Create a .env file and copy the contents from .env.example (present in packages/backend directory)**
114 |
115 | ### 7. **Generate the RSA keys**
116 |
117 | ```sh
118 | openssl genrsa -out private.pem 2048
119 | openssl rsa -in private.pem -pubout > public.pem
120 | ```
121 |
122 | ### 8. **Print the keys in the escaped format**
123 |
124 | ```sh
125 | awk -v ORS='\\n' '1' public.pem
126 | ```
127 |
128 | ### 9. **Copy the value of the key into the `HASURA_GRAPHQL_JWT_SECRET` key (in the .env file)**
129 |
130 | ### 10. **Start docker-compose**
131 |
132 | ```sh
133 | docker-compose up
134 | ```
135 |
136 | We need to start Docker and then run the above command which will change the current directory to the backend package’s directory and then start the backend package. If everything goes well, it’ll be up and running on http://localhost:8080/v1/graphql.
137 |
138 | ## Deployment
139 |
140 | We’re still working on this feature. The documentation will be updated soon.
141 |
142 | ## Other interesting repositories
143 |
144 | 1. [Hasura Next.js Boilerplate](https://github.com/ghoshnirmalya/nextjs-hasura-trello-clone)
145 | 2. [React Search Box](https://github.com/ghoshnirmalya/react-search-box)
146 | 3. [LinkedIn Clone using Create React App](https://github.com/ghoshnirmalya/linkedin-clone-react-frontend)
147 |
148 | ## License
149 |
150 | This project is licensed under the [MIT License](https://opensource.org/licenses/MIT).
151 |
--------------------------------------------------------------------------------
/frontend/public/images/bug_fixed.svg:
--------------------------------------------------------------------------------
1 | #29 bug fixed
--------------------------------------------------------------------------------
/frontend/components/pages/boards/show/list.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, FormEvent, KeyboardEvent } from "react";
2 | import { Droppable, Draggable } from "react-beautiful-dnd";
3 | import Scrollbar from "react-scrollbars-custom";
4 | import find from "lodash/find";
5 | import {
6 | Box,
7 | Stack,
8 | Heading,
9 | Badge,
10 | useDisclosure,
11 | Button,
12 | Link as _Link,
13 | Drawer,
14 | DrawerBody,
15 | DrawerFooter,
16 | DrawerHeader,
17 | DrawerOverlay,
18 | DrawerContent,
19 | DrawerCloseButton,
20 | FormControl,
21 | FormLabel,
22 | Input,
23 | Alert,
24 | AlertIcon,
25 | useColorMode,
26 | Textarea,
27 | } from "@chakra-ui/core";
28 | import Card from "components/pages/boards/show/card";
29 | import gql from "graphql-tag";
30 | import { useMutation } from "urql";
31 |
32 | const CREATE_CARD_MUTATION = gql`
33 | mutation createCard(
34 | $listId: uuid!
35 | $position: numeric
36 | $title: String
37 | $description: String
38 | $boardId: uuid!
39 | ) {
40 | insert_cards(
41 | objects: {
42 | list_id: $listId
43 | position: $position
44 | title: $title
45 | description: $description
46 | board_id: $boardId
47 | }
48 | ) {
49 | returning {
50 | id
51 | title
52 | description
53 | position
54 | }
55 | }
56 | }
57 | `;
58 |
59 | const CREATE_LIST_MUTATION = gql`
60 | mutation createList($boardId: uuid!, $position: numeric, $name: String) {
61 | insert_lists(
62 | objects: { board_id: $boardId, position: $position, name: $name }
63 | ) {
64 | returning {
65 | id
66 | name
67 | position
68 | }
69 | }
70 | }
71 | `;
72 |
73 | const List = ({
74 | lists,
75 | boardId,
76 | }: {
77 | lists: any;
78 | boardId: string | string[] | undefined;
79 | }) => {
80 | const { colorMode } = useColorMode();
81 | const bgColor = { light: "white", dark: "gray.800" };
82 | const borderColor = { light: "gray.300", dark: "gray.700" };
83 | const color = { light: "gray.900", dark: "gray.100" };
84 | const { isOpen, onOpen, onClose } = useDisclosure();
85 | const [description, setDescription] = useState("");
86 | const [title, setTitle] = useState("");
87 | const [name, setName] = useState("");
88 | const [listId, setListId] = useState("");
89 | const [
90 | { fetching: createCardMutationFetching, error: createCardMutationError },
91 | createCard,
92 | ] = useMutation(CREATE_CARD_MUTATION);
93 | const [
94 | { fetching: createListMutationFetching, error: createListMutationError },
95 | createList,
96 | ] = useMutation(CREATE_LIST_MUTATION);
97 |
98 | const getPositionOfNewCard = () => {
99 | const bufferForEachPosition = 1024;
100 | const list = find(lists, (l) => l.id === listId);
101 | const positionOfLastCard = list.cards.length
102 | ? list.cards[list.cards.length - 1].position
103 | : 0;
104 |
105 | return positionOfLastCard + bufferForEachPosition;
106 | };
107 |
108 | const handleSubmit = async (e: FormEvent) => {
109 | e.preventDefault();
110 |
111 | await createCard({
112 | listId,
113 | position: getPositionOfNewCard(),
114 | title,
115 | description,
116 | boardId,
117 | });
118 |
119 | if (!createCardMutationError) {
120 | onClose();
121 | setTitle("");
122 | setDescription("");
123 | setListId("");
124 | }
125 | };
126 |
127 | const handleAddNewList = async (e: KeyboardEvent) => {
128 | const getPositionOfNewList = () => {
129 | const bufferForEachPosition = 1024;
130 | let positionOfLastList = lists[lists.length - 1]
131 | ? lists[lists.length - 1].position
132 | : 1;
133 |
134 | return positionOfLastList + bufferForEachPosition;
135 | };
136 |
137 | if (e.key == "Enter") {
138 | e.preventDefault();
139 |
140 | await createList({
141 | boardId,
142 | position: getPositionOfNewList(),
143 | name,
144 | });
145 |
146 | if (!createListMutationError) {
147 | setName("");
148 | }
149 | }
150 | };
151 |
152 | const drawerNode = () => {
153 | return (
154 |
155 |
156 |
157 |
158 | Create card
159 |
160 | {createCardMutationError ? (
161 |
162 |
163 | There was an error processing your request. Please try again!
164 |
165 | ) : null}
166 |
167 |
168 | Title
169 | ) =>
176 | setTitle(e.currentTarget.value)
177 | }
178 | />
179 |
180 |
181 | Description
182 |
193 |
194 |
195 |
196 |
197 |
206 | Save
207 |
208 |
209 | Cancel
210 |
211 |
212 |
213 |
214 |
215 | );
216 | };
217 |
218 | return (
219 |
220 | {lists.map((list: any, index: number) => (
221 |
222 |
223 | {(provided) => (
224 |
229 |
237 |
244 |
245 | {list.name}
246 |
247 | {list.cards.length}
248 |
249 |
250 |
251 | {(provided, snapshot) => (
252 |
263 |
268 |
275 |
276 |
277 |
278 |
279 |
280 | {
283 | onOpen();
284 | setListId(list.id);
285 | }}
286 | py={2}
287 | w="full"
288 | >
289 | Add a new Card
290 |
291 |
292 |
293 |
294 |
295 | {provided.placeholder}
296 |
297 | )}
298 |
299 |
300 | )}
301 |
302 |
303 | ))}
304 | {drawerNode()}
305 |
306 |
312 | ) => {
316 | setName(e.currentTarget.value);
317 | }}
318 | onKeyDown={handleAddNewList}
319 | isDisabled={createListMutationFetching}
320 | />
321 |
322 |
323 |
324 | );
325 | };
326 |
327 | export default List;
328 |
--------------------------------------------------------------------------------
/frontend/components/pages/boards/show/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | Box,
4 | Heading,
5 | Grid,
6 | Stack,
7 | Link as _Link,
8 | useColorMode,
9 | Avatar,
10 | AvatarGroup,
11 | } from "@chakra-ui/core";
12 | import { NextPage } from "next";
13 | import gql from "graphql-tag";
14 | import { useSubscription, useMutation } from "urql";
15 | import Loader from "components/loader";
16 | import { useRouter } from "next/router";
17 | import { DragDropContext, Droppable } from "react-beautiful-dnd";
18 | import find from "lodash/find";
19 | import Scrollbar from "react-scrollbars-custom";
20 | import List from "components/pages/boards/show/list";
21 | import InviteUsers from "components/pages/boards/show/invite-users";
22 |
23 | const FETCH_BOARD_SUBSCRIPTION = gql`
24 | subscription fetchBoard($id: uuid!) {
25 | boards_by_pk(id: $id) {
26 | id
27 | name
28 | lists(order_by: { position: asc }) {
29 | id
30 | name
31 | position
32 | board_id
33 | cards(order_by: { position: asc }) {
34 | id
35 | title
36 | description
37 | position
38 | }
39 | }
40 | users {
41 | user_id
42 | user {
43 | email
44 | }
45 | }
46 | }
47 | }
48 | `;
49 |
50 | const UPDATE_CARD_MUTATION = gql`
51 | mutation updateCard($id: uuid!, $position: numeric) {
52 | update_cards(where: { id: { _eq: $id } }, _set: { position: $position }) {
53 | returning {
54 | id
55 | description
56 | position
57 | }
58 | }
59 | }
60 | `;
61 |
62 | const UPDATE_CARD_FOR_DIFFERENT_LISTS_MUTATION = gql`
63 | mutation updateCardForDifferentLists(
64 | $id: uuid!
65 | $position: numeric
66 | $listId: uuid!
67 | ) {
68 | update_cards(
69 | where: { id: { _eq: $id } }
70 | _set: { list_id: $listId, position: $position }
71 | ) {
72 | returning {
73 | id
74 | }
75 | }
76 | }
77 | `;
78 |
79 | const UPDATE_LIST_MUTATION = gql`
80 | mutation updateList($id: uuid!, $position: numeric) {
81 | update_lists(where: { id: { _eq: $id } }, _set: { position: $position }) {
82 | returning {
83 | id
84 | name
85 | position
86 | }
87 | }
88 | }
89 | `;
90 |
91 | const Board: NextPage<{ boardId?: string }> = ({ boardId }) => {
92 | const { colorMode } = useColorMode();
93 | const color = { light: "gray.900", dark: "gray.100" };
94 | const router = useRouter();
95 | const currentBoardId = boardId || router.query.boardId;
96 | const [{ data }] = useSubscription({
97 | query: FETCH_BOARD_SUBSCRIPTION,
98 | variables: { id: currentBoardId },
99 | });
100 | const [, updateCard] = useMutation(UPDATE_CARD_MUTATION);
101 | const [, updateCardForDifferentLists] = useMutation(
102 | UPDATE_CARD_FOR_DIFFERENT_LISTS_MUTATION
103 | );
104 | const [, updateList] = useMutation(UPDATE_LIST_MUTATION);
105 |
106 | if (!data) {
107 | return ;
108 | }
109 |
110 | const { name, lists, users } = data.boards_by_pk;
111 |
112 | const usersListNode = () => {
113 | return (
114 |
115 | {users.map((user: any) => {
116 | return ;
117 | })}
118 |
119 | );
120 | };
121 |
122 | const headingNode = () => {
123 | return (
124 |
125 |
126 | {name}
127 |
128 |
129 |
130 | {usersListNode()}
131 |
132 |
133 |
134 |
135 |
136 |
137 | );
138 | };
139 |
140 | const onDragEnd = (result: any, lists: any): any => {
141 | const { destination, source, type } = result;
142 |
143 | if (!destination) {
144 | return false;
145 | }
146 |
147 | if (
148 | source.droppableId === destination.droppableId &&
149 | source.index === destination.index
150 | ) {
151 | return false;
152 | }
153 |
154 | if (type === "list") {
155 | const destinationList = lists[destination.index];
156 | const sourceList = lists[source.index];
157 | const destinationMinusOneList = lists[destination.index - 1];
158 | const destinationPlusOneList = lists[destination.index + 1];
159 |
160 | const positionOfDestinationList = destinationList.position;
161 | const positionOfSourceList = sourceList.position;
162 | const positionOfDestinationMinusOneList =
163 | destinationMinusOneList && destinationMinusOneList.position;
164 | const positionOfDestinationPlusOneList =
165 | destinationPlusOneList && destinationPlusOneList.position;
166 |
167 | if (positionOfSourceList > positionOfDestinationList) {
168 | let updatedPositionOfSourceList;
169 |
170 | if (destinationMinusOneList) {
171 | updatedPositionOfSourceList =
172 | (positionOfDestinationList + positionOfDestinationMinusOneList) / 2;
173 | } else {
174 | updatedPositionOfSourceList = positionOfDestinationList / 2;
175 | }
176 |
177 | /**
178 | * Update source list
179 | */
180 | updateList({
181 | id: lists[source.index].id,
182 | position: updatedPositionOfSourceList,
183 | });
184 | } else {
185 | let updatedPositionOfSourceList;
186 |
187 | if (destinationPlusOneList) {
188 | updatedPositionOfSourceList =
189 | (positionOfDestinationList + positionOfDestinationPlusOneList) / 2;
190 | } else {
191 | updatedPositionOfSourceList = positionOfDestinationList + 1024;
192 | }
193 |
194 | /**
195 | * Update source list
196 | */
197 | updateCard({
198 | id: lists[source.index].id,
199 | position: updatedPositionOfSourceList,
200 | });
201 | }
202 | }
203 |
204 | if (type === "card") {
205 | /**
206 | * Card has been reordered within the same list
207 | */
208 | if (destination.droppableId === source.droppableId) {
209 | const list = find(lists, (l) => l.id === destination.droppableId);
210 |
211 | const destinationCard = list.cards[destination.index];
212 | const sourceCard = list.cards[source.index];
213 | const destinationMinusOneCard = list.cards[destination.index - 1];
214 | const destinationPlusOneCard = list.cards[destination.index + 1];
215 |
216 | const positionOfDestinationCard = destinationCard.position;
217 | const positionOfSourceCard = sourceCard.position;
218 | const positionOfDestinationMinusOneCard =
219 | destinationMinusOneCard && destinationMinusOneCard.position;
220 | const positionOfDestinationPlusOneCard =
221 | destinationPlusOneCard && destinationPlusOneCard.position;
222 |
223 | if (positionOfSourceCard > positionOfDestinationCard) {
224 | let updatedPositionOfSourceCard;
225 |
226 | if (destinationMinusOneCard) {
227 | updatedPositionOfSourceCard =
228 | (positionOfDestinationCard + positionOfDestinationMinusOneCard) /
229 | 2;
230 | } else {
231 | updatedPositionOfSourceCard = positionOfDestinationCard / 2;
232 | }
233 |
234 | /**
235 | * Update source card
236 | */
237 | updateCard({
238 | id: sourceCard.id,
239 | position: updatedPositionOfSourceCard,
240 | });
241 | } else {
242 | let updatedPositionOfSourceCard;
243 |
244 | if (destinationPlusOneCard) {
245 | updatedPositionOfSourceCard =
246 | (positionOfDestinationCard + positionOfDestinationPlusOneCard) /
247 | 2;
248 | } else {
249 | updatedPositionOfSourceCard = positionOfDestinationCard + 1024;
250 | }
251 |
252 | /**
253 | * Update source card
254 | */
255 | updateCard({
256 | id: sourceCard.id,
257 | position: updatedPositionOfSourceCard,
258 | });
259 | }
260 | } else {
261 | /**
262 | * Card has been reordered within different lists
263 | */
264 | const destinationList = find(
265 | lists,
266 | (l) => l.id === destination.droppableId
267 | );
268 | const sourceList = find(lists, (l) => l.id === source.droppableId);
269 | const destinationCard = destinationList.cards[destination.index];
270 | const sourceCard = sourceList.cards[source.index];
271 | const destinationMinusOneCard =
272 | destinationList.cards[destination.index - 1];
273 | const lastCardFromDestinationList =
274 | destinationList.cards[destinationList.cards.length - 1];
275 |
276 | const positionOfDestinationCard =
277 | destinationCard && destinationCard.position;
278 | const positionOfDestinationMinusOneCard =
279 | destinationMinusOneCard && destinationMinusOneCard.position;
280 | const positionOfLastCardFromDestinationList =
281 | lastCardFromDestinationList && lastCardFromDestinationList.position;
282 |
283 | let updatedPositionOfSourceCard;
284 |
285 | if (!destinationCard) {
286 | if (positionOfLastCardFromDestinationList) {
287 | updatedPositionOfSourceCard =
288 | positionOfLastCardFromDestinationList + 1024;
289 | } else {
290 | updatedPositionOfSourceCard = 1024;
291 | }
292 | } else if (!destinationMinusOneCard) {
293 | updatedPositionOfSourceCard = positionOfDestinationCard / 2;
294 | } else {
295 | updatedPositionOfSourceCard =
296 | (positionOfDestinationCard + positionOfDestinationMinusOneCard) / 2;
297 | }
298 |
299 | /**
300 | * Update source card
301 | */
302 | updateCardForDifferentLists({
303 | id: sourceCard.id,
304 | position: updatedPositionOfSourceCard,
305 | listId: destinationList.id,
306 | });
307 | }
308 | }
309 | };
310 |
311 | return (
312 |
313 | {headingNode()}
314 |
315 |
316 |
317 | onDragEnd(results, lists)}
319 | >
320 |
321 | {(provided: any) => (
322 |
323 |
324 | {provided.placeholder}
325 |
326 | )}
327 |
328 |
329 |
330 |
331 |
332 |
333 | );
334 | };
335 |
336 | export default Board;
337 |
--------------------------------------------------------------------------------