├── .env.example ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug.yml │ ├── config.yml │ ├── documentation.yml │ ├── feature-request.yml │ ├── gardening.yml │ ├── proposal.yml │ └── refactor.yml └── PULL_REQUEST_TEMPLATE │ ├── bug.md │ ├── documentation.md │ ├── feature.md │ ├── gardening.md │ ├── proposal.md │ └── refactor.md ├── .gitignore ├── .husky └── pre-push ├── .vscode ├── es-2069.code-snippets ├── launch.json ├── react.code-snippets └── settings.json ├── README.md ├── auto-imports.d.ts ├── biome.json ├── bun.lockb ├── components.json ├── modules ├── ai │ ├── README.md │ ├── ai-audio │ │ └── .gitkeep │ ├── ai-chat │ │ └── .gitkeep │ ├── ai-model │ │ ├── domain │ │ │ └── entities │ │ │ │ └── ai-model.entity.ts │ │ └── infras │ │ │ ├── http │ │ │ └── .gitkeep │ │ │ └── persistence │ │ │ ├── gemini │ │ │ └── .gitkeep │ │ │ └── mistral │ │ │ └── .gitkeep │ ├── fine-tunning │ │ └── .gitkeep │ └── image-generation │ │ └── .gitkeep ├── assistant │ ├── README.md │ ├── ai-message │ │ └── .gitkeep │ ├── ai-runner │ │ └── .gitkeep │ ├── assistant │ │ └── .gitkeep │ └── thread │ │ └── .gitkeep ├── core │ ├── __test__ │ │ └── helpers │ │ │ └── ids.test.ts │ ├── application-base │ │ ├── controllers │ │ │ └── .gitkeep │ │ ├── event-handler │ │ │ └── application-event.handler.ts │ │ └── services │ │ │ └── .gitkeep │ ├── domain-base │ │ ├── entities │ │ │ ├── aggregate.base.ts │ │ │ ├── entity.base.ts │ │ │ ├── unique-entity.ts │ │ │ └── value-object.base.ts │ │ ├── events │ │ │ ├── domain-event.base.ts │ │ │ ├── domain-event.helper.ts │ │ │ └── domain-event.types.ts │ │ ├── repo │ │ │ └── repository.port.ts │ │ ├── security │ │ │ └── .gitkeep │ │ └── use-cases.port.base.ts │ ├── exceptions │ │ ├── exception.base.ts │ │ ├── exception.codes.ts │ │ └── exceptions.ts │ ├── helpers │ │ ├── ids.ts │ │ └── object.ts │ └── infra-base │ │ ├── http │ │ └── middleware │ │ │ ├── error-handler.middleware.ts │ │ │ └── rate-limit.middleware.ts │ │ ├── logger │ │ └── logger.port.ts │ │ ├── mapper.base.ts │ │ ├── persistence │ │ ├── migrations │ │ │ └── plannet-scale.utils.migration.ts │ │ └── repo │ │ │ ├── repository.mysql.base.ts │ │ │ └── repository.query.base.ts │ │ ├── schedule │ │ └── .gitkeep │ │ ├── translation │ │ └── .gitkeep │ │ └── workers │ │ └── .gitkeep ├── document │ └── .gitkeep ├── field │ └── .gitkeep ├── file │ ├── application │ │ └── services │ │ │ └── minio │ │ │ └── .gitkeep │ ├── domain │ │ ├── entities │ │ │ └── file.entity.ts │ │ └── use-cases │ │ │ └── port │ │ │ └── upload-file.in-port.ts │ ├── helpers │ │ └── file.types.ts │ └── infras │ │ └── persistence │ │ └── .gitkeep ├── folder │ └── .gitkeep ├── hyerachy │ └── .gitkeep ├── onboarding │ └── README.md ├── preferences │ ├── README.md │ ├── ip │ │ └── .gitkeep │ └── user-setting │ │ └── .gitkeep ├── space │ └── .gitkeep ├── task │ ├── README.md │ ├── application │ │ └── controllers │ │ │ ├── task.controller.ts │ │ │ └── task.message.controller.ts │ ├── tag │ │ └── .gitkeep │ └── task │ │ └── .gitkeep ├── team │ ├── README.md │ ├── member │ │ └── .gitkeep │ ├── role │ │ └── .gitkeep │ └── team │ │ └── .gitkeep ├── user │ ├── README.md │ ├── invite │ │ └── .gitkeep │ └── user │ │ ├── __test__ │ │ ├── e2e │ │ │ └── user │ │ │ │ └── features │ │ │ │ ├── create-user.feature │ │ │ │ └── user-login.feature │ │ ├── mock │ │ │ └── .gitkeep │ │ └── perf │ │ │ ├── task │ │ │ └── create-task.artillery.yml │ │ │ └── user │ │ │ └── create-user.artillery.yml │ │ ├── application │ │ ├── controllers │ │ │ └── user.controller.ts │ │ ├── dtos │ │ │ ├── create-user.dto.ts │ │ │ └── login.dto.ts │ │ └── services │ │ │ ├── clerk │ │ │ └── clerk.service.ts │ │ │ └── nextjs │ │ │ └── .gitkeep │ │ ├── domain │ │ ├── entities │ │ │ ├── user.entity.ts │ │ │ └── user.types.ts │ │ ├── events │ │ │ ├── user-created.event.ts │ │ │ ├── user-deleted.event.ts │ │ │ ├── user-provider-changed.event.ts │ │ │ ├── user-role-changed.event.ts │ │ │ ├── user-status-changed.event.ts │ │ │ └── user-updated.event.ts │ │ ├── repo │ │ │ ├── user.model.ts │ │ │ └── user.repository.ts │ │ ├── use-cases │ │ │ ├── interactors │ │ │ │ ├── create-user.interactor.ts │ │ │ │ └── login.interactor.ts │ │ │ └── port │ │ │ │ ├── create-user.in-port.ts │ │ │ │ ├── create-user.out-port.ts │ │ │ │ ├── delete-user.in-port.ts │ │ │ │ ├── find-user-by-id.out-port.ts │ │ │ │ ├── find-user-by-nickname.out-port.ts │ │ │ │ ├── get-current-user.out-port.ts │ │ │ │ ├── login-email-password.in-port.ts │ │ │ │ └── login-email-password.out-port.ts │ │ ├── user.exceptions.ts │ │ └── value-objects │ │ │ ├── user-metadata.value-object.ts │ │ │ ├── user-password.value-object.ts │ │ │ └── user-providers.value-object.ts │ │ └── infras │ │ ├── graphql │ │ └── .gitkeep │ │ ├── http │ │ ├── middleware │ │ │ └── auth.middleware.ts │ │ └── v1 │ │ │ └── auth │ │ │ ├── .gitkeep │ │ │ └── auth.router.ts │ │ ├── mappers │ │ └── user.mapper.ts │ │ ├── persistence │ │ ├── kafka │ │ │ └── .gitkeep │ │ ├── memcache │ │ │ └── .gitkeep │ │ ├── plannet-scale │ │ │ ├── migrations │ │ │ │ └── 2023-12-20T02:55:13.184Z-init-sql.ts │ │ │ ├── plannet-scale.config.ts │ │ │ └── user.impl.reposity.ts │ │ └── redis │ │ │ └── .gitkeep │ │ └── securities │ │ └── .gitkeep ├── vilolation-content │ └── .gitkeep └── whiteboard │ └── .gitkeep ├── next.config.mjs ├── package.json ├── postcss.config.cjs ├── public ├── images │ ├── CleanArchitecture.png │ ├── QRCode.jpg │ ├── Thumbnail.png │ ├── clickup-landing │ │ ├── after-1.svg │ │ ├── after-2.svg │ │ ├── after.png │ │ ├── before-1.svg │ │ ├── before-2.png │ │ ├── before-3.svg │ │ ├── before-4.svg │ │ ├── before.svg │ │ ├── bring-teams-and-work-together.png │ │ ├── button-1.svg │ │ ├── button-10.svg │ │ ├── button-2.svg │ │ ├── button-3.svg │ │ ├── button-4.svg │ │ ├── button-5.svg │ │ ├── button-6.svg │ │ ├── button-7.svg │ │ ├── button-8.svg │ │ ├── button-9.svg │ │ ├── button-open-messenger.svg │ │ ├── button-svg-1.svg │ │ ├── button-svg-2.svg │ │ ├── button-svg.svg │ │ ├── button.svg │ │ ├── convene-headshot-png.png │ │ ├── convene-png.png │ │ ├── div-1.svg │ │ ├── div-2.svg │ │ ├── div-3.svg │ │ ├── div-4.svg │ │ ├── div-cuhomecollapse-collapsecontentimagebox-arrpv.png │ │ ├── div-cuhometeamstab-tabcardfeatureiconwrapper-rqvjb-mask-group-1.svg │ │ ├── div-cuhometeamstab-tabcardfeatureiconwrapper-rqvjb-mask-group-2.svg │ │ ├── div-cuhometeamstab-tabcardfeatureiconwrapper-rqvjb-mask-group-3.svg │ │ ├── div-cuhometeamstab-tabcardfeatureiconwrapper-rqvjb-mask-group.svg │ │ ├── div-cuhometesteverythingyourteamislookingfor-cardfeaturelistswit.svg │ │ ├── div-cuhometestteamsloveclickup-content-3wx7m-mask-group-1.svg │ │ ├── div-cuhometestteamsloveclickup-content-3wx7m-mask-group-2.svg │ │ ├── div-cuhometestteamsloveclickup-content-3wx7m-mask-group.svg │ │ ├── div-culandingpagefooter-background-8d6uv-mask-group.svg │ │ ├── div-row.svg │ │ ├── div.svg │ │ ├── everything-your-team-is-looking-for.png │ │ ├── finastra-png.png │ │ ├── heading-2-all-teams-love-clickup.png │ │ ├── heading-2-perfect-fit-for-every-team.png │ │ ├── item-svg-1.svg │ │ ├── item-svg-2.svg │ │ ├── item-svg-3.svg │ │ ├── item-svg.svg │ │ ├── link-picture-app-store-badge-white-png.png │ │ ├── link-picture-google-play-badge-white-png.png │ │ ├── link-svg.svg │ │ ├── link.svg │ │ ├── list-item-1.svg │ │ ├── list-item-2.svg │ │ ├── list-item.svg │ │ ├── main-1.png │ │ ├── main-2.svg │ │ ├── main-3.png │ │ ├── main-4.svg │ │ ├── main-5.svg │ │ ├── main-link-picture-v3-badge-link-black-png.png │ │ ├── main-svg-1.svg │ │ ├── main-svg-10.svg │ │ ├── main-svg-11.svg │ │ ├── main-svg-12.svg │ │ ├── main-svg-2.svg │ │ ├── main-svg-3.svg │ │ ├── main-svg-4.svg │ │ ├── main-svg-5.svg │ │ ├── main-svg-6.svg │ │ ├── main-svg-7.svg │ │ ├── main-svg-8.svg │ │ ├── main-svg-9.svg │ │ ├── main-svg.svg │ │ ├── main-users-love-us-png.png │ │ ├── main.png │ │ ├── one-app-to-replace-them-all.png │ │ ├── picture-ai-powered-productivity-png.png │ │ ├── picture-clickup-integration-replaces-svg.svg │ │ ├── picture-clickup-integrations-svg.svg │ │ ├── picture-clickup-logo-3-0-svg.svg │ │ ├── picture-collaborate-png.png │ │ ├── picture-logo-svg.svg │ │ ├── picture-modal-close-png.png │ │ ├── picture-projects-png.png │ │ ├── picture-projects-sm-png.png │ │ ├── picture-security-badge-white-svg.svg │ │ ├── picture-support-badge-white-svg.svg │ │ ├── picture-uptime-badge-white-svg.svg │ │ ├── pressed-png.png │ │ ├── ready-to-unleash-your.png │ │ ├── save-time-and-get-more-done.png │ │ ├── search-everything-png.png │ │ ├── stay-ahead-png.png │ │ ├── svg-1.svg │ │ ├── svg-2.svg │ │ ├── svg-3.svg │ │ ├── svg-4.svg │ │ ├── svg-5.svg │ │ ├── svg.svg │ │ ├── team-s-full-potential.png │ │ └── view-work-your-way-png.png │ └── dashboard │ │ └── background-dark.png ├── next.svg └── vercel.svg ├── scripts ├── generateMigrateFile.mjs └── migrate-db.mjs ├── src ├── app │ ├── (auth) │ │ ├── create-workspace │ │ │ └── [[...create-workspace]] │ │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── login │ │ │ └── [[...login]] │ │ │ │ └── page.tsx │ │ └── signup │ │ │ └── [[...signup]] │ │ │ └── page.tsx │ ├── (dashboard) │ │ ├── components │ │ │ ├── ButtonAI.tsx │ │ │ ├── ButtonActionNew.tsx │ │ │ ├── ButtonQuickActionMenu.tsx │ │ │ ├── ButtonUserMenu.tsx │ │ │ ├── DashboardAside │ │ │ │ ├── AsideFavorites.tsx │ │ │ │ ├── AsideNavigateMenus.tsx │ │ │ │ ├── AsideSpaces.tsx │ │ │ │ ├── AsideSupports.tsx │ │ │ │ └── AsideWorkspaceSelection.tsx │ │ │ ├── DashboardHeader.tsx │ │ │ ├── DashboardMain.tsx │ │ │ ├── DashboardSidebar.tsx │ │ │ ├── MainTopBar │ │ │ │ ├── ButtonToggleSidebar.tsx │ │ │ │ └── index.tsx │ │ │ └── SearchCommandPalette │ │ │ │ ├── CommandMenu.tsx │ │ │ │ └── index.tsx │ │ ├── layout.tsx │ │ ├── settings │ │ │ └── billling │ │ │ │ └── page.tsx │ │ └── w │ │ │ └── [organizationId] │ │ │ ├── dashboard │ │ │ └── page.tsx │ │ │ ├── docs │ │ │ └── page.tsx │ │ │ ├── goals │ │ │ └── page.tsx │ │ │ ├── home │ │ │ └── page.tsx │ │ │ ├── inbox │ │ │ └── page.tsx │ │ │ └── page.tsx │ ├── (marketing) │ │ ├── components │ │ │ ├── HomeLoginButton.tsx │ │ │ ├── HomeSignUpButton.tsx │ │ │ └── LandingPageTikup.tsx │ │ └── page.tsx │ ├── api │ │ └── v1 │ │ │ └── auth.ts │ ├── favicon.ico │ ├── globals.css │ └── layout.tsx ├── middleware.ts └── shared │ ├── components │ ├── accordion.tsx │ ├── button.tsx │ ├── calendar.tsx │ ├── checkbox.tsx │ ├── collapsible.tsx │ ├── command.tsx │ ├── dialog.tsx │ ├── dropdown-menu.tsx │ ├── input.tsx │ ├── menubar.tsx │ ├── popover.tsx │ ├── scroll-area.tsx │ ├── select.tsx │ ├── tabs.tsx │ └── tooltip.tsx │ ├── hooks │ └── usePreferences.ts │ └── utils │ └── string.ts ├── tailwind.config.ts ├── test └── architecture │ ├── application.test.ts │ ├── arch.setup.ts │ ├── domain.test.ts │ └── infrastructure.test.ts ├── tsconfig.json ├── typings └── event.d.ts └── vitest.config.ts /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY= 2 | 3 | CLERK_SECRET_KEY= 4 | IP_STACK_KEY= -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | open_collective: techmely 2 | patreon: techmely 3 | ko_fi: techmely -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | title: "[BUG]: " 4 | labels: ["bug"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill out this bug report! 10 | - type: textarea 11 | id: what-happened 12 | attributes: 13 | label: What happened? 14 | description: What did you do? What happened? What did you expect to happen? 15 | placeholder: Put your description of the bug here. 16 | validations: 17 | required: true 18 | - type: textarea 19 | id: user-info 20 | attributes: 21 | label: User Info 22 | description: | 23 | Please fill the following information to help us investigate the bug more easier. 24 | - User ID: 01122121212 25 | - Please goto this page to get check your network information and paste it here 26 | - https://ping.techmely.com 27 | render: Shell -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | name: Sample name 2 | description: Sample description 3 | blank_issues_enabled: true 4 | contact_links: 5 | - name: Discord Chat 6 | url: https://chat.techmely.com 7 | about: Ask questions and discuss with Techmely's team in real time. 8 | - name: Questions & Discussions 9 | url: https://github.com/harrytran998/techmely/discussions 10 | about: Use GitHub discussions for message-board style questions and discussions. 11 | - name: Donate 12 | url: https://opencollective.com/techmely/donate 13 | about: Love Techmely product? Please consider supporting us via Open Collective. 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation.yml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | description: Update or add documentation 3 | title: "[DOCS]: " 4 | labels: ["documentation"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill this out! 10 | - type: textarea 11 | id: describe-need 12 | attributes: 13 | label: Describe the need 14 | description: What do you wish was different about our docs? 15 | placeholder: Describe the need for documentation updates here. 16 | validations: 17 | required: true 18 | - type: checkboxes 19 | id: terms 20 | attributes: 21 | label: Code of Conduct 22 | description: By submitting this issue, you agree to follow our [Code of Conduct](../../docs/CODE_OF_CONDUCT.md) 23 | options: 24 | - label: I agree to follow this project's Code of Conduct 25 | required: true -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | name: 🦄 Feature Request 2 | description: Suggest an idea for a new feature or enhancement 3 | title: "[FEAT]: " 4 | labels: ["feature"] 5 | assignees: ["techmely"] 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | Thanks for taking the time to complete this feature! 11 | Please carefully read the contribution guidelines before request a review from others. 12 | https://github.com/techmely/essential-packages/blob/main/docs/contribution.md#create-feature-request 13 | - type: textarea 14 | id: describe-feature 15 | attributes: 16 | label: What does this MR do and why? 17 | description: What do you want to happen? What problem are you trying to solve? 18 | placeholder: Describe the need for the feature. 19 | validations: 20 | required: true 21 | - type: textarea 22 | id: resources 23 | attributes: 24 | label: The resources that you have researched 25 | description: To solve this problem, what's resources we need to access? 26 | 27 | - type: checkboxes 28 | id: Acceptance-Criteria 29 | attributes: 30 | label: Acceptance Criteria Checklist 31 | description: The standard criteria that a feature must satisfy to be accepted 32 | options: 33 | - label: Read the [contribution guide](https://github.com/techmely/essential-packages/blob/main/docs/contribution.md) 34 | - label: Check existing [discussions](https://github.com/techmely/essential-packages/discussions) and [issues](https://github.com/techmely/essential-packages/issues). 35 | required: true 36 | - label: The scope of the feature is clear and small enough to be implemented in a single MR - Not exceeding 500 lines of code and 40 files changed. 37 | required: true 38 | - label: The time doing this feature does not exceed 1 day. 39 | required: true 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/gardening.yml: -------------------------------------------------------------------------------- 1 | name: Gardening 2 | description: Dependencies, cleanup, reworking of code 3 | title: "[Garden 🍌🥦🍏]: " 4 | labels: ["Type: gardening", "Status: Triage"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill this out! 10 | - type: textarea 11 | id: describe-need 12 | attributes: 13 | label: Describe the need 14 | description: What do you want to happen? 15 | placeholder: Describe the gardening need here. 16 | validations: 17 | required: true 18 | - type: checkboxes 19 | id: terms 20 | attributes: 21 | label: Code of Conduct 22 | description: By submitting this issue, you agree to follow our [Code of Conduct](../../docs/CODE_OF_CONDUCT.md) 23 | options: 24 | - label: I agree to follow this project's Code of Conduct 25 | required: true -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/proposal.yml: -------------------------------------------------------------------------------- 1 | name: Proposal 📜 2 | description: Propose a new feature, or a change to something existing 3 | title: "[Proposal]: " 4 | labels: ["proposal 📜"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill this out! -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/refactor.yml: -------------------------------------------------------------------------------- 1 | name: Refactor 2 | description: Refactoring code 3 | title: "[Refactor]: " 4 | labels: ["refactor"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill this out! 10 | - type: textarea 11 | id: describe-need 12 | attributes: 13 | label: Describe the need 14 | description: What do you want to happen? 15 | placeholder: Describe the Refactor need here. 16 | validations: 17 | required: true 18 | - type: checkboxes 19 | id: terms 20 | attributes: 21 | label: Code of Conduct 22 | description: By submitting this issue, you agree to follow our [Code of Conduct](../../docs/CODE_OF_CONDUCT.md) 23 | options: 24 | - label: I agree to follow this project's Code of Conduct 25 | required: true -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | # Bug pull request template 2 | 3 | 4 | 5 | ## Checklist 6 | 7 | 8 | - [ ] Bug fix (non-breaking change which fixes an issue) 9 | - [ ] New feature (non-breaking change which adds functionality) 10 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 11 | - [ ] I have read the **CONTRIBUTING** document. 12 | - [ ] My code follows the code style of this project. 13 | - [ ] My change requires a change to the documentation. 14 | - [ ] I have updated the documentation accordingly. 15 | - [ ] I have added tests to cover my changes. 16 | - [ ] All new and existing tests passed. 17 | 18 | ## Description 19 | 20 | 21 | ## Related Issue 22 | 23 | 24 | 25 | 26 | 27 | ## Motivation and Context 28 | 29 | 30 | 31 | ## How Has This Been Tested? 32 | 33 | 34 | 35 | 36 | ## Screenshots (if appropriate) 37 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/documentation.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/.github/PULL_REQUEST_TEMPLATE/documentation.md -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | # Feature merge request template 2 | 3 | ## What does this MR do and why? 4 | 5 | What do you want to happen? What problem are you trying to solve? 6 | 7 | ## How we setup and verify this pull request on localhost? 8 | Please tell us how to setup and verify this pull request on localhost. This will lead to a faster review and merge PR. 9 | 10 | ## MR acceptance checklist 11 | The standard criteria that a feature must satisfy to be accepted 12 | 13 | 14 | ### Semantic 15 | 16 | **Required** 17 | 18 | - [ ] I have linked an issue or discussion 19 | - [ ] MR follows the basic team's coding conventions. 20 | - [ ] This PR follow [the coding conventions](../../docs/coding-convention.md) of this project 21 | - [ ] This PR resolve all comments of the reviewers and have at least 1 approval 22 | - [ ] Files change do not exceed 40 files(Normally should be 20~30 files). 23 | 24 | *Optional* 25 | - [ ] I have updated the documentation accordingly 26 | 27 | ### Testing 28 | 29 | **Required** 30 | - [ ] Pass all pipeline 31 | - [ ] All logic functions must have the unit tests guarantee the code coverage of obtaining at least 80% 32 | - [ ] MR should not reduce the entire project's test coverage. 33 | 34 | *Optional* 35 | 36 | - [ ] UI tests must guarantee the code coverage of obtaining at least 40%(include all fields, exact words, animations and behaviors...) 37 | 38 | ## The way we review code together 39 | 40 | Follow theo [Code Review Guidelines](https://www.pluralsight.com/blog/software-development/code-review-checklist) 41 | 42 | /closes # 43 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/gardening.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/.github/PULL_REQUEST_TEMPLATE/gardening.md -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/proposal.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/.github/PULL_REQUEST_TEMPLATE/proposal.md -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/refactor.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/.github/PULL_REQUEST_TEMPLATE/refactor.md -------------------------------------------------------------------------------- /.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 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 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 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | .yarn 39 | .env -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | bun run lint 5 | bun run lint.perf 6 | -------------------------------------------------------------------------------- /.vscode/es-2069.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "Import type": { 3 | "prefix": "impt", 4 | "body": ["import type { $2 } from '$1'"], 5 | "description": "Import type" 6 | }, 7 | "Console log": { 8 | "prefix": "clg", 9 | "body": ["console.log($1)"], 10 | "description": "Console log" 11 | }, 12 | "Console error": { 13 | "prefix": "cle", 14 | "body": ["console.error($1)"], 15 | "description": "Console error" 16 | }, 17 | "Console Count": { 18 | "prefix": "clc", 19 | "body": ["console.count($1)"], 20 | "description": "Console Count" 21 | }, 22 | "Export * from XXX": { 23 | "prefix": "exs", 24 | "body": ["export * from './${0}';"] 25 | }, 26 | "Export * from XXX + Type": { 27 | "prefix": "exst", 28 | "body": ["export * from './${0}';", "export * from './${0}.types';"] 29 | }, 30 | "Export custom hooks": { 31 | "prefix": "exch", 32 | "body": ["export function ${1:${TM_FILENAME_BASE}}() {", " $2", "}"], 33 | "description": "export custom hooks" 34 | } 35 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Next.js: debug server-side", 6 | "type": "node-terminal", 7 | "request": "launch", 8 | "command": "yarn dev" 9 | }, 10 | { 11 | "name": "Next.js: debug client-side", 12 | "type": "chrome", 13 | "request": "launch", 14 | "url": "http://localhost:3000" 15 | }, 16 | { 17 | "name": "Next.js: debug full stack", 18 | "type": "node-terminal", 19 | "request": "launch", 20 | "command": "yarn dev", 21 | "serverReadyAction": { 22 | "pattern": "started server on .+, url: (https?://.+)", 23 | "uriFormat": "%s", 24 | "action": "debugWithChrome" 25 | } 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "biomejs.biome", 3 | "[javascript]": { 4 | "editor.defaultFormatter": "biomejs.biome" 5 | }, 6 | "[typescript]": { 7 | "editor.defaultFormatter": "biomejs.biome" 8 | }, 9 | "[typescriptreact]": { 10 | "editor.defaultFormatter": "biomejs.biome" 11 | }, 12 | "editor.formatOnSave": true, 13 | "editor.fontLigatures": true, 14 | "editor.formatOnPaste": false, 15 | "cSpell.words": [ 16 | "animatejs", 17 | "biomejs", 18 | "birthdate", 19 | "cmdk", 20 | "consola", 21 | "dtos", 22 | "gsap", 23 | "infras", 24 | "Interactor", 25 | "interactors", 26 | "inversify", 27 | "knip", 28 | "kysely", 29 | "langchain", 30 | "lucide", 31 | "millionjs", 32 | "nextjs", 33 | "openai", 34 | "oxlint", 35 | "partytown", 36 | "pixelmatch", 37 | "planetscale", 38 | "pngjs", 39 | "ratelimit", 40 | "speedscope", 41 | "tasuku", 42 | "tauri", 43 | "tickup", 44 | "tsarch", 45 | "unplugin", 46 | "upstash", 47 | "valibot", 48 | "wasmer", 49 | "zustand" 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", 3 | "vcs": { 4 | "enabled": true, 5 | "clientKind": "git" 6 | }, 7 | "files": { 8 | "ignore": [".next", ".yarn", "node_modules", "public", "./auto-imports.d.ts"] 9 | }, 10 | "linter": { 11 | "enabled": true, 12 | "rules": { 13 | "recommended": true, 14 | "a11y": { 15 | "noSvgWithoutTitle": "off" 16 | }, 17 | "suspicious": { 18 | "noExplicitAny": "off", 19 | "noEmptyInterface": "off" 20 | } 21 | } 22 | }, 23 | "formatter": { 24 | "enabled": true, 25 | "indentWidth": 2, 26 | "lineWidth": 100, 27 | "indentStyle": "space", 28 | "formatWithErrors": false 29 | }, 30 | "organizeImports": { 31 | "enabled": true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/bun.lockb -------------------------------------------------------------------------------- /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.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "@/shared/components", 14 | "utils": "@/shared/utils/string" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /modules/ai/README.md: -------------------------------------------------------------------------------- 1 | # AI Aggregate 2 | 3 | AI Model is the aggregate root. -------------------------------------------------------------------------------- /modules/ai/ai-audio/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/ai/ai-audio/.gitkeep -------------------------------------------------------------------------------- /modules/ai/ai-chat/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/ai/ai-chat/.gitkeep -------------------------------------------------------------------------------- /modules/ai/ai-model/domain/entities/ai-model.entity.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/ai/ai-model/domain/entities/ai-model.entity.ts -------------------------------------------------------------------------------- /modules/ai/ai-model/infras/http/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/ai/ai-model/infras/http/.gitkeep -------------------------------------------------------------------------------- /modules/ai/ai-model/infras/persistence/gemini/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/ai/ai-model/infras/persistence/gemini/.gitkeep -------------------------------------------------------------------------------- /modules/ai/ai-model/infras/persistence/mistral/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/ai/ai-model/infras/persistence/mistral/.gitkeep -------------------------------------------------------------------------------- /modules/ai/fine-tunning/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/ai/fine-tunning/.gitkeep -------------------------------------------------------------------------------- /modules/ai/image-generation/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/ai/image-generation/.gitkeep -------------------------------------------------------------------------------- /modules/assistant/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/assistant/README.md -------------------------------------------------------------------------------- /modules/assistant/ai-message/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/assistant/ai-message/.gitkeep -------------------------------------------------------------------------------- /modules/assistant/ai-runner/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/assistant/ai-runner/.gitkeep -------------------------------------------------------------------------------- /modules/assistant/assistant/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/assistant/assistant/.gitkeep -------------------------------------------------------------------------------- /modules/assistant/thread/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/assistant/thread/.gitkeep -------------------------------------------------------------------------------- /modules/core/__test__/helpers/ids.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | 3 | import { DEFAULT_PREFIX_ID_LENGTH, generatePrefixId, generateUserId } from "../../helpers/ids"; 4 | 5 | describe("Test generate Prefix ID", () => { 6 | it("Should generate generic entity id", () => { 7 | const entityLength = DEFAULT_PREFIX_ID_LENGTH + 6; 8 | expect(generatePrefixId("entity")).toHaveLength(entityLength); 9 | }); 10 | it("Should generate user entity id", () => { 11 | const entityLength = DEFAULT_PREFIX_ID_LENGTH + 1; 12 | expect(generateUserId()).toHaveLength(entityLength); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /modules/core/application-base/controllers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/core/application-base/controllers/.gitkeep -------------------------------------------------------------------------------- /modules/core/application-base/event-handler/application-event.handler.ts: -------------------------------------------------------------------------------- 1 | import type { DomainEvent } from "../../domain-base/events/domain-event.base"; 2 | 3 | export interface ApplicationEventHandler { 4 | handle(domainEvent: IDomainEvent): Promise | Promise; 5 | } 6 | -------------------------------------------------------------------------------- /modules/core/application-base/services/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/core/application-base/services/.gitkeep -------------------------------------------------------------------------------- /modules/core/domain-base/entities/aggregate.base.ts: -------------------------------------------------------------------------------- 1 | import type { Emitter } from "mitt"; 2 | import type { LoggerPort } from "modules/core/infra-base/logger/logger.port"; 3 | import { DomainEvent } from "../events/domain-event.base"; 4 | import type { EmitDomainEvents } from "../events/domain-event.types"; 5 | import { Entity } from "./entity.base"; 6 | 7 | export abstract class AggregateRoot extends Entity { 8 | #domainEvents: DomainEvent[] = []; 9 | 10 | get domainEvents() { 11 | return this.#domainEvents; 12 | } 13 | 14 | set domainEvents(domainEvents: DomainEvent[]) { 15 | this.#domainEvents = domainEvents; 16 | } 17 | 18 | protected addEvent(domainEvent: DomainEvent | DomainEvent[]): void { 19 | if (Array.isArray(domainEvent)) { 20 | this.domainEvents = [...this.domainEvents, ...domainEvent]; 21 | } else { 22 | this.domainEvents.push(domainEvent); 23 | } 24 | } 25 | 26 | clearEvents(): void { 27 | this.domainEvents = []; 28 | } 29 | 30 | async publishEvents(logger: LoggerPort, emitter: Emitter) { 31 | const promiseEvents = this.domainEvents.map((event) => { 32 | logger.debug( 33 | `[RequestID] "${event.constructor.name}" event published for aggregate ${this.constructor.name} : ${this.id}`, 34 | ); 35 | return emitter.emitAsync(event.constructor.name, event); 36 | }); 37 | for await (const event of promiseEvents) { 38 | event; 39 | } 40 | this.clearEvents(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /modules/core/domain-base/entities/unique-entity.ts: -------------------------------------------------------------------------------- 1 | import type { EntityId } from "@techmely/types"; 2 | import { generatePrefixId } from "../../helpers/ids"; 3 | 4 | export class UniqueEntityID { 5 | protected readonly id: EntityId; 6 | 7 | constructor(_id?: EntityId) { 8 | this.id = _id || generatePrefixId("entity"); 9 | } 10 | 11 | equals(_id?: UniqueEntityID): boolean { 12 | if (!_id) { 13 | return false; 14 | } 15 | if (!(_id instanceof this.constructor)) { 16 | return false; 17 | } 18 | return _id.toValue() === this.id; 19 | } 20 | 21 | toString() { 22 | return String(this.id); 23 | } 24 | 25 | toValue(): EntityId { 26 | return this.id; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /modules/core/domain-base/entities/value-object.base.ts: -------------------------------------------------------------------------------- 1 | import { invariant, isEmpty } from "@techmely/utils"; 2 | import { ArgumentNotProvidedException } from "../../exceptions/exceptions"; 3 | import { convertPropsToObject } from "../../helpers/object"; 4 | 5 | /** 6 | * Domain Primitive is an object that contains only a single value 7 | */ 8 | export type Primitives = string | number | boolean; 9 | export interface DomainPrimitive { 10 | value: T; 11 | } 12 | 13 | type ValueObjectProps = T extends Primitives | Date ? DomainPrimitive : T; 14 | 15 | export abstract class ValueObject { 16 | protected readonly props: ValueObjectProps; 17 | 18 | constructor(props: ValueObjectProps) { 19 | this.#validateProps(props); 20 | this.validate(props); 21 | this.props = props; 22 | } 23 | 24 | protected abstract validate(props: ValueObjectProps): void; 25 | static isValueObject(obj: unknown): obj is ValueObject { 26 | return obj instanceof ValueObject; 27 | } 28 | 29 | equals(vo?: ValueObject) { 30 | if (!vo) return false; 31 | return JSON.stringify(this) === JSON.stringify(vo); 32 | } 33 | 34 | /** 35 | * Convert value obj to get raw properties 36 | */ 37 | raw() { 38 | if (this.#isDomainPrimitive(this.props)) { 39 | return this.props.value; 40 | } 41 | const clone = convertPropsToObject(this.props); 42 | return Object.freeze(clone); 43 | } 44 | 45 | #validateProps(props: ValueObjectProps) { 46 | invariant( 47 | !(isEmpty(props) || (this.#isDomainPrimitive(props) && isEmpty(props.value))), 48 | new ArgumentNotProvidedException("Property cannot be empty"), 49 | ); 50 | } 51 | 52 | #isDomainPrimitive(obj: unknown): obj is DomainPrimitive { 53 | if (Object.prototype.hasOwnProperty.call(obj, "value")) return true; 54 | return false; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /modules/core/domain-base/events/domain-event.base.ts: -------------------------------------------------------------------------------- 1 | import { invariant } from "@techmely/utils"; 2 | import { ArgumentNotProvidedException } from "../../exceptions/exceptions"; 3 | import { generatePrefixId } from "../../helpers/ids"; 4 | import { UniqueEntityID } from "../entities/unique-entity"; 5 | 6 | type DomainEventMetadata = { 7 | /** Timestamp when this domain event occurred */ 8 | readonly timestamp: number; 9 | 10 | /** ID for correlation purposes (for Integration Events,logs correlation, etc). 11 | */ 12 | readonly correlationId?: string; 13 | 14 | /** 15 | * Causation id used to reconstruct execution order if needed 16 | */ 17 | readonly causationId?: string; 18 | 19 | /** 20 | * User ID for debugging and logging purposes 21 | */ 22 | readonly userId?: string; 23 | }; 24 | 25 | export type IDomainEvent = Omit & { 26 | _metadata: DomainEventMetadata; 27 | aggregateId: UniqueEntityID; 28 | }; 29 | 30 | export abstract class DomainEvent { 31 | readonly id: UniqueEntityID; 32 | readonly aggregateId: UniqueEntityID; 33 | readonly _metadata: DomainEventMetadata; 34 | 35 | constructor(domainEvent: IDomainEvent) { 36 | invariant( 37 | domainEvent, 38 | new ArgumentNotProvidedException("Domain event props should not be empty"), 39 | ); 40 | invariant( 41 | domainEvent._metadata && !domainEvent._metadata.timestamp, 42 | new ArgumentNotProvidedException("Timestamp should be provided in domain event metadata"), 43 | ); 44 | this.id = new UniqueEntityID(generatePrefixId("de")); 45 | this.aggregateId = domainEvent.aggregateId; 46 | this._metadata = { 47 | correlationId: domainEvent?._metadata?.correlationId, 48 | causationId: domainEvent?._metadata?.causationId, 49 | timestamp: domainEvent?._metadata?.timestamp, 50 | userId: domainEvent?._metadata?.userId, 51 | }; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /modules/core/domain-base/events/domain-event.helper.ts: -------------------------------------------------------------------------------- 1 | import mitt, { type EventHandlerMap } from "mitt"; 2 | import type { EmitDomainEvents } from "./domain-event.types"; 3 | 4 | export function mittAsync(all?: EventHandlerMap) { 5 | const instance = mitt(all); 6 | 7 | instance.emitAsync = async function (type: keyof EmitDomainEvents, e: any) { 8 | const handlersType = this.all.get(type); 9 | // @ts-expect-error Ignore typecheck 10 | if (handlersType) for (const ht of handlersType) await ht(e); 11 | const handlersWildcard = this.all.get("*"); 12 | if (handlersWildcard) for (const hw of handlersWildcard) await hw(type, e); 13 | }; 14 | return instance; 15 | } 16 | -------------------------------------------------------------------------------- /modules/core/domain-base/events/domain-event.types.ts: -------------------------------------------------------------------------------- 1 | export type EmitDomainEvents = { 2 | userCreated: string; 3 | userUpdated: number; 4 | userRoleChanged: number; 5 | userDeleted: number; 6 | }; 7 | -------------------------------------------------------------------------------- /modules/core/domain-base/repo/repository.port.ts: -------------------------------------------------------------------------------- 1 | import type { StringEnum } from "@techmely/types"; 2 | 3 | export class Paginated { 4 | readonly count: number; 5 | readonly limit: number; 6 | readonly page: number; 7 | readonly data: readonly T[]; 8 | 9 | constructor(props: Paginated) { 10 | this.count = props.count; 11 | this.limit = props.limit; 12 | this.page = props.page; 13 | this.data = props.data; 14 | } 15 | } 16 | 17 | export type OrderBy = { field: string | true; param: "asc" | "desc" }; 18 | 19 | export type PaginatedQueryParams = { 20 | limit: number; 21 | page: number; 22 | offset: number; 23 | orderBy: OrderBy; 24 | }; 25 | 26 | export interface RepositoryPort { 27 | findById(id: string): Promise; 28 | findByKey(key: StringEnum): Promise; 29 | findAll(): Promise; 30 | findAllByIds(ids: string[]): Promise; 31 | findAllPaginated(params: PaginatedQueryParams): Promise>; 32 | existsById(id: string): Promise; 33 | count(): Promise; 34 | 35 | insert(entity: Entity): Promise | Promise; 36 | insertBulk(entity: Entity): Promise | Promise; 37 | insertMany(entities: Entity[]): Promise | Promise; 38 | insertBulkMany(entities: Entity[]): Promise | Promise; 39 | 40 | update(entity: Entity): Promise | Promise; 41 | updateBulk(entity: Entity): Promise | Promise; 42 | updateMany(entities: Entity[]): Promise | Promise; 43 | updateBulkMany(entities: Entity[]): Promise | Promise; 44 | 45 | delete(entity: Entity): Promise; 46 | deleteById(id: string): Promise; 47 | deleteAllByIds(ids: string[]): Promise; 48 | deleteBulk(entity: Entity): Promise; 49 | 50 | transaction(handler: () => Promise): Promise; 51 | } 52 | -------------------------------------------------------------------------------- /modules/core/domain-base/security/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/core/domain-base/security/.gitkeep -------------------------------------------------------------------------------- /modules/core/domain-base/use-cases.port.base.ts: -------------------------------------------------------------------------------- 1 | export interface UseCase { 2 | execute(request?: Request): Promise | Response; 3 | } 4 | -------------------------------------------------------------------------------- /modules/core/exceptions/exception.base.ts: -------------------------------------------------------------------------------- 1 | export interface NormalizedException { 2 | message: string; 3 | code: string; 4 | correlationId: string; 5 | stack?: string; 6 | cause?: string; 7 | /** 8 | * ^ Consider adding optional `metadata` object to 9 | * exceptions (if language doesn't support anything 10 | * similar by default) and pass some useful technical 11 | * information about the exception when throwing. 12 | * This will make debugging easier. 13 | */ 14 | metadata?: Record; 15 | } 16 | 17 | export abstract class ExceptionBase extends Error { 18 | abstract code: string; 19 | readonly correlationId: string; 20 | 21 | /** 22 | * @param {string} message 23 | * @param {ObjectLiteral} [metadata={}] 24 | * **BE CAREFUL** not to include sensitive info in 'metadata' 25 | * to prevent leaks since all exception's data will end up 26 | * in application's log files. Only include non-sensitive 27 | * info that may help with debugging. 28 | */ 29 | constructor( 30 | readonly message: string, 31 | cause?: Error, 32 | readonly metadata?: Record, 33 | ) { 34 | super(message); 35 | Error.captureStackTrace(this, this.constructor); 36 | const ctx = { 37 | requestId: "1", 38 | }; 39 | this.correlationId = ctx.requestId; 40 | } 41 | 42 | toJSON(): NormalizedException { 43 | return { 44 | message: this.message, 45 | code: this.code, 46 | stack: this.stack, 47 | correlationId: this.correlationId, 48 | cause: JSON.stringify(this.cause), 49 | metadata: this.metadata, 50 | }; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /modules/core/exceptions/exception.codes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Adding a `code` string with a custom status code for every 3 | * exception is a good practice. 4 | * 5 | * Since when that exception is transferred to another process `instanceof` 6 | * check cannot be performed anymore so a `code` string is used instead. 7 | * 8 | * Code constants can be stored in a separate file so they 9 | * can be shared and reused on a receiving side (code sharing is 10 | * useful when developing fullstack apps or microservices) 11 | */ 12 | export const ARGUMENT_INVALID = "GENERIC.ARGUMENT_INVALID"; 13 | export const ARGUMENT_OUT_OF_RANGE = "GENERIC.ARGUMENT_OUT_OF_RANGE"; 14 | export const ARGUMENT_NOT_PROVIDED = "GENERIC.ARGUMENT_NOT_PROVIDED"; 15 | export const NOT_FOUND = "GENERIC.NOT_FOUND"; 16 | export const CONFLICT = "GENERIC.CONFLICT"; 17 | export const INTERNAL_SERVER_ERROR = "GENERIC.INTERNAL_SERVER_ERROR"; 18 | -------------------------------------------------------------------------------- /modules/core/exceptions/exceptions.ts: -------------------------------------------------------------------------------- 1 | import { ExceptionBase } from "./exception.base"; 2 | import { 3 | ARGUMENT_INVALID, 4 | ARGUMENT_NOT_PROVIDED, 5 | ARGUMENT_OUT_OF_RANGE, 6 | CONFLICT, 7 | INTERNAL_SERVER_ERROR, 8 | NOT_FOUND, 9 | } from "./exception.codes"; 10 | 11 | /** 12 | * Used to indicate that an argument was not provided (is empty object/array, null of undefined). 13 | * 14 | * @class ArgumentNotProvidedException 15 | * @extends {ExceptionBase} 16 | */ 17 | export class ArgumentNotProvidedException extends ExceptionBase { 18 | readonly code = ARGUMENT_NOT_PROVIDED; 19 | } 20 | 21 | /** 22 | * Used to indicate that an incorrect argument was provided to a method/function/class constructor 23 | * 24 | * @class ArgumentInvalidException 25 | * @extends {ExceptionBase} 26 | */ 27 | export class ArgumentInvalidException extends ExceptionBase { 28 | readonly code = ARGUMENT_INVALID; 29 | } 30 | 31 | /** 32 | * Used to indicate that an argument is out of allowed range 33 | * (for example: incorrect string/array length, number not in allowed min/max range etc) 34 | * 35 | * @class ArgumentOutOfRangeException 36 | * @extends {ExceptionBase} 37 | */ 38 | export class ArgumentOutOfRangeException extends ExceptionBase { 39 | readonly code = ARGUMENT_OUT_OF_RANGE; 40 | } 41 | 42 | /** 43 | * Used to indicate conflicting entities (usually in the database) 44 | * 45 | * @class ConflictException 46 | * @extends {ExceptionBase} 47 | */ 48 | export class ConflictException extends ExceptionBase { 49 | readonly code = CONFLICT; 50 | } 51 | 52 | /** 53 | * Used to indicate that entity is not found 54 | * 55 | * @class NotFoundException 56 | * @extends {ExceptionBase} 57 | */ 58 | export class NotFoundException extends ExceptionBase { 59 | static readonly message = "Not found"; 60 | 61 | readonly code = NOT_FOUND; 62 | } 63 | 64 | /** 65 | * Used to indicate an internal server error that does not fall under all other errors 66 | * 67 | * @class InternalServerErrorException 68 | * @extends {ExceptionBase} 69 | */ 70 | export class InternalServerErrorException extends ExceptionBase { 71 | message = "Internal server error"; 72 | 73 | readonly code = INTERNAL_SERVER_ERROR; 74 | } 75 | -------------------------------------------------------------------------------- /modules/core/helpers/ids.ts: -------------------------------------------------------------------------------- 1 | import { customAlphabet } from "nanoid"; 2 | 3 | /** 4 | * You just increase the length if necessary to improve performance 5 | * Attention to the security(id is key) 6 | * 7 | * Characters Length Total States 8 | * UUID 16 32 16^32 = 3.4e+38 9 | * Base58 58 22 58^22 = 6.2e+38 10 | * --------------------------------------------------------- 11 | * Length Example Total States 12 | * nanoid(8) re6ZkUUV 1.3e+14 13 | * nanoid(12) pfpPYdZGbZvw 1.4e+21 14 | * nanoid(16) sFDUZScHfZTfkLwk 1.6e+28 15 | * nanoid(24) u7vzXJL9cGqUeabGPAZ5XUJ6 2.1e+42 16 | * nanoid(32) qkvPDeH6JyAsRhaZ3X4ZLDPSLFP7MnJz 2.7e+56 17 | * 18 | * See @https://unkey.dev/blog/uuid-ux 19 | */ 20 | export const generateId = customAlphabet( 21 | "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz", 22 | ); 23 | 24 | export const DEFAULT_PREFIX_ID_LENGTH = 22; 25 | export const ENTITY_ID_LENGTH = 25; 26 | 27 | export function generatePrefixId(prefix = "tu", length = DEFAULT_PREFIX_ID_LENGTH): string { 28 | return `${prefix}_${generateId(length)}`; 29 | } 30 | 31 | export function generateUserId(length = DEFAULT_PREFIX_ID_LENGTH) { 32 | return generatePrefixId("u", length); 33 | } 34 | -------------------------------------------------------------------------------- /modules/core/helpers/object.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from "../domain-base/entities/entity.base"; 2 | import { ValueObject } from "../domain-base/entities/value-object.base"; 3 | 4 | function isEntity(obj: unknown): obj is Entity { 5 | /** 6 | * 'instanceof Entity' causes error here for some reason. 7 | * Probably creates some circular dependency. This is a workaround 8 | * until I find a solution :) 9 | */ 10 | return ( 11 | Object.prototype.hasOwnProperty.call(obj, "toObject") && 12 | Object.prototype.hasOwnProperty.call(obj, "id") && 13 | ValueObject.isValueObject((obj as Entity).id) 14 | ); 15 | } 16 | 17 | function convertToPlainObject(item: any): any { 18 | if (ValueObject.isValueObject(item)) { 19 | return item.raw(); 20 | } 21 | if (isEntity(item)) { 22 | return item.toObject(); 23 | } 24 | return item; 25 | } 26 | 27 | /** 28 | * Converts Entity/Value Objects props to a plain object. 29 | * Useful for testing and debugging. 30 | * @param props 31 | */ 32 | export function convertPropsToObject(props: any): any { 33 | const propsCopy = structuredClone(props); 34 | 35 | // eslint-disable-next-line guard-for-in 36 | for (const prop in propsCopy) { 37 | if (Array.isArray(propsCopy[prop])) { 38 | propsCopy[prop] = (propsCopy[prop] as Array).map((item) => { 39 | return convertToPlainObject(item); 40 | }); 41 | } 42 | propsCopy[prop] = convertToPlainObject(propsCopy[prop]); 43 | } 44 | 45 | return propsCopy; 46 | } 47 | -------------------------------------------------------------------------------- /modules/core/infra-base/http/middleware/error-handler.middleware.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/core/infra-base/http/middleware/error-handler.middleware.ts -------------------------------------------------------------------------------- /modules/core/infra-base/http/middleware/rate-limit.middleware.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/core/infra-base/http/middleware/rate-limit.middleware.ts -------------------------------------------------------------------------------- /modules/core/infra-base/logger/logger.port.ts: -------------------------------------------------------------------------------- 1 | export interface LoggerPort { 2 | log(message: string, ...meta: unknown[]): void; 3 | error(message: string, trace?: unknown, ...meta: unknown[]): void; 4 | warn(message: string, ...meta: unknown[]): void; 5 | debug(message: string, ...meta: unknown[]): void; 6 | } 7 | -------------------------------------------------------------------------------- /modules/core/infra-base/mapper.base.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from "../domain-base/entities/entity.base"; 2 | 3 | export interface Mapper, DbRecord, Response = unknown> { 4 | toPersistence(entity: DomainEntity): DbRecord; 5 | toDomain(record: DbRecord): DomainEntity; 6 | toResponse(entity: DomainEntity): Response; 7 | } 8 | -------------------------------------------------------------------------------- /modules/core/infra-base/persistence/migrations/plannet-scale.utils.migration.ts: -------------------------------------------------------------------------------- 1 | import { CreateTableBuilder, sql } from "kysely"; 2 | 3 | export function psEnumSql(...args: string[]) { 4 | return sql`enum(${sql.join(args.map(sql.lit))})`; 5 | } 6 | 7 | export function psConcatSql(...args: string[]) { 8 | return sql`concat(${args.join(', "", ')})`; 9 | } 10 | 11 | export function psMySqlUuid() { 12 | return sql`(UUID())`; 13 | } 14 | 15 | export function psWithTimestamps( 16 | qb: CreateTableBuilder, 17 | ) { 18 | return qb 19 | .addColumn("created_at", "timestamp", (col) => col.defaultTo(sql`CURRENT_TIMESTAMP`)) 20 | .addColumn("updated_at", "timestamp", (col) => col.defaultTo(sql`CURRENT_TIMESTAMP`)); 21 | } 22 | 23 | export function psWithUser
( 24 | qb: CreateTableBuilder, 25 | ) { 26 | return qb 27 | .addColumn("created_by", "varchar(255)", (col) => col.references("user.id")) 28 | .addColumn("updated_by", "varchar(255)", (col) => col.references("user.id")); 29 | } 30 | 31 | export function psWithMySqlV8
( 32 | qb: CreateTableBuilder, 33 | ) { 34 | return qb.modifyEnd(sql`COLLATE utf8mb4_0900_ai_ci`); 35 | } 36 | -------------------------------------------------------------------------------- /modules/core/infra-base/persistence/repo/repository.query.base.ts: -------------------------------------------------------------------------------- 1 | import type { OrderBy, PaginatedQueryParams } from "../../../domain-base/repo/repository.port"; 2 | 3 | /** 4 | * Base class for regular queries 5 | */ 6 | abstract class QueryBase {} 7 | 8 | /** 9 | * Base class for paginated queries 10 | */ 11 | export abstract class PaginatedQueryBase extends QueryBase { 12 | limit: number; 13 | offset: number; 14 | orderBy: OrderBy; 15 | page: number; 16 | 17 | constructor(props: PaginatedParams) { 18 | super(); 19 | this.limit = props.limit || 20; 20 | this.offset = props.page ? props.page * this.limit : 0; 21 | this.page = props.page || 0; 22 | this.orderBy = props.orderBy || { field: true, param: "desc" }; 23 | } 24 | } 25 | 26 | // Paginated query parameters 27 | export type PaginatedParams = Omit & 28 | Partial>; 29 | -------------------------------------------------------------------------------- /modules/core/infra-base/schedule/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/core/infra-base/schedule/.gitkeep -------------------------------------------------------------------------------- /modules/core/infra-base/translation/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/core/infra-base/translation/.gitkeep -------------------------------------------------------------------------------- /modules/core/infra-base/workers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/core/infra-base/workers/.gitkeep -------------------------------------------------------------------------------- /modules/document/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/document/.gitkeep -------------------------------------------------------------------------------- /modules/field/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/field/.gitkeep -------------------------------------------------------------------------------- /modules/file/application/services/minio/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/file/application/services/minio/.gitkeep -------------------------------------------------------------------------------- /modules/file/domain/entities/file.entity.ts: -------------------------------------------------------------------------------- 1 | import { AggregateRoot } from "../../../core/domain-base/entities/aggregate.base"; 2 | import type { FileProps } from "../../helpers/file.types"; 3 | 4 | export class FileEntity extends AggregateRoot { 5 | validate(): void { 6 | throw new Error("Method not implemented."); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /modules/file/domain/use-cases/port/upload-file.in-port.ts: -------------------------------------------------------------------------------- 1 | import type { UseCase } from "../../../../core/domain-base/use-cases.port.base"; 2 | import type { FileEntity } from "../../entities/file.entity"; 3 | 4 | export interface UploadFileCommand { 5 | path: string; 6 | file: Buffer | File; 7 | } 8 | 9 | export abstract class UploadFileInPort implements UseCase { 10 | abstract execute(uploadFileCommand: UploadFileCommand): Promise; 11 | } 12 | -------------------------------------------------------------------------------- /modules/file/helpers/file.types.ts: -------------------------------------------------------------------------------- 1 | export type FileProps = { 2 | contentType: string; 3 | size: number; 4 | name: string; 5 | path: string; 6 | }; 7 | -------------------------------------------------------------------------------- /modules/file/infras/persistence/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/file/infras/persistence/.gitkeep -------------------------------------------------------------------------------- /modules/folder/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/folder/.gitkeep -------------------------------------------------------------------------------- /modules/hyerachy/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/hyerachy/.gitkeep -------------------------------------------------------------------------------- /modules/onboarding/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/onboarding/README.md -------------------------------------------------------------------------------- /modules/preferences/README.md: -------------------------------------------------------------------------------- 1 | # Preference bounded context 2 | 3 | Preference contains aggregates: ip, user-setting. 4 | 5 | Each aggregate is isolated and has a clear and simple interface. -------------------------------------------------------------------------------- /modules/preferences/ip/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/preferences/ip/.gitkeep -------------------------------------------------------------------------------- /modules/preferences/user-setting/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/preferences/user-setting/.gitkeep -------------------------------------------------------------------------------- /modules/space/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/space/.gitkeep -------------------------------------------------------------------------------- /modules/task/README.md: -------------------------------------------------------------------------------- 1 | # Task Aggregate -------------------------------------------------------------------------------- /modules/task/application/controllers/task.controller.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/task/application/controllers/task.controller.ts -------------------------------------------------------------------------------- /modules/task/application/controllers/task.message.controller.ts: -------------------------------------------------------------------------------- 1 | import type { ApplicationEventHandler } from "modules/core/application-base/event-handler/application-event.handler"; 2 | import type { UserCreatedDomainEvent } from "modules/user/user/domain/events/user-created.event"; 3 | 4 | export class AfterUserCreated implements ApplicationEventHandler { 5 | handle(domainEvent: UserCreatedDomainEvent): Promise | Promise { 6 | throw new Error("Method not implemented."); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /modules/task/tag/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/task/tag/.gitkeep -------------------------------------------------------------------------------- /modules/task/task/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/task/task/.gitkeep -------------------------------------------------------------------------------- /modules/team/README.md: -------------------------------------------------------------------------------- 1 | # Team Aggregate -------------------------------------------------------------------------------- /modules/team/member/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/team/member/.gitkeep -------------------------------------------------------------------------------- /modules/team/role/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/team/role/.gitkeep -------------------------------------------------------------------------------- /modules/team/team/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/team/team/.gitkeep -------------------------------------------------------------------------------- /modules/user/README.md: -------------------------------------------------------------------------------- 1 | # User Aggregate -------------------------------------------------------------------------------- /modules/user/invite/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/user/invite/.gitkeep -------------------------------------------------------------------------------- /modules/user/user/__test__/e2e/user/features/create-user.feature: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/user/user/__test__/e2e/user/features/create-user.feature -------------------------------------------------------------------------------- /modules/user/user/__test__/e2e/user/features/user-login.feature: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/user/user/__test__/e2e/user/features/user-login.feature -------------------------------------------------------------------------------- /modules/user/user/__test__/mock/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/user/user/__test__/mock/.gitkeep -------------------------------------------------------------------------------- /modules/user/user/__test__/perf/task/create-task.artillery.yml: -------------------------------------------------------------------------------- 1 | # ... UPDATING -------------------------------------------------------------------------------- /modules/user/user/__test__/perf/user/create-user.artillery.yml: -------------------------------------------------------------------------------- 1 | config: 2 | target: http://localhost:3000/v1 3 | phases: 4 | - duration: 2 5 | arrivalRate: 150 6 | plugins: 7 | faker: 8 | locale: vi 9 | variables: 10 | email: '$faker.internet.email' 11 | unverifiedEmail: '' 12 | isEmailVerified: '' 13 | nickname: '$faker' 14 | mobile: '' 15 | birthday: '$faker.date.birthdate' 16 | name: '' 17 | avatarUrl: '' 18 | role: '$faker.helpers.arrayElement(["SUPER_ADMIN", "MODERATOR", "ADMIN", "MEMBER", "GUEST"])' 19 | status: '$faker.helpers.arrayElement(["VERIFIED", "BLACKLIST", "INACTIVE", "ACTIVE", "CLOSED"])' 20 | locale: '' 21 | gender: '' 22 | openPlatform: '' 23 | utmCampaign: '' 24 | utmMedium: '' 25 | utmSource: '' 26 | googleId: '' 27 | scenarios: 28 | - flow: 29 | - post: 30 | url: '/users' 31 | json: 32 | .... UPDATING -------------------------------------------------------------------------------- /modules/user/user/application/controllers/user.controller.ts: -------------------------------------------------------------------------------- 1 | import type { CreateUserInPort } from "../../domain/use-cases/port/create-user.in-port"; 2 | import type { LoginEmailPasswordInPort } from "../../domain/use-cases/port/login-email-password.in-port"; 3 | import type { CreateUserDto } from "../dtos/create-user.dto"; 4 | import type { LoginEmailPasswordDTO } from "../dtos/login.dto"; 5 | 6 | export class UserController { 7 | constructor( 8 | private readonly createUserUseCase: CreateUserInPort, 9 | private readonly loginEmailPasswordUseCase: LoginEmailPasswordInPort, 10 | ) {} 11 | 12 | createUser(body: CreateUserDto) { 13 | return this.createUserUseCase.execute(body); 14 | } 15 | 16 | loginEmailPassword(body: LoginEmailPasswordDTO) { 17 | return this.loginEmailPasswordUseCase.execute(body); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /modules/user/user/application/dtos/create-user.dto.ts: -------------------------------------------------------------------------------- 1 | import type { UserProps } from "../../domain/entities/user.types"; 2 | 3 | export interface CreateUserDto extends UserProps {} 4 | -------------------------------------------------------------------------------- /modules/user/user/application/dtos/login.dto.ts: -------------------------------------------------------------------------------- 1 | import type { LoginEmailPasswordCommand } from "../../domain/use-cases/port/login-email-password.in-port"; 2 | 3 | export interface LoginEmailPasswordDTO extends LoginEmailPasswordCommand {} 4 | 5 | export interface LoginEmailPasswordDtoResponse { 6 | accessToken: string; 7 | refreshToken: string; 8 | } 9 | -------------------------------------------------------------------------------- /modules/user/user/application/services/clerk/clerk.service.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/user/user/application/services/clerk/clerk.service.ts -------------------------------------------------------------------------------- /modules/user/user/application/services/nextjs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/user/user/application/services/nextjs/.gitkeep -------------------------------------------------------------------------------- /modules/user/user/domain/entities/user.entity.ts: -------------------------------------------------------------------------------- 1 | import { AggregateRoot } from "modules/core/domain-base/entities/aggregate.base"; 2 | import { UniqueEntityID } from "modules/core/domain-base/entities/unique-entity"; 3 | import { UserCreatedDomainEvent } from "../events/user-created.event"; 4 | import { type CreateUserProps, type UserProps, UserRoles, UserStatus } from "./user.types"; 5 | 6 | export class UserEntity extends AggregateRoot { 7 | static create(createProps: CreateUserProps) { 8 | const id = new UniqueEntityID(); 9 | const props: UserProps = { role: UserRoles.MEMBER, status: UserStatus.ACTIVE, ...createProps }; 10 | const user = new UserEntity({ id, props }); 11 | user.addEvent( 12 | new UserCreatedDomainEvent({ 13 | aggregateId: id, 14 | ...props, 15 | ...props?.metadata?.raw(), 16 | ...props?.provider?.raw(), 17 | }), 18 | ); 19 | return user; 20 | } 21 | 22 | validate(): void { 23 | throw new Error("Method not implemented."); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /modules/user/user/domain/entities/user.types.ts: -------------------------------------------------------------------------------- 1 | import type { MarkOptional, OmitProperties } from "ts-essentials"; 2 | import type { UserMetadata } from "../value-objects/user-metadata.value-object"; 3 | import { UserProvider } from "../value-objects/user-providers.value-object"; 4 | 5 | export interface UserProps { 6 | email: string; 7 | unverifiedEmail: string; 8 | isEmailVerified: boolean; 9 | nickname: string; 10 | mobile: string; 11 | birthday: string; 12 | name: string; 13 | avatarUrl: string; 14 | locale?: string; 15 | gender?: string; 16 | role: UserRoles; 17 | status: UserStatus; 18 | metadata?: UserMetadata; 19 | provider?: UserProvider; 20 | } 21 | 22 | // Properties that are needed for a user creation 23 | export interface CreateUserProps extends MarkOptional {} 24 | 25 | export interface IUserCreatedDE extends UserProps {} 26 | export interface IUserUpdatedDE 27 | extends OmitProperties, "provider" | "roles" | "status" | "metadata"> {} 28 | 29 | export enum UserRoles { 30 | SUPER_ADMIN = "SUPER_ADMIN", 31 | MODERATOR = "MODERATOR", 32 | ADMIN = "ADMIN", 33 | MEMBER = "MEMBER", 34 | GUEST = "GUEST", 35 | } 36 | 37 | export enum UserStatus { 38 | VERIFIED = "VERIFIED", 39 | BLACKLIST = "BLACKLIST", 40 | INACTIVE = "INACTIVE", 41 | ACTIVE = "ACTIVE", 42 | CLOSED = "CLOSED", 43 | } 44 | -------------------------------------------------------------------------------- /modules/user/user/domain/events/user-created.event.ts: -------------------------------------------------------------------------------- 1 | import { DomainEvent, type IDomainEvent } from "modules/core/domain-base/events/domain-event.base"; 2 | import { type IUserCreatedDE, UserRoles, UserStatus } from "../entities/user.types"; 3 | import { UserMetadata } from "../value-objects/user-metadata.value-object"; 4 | import { UserProvider } from "../value-objects/user-providers.value-object"; 5 | 6 | export class UserCreatedDomainEvent extends DomainEvent implements IUserCreatedDE { 7 | email: string; 8 | unverifiedEmail: string; 9 | isEmailVerified: boolean; 10 | nickname: string; 11 | mobile: string; 12 | birthday: string; 13 | name: string; 14 | avatarUrl: string; 15 | role: UserRoles; 16 | status: UserStatus; 17 | locale?: string; 18 | gender?: string; 19 | provider?: UserProvider; 20 | metadata?: UserMetadata; 21 | 22 | constructor(props: IDomainEvent) { 23 | super(props); 24 | this.email = props.email; 25 | this.unverifiedEmail = props.unverifiedEmail; 26 | this.isEmailVerified = props.isEmailVerified; 27 | this.nickname = props.nickname; 28 | this.mobile = props.mobile; 29 | this.birthday = props.birthday; 30 | this.name = props.name; 31 | this.locale = props.locale; 32 | this.avatarUrl = props.avatarUrl; 33 | this.gender = props.gender; 34 | this.provider = props.provider; 35 | this.role = props.role; 36 | this.status = props.status; 37 | this.metadata = props.metadata; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /modules/user/user/domain/events/user-deleted.event.ts: -------------------------------------------------------------------------------- 1 | import { DomainEvent } from "modules/core/domain-base/events/domain-event.base"; 2 | 3 | export class UserDeletedDomainEvent extends DomainEvent {} 4 | -------------------------------------------------------------------------------- /modules/user/user/domain/events/user-provider-changed.event.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/user/user/domain/events/user-provider-changed.event.ts -------------------------------------------------------------------------------- /modules/user/user/domain/events/user-role-changed.event.ts: -------------------------------------------------------------------------------- 1 | import { DomainEvent, type IDomainEvent } from "modules/core/domain-base/events/domain-event.base"; 2 | import { UserRoles } from "../entities/user.types"; 3 | 4 | export class UserRoleChangedDomainEvent extends DomainEvent { 5 | readonly oldRole: UserRoles; 6 | 7 | readonly newRole: UserRoles; 8 | 9 | constructor(props: IDomainEvent) { 10 | super(props); 11 | this.oldRole = props.oldRole; 12 | this.newRole = props.newRole; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /modules/user/user/domain/events/user-status-changed.event.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/user/user/domain/events/user-status-changed.event.ts -------------------------------------------------------------------------------- /modules/user/user/domain/events/user-updated.event.ts: -------------------------------------------------------------------------------- 1 | import { DomainEvent, type IDomainEvent } from "modules/core/domain-base/events/domain-event.base"; 2 | import type { IUserUpdatedDE } from "../entities/user.types"; 3 | 4 | export class UserUpdatedDomainEvent extends DomainEvent implements IUserUpdatedDE { 5 | email?: string; 6 | unverifiedEmail?: string; 7 | isEmailVerified?: boolean; 8 | nickname?: string; 9 | mobile?: string; 10 | birthday?: string; 11 | name?: string; 12 | locale?: string | undefined; 13 | avatarUrl?: string; 14 | gender?: string | undefined; 15 | 16 | constructor(props: IDomainEvent) { 17 | super(props); 18 | this.email = props.email; 19 | this.unverifiedEmail = props.unverifiedEmail; 20 | this.isEmailVerified = props.isEmailVerified; 21 | this.nickname = props.nickname; 22 | this.mobile = props.mobile; 23 | this.birthday = props.birthday; 24 | this.name = props.name; 25 | this.locale = props.locale; 26 | this.avatarUrl = props.avatarUrl; 27 | this.gender = props.gender; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /modules/user/user/domain/repo/user.model.ts: -------------------------------------------------------------------------------- 1 | import { ENTITY_ID_LENGTH } from "modules/core/helpers/ids"; 2 | import { 3 | type Output, 4 | boolean, 5 | date, 6 | email, 7 | enum_, 8 | minLength, 9 | object, 10 | optional, 11 | string, 12 | } from "valibot"; 13 | import { UserRoles, UserStatus } from "../entities/user.types"; 14 | 15 | export const userSchema = object({ 16 | id: string([minLength(ENTITY_ID_LENGTH)]), 17 | email: string([email()]), 18 | unverifiedEmail: string([email()]), 19 | isEmailVerified: boolean(), 20 | nickname: string(), 21 | mobile: string(), 22 | birthday: string(), 23 | name: string(), 24 | avatarUrl: string(), 25 | role: enum_(UserRoles), 26 | status: enum_(UserStatus), 27 | locale: optional(string()), 28 | gender: optional(string()), 29 | openPlatform: string(), 30 | utmCampaign: string(), 31 | utmMedium: string(), 32 | utmSource: string(), 33 | googleId: optional(string()), 34 | githubId: optional(string()), 35 | facebookId: optional(string()), 36 | appleId: optional(string()), 37 | createdAt: optional(date(), new Date()), 38 | updatedAt: optional(date(), new Date()), 39 | }); 40 | 41 | export type UserModel = Output; 42 | -------------------------------------------------------------------------------- /modules/user/user/domain/repo/user.repository.ts: -------------------------------------------------------------------------------- 1 | import type { RepositoryPort } from "modules/core/domain-base/repo/repository.port"; 2 | import type { UserEntity } from "../entities/user.entity"; 3 | 4 | export interface IUserRepository extends RepositoryPort {} 5 | -------------------------------------------------------------------------------- /modules/user/user/domain/use-cases/interactors/create-user.interactor.ts: -------------------------------------------------------------------------------- 1 | import { UserEntity } from "../../entities/user.entity"; 2 | import type { CreateUserCommand, CreateUserInPort } from "../port/create-user.in-port"; 3 | import type { CreateUserOutPort } from "../port/create-user.out-port"; 4 | 5 | export class CreateUserInteractor implements CreateUserInPort { 6 | constructor(private readonly createUserPort: CreateUserOutPort) {} 7 | 8 | execute(command: CreateUserCommand): Promise { 9 | const user = UserEntity.create(command); 10 | return this.createUserPort.insert(user); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /modules/user/user/domain/use-cases/interactors/login.interactor.ts: -------------------------------------------------------------------------------- 1 | import { UserEntity } from "../../entities/user.entity"; 2 | import type { 3 | LoginEmailPasswordCommand, 4 | LoginEmailPasswordInPort, 5 | } from "../port/login-email-password.in-port"; 6 | import type { LoginEmailPasswordOutPort } from "../port/login-email-password.out-port"; 7 | 8 | export class LoginEmailPasswordInteractor implements LoginEmailPasswordInPort { 9 | constructor(private readonly loginEmailPasswordPort: LoginEmailPasswordOutPort) {} 10 | 11 | async execute(command: LoginEmailPasswordCommand) { 12 | const isEmailExist = await this.loginEmailPasswordPort.findByKey("email"); 13 | const isValidPassword = true; 14 | const accessToken = "accessToken"; 15 | const refreshToken = "refreshToken"; 16 | // return this.loginEmailPasswordPort.insert(user); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /modules/user/user/domain/use-cases/port/create-user.in-port.ts: -------------------------------------------------------------------------------- 1 | import type { UseCase } from "modules/core/domain-base/use-cases.port.base"; 2 | import type { UserEntity } from "../../entities/user.entity"; 3 | import type { CreateUserProps } from "../../entities/user.types"; 4 | 5 | export interface CreateUserCommand extends CreateUserProps {} 6 | 7 | export abstract class CreateUserInPort implements UseCase { 8 | abstract execute(createUserCommand: CreateUserCommand): Promise; 9 | } 10 | -------------------------------------------------------------------------------- /modules/user/user/domain/use-cases/port/create-user.out-port.ts: -------------------------------------------------------------------------------- 1 | import type { UserEntity } from "../../entities/user.entity"; 2 | 3 | export abstract class CreateUserOutPort { 4 | abstract insert(user: UserEntity): Promise; 5 | } 6 | -------------------------------------------------------------------------------- /modules/user/user/domain/use-cases/port/delete-user.in-port.ts: -------------------------------------------------------------------------------- 1 | import type { UseCase } from "modules/core/domain-base/use-cases.port.base"; 2 | import type { UserEntity } from "../../entities/user.entity"; 3 | 4 | export interface DeleteUserCommand { 5 | orderId: string; 6 | } 7 | 8 | export abstract class DeleteUserInPort implements UseCase { 9 | abstract execute(deleteUserCommand: DeleteUserCommand): Promise; 10 | } 11 | -------------------------------------------------------------------------------- /modules/user/user/domain/use-cases/port/find-user-by-id.out-port.ts: -------------------------------------------------------------------------------- 1 | import type { UserEntity } from "../../entities/user.entity"; 2 | 3 | export abstract class FindUserByIdOutPort { 4 | abstract findUserById(userId: string): Promise; 5 | } 6 | -------------------------------------------------------------------------------- /modules/user/user/domain/use-cases/port/find-user-by-nickname.out-port.ts: -------------------------------------------------------------------------------- 1 | import type { UserEntity } from "../../entities/user.entity"; 2 | 3 | export abstract class FindUserByNicknameOutPort { 4 | abstract findUserByNickname(nickname: string): Promise; 5 | } 6 | -------------------------------------------------------------------------------- /modules/user/user/domain/use-cases/port/get-current-user.out-port.ts: -------------------------------------------------------------------------------- 1 | import type { UserEntity } from "../../entities/user.entity"; 2 | 3 | export abstract class GetCurrentUserOutPort { 4 | abstract getCurrentUser(): Promise; 5 | } 6 | -------------------------------------------------------------------------------- /modules/user/user/domain/use-cases/port/login-email-password.in-port.ts: -------------------------------------------------------------------------------- 1 | import type { UseCase } from "modules/core/domain-base/use-cases.port.base"; 2 | 3 | export interface LoginEmailPasswordCommand { 4 | username: string; 5 | password: string; 6 | } 7 | 8 | export abstract class LoginEmailPasswordInPort implements UseCase { 9 | abstract execute(loginCommand: LoginEmailPasswordCommand): Promise; 10 | } 11 | -------------------------------------------------------------------------------- /modules/user/user/domain/use-cases/port/login-email-password.out-port.ts: -------------------------------------------------------------------------------- 1 | import type { UserEntity } from "../../entities/user.entity"; 2 | 3 | export abstract class LoginEmailPasswordOutPort { 4 | abstract insert(user: UserEntity): Promise; 5 | abstract findByKey(key: string): Promise; 6 | } 7 | -------------------------------------------------------------------------------- /modules/user/user/domain/user.exceptions.ts: -------------------------------------------------------------------------------- 1 | import { ExceptionBase } from "modules/core/exceptions/exception.base"; 2 | 3 | const EMAIL_NOT_FOUND = "USER.EMAIL_NOT_FOUND"; 4 | const USERNAME_NOT_FOUND = "USER.USERNAME_NOT_FOUND"; 5 | 6 | export class UserEmailDoNotExistException extends ExceptionBase { 7 | readonly code = EMAIL_NOT_FOUND; 8 | } 9 | 10 | export class UserNameDoNotExistException extends ExceptionBase { 11 | readonly code = USERNAME_NOT_FOUND; 12 | } 13 | -------------------------------------------------------------------------------- /modules/user/user/domain/value-objects/user-metadata.value-object.ts: -------------------------------------------------------------------------------- 1 | import { invariant } from "@techmely/utils"; 2 | import { ValueObject } from "modules/core/domain-base/entities/value-object.base"; 3 | import { 4 | ArgumentInvalidException, 5 | ArgumentNotProvidedException, 6 | } from "modules/core/exceptions/exceptions"; 7 | 8 | export interface IUserMetadata { 9 | openPlatform: string; 10 | utmCampaign: string; 11 | utmMedium: string; 12 | utmSource: string; 13 | } 14 | 15 | export class UserMetadata extends ValueObject { 16 | protected validate(props: IUserMetadata): void { 17 | const USER_METADATA_LENGTH = 4; 18 | const niceUserKeyMetadata = Object.keys(props).length === USER_METADATA_LENGTH; 19 | invariant( 20 | niceUserKeyMetadata, 21 | new ArgumentNotProvidedException("User metadata keys not correct"), 22 | ); 23 | const niceValueUserMetadata = 24 | Object.values(props).filter(Boolean).length === USER_METADATA_LENGTH; 25 | invariant( 26 | niceValueUserMetadata, 27 | new ArgumentInvalidException("User metadata values not correct"), 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /modules/user/user/domain/value-objects/user-password.value-object.ts: -------------------------------------------------------------------------------- 1 | import { verify as argon2Verify } from "argon2"; 2 | import { ValueObject } from "modules/core/domain-base/entities/value-object.base"; 3 | 4 | export interface IUserPassword { 5 | value: string; 6 | } 7 | 8 | export class UserProvider extends ValueObject { 9 | get value() { 10 | return this.props.value; 11 | } 12 | 13 | async comparePassword(plainPassword: string) { 14 | return argon2Verify(this.value, plainPassword); 15 | } 16 | 17 | protected validate(props: IUserPassword): void {} 18 | } 19 | -------------------------------------------------------------------------------- /modules/user/user/domain/value-objects/user-providers.value-object.ts: -------------------------------------------------------------------------------- 1 | import { ValueObject } from "modules/core/domain-base/entities/value-object.base"; 2 | 3 | export interface IUserProvider { 4 | githubId?: string; 5 | googleId?: string; 6 | facebookId?: string; 7 | appleId?: string; 8 | } 9 | 10 | export class UserProvider extends ValueObject { 11 | protected validate(props: IUserProvider): void {} 12 | } 13 | -------------------------------------------------------------------------------- /modules/user/user/infras/graphql/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/user/user/infras/graphql/.gitkeep -------------------------------------------------------------------------------- /modules/user/user/infras/http/middleware/auth.middleware.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/user/user/infras/http/middleware/auth.middleware.ts -------------------------------------------------------------------------------- /modules/user/user/infras/http/v1/auth/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/user/user/infras/http/v1/auth/.gitkeep -------------------------------------------------------------------------------- /modules/user/user/infras/http/v1/auth/auth.router.ts: -------------------------------------------------------------------------------- 1 | import { mittAsync } from "modules/core/domain-base/events/domain-event.helper"; 2 | import { UserController } from "modules/user/user/application/controllers/user.controller"; 3 | import { CreateUserInteractor } from "modules/user/user/domain/use-cases/interactors/create-user.interactor"; 4 | import { LoginEmailPasswordInteractor } from "modules/user/user/domain/use-cases/interactors/login.interactor"; 5 | import { UserMapper } from "../../../mappers/user.mapper"; 6 | import { UserPlanetScaleRepository } from "../../../persistence/plannet-scale/user.impl.reposity"; 7 | 8 | const userMapper = new UserMapper(); 9 | const userRepo = new UserPlanetScaleRepository(userMapper, mittAsync()); 10 | const createUserUseCases = new CreateUserInteractor(userRepo); 11 | const loginUserPasswordUseCases = new LoginEmailPasswordInteractor(userRepo); 12 | const userController = new UserController(createUserUseCases, loginUserPasswordUseCases); 13 | 14 | // POST("/v1/auth/email-password", userController.loginEmailPassword) 15 | 16 | // NEXTJS handler (req) { 17 | /** 18 | * const body = req.body 19 | * userController.loginEmailPassword(body) 20 | * */ 21 | // } 22 | // 23 | -------------------------------------------------------------------------------- /modules/user/user/infras/persistence/kafka/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/user/user/infras/persistence/kafka/.gitkeep -------------------------------------------------------------------------------- /modules/user/user/infras/persistence/memcache/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/user/user/infras/persistence/memcache/.gitkeep -------------------------------------------------------------------------------- /modules/user/user/infras/persistence/plannet-scale/migrations/2023-12-20T02:55:13.184Z-init-sql.ts: -------------------------------------------------------------------------------- 1 | import { Kysely } from "kysely"; 2 | import type { UserModel } from "modules/user/user/domain/repo/user.model"; 3 | 4 | type DatabaseTables = { 5 | user: UserModel; 6 | }; 7 | 8 | export async function up(db: Kysely) {} 9 | export async function down(db: Kysely) {} 10 | -------------------------------------------------------------------------------- /modules/user/user/infras/persistence/plannet-scale/plannet-scale.config.ts: -------------------------------------------------------------------------------- 1 | import { CamelCasePlugin, Kysely } from "kysely"; 2 | import { PlanetScaleDialect } from "kysely-planetscale"; 3 | import type { UserModel } from "modules/user/user/domain/repo/user.model"; 4 | 5 | let dbClient: Kysely; 6 | 7 | export type AppDatabase = { 8 | users: UserModel; 9 | }; 10 | 11 | export const getDBClient = (): Kysely => { 12 | dbClient = 13 | dbClient || 14 | new Kysely({ 15 | log: process.env.VITE_NODE_ENV === "development" ? ["error", "error"] : undefined, 16 | dialect: new PlanetScaleDialect({ 17 | username: process.env.DB_USERNAME, 18 | password: process.env.DB_PASSWORD, 19 | host: process.env.DB_HOST, 20 | }), 21 | plugins: [new CamelCasePlugin()], 22 | }); 23 | return dbClient; 24 | }; 25 | -------------------------------------------------------------------------------- /modules/user/user/infras/persistence/plannet-scale/user.impl.reposity.ts: -------------------------------------------------------------------------------- 1 | import { consola } from "consola"; 2 | import type { Emitter } from "mitt"; 3 | import type { EmitDomainEvents } from "modules/core/domain-base/events/domain-event.types"; 4 | import { MySQLRepositoryBase } from "modules/core/infra-base/persistence/repo/repository.mysql.base"; 5 | import type { UserEntity } from "modules/user/user/domain/entities/user.entity"; 6 | import type { UserModel } from "modules/user/user/domain/repo/user.model"; 7 | import type { IUserRepository } from "modules/user/user/domain/repo/user.repository"; 8 | import type { UserMapper } from "../../mappers/user.mapper"; 9 | import { getDBClient } from "./plannet-scale.config"; 10 | 11 | export class UserPlanetScaleRepository 12 | extends MySQLRepositoryBase 13 | implements IUserRepository 14 | { 15 | protected tableName = "users"; 16 | protected db = getDBClient(); 17 | 18 | constructor(mapper: UserMapper, emitter: Emitter) { 19 | super(mapper, emitter, consola); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /modules/user/user/infras/persistence/redis/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/user/user/infras/persistence/redis/.gitkeep -------------------------------------------------------------------------------- /modules/user/user/infras/securities/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/user/user/infras/securities/.gitkeep -------------------------------------------------------------------------------- /modules/vilolation-content/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/vilolation-content/.gitkeep -------------------------------------------------------------------------------- /modules/whiteboard/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/modules/whiteboard/.gitkeep -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | import { withHydrationOverlay } from "@builder.io/react-hydration-overlay/next"; 2 | import million from "million/compiler"; 3 | import WpAutoImport from "unplugin-auto-import/webpack"; 4 | 5 | /** @type {import('next').NextConfig} */ 6 | const nextConfig = { 7 | images: { 8 | remotePatterns: [ 9 | { 10 | protocol: "https", 11 | hostname: "img.clerk.com", 12 | }, 13 | ], 14 | }, 15 | webpack(config) { 16 | config.plugins.push( 17 | WpAutoImport({ 18 | imports: [ 19 | "react", 20 | { 21 | "next/navigation": [ 22 | "useRouter", 23 | "useSearchParams", 24 | "useParams", 25 | "usePathname", 26 | "redirect", 27 | "permanentRedirect", 28 | ], 29 | "next/link": [["default", "Link"]], 30 | "next/image": [["default", "NImage"]], 31 | "next/script": [["default", "NScript"]], 32 | "react-use": ["useToggle"], 33 | }, 34 | ], 35 | dirs: ["./src/shared/**"], 36 | }), 37 | ); 38 | 39 | return config; 40 | }, 41 | }; 42 | 43 | const plugins = []; 44 | const pluginOptions = [ 45 | { 46 | id: "million", 47 | options: { 48 | auto: true, 49 | }, 50 | }, 51 | { 52 | id: "mismatch-hydration", 53 | options: { 54 | appRootSelector: "main", 55 | }, 56 | }, 57 | ]; 58 | 59 | if (process.env.ANALYZE === "true") { 60 | const withBundleAnalyzer = require("@next/bundle-analyzer")({ 61 | enabled: true, 62 | }); 63 | plugins.push({ id: "analyzer", plugin: withBundleAnalyzer }); 64 | } 65 | 66 | plugins.push({ id: "million", plugin: million.next }); 67 | plugins.push({ id: "mismatch-hydration", plugin: withHydrationOverlay }); 68 | 69 | export default () => { 70 | return plugins.reduce((acc, curr) => { 71 | const options = pluginOptions.find((id) => curr.id === id); 72 | if (options && Object.keys(options).length > 0) return curr.plugin(acc, options); 73 | return curr.plugin(acc); 74 | }, nextConfig); 75 | }; 76 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/images/CleanArchitecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/public/images/CleanArchitecture.png -------------------------------------------------------------------------------- /public/images/QRCode.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/public/images/QRCode.jpg -------------------------------------------------------------------------------- /public/images/Thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/public/images/Thumbnail.png -------------------------------------------------------------------------------- /public/images/clickup-landing/after-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /public/images/clickup-landing/after.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/public/images/clickup-landing/after.png -------------------------------------------------------------------------------- /public/images/clickup-landing/before-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/public/images/clickup-landing/before-2.png -------------------------------------------------------------------------------- /public/images/clickup-landing/before-3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /public/images/clickup-landing/before-4.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /public/images/clickup-landing/bring-teams-and-work-together.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/public/images/clickup-landing/bring-teams-and-work-together.png -------------------------------------------------------------------------------- /public/images/clickup-landing/button-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /public/images/clickup-landing/button-4.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /public/images/clickup-landing/button-5.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /public/images/clickup-landing/button-7.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /public/images/clickup-landing/button-8.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /public/images/clickup-landing/button-open-messenger.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /public/images/clickup-landing/button-svg-1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /public/images/clickup-landing/button-svg-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /public/images/clickup-landing/button-svg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /public/images/clickup-landing/convene-headshot-png.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/public/images/clickup-landing/convene-headshot-png.png -------------------------------------------------------------------------------- /public/images/clickup-landing/convene-png.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/public/images/clickup-landing/convene-png.png -------------------------------------------------------------------------------- /public/images/clickup-landing/div-1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /public/images/clickup-landing/div-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /public/images/clickup-landing/div-3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /public/images/clickup-landing/div-4.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/images/clickup-landing/div-cuhomecollapse-collapsecontentimagebox-arrpv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/public/images/clickup-landing/div-cuhomecollapse-collapsecontentimagebox-arrpv.png -------------------------------------------------------------------------------- /public/images/clickup-landing/div-cuhometeamstab-tabcardfeatureiconwrapper-rqvjb-mask-group-1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /public/images/clickup-landing/div-cuhometeamstab-tabcardfeatureiconwrapper-rqvjb-mask-group.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /public/images/clickup-landing/div-cuhometesteverythingyourteamislookingfor-cardfeaturelistswit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /public/images/clickup-landing/div-culandingpagefooter-background-8d6uv-mask-group.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /public/images/clickup-landing/div.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/images/clickup-landing/everything-your-team-is-looking-for.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/public/images/clickup-landing/everything-your-team-is-looking-for.png -------------------------------------------------------------------------------- /public/images/clickup-landing/finastra-png.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/public/images/clickup-landing/finastra-png.png -------------------------------------------------------------------------------- /public/images/clickup-landing/heading-2-all-teams-love-clickup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/public/images/clickup-landing/heading-2-all-teams-love-clickup.png -------------------------------------------------------------------------------- /public/images/clickup-landing/heading-2-perfect-fit-for-every-team.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/public/images/clickup-landing/heading-2-perfect-fit-for-every-team.png -------------------------------------------------------------------------------- /public/images/clickup-landing/item-svg-1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /public/images/clickup-landing/item-svg-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /public/images/clickup-landing/item-svg-3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /public/images/clickup-landing/item-svg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /public/images/clickup-landing/link-picture-app-store-badge-white-png.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/public/images/clickup-landing/link-picture-app-store-badge-white-png.png -------------------------------------------------------------------------------- /public/images/clickup-landing/link-picture-google-play-badge-white-png.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/public/images/clickup-landing/link-picture-google-play-badge-white-png.png -------------------------------------------------------------------------------- /public/images/clickup-landing/list-item-1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/images/clickup-landing/list-item-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/images/clickup-landing/list-item.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/images/clickup-landing/main-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/public/images/clickup-landing/main-1.png -------------------------------------------------------------------------------- /public/images/clickup-landing/main-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /public/images/clickup-landing/main-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/public/images/clickup-landing/main-3.png -------------------------------------------------------------------------------- /public/images/clickup-landing/main-4.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /public/images/clickup-landing/main-5.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /public/images/clickup-landing/main-link-picture-v3-badge-link-black-png.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/public/images/clickup-landing/main-link-picture-v3-badge-link-black-png.png -------------------------------------------------------------------------------- /public/images/clickup-landing/main-svg-10.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /public/images/clickup-landing/main-svg-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /public/images/clickup-landing/main-svg-9.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /public/images/clickup-landing/main-svg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /public/images/clickup-landing/main-users-love-us-png.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/public/images/clickup-landing/main-users-love-us-png.png -------------------------------------------------------------------------------- /public/images/clickup-landing/main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/public/images/clickup-landing/main.png -------------------------------------------------------------------------------- /public/images/clickup-landing/one-app-to-replace-them-all.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/public/images/clickup-landing/one-app-to-replace-them-all.png -------------------------------------------------------------------------------- /public/images/clickup-landing/picture-ai-powered-productivity-png.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/public/images/clickup-landing/picture-ai-powered-productivity-png.png -------------------------------------------------------------------------------- /public/images/clickup-landing/picture-collaborate-png.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/public/images/clickup-landing/picture-collaborate-png.png -------------------------------------------------------------------------------- /public/images/clickup-landing/picture-logo-svg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /public/images/clickup-landing/picture-modal-close-png.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/public/images/clickup-landing/picture-modal-close-png.png -------------------------------------------------------------------------------- /public/images/clickup-landing/picture-projects-png.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/public/images/clickup-landing/picture-projects-png.png -------------------------------------------------------------------------------- /public/images/clickup-landing/picture-projects-sm-png.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/public/images/clickup-landing/picture-projects-sm-png.png -------------------------------------------------------------------------------- /public/images/clickup-landing/picture-security-badge-white-svg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /public/images/clickup-landing/picture-uptime-badge-white-svg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /public/images/clickup-landing/pressed-png.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/public/images/clickup-landing/pressed-png.png -------------------------------------------------------------------------------- /public/images/clickup-landing/ready-to-unleash-your.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/public/images/clickup-landing/ready-to-unleash-your.png -------------------------------------------------------------------------------- /public/images/clickup-landing/save-time-and-get-more-done.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/public/images/clickup-landing/save-time-and-get-more-done.png -------------------------------------------------------------------------------- /public/images/clickup-landing/search-everything-png.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/public/images/clickup-landing/search-everything-png.png -------------------------------------------------------------------------------- /public/images/clickup-landing/stay-ahead-png.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/public/images/clickup-landing/stay-ahead-png.png -------------------------------------------------------------------------------- /public/images/clickup-landing/svg-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /public/images/clickup-landing/svg-5.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /public/images/clickup-landing/svg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /public/images/clickup-landing/team-s-full-potential.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/public/images/clickup-landing/team-s-full-potential.png -------------------------------------------------------------------------------- /public/images/clickup-landing/view-work-your-way-png.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/public/images/clickup-landing/view-work-your-way-png.png -------------------------------------------------------------------------------- /public/images/dashboard/background-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/public/images/dashboard/background-dark.png -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scripts/generateMigrateFile.mjs: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import { isEmpties } from "@techmely/utils"; 4 | 5 | function generateFilePath(name, domain, persistence) { 6 | const currentDate = new Date(); 7 | const isoDate = currentDate.toISOString(); 8 | const fileName = `${isoDate}-${name}.ts`; 9 | 10 | return path.join( 11 | process.cwd(), 12 | "modules", 13 | domain, 14 | "infras", 15 | "persistence", 16 | persistence, 17 | "migrations", 18 | fileName, 19 | ); 20 | } 21 | 22 | function createFile(fileName) { 23 | fs.writeFile(fileName, "", "utf8", (error) => { 24 | if (error) { 25 | console.error(`Error creating file: ${error}`); 26 | } else { 27 | console.log(`File created: ${fileName}`); 28 | } 29 | }); 30 | } 31 | 32 | function writeFileContent( 33 | filePath, 34 | domain, 35 | content = `import { Kysely } from "kysely"; 36 | type DatabaseTables = { 37 | 38 | }; 39 | 40 | export async function up(db: Kysely) {} 41 | export async function down(db: Kysely) {}`, 42 | ) { 43 | fs.appendFileSync(filePath, content, { encoding: "utf-8" }); 44 | console.log(`Append up/down migrate file domain ${domain} successfully!`); 45 | } 46 | 47 | const [_, __, fileName, domain, persistence] = process.argv; 48 | if (isEmpties(fileName, domain, persistence)) { 49 | console.error("Please provide filename + domain + persistance"); 50 | process.exit(1); 51 | } 52 | 53 | const filePath = generateFilePath(fileName, domain, persistence); 54 | createFile(filePath); 55 | writeFileContent(filePath, domain); 56 | -------------------------------------------------------------------------------- /scripts/migrate-db.mjs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/scripts/migrate-db.mjs -------------------------------------------------------------------------------- /src/app/(auth)/create-workspace/[[...create-workspace]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { OrganizationList } from "@clerk/nextjs"; 2 | 3 | export default function CreateWorkspacePage() { 4 | return ( 5 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/app/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | import React, { PropsWithChildren } from "react"; 2 | 3 | const AuthLayout: React.FC = ({ children }) => { 4 | return
{children}
; 5 | }; 6 | 7 | export default AuthLayout; 8 | -------------------------------------------------------------------------------- /src/app/(auth)/login/[[...login]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignIn } from "@clerk/nextjs"; 2 | 3 | const LoginPage: React.FC = (props) => { 4 | return ; 5 | }; 6 | 7 | export default LoginPage; 8 | -------------------------------------------------------------------------------- /src/app/(auth)/signup/[[...signup]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignUp } from "@clerk/nextjs"; 2 | 3 | const SignUpPage: React.FC = (props) => { 4 | return ; 5 | }; 6 | 7 | export default SignUpPage; 8 | -------------------------------------------------------------------------------- /src/app/(dashboard)/components/ButtonAI.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | 5 | const ButtonAI: React.FC = (props) => { 6 | return ( 7 | <> 8 | 9 | 10 | 19 | 20 | 21 | 22 | Tickup AI Helper 23 | 24 | 25 |
Hello AI
26 | 27 | 28 | 29 |
30 |
31 | 32 | ); 33 | }; 34 | 35 | export default ButtonAI; 36 | -------------------------------------------------------------------------------- /src/app/(dashboard)/components/ButtonActionNew.tsx: -------------------------------------------------------------------------------- 1 | import { PlusSquareIcon } from "lucide-react"; 2 | 3 | const ButtonActionNew: React.FC = (props) => { 4 | // const [isOpen, setIsOpen] = useState(false); 5 | return ( 6 | 7 | 8 | 9 | 10 | {/* biome-ignore lint/a11y/noNoninteractiveTabindex: */} 11 | 12 | 13 | 17 | 18 | 19 | 20 | 21 |

Create Item

22 |
23 |
24 |
25 | 26 | 27 | Tickup AI Helper 28 | 29 | 30 |
Hello AI
31 | 32 | 33 | 34 |
35 |
36 | ); 37 | }; 38 | 39 | export default ButtonActionNew; 40 | -------------------------------------------------------------------------------- /src/app/(dashboard)/components/ButtonQuickActionMenu.tsx: -------------------------------------------------------------------------------- 1 | import { GripIcon } from "lucide-react"; 2 | import React from "react"; 3 | 4 | const ButtonQuickActionMenu: React.FC = () => { 5 | return ( 6 | 7 | 8 | 9 | 10 | {/* biome-ignore lint/a11y/noNoninteractiveTabindex: */} 11 | 12 | 13 | 16 | 17 | 18 | 19 | 20 |

Quick Action Menu

21 |
22 |
23 |
24 | 25 | My Account 26 | 27 | Profile 28 | Billing 29 | Team 30 | Subscription 31 | 32 |
33 | ); 34 | }; 35 | 36 | export default ButtonQuickActionMenu; 37 | -------------------------------------------------------------------------------- /src/app/(dashboard)/components/ButtonUserMenu.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useUser } from "@clerk/nextjs"; 4 | import React from "react"; 5 | 6 | const ButtonUserMenu: React.FC = () => { 7 | const { user } = useUser(); 8 | return ( 9 | 10 | 11 | 25 | 26 | 27 | My Account 28 | 29 | Profile 30 | Billing 31 | Team 32 | Subscription 33 | 34 | 35 | ); 36 | }; 37 | 38 | export default ButtonUserMenu; 39 | -------------------------------------------------------------------------------- /src/app/(dashboard)/components/DashboardAside/AsideFavorites.tsx: -------------------------------------------------------------------------------- 1 | import { ChevronRightIcon, PinIcon } from "lucide-react"; 2 | import React from "react"; 3 | 4 | const AsideFavorites: React.FC = () => { 5 | const handlePinFavorites = () => {}; 6 | 7 | return ( 8 |
9 | 13 | 14 | 15 | 16 | 24 | 25 | 26 |

Pin favorites to Top

27 |
28 |
29 |
30 |
31 | ); 32 | }; 33 | 34 | export default AsideFavorites; 35 | -------------------------------------------------------------------------------- /src/app/(dashboard)/components/DashboardAside/AsideNavigateMenus.tsx: -------------------------------------------------------------------------------- 1 | import { useAuth } from "@clerk/nextjs"; 2 | import { GoalIcon, HomeIcon, InboxIcon } from "lucide-react"; 3 | import { For } from "million/react"; 4 | import React from "react"; 5 | 6 | const AsideNavigateMenus: React.FC = () => { 7 | const { orgId } = useAuth(); 8 | 9 | const menuItems = [ 10 | { 11 | name: "Home", 12 | href: `/w/${orgId}/home`, 13 | icon: , 14 | }, 15 | { 16 | name: "Inbox", 17 | href: `/w/${orgId}/inbox`, 18 | icon: , 19 | }, 20 | { 21 | name: "Docs", 22 | href: `/w/${orgId}/docs`, 23 | icon: ( 24 | 25 | 29 | 30 | ), 31 | }, 32 | { 33 | name: "Dashboard", 34 | href: `/w/${orgId}/dashboard`, 35 | icon: ( 36 | 37 | 41 | 45 | 46 | ), 47 | }, 48 | { 49 | name: "Goals", 50 | href: `/w/${orgId}/goals`, 51 | icon: , 52 | }, 53 | ]; 54 | 55 | return ( 56 |
    57 | 58 | {(item) => ( 59 |
  • 60 | 70 |
  • 71 | )} 72 |
    73 |
74 | ); 75 | }; 76 | 77 | export default AsideNavigateMenus; 78 | -------------------------------------------------------------------------------- /src/app/(dashboard)/components/DashboardAside/AsideSpaces.tsx: -------------------------------------------------------------------------------- 1 | import { PlusIcon, SearchIcon } from "lucide-react"; 2 | import React from "react"; 3 | 4 | const AsideSpaces: React.FC = () => { 5 | return ( 6 |
7 | Spaces 8 |
9 | 10 | 11 | 12 | 25 | 26 | 27 |

Space Settings

28 |
29 |
30 |
31 | 32 | 33 | 34 | 37 | 38 | 39 |

Search

40 |
41 |
42 |
43 | 44 | 45 | 46 | 49 | 50 | 51 |

New Spaces

52 |
53 |
54 |
55 |
56 |
57 | ); 58 | }; 59 | 60 | export default AsideSpaces; 61 | -------------------------------------------------------------------------------- /src/app/(dashboard)/components/DashboardAside/AsideSupports.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const AsideSupports: React.FC = () => { 4 | return ( 5 |
6 | 15 | 16 | 17 | 18 | 26 | 27 | 28 |

Resources Center

29 |
30 |
31 |
32 |
33 | ); 34 | }; 35 | 36 | export default AsideSupports; 37 | -------------------------------------------------------------------------------- /src/app/(dashboard)/components/DashboardMain.tsx: -------------------------------------------------------------------------------- 1 | import React, { PropsWithChildren } from "react"; 2 | import DashboardSidebar from "./DashboardSidebar"; 3 | import MainTopBar from "./MainTopBar"; 4 | 5 | const DashboardMain: React.FC = ({ children }) => { 6 | return ( 7 |
8 | 9 |
10 | 11 |
12 |
{children}
13 |
14 |
15 | ); 16 | }; 17 | 18 | export default DashboardMain; 19 | -------------------------------------------------------------------------------- /src/app/(dashboard)/components/DashboardSidebar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import AsideFavorites from "./DashboardAside/AsideFavorites"; 3 | import AsideNavigateMenus from "./DashboardAside/AsideNavigateMenus"; 4 | import AsideSpaces from "./DashboardAside/AsideSpaces"; 5 | import AsideSupports from "./DashboardAside/AsideSupports"; 6 | import AsideWorkspaceSelection from "./DashboardAside/AsideWorkspaceSelection"; 7 | 8 | const DashboardSidebar: React.FC = (props) => { 9 | const preferences = usePreferencesState(); 10 | return ( 11 | 12 | 13 | 24 | 25 | 26 | ); 27 | }; 28 | 29 | export default DashboardSidebar; 30 | -------------------------------------------------------------------------------- /src/app/(dashboard)/components/MainTopBar/ButtonToggleSidebar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { SidebarIcon } from "lucide-react"; 4 | 5 | const ButtonToggleSidebar = forwardRef(function XXX(props, ref) { 6 | const preferences = usePreferencesState(); 7 | 8 | const handleToggleSidebar = () => { 9 | preferences.toggleOpenSidebar(true); 10 | }; 11 | 12 | if (preferences.isOpenSidebar) return
; 13 | 14 | return ( 15 | 18 | ); 19 | }); 20 | 21 | export default ButtonToggleSidebar; 22 | -------------------------------------------------------------------------------- /src/app/(dashboard)/components/MainTopBar/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useAuth } from "@clerk/nextjs"; 4 | import { GoalIcon, HomeIcon, InboxIcon } from "lucide-react"; 5 | import React from "react"; 6 | import ButtonToggleSidebar from "./ButtonToggleSidebar"; 7 | 8 | const MainTopBar: React.FC = () => { 9 | const pathname = usePathname(); 10 | const { orgId } = useAuth(); 11 | 12 | const pageItems = [ 13 | { 14 | name: "Home", 15 | path: `/w/${orgId}/home`, 16 | icon: , 17 | }, 18 | { 19 | name: "Inbox", 20 | path: `/w/${orgId}/inbox`, 21 | icon: , 22 | }, 23 | { 24 | name: "Docs", 25 | path: `/w/${orgId}/docs`, 26 | icon: ( 27 | 28 | 32 | 33 | ), 34 | }, 35 | { 36 | name: "Dashboard", 37 | path: `/w/${orgId}/dashboard`, 38 | icon: ( 39 | 40 | 44 | 48 | 49 | ), 50 | }, 51 | { 52 | name: "Goals", 53 | path: `/w/${orgId}/goals`, 54 | icon: , 55 | }, 56 | ]; 57 | const currentTopBar = pageItems.find((i) => i.path === pathname); 58 | if (!currentTopBar) return null; 59 | 60 | return ( 61 |
62 |
63 | 64 |
65 | {currentTopBar.icon} 66 | {currentTopBar.name} 67 |
68 |
69 |
70 | ); 71 | }; 72 | 73 | export default MainTopBar; 74 | -------------------------------------------------------------------------------- /src/app/(dashboard)/components/SearchCommandPalette/CommandMenu.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | export function CommandMenu() { 4 | const [isOpen, setIsOpen] = useState(false); 5 | 6 | useEffect(() => { 7 | const handleKeyDown = (e: KeyboardEvent) => { 8 | if (e.key === "k" && (e.metaKey || e.ctrlKey)) { 9 | e.preventDefault(); 10 | setIsOpen(!isOpen); 11 | } 12 | }; 13 | document.addEventListener("keydown", handleKeyDown); 14 | }, []); 15 | 16 | return ( 17 | 18 | 19 | 20 | No results found. 21 | 22 | Calendar 23 | Search Emoji 24 | Calculator 25 | 26 | 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/app/(dashboard)/components/SearchCommandPalette/index.tsx: -------------------------------------------------------------------------------- 1 | import { CommandIcon, SearchIcon } from "lucide-react"; 2 | import { CommandMenu } from "./CommandMenu"; 3 | 4 | const SearchCommandPalette: React.FC = () => { 5 | return ( 6 |
7 |
8 | 9 |
10 | 11 |
12 | 13 | K 14 |
15 | 16 |
17 | ); 18 | }; 19 | 20 | export default SearchCommandPalette; 21 | -------------------------------------------------------------------------------- /src/app/(dashboard)/layout.tsx: -------------------------------------------------------------------------------- 1 | import React, { PropsWithChildren } from "react"; 2 | import DashboardHeader from "./components/DashboardHeader"; 3 | import DashboardMain from "./components/DashboardMain"; 4 | 5 | const DashboardLayout: React.FC = async ({ children }) => { 6 | return ( 7 | <> 8 | 9 | {children} 10 | 11 | ); 12 | }; 13 | 14 | export default DashboardLayout; 15 | -------------------------------------------------------------------------------- /src/app/(dashboard)/settings/billling/page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const BillingPage: React.FC = (props) => { 4 | return
BillingPage
; 5 | }; 6 | 7 | export default BillingPage; 8 | -------------------------------------------------------------------------------- /src/app/(dashboard)/w/[organizationId]/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | import { currentUser } from "@clerk/nextjs/server"; 2 | 3 | const DashboardPage: React.FC = async () => { 4 | const user = await currentUser(); 5 | 6 | return ( 7 |
8 |

Dashboard Page

9 |

10 | {JSON.stringify({ email: user?.emailAddresses, name: user?.username })} 11 |

12 | {user?.imageUrl && User} 13 |
14 | ); 15 | }; 16 | 17 | export default DashboardPage; 18 | -------------------------------------------------------------------------------- /src/app/(dashboard)/w/[organizationId]/docs/page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const DocsPage: React.FC = () => { 4 | return
Doc Page
; 5 | }; 6 | 7 | export default DocsPage; 8 | -------------------------------------------------------------------------------- /src/app/(dashboard)/w/[organizationId]/goals/page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const GoalsPage: React.FC = () => { 4 | return
Doc Page
; 5 | }; 6 | 7 | export default GoalsPage; 8 | -------------------------------------------------------------------------------- /src/app/(dashboard)/w/[organizationId]/home/page.tsx: -------------------------------------------------------------------------------- 1 | import { auth } from "@clerk/nextjs"; 2 | 3 | const HomePage: React.FC = () => { 4 | return
HomePage
; 5 | }; 6 | 7 | export default HomePage; 8 | -------------------------------------------------------------------------------- /src/app/(dashboard)/w/[organizationId]/inbox/page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const InboxPage: React.FC = () => { 4 | return
Doc Page
; 5 | }; 6 | 7 | export default InboxPage; 8 | -------------------------------------------------------------------------------- /src/app/(dashboard)/w/[organizationId]/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useAuth } from "@clerk/nextjs"; 4 | 5 | const WorkspacePage: React.FC = (props) => { 6 | const { userId } = useAuth(); 7 | const preferences = usePreferencesState(); 8 | 9 | const handleToggleSidebar = () => { 10 | preferences.toggleOpenSidebar(true); 11 | }; 12 | 13 | return ( 14 |
15 | {!preferences.isOpenSidebar && } 16 | WorkspacePage 17 | {/*

{user}

*/} 18 |

{userId}

19 |
20 | ); 21 | }; 22 | 23 | export default WorkspacePage; 24 | -------------------------------------------------------------------------------- /src/app/(marketing)/components/HomeLoginButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | const HomeLoginButton: React.FC = (props) => { 4 | const router = useRouter(); 5 | const navigateToLogin = () => { 6 | router.push("/login"); 7 | }; 8 | return ( 9 | 16 | ); 17 | }; 18 | 19 | export default HomeLoginButton; 20 | -------------------------------------------------------------------------------- /src/app/(marketing)/components/HomeSignUpButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | const HomeSignUpButton: React.FC = (props) => { 4 | const router = useRouter(); 5 | const navigateToSignUp = () => { 6 | router.push("/signup"); 7 | }; 8 | return ( 9 | 16 | ); 17 | }; 18 | 19 | export default HomeSignUpButton; 20 | -------------------------------------------------------------------------------- /src/app/(marketing)/page.tsx: -------------------------------------------------------------------------------- 1 | import { TickUpLandingPage } from "./components/LandingPageTikup"; 2 | 3 | export default function Home() { 4 | return ( 5 | 6 | //
7 | //
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/app/api/v1/auth.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from "next"; 2 | 3 | export default async function handler(req: NextApiRequest, res: NextApiResponse) {} 4 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 222.2 84% 4.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 222.2 84% 4.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 222.2 84% 4.9%; 15 | 16 | --primary: 222.2 47.4% 11.2%; 17 | --primary-foreground: 210 40% 98%; 18 | 19 | --secondary: 210 40% 96.1%; 20 | --secondary-foreground: 222.2 47.4% 11.2%; 21 | 22 | --muted: 210 40% 96.1%; 23 | --muted-foreground: 215.4 16.3% 46.9%; 24 | 25 | --accent: 210 40% 96.1%; 26 | --accent-foreground: 222.2 47.4% 11.2%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 210 40% 98%; 30 | 31 | --border: 214.3 31.8% 91.4%; 32 | --input: 214.3 31.8% 91.4%; 33 | --ring: 222.2 84% 4.9%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 222.2 84% 4.9%; 40 | --foreground: 210 40% 98%; 41 | 42 | --card: 222.2 84% 4.9%; 43 | --card-foreground: 210 40% 98%; 44 | 45 | --popover: 222.2 84% 4.9%; 46 | --popover-foreground: 210 40% 98%; 47 | 48 | --primary: 210 40% 98%; 49 | --primary-foreground: 222.2 47.4% 11.2%; 50 | 51 | --secondary: 217.2 32.6% 17.5%; 52 | --secondary-foreground: 210 40% 98%; 53 | 54 | --muted: 217.2 32.6% 17.5%; 55 | --muted-foreground: 215 20.2% 65.1%; 56 | 57 | --accent: 217.2 32.6% 17.5%; 58 | --accent-foreground: 210 40% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 210 40% 98%; 62 | 63 | --border: 217.2 32.6% 17.5%; 64 | --input: 217.2 32.6% 17.5%; 65 | --ring: 212.7 26.8% 83.9%; 66 | } 67 | 68 | * { 69 | @apply border-border; 70 | } 71 | html { 72 | @apply h-full 73 | } 74 | body { 75 | @apply bg-background text-foreground h-full; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { ClerkProvider } from "@clerk/nextjs"; 2 | import type { Metadata } from "next"; 3 | import { Be_Vietnam_Pro } from "next/font/google"; 4 | import "./globals.css"; 5 | 6 | const FontBeVietnamPro = Be_Vietnam_Pro({ 7 | weight: ["400", "500", "700"], 8 | subsets: ["vietnamese", "latin"], 9 | }); 10 | 11 | export const metadata: Metadata = { 12 | title: "Tickup - Techmely 🔥", 13 | description: "Super clone ClickUp - Techmely", 14 | }; 15 | 16 | export default async function RootLayout({ 17 | children, 18 | }: { 19 | children: React.ReactNode; 20 | }) { 21 | /** 22 | * Ta cần define ra css variables ở đây để khi nào ta muốn reactive một cái gì đó 23 | * liên quan tới UI thì sẽ chạm vào nó để thay đổi global mà không cần thay đổi CSS 24 | * VD: 25 | * - Chèn thanh thông báo chương trình khuyến mãi/thông báo quan trọng lên trên cùng Web 26 | * 27 | */ 28 | const bodyStyles = { 29 | "--global-actions-bar-height": "56px", 30 | } as React.CSSProperties; 31 | 32 | return ( 33 | 34 | 35 | 36 | {children} 37 | 38 | 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { authMiddleware, redirectToSignIn } from "@clerk/nextjs"; 2 | import { NextResponse } from "next/server"; 3 | 4 | const publicRoutes = ["/", "/login", "/signup"]; 5 | 6 | export default authMiddleware({ 7 | publicRoutes, 8 | afterAuth({ userId, orgId, isPublicRoute }, req) { 9 | // Chưa đăng nhập + không phải public route 10 | if (!userId && !isPublicRoute) { 11 | return redirectToSignIn({ returnBackUrl: req.url }); 12 | } 13 | // Đã đăng nhập rồi 14 | if (userId) { 15 | if (!orgId) { 16 | return NextResponse.redirect(new URL("/create-workspace", req.url), 308); 17 | } 18 | if (orgId && isPublicRoute) { 19 | return NextResponse.redirect(new URL(`/w/${orgId}/home`, req.url), 308); 20 | } 21 | } 22 | }, 23 | }); 24 | 25 | export const config = { 26 | matcher: ["/((?!.+\\.[\\w]+$|_next).*)", "/", "/(api|trpc)(.*)"], 27 | }; 28 | -------------------------------------------------------------------------------- /src/shared/components/accordion.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as AccordionPrimitive from "@radix-ui/react-accordion"; 4 | import { ChevronDown } from "lucide-react"; 5 | import * as React from "react"; 6 | 7 | import { cn } from "@/shared/utils/string"; 8 | 9 | const Accordion = AccordionPrimitive.Root; 10 | 11 | const AccordionItem = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, ...props }, ref) => ( 15 | 16 | )); 17 | AccordionItem.displayName = "AccordionItem"; 18 | 19 | const AccordionTrigger = React.forwardRef< 20 | React.ElementRef, 21 | React.ComponentPropsWithoutRef 22 | >(({ className, children, ...props }, ref) => ( 23 | 24 | svg]:rotate-180", 28 | className, 29 | )} 30 | {...props} 31 | > 32 | {children} 33 | 34 | 35 | 36 | )); 37 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; 38 | 39 | const AccordionContent = React.forwardRef< 40 | React.ElementRef, 41 | React.ComponentPropsWithoutRef 42 | >(({ className, children, ...props }, ref) => ( 43 | 51 |
{children}
52 |
53 | )); 54 | AccordionContent.displayName = AccordionPrimitive.Content.displayName; 55 | 56 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; 57 | -------------------------------------------------------------------------------- /src/shared/components/button.tsx: -------------------------------------------------------------------------------- 1 | import { Slot } from "@radix-ui/react-slot"; 2 | import { type VariantProps, cva } from "class-variance-authority"; 3 | import * as React from "react"; 4 | 5 | import { cn } from "@/shared/utils/string"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", 14 | outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 15 | secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", 16 | ghost: "hover:bg-accent hover:text-accent-foreground", 17 | link: "text-primary underline-offset-4 hover:underline", 18 | }, 19 | size: { 20 | default: "h-10 px-4 py-2", 21 | sm: "h-9 rounded-md px-3", 22 | lg: "h-11 rounded-md px-8", 23 | icon: "h-10 w-10", 24 | }, 25 | }, 26 | defaultVariants: { 27 | variant: "default", 28 | size: "default", 29 | }, 30 | }, 31 | ); 32 | 33 | export interface ButtonProps 34 | extends React.ButtonHTMLAttributes, 35 | VariantProps { 36 | asChild?: boolean; 37 | } 38 | 39 | const Button = React.forwardRef( 40 | ({ className, variant, size, asChild = false, ...props }, ref) => { 41 | const Comp = asChild ? Slot : "button"; 42 | return ( 43 | 44 | ); 45 | }, 46 | ); 47 | Button.displayName = "Button"; 48 | 49 | export { Button, buttonVariants }; 50 | -------------------------------------------------------------------------------- /src/shared/components/checkbox.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; 4 | import { Check } from "lucide-react"; 5 | import * as React from "react"; 6 | 7 | import { cn } from "@/shared/utils/string"; 8 | 9 | const Checkbox = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => ( 13 | 21 | 22 | 23 | 24 | 25 | )); 26 | Checkbox.displayName = CheckboxPrimitive.Root.displayName; 27 | 28 | export { Checkbox }; 29 | -------------------------------------------------------------------------------- /src/shared/components/collapsible.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"; 4 | 5 | const Collapsible = CollapsiblePrimitive.Root; 6 | 7 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger; 8 | 9 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent; 10 | 11 | export { Collapsible, CollapsibleTrigger, CollapsibleContent }; 12 | -------------------------------------------------------------------------------- /src/shared/components/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/shared/utils/string"; 4 | 5 | export type InputProps = React.InputHTMLAttributes; 6 | 7 | const Input = React.forwardRef( 8 | ({ className, type, ...props }, ref) => { 9 | return ( 10 | 19 | ); 20 | }, 21 | ); 22 | Input.displayName = "Input"; 23 | 24 | export { Input }; 25 | -------------------------------------------------------------------------------- /src/shared/components/popover.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as PopoverPrimitive from "@radix-ui/react-popover"; 4 | import * as React from "react"; 5 | 6 | import { cn } from "@/shared/utils/string"; 7 | 8 | const Popover = PopoverPrimitive.Root; 9 | 10 | const PopoverTrigger = PopoverPrimitive.Trigger; 11 | 12 | const PopoverContent = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef 15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 16 | 17 | 27 | 28 | )); 29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName; 30 | 31 | export { Popover, PopoverTrigger, PopoverContent }; 32 | -------------------------------------------------------------------------------- /src/shared/components/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"; 4 | import * as React from "react"; 5 | 6 | import { cn } from "@/shared/utils/string"; 7 | 8 | const ScrollArea = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, children, ...props }, ref) => ( 12 | 17 | 18 | {children} 19 | 20 | 21 | 22 | 23 | )); 24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; 25 | 26 | const ScrollBar = React.forwardRef< 27 | React.ElementRef, 28 | React.ComponentPropsWithoutRef 29 | >(({ className, orientation = "vertical", ...props }, ref) => ( 30 | 41 | 42 | 43 | )); 44 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName; 45 | 46 | export { ScrollArea, ScrollBar }; 47 | -------------------------------------------------------------------------------- /src/shared/components/tabs.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as TabsPrimitive from "@radix-ui/react-tabs"; 4 | import * as React from "react"; 5 | 6 | import { cn } from "@/shared/utils/string"; 7 | 8 | const Tabs = TabsPrimitive.Root; 9 | 10 | const TabsList = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, ...props }, ref) => ( 14 | 22 | )); 23 | TabsList.displayName = TabsPrimitive.List.displayName; 24 | 25 | const TabsTrigger = React.forwardRef< 26 | React.ElementRef, 27 | React.ComponentPropsWithoutRef 28 | >(({ className, ...props }, ref) => ( 29 | 37 | )); 38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; 39 | 40 | const TabsContent = React.forwardRef< 41 | React.ElementRef, 42 | React.ComponentPropsWithoutRef 43 | >(({ className, ...props }, ref) => ( 44 | 52 | )); 53 | TabsContent.displayName = TabsPrimitive.Content.displayName; 54 | 55 | export { Tabs, TabsList, TabsTrigger, TabsContent }; 56 | -------------------------------------------------------------------------------- /src/shared/components/tooltip.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"; 4 | import * as React from "react"; 5 | 6 | import { cn } from "@/shared/utils/string"; 7 | 8 | const TooltipProvider = TooltipPrimitive.Provider; 9 | 10 | const Tooltip = TooltipPrimitive.Root; 11 | 12 | const TooltipTrigger = TooltipPrimitive.Trigger; 13 | 14 | const TooltipContent = React.forwardRef< 15 | React.ElementRef, 16 | React.ComponentPropsWithoutRef 17 | >(({ className, sideOffset = 4, ...props }, ref) => ( 18 | 27 | )); 28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName; 29 | 30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; 31 | -------------------------------------------------------------------------------- /src/shared/hooks/usePreferences.ts: -------------------------------------------------------------------------------- 1 | import { noop } from "@techmely/utils"; 2 | import { create } from "zustand"; 3 | 4 | type PreferencesStore = { 5 | isOpenSidebar: boolean; 6 | toggleOpenSidebar: (v: boolean) => void; 7 | }; 8 | 9 | const initPreferencesState: PreferencesStore = { 10 | isOpenSidebar: true, 11 | toggleOpenSidebar: noop, 12 | }; 13 | 14 | export const usePreferencesState = create((set) => ({ 15 | ...initPreferencesState, 16 | toggleOpenSidebar: (isOpenSidebar) => set({ isOpenSidebar }), 17 | })); 18 | -------------------------------------------------------------------------------- /src/shared/utils/string.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /test/architecture/application.test.ts: -------------------------------------------------------------------------------- 1 | import { filesOfProject } from "tsarch"; 2 | import { describe, expect, it } from "vitest"; 3 | 4 | describe.concurrent("Application boundaries", () => { 5 | it("Should allow multiple patterns for dtos", async () => { 6 | const violations = await filesOfProject() 7 | .inFolder("*/*/dtos") 8 | .should() 9 | .matchPattern(".dto.ts") 10 | .check(); 11 | 12 | expect(violations).toEqual([]); 13 | }); 14 | 15 | it("controllers should not depend on the interactors", async () => { 16 | const rule = filesOfProject() 17 | .inFolder("*/*") 18 | .matchingPattern(".controller.ts") 19 | .shouldNot() 20 | .dependOnFiles() 21 | .matchingPattern(".interactor.ts"); 22 | 23 | await expect(rule).toPassAsync(); 24 | }); 25 | 26 | it("controllers should not depend on the dtos", async () => { 27 | const rule = filesOfProject() 28 | .inFolder("*/*") 29 | .matchingPattern(".controller.ts") 30 | .shouldNot() 31 | .dependOnFiles() 32 | .inFolder("*/infras/*"); 33 | 34 | await expect(rule).toPassAsync(); 35 | }); 36 | 37 | it("controllers should not depend on the repository", async () => { 38 | const rule = filesOfProject() 39 | .inFolder("*/*") 40 | .matchingPattern(".controller.ts") 41 | .shouldNot() 42 | .dependOnFiles() 43 | .inFolder("*/infras/*"); 44 | 45 | await expect(rule).toPassAsync(); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /test/architecture/arch.setup.ts: -------------------------------------------------------------------------------- 1 | import { JestResultFactory, JestViolationFactory } from "tsarch"; 2 | import { expect } from "vitest"; 3 | 4 | expect.extend({ 5 | async toPassAsync(checkable) { 6 | if (!checkable) { 7 | return JestResultFactory.error("expected something checkable as an argument for expect()"); 8 | } 9 | const violations = await checkable.check(); 10 | const jestViolations = violations.map((v) => JestViolationFactory.from(v)); 11 | return JestResultFactory.result(this.isNot, jestViolations); 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /test/architecture/infrastructure.test.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techmely/tickup/9594899e4b64f356e4f28c55b722a422976185b9/test/architecture/infrastructure.test.ts -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "verbatimModuleSyntax": true, 15 | "noImplicitAny": false, 16 | "jsx": "preserve", 17 | "incremental": true, 18 | "emitDecoratorMetadata": true, 19 | "experimentalDecorators": true, 20 | "types": ["reflect-metadata"], 21 | "plugins": [ 22 | { 23 | "name": "next" 24 | } 25 | ], 26 | "paths": { 27 | "@/*": ["./src/*"], 28 | "modules/*": ["./modules/*"], 29 | } 30 | }, 31 | "include": ["next-env.d.ts", "typings", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 32 | "exclude": ["node_modules"] 33 | } 34 | -------------------------------------------------------------------------------- /typings/event.d.ts: -------------------------------------------------------------------------------- 1 | import { StringEnum } from "@techmely/types"; 2 | 3 | declare module "mitt" { 4 | declare type EventType = string | symbol; 5 | declare type Handler = (event: T) => void; 6 | declare type WildcardHandler> = ( 7 | type: keyof T, 8 | event: T[keyof T], 9 | ) => void; 10 | declare type EventHandlerList = Array>; 11 | declare type WildCardEventHandlerList> = Array>; 12 | declare type EventHandlerMap> = Map< 13 | keyof Events | "*", 14 | EventHandlerList | WildCardEventHandlerList 15 | >; 16 | interface Emitter> { 17 | all: EventHandlerMap; 18 | on(type: Key, handler: Handler): void; 19 | on(type: "*", handler: WildcardHandler): void; 20 | off(type: Key, handler?: Handler): void; 21 | off(type: "*", handler: WildcardHandler): void; 22 | emit(type: Key, event: Events[Key]): void; 23 | emit(type: undefined extends Events[Key] ? Key : never): void; 24 | emitAsync(type: StringEnum, event: unknown): Promise; 25 | } 26 | 27 | /** 28 | * Mitt: Tiny (~200b) functional event emitter / pubsub. 29 | * @name mitt 30 | * @returns {Mitt} 31 | */ 32 | export default function mitt>( 33 | all?: EventHandlerMap, 34 | ): Emitter; 35 | } 36 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | setupFiles: ["./test/architecture/arch.setup.ts"], 6 | include: ["test/**/*.test.ts"], 7 | typecheck: { enabled: false }, 8 | }, 9 | }); 10 | --------------------------------------------------------------------------------