├── .env.example
├── .eslintrc.json
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── PULL_REQUEST_TEMPLATE.md
├── .gitignore
├── .husky
└── pre-commit
├── .lintstagedrc.json
├── .prettierignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── SECURITY.md
├── components.json
├── cypress.config.ts
├── cypress
├── e2e
│ ├── login.cy.ts
│ └── registration.cy.ts
├── fixtures
│ └── example.json
└── support
│ ├── commands.ts
│ └── e2e.ts
├── docker-compose.yml
├── docs
└── images
│ └── screenshot-1.png
├── next.config.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── prettier.config.js
├── prisma
├── migrations
│ ├── 20230828122827_add_boards_table
│ │ └── migration.sql
│ ├── 20230828123239_rename_user_id_to_owner_id
│ │ └── migration.sql
│ ├── 20230828143722_add_unique_constraint_to_the_slug_and_ownerid_columns
│ │ └── migration.sql
│ ├── 20230830160003_add_starredboards_pivot
│ │ └── migration.sql
│ ├── 20230902154755_rename_the_image_column_to_background
│ │ └── migration.sql
│ ├── 20231008153940_fix_table_names
│ │ └── migration.sql
│ └── migration_lock.toml
├── schema.prisma
└── seed.ts
├── public
├── next.svg
└── vercel.svg
├── src
├── app
│ ├── (auth)
│ │ ├── login
│ │ │ ├── discord-auth.tsx
│ │ │ ├── github-auth.tsx
│ │ │ ├── login-form.tsx
│ │ │ └── page.tsx
│ │ └── register
│ │ │ ├── actions.ts
│ │ │ ├── page.tsx
│ │ │ └── registration-form.tsx
│ ├── (dashboard)
│ │ ├── [username]
│ │ │ └── [boardSlug]
│ │ │ │ ├── @card
│ │ │ │ ├── (.)cards
│ │ │ │ │ └── [cardId]
│ │ │ │ │ │ ├── _components
│ │ │ │ │ │ ├── activity
│ │ │ │ │ │ │ └── activity.tsx
│ │ │ │ │ │ ├── description
│ │ │ │ │ │ │ ├── card-description.tsx
│ │ │ │ │ │ │ └── description-wysiwyg.tsx
│ │ │ │ │ │ ├── dialog-wrapper.tsx
│ │ │ │ │ │ ├── header
│ │ │ │ │ │ │ └── card-header.tsx
│ │ │ │ │ │ └── labels
│ │ │ │ │ │ │ ├── label-form.tsx
│ │ │ │ │ │ │ ├── label-list.tsx
│ │ │ │ │ │ │ └── labels.tsx
│ │ │ │ │ │ ├── actions.ts
│ │ │ │ │ │ ├── error.tsx
│ │ │ │ │ │ └── page.tsx
│ │ │ │ └── default.tsx
│ │ │ │ ├── _components
│ │ │ │ ├── background.tsx
│ │ │ │ ├── board.tsx
│ │ │ │ ├── card
│ │ │ │ │ ├── card-item.tsx
│ │ │ │ │ ├── card-skeleton.tsx
│ │ │ │ │ ├── cards-area.tsx
│ │ │ │ │ └── new-card.tsx
│ │ │ │ ├── header
│ │ │ │ │ ├── board-header.tsx
│ │ │ │ │ ├── fullscreen-button.tsx
│ │ │ │ │ └── member-list.tsx
│ │ │ │ └── list
│ │ │ │ │ ├── list-item.tsx
│ │ │ │ │ ├── list-list.tsx
│ │ │ │ │ └── new-list.tsx
│ │ │ │ ├── actions.tsx
│ │ │ │ ├── cards
│ │ │ │ └── [cardId]
│ │ │ │ │ └── page.tsx
│ │ │ │ ├── error.tsx
│ │ │ │ ├── layout.tsx
│ │ │ │ └── page.tsx
│ │ ├── _components
│ │ │ ├── boards
│ │ │ │ ├── board-list.tsx
│ │ │ │ └── boards.tsx
│ │ │ └── profile
│ │ │ │ ├── edit-profile.tsx
│ │ │ │ ├── profile-details.tsx
│ │ │ │ └── profile.tsx
│ │ ├── actions.ts
│ │ ├── layout.tsx
│ │ ├── new
│ │ │ ├── _components
│ │ │ │ ├── background
│ │ │ │ │ ├── background-select.tsx
│ │ │ │ │ ├── color-picker.tsx
│ │ │ │ │ └── image-picker.tsx
│ │ │ │ └── new-form.tsx
│ │ │ ├── actions.ts
│ │ │ └── page.tsx
│ │ ├── page.tsx
│ │ └── settings
│ │ │ ├── _components
│ │ │ └── header.tsx
│ │ │ ├── actions.ts
│ │ │ ├── layout.tsx
│ │ │ └── password
│ │ │ └── page.tsx
│ ├── api
│ │ └── auth
│ │ │ └── [...nextauth]
│ │ │ ├── actions.ts
│ │ │ ├── options.ts
│ │ │ └── route.ts
│ ├── apple-icon.png
│ ├── favicon.ico
│ ├── icon.png
│ ├── layout.tsx
│ └── not-found.tsx
├── assets
│ ├── 404.svg
│ ├── board.svg
│ ├── logo.svg
│ └── star.svg
├── components
│ ├── boards
│ │ ├── board-item.tsx
│ │ └── board-menu-item.tsx
│ ├── color-selector.tsx
│ ├── editable-title.tsx
│ ├── navbar
│ │ ├── actions.ts
│ │ ├── create-button.tsx
│ │ ├── dropdowns
│ │ │ ├── boards-dropdown.tsx
│ │ │ └── theme-toggle.tsx
│ │ ├── navbar.tsx
│ │ └── user-actions.tsx
│ ├── navigate.tsx
│ ├── not-found.tsx
│ ├── theme-provider.tsx
│ ├── ui
│ │ ├── alert.tsx
│ │ ├── avatar.tsx
│ │ ├── badge.tsx
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ ├── checkbox.tsx
│ │ ├── dialog.tsx
│ │ ├── dropdown-menu.tsx
│ │ ├── form.tsx
│ │ ├── hover-card.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── loading-button.tsx
│ │ ├── scroll-area.tsx
│ │ ├── separator.tsx
│ │ ├── sheet.tsx
│ │ ├── skeleton.tsx
│ │ ├── switch.tsx
│ │ ├── tabs.tsx
│ │ ├── textarea.tsx
│ │ ├── toggle.tsx
│ │ └── tooltip.tsx
│ └── wysiwyg
│ │ └── rich-text-editor.tsx
├── config
│ └── site.ts
├── context
│ ├── auth-context.tsx
│ ├── board-context.tsx
│ ├── board-list-context.tsx
│ ├── card-context.tsx
│ └── fullscreen-context.tsx
├── env.mjs
├── hooks
│ ├── use-outer-click.ts
│ └── use-ripple.tsx
├── lib
│ ├── fonts.ts
│ ├── prisma.ts
│ ├── schemas
│ │ ├── board.ts
│ │ ├── card.ts
│ │ ├── label.ts
│ │ ├── list.ts
│ │ ├── login.ts
│ │ ├── password.ts
│ │ ├── profile.ts
│ │ └── register.ts
│ └── utils.ts
├── middleware.ts
├── styles
│ └── globals.css
└── types
│ ├── board.ts
│ └── next-auth.d.ts
├── tailwind.config.ts
└── tsconfig.json
/.env.example:
--------------------------------------------------------------------------------
1 | DATABASE_URL="mysql://root:password@localhost:3306/trello_clone?schema=public"
2 |
3 | NEXTAUTH_SECRET=SECRET
4 | NEXTAUTH_URL=http://localhost:3000
5 | APP_URL=http://localhost:3000
6 |
7 | # OPTIONAL
8 | GITHUB_SECRET=
9 | GITHUB_ID=
10 |
11 | DISCORD_ID=
12 | DISCORD_SECRET=
13 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ""
5 | labels: bug
6 | assignees: ""
7 | ---
8 |
9 | **Describe the bug**
10 | A clear and concise description of what the bug is.
11 |
12 | **To Reproduce**
13 | Steps to reproduce the behavior:
14 |
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Additional context**
27 | Add any other context about the problem here.
28 |
29 | **Please choose an option below**
30 |
31 | - [ ] I would like to work on this issue.
32 | - [ ] I just opened the issue and would like someone else to handle it.
33 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ""
5 | labels: enhancement
6 | assignees: ""
7 | ---
8 |
9 | **Is your feature request related to a problem? Please describe.**
10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
11 |
12 | **Describe the solution you'd like**
13 | A clear and concise description of what you want to happen.
14 |
15 | **Describe alternatives you've considered**
16 | A clear and concise description of any alternative solutions or features you've considered.
17 |
18 | **Additional context**
19 | Add any other context or screenshots about the feature request here.
20 |
21 | **Please choose an option below**
22 |
23 | - [ ] I would like to work on this issue.
24 | - [ ] I just opened the issue and would like someone else to handle it.
25 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## Description
2 |
3 | Please include a summary of the change and which issue is fixed.
4 |
5 | ## Type of change
6 |
7 | Please delete options that are not relevant.
8 |
9 | - [ ] Bug fix (non-breaking change which fixes an issue)
10 | - [ ] New feature (non-breaking change which adds functionality)
11 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
12 | - [ ] This change requires a documentation update
13 |
14 | ## Checklist:
15 |
16 | - [ ] My code follows the style and structure of this project
17 | - [ ] My code follows the principles of clean code
18 | - [ ] My changes generate no new warnings, bugs or errors
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 | /data
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 | .env
31 |
32 | # vercel
33 | .vercel
34 |
35 | # typescript
36 | *.tsbuildinfo
37 | next-env.d.ts
38 |
39 | # ide
40 | .idea/
41 | .vscode/
42 | .vs/
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | npx lint-staged
5 |
--------------------------------------------------------------------------------
/.lintstagedrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "*.{ts,tsx,md,json,css}": ["npm run format"]
3 | }
4 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | .next
2 | dist
3 | node_modules
4 | build
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, gender identity and expression, level of experience,
9 | education, socio-economic status, nationality, personal appearance, race,
10 | religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | - Using welcoming and inclusive language
18 | - Being respectful of differing viewpoints and experiences
19 | - Gracefully accepting constructive criticism
20 | - Focusing on what is best for the community
21 | - Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | - The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | - Trolling, insulting/derogatory comments, and personal or political attacks
28 | - Public or private harassment
29 | - Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | - Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at {{ email }}. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | First of all, thank you for taking the time to contribute! Every contribution is appreciated.
4 | Reading and following these guidelines will help us make the contribution process easy and effective for everyone involved.
5 |
6 | - [Contributing](#contributing)
7 | - [Code of Conduct](#code-of-conduct)
8 | - [Ways of contributing](#ways-of-contributing)
9 | - [Issues](#issues)
10 | - [Found a bug?](#found-a-bug)
11 | - [Feature Request, Suggestion](#feature-request-suggestion)
12 | - [Pull Request](#pull-request)
13 | - [Other ways](#other-ways)
14 | - [General information](#general-information)
15 |
16 |
17 |
18 | ## Code of Conduct
19 |
20 | By participating and contributing to this project, you agree to abide by our [Code of Conduct](CODE_OF_CONDUCT.md).
21 |
22 |
23 |
24 | ## Ways of contributing
25 |
26 | Contributions to this repository are made via **Issues** and **Pull Requests**.
27 |
28 | ### Issues
29 |
30 | #### Found a bug?
31 |
32 | Open a new issue describing the bug and how it occurred.
33 | Please use an issue template, it will guide you on how to write your issue.
34 |
35 | #### Feature Request, Suggestion
36 |
37 | Again, you should use one of our issue templates.
38 | If you want to work on your suggestion as well, please open an issue before you start working on it, so we know about it.
39 |
40 | ### Pull Request
41 |
42 | If there's an issue you want to fix or develop, or if you've submitted an issue as a feature request and want to work on the solution:
43 |
44 | 1. Write a comment under the issue that you would like to work on it, so I can assign you the issue
45 | 2. Fork this repository
46 | 3. Clone your fork
47 | 4. Start working on your changes
48 | 5. Commit and push your changes
49 | 6. Open a Pull Request with your changes
50 |
51 | ### Other ways
52 |
53 | If you don't have any ideas yet, but want to support the project:
54 |
55 | 1. Add a GitHub star to this project
56 | 2. Tweet about the project
57 | 3. Write a review or blog post about the project
58 |
59 |
60 |
61 | ## General information
62 |
63 | - Please use a [pull request template](./.github/PULL_REQUEST_TEMPLATE.md) when you open a PR.
64 | - Please, when writing your commit messages, use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/).
65 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Olivér Mrakovics
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Trello Clone
2 |
3 | An open source Trello Clone built with everything new in the [Next.js 14](https://github.com/vercel/next.js) ecosystem.
4 |
5 |
6 |
7 |
8 |
9 | ## Tech Stack
10 |
11 | - Language: [TypeScript](https://www.typescriptlang.org/)
12 | - Framework: [React](https://react.dev/), [Next.js](https://nextjs.org/)
13 | - Styles: [TailwindCSS](https://tailwindcss.com/)
14 | - UI Components: [shadcn/ui](https://ui.shadcn.com/)
15 | - ORM: [Prisma](https://www.prisma.io/)
16 | - Authentication: [NextAuth](https://next-auth.js.org/)
17 | - Testing: [Cypress](https://www.cypress.io/)
18 | - Validation: [zod](https://zod.dev/)
19 | - Forms: [react-hook-form](https://react-hook-form.com/)
20 | - WYSIWYG: [Tiptap](https://tiptap.dev/)
21 | - Primitives: [RadixUI](https://www.radix-ui.com/)
22 | - Icons: [Lucide](https://lucide.dev/icons/)
23 | - DND: [react-beautiful-dnd](https://github.com/atlassian/react-beautiful-dnd)
24 | - Formatting: [Prettier](https://prettier.io/)
25 | - Background Images: [Unsplash](https://unsplash.com/)
26 | - Illustrations: [storyset](https://storyset.com/)
27 |
28 | ## Running Locally
29 |
30 | ```sh
31 | # Clone the project:
32 | git clone https://github.com/0l1v3rr/trello-clone.git
33 | cd trello-clone
34 |
35 | # Copy the .env.example file and rename it to .env
36 | # Also, make the appropriate changes
37 | cp .env.example .env
38 |
39 | # Run the database with docker:
40 | docker compose up -d
41 |
42 | # Install the dependencies:
43 | npm i
44 | npm run prepare
45 |
46 | # Reset the DB
47 | npx prisma migrate reset
48 |
49 | # Run the application
50 | npm run dev
51 |
52 | # Also, you can preview the app to see how it would work in production
53 | npm run preview
54 | ```
55 |
56 | After the successful installation, the app should start [here](http://localhost:3000). You can log in with the following credentials: username: **test**, password: **test**
57 |
58 | ## Contributing
59 |
60 | **Contributions are more than welcome!** Please open an issue if you have any questions or suggestions. See the [contributing guide](./CONTRIBUTING.md) for more information.
61 |
62 | ## License
63 |
64 | This project is licensed under the [MIT License](./LICENSE).
65 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | | Version | Supported |
6 | | ------- | ------------------ |
7 | | 1.x.x | :white_check_mark: |
8 |
9 | ## Reporting a Vulnerability
10 |
11 | Please report (suspected) security vulnerabilities via **[email](mailto:oliver.mrakovics@gmail.com)**. You will receive a response from us within 48 hours. If the issue is confirmed, we will release a patch as soon as possible depending on complexity but historically within a few days.
12 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.js",
8 | "css": "app/styles/globals.css",
9 | "baseColor": "slate",
10 | "cssVariables": true
11 | },
12 | "aliases": {
13 | "components": "@/components",
14 | "utils": "@/lib/utils"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/cypress.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "cypress";
2 |
3 | export default defineConfig({
4 | e2e: {
5 | baseUrl: "http://localhost:3000",
6 | setupNodeEvents(on, config) {
7 | // implement node event listeners here
8 | },
9 | },
10 | });
11 |
--------------------------------------------------------------------------------
/cypress/e2e/login.cy.ts:
--------------------------------------------------------------------------------
1 | describe("login page tests", () => {
2 | beforeEach(() => {
3 | cy.visit("/login");
4 | });
5 |
6 | it("should log in a user", () => {
7 | cy.get('[data-test-id="login-email"]').type("test");
8 | cy.get('[data-test-id="login-password"').type("test");
9 | cy.get('[data-test-id="login-submit-btn"]').click();
10 | cy.location("pathname").should("eq", "/");
11 |
12 | cy.visit("/login");
13 |
14 | cy.get('[data-test-id="login-email"]').type("test@gmail.com");
15 | cy.get('[data-test-id="login-password"').type("test");
16 | cy.get('[data-test-id="login-submit-btn"]').click();
17 | cy.location("pathname").should("eq", "/");
18 | });
19 |
20 | it("should not log in a user because of wrong credentials", () => {
21 | cy.contains(/Invalid login credentials/i).should("not.exist");
22 | cy.get('[data-test-id="login-submit-btn"]').click();
23 | cy.contains(/Invalid login credentials/i);
24 |
25 | cy.reload();
26 |
27 | cy.contains(/Invalid login credentials/i).should("not.exist");
28 | cy.get('[data-test-id="login-email"]').type("doesntexist@aaa.com");
29 | cy.get('[data-test-id="login-password"').type(
30 | "ThisIsAnIncorrectPasswordComeOn!"
31 | );
32 | cy.get('[data-test-id="login-submit-btn"]').click();
33 | cy.contains(/Invalid login credentials/i);
34 | });
35 |
36 | it("should redirect to the register page", () => {
37 | cy.get('[data-test-id="reg-btn"]').click();
38 | cy.location("pathname").should("eq", "/register");
39 | });
40 | });
41 |
--------------------------------------------------------------------------------
/cypress/e2e/registration.cy.ts:
--------------------------------------------------------------------------------
1 | describe("registration page tests", () => {
2 | beforeEach(() => {
3 | cy.visit("/register");
4 | });
5 |
6 | it("should create a user", () => {
7 | cy.get('[data-test-id="reg-name"]').type("John Doe");
8 | cy.get('[data-test-id="reg-email"]').type("johndoe@test.com");
9 | cy.get('[data-test-id="reg-password"]').type("TestPass1@3");
10 | cy.get('[data-test-id="reg-password-confirm"]').type("TestPass1@3");
11 | cy.get('[data-test-id="reg-submit-btn"]').click();
12 | cy.location("pathname").should("eq", "/login");
13 | });
14 |
15 | it("should not create a user because the given email already exist", () => {
16 | cy.contains(/User with this email already exists/i).should("not.exist");
17 | cy.get('[data-test-id="reg-name"]').type("Test User");
18 | cy.get('[data-test-id="reg-email"]').type("test@gmail.com");
19 | cy.get('[data-test-id="reg-password"]').type("TestPass1@3");
20 | cy.get('[data-test-id="reg-password-confirm"]').type("TestPass1@3");
21 | cy.get('[data-test-id="reg-submit-btn"]').click();
22 | cy.contains(/User with this email already exists/i);
23 | });
24 |
25 | it("should redirect to the login page", () => {
26 | cy.get('[data-test-id="login-btn"]').click();
27 | cy.location("pathname").should("eq", "/login");
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/cypress/fixtures/example.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Using fixtures to represent data",
3 | "email": "hello@cypress.io",
4 | "body": "Fixtures are a great way to mock data for responses to routes"
5 | }
6 |
--------------------------------------------------------------------------------
/cypress/support/commands.ts:
--------------------------------------------------------------------------------
1 | ///
2 | // ***********************************************
3 | // This example commands.ts shows you how to
4 | // create various custom commands and overwrite
5 | // existing commands.
6 | //
7 | // For more comprehensive examples of custom
8 | // commands please read more here:
9 | // https://on.cypress.io/custom-commands
10 | // ***********************************************
11 | //
12 | //
13 | // -- This is a parent command --
14 | // Cypress.Commands.add('login', (email, password) => { ... })
15 | //
16 | //
17 | // -- This is a child command --
18 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
19 | //
20 | //
21 | // -- This is a dual command --
22 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
23 | //
24 | //
25 | // -- This will overwrite an existing command --
26 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
27 | //
28 | // declare global {
29 | // namespace Cypress {
30 | // interface Chainable {
31 | // login(email: string, password: string): Chainable
32 | // drag(subject: string, options?: Partial): Chainable
33 | // dismiss(subject: string, options?: Partial): Chainable
34 | // visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable
35 | // }
36 | // }
37 | // }
38 |
--------------------------------------------------------------------------------
/cypress/support/e2e.ts:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/e2e.ts is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import "./commands";
18 |
19 | // Alternatively you can use CommonJS syntax:
20 | // require('./commands')
21 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.8'
2 | services:
3 | db:
4 | image: mysql:8.1
5 | environment:
6 | MYSQL_DATABASE: 'trello_clone'
7 | MYSQL_PASSWORD: 'password'
8 | MYSQL_ROOT_PASSWORD: 'password'
9 | ports:
10 | - '3306:3306'
11 | expose:
12 | - '3306'
13 | volumes:
14 | - ./data:/var/lib/mysql
15 |
--------------------------------------------------------------------------------
/docs/images/screenshot-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/0l1v3rr/trello-clone/df221aac8259a51c6cfd491606e6961a9358f7d9/docs/images/screenshot-1.png
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | experimental: {
4 | serverComponentsExternalPackages: ["@prisma/client", "bcrypt"],
5 | },
6 | images: {
7 | remotePatterns: [{ protocol: "https", hostname: "**.unsplash.com" }],
8 | },
9 | };
10 |
11 | module.exports = nextConfig;
12 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "trello-clone",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "lint:fix": "next lint --fix",
11 | "preview": "next build && next start",
12 | "format": "prettier --write \"**/*.{ts,tsx,js,json,md,css}\" --cache",
13 | "format:check": "prettier --check \"**/*.{ts,tsx,js,json,md,css}\" --cache",
14 | "prepare": "husky install"
15 | },
16 | "prisma": {
17 | "seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
18 | },
19 | "dependencies": {
20 | "@hookform/resolvers": "^3.3.2",
21 | "@ianvs/prettier-plugin-sort-imports": "^4.1.1",
22 | "@prisma/client": "^5.7.1",
23 | "@radix-ui/react-avatar": "^1.0.4",
24 | "@radix-ui/react-checkbox": "^1.0.4",
25 | "@radix-ui/react-dialog": "^1.0.5",
26 | "@radix-ui/react-dropdown-menu": "^2.0.6",
27 | "@radix-ui/react-hover-card": "^1.0.7",
28 | "@radix-ui/react-label": "^2.0.2",
29 | "@radix-ui/react-scroll-area": "^1.0.5",
30 | "@radix-ui/react-separator": "^1.0.3",
31 | "@radix-ui/react-slot": "^1.0.2",
32 | "@radix-ui/react-switch": "^1.0.3",
33 | "@radix-ui/react-tabs": "^1.0.4",
34 | "@radix-ui/react-toggle": "^1.0.3",
35 | "@radix-ui/react-tooltip": "^1.0.7",
36 | "@t3-oss/env-nextjs": "^0.7.1",
37 | "@tiptap/react": "^2.1.13",
38 | "@tiptap/starter-kit": "^2.1.13",
39 | "@types/node": "20.10.5",
40 | "@types/react": "18.2.45",
41 | "@types/react-dom": "18.2.18",
42 | "autoprefixer": "10.4.16",
43 | "bcrypt": "^5.1.1",
44 | "class-variance-authority": "^0.7.0",
45 | "clsx": "^2.0.0",
46 | "cypress": "^13.6.1",
47 | "eslint": "8.56.0",
48 | "eslint-config-next": "^14.1.0",
49 | "html-react-parser": "^5.0.11",
50 | "lucide-react": "^0.298.0",
51 | "next": "^14.1.0",
52 | "next-auth": "^4.24.5",
53 | "next-themes": "^0.2.1",
54 | "postcss": "8.4.32",
55 | "react": "^18.2.0",
56 | "react-beautiful-dnd": "^13.1.1",
57 | "react-dom": "^18.2.0",
58 | "react-hook-form": "^7.49.2",
59 | "react-icons": "^4.12.0",
60 | "tailwind-merge": "^2.1.0",
61 | "tailwindcss": "3.4.0",
62 | "tailwindcss-animate": "^1.0.7",
63 | "ts-node": "^10.9.2",
64 | "typescript": "5.3.3",
65 | "zod": "^3.22.4"
66 | },
67 | "devDependencies": {
68 | "@types/bcrypt": "^5.0.2",
69 | "@types/react-beautiful-dnd": "^13.1.7",
70 | "husky": "^8.0.0",
71 | "lint-staged": "^15.2.0",
72 | "prettier": "^3.1.1",
73 | "prettier-plugin-tailwindcss": "^0.5.9",
74 | "prisma": "^5.7.1"
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('prettier').Config} */
2 | module.exports = {
3 | endOfLine: "lf",
4 | semi: true,
5 | singleQuote: false,
6 | tabWidth: 2,
7 | trailingComma: "es5",
8 | importOrder: [
9 | "^(react/(.*)$)|^(react$)",
10 | "^(next/(.*)$)|^(next$)",
11 | "",
12 | "^types$",
13 | "^@/types/(.*)$",
14 | "^@/config/(.*)$",
15 | "^@/lib/(.*)$",
16 | "^@/hooks/(.*)$",
17 | "^@/components/ui/(.*)$",
18 | "^@/components/(.*)$",
19 | "^@/styles/(.*)$",
20 | "^@/app/(.*)$",
21 | "^[./]",
22 | ],
23 | plugins: [
24 | "@ianvs/prettier-plugin-sort-imports",
25 | "prettier-plugin-tailwindcss",
26 | ],
27 | };
28 |
--------------------------------------------------------------------------------
/prisma/migrations/20230828122827_add_boards_table/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - You are about to drop the `user` table. If the table is not empty, all the data it contains will be lost.
5 |
6 | */
7 | -- CreateTable
8 | CREATE TABLE `users` (
9 | `id` VARCHAR(191) NOT NULL,
10 | `name` VARCHAR(191) NOT NULL,
11 | `email` VARCHAR(191) NOT NULL,
12 | `username` VARCHAR(191) NOT NULL,
13 | `password` VARCHAR(191) NULL,
14 | `status` VARCHAR(191) NULL,
15 | `image` VARCHAR(191) NULL,
16 | `active` BOOLEAN NULL,
17 | `role` ENUM('USER', 'ADMIN') NOT NULL DEFAULT 'USER',
18 | `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
19 | `updatedAt` DATETIME(3) NOT NULL,
20 |
21 | UNIQUE INDEX `users_email_key`(`email`),
22 | UNIQUE INDEX `users_username_key`(`username`),
23 | PRIMARY KEY (`id`)
24 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
25 |
26 | -- CreateTable
27 | CREATE TABLE `boards` (
28 | `id` VARCHAR(191) NOT NULL,
29 | `name` VARCHAR(191) NOT NULL,
30 | `slug` VARCHAR(191) NOT NULL,
31 | `image` JSON NOT NULL,
32 | `public` BOOLEAN NOT NULL,
33 | `userId` VARCHAR(191) NOT NULL,
34 | `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
35 | `updatedAt` DATETIME(3) NOT NULL,
36 |
37 | PRIMARY KEY (`id`)
38 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
39 |
40 | -- CreateTable
41 | CREATE TABLE `_GuestBoards` (
42 | `A` VARCHAR(191) NOT NULL,
43 | `B` VARCHAR(191) NOT NULL,
44 |
45 | UNIQUE INDEX `_GuestBoards_AB_unique`(`A`, `B`),
46 | INDEX `_GuestBoards_B_index`(`B`)
47 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
48 |
49 | -- AddForeignKey
50 | ALTER TABLE `boards` ADD CONSTRAINT `boards_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `users`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
51 |
52 | -- AddForeignKey
53 | ALTER TABLE `_GuestBoards` ADD CONSTRAINT `_GuestBoards_A_fkey` FOREIGN KEY (`A`) REFERENCES `boards`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
54 |
55 | -- AddForeignKey
56 | ALTER TABLE `_GuestBoards` ADD CONSTRAINT `_GuestBoards_B_fkey` FOREIGN KEY (`B`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
57 |
--------------------------------------------------------------------------------
/prisma/migrations/20230828123239_rename_user_id_to_owner_id/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - You are about to drop the column `userId` on the `boards` table. All the data in the column will be lost.
5 | - Added the required column `ownerId` to the `boards` table without a default value. This is not possible if the table is not empty.
6 |
7 | */
8 | -- DropForeignKey
9 | ALTER TABLE `boards` DROP FOREIGN KEY `boards_userId_fkey`;
10 |
11 | -- AlterTable
12 | ALTER TABLE `boards` DROP COLUMN `userId`,
13 | ADD COLUMN `ownerId` VARCHAR(191) NOT NULL;
14 |
15 | -- AddForeignKey
16 | ALTER TABLE `boards` ADD CONSTRAINT `boards_ownerId_fkey` FOREIGN KEY (`ownerId`) REFERENCES `users`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
17 |
--------------------------------------------------------------------------------
/prisma/migrations/20230828143722_add_unique_constraint_to_the_slug_and_ownerid_columns/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - A unique constraint covering the columns `[slug,ownerId]` on the table `boards` will be added. If there are existing duplicate values, this will fail.
5 |
6 | */
7 | -- CreateIndex
8 | CREATE UNIQUE INDEX `boards_slug_ownerId_key` ON `boards`(`slug`, `ownerId`);
9 |
--------------------------------------------------------------------------------
/prisma/migrations/20230830160003_add_starredboards_pivot/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE `_StarredBoards` (
3 | `A` VARCHAR(191) NOT NULL,
4 | `B` VARCHAR(191) NOT NULL,
5 |
6 | UNIQUE INDEX `_StarredBoards_AB_unique`(`A`, `B`),
7 | INDEX `_StarredBoards_B_index`(`B`)
8 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
9 |
10 | -- AddForeignKey
11 | ALTER TABLE `_StarredBoards` ADD CONSTRAINT `_StarredBoards_A_fkey` FOREIGN KEY (`A`) REFERENCES `boards`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
12 |
13 | -- AddForeignKey
14 | ALTER TABLE `_StarredBoards` ADD CONSTRAINT `_StarredBoards_B_fkey` FOREIGN KEY (`B`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
15 |
--------------------------------------------------------------------------------
/prisma/migrations/20230902154755_rename_the_image_column_to_background/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - You are about to drop the column `image` on the `boards` table. All the data in the column will be lost.
5 | - Added the required column `background` to the `boards` table without a default value. This is not possible if the table is not empty.
6 |
7 | */
8 | -- AlterTable
9 | ALTER TABLE `boards` DROP COLUMN `image`,
10 | ADD COLUMN `background` JSON NOT NULL;
11 |
--------------------------------------------------------------------------------
/prisma/migrations/20231008153940_fix_table_names/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE `labels` (
3 | `id` VARCHAR(191) NOT NULL,
4 | `title` VARCHAR(191) NOT NULL,
5 | `color` VARCHAR(191) NOT NULL,
6 | `boardId` VARCHAR(191) NOT NULL,
7 | `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
8 | `updatedAt` DATETIME(3) NOT NULL,
9 |
10 | PRIMARY KEY (`id`)
11 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
12 |
13 | -- CreateTable
14 | CREATE TABLE `lists` (
15 | `id` VARCHAR(191) NOT NULL,
16 | `title` VARCHAR(191) NOT NULL,
17 | `boardId` VARCHAR(191) NOT NULL,
18 | `position` INTEGER NOT NULL,
19 | `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
20 | `updatedAt` DATETIME(3) NOT NULL,
21 |
22 | PRIMARY KEY (`id`)
23 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
24 |
25 | -- CreateTable
26 | CREATE TABLE `cards` (
27 | `id` VARCHAR(191) NOT NULL,
28 | `title` VARCHAR(191) NOT NULL,
29 | `description` LONGTEXT NULL,
30 | `position` INTEGER NOT NULL,
31 | `listId` VARCHAR(191) NOT NULL,
32 | `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
33 | `updatedAt` DATETIME(3) NOT NULL,
34 |
35 | PRIMARY KEY (`id`)
36 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
37 |
38 | -- CreateTable
39 | CREATE TABLE `_CardToLabel` (
40 | `A` VARCHAR(191) NOT NULL,
41 | `B` VARCHAR(191) NOT NULL,
42 |
43 | UNIQUE INDEX `_CardToLabel_AB_unique`(`A`, `B`),
44 | INDEX `_CardToLabel_B_index`(`B`)
45 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
46 |
47 | -- AddForeignKey
48 | ALTER TABLE `labels` ADD CONSTRAINT `labels_boardId_fkey` FOREIGN KEY (`boardId`) REFERENCES `boards`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
49 |
50 | -- AddForeignKey
51 | ALTER TABLE `lists` ADD CONSTRAINT `lists_boardId_fkey` FOREIGN KEY (`boardId`) REFERENCES `boards`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
52 |
53 | -- AddForeignKey
54 | ALTER TABLE `cards` ADD CONSTRAINT `cards_listId_fkey` FOREIGN KEY (`listId`) REFERENCES `lists`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
55 |
56 | -- AddForeignKey
57 | ALTER TABLE `_CardToLabel` ADD CONSTRAINT `_CardToLabel_A_fkey` FOREIGN KEY (`A`) REFERENCES `cards`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
58 |
59 | -- AddForeignKey
60 | ALTER TABLE `_CardToLabel` ADD CONSTRAINT `_CardToLabel_B_fkey` FOREIGN KEY (`B`) REFERENCES `labels`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
61 |
--------------------------------------------------------------------------------
/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (i.e. Git)
3 | provider = "mysql"
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | generator client {
2 | provider = "prisma-client-js"
3 | }
4 |
5 | datasource db {
6 | provider = "mysql"
7 | url = env("DATABASE_URL")
8 | }
9 |
10 | model User {
11 | id String @id @default(uuid())
12 | name String
13 | email String @unique
14 | username String @unique
15 | password String?
16 | status String?
17 | image String?
18 | active Boolean?
19 | role RoleEnum @default(USER)
20 | boards Board[] @relation("OwnedBoards")
21 | guestBoards Board[] @relation("GuestBoards")
22 | starredBoards Board[] @relation("StarredBoards")
23 | createdAt DateTime @default(now())
24 | updatedAt DateTime @updatedAt
25 |
26 | @@map(name: "users")
27 | }
28 |
29 | enum RoleEnum {
30 | USER
31 | ADMIN
32 | }
33 |
34 | model Board {
35 | id String @id @default(uuid())
36 | name String
37 | slug String
38 | background Json
39 | public Boolean
40 | owner User @relation("OwnedBoards", fields: [ownerId], references: [id])
41 | ownerId String
42 | members User[] @relation("GuestBoards")
43 | starsGivenBy User[] @relation("StarredBoards")
44 | labels Label[]
45 | lists List[]
46 | createdAt DateTime @default(now())
47 | updatedAt DateTime @updatedAt
48 |
49 | @@unique([slug, ownerId])
50 | @@map(name: "boards")
51 | }
52 |
53 | model Label {
54 | id String @id @default(uuid())
55 | title String
56 | color String
57 | board Board @relation(fields: [boardId], references: [id])
58 | boardId String
59 | cards Card[]
60 | createdAt DateTime @default(now())
61 | updatedAt DateTime @updatedAt
62 |
63 | @@map(name: "labels")
64 | }
65 |
66 | model List {
67 | id String @id @default(uuid())
68 | board Board @relation(fields: [boardId], references: [id])
69 | title String
70 | boardId String
71 | position Int
72 | cards Card[]
73 | createdAt DateTime @default(now())
74 | updatedAt DateTime @updatedAt
75 |
76 | @@map(name: "lists")
77 | }
78 |
79 | model Card {
80 | id String @id @default(uuid())
81 | title String
82 | description String? @db.LongText
83 | position Int
84 | labels Label[]
85 | list List @relation(fields: [listId], references: [id])
86 | listId String
87 | createdAt DateTime @default(now())
88 | updatedAt DateTime @updatedAt
89 |
90 | @@map(name: "cards")
91 | }
92 |
--------------------------------------------------------------------------------
/prisma/seed.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client";
2 | import * as bcrypt from "bcrypt";
3 |
4 | const prisma = new PrismaClient();
5 |
6 | async function main() {
7 | const password = await bcrypt.hash("test", 10);
8 |
9 | await prisma.user.createMany({
10 | data: [
11 | {
12 | id: "user-1",
13 | email: "test@gmail.com",
14 | name: "Test User",
15 | username: "test",
16 | password,
17 | },
18 | {
19 | id: "user-2",
20 | email: "john@gmail.com",
21 | name: "John Doe",
22 | username: "john",
23 | password,
24 | },
25 | ],
26 | });
27 |
28 | await prisma.board.createMany({
29 | data: [
30 | {
31 | id: "board-1",
32 | name: "Test Board",
33 | slug: "test-board",
34 | ownerId: "user-1",
35 | public: true,
36 | background: { type: "color", value: "#1abc9c" },
37 | },
38 | {
39 | id: "board-2",
40 | name: "Test Board 2",
41 | slug: "test-board-2",
42 | ownerId: "user-1",
43 | public: false,
44 | background: {
45 | type: "image",
46 | value:
47 | "https://images.unsplash.com/photo-1470115636492-6d2b56f9146d?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8bGFuZHNjYXBlfHx8fHx8MTY5MzIyNjY3Mg&ixlib=rb-4.0.3&q=80&w=1080",
48 | },
49 | },
50 | {
51 | id: "board-3",
52 | name: "Test Board 3",
53 | slug: "test-board-3",
54 | ownerId: "user-2",
55 | public: false,
56 | background: { type: "color", value: "#e67e22" },
57 | },
58 | ],
59 | });
60 |
61 | await prisma.user.update({
62 | where: { id: "user-1" },
63 | data: {
64 | starredBoards: { connect: { id: "board-2" } },
65 | guestBoards: { connect: { id: "board-3" } },
66 | },
67 | });
68 |
69 | await prisma.user.update({
70 | where: { id: "user-2" },
71 | data: {
72 | guestBoards: { connect: { id: "board-2" } },
73 | },
74 | });
75 |
76 | await prisma.label.createMany({
77 | data: [
78 | {
79 | id: "label-1",
80 | title: "Backend",
81 | color: "#6622cc",
82 | boardId: "board-2",
83 | },
84 | {
85 | id: "label-2",
86 | title: "Frontend",
87 | color: "#22ccaa",
88 | boardId: "board-2",
89 | },
90 | ],
91 | });
92 |
93 | await prisma.list.create({
94 | data: {
95 | id: "list-1",
96 | position: 1,
97 | title: "Backlog",
98 | boardId: "board-2",
99 | cards: {
100 | create: [
101 | {
102 | title: "Test card",
103 | description: "This is a test card with HTML formatting.",
104 | position: 1,
105 | labels: {
106 | connect: [{ id: "label-1" }, { id: "label-2" }],
107 | },
108 | },
109 | {
110 | title: "Another test card",
111 | position: 2,
112 | },
113 | ],
114 | },
115 | },
116 | });
117 | }
118 |
119 | main()
120 | .then(async () => {
121 | await prisma.$disconnect();
122 | })
123 | .catch(async (e) => {
124 | console.error(e);
125 | await prisma.$disconnect();
126 | process.exit(1);
127 | });
128 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/(auth)/login/discord-auth.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { FC } from "react";
4 | import { useSearchParams } from "next/navigation";
5 | import { ClientSafeProvider, signIn } from "next-auth/react";
6 | import { BsDiscord } from "react-icons/bs";
7 | import { Button } from "@/components/ui/button";
8 |
9 | interface DiscordAuthProps {
10 | discord?: ClientSafeProvider;
11 | }
12 |
13 | const DiscordAuth: FC = ({ discord }) => {
14 | const params = useSearchParams();
15 |
16 | return (
17 |
30 | );
31 | };
32 |
33 | export default DiscordAuth;
34 |
--------------------------------------------------------------------------------
/src/app/(auth)/login/github-auth.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { FC } from "react";
4 | import { useSearchParams } from "next/navigation";
5 | import { ClientSafeProvider, signIn } from "next-auth/react";
6 | import { FaGithub } from "react-icons/fa";
7 | import { Button } from "@/components/ui/button";
8 |
9 | interface GitHubAuthProps {
10 | github?: ClientSafeProvider;
11 | }
12 |
13 | const GithubAuth: FC = ({ github }) => {
14 | const params = useSearchParams();
15 |
16 | return (
17 |
30 | );
31 | };
32 |
33 | export default GithubAuth;
34 |
--------------------------------------------------------------------------------
/src/app/(auth)/login/login-form.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 | import Link from "next/link";
5 | import { useRouter, useSearchParams } from "next/navigation";
6 | import { zodResolver } from "@hookform/resolvers/zod";
7 | import { signIn } from "next-auth/react";
8 | import { useForm } from "react-hook-form";
9 | import * as z from "zod";
10 | import { loginSchema } from "@/lib/schemas/login";
11 | import { Alert } from "@/components/ui/alert";
12 | import { Button } from "@/components/ui/button";
13 | import {
14 | Form,
15 | FormControl,
16 | FormField,
17 | FormItem,
18 | FormLabel,
19 | FormMessage,
20 | } from "@/components/ui/form";
21 | import { Input } from "@/components/ui/input";
22 | import LoadingButton from "@/components/ui/loading-button";
23 |
24 | const LoginForm = () => {
25 | const params = useSearchParams();
26 | const router = useRouter();
27 |
28 | const [error, setError] = useState();
29 |
30 | const form = useForm>({
31 | resolver: zodResolver(loginSchema),
32 | defaultValues: {
33 | username: params.get("email") ?? "",
34 | password: "",
35 | },
36 | });
37 |
38 | const onSubmit = async (values: z.infer) => {
39 | const res = await signIn("credentials", {
40 | ...values,
41 | callbackUrl: params.get("callbackUrl") ?? "/",
42 | redirect: false,
43 | });
44 |
45 | if (!res || res.error) {
46 | setError("Invalid login credentials");
47 | } else {
48 | router.push(params.get("callbackUrl") ?? "/");
49 | }
50 | };
51 |
52 | return (
53 |
115 |
116 | );
117 | };
118 |
119 | export default LoginForm;
120 |
--------------------------------------------------------------------------------
/src/app/(auth)/login/page.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Metadata } from "next";
3 | import { getProviders } from "next-auth/react";
4 | import {
5 | Card,
6 | CardContent,
7 | CardDescription,
8 | CardHeader,
9 | CardTitle,
10 | } from "@/components/ui/card";
11 | import DiscordAuth from "@/app/(auth)/login/discord-auth";
12 | import GithubAuth from "@/app/(auth)/login/github-auth";
13 | import LoginForm from "@/app/(auth)/login/login-form";
14 |
15 | export const metadata: Metadata = {
16 | title: "Login",
17 | };
18 |
19 | const LoginPage = async () => {
20 | const providers = await getProviders();
21 |
22 | return (
23 |
24 |
25 |
26 | Login
27 | Trello Clone
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | );
45 | };
46 |
47 | export default LoginPage;
48 |
--------------------------------------------------------------------------------
/src/app/(auth)/register/actions.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import * as bcrypt from "bcrypt";
4 | import * as z from "zod";
5 | import { prisma } from "@/lib/prisma";
6 | import { registerSchema } from "@/lib/schemas/register";
7 | import {
8 | generateRandomHex,
9 | generateUsernameFromEmail,
10 | slugify,
11 | } from "@/lib/utils";
12 | import { createBoard } from "@/app/(dashboard)/new/actions";
13 |
14 | export async function createUser(user: z.infer) {
15 | const res = registerSchema.safeParse(user);
16 | if (!res.success) {
17 | throw new Error(res.error.errors.at(0)?.message);
18 | }
19 |
20 | const foundUser = await prisma.user.findUnique({
21 | where: { email: user.email },
22 | });
23 | if (foundUser) {
24 | throw new Error("User with this email already exists");
25 | }
26 |
27 | const { passwordConfirmation, ...data } = user;
28 |
29 | const password = await bcrypt.hash(data.password, 10);
30 | const newUser = await prisma.user.create({
31 | data: {
32 | ...data,
33 | username: generateUsernameFromEmail(data.email),
34 | password,
35 | },
36 | });
37 |
38 | await createBoard(newUser.id, {
39 | name: `${newUser.name}'s Board`,
40 | slug: `${slugify(newUser.name)}s-board`,
41 | public: false,
42 | background: { type: "color", value: generateRandomHex() },
43 | });
44 |
45 | return newUser;
46 | }
47 |
--------------------------------------------------------------------------------
/src/app/(auth)/register/page.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Metadata } from "next";
3 | import {
4 | Card,
5 | CardContent,
6 | CardDescription,
7 | CardHeader,
8 | CardTitle,
9 | } from "@/components/ui/card";
10 | import RegisterForm from "@/app/(auth)/register/registration-form";
11 |
12 | export const metadata: Metadata = {
13 | title: "Registration",
14 | };
15 |
16 | const LoginPage = async () => {
17 | return (
18 |
19 |
20 |
21 | Register
22 | Trello Clone
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | );
31 | };
32 |
33 | export default LoginPage;
34 |
--------------------------------------------------------------------------------
/src/app/(auth)/register/registration-form.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 | import Link from "next/link";
5 | import { zodResolver } from "@hookform/resolvers/zod";
6 | import { signIn } from "next-auth/react";
7 | import { useForm } from "react-hook-form";
8 | import * as z from "zod";
9 | import { registerSchema } from "@/lib/schemas/register";
10 | import { Alert } from "@/components/ui/alert";
11 | import { Button } from "@/components/ui/button";
12 | import {
13 | Form,
14 | FormControl,
15 | FormField,
16 | FormItem,
17 | FormLabel,
18 | FormMessage,
19 | } from "@/components/ui/form";
20 | import { Input } from "@/components/ui/input";
21 | import LoadingButton from "@/components/ui/loading-button";
22 | import { createUser } from "@/app/(auth)/register/actions";
23 |
24 | const RegistrationForm = () => {
25 | const [error, setError] = useState();
26 |
27 | const form = useForm>({
28 | resolver: zodResolver(registerSchema),
29 | defaultValues: {
30 | email: "",
31 | name: "",
32 | password: "",
33 | passwordConfirmation: "",
34 | },
35 | });
36 |
37 | const onSubmit = async (values: z.infer) => {
38 | try {
39 | await createUser(values);
40 | await signIn("credentials", {
41 | username: values.email,
42 | password: values.password,
43 | callbackUrl: "/",
44 | redirect: true,
45 | });
46 | } catch (e: any) {
47 | setError(e.message);
48 | }
49 | };
50 |
51 | return (
52 |
151 |
152 | );
153 | };
154 |
155 | export default RegistrationForm;
156 |
--------------------------------------------------------------------------------
/src/app/(dashboard)/[username]/[boardSlug]/@card/(.)cards/[cardId]/_components/activity/activity.tsx:
--------------------------------------------------------------------------------
1 | const Activity = () => {
2 | return (
3 |
7 | );
8 | };
9 |
10 | export default Activity;
11 |
--------------------------------------------------------------------------------
/src/app/(dashboard)/[username]/[boardSlug]/@card/(.)cards/[cardId]/_components/description/card-description.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 | import { useCardContext } from "@/context/card-context";
5 | import parse from "html-react-parser";
6 | import { cn } from "@/lib/utils";
7 | import { Button } from "@/components/ui/button";
8 | import DescriptionWysiwyg from "@/app/(dashboard)/[username]/[boardSlug]/@card/(.)cards/[cardId]/_components/description/description-wysiwyg";
9 |
10 | const CardDescription = () => {
11 | const { card, permission } = useCardContext();
12 | const [descriptionEdit, setDescriptionEdit] = useState(false);
13 |
14 | return (
15 |
16 |
17 | Description
18 |
19 | {descriptionEdit ? (
20 |
setDescriptionEdit(false)} />
21 | ) : (
22 |
39 | )}
40 |
41 | );
42 | };
43 |
44 | export default CardDescription;
45 |
--------------------------------------------------------------------------------
/src/app/(dashboard)/[username]/[boardSlug]/@card/(.)cards/[cardId]/_components/description/description-wysiwyg.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { FC, FormEvent, useState, useTransition } from "react";
4 | import { useCardContext } from "@/context/card-context";
5 | import { Button } from "@/components/ui/button";
6 | import LoadingButton from "@/components/ui/loading-button";
7 | import RichTextEditor from "@/components/wysiwyg/rich-text-editor";
8 | import { updateCard } from "@/app/(dashboard)/[username]/[boardSlug]/actions";
9 |
10 | interface DescriptionWysiwygProps {
11 | onReturn: () => void;
12 | }
13 |
14 | const DescriptionWysiwyg: FC = ({ onReturn }) => {
15 | const { card, revalidateCard } = useCardContext();
16 | const [isSubmitting, startTransition] = useTransition();
17 | const [content, setContent] = useState(card.description ?? "");
18 |
19 | function handleSubmit(e: FormEvent) {
20 | e.preventDefault();
21 | startTransition(async () => {
22 | await updateCard(card.id, { description: content });
23 | await revalidateCard();
24 | onReturn();
25 | });
26 | }
27 |
28 | return (
29 |
41 | );
42 | };
43 |
44 | export default DescriptionWysiwyg;
45 |
--------------------------------------------------------------------------------
/src/app/(dashboard)/[username]/[boardSlug]/@card/(.)cards/[cardId]/_components/dialog-wrapper.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { FC, PropsWithChildren } from "react";
4 | import { Dialog, DialogContent } from "@/components/ui/dialog";
5 |
6 | interface DialogWrapperProps extends PropsWithChildren {}
7 |
8 | const DialogWrapper: FC = ({ children }) => {
9 | return (
10 |
18 | );
19 | };
20 |
21 | export default DialogWrapper;
22 |
--------------------------------------------------------------------------------
/src/app/(dashboard)/[username]/[boardSlug]/@card/(.)cards/[cardId]/_components/header/card-header.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { revalidatePath } from "next/cache";
4 | import { useRouter } from "next/navigation";
5 | import { useCardContext } from "@/context/card-context";
6 | import { X } from "lucide-react";
7 | import { Button } from "@/components/ui/button";
8 | import {
9 | DialogDescription,
10 | DialogHeader,
11 | DialogTitle,
12 | } from "@/components/ui/dialog";
13 | import EditableTitle from "@/components/editable-title";
14 | import { updateCard } from "@/app/(dashboard)/[username]/[boardSlug]/actions";
15 |
16 | const CardHeader = () => {
17 | const router = useRouter();
18 | const { card, board } = useCardContext();
19 |
20 | return (
21 |
22 |
23 |
24 | updateCard(card.id, { title })}
27 | className="w-ful text-3xl font-semibold hover:bg-transparent"
28 | />
29 |
30 |
31 |
42 |
43 |
44 |
45 | in list {card.list.title}
46 |
47 |
48 | );
49 | };
50 |
51 | export default CardHeader;
52 |
--------------------------------------------------------------------------------
/src/app/(dashboard)/[username]/[boardSlug]/@card/(.)cards/[cardId]/_components/labels/label-list.tsx:
--------------------------------------------------------------------------------
1 | import { FC, useMemo, useState } from "react";
2 | import { useCardContext } from "@/context/card-context";
3 | import { Label } from "@prisma/client";
4 | import { PencilIcon } from "lucide-react";
5 | import { cn, getTextColor } from "@/lib/utils";
6 | import { Button } from "@/components/ui/button";
7 | import { Checkbox } from "@/components/ui/checkbox";
8 | import {
9 | DropdownMenuContent,
10 | DropdownMenuLabel,
11 | DropdownMenuSeparator,
12 | } from "@/components/ui/dropdown-menu";
13 | import { Input } from "@/components/ui/input";
14 |
15 | interface LabelListProps {
16 | onCreateBtnClick: () => void;
17 | onEditBtnClick: (label: Label) => void;
18 | }
19 |
20 | const LabelList: FC = ({
21 | onCreateBtnClick,
22 | onEditBtnClick,
23 | }) => {
24 | const { board, card, toggleLabel } = useCardContext();
25 | const cardLabelIds = card.labels.map((l) => l.id);
26 |
27 | const [searchTerm, setSearchTerm] = useState("");
28 | const boardLabels = useMemo(() => {
29 | if (!searchTerm.trim()) return board.labels;
30 |
31 | return board.labels.filter((x) =>
32 | x.title.toLowerCase().includes(searchTerm.toLowerCase())
33 | );
34 | }, [board.labels, searchTerm]);
35 |
36 | return (
37 |
38 | Labels
39 |
40 |
41 |
42 |
setSearchTerm(e.target.value)}
45 | placeholder="Search labels..."
46 | />
47 |
48 | {boardLabels.map((label) => (
49 |
50 |
toggleLabel(label, !e)}
55 | />
56 |
66 |
73 |
74 | ))}
75 |
76 |
83 |
84 |
85 | );
86 | };
87 |
88 | export default LabelList;
89 |
--------------------------------------------------------------------------------
/src/app/(dashboard)/[username]/[boardSlug]/@card/(.)cards/[cardId]/_components/labels/labels.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 | import { useCardContext } from "@/context/card-context";
5 | import { Label } from "@prisma/client";
6 | import { Plus } from "lucide-react";
7 | import { getTextColor } from "@/lib/utils";
8 | import { Badge } from "@/components/ui/badge";
9 | import { Button } from "@/components/ui/button";
10 | import {
11 | DropdownMenu,
12 | DropdownMenuTrigger,
13 | } from "@/components/ui/dropdown-menu";
14 | import LabelForm from "@/app/(dashboard)/[username]/[boardSlug]/@card/(.)cards/[cardId]/_components/labels/label-form";
15 | import LabelList from "@/app/(dashboard)/[username]/[boardSlug]/@card/(.)cards/[cardId]/_components/labels/label-list";
16 |
17 | const Labels = () => {
18 | const { card, permission } = useCardContext();
19 | const [activeDropdown, setActiveDropdown] = useState<"FORM" | "LIST">("LIST");
20 | const [activeLabel, setActiveLabel] = useState