├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── semantic-pull-request.yml ├── .gitignore ├── .npmrc ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── apps └── web │ ├── .env.example │ ├── .eslintrc │ ├── .gitignore │ ├── .prettierrc │ ├── .vscode │ ├── extensions.json │ └── settings.json │ ├── README.md │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── postcss.config.js │ ├── public │ ├── favicon.ico │ ├── kubestack.png │ └── vercel.svg │ ├── src │ ├── components │ │ ├── DefaultLayout.tsx │ │ ├── Layout.tsx │ │ ├── Loading.tsx │ │ ├── Logo.tsx │ │ ├── Pricing.tsx │ │ └── Pricing │ │ │ └── PriceCard.tsx │ ├── middleware.ts │ ├── pages │ │ ├── _app.tsx │ │ ├── api │ │ │ ├── trpc │ │ │ │ └── [trpc].ts │ │ │ └── webhook │ │ │ │ └── stripe.ts │ │ ├── dashboard.tsx │ │ ├── index.tsx │ │ ├── payment │ │ │ └── success.tsx │ │ ├── plan.tsx │ │ ├── settings.tsx │ │ ├── sign-in │ │ │ └── [[...index]].tsx │ │ └── sign-up │ │ │ └── [[...index]].tsx │ ├── server │ │ ├── auth │ │ │ ├── middleware.ts │ │ │ └── types.ts │ │ ├── context.ts │ │ ├── core │ │ │ └── user.ts │ │ ├── env.js │ │ ├── payment │ │ │ └── stripe.ts │ │ ├── prisma.ts │ │ ├── routers │ │ │ ├── _app.ts │ │ │ ├── payment.ts │ │ │ ├── post.test.ts │ │ │ └── post.ts │ │ └── trpc.ts │ ├── styles │ │ └── global.css │ └── utils │ │ ├── constants.ts │ │ ├── publicRuntimeConfig.ts │ │ ├── routing.ts │ │ └── trpc.ts │ ├── tailwind.config.js │ └── tsconfig.json ├── index.js ├── package.json ├── packages ├── database │ ├── .env.example │ ├── .gitignore │ ├── index.ts │ ├── package.json │ └── prisma │ │ ├── migrations │ │ ├── 20221222224246_init │ │ │ └── migration.sql │ │ ├── 20221224141953_payment │ │ │ └── migration.sql │ │ ├── 20221226165737_user_info │ │ │ └── migration.sql │ │ ├── 20221226220318_subscription_date │ │ │ └── migration.sql │ │ ├── 20221226220758_rename_subscription_end │ │ │ └── migration.sql │ │ ├── 20221229004345_remove_post │ │ │ └── migration.sql │ │ └── migration_lock.toml │ │ └── schema.prisma ├── eslint-config-custom │ ├── index.js │ └── package.json ├── prettier-config │ ├── index.js │ └── package.json └── tsconfig │ ├── base.json │ └── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── turbo.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: Kubessandra 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: '/' 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 2 8 | -------------------------------------------------------------------------------- /.github/workflows/semantic-pull-request.yml: -------------------------------------------------------------------------------- 1 | # This workflow ensures PR titles match the Conventional Commits spec. 2 | # see https://www.conventionalcommits.org/en/v1.0.0/ 3 | name: "Lint PR" 4 | 5 | on: 6 | pull_request_target: 7 | types: 8 | - opened 9 | - edited 10 | - synchronize 11 | 12 | jobs: 13 | main: 14 | name: Validate PR title 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: amannn/action-semantic-pull-request@v5 18 | # Action constomization is available here : 19 | # https://github.com/marketplace/actions/semantic-pull-request#configuration 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | .turbo 3 | logs 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | lerna-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # TypeScript v1 declaration files 46 | typings/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Microbundle cache 58 | .rpt2_cache/ 59 | .rts2_cache_cjs/ 60 | .rts2_cache_es/ 61 | .rts2_cache_umd/ 62 | 63 | # Optional REPL history 64 | .node_repl_history 65 | 66 | # Output of 'npm pack' 67 | *.tgz 68 | 69 | # Yarn Integrity file 70 | .yarn-integrity 71 | 72 | # dotenv environment variables file 73 | .env 74 | .env.test 75 | 76 | # parcel-bundler cache (https://parceljs.org/) 77 | .cache 78 | 79 | # Next.js build output 80 | .next 81 | 82 | # Nuxt.js build / generate output 83 | .nuxt 84 | dist 85 | 86 | # Gatsby files 87 | .cache/ 88 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 89 | # https://nextjs.org/blog/next-9-1#public-directory-support 90 | # public 91 | 92 | # vuepress build output 93 | .vuepress/dist 94 | 95 | # Serverless directories 96 | .serverless/ 97 | 98 | # FuseBox cache 99 | .fusebox/ 100 | 101 | # DynamoDB Local files 102 | .dynamodb/ 103 | 104 | # TernJS port file 105 | .tern-port 106 | 107 | build/** 108 | dist/** 109 | .next/** 110 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | use-node-version=18.4.0 2 | engine-strict=true 3 | public-hoist-pattern[]=*prisma* 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We want this community to be friendly and respectful to each other. Please follow it in all your interactions with the project. 4 | 5 | ## How can I contribute? 6 | 7 | - **Give us a star.** It may not seem like much, but it really makes a 8 | difference. This is something that everyone can do to help out. Github 9 | stars help the project gain visibility and stand out. 10 | 11 | - **Join the community.** Sometimes helping people can be as easy as listening 12 | to their problems and offering a different perspective. Join our Slack, have a 13 | look at discussions in the forum and take part in community events. More info 14 | on this in [Communication](#communication). 15 | 16 | - **Help with open issues.** Sometimes, issues lack necessary information and some 17 | are duplicates of older issues. 18 | You can help out by guiding people through the process of filling out the 19 | issue template, asking for clarifying information or pointing them to existing 20 | issues that match their description of the problem. 21 | 22 | - **Review documentation changes.** Most documentation just needs a review for 23 | proper spelling and grammar. If you think a document can be improved in any 24 | way, feel free to hit the `edit` button at the top of the page. More info on 25 | contributing to the documentation [here](#contribute-documentation). 26 | 27 | - **Help with tests.** Pull requests may lack proper tests or test plans. These 28 | are needed for the change to be implemented safely. 29 | 30 | - **Create a pull request.** 31 | 32 | ## Communication 33 | 34 | - We use [Discord](https://discord.gg/v7sbenECgC) 35 | - You can also join us on [Twitter](https://twitter.com/kubessandra) 36 | 37 | ## Pull request process 38 | 39 | All contributions are made via pull requests. To make a pull request, you will 40 | need a GitHub account; if you are unclear on this process, see GitHub's 41 | documentation on [forking](https://help.github.com/articles/fork-a-repo) and 42 | [pull requests](https://help.github.com/articles/using-pull-requests). Pull 43 | requests should be targeted at the `main` branch. 44 | 45 | 1. Create a feature branch off of `main` so that changes do not get mixed up. 46 | 2. [Rebase](http://git-scm.com/book/en/Git-Branching-Rebasing) your local 47 | changes against the `main` branch. 48 | 49 | If a pull request is not ready to be reviewed yet 50 | [it should be marked as a "Draft"](https://docs.github.com/en/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/changing-the-stage-of-a-pull-request). 51 | 52 | ### Working with forks 53 | 54 | ```bash 55 | # First you clone the original repository 56 | git clone git@github.com:kubessandra/KubeStack.git 57 | # Next you add a git remote that is your fork: 58 | git remote add fork git@github.com:/KubeStack.git 59 | # Next you fetch the latest changes from origin for master: 60 | git fetch origin 61 | git checkout main 62 | git pull --rebase 63 | # Next you create a new feature branch off of master: 64 | git checkout my-feature-branch 65 | # Now you do your work and commit your changes: 66 | git add -A 67 | git commit -a -m "fix: this is the subject line" -m "This is the body line. Closes #123" 68 | # And the last step is pushing this to your fork 69 | git push -u fork my-feature-branch 70 | ``` 71 | 72 | Now go to the project's GitHub Pull Request page and click "New pull request" 73 | 74 | ## Setup 75 | 76 | ### PNPM 77 | 78 | First, you will need to have [`pnpm`](https://pnpm.io/) installed on your machine. 79 | 80 | From the [installation page](https://pnpm.io/installation), choose your preferred install method and run it. 81 | 82 | ### NodeJS 83 | 84 | Next, you'll need to have [NodeJS](https://nodejs.org/en/) installed, with the required version. The required version is specified in the [`.npmrc`](.npmrc) root file, aswell as in every `package.json` file : 85 | 86 | Feel free to use the node-version manager of your choice (like [nvm](https://github.com/nvm-sh/nvm)), but we recommend using the integrated manager of pnpm. The [`pnpm env`](https://pnpm.io/fr/cli/env) command allows you to manage whatever node engine pnpm should use. Go into any `package.json` file and find the required node version, then enter the following command : 87 | 88 | ```sh 89 | $ pnpm env use --global 18.4.0 90 | ``` 91 | 92 | If you enter the wrong version, or if the required node version changes, pnpm will throw errors until you make the apropriate changes. 93 | 94 | > :information_source: Using the `pnpm env` command, pnpm will resolve a separate node instance from the one you installed on your system. Note that the _pnpm_ version will be used for dependencies resolution **aswell** as for scripts. 95 | 96 | ### Environment variables. 97 | 98 | You need to replace every `.env.example` that you can find by a `.env` with your environment variables. 99 | 100 | You can find a `.env.example` in: 101 | 102 | - `apps/web` 103 | - `package/database` 104 | 105 | ## Development workflow 106 | 107 | To get started with the project, run `pnpm` in the root directory to install the required dependencies for each package: 108 | 109 | ```sh 110 | pnpm i 111 | pnpm dev 112 | ``` 113 | 114 | ### Commit message convention 115 | 116 | We follow the [conventional commits specification](https://www.conventionalcommits.org/en) for our commit messages: 117 | 118 | - `fix`: bug fixes, e.g. fix crash due to deprecated method. 119 | - `feat`: new features, e.g. add new method to the module. 120 | - `refactor`: code refactor, e.g. migrate from class components to hooks. 121 | - `docs`: changes into documentation, e.g. add usage example for the module.. 122 | - `test`: adding or updating tests, e.g. add integration tests using detox. 123 | - `chore`: tooling changes, e.g. change CI config. 124 | 125 | Our pre-commit hooks verify that your commit message matches this format when committing. 126 | 127 | ### Linting and tests 128 | 129 | [ESLint](https://eslint.org/), [Prettier](https://prettier.io/), [TypeScript](https://www.typescriptlang.org/) 130 | 131 | We use [TypeScript](https://www.typescriptlang.org/) for type checking, [ESLint](https://eslint.org/) with [Prettier](https://prettier.io/) for linting and formatting the code, and [Jest](https://jestjs.io/) for testing. 132 | 133 | Our pre-commit hooks verify that the linter and tests pass when committing. 134 | 135 | ```sh 136 | pnpm lint 137 | pnpm lint-fix 138 | ``` 139 | 140 | ### Sending a pull request 141 | 142 | > **Working on your first pull request?** You can learn how from this _free_ series: [How to Contribute to an Open Source Project on GitHub](https://app.egghead.io/playlists/how-to-contribute-to-an-open-source-project-on-github). 143 | 144 | When you're sending a pull request: 145 | 146 | - Prefer small pull requests focused on one change. 147 | - Verify that linters and tests are passing. 148 | - Review the documentation to make sure it looks good. 149 | - Follow the pull request template when opening a pull request. 150 | - For pull requests that change the API or implementation, discuss with maintainers first by opening an issue. 151 | 152 | ## Code of Conduct 153 | 154 | ### Our Pledge 155 | 156 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 157 | 158 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. 159 | 160 | ### Our Standards 161 | 162 | Examples of behavior that contributes to a positive environment for our community include: 163 | 164 | - Demonstrating empathy and kindness toward other people 165 | - Being respectful of differing opinions, viewpoints, and experiences 166 | - Giving and gracefully accepting constructive feedback 167 | - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience 168 | - Focusing on what is best not just for us as individuals, but for the overall community 169 | 170 | Examples of unacceptable behavior include: 171 | 172 | - The use of sexualized language or imagery, and sexual attention or 173 | advances of any kind 174 | - Trolling, insulting or derogatory comments, and personal or political attacks 175 | - Public or private harassment 176 | - Publishing others' private information, such as a physical or email 177 | address, without their explicit permission 178 | - Other conduct which could reasonably be considered inappropriate in a 179 | professional setting 180 | 181 | ### Enforcement Responsibilities 182 | 183 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. 184 | 185 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. 186 | 187 | ### Scope 188 | 189 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. 190 | 191 | ### Enforcement 192 | 193 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [INSERT CONTACT METHOD]. All complaints will be reviewed and investigated promptly and fairly. 194 | 195 | All community leaders are obligated to respect the privacy and security of the reporter of any incident. 196 | 197 | ### Enforcement Guidelines 198 | 199 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: 200 | 201 | #### 1. Correction 202 | 203 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. 204 | 205 | **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. 206 | 207 | #### 2. Warning 208 | 209 | **Community Impact**: A violation through a single incident or series of actions. 210 | 211 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. 212 | 213 | #### 3. Temporary Ban 214 | 215 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. 216 | 217 | **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. 218 | 219 | #### 4. Permanent Ban 220 | 221 | **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. 222 | 223 | **Consequence**: A permanent ban from any sort of public interaction within the community. 224 | 225 | ### Attribution 226 | 227 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, 228 | available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 229 | 230 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). 231 | 232 | [homepage]: https://www.contributor-covenant.org 233 | 234 | For answers to common questions about this code of conduct, see the FAQ at 235 | https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. 236 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Kubessandra 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KubeStack 2 |

3 | 4 |

5 | 6 |

7 | 8 | 9 |

10 | 11 | # What's KubeStack 12 | 13 | An amazing SaaS plateform boilerplate. 14 | 15 | ## Run it in local 16 | 17 | ``` 18 | pnpm i 19 | pnpm dev 20 | ``` 21 | 22 | ## Tech stack 23 | 24 | - [pnpm](https://pnpm.io) 25 | - [Turborepo](https://turbo.build) 26 | - [Trpc](https://trpc.io) 27 | - [Prisma](https://prisma.io) 28 | - [Clerk](https://clerk.dev) 29 | - [Stripe](https://stripe.com) 30 | - [NextJS](https://nextjs.org) 31 | - [Typescript](https://typescriptlang.org) 32 | - [TailwindCSS](https://tailwindcss.com) 33 | - [Eslint](https://eslint.org) 34 | - [Prettier](https://prettier.io) 35 | 36 | ## Ressources 37 | 38 | - [Contributing](CONTRIBUTING.md) 39 | - [Discord](https://discord.gg/v7sbenECgC) 40 | - [Live](https://twitch.tv/kubessandra) 41 | - [Twitter](https://twitter.com/kubessandra) 42 | -------------------------------------------------------------------------------- /apps/web/.env.example: -------------------------------------------------------------------------------- 1 | ## env are parsed by next.config.js and next.config.js is 2 | ## saying which env is open to public(frontend) or backend. 3 | 4 | # Public 5 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY= 6 | 7 | # Private 8 | CLERK_SECRET_KEY= 9 | STRIPE_SECRET= 10 | STRIPE_WEBHOOK_SECRET= 11 | FRONT_URL=http://localhost:3000 12 | -------------------------------------------------------------------------------- /apps/web/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@kubessandra/eslint-config-custom"], 3 | "overrides": [ 4 | { 5 | "files": ["*.ts", "*.tsx"], 6 | "parserOptions": { 7 | "project": "tsconfig.json", 8 | "ecmaVersion": 2018, 9 | "sourceType": "module" 10 | } 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /apps/web/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | *.db 37 | *.db-journal 38 | 39 | 40 | # testing 41 | playwright/test-results 42 | -------------------------------------------------------------------------------- /apps/web/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "prettier": "@kubessandra/prettier-config" 3 | } 4 | -------------------------------------------------------------------------------- /apps/web/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "esbenp.prettier-vscode", 4 | "dbaeumer.vscode-eslint", 5 | "prisma.prisma" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /apps/web/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /apps/web/README.md: -------------------------------------------------------------------------------- 1 | # Web Plateform 2 | 3 | ## Stripe Testing 4 | 5 | To be able to test the subscription / payment you need to launch the stripe cli with: 6 | 7 | ```sh 8 | stripe listen --forward-to localhost:3000/api/webhook/stripe 9 | ``` 10 | -------------------------------------------------------------------------------- /apps/web/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /apps/web/next.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /* eslint-disable @typescript-eslint/no-var-requires */ 3 | const { env } = require("./src/server/env"); 4 | 5 | /** 6 | * Don't be scared of the generics here. 7 | * All they do is to give us autocompletion when using this. 8 | * 9 | * @template {import('next').NextConfig} T 10 | * @param {T} config - A generic parameter that flows through to the return type 11 | * @constraint {{import('next').NextConfig}} 12 | */ 13 | function getConfig(config) { 14 | return config; 15 | } 16 | 17 | /** 18 | * @link https://nextjs.org/docs/api-reference/next.config.js/introduction 19 | */ 20 | module.exports = getConfig({ 21 | /** 22 | * Dynamic configuration available for the browser and server. 23 | * Note: requires `ssr: true` or a `getInitialProps` in `_app.tsx` 24 | * @link https://nextjs.org/docs/api-reference/next.config.js/runtime-configuration 25 | */ 26 | publicRuntimeConfig: { 27 | NODE_ENV: env.NODE_ENV, 28 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY, 29 | }, 30 | }); 31 | -------------------------------------------------------------------------------- /apps/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kubessandra/web", 3 | "version": "1.0.0", 4 | "private": false, 5 | "scripts": { 6 | "dev": "run-p next:dev stripe:dev", 7 | "stripe:dev": "stripe listen --forward-to localhost:3000/api/webhook/stripe", 8 | "next:dev": "next dev", 9 | "build": "next build", 10 | "start": "next start", 11 | "lint": "eslint ./src", 12 | "lint-fix": "pnpm lint --fix", 13 | "test": "test" 14 | }, 15 | "dependencies": { 16 | "@clerk/nextjs": "^4.9.1", 17 | "@headlessui/react": "^1.7.7", 18 | "@heroicons/react": "^2.0.13", 19 | "@kubessandra/database": "workspace:*", 20 | "@tanstack/react-query": "^4.20.4", 21 | "@trpc/client": "^10.6.0", 22 | "@trpc/next": "^10.6.0", 23 | "@trpc/react-query": "^10.6.0", 24 | "@trpc/server": "^10.6.0", 25 | "@types/micro-cors": "^0.1.2", 26 | "clsx": "^1.1.1", 27 | "eslint-config-next": "^13.1.0", 28 | "micro": "^10.0.1", 29 | "micro-cors": "^0.1.1", 30 | "next": "^13.1.0", 31 | "react": "^18.2.0", 32 | "react-dom": "^18.2.0", 33 | "stripe": "^11.5.0", 34 | "superjson": "^1.7.4", 35 | "zod": "^3.0.0" 36 | }, 37 | "devDependencies": { 38 | "@kubessandra/eslint-config-custom": "workspace:*", 39 | "@kubessandra/prettier-config": "workspace:*", 40 | "@tailwindcss/aspect-ratio": "^0.4.2", 41 | "@tailwindcss/forms": "^0.5.3", 42 | "@types/node": "^18.7.20", 43 | "@types/react": "^18.0.9", 44 | "autoprefixer": "^10.4.13", 45 | "eslint": "^8.30.0", 46 | "npm-run-all": "^4.1.5", 47 | "postcss": "^8.4.20", 48 | "prettier": "^2.7.1", 49 | "tailwindcss": "^3.2.4", 50 | "tsx": "^3.9.0", 51 | "typescript": "^4.8.3" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /apps/web/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /apps/web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kubessandra/KubeStack/845e363c4915a1127388bd1207c04df0fdfdc24d/apps/web/public/favicon.ico -------------------------------------------------------------------------------- /apps/web/public/kubestack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kubessandra/KubeStack/845e363c4915a1127388bd1207c04df0fdfdc24d/apps/web/public/kubestack.png -------------------------------------------------------------------------------- /apps/web/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /apps/web/src/components/DefaultLayout.tsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import { ReactNode } from "react"; 3 | 4 | type DefaultLayoutProps = { children: ReactNode }; 5 | 6 | export const DefaultLayout = ({ children }: DefaultLayoutProps) => { 7 | return ( 8 | <> 9 | 10 | Prisma Starter 11 | 12 | 13 | 14 |
{children}
15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /apps/web/src/components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { Fragment, useState } from "react"; 2 | import { Dialog, Menu, Transition } from "@headlessui/react"; 3 | import { 4 | Bars3BottomLeftIcon, 5 | BellIcon, 6 | XMarkIcon, 7 | } from "@heroicons/react/24/outline"; 8 | import { MagnifyingGlassIcon } from "@heroicons/react/20/solid"; 9 | import { Logo } from "./Logo"; 10 | import Link from "next/link"; 11 | import { getSidebarRoutes, IRoute } from "~/utils/routing"; 12 | import { useRouter } from "next/router"; 13 | import { UserButton } from "@clerk/nextjs"; 14 | 15 | function classNames(...classes: string[]) { 16 | return classes.filter(Boolean).join(" "); 17 | } 18 | 19 | interface NavigationObject extends IRoute { 20 | current: boolean; 21 | } 22 | 23 | interface LayoutProps { 24 | children: React.ReactNode; 25 | } 26 | 27 | export default function Layout(props: LayoutProps) { 28 | const { children } = props; 29 | const router = useRouter(); 30 | const [sidebarOpen, setSidebarOpen] = useState(false); 31 | 32 | const navigation: NavigationObject[] = getSidebarRoutes().map((route) => ({ 33 | ...route, 34 | current: route.path === router.pathname, 35 | })); 36 | 37 | return ( 38 | <> 39 |
40 | 41 | 46 | 55 |
56 | 57 | 58 |
59 | 68 | 69 | 78 |
79 | 90 |
91 |
92 | 96 | 97 | 98 |
99 | 126 |
127 |
128 |
129 | 132 |
133 |
134 |
135 | 136 | {/* Static sidebar for desktop */} 137 |
138 | {/* Sidebar component, swap this element with another sidebar if you like */} 139 |
140 | 144 | 145 | 146 |
147 | 174 |
175 |
176 |
177 |
178 |
179 | 187 |
188 |
189 |
190 | 193 |
194 |
195 |
200 | 207 |
208 |
209 |
210 |
211 | 218 | 219 | {/* Profile dropdown */} 220 | 221 |
222 |
223 |
224 | 225 |
226 |
227 |
228 | {children} 229 |
230 |
231 |
232 |
233 |
234 | 235 | ); 236 | } 237 | -------------------------------------------------------------------------------- /apps/web/src/components/Loading.tsx: -------------------------------------------------------------------------------- 1 | const Loading = () => { 2 | return
loading...
; 3 | }; 4 | 5 | export default Loading; 6 | -------------------------------------------------------------------------------- /apps/web/src/components/Logo.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | 3 | interface LogoProps { 4 | size: number; 5 | } 6 | 7 | export const Logo = (props: LogoProps) => { 8 | const { size } = props; 9 | return ( 10 | <> 11 | KubeStack 12 | Logo 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /apps/web/src/components/Pricing.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import { STARTER_PRICE_ID } from "~/utils/constants"; 3 | import { trpc } from "~/utils/trpc"; 4 | import Loading from "./Loading"; 5 | import PriceCard from "./Pricing/PriceCard"; 6 | 7 | const tiers = [ 8 | { 9 | id: "free-tier", 10 | name: "Free", 11 | priceMonthly: 0, 12 | description: 13 | "Lorem ipsum dolor sit amet consect etur adipisicing elit. Itaque amet indis perferendis.", 14 | features: ["Pariatur quod similique"], 15 | }, 16 | { 17 | id: STARTER_PRICE_ID, 18 | name: "Pro", 19 | priceMonthly: 25, 20 | description: 21 | "Lorem ipsum dolor sit amet consect etur adipisicing elit. Itaque amet indis perferendis.", 22 | features: [ 23 | "Pariatur quod similique", 24 | "Sapiente libero doloribus modi nostrum", 25 | ], 26 | }, 27 | ]; 28 | 29 | export default function Pricing() { 30 | const router = useRouter(); 31 | const createCheckoutSession = trpc.payment.createSession.useMutation(); 32 | const { data: paymentInfo, isLoading } = 33 | trpc.payment.getPaymentInfo.useQuery(); 34 | const customerPortalMutation = 35 | trpc.payment.createCustomerPortal.useMutation(); 36 | 37 | if (isLoading || !paymentInfo) return ; 38 | 39 | const { isSubscriptionActive } = paymentInfo; 40 | const handlePriceClick = async (priceId: string) => { 41 | let redirectUrl: string; 42 | if (priceId === "free-tier" || isSubscriptionActive) { 43 | redirectUrl = await customerPortalMutation.mutateAsync(); 44 | } else { 45 | redirectUrl = await createCheckoutSession.mutateAsync({ priceId }); 46 | } 47 | router.push(redirectUrl); 48 | }; 49 | const currentSubId = isSubscriptionActive ? STARTER_PRICE_ID : "free-tier"; 50 | 51 | return ( 52 |
53 |
54 |
55 | 60 |
61 |
62 |
63 |

64 | Pricing 65 |

66 |

67 | The right price for you,{" "} 68 |
69 | whoever you are 70 |

71 |

72 | Lorem ipsum dolor sit, amet consectetur adipisicing elit. Velit 73 | numquam eligendi quos odit doloribus molestiae voluptatum. 74 |

75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 | {tiers.map((tier) => { 83 | const isCurrentSub = currentSubId === tier.id; 84 | return ( 85 | 98 | ); 99 | })} 100 |
101 |
102 |
103 |
104 |
105 | ); 106 | } 107 | -------------------------------------------------------------------------------- /apps/web/src/components/Pricing/PriceCard.tsx: -------------------------------------------------------------------------------- 1 | import CheckIcon from "@heroicons/react/24/outline/CheckIcon"; 2 | 3 | interface PriceCardProps { 4 | name: string; 5 | id: string; 6 | priceMonthly: number; 7 | description: string; 8 | features: string[]; 9 | onButtonClick?: (id: string) => void; 10 | buttonTitle?: string; 11 | currentSub?: boolean; 12 | } 13 | 14 | const PriceCard = (props: PriceCardProps) => { 15 | const { 16 | name, 17 | id, 18 | priceMonthly, 19 | features, 20 | description, 21 | onButtonClick = () => undefined, 22 | buttonTitle = "Get Started Today", 23 | currentSub = false, 24 | } = props; 25 | return ( 26 |
30 |
31 |

35 | {name} 36 |

37 |
38 | ${priceMonthly} 39 | 40 | /mo 41 | 42 |
43 |

{description}

44 |
45 |
46 |
47 |
    48 | {features.map((feature) => ( 49 |
  • 50 |
    51 |
    56 |

    57 | {feature} 58 |

    59 |
  • 60 | ))} 61 |
62 |
63 | 71 |
72 |
73 |
74 |
75 | ); 76 | }; 77 | 78 | export default PriceCard; 79 | -------------------------------------------------------------------------------- /apps/web/src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { getAuth, withClerkMiddleware } from "@clerk/nextjs/server"; 2 | import { NextResponse } from "next/server"; 3 | import type { NextRequest } from "next/server"; 4 | import { publicRoutes } from "~/utils/routing"; 5 | 6 | const publicPaths = Object.values(publicRoutes).map((route) => route.path); 7 | const isPublic = (path: string) => { 8 | return publicPaths.find((x) => path.match(new RegExp(`^${x}$`))); 9 | }; 10 | 11 | export default withClerkMiddleware((request: NextRequest) => { 12 | if (isPublic(request.nextUrl.pathname)) { 13 | return NextResponse.next(); 14 | } 15 | 16 | const { userId } = getAuth(request); 17 | 18 | if (!userId) { 19 | const signInUrl = new URL(publicRoutes.SIGN_IN.path, request.url); 20 | signInUrl.searchParams.set("redirect_url", request.url); 21 | return NextResponse.redirect(signInUrl); 22 | } 23 | 24 | return NextResponse.next(); 25 | }); 26 | 27 | // Stop Middleware running on static files 28 | export const config = { 29 | matcher: ["/(.*?trpc.*?|(?!static|.*\\..*|_next|favicon.ico|api).*)"], 30 | }; 31 | -------------------------------------------------------------------------------- /apps/web/src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { ClerkProvider } from "@clerk/nextjs"; 2 | import type { NextPage } from "next"; 3 | import type { AppType, AppProps } from "next/app"; 4 | import type { ReactElement, ReactNode } from "react"; 5 | import { DefaultLayout } from "~/components/DefaultLayout"; 6 | import { trpc } from "~/utils/trpc"; 7 | 8 | import "../styles/global.css"; 9 | 10 | export type NextPageWithLayout< 11 | TProps = Record, 12 | TInitialProps = TProps 13 | > = NextPage & { 14 | getLayout?: (page: ReactElement) => ReactNode; 15 | }; 16 | 17 | type AppPropsWithLayout = AppProps & { 18 | Component: NextPageWithLayout; 19 | }; 20 | 21 | const MyApp = (({ Component, pageProps }: AppPropsWithLayout) => { 22 | const getLayout = 23 | Component.getLayout ?? ((page) => {page}); 24 | 25 | return ( 26 | 27 | {getLayout()} 28 | 29 | ); 30 | }) as AppType; 31 | 32 | export default trpc.withTRPC(MyApp); 33 | -------------------------------------------------------------------------------- /apps/web/src/pages/api/trpc/[trpc].ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains tRPC's HTTP response handler 3 | */ 4 | import * as trpcNext from "@trpc/server/adapters/next"; 5 | import { createContext } from "~/server/context"; 6 | import { appRouter } from "~/server/routers/_app"; 7 | 8 | export default trpcNext.createNextApiHandler({ 9 | router: appRouter, 10 | /** 11 | * @link https://trpc.io/docs/context 12 | */ 13 | createContext, 14 | /** 15 | * @link https://trpc.io/docs/error-handling 16 | */ 17 | onError({ error }) { 18 | if (error.code === "INTERNAL_SERVER_ERROR") { 19 | // send to bug reporting 20 | console.error("Something went wrong", error); 21 | } 22 | }, 23 | /** 24 | * Enable query batching 25 | */ 26 | batching: { 27 | enabled: true, 28 | }, 29 | /** 30 | * @link https://trpc.io/docs/caching#api-response-caching 31 | */ 32 | // responseMeta() { 33 | // // ... 34 | // }, 35 | }); 36 | -------------------------------------------------------------------------------- /apps/web/src/pages/api/webhook/stripe.ts: -------------------------------------------------------------------------------- 1 | import Cors from "micro-cors"; 2 | import { buffer } from "micro"; 3 | import type { NextApiRequest, NextApiResponse } from "next"; 4 | import Stripe from "stripe"; 5 | import { env } from "~/server/env"; 6 | import { stripe } from "~/server/payment/stripe"; 7 | import { prisma } from "~/server/prisma"; 8 | 9 | const cors = Cors({ 10 | allowMethods: ["POST", "HEAD"], 11 | }); 12 | 13 | const getNewSubDate = () => { 14 | const newSubDate = new Date(); 15 | newSubDate.setMonth(newSubDate.getMonth() + 1); 16 | newSubDate.setHours(newSubDate.getHours() + 48); 17 | return newSubDate; 18 | }; 19 | 20 | const billingPaid = async (invoice: Stripe.Invoice) => { 21 | if (!invoice.customer) 22 | throw new Error(`No customers assign to this invoice: ${invoice.id}`); 23 | const customerId = 24 | typeof invoice.customer === "string" 25 | ? invoice.customer 26 | : invoice.customer.id; 27 | const newSubDate = getNewSubDate(); 28 | await prisma.paymentInfo.update({ 29 | where: { 30 | customerId, 31 | }, 32 | data: { 33 | subscriptionEndAt: newSubDate, 34 | }, 35 | }); 36 | }; 37 | 38 | const stripeHandler = async (req: NextApiRequest, res: NextApiResponse) => { 39 | let event: Stripe.Event; 40 | const endpointSecret = env.STRIPE_WEBHOOK_SECRET; 41 | const signature = req.headers["stripe-signature"] as string; 42 | const buf = await buffer(req); 43 | try { 44 | event = stripe.webhooks.constructEvent( 45 | buf.toString(), 46 | signature, 47 | endpointSecret 48 | ); 49 | } catch (err: any) { 50 | console.log(`⚠️ Webhook signature verification failed.`, err.message); 51 | res.status(400); 52 | res.send("Signature verification failed"); 53 | return; 54 | } 55 | switch (event.type) { 56 | case "invoice.paid": 57 | console.log("[Payment], Invoice paid"); 58 | let invoice = event.data.object as Stripe.Invoice; 59 | await billingPaid(invoice); 60 | break; 61 | case "invoice.payment_failed": 62 | invoice = event.data.object as Stripe.Invoice; 63 | console.warn( 64 | `[PaymentFailed] payement failed for user ${invoice.customer}, invoiceId: ${invoice.id}` 65 | ); 66 | // TODO notify user that the payment failed (RECURRING) 67 | break; 68 | default: 69 | console.log(`Unhandled event type ${event.type}. (Not critical)`); 70 | } 71 | res.status(200); 72 | res.send("success"); 73 | }; 74 | 75 | export default cors(stripeHandler as any); 76 | export const config = { api: { bodyParser: false } }; 77 | -------------------------------------------------------------------------------- /apps/web/src/pages/dashboard.tsx: -------------------------------------------------------------------------------- 1 | import { UserButton, useUser } from "@clerk/nextjs"; 2 | import { ReactElement } from "react"; 3 | import Layout from "~/components/Layout"; 4 | import Loading from "~/components/Loading"; 5 | 6 | const Dashboard = () => { 7 | const { user, isLoaded } = useUser(); 8 | 9 | if (!isLoaded || !user) return ; 10 | 11 | return ( 12 |
13 |

Welcome to the dashboard

14 |

{user.emailAddresses[0]?.emailAddress || "none"}

15 | 16 |
17 | ); 18 | }; 19 | 20 | Dashboard.getLayout = (page: ReactElement) => {page}; 21 | 22 | export default Dashboard; 23 | -------------------------------------------------------------------------------- /apps/web/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { Fragment } from "react"; 2 | import { Popover, Transition } from "@headlessui/react"; 3 | import { 4 | ArrowPathIcon, 5 | Bars3Icon, 6 | CloudArrowUpIcon, 7 | CogIcon, 8 | LockClosedIcon, 9 | ServerIcon, 10 | ShieldCheckIcon, 11 | XMarkIcon, 12 | } from "@heroicons/react/24/outline"; 13 | import { 14 | ArrowTopRightOnSquareIcon, 15 | ChevronRightIcon, 16 | } from "@heroicons/react/20/solid"; 17 | import Link from "next/link"; 18 | import { Logo } from "~/components/Logo"; 19 | import securedRoutes, { publicRoutes } from "~/utils/routing"; 20 | import { SignedIn, SignedOut } from "@clerk/nextjs"; 21 | 22 | const navigation = [ 23 | { name: "Product", href: "#" }, 24 | { name: "Features", href: "#" }, 25 | { name: "Marketplace", href: "#" }, 26 | { name: "Company", href: "#" }, 27 | ]; 28 | const features = [ 29 | { 30 | name: "Push to Deploy", 31 | description: 32 | "Ac tincidunt sapien vehicula erat auctor pellentesque rhoncus. Et magna sit morbi vitae lobortis.", 33 | icon: CloudArrowUpIcon, 34 | }, 35 | { 36 | name: "SSL Certificates", 37 | description: 38 | "Qui aut temporibus nesciunt vitae dicta repellat sit dolores pariatur. Temporibus qui illum aut.", 39 | icon: LockClosedIcon, 40 | }, 41 | { 42 | name: "Simple Queues", 43 | description: 44 | "Rerum quas incidunt deleniti quaerat suscipit mollitia. Amet repellendus ut odit dolores qui.", 45 | icon: ArrowPathIcon, 46 | }, 47 | { 48 | name: "Advanced Security", 49 | description: 50 | "Ullam laboriosam est voluptatem maxime ut mollitia commodi. Et dignissimos suscipit perspiciatis.", 51 | icon: ShieldCheckIcon, 52 | }, 53 | { 54 | name: "Powerful API", 55 | description: 56 | "Ab a facere voluptatem in quia corrupti veritatis aliquam. Veritatis labore quaerat ipsum quaerat id.", 57 | icon: CogIcon, 58 | }, 59 | { 60 | name: "Database Backups", 61 | description: 62 | "Quia qui et est officia cupiditate qui consectetur. Ratione similique et impedit ea ipsum et.", 63 | icon: ServerIcon, 64 | }, 65 | ]; 66 | const blogPosts = [ 67 | { 68 | id: 1, 69 | title: "Boost your conversion rate", 70 | href: "#", 71 | date: "Mar 16, 2020", 72 | datetime: "2020-03-16", 73 | category: { name: "Article", href: "#" }, 74 | imageUrl: 75 | "https://images.unsplash.com/photo-1496128858413-b36217c2ce36?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1679&q=80", 76 | preview: 77 | "Lorem ipsum dolor sit amet consectetur adipisicing elit. Architecto accusantium praesentium eius, ut atque fuga culpa, similique sequi cum eos quis dolorum.", 78 | author: { 79 | name: "Roel Aufderehar", 80 | imageUrl: 81 | "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80", 82 | href: "#", 83 | }, 84 | readingLength: "6 min", 85 | }, 86 | { 87 | id: 2, 88 | title: "How to use search engine optimization to drive sales", 89 | href: "#", 90 | date: "Mar 10, 2020", 91 | datetime: "2020-03-10", 92 | category: { name: "Video", href: "#" }, 93 | imageUrl: 94 | "https://images.unsplash.com/photo-1547586696-ea22b4d4235d?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1679&q=80", 95 | preview: 96 | "Lorem ipsum dolor sit amet consectetur adipisicing elit. Velit facilis asperiores porro quaerat doloribus, eveniet dolore. Adipisci tempora aut inventore optio animi., tempore temporibus quo laudantium.", 97 | author: { 98 | name: "Brenna Goyette", 99 | imageUrl: 100 | "https://images.unsplash.com/photo-1550525811-e5869dd03032?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80", 101 | href: "#", 102 | }, 103 | readingLength: "4 min", 104 | }, 105 | { 106 | id: 3, 107 | title: "Improve your customer experience", 108 | href: "#", 109 | date: "Feb 12, 2020", 110 | datetime: "2020-02-12", 111 | category: { name: "Case Study", href: "#" }, 112 | imageUrl: 113 | "https://images.unsplash.com/photo-1492724441997-5dc865305da7?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1679&q=80", 114 | preview: 115 | "Lorem ipsum dolor sit amet consectetur adipisicing elit. Sint harum rerum voluptatem quo recusandae magni placeat saepe molestiae, sed excepturi cumque corporis perferendis hic.", 116 | author: { 117 | name: "Daniela Metz", 118 | imageUrl: 119 | "https://images.unsplash.com/photo-1487412720507-e7ab37603c6f?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80", 120 | href: "#", 121 | }, 122 | readingLength: "11 min", 123 | }, 124 | ]; 125 | const footerNavigation = { 126 | solutions: [ 127 | { name: "Marketing", href: "#" }, 128 | { name: "Analytics", href: "#" }, 129 | { name: "Commerce", href: "#" }, 130 | { name: "Insights", href: "#" }, 131 | ], 132 | support: [ 133 | { name: "Pricing", href: "#" }, 134 | { name: "Documentation", href: "#" }, 135 | { name: "Guides", href: "#" }, 136 | { name: "API Status", href: "#" }, 137 | ], 138 | company: [ 139 | { name: "About", href: "#" }, 140 | { name: "Blog", href: "#" }, 141 | { name: "Jobs", href: "#" }, 142 | { name: "Press", href: "#" }, 143 | { name: "Partners", href: "#" }, 144 | ], 145 | legal: [ 146 | { name: "Claim", href: "#" }, 147 | { name: "Privacy", href: "#" }, 148 | { name: "Terms", href: "#" }, 149 | ], 150 | social: [ 151 | { 152 | name: "Facebook", 153 | href: "#", 154 | icon: (props) => ( 155 | 156 | 161 | 162 | ), 163 | }, 164 | { 165 | name: "Instagram", 166 | href: "#", 167 | icon: (props) => ( 168 | 169 | 174 | 175 | ), 176 | }, 177 | { 178 | name: "Twitter", 179 | href: "#", 180 | icon: (props) => ( 181 | 182 | 183 | 184 | ), 185 | }, 186 | { 187 | name: "GitHub", 188 | href: "#", 189 | icon: (props) => ( 190 | 191 | 196 | 197 | ), 198 | }, 199 | { 200 | name: "Dribbble", 201 | href: "#", 202 | icon: (props: any) => ( 203 | 204 | 209 | 210 | ), 211 | }, 212 | ], 213 | }; 214 | 215 | export default function Landing() { 216 | return ( 217 |
218 |
219 | 220 |
221 | 274 |
275 | 276 | 285 | 289 |
290 |
291 |
292 | 297 |
298 |
299 | 300 | Close menu 301 | 303 |
304 |
305 |
306 |
307 | {navigation.map((item) => ( 308 | 313 | {item.name} 314 | 315 | ))} 316 |
317 | 325 |
326 |

327 | Existing customer?{" "} 328 | 332 | Login 333 | 334 |

335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 |
344 |
345 |
346 |
347 | 351 | 352 | We're hiring 353 | 354 | 355 | Visit our careers page 356 | 357 | 362 |

363 | A better way to 364 | 365 | ship web apps 366 | 367 |

368 |

369 | Anim aute id magna aliqua ad ad non deserunt sunt. Qui 370 | irure qui Lorem cupidatat commodo. Elit sunt amet fugiat 371 | veniam occaecat fugiat. 372 |

373 |
374 |
378 |
379 |
380 | 383 | 389 |
390 |
391 | 397 |
398 |
399 |

400 | Start your free 14-day trial, no credit card 401 | necessary. By providing your email, you agree to our{" "} 402 | 403 | terms of service 404 | 405 | . 406 |

407 |
408 |
409 |
410 |
411 |
412 |
413 | {/* Illustration taken from Lucid Illustrations: https://lucid.pixsellz.io/ */} 414 | 419 |
420 |
421 |
422 |
423 |
424 | 425 | {/* Feature section with screenshot */} 426 |
427 |
428 |
429 |

430 | Serverless 431 |

432 |

433 | No server? No problem. 434 |

435 |

436 | Phasellus lorem quam molestie id quisque diam aenean nulla in. 437 | Accumsan in quis quis nunc, ullamcorper malesuada. Eleifend 438 | condimentum id viverra nulla. 439 |

440 |
441 |
442 | 447 |
448 |
449 |
450 | 451 | {/* Feature section with grid */} 452 |
453 |
454 |

455 | Deploy faster 456 |

457 |

458 | Everything you need to deploy your app 459 |

460 |

461 | Phasellus lorem quam molestie id quisque diam aenean nulla in. 462 | Accumsan in quis quis nunc, ullamcorper malesuada. Eleifend 463 | condimentum id viverra nulla. 464 |

465 |
466 |
467 | {features.map((feature) => ( 468 |
469 |
470 |
471 |
472 | 473 | 478 |
479 |

480 | {feature.name} 481 |

482 |

483 | {feature.description} 484 |

485 |
486 |
487 |
488 | ))} 489 |
490 |
491 |
492 |
493 | 494 | {/* Testimonial section */} 495 |
496 |
497 |
498 | 512 |
513 |
514 |
515 |
516 | 524 |

525 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. 526 | Sed urna nulla vitae laoreet augue. Amet feugiat est 527 | integer dolor auctor adipiscing nunc urna, sit. 528 |

529 |
530 |
531 |

532 | Judith Black 533 |

534 |

535 | CEO at PureInsights 536 |

537 |
538 |
539 |
540 |
541 |
542 |
543 | 544 | {/* Blog section */} 545 |
546 |
547 |
548 |

Learn

549 |

550 | Helpful Resources 551 |

552 |

553 | Phasellus lorem quam molestie id quisque diam aenean nulla in. 554 | Accumsan in quis quis nunc, ullamcorper malesuada. Eleifend 555 | condimentum id viverra nulla. 556 |

557 |
558 |
559 | {blogPosts.map((post) => ( 560 |
564 |
565 | 570 |
571 |
572 | 590 |
591 |
592 | 593 | {post.author.name} 598 | 599 |
600 |
601 |

602 | 606 | {post.author.name} 607 | 608 |

609 |
610 | 611 | 612 | {post.readingLength} read 613 |
614 |
615 |
616 |
617 |
618 | ))} 619 |
620 |
621 |
622 | 623 | {/* CTA Section */} 624 |
625 |
626 | 631 | 636 |
637 |
638 |

639 | Award winning support 640 |

641 |

642 | We’re here to help 643 |

644 |

645 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Et, 646 | egestas tempus tellus etiam sed. Quam a scelerisque amet 647 | ullamcorper eu enim et fermentum, augue. Aliquet amet volutpat 648 | quisque ut interdum tincidunt duis. 649 |

650 | 664 |
665 |
666 |
667 |
668 |
669 | 672 |
673 |
674 |
675 | Company name 680 |

681 | Making the world a better place through constructing elegant 682 | hierarchies. 683 |

684 |
685 | {footerNavigation.social.map((item) => ( 686 | 691 | {item.name} 692 | 694 | ))} 695 |
696 |
697 |
698 |
699 |
700 |

701 | Solutions 702 |

703 | 715 |
716 |
717 |

718 | Support 719 |

720 | 732 |
733 |
734 |
735 |
736 |

737 | Company 738 |

739 | 751 |
752 |
753 |

754 | Legal 755 |

756 | 768 |
769 |
770 |
771 |
772 |
773 |

774 | © 2020 Your Company, Inc. All rights reserved. 775 |

776 |
777 |
778 |
779 |
780 |
781 | ); 782 | } 783 | -------------------------------------------------------------------------------- /apps/web/src/pages/payment/success.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from "react"; 2 | import { TWITTER_LINK } from "~/utils/constants"; 3 | import Layout from "~/components/Layout"; 4 | import Link from "next/link"; 5 | import securedRoutes from "~/utils/routing"; 6 | 7 | export default function SuccessPage() { 8 | return ( 9 |
10 |
11 | 17 | 22 | 23 | 31 | 32 | 33 | 34 | 35 | 36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | 44 | You can contact us at any moment on our twitter.{" "} 45 | 49 | 52 | 53 |
54 |
55 |
56 |

57 | Thank you for your subscription{" "} 58 |

59 |

60 | Let's create some content now 61 |

62 |
63 | 67 | Get started 68 | 71 | 72 |
73 |
74 |
75 | 81 | 86 | 87 | 95 | 96 | 97 | 98 | 99 | 100 |
101 |
102 |
103 |
104 |
105 |
106 | ); 107 | } 108 | 109 | SuccessPage.getLayout = (page: ReactElement) => {page}; 110 | -------------------------------------------------------------------------------- /apps/web/src/pages/plan.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import { ReactElement } from "react"; 3 | import Layout from "~/components/Layout"; 4 | import Pricing from "~/components/Pricing"; 5 | import { trpc } from "~/utils/trpc"; 6 | 7 | const PlanPage = () => { 8 | const router = useRouter(); 9 | const customerPortalMutation = 10 | trpc.payment.createCustomerPortal.useMutation(); 11 | 12 | return ( 13 |
14 | 15 | 24 |
25 | ); 26 | }; 27 | 28 | PlanPage.getLayout = (page: ReactElement) => {page}; 29 | 30 | export default PlanPage; 31 | -------------------------------------------------------------------------------- /apps/web/src/pages/settings.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from "react"; 2 | import Layout from "~/components/Layout"; 3 | 4 | const SettingsPage = () => { 5 | return
Settings page
; 6 | }; 7 | 8 | SettingsPage.getLayout = (page: ReactElement) => {page}; 9 | 10 | export default SettingsPage; 11 | -------------------------------------------------------------------------------- /apps/web/src/pages/sign-in/[[...index]].tsx: -------------------------------------------------------------------------------- 1 | import { SignIn } from "@clerk/nextjs"; 2 | import securedRoutes from "~/utils/routing"; 3 | 4 | const SignInPage = () => ( 5 |
6 | 12 |
13 | ); 14 | 15 | export default SignInPage; 16 | -------------------------------------------------------------------------------- /apps/web/src/pages/sign-up/[[...index]].tsx: -------------------------------------------------------------------------------- 1 | import { SignUp } from "@clerk/nextjs"; 2 | import securedRoutes from "~/utils/routing"; 3 | 4 | const SignUpPage = () => ( 5 |
6 | 12 |
13 | ); 14 | 15 | export default SignUpPage; 16 | -------------------------------------------------------------------------------- /apps/web/src/server/auth/middleware.ts: -------------------------------------------------------------------------------- 1 | import { TRPCError } from "@trpc/server"; 2 | import { middleware, publicProcedure } from "../trpc"; 3 | 4 | export const isAuthed = middleware(({ next, ctx }) => { 5 | if (!ctx.session || !ctx.user) { 6 | throw new TRPCError({ 7 | code: "UNAUTHORIZED", 8 | message: "No Session available", 9 | cause: "No Session available", 10 | }); 11 | } 12 | if (!ctx.user.paymentInfo) { 13 | throw new TRPCError({ 14 | code: "UNAUTHORIZED", 15 | message: "No customer registered on the platform", 16 | cause: "No customer registered on the platform", 17 | }); 18 | } 19 | return next({ 20 | ctx: { 21 | session: ctx.session, 22 | user: { 23 | ...ctx.user, 24 | paymentInfo: ctx.user.paymentInfo, 25 | }, 26 | }, 27 | }); 28 | }); 29 | 30 | export const authProcedure = publicProcedure.use(isAuthed); 31 | -------------------------------------------------------------------------------- /apps/web/src/server/auth/types.ts: -------------------------------------------------------------------------------- 1 | export interface Session { 2 | externalId: string; 3 | email: string; 4 | } 5 | -------------------------------------------------------------------------------- /apps/web/src/server/context.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import * as trpc from "@trpc/server"; 3 | import * as trpcNext from "@trpc/server/adapters/next"; 4 | import { Session } from "./auth/types"; 5 | import { prisma } from "./prisma"; 6 | import { createPaymentAccount, createUser } from "./core/user"; 7 | import { clerkClient, getAuth } from "@clerk/nextjs/server"; 8 | 9 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 10 | interface CreateContextOptions { 11 | session: Session | null; 12 | } 13 | 14 | /** 15 | * Inner function for `createContext` where we create the context. 16 | * This is useful for testing when we don't want to mock Next.js' request/response 17 | */ 18 | export async function createContextInner(_opts: CreateContextOptions) { 19 | const externalId = _opts.session?.externalId; 20 | const email = _opts.session?.email; 21 | if (externalId && email) { 22 | let userInfo = await prisma.userInfo.findUnique({ 23 | where: { externalId }, 24 | include: { paymentInfo: true }, 25 | }); 26 | if (!userInfo) { 27 | userInfo = await createUser({ externalId, email }); 28 | } 29 | if (!userInfo.paymentInfo) { 30 | userInfo = await createPaymentAccount({ userId: userInfo.id, email }); 31 | } 32 | return { session: _opts.session, user: userInfo }; 33 | } 34 | return { session: null, user: null }; 35 | } 36 | 37 | export type Context = trpc.inferAsyncReturnType; 38 | 39 | /** 40 | * Creates context for an incoming request 41 | * @link https://trpc.io/docs/context 42 | */ 43 | export async function createContext( 44 | opts: trpcNext.CreateNextContextOptions 45 | ): Promise { 46 | // for API-response caching see https://trpc.io/docs/caching 47 | let session = null; 48 | const { userId } = getAuth(opts.req); 49 | const user = userId ? await clerkClient.users.getUser(userId) : null; 50 | if (user && userId) { 51 | const email = user.emailAddresses[0]?.emailAddress; 52 | if (!email) throw new Error("No email address for the session"); 53 | session = { 54 | externalId: userId, 55 | email, 56 | }; 57 | } 58 | return await createContextInner({ session }); 59 | } 60 | -------------------------------------------------------------------------------- /apps/web/src/server/core/user.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * This is an example router, you can delete this file and then update `../pages/api/trpc/[trpc].tsx` 4 | */ 5 | import { router } from "../trpc"; 6 | 7 | import { createCustomer } from "../payment/stripe"; 8 | import { prisma } from "../prisma"; 9 | 10 | interface CreateUserParams { 11 | externalId: string; 12 | email: string; 13 | } 14 | 15 | export const createUser = async ({ externalId, email }: CreateUserParams) => { 16 | const userCreated = await prisma.userInfo.create({ 17 | data: { externalId, email }, 18 | include: { 19 | paymentInfo: true, 20 | }, 21 | }); 22 | return userCreated; 23 | }; 24 | 25 | interface CreatePaymentAccountParams { 26 | userId: string; 27 | email: string; 28 | } 29 | 30 | export const createPaymentAccount = async ({ 31 | userId, 32 | email, 33 | }: CreatePaymentAccountParams) => { 34 | console.log("[Payment], Creating stripe account for: ", userId); 35 | const newCustomer = await createCustomer({ userId, email }); 36 | const userUpdated = await prisma.userInfo.update({ 37 | where: { id: userId }, 38 | data: { 39 | paymentInfo: { 40 | create: { 41 | customerId: newCustomer.id, 42 | }, 43 | }, 44 | }, 45 | include: { 46 | paymentInfo: true, 47 | }, 48 | }); 49 | return userUpdated; 50 | }; 51 | 52 | export const paymentRouter = router({}); 53 | -------------------------------------------------------------------------------- /apps/web/src/server/env.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /** 3 | * This file is included in `/next.config.js` which ensures the app isn't built with invalid env vars. 4 | * It has to be a `.js`-file to be imported there. 5 | */ 6 | /* eslint-disable @typescript-eslint/no-var-requires */ 7 | const { z } = require("zod"); 8 | 9 | /** 10 | * Public and private env are handled by the next.config.js 11 | */ 12 | 13 | const envSchema = z.object({ 14 | STRIPE_SECRET: z.string(), 15 | STRIPE_WEBHOOK_SECRET: z.string(), 16 | FRONT_URL: z.string(), 17 | NODE_ENV: z.enum(["development", "test", "production"]), 18 | CLERK_SECRET_KEY: z.string(), 19 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: z.string(), 20 | }); 21 | 22 | const env = envSchema.safeParse(process.env); 23 | 24 | if (!env.success) { 25 | console.error( 26 | "❌ Invalid environment variables:", 27 | JSON.stringify(env.error.format(), null, 4) 28 | ); 29 | process.exit(1); 30 | } 31 | module.exports.env = env.data; 32 | -------------------------------------------------------------------------------- /apps/web/src/server/payment/stripe.ts: -------------------------------------------------------------------------------- 1 | import Stripe from "stripe"; 2 | import { SUCCESS_URL } from "~/utils/constants"; 3 | import securedRoutes from "~/utils/routing"; 4 | import { env } from "../env"; 5 | 6 | export const stripe = new Stripe(env.STRIPE_SECRET, { 7 | apiVersion: "2022-11-15", 8 | }); 9 | 10 | interface CreateCheckoutParams { 11 | userId: string; 12 | priceId: string; 13 | customerId: string; 14 | } 15 | 16 | export const isHavingActiveSubscription = (subEndDate?: Date | null): boolean => 17 | !!(subEndDate && subEndDate?.getTime() > Date.now()); 18 | 19 | export const createCheckoutSession = async ({ 20 | priceId, 21 | userId, 22 | customerId, 23 | }: CreateCheckoutParams) => { 24 | const session = await stripe.checkout.sessions.create({ 25 | billing_address_collection: "auto", 26 | line_items: [ 27 | { 28 | price: priceId, 29 | quantity: 1, 30 | }, 31 | ], 32 | customer: customerId, 33 | metadata: { 34 | userId, 35 | }, 36 | mode: "subscription", 37 | success_url: `${env.FRONT_URL}/${SUCCESS_URL}?session_id={CHECKOUT_SESSION_ID}`, 38 | cancel_url: `${env.FRONT_URL}/${securedRoutes.DASHBOARD.path}`, 39 | }); 40 | return session; 41 | }; 42 | 43 | // --- 44 | 45 | interface CreateCustomerPortalParams { 46 | customerId: string; 47 | } 48 | 49 | export const createCustomerPortal = async ({ 50 | customerId, 51 | }: CreateCustomerPortalParams) => { 52 | const returnUrl = `${env.FRONT_URL}/${securedRoutes.DASHBOARD.path}`; 53 | const portalSession = await stripe.billingPortal.sessions.create({ 54 | customer: customerId, 55 | return_url: returnUrl, 56 | }); 57 | return portalSession.url; 58 | }; 59 | 60 | // --- 61 | 62 | interface CreateCustomerProps { 63 | userId: string; 64 | email: string; 65 | } 66 | 67 | export const createCustomer = async ({ 68 | userId, 69 | email, 70 | }: CreateCustomerProps) => { 71 | return await stripe.customers.create({ 72 | email, 73 | metadata: { 74 | userId, 75 | }, 76 | }); 77 | }; 78 | -------------------------------------------------------------------------------- /apps/web/src/server/prisma.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Instantiates a single instance PrismaClient and save it on the global object. 3 | * @link https://www.prisma.io/docs/support/help-articles/nextjs-prisma-client-dev-practices 4 | */ 5 | import { env } from "./env"; 6 | import { PrismaClient } from "@kubessandra/database"; 7 | 8 | const prismaGlobal = global as typeof global & { 9 | prisma?: PrismaClient; 10 | }; 11 | 12 | export const prisma: PrismaClient = 13 | prismaGlobal.prisma || 14 | new PrismaClient({ 15 | log: 16 | env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"], 17 | }); 18 | 19 | if (env.NODE_ENV !== "production") { 20 | prismaGlobal.prisma = prisma; 21 | } 22 | -------------------------------------------------------------------------------- /apps/web/src/server/routers/_app.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains the root router of your tRPC-backend 3 | */ 4 | import { publicProcedure, router } from "../trpc"; 5 | import { postRouter } from "./post"; 6 | import { paymentRouter } from "./payment"; 7 | 8 | export const appRouter = router({ 9 | healthcheck: publicProcedure.query(() => "yay!"), 10 | payment: paymentRouter, 11 | post: postRouter, 12 | }); 13 | 14 | export type AppRouter = typeof appRouter; 15 | -------------------------------------------------------------------------------- /apps/web/src/server/routers/payment.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * This is an example router, you can delete this file and then update `../pages/api/trpc/[trpc].tsx` 4 | */ 5 | import { router } from "../trpc"; 6 | import { TRPCError } from "@trpc/server"; 7 | import { z } from "zod"; 8 | 9 | import { authProcedure } from "../auth/middleware"; 10 | 11 | import { 12 | createCheckoutSession, 13 | createCustomerPortal, 14 | isHavingActiveSubscription, 15 | } from "../payment/stripe"; 16 | 17 | export const paymentRouter = router({ 18 | getPaymentInfo: authProcedure.query(async ({ ctx }) => { 19 | const paymentInfo = ctx.user.paymentInfo; 20 | const isSubscriptionActive = isHavingActiveSubscription( 21 | ctx.user.paymentInfo.subscriptionEndAt 22 | ); 23 | return { 24 | ...paymentInfo, 25 | isSubscriptionActive, 26 | }; 27 | }), 28 | createSession: authProcedure 29 | .input( 30 | z.object({ 31 | priceId: z.string().max(250), 32 | }) 33 | ) 34 | .mutation(async ({ input, ctx }) => { 35 | if (isHavingActiveSubscription(ctx.user.paymentInfo.subscriptionEndAt)) { 36 | throw new TRPCError({ 37 | code: "INTERNAL_SERVER_ERROR", 38 | message: "Already having a subscription running", 39 | }); 40 | } 41 | const userId = ctx.user.id; 42 | const customerId = ctx.user.paymentInfo.customerId; 43 | const session = await createCheckoutSession({ 44 | userId: userId, 45 | customerId, 46 | priceId: input.priceId, 47 | }); 48 | 49 | if (!session.url) { 50 | throw new TRPCError({ 51 | code: "INTERNAL_SERVER_ERROR", 52 | message: "No session url, the session is probably not active", 53 | }); 54 | } 55 | return session.url; 56 | }), 57 | createCustomerPortal: authProcedure.mutation(async ({ ctx }) => { 58 | const customerId = ctx.user.paymentInfo.customerId; 59 | const portalUrl = await createCustomerPortal({ customerId }); 60 | return portalUrl; 61 | }), 62 | }); 63 | -------------------------------------------------------------------------------- /apps/web/src/server/routers/post.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Integration test example for the `post` router 3 | */ 4 | import { createContextInner } from "../context"; 5 | import { AppRouter, appRouter } from "./_app"; 6 | import { inferProcedureInput } from "@trpc/server"; 7 | 8 | test("add and get post", async () => { 9 | const ctx = await createContextInner({}); 10 | const caller = appRouter.createCaller(ctx); 11 | 12 | const input: inferProcedureInput = { 13 | text: "hello test", 14 | title: "hello test", 15 | }; 16 | 17 | const post = await caller.post.add(input); 18 | const byId = await caller.post.byId({ id: post.id }); 19 | 20 | expect(byId).toMatchObject(input); 21 | }); 22 | -------------------------------------------------------------------------------- /apps/web/src/server/routers/post.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * This is an example router, you can delete this file and then update `../pages/api/trpc/[trpc].tsx` 4 | */ 5 | import { router, publicProcedure } from "../trpc"; 6 | import { Prisma } from "@prisma/client"; 7 | import { TRPCError } from "@trpc/server"; 8 | import { z } from "zod"; 9 | import { prisma } from "~/server/prisma"; 10 | 11 | /** 12 | * Default selector for Post. 13 | * It's important to always explicitly say which fields you want to return in order to not leak extra information 14 | * @see https://github.com/prisma/prisma/issues/9353 15 | */ 16 | const defaultPostSelect = Prisma.validator()({ 17 | id: true, 18 | title: true, 19 | text: true, 20 | createdAt: true, 21 | updatedAt: true, 22 | }); 23 | 24 | export const postRouter = router({ 25 | list: publicProcedure 26 | .input( 27 | z.object({ 28 | limit: z.number().min(1).max(100).nullish(), 29 | cursor: z.string().nullish(), 30 | }) 31 | ) 32 | .query(async ({ input }) => { 33 | /** 34 | * For pagination docs you can have a look here 35 | * @see https://trpc.io/docs/useInfiniteQuery 36 | * @see https://www.prisma.io/docs/concepts/components/prisma-client/pagination 37 | */ 38 | 39 | const limit = input.limit ?? 50; 40 | const { cursor } = input; 41 | 42 | const items = await prisma.post.findMany({ 43 | select: defaultPostSelect, 44 | // get an extra item at the end which we'll use as next cursor 45 | take: limit + 1, 46 | where: {}, 47 | cursor: cursor 48 | ? { 49 | id: cursor, 50 | } 51 | : undefined, 52 | orderBy: { 53 | createdAt: "desc", 54 | }, 55 | }); 56 | let nextCursor: typeof cursor | undefined = undefined; 57 | if (items.length > limit) { 58 | // Remove the last item and use it as next cursor 59 | 60 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 61 | const nextItem = items.pop()!; 62 | nextCursor = nextItem.id; 63 | } 64 | 65 | return { 66 | items: items.reverse(), 67 | nextCursor, 68 | }; 69 | }), 70 | byId: publicProcedure 71 | .input( 72 | z.object({ 73 | id: z.string(), 74 | }) 75 | ) 76 | .query(async ({ input }) => { 77 | const { id } = input; 78 | const post = await prisma.post.findUnique({ 79 | where: { id }, 80 | select: defaultPostSelect, 81 | }); 82 | if (!post) { 83 | throw new TRPCError({ 84 | code: "NOT_FOUND", 85 | message: `No post with id '${id}'`, 86 | }); 87 | } 88 | return post; 89 | }), 90 | add: publicProcedure 91 | .input( 92 | z.object({ 93 | id: z.string().uuid().optional(), 94 | title: z.string().min(1).max(32), 95 | text: z.string().min(1), 96 | }) 97 | ) 98 | .mutation(async ({ input }) => { 99 | const post = await prisma.post.create({ 100 | data: input, 101 | select: defaultPostSelect, 102 | }); 103 | return post; 104 | }), 105 | }); 106 | -------------------------------------------------------------------------------- /apps/web/src/server/trpc.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This is your entry point to setup the root configuration for tRPC on the server. 3 | * - `initTRPC` should only be used once per app. 4 | * - We export only the functionality that we use so we can enforce which base procedures should be used 5 | * 6 | * Learn how to create protected base procedures and other things below: 7 | * @see https://trpc.io/docs/v10/router 8 | * @see https://trpc.io/docs/v10/procedures 9 | */ 10 | 11 | import { Context } from "./context"; 12 | import { initTRPC } from "@trpc/server"; 13 | import superjson from "superjson"; 14 | 15 | const t = initTRPC.context().create({ 16 | /** 17 | * @see https://trpc.io/docs/v10/data-transformers 18 | */ 19 | transformer: superjson, 20 | /** 21 | * @see https://trpc.io/docs/v10/error-formatting 22 | */ 23 | errorFormatter({ shape }) { 24 | return shape; 25 | }, 26 | }); 27 | 28 | /** 29 | * Create a router 30 | * @see https://trpc.io/docs/v10/router 31 | */ 32 | export const router = t.router; 33 | 34 | /** 35 | * Create an unprotected procedure 36 | * @see https://trpc.io/docs/v10/procedures 37 | **/ 38 | export const publicProcedure = t.procedure; 39 | 40 | /** 41 | * @see https://trpc.io/docs/v10/middlewares 42 | */ 43 | export const middleware = t.middleware; 44 | 45 | /** 46 | * @see https://trpc.io/docs/v10/merging-routers 47 | */ 48 | export const mergeRouters = t.mergeRouters; 49 | -------------------------------------------------------------------------------- /apps/web/src/styles/global.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | height: 100%; 7 | } 8 | 9 | #__next { 10 | min-height: 100%; 11 | height: 0; 12 | } 13 | 14 | html { 15 | height: 100%; 16 | } 17 | -------------------------------------------------------------------------------- /apps/web/src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export const STARTER_PRICE_ID = "price_1MJNSlINRf8NBPell5JwSwCl"; 2 | export const TWITTER_LINK = "https://twitter.com/kubessandra"; 3 | 4 | export const SUCCESS_URL = "/payment/success"; 5 | -------------------------------------------------------------------------------- /apps/web/src/utils/publicRuntimeConfig.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Dynamic configuration available for the browser and server populated from your `next.config.js`. 3 | * Note: requires `ssr: true` or a `getInitialProps` in `_app.tsx` 4 | * @link https://nextjs.org/docs/api-reference/next.config.js/runtime-configuration 5 | */ 6 | import type * as config from "../../next.config"; 7 | import getConfig from "next/config"; 8 | 9 | /** 10 | * Inferred type from `publicRuntime` in `next.config.js` 11 | */ 12 | type PublicRuntimeConfig = typeof config.publicRuntimeConfig; 13 | 14 | const nextConfig = getConfig(); 15 | 16 | export const publicRuntimeConfig = 17 | nextConfig.publicRuntimeConfig as PublicRuntimeConfig; 18 | -------------------------------------------------------------------------------- /apps/web/src/utils/routing.ts: -------------------------------------------------------------------------------- 1 | import { SVGProps } from "react"; 2 | 3 | import { HomeIcon, CogIcon, CreditCardIcon } from "@heroicons/react/24/outline"; 4 | 5 | export interface IRoute { 6 | path: string; 7 | name: string; 8 | sidebar: boolean; 9 | icon?: (props: SVGProps) => JSX.Element; 10 | } 11 | 12 | type SecuredRoutes = typeof securedRoutes; 13 | const securedRoutes = { 14 | DASHBOARD: { 15 | path: "/dashboard", 16 | name: "Dashboard", 17 | sidebar: true, 18 | icon: HomeIcon, 19 | }, 20 | PLAN: { 21 | path: "/plan", 22 | name: "Plan", 23 | sidebar: true, 24 | icon: CreditCardIcon, 25 | }, 26 | SETTINGS: { 27 | path: "/settings", 28 | name: "Settings", 29 | sidebar: true, 30 | icon: CogIcon, 31 | }, 32 | } as const; 33 | 34 | export const publicRoutes = { 35 | HOME: { 36 | path: "/", 37 | }, 38 | SIGN_IN: { 39 | path: "/sign-in", 40 | }, 41 | SIGN_UP: { 42 | path: "/sign-up", 43 | }, 44 | } as const; 45 | 46 | export const getSecuredRoutes = (): { 47 | [key in keyof SecuredRoutes]: IRoute; 48 | } => { 49 | return securedRoutes; 50 | }; 51 | 52 | export const getSecuredRoutesPaths = (): string[] => { 53 | return Object.values(securedRoutes).map((route) => route.path); 54 | }; 55 | 56 | export const isPathnameInSecuredRoutes = (pathname: string) => { 57 | return getSecuredRoutesPaths() 58 | .map((route) => pathname.includes(route)) 59 | .includes(true); 60 | }; 61 | 62 | export const getSidebarRoutes = (): IRoute[] => { 63 | return Object.values(securedRoutes).filter((route) => route.sidebar); 64 | }; 65 | 66 | export default securedRoutes; 67 | -------------------------------------------------------------------------------- /apps/web/src/utils/trpc.ts: -------------------------------------------------------------------------------- 1 | import { httpBatchLink, loggerLink } from "@trpc/client"; 2 | import { createTRPCNext } from "@trpc/next"; 3 | import { inferRouterInputs, inferRouterOutputs } from "@trpc/server"; 4 | import { NextPageContext } from "next"; 5 | import superjson from "superjson"; 6 | // ℹ️ Type-only import: 7 | // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export 8 | import type { AppRouter } from "~/server/routers/_app"; 9 | 10 | function getBaseUrl() { 11 | if (typeof window !== "undefined") { 12 | return ""; 13 | } 14 | // reference for vercel.com 15 | if (process.env.VERCEL_URL) { 16 | return `https://${process.env.VERCEL_URL}`; 17 | } 18 | 19 | // // reference for render.com 20 | if (process.env.RENDER_INTERNAL_HOSTNAME) { 21 | return `http://${process.env.RENDER_INTERNAL_HOSTNAME}:${process.env.PORT}`; 22 | } 23 | 24 | // assume localhost 25 | return `http://127.0.0.1:${process.env.PORT ?? 3000}`; 26 | } 27 | 28 | /** 29 | * Extend `NextPageContext` with meta data that can be picked up by `responseMeta()` when server-side rendering 30 | */ 31 | export interface SSRContext extends NextPageContext { 32 | /** 33 | * Set HTTP Status code 34 | * @example 35 | * const utils = trpc.useContext(); 36 | * if (utils.ssrContext) { 37 | * utils.ssrContext.status = 404; 38 | * } 39 | */ 40 | status?: number; 41 | } 42 | 43 | /** 44 | * A set of strongly-typed React hooks from your `AppRouter` type signature with `createReactQueryHooks`. 45 | * @link https://trpc.io/docs/react#3-create-trpc-hooks 46 | */ 47 | export const trpc = createTRPCNext({ 48 | config({ ctx }) { 49 | /** 50 | * If you want to use SSR, you need to use the server's full URL 51 | * @link https://trpc.io/docs/ssr 52 | */ 53 | return { 54 | /** 55 | * @link https://trpc.io/docs/data-transformers 56 | */ 57 | transformer: superjson, 58 | /** 59 | * @link https://trpc.io/docs/links 60 | */ 61 | links: [ 62 | // adds pretty logs to your console in development and logs errors in production 63 | loggerLink({ 64 | enabled: (opts) => 65 | process.env.NODE_ENV === "development" || 66 | (opts.direction === "down" && opts.result instanceof Error), 67 | }), 68 | httpBatchLink({ 69 | url: `${getBaseUrl()}/api/trpc`, 70 | /** 71 | * Set custom request headers on every request from tRPC 72 | * @link https://trpc.io/docs/ssr 73 | */ 74 | headers() { 75 | if (ctx?.req) { 76 | // To use SSR properly, you need to forward the client's headers to the server 77 | // This is so you can pass through things like cookies when we're server-side rendering 78 | 79 | // If you're using Node 18, omit the "connection" header 80 | const { 81 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 82 | connection: _connection, 83 | ...headers 84 | } = ctx.req.headers; 85 | return { 86 | ...headers, 87 | // Optional: inform server that it's an SSR request 88 | "x-ssr": "1", 89 | }; 90 | } 91 | return {}; 92 | }, 93 | }), 94 | ], 95 | /** 96 | * @link https://react-query.tanstack.com/reference/QueryClient 97 | */ 98 | // queryClientConfig: { defaultOptions: { queries: { staleTime: 60 } } }, 99 | }; 100 | }, 101 | /** 102 | * @link https://trpc.io/docs/ssr 103 | */ 104 | ssr: true, 105 | /** 106 | * Set headers or status code when doing SSR 107 | */ 108 | responseMeta(opts) { 109 | const ctx = opts.ctx as SSRContext; 110 | 111 | if (ctx.status) { 112 | // If HTTP status set, propagate that 113 | return { 114 | status: ctx.status, 115 | }; 116 | } 117 | 118 | const error = opts.clientErrors[0]; 119 | if (error) { 120 | // Propagate http first error from API calls 121 | return { 122 | status: error.data?.httpStatus ?? 500, 123 | }; 124 | } 125 | 126 | // for app caching with SSR see https://trpc.io/docs/caching 127 | 128 | return {}; 129 | }, 130 | }); 131 | 132 | export type RouterInput = inferRouterInputs; 133 | export type RouterOutput = inferRouterOutputs; 134 | -------------------------------------------------------------------------------- /apps/web/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** @type {import('tailwindcss').Config} */ 3 | const colors = require("tailwindcss/colors"); 4 | 5 | module.exports = { 6 | content: [ 7 | "./src/pages/**/*.{js,ts,jsx,tsx}", 8 | "./src/components/**/*.{js,ts,jsx,tsx}", 9 | ], 10 | theme: { 11 | extend: { 12 | colors: { 13 | teal: colors.teal, 14 | cyan: colors.cyan, 15 | }, 16 | }, 17 | }, 18 | plugins: [ 19 | require("@tailwindcss/forms"), 20 | require("@tailwindcss/aspect-ratio"), 21 | ], 22 | }; 23 | -------------------------------------------------------------------------------- /apps/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "strictNullChecks": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "noUncheckedIndexedAccess": true, 16 | "isolatedModules": true, 17 | "jsx": "preserve", 18 | "incremental": true, 19 | "baseUrl": ".", 20 | "paths": { 21 | "~/*": ["src/*"] 22 | } 23 | }, 24 | "include": [ 25 | "next-env.d.ts", 26 | "**/*.ts", 27 | "**/*.tsx", 28 | "./*.js", 29 | "./src/**/*.js" 30 | ], 31 | "exclude": ["node_modules"] 32 | } 33 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["next", "turbo", "prettier"], 3 | rules: { 4 | "@next/next/no-html-link-for-pages": "off", 5 | "react/jsx-key": "off", 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "socialgenerator", 3 | "version": "1.0.0", 4 | "description": "generate social videos ", 5 | "main": "./index.ts", 6 | "types": "./index.ts", 7 | "dependencies": { 8 | "pnpm": "^7.19.0" 9 | }, 10 | "devDependencies": { 11 | "turbo": "^1.6.3" 12 | }, 13 | "scripts": { 14 | "build": "turbo run build", 15 | "test": "turbo run test", 16 | "lint": "turbo run lint", 17 | "lint-fix": "turbo run lint-fix", 18 | "dev": "turbo run dev" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/Kubessandra/SocialGenerator.git" 23 | }, 24 | "author": "kubessandra", 25 | "bugs": { 26 | "url": "https://github.com/Kubessandra/SocialGenerator/issues" 27 | }, 28 | "homepage": "https://github.com/Kubessandra/SocialGenerator#readme" 29 | } 30 | -------------------------------------------------------------------------------- /packages/database/.env.example: -------------------------------------------------------------------------------- 1 | # Environment variables declared in this file are automatically made available to Prisma. 2 | # See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema 3 | 4 | # Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB. 5 | # See the documentation for all the connection string options: https://pris.ly/d/connection-strings 6 | 7 | DATABASE_URL="postgresql://postgres:postgres@127.0.0.1:5432/postgres?schema=public" 8 | -------------------------------------------------------------------------------- /packages/database/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | # Keep environment variables out of version control 3 | .env 4 | -------------------------------------------------------------------------------- /packages/database/index.ts: -------------------------------------------------------------------------------- 1 | export * from "@prisma/client"; 2 | -------------------------------------------------------------------------------- /packages/database/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kubessandra/database", 3 | "scripts": { 4 | "db:generate": "prisma generate", 5 | "db:migrate": "prisma migrate dev", 6 | "db:studio": "prisma studio" 7 | }, 8 | "devDependencies": { 9 | "prisma": "^4.10.0" 10 | }, 11 | "dependencies": { 12 | "@prisma/client": "^4.10.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20221222224246_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Post" ( 3 | "id" TEXT NOT NULL, 4 | "title" TEXT NOT NULL, 5 | "text" TEXT NOT NULL, 6 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 7 | "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 8 | 9 | CONSTRAINT "Post_pkey" PRIMARY KEY ("id") 10 | ); 11 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20221224141953_payment/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "PaymentInfos" ( 3 | "id" TEXT NOT NULL, 4 | "customerId" TEXT NOT NULL, 5 | "userId" TEXT NOT NULL, 6 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 7 | "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 8 | 9 | CONSTRAINT "PaymentInfos_pkey" PRIMARY KEY ("id") 10 | ); 11 | 12 | -- CreateIndex 13 | CREATE UNIQUE INDEX "PaymentInfos_customerId_key" ON "PaymentInfos"("customerId"); 14 | 15 | -- CreateIndex 16 | CREATE UNIQUE INDEX "PaymentInfos_userId_key" ON "PaymentInfos"("userId"); 17 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20221226165737_user_info/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `PaymentInfos` table. If the table is not empty, all the data it contains will be lost. 5 | 6 | */ 7 | -- DropTable 8 | DROP TABLE "PaymentInfos"; 9 | 10 | -- CreateTable 11 | CREATE TABLE "UserInfo" ( 12 | "id" TEXT NOT NULL, 13 | "email" TEXT NOT NULL, 14 | "externalId" TEXT NOT NULL, 15 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 16 | "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 17 | 18 | CONSTRAINT "UserInfo_pkey" PRIMARY KEY ("id") 19 | ); 20 | 21 | -- CreateTable 22 | CREATE TABLE "PaymentInfo" ( 23 | "id" TEXT NOT NULL, 24 | "customerId" TEXT NOT NULL, 25 | "userId" TEXT NOT NULL, 26 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 27 | "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 28 | 29 | CONSTRAINT "PaymentInfo_pkey" PRIMARY KEY ("id") 30 | ); 31 | 32 | -- CreateIndex 33 | CREATE UNIQUE INDEX "UserInfo_email_key" ON "UserInfo"("email"); 34 | 35 | -- CreateIndex 36 | CREATE UNIQUE INDEX "UserInfo_externalId_key" ON "UserInfo"("externalId"); 37 | 38 | -- CreateIndex 39 | CREATE UNIQUE INDEX "PaymentInfo_customerId_key" ON "PaymentInfo"("customerId"); 40 | 41 | -- CreateIndex 42 | CREATE UNIQUE INDEX "PaymentInfo_userId_key" ON "PaymentInfo"("userId"); 43 | 44 | -- AddForeignKey 45 | ALTER TABLE "PaymentInfo" ADD CONSTRAINT "PaymentInfo_userId_fkey" FOREIGN KEY ("userId") REFERENCES "UserInfo"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 46 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20221226220318_subscription_date/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "PaymentInfo" ADD COLUMN "subscriptionEnd" TIMESTAMP(3); 3 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20221226220758_rename_subscription_end/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `subscriptionEnd` on the `PaymentInfo` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "PaymentInfo" DROP COLUMN "subscriptionEnd", 9 | ADD COLUMN "subscriptionEndAt" TIMESTAMP(3); 10 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20221229004345_remove_post/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `Post` table. If the table is not empty, all the data it contains will be lost. 5 | 6 | */ 7 | -- DropTable 8 | DROP TABLE "Post"; 9 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /packages/database/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "postgresql" 10 | url = env("DATABASE_URL") 11 | } 12 | 13 | model UserInfo { 14 | id String @id @default(uuid()) 15 | email String @unique 16 | externalId String @unique 17 | 18 | paymentInfo PaymentInfo? 19 | 20 | createdAt DateTime @default(now()) 21 | updatedAt DateTime @default(now()) @updatedAt 22 | } 23 | 24 | model PaymentInfo { 25 | id String @id @default(uuid()) 26 | customerId String @unique 27 | subscriptionEndAt DateTime? 28 | user UserInfo @relation(fields: [userId], references: [id]) 29 | userId String @unique 30 | 31 | createdAt DateTime @default(now()) 32 | updatedAt DateTime @default(now()) @updatedAt 33 | } 34 | -------------------------------------------------------------------------------- /packages/eslint-config-custom/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "@typescript-eslint/parser", 3 | extends: [ 4 | "plugin:react/recommended", 5 | "plugin:react-hooks/recommended", 6 | "plugin:prettier/recommended", 7 | "plugin:@typescript-eslint/recommended", 8 | ], 9 | rules: { 10 | "@typescript-eslint/explicit-function-return-type": "off", 11 | "@typescript-eslint/explicit-module-boundary-types": "off", 12 | "react/react-in-jsx-scope": "off", 13 | "react/prop-types": "off", 14 | }, 15 | settings: { 16 | react: { 17 | version: "detect", 18 | }, 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /packages/eslint-config-custom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kubessandra/eslint-config-custom", 3 | "main": "index.js", 4 | "dependencies": { 5 | "@typescript-eslint/eslint-plugin": "^5.37.0", 6 | "@typescript-eslint/parser": "^5.37.0", 7 | "eslint": "^8.30.0", 8 | "eslint-config-next": "^13.1.0", 9 | "eslint-config-prettier": "^8.5.0", 10 | "eslint-config-turbo": "^0.0.7", 11 | "eslint-plugin-prettier": "^4.2.1", 12 | "eslint-plugin-react": "^7.31.11", 13 | "eslint-plugin-react-hooks": "^4.6.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/prettier-config/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | tabWidth: 2, 4 | semi: false, 5 | trailingComma: "all", 6 | printWidth: 100, 7 | }; 8 | -------------------------------------------------------------------------------- /packages/prettier-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kubessandra/prettier-config", 3 | "main": "index.js", 4 | "devDependencies": { 5 | "prettier": "^2.8.1", 6 | "prettier-plugin-tailwindcss": "^0.2.1" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/tsconfig/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Default", 4 | "compilerOptions": { 5 | "composite": false, 6 | "declaration": true, 7 | "declarationMap": true, 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "inlineSources": false, 11 | "isolatedModules": true, 12 | "moduleResolution": "node", 13 | "noUnusedLocals": false, 14 | "noUnusedParameters": false, 15 | "preserveWatchOutput": true, 16 | "skipLibCheck": true, 17 | "strict": true 18 | }, 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/tsconfig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kubessandra/tsconfig", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "keywords": [], 9 | "author": "" 10 | } 11 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "apps/*" 3 | - "packages/*" 4 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "pipeline": { 4 | "build": { 5 | "dependsOn": ["^build", "^db:generate"], 6 | "outputs": [".next/**"] 7 | }, 8 | "dev": { 9 | "dependsOn": ["^db:generate"], 10 | "cache": false 11 | }, 12 | "test": { 13 | "dependsOn": ["build"], 14 | "outputs": [], 15 | "inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts", "test/**/*.tsx"] 16 | }, 17 | "lint": { 18 | "outputs": [] 19 | }, 20 | "deploy": { 21 | "dependsOn": ["build", "test", "lint"], 22 | "outputs": [] 23 | }, 24 | "lint-fix": { 25 | "outputs": [] 26 | }, 27 | "db:generate": { 28 | "cache": false 29 | }, 30 | "db:push": { 31 | "cache": false 32 | } 33 | } 34 | } 35 | --------------------------------------------------------------------------------