├── .env.example ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── feature_request.yml ├── renovate.json └── workflows │ └── ci.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── apps ├── expo │ ├── .expo-shared │ │ └── assets.json │ ├── app.config.ts │ ├── assets │ │ └── icon.png │ ├── babel.config.js │ ├── eas.json │ ├── expo-plugins │ │ └── with-modify-gradle.js │ ├── metro.config.js │ ├── package.json │ ├── src │ │ ├── app │ │ │ ├── _layout.tsx │ │ │ ├── index.tsx │ │ │ └── post │ │ │ │ └── [id].tsx │ │ ├── styles.css │ │ ├── types │ │ │ └── nativewind-env.d.ts │ │ └── utils │ │ │ └── api.tsx │ ├── tailwind.config.ts │ └── tsconfig.json └── nextjs │ ├── .vscode │ └── settings.json │ ├── README.md │ ├── next.config.mjs │ ├── package.json │ ├── postcss.config.cjs │ ├── public │ ├── favicon.ico │ ├── og-image.png │ └── t3-icon.svg │ ├── src │ ├── app │ │ ├── (auth) │ │ │ ├── layout.tsx │ │ │ ├── signin │ │ │ │ ├── email-signin.tsx │ │ │ │ ├── oauth-signin.tsx │ │ │ │ └── page.tsx │ │ │ ├── signout │ │ │ │ └── page.tsx │ │ │ └── sso-callback │ │ │ │ └── page.tsx │ │ ├── (dashboard) │ │ │ ├── [workspaceId] │ │ │ │ ├── [projectId] │ │ │ │ │ ├── _components │ │ │ │ │ │ ├── loading-card.tsx │ │ │ │ │ │ └── overview.tsx │ │ │ │ │ ├── api-keys │ │ │ │ │ │ ├── data-table.tsx │ │ │ │ │ │ ├── loading.tsx │ │ │ │ │ │ ├── new-api-key-dialog.tsx │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── danger │ │ │ │ │ │ ├── delete-project.tsx │ │ │ │ │ │ ├── loading.tsx │ │ │ │ │ │ ├── page.tsx │ │ │ │ │ │ ├── transfer-to-organization.tsx │ │ │ │ │ │ └── transfer-to-personal.tsx │ │ │ │ │ ├── error.tsx │ │ │ │ │ ├── ingestions │ │ │ │ │ │ └── [ingestionId] │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── overview │ │ │ │ │ │ ├── loading.tsx │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── settings │ │ │ │ │ │ ├── _components │ │ │ │ │ │ └── rename-project.tsx │ │ │ │ │ │ ├── loading.tsx │ │ │ │ │ │ └── page.tsx │ │ │ │ ├── _components │ │ │ │ │ ├── create-api-key-form.tsx │ │ │ │ │ ├── create-project-form.tsx │ │ │ │ │ ├── project-card.tsx │ │ │ │ │ └── sidebar.tsx │ │ │ │ ├── billing │ │ │ │ │ ├── loading.tsx │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── subscription-form.tsx │ │ │ │ ├── danger │ │ │ │ │ ├── delete-workspace.tsx │ │ │ │ │ ├── loading.tsx │ │ │ │ │ └── page.tsx │ │ │ │ ├── layout.tsx │ │ │ │ ├── loading.tsx │ │ │ │ ├── page.tsx │ │ │ │ ├── settings │ │ │ │ │ ├── _components │ │ │ │ │ │ ├── invite-member-dialog.tsx │ │ │ │ │ │ ├── organization-image.tsx │ │ │ │ │ │ ├── organization-members.tsx │ │ │ │ │ │ └── organization-name.tsx │ │ │ │ │ └── page.tsx │ │ │ │ └── sync-active-org-from-url.tsx │ │ │ ├── _components │ │ │ │ ├── breadcrumbs.tsx │ │ │ │ ├── dashboard-shell.tsx │ │ │ │ ├── date-range-picker.tsx │ │ │ │ ├── main-nav.tsx │ │ │ │ ├── project-switcher.tsx │ │ │ │ ├── search.tsx │ │ │ │ └── workspace-switcher.tsx │ │ │ ├── layout.tsx │ │ │ └── onboarding │ │ │ │ ├── create-api-key.tsx │ │ │ │ ├── create-project.tsx │ │ │ │ ├── done.tsx │ │ │ │ ├── intro.tsx │ │ │ │ ├── multi-step-form.tsx │ │ │ │ └── page.tsx │ │ ├── (marketing) │ │ │ ├── layout.tsx │ │ │ ├── page.tsx │ │ │ ├── pricing │ │ │ │ ├── page.tsx │ │ │ │ └── subscribe-now.tsx │ │ │ ├── privacy │ │ │ │ └── page.mdx │ │ │ └── terms │ │ │ │ └── page.mdx │ │ ├── api │ │ │ ├── trpc │ │ │ │ ├── edge │ │ │ │ │ └── [trpc] │ │ │ │ │ │ └── route.ts │ │ │ │ └── lambda │ │ │ │ │ └── [trpc] │ │ │ │ │ └── route.ts │ │ │ └── webhooks │ │ │ │ └── stripe │ │ │ │ └── route.ts │ │ ├── config.tsx │ │ └── layout.tsx │ ├── components │ │ ├── footer.tsx │ │ ├── mobile-nav.tsx │ │ ├── tailwind-indicator.tsx │ │ ├── theme-provider.tsx │ │ ├── theme-toggle.tsx │ │ └── user-nav.tsx │ ├── env.mjs │ ├── lib │ │ ├── currency.ts │ │ ├── generate-pattern.ts │ │ ├── project-guard.ts │ │ ├── use-debounce.tsx │ │ └── zod-form.tsx │ ├── mdx-components.tsx │ ├── middleware.ts │ ├── styles │ │ ├── calsans.ttf │ │ └── globals.css │ └── trpc │ │ ├── client.ts │ │ ├── server.ts │ │ └── shared.ts │ ├── tailwind.config.ts │ └── tsconfig.json ├── package.json ├── packages ├── api │ ├── package.json │ ├── src │ │ ├── edge.ts │ │ ├── env.mjs │ │ ├── index.ts │ │ ├── lambda.ts │ │ ├── root.ts │ │ ├── router │ │ │ ├── auth.ts │ │ │ ├── ingestion.ts │ │ │ ├── organizations.ts │ │ │ ├── project.ts │ │ │ └── stripe.ts │ │ ├── transformer.ts │ │ ├── trpc.ts │ │ └── validators.ts │ └── tsconfig.json ├── db │ ├── index.ts │ ├── package.json │ ├── prisma │ │ ├── enums.ts │ │ ├── schema.prisma │ │ └── types.ts │ └── tsconfig.json ├── stripe │ ├── package.json │ ├── src │ │ ├── env.mjs │ │ ├── index.ts │ │ ├── plans.ts │ │ └── webhooks.ts │ └── tsconfig.json └── ui │ ├── package.json │ ├── src │ ├── avatar.tsx │ ├── button.tsx │ ├── calendar.tsx │ ├── card.tsx │ ├── checkbox.tsx │ ├── command.tsx │ ├── data-table.tsx │ ├── dialog.tsx │ ├── dropdown-menu.tsx │ ├── form.tsx │ ├── icons.tsx │ ├── index.ts │ ├── input.tsx │ ├── label.tsx │ ├── popover.tsx │ ├── scroll-area.tsx │ ├── select.tsx │ ├── sheet.tsx │ ├── table.tsx │ ├── tabs.tsx │ ├── toast.tsx │ ├── toaster.tsx │ ├── use-toast.tsx │ └── utils │ │ └── cn.ts │ ├── tailwind.config.ts │ └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── tooling ├── eslint │ ├── base.js │ ├── nextjs.js │ ├── package.json │ ├── react.js │ └── tsconfig.json ├── prettier │ ├── index.mjs │ ├── package.json │ └── tsconfig.json ├── tailwind │ ├── index.ts │ ├── package.json │ ├── postcss.js │ └── tsconfig.json └── typescript │ ├── base.json │ └── package.json ├── tsconfig.json ├── turbo.json └── turbo └── generators ├── config.ts └── templates ├── package.json.hbs └── tsconfig.json.hbs /.env.example: -------------------------------------------------------------------------------- 1 | # Since .env.local is gitignored, you can use .env.example to build a new `.env` file when you clone the repo. 2 | # Keep this file up-to-date when you add new variables to \`.env\`. 3 | 4 | # This file will be committed to version control, so make sure not to have any secrets in it. 5 | # If you are cloning this repo, create a copy of this file named `.env` and populate it with your secrets. 6 | 7 | # We use dotenv to load Prisma from Next.js' .env.local file 8 | # @see https://www.prisma.io/docs/reference/database-reference/connection-urls 9 | DATABASE_URL='mysql://user:password@host/db?sslaccept=strict' 10 | 11 | # Clerk 12 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="pk_test_" 13 | CLERK_SECRET_KEY="sk_test_" 14 | 15 | # Stripe 16 | STRIPE_API_KEY="sk_test_" 17 | STRIPE_WEBHOOK_SECRET="whsec_" 18 | NEXT_PUBLIC_STRIPE_STD_PRODUCT_ID="prod_" 19 | NEXT_PUBLIC_STRIPE_STD_MONTHLY_PRICE_ID="price_" 20 | NEXT_PUBLIC_STRIPE_PRO_PRODUCT_ID="prod_" 21 | NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PRICE_ID="price_" 22 | 23 | # Misc 24 | NEXTJS_URL="http://localhost:3000" -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: juliusmarminge 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: 🐞 Bug Report 2 | description: Create a bug report to help us improve 3 | title: "bug: " 4 | labels: ["🐞❔ unconfirmed bug"] 5 | body: 6 | - type: textarea 7 | attributes: 8 | label: Provide environment information 9 | description: | 10 | Run this command in your project root and paste the results in a code block: 11 | ```bash 12 | npx envinfo --system --binaries 13 | ``` 14 | validations: 15 | required: true 16 | - type: textarea 17 | attributes: 18 | label: Describe the bug 19 | description: A clear and concise description of the bug, as well as what you expected to happen when encountering it. 20 | validations: 21 | required: true 22 | - type: input 23 | attributes: 24 | label: Link to reproduction 25 | description: Please provide a link to a reproduction of the bug. Issues without a reproduction repo may be ignored. 26 | validations: 27 | required: true 28 | - type: textarea 29 | attributes: 30 | label: To reproduce 31 | description: Describe how to reproduce your bug. Steps, code snippets, reproduction repos etc. 32 | validations: 33 | required: true 34 | - type: textarea 35 | attributes: 36 | label: Additional information 37 | description: Add any other information related to the bug here, screenshots if applicable. 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | # This template is heavily inspired by the Next.js's template: 2 | # See here: https://github.com/vercel/next.js/blob/canary/.github/ISSUE_TEMPLATE/3.feature_request.yml 3 | 4 | name: 🛠 Feature Request 5 | description: Create a feature request for the core packages 6 | title: 'feat: ' 7 | labels: ['✨ enhancement'] 8 | body: 9 | - type: markdown 10 | attributes: 11 | value: | 12 | Thank you for taking the time to file a feature request. Please fill out this form as completely as possible. 13 | - type: textarea 14 | attributes: 15 | label: Describe the feature you'd like to request 16 | description: Please describe the feature as clear and concise as possible. Remember to add context as to why you believe this feature is needed. 17 | validations: 18 | required: true 19 | - type: textarea 20 | attributes: 21 | label: Describe the solution you'd like to see 22 | description: Please describe the solution you would like to see. Adding example usage is a good way to provide context. 23 | validations: 24 | required: true 25 | - type: textarea 26 | attributes: 27 | label: Additional information 28 | description: Add any other information related to the feature here. If your feature request is related to any issues or discussions, link them here. 29 | 30 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:base"], 4 | "packageRules": [ 5 | { 6 | "matchPackagePatterns": ["^@acme/"], 7 | "enabled": false 8 | } 9 | ], 10 | "updateInternalDeps": true, 11 | "rangeStrategy": "bump" 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: ["*"] 6 | push: 7 | branches: ["main"] 8 | merge_group: 9 | 10 | # You can leverage Vercel Remote Caching with Turbo to speed up your builds 11 | # @link https://turborepo.org/docs/core-concepts/remote-caching#remote-caching-on-vercel-builds 12 | env: 13 | TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} 14 | TURBO_TEAM: ${{ secrets.TURBO_TEAM }} 15 | 16 | jobs: 17 | build-lint: 18 | env: 19 | DATABASE_URL: file:./db.sqlite 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - name: Checkout repo 24 | uses: actions/checkout@v4 25 | 26 | - name: Setup pnpm 27 | uses: pnpm/action-setup@v2.4.0 28 | 29 | - name: Setup Node 20 30 | uses: actions/setup-node@v4 31 | with: 32 | node-version: 20 33 | 34 | - name: Get pnpm store directory 35 | id: pnpm-cache 36 | run: | 37 | echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT 38 | 39 | - name: Setup pnpm cache 40 | uses: actions/cache@v4 41 | with: 42 | path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }} 43 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 44 | restore-keys: | 45 | ${{ runner.os }}-pnpm-store- 46 | 47 | - name: Install deps (with cache) 48 | run: pnpm install 49 | 50 | - run: cp .env.example .env.local 51 | 52 | - name: Build, lint and type-check 53 | run: pnpm turbo build lint typecheck format 54 | 55 | - name: Check workspaces 56 | run: pnpm manypkg check 57 | -------------------------------------------------------------------------------- /.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 | # database 12 | **/prisma/db.sqlite 13 | **/prisma/db.sqlite-journal 14 | 15 | # next.js 16 | .next/ 17 | out/ 18 | next-env.d.ts 19 | 20 | # expo 21 | .expo/ 22 | dist/ 23 | expo-env.d.ts 24 | 25 | # production 26 | build 27 | 28 | # misc 29 | .DS_Store 30 | *.pem 31 | 32 | # debug 33 | npm-debug.log* 34 | yarn-debug.log* 35 | yarn-error.log* 36 | .pnpm-debug.log* 37 | 38 | # local env files 39 | .env 40 | .env*.local 41 | 42 | # vercel 43 | .vercel 44 | 45 | # typescript 46 | *.tsbuildinfo 47 | 48 | # turbo 49 | .turbo 50 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # Expo doesn't play nice with pnpm by default. 2 | # The symbolic links of pnpm break the rules of Expo monorepos. 3 | # @link https://docs.expo.dev/guides/monorepos/#common-issues 4 | node-linker=hoisted 5 | 6 | # In order to cache Prisma correctly 7 | public-hoist-pattern[]=*prisma* 8 | 9 | # FIXME: @prisma/client is required by the @acme/auth, 10 | # but we don't want it installed there since it's already 11 | # installed in the @acme/db package 12 | strict-peer-dependencies=false 13 | 14 | # Prevent pnpm from adding the "workspace:"" prefix to local 15 | # packages as it casues issues with manypkg 16 | # @link https://pnpm.io/npmrc#prefer-workspace-packages 17 | save-workspace-protocol=false 18 | prefer-workspace-packages=true -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20.10 -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "esbenp.prettier-vscode", 4 | "dbaeumer.vscode-eslint", 5 | "bradlc.vscode-tailwindcss", 6 | "Prisma.prisma" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Next.js", 6 | "type": "node-terminal", 7 | "request": "launch", 8 | "command": "pnpm dev", 9 | "cwd": "${workspaceFolder}/apps/nextjs/", 10 | "skipFiles": ["/**"] 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[prisma]": { 3 | "editor.defaultFormatter": "Prisma.prisma" 4 | }, 5 | "editor.codeActionsOnSave": { 6 | "source.fixAll.eslint": "explicit" 7 | }, 8 | "editor.defaultFormatter": "esbenp.prettier-vscode", 9 | "editor.formatOnSave": true, 10 | "eslint.rules.customizations": [{ "rule": "*", "severity": "warn" }], 11 | "eslint.workingDirectories": [ 12 | { "pattern": "apps/*/" }, 13 | { "pattern": "packages/*/" }, 14 | { "pattern": "tooling/*/" } 15 | ], 16 | "tailwindCSS.experimental.configFile": "./tooling/tailwind/index.ts", 17 | "typescript.tsdk": "node_modules/typescript/lib", 18 | "typescript.enablePromptUseWorkspaceTsdk": true, 19 | "typescript.preferences.autoImportFileExcludePatterns": [ 20 | "next/router.d.ts", 21 | "next/dist/client/router.d.ts" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Julius Marminge 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 | -------------------------------------------------------------------------------- /apps/expo/.expo-shared/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true, 3 | "40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true 4 | } 5 | -------------------------------------------------------------------------------- /apps/expo/app.config.ts: -------------------------------------------------------------------------------- 1 | import type { ExpoConfig } from "@expo/config"; 2 | 3 | const defineConfig = (): ExpoConfig => ({ 4 | name: "expo", 5 | slug: "expo", 6 | scheme: "expo", 7 | version: "0.1.0", 8 | orientation: "portrait", 9 | icon: "./assets/icon.png", 10 | userInterfaceStyle: "light", 11 | splash: { 12 | image: "./assets/icon.png", 13 | resizeMode: "contain", 14 | backgroundColor: "#1F104A", 15 | }, 16 | updates: { 17 | fallbackToCacheTimeout: 0, 18 | }, 19 | assetBundlePatterns: ["**/*"], 20 | ios: { 21 | bundleIdentifier: "your.bundle.identifier", 22 | supportsTablet: true, 23 | }, 24 | android: { 25 | package: "your.bundle.identifier", 26 | adaptiveIcon: { 27 | foregroundImage: "./assets/icon.png", 28 | backgroundColor: "#1F104A", 29 | }, 30 | }, 31 | // extra: { 32 | // eas: { 33 | // projectId: "your-eas-project-id", 34 | // }, 35 | // }, 36 | experiments: { 37 | tsconfigPaths: true, 38 | typedRoutes: true, 39 | }, 40 | plugins: ["expo-router", "./expo-plugins/with-modify-gradle.js"], 41 | }); 42 | 43 | export default defineConfig; 44 | -------------------------------------------------------------------------------- /apps/expo/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juliusmarminge/acme-corp/90a28878e07f15517fdb0892a5c7ecb364364b14/apps/expo/assets/icon.png -------------------------------------------------------------------------------- /apps/expo/babel.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import("@babel/core").ConfigFunction} */ 2 | module.exports = function (api) { 3 | api.cache.forever(); 4 | 5 | return { 6 | presets: [ 7 | ["babel-preset-expo", { jsxImportSource: "nativewind" }], 8 | "nativewind/babel", 9 | ], 10 | plugins: [ 11 | require.resolve("expo-router/babel"), 12 | require.resolve("react-native-reanimated/plugin"), 13 | ], 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /apps/expo/eas.json: -------------------------------------------------------------------------------- 1 | { 2 | "cli": { 3 | "version": ">= 4.1.2" 4 | }, 5 | "build": { 6 | "base": { 7 | "node": "18.16.1", 8 | "ios": { 9 | "resourceClass": "m-medium" 10 | } 11 | }, 12 | "development": { 13 | "extends": "base", 14 | "developmentClient": true, 15 | "distribution": "internal" 16 | }, 17 | "preview": { 18 | "extends": "base", 19 | "distribution": "internal", 20 | "ios": { 21 | "simulator": true 22 | } 23 | }, 24 | "production": { 25 | "extends": "base" 26 | } 27 | }, 28 | "submit": { 29 | "production": {} 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /apps/expo/expo-plugins/with-modify-gradle.js: -------------------------------------------------------------------------------- 1 | // This plugin is required for fixing `.apk` build issue 2 | // It appends Expo and RN versions into the `build.gradle` file 3 | // References: 4 | // https://github.com/t3-oss/create-t3-turbo/issues/120 5 | // https://github.com/expo/expo/issues/18129 6 | 7 | /** @type {import("@expo/config-plugins").ConfigPlugin} */ 8 | const defineConfig = (config) => { 9 | // eslint-disable-next-line @typescript-eslint/no-var-requires 10 | return require("@expo/config-plugins").withProjectBuildGradle( 11 | config, 12 | (config) => { 13 | if (!config.modResults.contents.includes("ext.getPackageJsonVersion =")) { 14 | config.modResults.contents = config.modResults.contents.replace( 15 | "buildscript {", 16 | `buildscript { 17 | ext.getPackageJsonVersion = { packageName -> 18 | new File(['node', '--print', "JSON.parse(require('fs').readFileSync(require.resolve('\${packageName}/package.json'), 'utf-8')).version"].execute(null, rootDir).text.trim()) 19 | }`, 20 | ); 21 | } 22 | 23 | if (!config.modResults.contents.includes("reactNativeVersion =")) { 24 | config.modResults.contents = config.modResults.contents.replace( 25 | "ext {", 26 | `ext { 27 | reactNativeVersion = "\${ext.getPackageJsonVersion('react-native')}"`, 28 | ); 29 | } 30 | 31 | if (!config.modResults.contents.includes("expoPackageVersion =")) { 32 | config.modResults.contents = config.modResults.contents.replace( 33 | "ext {", 34 | `ext { 35 | expoPackageVersion = "\${ext.getPackageJsonVersion('expo')}"`, 36 | ); 37 | } 38 | 39 | return config; 40 | }, 41 | ); 42 | }; 43 | 44 | module.exports = defineConfig; 45 | -------------------------------------------------------------------------------- /apps/expo/metro.config.js: -------------------------------------------------------------------------------- 1 | // Learn more: https://docs.expo.dev/guides/monorepos/ 2 | const { getDefaultConfig } = require("@expo/metro-config"); 3 | const { withNativeWind } = require("nativewind/metro"); 4 | 5 | const path = require("path"); 6 | 7 | const projectRoot = __dirname; 8 | const workspaceRoot = path.resolve(projectRoot, "../.."); 9 | 10 | // Create the default Metro config 11 | const config = getDefaultConfig(projectRoot, { isCSSEnabled: true }); 12 | 13 | if (config.resolver) { 14 | // 1. Watch all files within the monorepo 15 | config.watchFolders = [workspaceRoot]; 16 | // 2. Let Metro know where to resolve packages and in what order 17 | config.resolver.nodeModulesPaths = [ 18 | path.resolve(projectRoot, "node_modules"), 19 | path.resolve(workspaceRoot, "node_modules"), 20 | ]; 21 | // 3. Force Metro to resolve (sub)dependencies only from the `nodeModulesPaths` 22 | config.resolver.disableHierarchicalLookup = true; 23 | } 24 | 25 | // @ts-expect-error - FIXME: type is mismatching? 26 | module.exports = withNativeWind(config, { 27 | input: "./src/styles.css", 28 | configPath: "./tailwind.config.ts", 29 | }); 30 | -------------------------------------------------------------------------------- /apps/expo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@acme/expo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "main": "expo-router/entry", 6 | "scripts": { 7 | "clean": "git clean -xdf .expo .turbo node_modules", 8 | "dev": "expo start --ios", 9 | "dev:android": "expo start --android", 10 | "dev:ios": "expo start --ios", 11 | "android": "expo run:android", 12 | "ios": "expo run:ios", 13 | "format": "prettier --check . --ignore-path ../../.gitignore", 14 | "lint": "eslint .", 15 | "typecheck": "tsc --noEmit" 16 | }, 17 | "dependencies": { 18 | "@expo/metro-config": "^0.10.7", 19 | "@shopify/flash-list": "1.4.3", 20 | "@tanstack/react-query": "^5.17.15", 21 | "@trpc/client": "next", 22 | "@trpc/react-query": "next", 23 | "@trpc/server": "next", 24 | "expo": "^49.0.22", 25 | "expo-constants": "~14.4.2", 26 | "expo-linking": "~5.0.2", 27 | "expo-router": "2.0.14", 28 | "expo-splash-screen": "~0.22.0", 29 | "expo-status-bar": "~1.7.1", 30 | "nativewind": "^4.0.23", 31 | "react": "18.2.0", 32 | "react-dom": "18.2.0", 33 | "react-native": "0.73.1", 34 | "react-native-gesture-handler": "~2.12.0", 35 | "react-native-reanimated": "~3.3.0", 36 | "react-native-safe-area-context": "4.6.3", 37 | "react-native-screens": "~3.22.1", 38 | "superjson": "2.2.1" 39 | }, 40 | "devDependencies": { 41 | "@acme/api": "0.1.0", 42 | "@acme/eslint-config": "0.2.0", 43 | "@acme/prettier-config": "0.1.0", 44 | "@acme/tailwind-config": "0.1.0", 45 | "@acme/tsconfig": "0.1.0", 46 | "@babel/core": "^7.23.7", 47 | "@babel/preset-env": "^7.23.8", 48 | "@babel/runtime": "^7.23.8", 49 | "@expo/config-plugins": "^7.8.4", 50 | "@types/babel__core": "^7.20.5", 51 | "@types/react": "^18.2.48", 52 | "eslint": "^8.56.0", 53 | "prettier": "^3.2.4", 54 | "tailwindcss": "3.4.1", 55 | "typescript": "^5.3.3" 56 | }, 57 | "eslintConfig": { 58 | "root": true, 59 | "extends": [ 60 | "@acme/eslint-config/base", 61 | "@acme/eslint-config/react" 62 | ], 63 | "ignorePatterns": [ 64 | "expo-plugins/**" 65 | ] 66 | }, 67 | "prettier": "@acme/prettier-config" 68 | } 69 | -------------------------------------------------------------------------------- /apps/expo/src/app/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { Stack } from "expo-router"; 2 | import { StatusBar } from "expo-status-bar"; 3 | 4 | import { TRPCProvider } from "~/utils/api"; 5 | 6 | import "../styles.css"; 7 | 8 | // This is the main layout of the app 9 | // It wraps your pages with the providers they need 10 | export default function RootLayout() { 11 | return ( 12 | 13 | {/* 14 | The Stack component displays the current page. 15 | It also allows you to configure your screens 16 | */} 17 | 24 | 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /apps/expo/src/app/post/[id].tsx: -------------------------------------------------------------------------------- 1 | // import { SafeAreaView, Text, View } from "react-native"; 2 | // import { Stack, useGlobalSearchParams } from "expo-router"; 3 | 4 | // import { api } from "~/utils/api"; 5 | 6 | // export default function Post() { 7 | // const { id } = useGlobalSearchParams(); 8 | // if (!id || typeof id !== "string") throw new Error("unreachable"); 9 | // const { data } = api.post.byId.useQuery({ id: parseInt(id) }); 10 | 11 | // if (!data) return null; 12 | 13 | // return ( 14 | // 15 | // 16 | // 17 | // {data.title} 18 | // {data.content} 19 | // 20 | // 21 | // ); 22 | // } 23 | -------------------------------------------------------------------------------- /apps/expo/src/styles.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /apps/expo/src/types/nativewind-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /apps/expo/src/utils/api.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import Constants from "expo-constants"; 3 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 4 | import { httpBatchLink, loggerLink } from "@trpc/client"; 5 | import { createTRPCReact } from "@trpc/react-query"; 6 | import superjson from "superjson"; 7 | 8 | import type { AppRouter } from "@acme/api"; 9 | 10 | /** 11 | * A set of typesafe hooks for consuming your API. 12 | */ 13 | export const api = createTRPCReact(); 14 | export { type RouterInputs, type RouterOutputs } from "@acme/api"; 15 | 16 | /** 17 | * Extend this function when going to production by 18 | * setting the baseUrl to your production API URL. 19 | */ 20 | const getBaseUrl = () => { 21 | /** 22 | * Gets the IP address of your host-machine. If it cannot automatically find it, 23 | * you'll have to manually set it. NOTE: Port 3000 should work for most but confirm 24 | * you don't have anything else running on it, or you'd have to change it. 25 | * 26 | * **NOTE**: This is only for development. In production, you'll want to set the 27 | * baseUrl to your production API URL. 28 | */ 29 | const debuggerHost = Constants.expoConfig?.hostUri; 30 | const localhost = debuggerHost?.split(":")[0]; 31 | 32 | if (!localhost) { 33 | // return "https://turbo.t3.gg"; 34 | throw new Error( 35 | "Failed to get localhost. Please point to your production server.", 36 | ); 37 | } 38 | return `http://${localhost}:3000`; 39 | }; 40 | 41 | /** 42 | * A wrapper for your app that provides the TRPC context. 43 | * Use only in _app.tsx 44 | */ 45 | export function TRPCProvider(props: { children: React.ReactNode }) { 46 | const [queryClient] = useState(() => new QueryClient()); 47 | const [trpcClient] = useState(() => 48 | api.createClient({ 49 | transformer: superjson, // TODO: Add transforming for Dinero.js 50 | links: [ 51 | httpBatchLink({ 52 | url: `${getBaseUrl()}/api/trpc`, 53 | headers() { 54 | const headers = new Map(); 55 | headers.set("x-trpc-source", "expo-react"); 56 | return Object.fromEntries(headers); 57 | }, 58 | }), 59 | loggerLink({ 60 | enabled: (opts) => 61 | process.env.NODE_ENV === "development" || 62 | (opts.direction === "down" && opts.result instanceof Error), 63 | colorMode: "ansi", 64 | }), 65 | ], 66 | }), 67 | ); 68 | 69 | return ( 70 | 71 | 72 | {props.children} 73 | 74 | 75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /apps/expo/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | // @ts-expect-error - no types 2 | import nativewind from "nativewind/preset"; 3 | import type { Config } from "tailwindcss"; 4 | 5 | import baseConfig from "@acme/tailwind-config"; 6 | 7 | export default { 8 | content: ["./src/**/*.{ts,tsx}"], 9 | presets: [baseConfig, nativewind], 10 | } satisfies Config; 11 | -------------------------------------------------------------------------------- /apps/expo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@acme/tsconfig/base.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "~/*": ["./src/*"], 7 | }, 8 | "jsx": "react-native", 9 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json", 10 | "types": ["nativewind/types"], 11 | }, 12 | "include": [ 13 | "src", 14 | "*.ts", 15 | "index.tsx", 16 | "*.js", 17 | ".expo/types/**/*.ts", 18 | "expo-env.d.ts", 19 | ], 20 | "exclude": ["node_modules"], 21 | } 22 | -------------------------------------------------------------------------------- /apps/nextjs/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "../../node_modules/typescript/lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true 4 | } 5 | -------------------------------------------------------------------------------- /apps/nextjs/README.md: -------------------------------------------------------------------------------- 1 | # Create T3 App 2 | 3 | This is a [T3 Stack](https://create.t3.gg/) project bootstrapped with `create-t3-app`. 4 | 5 | ## What's next? How do I make an app with this? 6 | 7 | We try to keep this project as simple as possible, so you can start with just the scaffolding we set up for you, and add additional things later when they become necessary. 8 | 9 | If you are not familiar with the different technologies used in this project, please refer to the respective docs. If you still are in the wind, please join our [Discord](https://t3.gg/discord) and ask for help. 10 | 11 | - [Next.js](https://nextjs.org) 12 | - [NextAuth.js](https://next-auth.js.org) 13 | - [Prisma](https://prisma.io) 14 | - [Tailwind CSS](https://tailwindcss.com) 15 | - [tRPC](https://trpc.io) 16 | 17 | ## Learn More 18 | 19 | To learn more about the [T3 Stack](https://create.t3.gg/), take a look at the following resources: 20 | 21 | - [Documentation](https://create.t3.gg/) 22 | - [Learn the T3 Stack](https://create.t3.gg/en/faq#what-learning-resources-are-currently-available) — Check out these awesome tutorials 23 | 24 | You can check out the [create-t3-app GitHub repository](https://github.com/t3-oss/create-t3-app) — your feedback and contributions are welcome! 25 | 26 | ## How do I deploy this? 27 | 28 | Follow our deployment guides for [Vercel](https://create.t3.gg/en/deployment/vercel) and [Docker](https://create.t3.gg/en/deployment/docker) for more information. 29 | -------------------------------------------------------------------------------- /apps/nextjs/next.config.mjs: -------------------------------------------------------------------------------- 1 | import "./src/env.mjs"; 2 | import "@acme/api/env"; 3 | import "@acme/stripe/env"; 4 | 5 | import withMDX from "@next/mdx"; 6 | 7 | /** @type {import("next").NextConfig} */ 8 | const config = { 9 | reactStrictMode: true, 10 | /** Enables hot reloading for local packages without a build step */ 11 | transpilePackages: ["@acme/api", "@acme/db", "@acme/stripe", "@acme/ui"], 12 | pageExtensions: ["ts", "tsx", "mdx"], 13 | experimental: { 14 | mdxRs: true, 15 | }, 16 | 17 | /** We already do linting and typechecking as separate tasks in CI */ 18 | eslint: { ignoreDuringBuilds: true }, 19 | typescript: { ignoreBuildErrors: true }, 20 | }; 21 | 22 | export default withMDX()(config); 23 | -------------------------------------------------------------------------------- /apps/nextjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@acme/nextjs", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "pnpm with-env next build", 7 | "clean": "git clean -xdf .next .turbo node_modules", 8 | "dev": "pnpm with-env next dev", 9 | "lint": "next lint", 10 | "format": "prettier --check \"**/*.{js,cjs,mjs,ts,tsx,md,json}\"", 11 | "start": "pnpm with-env next start", 12 | "typecheck": "tsc --noEmit", 13 | "with-env": "dotenv -e ../../.env.local --" 14 | }, 15 | "dependencies": { 16 | "@acme/api": "^0.1.0", 17 | "@acme/db": "^0.1.0", 18 | "@acme/stripe": "^0.1.0", 19 | "@acme/ui": "^0.1.0", 20 | "@clerk/nextjs": "^4.29.4", 21 | "@dinero.js/currencies": "2.0.0-alpha.14", 22 | "@hookform/resolvers": "^3.3.4", 23 | "@next/mdx": "^14.1.0", 24 | "@t3-oss/env-nextjs": "^0.7.3", 25 | "@tanstack/react-query": "^5.17.15", 26 | "@tanstack/react-table": "^8.11.3", 27 | "@trpc/client": "next", 28 | "@trpc/next": "next", 29 | "@trpc/react-query": "next", 30 | "@trpc/server": "next", 31 | "@vercel/analytics": "^1.1.2", 32 | "date-fns": "^3.2.0", 33 | "dinero.js": "2.0.0-alpha.14", 34 | "framer-motion": "^10.18.0", 35 | "next": "^14.1.0", 36 | "next-themes": "^0.2.1", 37 | "react": "18.2.0", 38 | "react-day-picker": "^8.10.0", 39 | "react-dom": "18.2.0", 40 | "react-hook-form": "^7.49.2", 41 | "react-image-crop": "^11.0.4", 42 | "react-wrap-balancer": "^1.1.0", 43 | "recharts": "^2.10.3", 44 | "superjson": "2.2.1", 45 | "tailwindcss-animate": "^1.0.7", 46 | "zod": "^3.22.4" 47 | }, 48 | "devDependencies": { 49 | "@acme/eslint-config": "^0.2.0", 50 | "@acme/prettier-config": "^0.1.0", 51 | "@acme/tailwind-config": "^0.1.0", 52 | "@acme/tsconfig": "^0.1.0", 53 | "@types/mdx": "^2.0.10", 54 | "@types/node": "^20.11.5", 55 | "@types/react": "^18.2.48", 56 | "@types/react-dom": "^18.2.18", 57 | "autoprefixer": "^10.4.17", 58 | "dotenv-cli": "^7.3.0", 59 | "eslint": "^8.56.0", 60 | "prettier": "^3.2.4", 61 | "tailwindcss": "3.4.1", 62 | "typescript": "^5.3.3" 63 | }, 64 | "eslintConfig": { 65 | "root": true, 66 | "extends": [ 67 | "@acme/eslint-config/base", 68 | "@acme/eslint-config/nextjs", 69 | "@acme/eslint-config/react" 70 | ] 71 | }, 72 | "prettier": "@acme/prettier-config" 73 | } 74 | -------------------------------------------------------------------------------- /apps/nextjs/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | // @ts-expect-error - No types for postcss 2 | module.exports = require("@acme/tailwind-config/postcss"); 3 | -------------------------------------------------------------------------------- /apps/nextjs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juliusmarminge/acme-corp/90a28878e07f15517fdb0892a5c7ecb364364b14/apps/nextjs/public/favicon.ico -------------------------------------------------------------------------------- /apps/nextjs/public/og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juliusmarminge/acme-corp/90a28878e07f15517fdb0892a5c7ecb364364b14/apps/nextjs/public/og-image.png -------------------------------------------------------------------------------- /apps/nextjs/public/t3-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | import * as Icons from "@acme/ui/icons"; 4 | 5 | import { siteConfig } from "~/app/config"; 6 | import { SiteFooter } from "~/components/footer"; 7 | 8 | export default function AuthLayout(props: { children: React.ReactNode }) { 9 | return ( 10 | <> 11 |
12 |
13 |
20 |
21 | 25 | 26 | {siteConfig.name} 27 | 28 |
29 | 30 |
31 | {props.children} 32 |
33 |
34 | 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(auth)/signin/oauth-signin.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { useSignIn } from "@clerk/nextjs"; 5 | import type { OAuthStrategy } from "@clerk/types"; 6 | 7 | import { Button } from "@acme/ui/button"; 8 | import * as Icons from "@acme/ui/icons"; 9 | import { useToast } from "@acme/ui/use-toast"; 10 | 11 | export function OAuthSignIn() { 12 | const [isLoading, setIsLoading] = React.useState(null); 13 | const { signIn, isLoaded: signInLoaded } = useSignIn(); 14 | const { toast } = useToast(); 15 | 16 | const oauthSignIn = async (provider: OAuthStrategy) => { 17 | if (!signInLoaded) return null; 18 | try { 19 | setIsLoading(provider); 20 | await signIn.authenticateWithRedirect({ 21 | strategy: provider, 22 | redirectUrl: "/sso-callback", 23 | redirectUrlComplete: "/dashboard", 24 | }); 25 | } catch (cause) { 26 | console.error(cause); 27 | setIsLoading(null); 28 | toast({ 29 | variant: "destructive", 30 | title: "Error", 31 | description: "Something went wrong, please try again.", 32 | }); 33 | } 34 | }; 35 | 36 | return ( 37 |
38 | 50 | 62 |
63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(auth)/signin/page.tsx: -------------------------------------------------------------------------------- 1 | import type { Route } from "next"; 2 | import Link from "next/link"; 3 | 4 | import { EmailSignIn } from "./email-signin"; 5 | import { OAuthSignIn } from "./oauth-signin"; 6 | 7 | export const runtime = "edge"; 8 | 9 | export default function AuthenticationPage() { 10 | return ( 11 |
12 |
13 |

14 | Create an account 15 |

16 |

17 | Enter your email below to create your account 18 |

19 |
20 |
21 | 22 | 23 |
24 |
25 | 26 |
27 |
28 | 29 | Or continue with 30 | 31 |
32 |
33 | 34 | 35 |
36 | 37 |

38 | By clicking continue, you agree to our{" "} 39 | 43 | Terms of Service 44 | {" "} 45 | and{" "} 46 | 50 | Privacy Policy 51 | 52 | . 53 |

54 |
55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(auth)/signout/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRouter } from "next/navigation"; 4 | import { SignOutButton } from "@clerk/nextjs"; 5 | 6 | import { Button } from "@acme/ui/button"; 7 | 8 | export const runtime = "edge"; 9 | 10 | export default function AuthenticationPage() { 11 | const router = useRouter(); 12 | 13 | return ( 14 |
15 |
16 |

Sign Out

17 |

18 | Are you sure you want to sign out? 19 |

20 | router.push("/?redirect=false")}> 21 | 22 | 23 |
24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(auth)/sso-callback/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect } from "react"; 4 | import { useClerk } from "@clerk/nextjs"; 5 | import type { HandleOAuthCallbackParams } from "@clerk/types"; 6 | 7 | import * as Icons from "@acme/ui/icons"; 8 | 9 | export const runtime = "edge"; 10 | 11 | export default function SSOCallback(props: { 12 | searchParams: HandleOAuthCallbackParams; 13 | }) { 14 | const { handleRedirectCallback } = useClerk(); 15 | 16 | useEffect(() => { 17 | void handleRedirectCallback(props.searchParams); 18 | }, [props.searchParams, handleRedirectCallback]); 19 | 20 | return ( 21 |
22 | 23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(dashboard)/[workspaceId]/[projectId]/_components/loading-card.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Card, 3 | CardContent, 4 | CardDescription, 5 | CardHeader, 6 | CardTitle, 7 | } from "@acme/ui/card"; 8 | import * as Icons from "@acme/ui/icons"; 9 | 10 | export function LoadingCard(props: { 11 | title: string; 12 | description: string; 13 | className?: string; 14 | }) { 15 | return ( 16 | 17 | 18 | {props.title} 19 | {props.description} 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(dashboard)/[workspaceId]/[projectId]/_components/overview.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Bar, BarChart, ResponsiveContainer, XAxis, YAxis } from "recharts"; 4 | 5 | const data = [ 6 | { 7 | name: "Jan", 8 | total: Math.floor(Math.random() * 5000) + 1000, 9 | }, 10 | { 11 | name: "Feb", 12 | total: Math.floor(Math.random() * 5000) + 1000, 13 | }, 14 | { 15 | name: "Mar", 16 | total: Math.floor(Math.random() * 5000) + 1000, 17 | }, 18 | { 19 | name: "Apr", 20 | total: Math.floor(Math.random() * 5000) + 1000, 21 | }, 22 | { 23 | name: "May", 24 | total: Math.floor(Math.random() * 5000) + 1000, 25 | }, 26 | { 27 | name: "Jun", 28 | total: Math.floor(Math.random() * 5000) + 1000, 29 | }, 30 | { 31 | name: "Jul", 32 | total: Math.floor(Math.random() * 5000) + 1000, 33 | }, 34 | { 35 | name: "Aug", 36 | total: Math.floor(Math.random() * 5000) + 1000, 37 | }, 38 | { 39 | name: "Sep", 40 | total: Math.floor(Math.random() * 5000) + 1000, 41 | }, 42 | { 43 | name: "Oct", 44 | total: Math.floor(Math.random() * 5000) + 1000, 45 | }, 46 | { 47 | name: "Nov", 48 | total: Math.floor(Math.random() * 5000) + 1000, 49 | }, 50 | { 51 | name: "Dec", 52 | total: Math.floor(Math.random() * 5000) + 1000, 53 | }, 54 | ]; 55 | 56 | export function Overview() { 57 | return ( 58 | 59 | 60 | 67 | `$${value}`} 73 | /> 74 | 75 | 76 | 77 | ); 78 | } 79 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(dashboard)/[workspaceId]/[projectId]/api-keys/loading.tsx: -------------------------------------------------------------------------------- 1 | import { DashboardShell } from "~/app/(dashboard)/_components/dashboard-shell"; 2 | import { DataTable } from "./data-table"; 3 | import { NewApiKeyDialog } from "./new-api-key-dialog"; 4 | 5 | export default function Loading() { 6 | return ( 7 | } 11 | > 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(dashboard)/[workspaceId]/[projectId]/api-keys/new-api-key-dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { useRouter } from "next/navigation"; 5 | 6 | import { Button } from "@acme/ui/button"; 7 | import { 8 | Dialog, 9 | DialogContent, 10 | DialogDescription, 11 | DialogHeader, 12 | DialogTitle, 13 | DialogTrigger, 14 | } from "@acme/ui/dialog"; 15 | 16 | import { CreateApiKeyForm } from "../../_components/create-api-key-form"; 17 | 18 | export function NewApiKeyDialog(props: { projectId: string }) { 19 | const router = useRouter(); 20 | 21 | const [dialogOpen, setDialogOpen] = React.useState(false); 22 | 23 | return ( 24 | 25 | 26 | 27 | 28 | 29 | 30 | Create API Key 31 | 32 | Fill out the form to create an API key. 33 | 34 | 35 | { 38 | setDialogOpen(false); 39 | router.refresh(); 40 | }} 41 | /> 42 | 43 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(dashboard)/[workspaceId]/[projectId]/api-keys/page.tsx: -------------------------------------------------------------------------------- 1 | import { DashboardShell } from "~/app/(dashboard)/_components/dashboard-shell"; 2 | import { userCanAccess } from "~/lib/project-guard"; 3 | import { api } from "~/trpc/server"; 4 | import { DataTable } from "./data-table"; 5 | import { NewApiKeyDialog } from "./new-api-key-dialog"; 6 | 7 | export const runtime = "edge"; 8 | 9 | export default async function ApiKeysPage(props: { 10 | params: { projectId: string; workspaceId: string }; 11 | }) { 12 | await userCanAccess(props.params.projectId); 13 | 14 | const apiKeys = await api.project.listApiKeys.query({ 15 | projectId: props.params.projectId, 16 | }); 17 | 18 | return ( 19 | } 23 | > 24 | 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(dashboard)/[workspaceId]/[projectId]/danger/delete-project.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useParams, useRouter } from "next/navigation"; 4 | 5 | import { Button } from "@acme/ui/button"; 6 | import { 7 | Card, 8 | CardDescription, 9 | CardFooter, 10 | CardHeader, 11 | CardTitle, 12 | } from "@acme/ui/card"; 13 | import { 14 | Dialog, 15 | DialogClose, 16 | DialogContent, 17 | DialogDescription, 18 | DialogFooter, 19 | DialogHeader, 20 | DialogTitle, 21 | DialogTrigger, 22 | } from "@acme/ui/dialog"; 23 | import * as Icons from "@acme/ui/icons"; 24 | import { useToast } from "@acme/ui/use-toast"; 25 | 26 | import { api } from "~/trpc/client"; 27 | 28 | export function DeleteProject() { 29 | const { projectId } = useParams<{ projectId: string }>(); 30 | const toaster = useToast(); 31 | const router = useRouter(); 32 | 33 | const title = "Delete project"; 34 | const description = "This will delete the project and all of its data."; 35 | 36 | return ( 37 | 38 | 39 | {title} 40 | 41 | {description} 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | {title} 52 | {description} 53 | 54 |
55 | 56 |

This action can not be reverted

57 |
58 | 59 | 60 | 61 | 62 | 83 | 84 |
85 |
86 |
87 |
88 | ); 89 | } 90 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(dashboard)/[workspaceId]/[projectId]/danger/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@acme/ui/button"; 2 | import { 3 | Card, 4 | CardDescription, 5 | CardFooter, 6 | CardHeader, 7 | CardTitle, 8 | } from "@acme/ui/card"; 9 | 10 | import { DashboardShell } from "~/app/(dashboard)/_components/dashboard-shell"; 11 | 12 | export default function Loading() { 13 | return ( 14 | 19 | 20 | 21 | Transfer to Organization 22 | 23 | Transfer this project to an organization 24 | 25 | 26 | 27 | 30 | 31 | 32 | 33 | 34 | 35 | Transfer to Personal 36 | 37 | Transfer this project to your personal workspace 38 | 39 | 40 | 41 | 44 | 45 | 46 | 47 | 48 | 49 | Delete project 50 | 51 | This will delete the project and all of its data. 52 | 53 | 54 | 55 | 58 | 59 | 60 | 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(dashboard)/[workspaceId]/[projectId]/danger/page.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from "react"; 2 | 3 | import { Button } from "@acme/ui/button"; 4 | import { 5 | Card, 6 | CardDescription, 7 | CardFooter, 8 | CardHeader, 9 | CardTitle, 10 | } from "@acme/ui/card"; 11 | 12 | import { DashboardShell } from "~/app/(dashboard)/_components/dashboard-shell"; 13 | import { userCanAccess } from "~/lib/project-guard"; 14 | import { api } from "~/trpc/server"; 15 | import { DeleteProject } from "./delete-project"; 16 | import { TransferProjectToOrganization } from "./transfer-to-organization"; 17 | import { TransferProjectToPersonal } from "./transfer-to-personal"; 18 | 19 | export const runtime = "edge"; 20 | 21 | export default async function DangerZonePage(props: { 22 | params: { projectId: string; workspaceId: string }; 23 | }) { 24 | await userCanAccess(props.params.projectId); 25 | 26 | return ( 27 | 32 | 35 | 36 | Transfer to Organization 37 | 38 | Transfer this project to an organization 39 | 40 | 41 | 42 | 43 | 44 | 45 | } 46 | > 47 | 50 | 51 | 52 | 53 | 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(dashboard)/[workspaceId]/[projectId]/danger/transfer-to-personal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useParams, useRouter } from "next/navigation"; 4 | import { useAuth } from "@clerk/nextjs"; 5 | 6 | import { Button } from "@acme/ui/button"; 7 | import { 8 | Card, 9 | CardDescription, 10 | CardFooter, 11 | CardHeader, 12 | CardTitle, 13 | } from "@acme/ui/card"; 14 | import { 15 | Dialog, 16 | DialogClose, 17 | DialogContent, 18 | DialogDescription, 19 | DialogFooter, 20 | DialogHeader, 21 | DialogTitle, 22 | DialogTrigger, 23 | } from "@acme/ui/dialog"; 24 | import { useToast } from "@acme/ui/use-toast"; 25 | 26 | import { api } from "~/trpc/client"; 27 | 28 | export function TransferProjectToPersonal() { 29 | const { projectId } = useParams<{ projectId: string }>(); 30 | const { userId } = useAuth(); 31 | const toaster = useToast(); 32 | const router = useRouter(); 33 | 34 | const title = "Transfer to Personal"; 35 | const description = "Transfer this project to your personal workspace"; 36 | 37 | return ( 38 | 39 | 40 | {title} 41 | 42 | {description} 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | {title} 53 | {description} 54 | 55 | 56 | 57 | 58 | 59 | 60 | 81 | 82 | 83 | 84 | 85 | 86 | ); 87 | } 88 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(dashboard)/[workspaceId]/[projectId]/error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { usePathname } from "next/navigation"; 5 | 6 | import { Button } from "@acme/ui/button"; 7 | 8 | import { DashboardShell } from "../../_components/dashboard-shell"; 9 | 10 | export default function Error(props: { error: Error; reset: () => void }) { 11 | React.useEffect(() => { 12 | // Log the error to an error reporting service 13 | console.error(props.error); 14 | }, [props.error]); 15 | 16 | // This should prob go in some config to make sure it's synced between loading.tsx, page.tsx and error.tsx etc 17 | const pathname = usePathname(); 18 | const path = pathname.split("/")[3]; 19 | const { title, description } = (() => { 20 | switch (path) { 21 | case "ingestions": 22 | return { 23 | title: "Ingestions", 24 | description: "Ingestion details", 25 | }; 26 | case "pulls": 27 | return { 28 | title: "Pull Request", 29 | description: "Browse pull requests changes", 30 | }; 31 | default: 32 | return { 33 | title: "Overview", 34 | description: "Get an overview of how the project is going", 35 | }; 36 | } 37 | })(); 38 | 39 | return ( 40 | 41 |
42 |

Something went wrong!

43 |

44 | {`We're sorry, something went wrong. Please try again.`} 45 |

46 | 47 |
48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(dashboard)/[workspaceId]/[projectId]/ingestions/[ingestionId]/page.tsx: -------------------------------------------------------------------------------- 1 | import { format } from "date-fns"; 2 | 3 | import { 4 | Table, 5 | TableBody, 6 | TableCell, 7 | TableHead, 8 | TableHeader, 9 | TableRow, 10 | } from "@acme/ui/table"; 11 | 12 | import { DashboardShell } from "~/app/(dashboard)/_components/dashboard-shell"; 13 | import { userCanAccess } from "~/lib/project-guard"; 14 | import { api } from "~/trpc/server"; 15 | 16 | export const runtime = "edge"; 17 | 18 | export default async function IngestionPage(props: { 19 | params: { workspaceId: string; projectId: string; ingestionId: string }; 20 | }) { 21 | await userCanAccess(props.params.projectId); 22 | 23 | const ingestion = await api.ingestion.byId.query({ 24 | id: props.params.ingestionId, 25 | }); 26 | 27 | return ( 28 | 33 | 34 | 35 | 36 | Id 37 | Created At 38 | Commit 39 | Origin 40 | Parent 41 | 42 | 43 | 44 | 45 | {ingestion.id} 46 | 47 | {format(ingestion.createdAt, "yyyy-MM-dd HH:mm:ss")} 48 | 49 | {ingestion.hash} 50 | {ingestion.origin} 51 | {ingestion.parent} 52 | 53 | 54 |
55 |

Schema

56 |
57 |         {JSON.stringify(ingestion.schema, null, 4)}
58 |       
59 |
60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(dashboard)/[workspaceId]/[projectId]/overview/loading.tsx: -------------------------------------------------------------------------------- 1 | import { DashboardShell } from "../../../_components/dashboard-shell"; 2 | 3 | export default function Loading() { 4 | return ( 5 | 10 |
11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(dashboard)/[workspaceId]/[projectId]/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | 3 | export const runtime = "edge"; 4 | 5 | /** 6 | * Suboptimal, would be better off doing this in middleware 7 | */ 8 | export default function ProjectPage(props: { 9 | params: { workspaceId: string; projectId: string }; 10 | }) { 11 | redirect(`/${props.params.workspaceId}/${props.params.projectId}/overview`); 12 | } 13 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(dashboard)/[workspaceId]/[projectId]/settings/_components/rename-project.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | 5 | import type { RenameProject } from "@acme/api/validators"; 6 | import { renameProjectSchema } from "@acme/api/validators"; 7 | import { Button } from "@acme/ui/button"; 8 | import { 9 | Card, 10 | CardContent, 11 | CardDescription, 12 | CardFooter, 13 | CardHeader, 14 | CardTitle, 15 | } from "@acme/ui/card"; 16 | import { 17 | Form, 18 | FormControl, 19 | FormField, 20 | FormItem, 21 | FormLabel, 22 | FormMessage, 23 | } from "@acme/ui/form"; 24 | import { Input } from "@acme/ui/input"; 25 | import { useToast } from "@acme/ui/use-toast"; 26 | 27 | import { useZodForm } from "~/lib/zod-form"; 28 | import { api } from "~/trpc/client"; 29 | 30 | export function RenameProject(props: { 31 | currentName: string; 32 | projectId: string; 33 | }) { 34 | const { toast } = useToast(); 35 | 36 | const form = useZodForm({ 37 | schema: renameProjectSchema, 38 | defaultValues: { 39 | projectId: props.projectId, 40 | name: props.currentName, 41 | }, 42 | }); 43 | 44 | async function onSubmit(data: RenameProject) { 45 | await api.project.rename.mutate(data); 46 | toast({ 47 | title: "Project name updated", 48 | description: "Your project's name has been updated.", 49 | }); 50 | } 51 | 52 | return ( 53 | 54 | 55 | Project name 56 | 57 | Change the display name of your project 58 | 59 | 60 | 61 |
62 | 63 | 64 | ( 68 | 69 | Name 70 | 71 | 72 | 73 | 74 | 75 | )} 76 | /> 77 | 78 | 79 | 82 | 83 |
84 | 85 |
86 | ); 87 | } 88 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(dashboard)/[workspaceId]/[projectId]/settings/loading.tsx: -------------------------------------------------------------------------------- 1 | import { DashboardShell } from "~/app/(dashboard)/_components/dashboard-shell"; 2 | import { RenameProject } from "./_components/rename-project"; 3 | 4 | export default function Loading() { 5 | return ( 6 | 11 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(dashboard)/[workspaceId]/[projectId]/settings/page.tsx: -------------------------------------------------------------------------------- 1 | import { DashboardShell } from "~/app/(dashboard)/_components/dashboard-shell"; 2 | import { api } from "~/trpc/server"; 3 | import { RenameProject } from "./_components/rename-project"; 4 | 5 | export const runtime = "edge"; 6 | 7 | export default async function ProjectSettingsPage(props: { 8 | params: { workspaceId: string; projectId: string }; 9 | }) { 10 | const { projectId } = props.params; 11 | const project = await api.project.byId.query({ id: projectId }); 12 | 13 | return ( 14 | 19 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(dashboard)/[workspaceId]/_components/create-project-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRouter } from "next/navigation"; 4 | 5 | import type { CreateProject } from "@acme/api/validators"; 6 | import { createProjectSchema } from "@acme/api/validators"; 7 | import { Button } from "@acme/ui/button"; 8 | import { 9 | Form, 10 | FormControl, 11 | FormDescription, 12 | FormField, 13 | FormItem, 14 | FormLabel, 15 | FormMessage, 16 | } from "@acme/ui/form"; 17 | import { Input } from "@acme/ui/input"; 18 | import { useToast } from "@acme/ui/use-toast"; 19 | 20 | import { useZodForm } from "~/lib/zod-form"; 21 | import { api } from "~/trpc/client"; 22 | 23 | export const CreateProjectForm = (props: { 24 | workspaceId: string; 25 | // defaults to redirecting to the project page 26 | onSuccess?: (project: CreateProject & { id: string }) => void; 27 | }) => { 28 | const router = useRouter(); 29 | const toaster = useToast(); 30 | 31 | const form = useZodForm({ schema: createProjectSchema }); 32 | 33 | async function onSubmit(data: CreateProject) { 34 | try { 35 | const projectId = await api.project.create.mutate(data); 36 | if (props.onSuccess) { 37 | props.onSuccess({ 38 | ...data, 39 | id: projectId, 40 | }); 41 | } else { 42 | router.push(`/${props.workspaceId}/${projectId}/overview`); 43 | } 44 | toaster.toast({ 45 | title: "Project created", 46 | description: `Project ${data.name} created successfully.`, 47 | }); 48 | } catch (error) { 49 | toaster.toast({ 50 | title: "Error creating project", 51 | variant: "destructive", 52 | description: 53 | "An issue occurred while creating your project. Please try again.", 54 | }); 55 | } 56 | } 57 | 58 | return ( 59 |
60 | 61 | ( 65 | 66 | Name * 67 | 68 | 69 | 70 | 71 | A name to identify your app in the dashboard. 72 | 73 | 74 | 75 | )} 76 | /> 77 | 78 | ( 82 | 83 | URL 84 | 85 | 86 | 87 | The URL of your app 88 | 89 | 90 | )} 91 | /> 92 | 93 | 94 | 95 | 96 | ); 97 | }; 98 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(dashboard)/[workspaceId]/_components/project-card.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | import type { RouterOutputs } from "@acme/api"; 4 | import { ProjectTier } from "@acme/db"; 5 | import { cn } from "@acme/ui"; 6 | import { Card, CardDescription, CardHeader, CardTitle } from "@acme/ui/card"; 7 | 8 | import { getRandomPatternStyle } from "~/lib/generate-pattern"; 9 | 10 | function ProjectTierIndicator(props: { tier: ProjectTier }) { 11 | return ( 12 | 19 | {props.tier} 20 | 21 | ); 22 | } 23 | 24 | export function ProjectCard(props: { 25 | workspaceId: string; 26 | project: RouterOutputs["project"]["listByActiveWorkspace"]["projects"][number]; 27 | }) { 28 | const { project } = props; 29 | return ( 30 | 31 | 32 |
33 | 34 | 35 | {project.name} 36 | 37 | 38 | {project.url}  39 | 40 | 41 | 42 | ); 43 | } 44 | 45 | ProjectCard.Skeleton = function ProjectCardSkeleton(props: { 46 | pulse?: boolean; 47 | }) { 48 | const { pulse = true } = props; 49 | return ( 50 | 51 |
52 | 53 | 54 | 55 |   56 | 57 | 58 | 59 | 60 |   61 | 62 | 63 | 64 | ); 65 | }; 66 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(dashboard)/[workspaceId]/_components/sidebar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Link from "next/link"; 4 | import { useParams, usePathname } from "next/navigation"; 5 | 6 | import { cn } from "@acme/ui"; 7 | import * as Icons from "@acme/ui/icons"; 8 | 9 | const workspaceItems = [ 10 | { 11 | title: "Projects", 12 | href: "/", 13 | icon: Icons.Post, 14 | }, 15 | { 16 | title: "Billing", 17 | href: "/billing", 18 | icon: Icons.Billing, 19 | }, 20 | { 21 | title: "Danger Zone", 22 | href: "/danger", 23 | icon: Icons.Warning, 24 | }, 25 | { 26 | title: "Settings", 27 | href: "/settings", 28 | icon: Icons.Settings, 29 | }, 30 | ] as const; 31 | 32 | const projectItems = [ 33 | { 34 | title: "Dashboard", 35 | href: "/", 36 | icon: Icons.Dashboard, 37 | }, 38 | { 39 | title: "API Keys", 40 | href: "/api-keys", 41 | icon: Icons.Key, 42 | }, 43 | { 44 | title: "Danger Zone", 45 | href: "/danger", 46 | icon: Icons.Warning, 47 | }, 48 | { 49 | title: "Settings", 50 | href: "/settings", 51 | icon: Icons.Settings, 52 | }, 53 | ] as const; 54 | 55 | export function SidebarNav() { 56 | const params = useParams<{ 57 | workspaceId: string; 58 | projectId?: string; 59 | }>(); 60 | const path = usePathname(); 61 | 62 | // remove the workspaceId and projectId from the path when comparing active links in sidebar 63 | const pathname = 64 | path 65 | .replace(`/${params.workspaceId}`, "") 66 | .replace(`/${params.projectId}`, "") || "/"; 67 | 68 | const items = params.projectId ? projectItems : workspaceItems; 69 | if (!items?.length) { 70 | return null; 71 | } 72 | 73 | return ( 74 | 106 | ); 107 | } 108 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(dashboard)/[workspaceId]/billing/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardContent, CardHeader, CardTitle } from "@acme/ui/card"; 2 | 3 | import { DashboardShell } from "../../_components/dashboard-shell"; 4 | 5 | export default function Loading() { 6 | return ( 7 | 12 | 13 | 14 | 15 | ); 16 | } 17 | 18 | function LoadingCard(props: { title: string }) { 19 | return ( 20 | 21 | 22 | {props.title} 23 | 24 | 25 |
26 | 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(dashboard)/[workspaceId]/billing/page.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Card, 3 | CardContent, 4 | CardFooter, 5 | CardHeader, 6 | CardTitle, 7 | } from "@acme/ui/card"; 8 | 9 | import { api } from "~/trpc/server"; 10 | import { DashboardShell } from "../../_components/dashboard-shell"; 11 | import { SubscriptionForm } from "./subscription-form"; 12 | 13 | export const runtime = "edge"; 14 | 15 | export default function BillingPage() { 16 | return ( 17 | 22 | 23 | 24 | 25 | 26 | ); 27 | } 28 | 29 | async function SubscriptionCard() { 30 | const subscription = await api.auth.mySubscription.query(); 31 | 32 | return ( 33 | 34 | 35 | Subscription 36 | 37 | 38 | {subscription ? ( 39 |

40 | You are currently on the {subscription.plan} plan. 41 | Your subscription will renew on{" "} 42 | {subscription.endsAt?.toLocaleDateString()}. 43 |

44 | ) : ( 45 |

You are not subscribed to any plan.

46 | )} 47 |
48 | 49 | 50 | 51 |
52 | ); 53 | } 54 | 55 | function UsageCard() { 56 | return ( 57 | 58 | 59 | Usage 60 | 61 | TODO 62 | 63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(dashboard)/[workspaceId]/billing/subscription-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@acme/ui/button"; 4 | 5 | import { api } from "~/trpc/client"; 6 | 7 | export function SubscriptionForm(props: { hasSubscription: boolean }) { 8 | async function createSession() { 9 | const { url } = await api.stripe.createSession.mutate({ planId: "" }); 10 | if (url) window.location.href = url; 11 | } 12 | 13 | return ( 14 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(dashboard)/[workspaceId]/danger/delete-workspace.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRouter } from "next/navigation"; 4 | import { useAuth } from "@clerk/nextjs"; 5 | 6 | import { Button } from "@acme/ui/button"; 7 | import { 8 | Card, 9 | CardDescription, 10 | CardFooter, 11 | CardHeader, 12 | CardTitle, 13 | } from "@acme/ui/card"; 14 | import { 15 | Dialog, 16 | DialogClose, 17 | DialogContent, 18 | DialogDescription, 19 | DialogFooter, 20 | DialogHeader, 21 | DialogTitle, 22 | DialogTrigger, 23 | } from "@acme/ui/dialog"; 24 | import * as Icons from "@acme/ui/icons"; 25 | import { useToast } from "@acme/ui/use-toast"; 26 | 27 | import { api } from "~/trpc/client"; 28 | 29 | export function DeleteWorkspace() { 30 | const toaster = useToast(); 31 | const router = useRouter(); 32 | const { orgId } = useAuth(); 33 | 34 | const title = "Delete workspace"; 35 | const description = "This will delete the workspace and all of its data."; 36 | 37 | return ( 38 | 39 | 40 | {title} 41 | 42 | {description} 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | {!orgId && ( 51 | 52 | You can not delete your personal workspace 53 | 54 | )} 55 | 56 | 57 | {title} 58 | {description} 59 | 60 |
61 | 62 |

This action can not be reverted

63 |
64 | 65 | 66 | 67 | 68 | 85 | 86 |
87 |
88 |
89 |
90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(dashboard)/[workspaceId]/danger/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@acme/ui/button"; 2 | import { 3 | Card, 4 | CardDescription, 5 | CardFooter, 6 | CardHeader, 7 | CardTitle, 8 | } from "@acme/ui/card"; 9 | 10 | import { DashboardShell } from "../../_components/dashboard-shell"; 11 | 12 | export default function Loading() { 13 | return ( 14 | 19 | 20 | 21 | Delete workspace 22 | 23 | This will delete the workspace and all of its data. 24 | 25 | 26 | 27 | 30 | 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(dashboard)/[workspaceId]/danger/page.tsx: -------------------------------------------------------------------------------- 1 | import { DashboardShell } from "../../_components/dashboard-shell"; 2 | import { DeleteWorkspace } from "./delete-workspace"; 3 | 4 | export const runtime = "edge"; 5 | 6 | export default function DangerZonePage() { 7 | return ( 8 | 13 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(dashboard)/[workspaceId]/layout.tsx: -------------------------------------------------------------------------------- 1 | import { SidebarNav } from "./_components/sidebar"; 2 | import { SyncActiveOrgFromUrl } from "./sync-active-org-from-url"; 3 | 4 | export default function WorkspaceLayout(props: { 5 | children: React.ReactNode; 6 | params: { workspaceId: string }; 7 | }) { 8 | return ( 9 | <> 10 | {/* TODO: Nuke it when we can do it serverside in Clerk! */} 11 | 12 |
13 | 16 |
17 | {props.children} 18 |
19 |
20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(dashboard)/[workspaceId]/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@acme/ui/button"; 2 | 3 | import { DashboardShell } from "../_components/dashboard-shell"; 4 | import { ProjectCard } from "./_components/project-card"; 5 | 6 | export default function Loading() { 7 | return ( 8 | Create a new project} 12 | > 13 |
    14 | 15 | 16 | 17 |
18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(dashboard)/[workspaceId]/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { Balancer } from "react-wrap-balancer"; 3 | 4 | import { Button } from "@acme/ui/button"; 5 | 6 | import { api } from "~/trpc/server"; 7 | import { DashboardShell } from "../_components/dashboard-shell"; 8 | import { ProjectCard } from "./_components/project-card"; 9 | 10 | export const runtime = "edge"; 11 | 12 | export default async function Page(props: { params: { workspaceId: string } }) { 13 | const { projects, limitReached } = 14 | await api.project.listByActiveWorkspace.query(); 15 | 16 | return ( 17 | Project limit reached 23 | ) : ( 24 | 27 | ) 28 | } 29 | > 30 |
    31 | {projects.map((project) => ( 32 |
  • 33 | 37 |
  • 38 | ))} 39 |
40 | 41 | {projects.length === 0 && ( 42 |
43 |
    44 | 45 | 46 | 47 |
48 |
49 | 50 |

51 | This workspace has no projects yet 52 |

53 |

54 | Create your first project to get started 55 |

56 |
57 |
58 |
59 | )} 60 |
61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(dashboard)/[workspaceId]/settings/_components/organization-name.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { useOrganization } from "@clerk/nextjs"; 5 | 6 | import { Button } from "@acme/ui/button"; 7 | import { 8 | Card, 9 | CardContent, 10 | CardDescription, 11 | CardFooter, 12 | CardHeader, 13 | CardTitle, 14 | } from "@acme/ui/card"; 15 | import { Input } from "@acme/ui/input"; 16 | import { Label } from "@acme/ui/label"; 17 | import { useToast } from "@acme/ui/use-toast"; 18 | 19 | export function OrganizationName(props: { name: string; orgId: string }) { 20 | const { organization } = useOrganization(); 21 | const [updating, setUpdating] = React.useState(false); 22 | const { toast } = useToast(); 23 | 24 | return ( 25 | 26 | 27 | Organization Name 28 | Change the name of your organization 29 | 30 | 31 |
{ 34 | e.preventDefault(); 35 | const name = new FormData(e.currentTarget).get("name"); 36 | if (!name || typeof name !== "string") return; 37 | setUpdating(true); 38 | await organization?.update({ name, slug: props.orgId }); 39 | setUpdating(false); 40 | toast({ 41 | title: "Organization name updated", 42 | description: "Your organization name has been updated.", 43 | }); 44 | }} 45 | > 46 | 47 | 48 | 49 | 50 | 51 | 59 | 60 |
61 |
62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(dashboard)/[workspaceId]/sync-active-org-from-url.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { useParams } from "next/navigation"; 5 | import { useOrganizationList } from "@clerk/nextjs"; 6 | 7 | /** 8 | * I couldn't find a way to do this on the server :thinking: Clerk is adding support for this soon. 9 | * If I go to /[workspaceId]/**, I want to set the active organization to the workspaceId, 10 | * If it's a personal worksapce, set the organization to null, else find the organization by id 11 | * and set it to that. 12 | */ 13 | export function SyncActiveOrgFromUrl() { 14 | const { workspaceId } = useParams<{ workspaceId: string }>(); 15 | const { setActive, userMemberships, isLoaded } = useOrganizationList({ 16 | userMemberships: { 17 | infinite: true, 18 | }, 19 | }); 20 | 21 | React.useEffect(() => { 22 | if (!isLoaded || userMemberships.isLoading) return; 23 | 24 | if (!workspaceId?.startsWith("org_")) { 25 | void setActive({ organization: null }); 26 | return; 27 | } 28 | 29 | const org = userMemberships?.data?.find( 30 | ({ organization }) => organization.id === workspaceId, 31 | ); 32 | 33 | if (org) { 34 | void setActive(org); 35 | } 36 | 37 | // eslint-disable-next-line react-hooks/exhaustive-deps 38 | }, [workspaceId, isLoaded]); 39 | 40 | return null; 41 | } 42 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(dashboard)/_components/breadcrumbs.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Link from "next/link"; 4 | import { usePathname } from "next/navigation"; 5 | 6 | import { cn } from "@acme/ui"; 7 | 8 | const items = { 9 | overview: "Overview", 10 | analytics: "Analytics", 11 | reports: "Reports", 12 | notifications: "Notifications", 13 | }; 14 | 15 | export function Breadcrumbs() { 16 | const pathname = usePathname(); 17 | const [_, workspaceId, projectId, ...rest] = pathname.split("/"); 18 | const baseUrl = `/${workspaceId}/${projectId}`; 19 | const restAsString = rest.join("/"); 20 | 21 | return ( 22 |
23 | {Object.entries(items).map(([key, value]) => { 24 | const isActive = 25 | key === restAsString || (key !== "" && restAsString.startsWith(key)); 26 | return ( 27 | 35 | {value} 36 | 37 | ); 38 | })} 39 |
40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(dashboard)/_components/dashboard-shell.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { Breadcrumbs } from "./breadcrumbs"; 4 | 5 | export function DashboardShell(props: { 6 | title: string; 7 | description: React.ReactNode; 8 | breadcrumb?: boolean; 9 | headerAction?: React.ReactNode; 10 | children: React.ReactNode; 11 | className?: string; 12 | }) { 13 | return ( 14 |
15 |
16 |
17 |

18 | {props.title} 19 |

20 | {typeof props.description === "string" ? ( 21 |

22 | {props.description} 23 |

24 | ) : ( 25 | props.description 26 | )} 27 |
28 | {props.headerAction} 29 |
30 | {props.breadcrumb && } 31 |
{props.children}
32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(dashboard)/_components/date-range-picker.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { addDays, format } from "date-fns"; 5 | import { Calendar as CalendarIcon } from "lucide-react"; 6 | 7 | import { cn } from "@acme/ui"; 8 | import { Button } from "@acme/ui/button"; 9 | import { Calendar } from "@acme/ui/calendar"; 10 | import type { DateRange } from "@acme/ui/calendar"; 11 | import { Popover, PopoverContent, PopoverTrigger } from "@acme/ui/popover"; 12 | 13 | export function CalendarDateRangePicker({ 14 | className, 15 | align = "end", 16 | }: React.HTMLAttributes & { 17 | align?: "center" | "end" | "start"; 18 | }) { 19 | const [date, setDate] = React.useState({ 20 | from: new Date(2023, 0, 20), 21 | to: addDays(new Date(2023, 0, 20), 20), 22 | }); 23 | 24 | return ( 25 |
26 | 27 | 28 | 51 | 52 | 53 | 61 | 62 | 63 |
64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(dashboard)/_components/main-nav.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | import { cn } from "@acme/ui"; 4 | 5 | import { navItems } from "~/app/config"; 6 | 7 | // TODO: idx not needed as key when all items have unique hrefs 8 | // also, the active link should be filtered by href and not idx 9 | export function MainNav({ 10 | className, 11 | ...props 12 | }: React.HTMLAttributes) { 13 | return ( 14 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(dashboard)/_components/search.tsx: -------------------------------------------------------------------------------- 1 | import { Input } from "@acme/ui/input"; 2 | 3 | export function Search() { 4 | return ( 5 |
6 | 11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(dashboard)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from "react"; 2 | import Link from "next/link"; 3 | 4 | import * as Icons from "@acme/ui/icons"; 5 | 6 | import { SiteFooter } from "~/components/footer"; 7 | import { UserNav } from "~/components/user-nav"; 8 | import { api } from "~/trpc/server"; 9 | import { ProjectSwitcher } from "./_components/project-switcher"; 10 | import { Search } from "./_components/search"; 11 | import { WorkspaceSwitcher } from "./_components/workspace-switcher"; 12 | 13 | export default function DashboardLayout(props: { children: React.ReactNode }) { 14 | return ( 15 |
16 | 36 |
37 | {props.children} 38 |
39 | 40 |
41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(dashboard)/onboarding/create-api-key.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { useRouter, useSearchParams } from "next/navigation"; 3 | import { motion } from "framer-motion"; 4 | import { Balancer } from "react-wrap-balancer"; 5 | 6 | import { CreateApiKeyForm } from "../[workspaceId]/_components/create-api-key-form"; 7 | 8 | export function CreateApiKey() { 9 | const router = useRouter(); 10 | const projectId = useSearchParams().get("projectId"); 11 | 12 | useEffect(() => { 13 | if (!projectId) { 14 | router.push(`/onboarding`); 15 | } 16 | }, [projectId, router]); 17 | 18 | return ( 19 | 24 | 36 | 47 | 48 | {`Next, let's create an API key for your project`} 49 | 50 | 51 | 61 | { 64 | const searchParams = new URLSearchParams(window.location.search); 65 | searchParams.set("step", "done"); 66 | searchParams.set("apiKey", apiKey); 67 | router.push(`/onboarding?${searchParams.toString()}`); 68 | }} 69 | /> 70 | 71 | 72 | 73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(dashboard)/onboarding/create-project.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRouter } from "next/navigation"; 4 | import { motion } from "framer-motion"; 5 | import { Balancer } from "react-wrap-balancer"; 6 | 7 | import { CreateProjectForm } from "../[workspaceId]/_components/create-project-form"; 8 | 9 | export function CreateProject(props: { workspaceId: string }) { 10 | const router = useRouter(); 11 | 12 | return ( 13 | 18 | 30 | 41 | 42 | {`Let's start off by creating your first project`} 43 | 44 | 45 | 55 | { 58 | const searchParams = new URLSearchParams(window.location.search); 59 | searchParams.set("step", "create-api-key"); 60 | searchParams.set("projectId", id); 61 | router.push(`/onboarding?${searchParams.toString()}`); 62 | }} 63 | /> 64 | 65 | 66 | 67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(dashboard)/onboarding/done.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useTransition } from "react"; 2 | import Link from "next/link"; 3 | import { useRouter, useSearchParams } from "next/navigation"; 4 | import { motion } from "framer-motion"; 5 | 6 | export function Done(props: { workspaceId: string }) { 7 | const router = useRouter(); 8 | const search = useSearchParams(); 9 | const step = search.get("step"); 10 | const projectId = search.get("projectId"); 11 | const apiKey = search.get("apiKey"); 12 | 13 | const [_, startTransition] = useTransition(); 14 | useEffect(() => { 15 | if (step === "done") { 16 | setTimeout(() => { 17 | startTransition(() => { 18 | router.push(`${props.workspaceId}/${projectId}/overview`); 19 | router.refresh(); 20 | }); 21 | }, 2000); 22 | } 23 | }, [projectId, props.workspaceId, router, step, apiKey]); 24 | 25 | return ( 26 | 33 | 46 |

47 | You are all set! 48 |

49 |

50 | Congratulations, you have successfully created your first project. 51 | Check out the docs to learn more on how to 52 | use the platform. 53 |

54 |

55 | You will be redirected to your project momentarily. 56 |

57 |
58 |
59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(dashboard)/onboarding/intro.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRouter } from "next/navigation"; 4 | import { motion } from "framer-motion"; 5 | import { Balancer } from "react-wrap-balancer"; 6 | 7 | import { Button } from "@acme/ui/button"; 8 | 9 | import { useDebounce } from "~/lib/use-debounce"; 10 | 11 | export default function Intro() { 12 | const router = useRouter(); 13 | 14 | const showText = useDebounce(true, 800); 15 | 16 | return ( 17 | 22 | {showText && ( 23 | 35 | 46 | Welcome to Acme Corp 47 | 48 | 59 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do 60 | eiusmod tempor incididunt. 61 | 62 | 72 | 78 | 79 | 80 | )} 81 | 82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(dashboard)/onboarding/multi-step-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useSearchParams } from "next/navigation"; 4 | import { AnimatePresence } from "framer-motion"; 5 | 6 | import { CreateApiKey } from "./create-api-key"; 7 | import { CreateProject } from "./create-project"; 8 | import { Done } from "./done"; 9 | import Intro from "./intro"; 10 | 11 | export function Onboarding(props: { workspaceId: string }) { 12 | const search = useSearchParams(); 13 | const step = search.get("step"); 14 | 15 | return ( 16 |
17 | 18 | {!step && } 19 | {step === "create-project" && ( 20 | 21 | )} 22 | {step === "create-api-key" && } 23 | {step === "done" && } 24 | 25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(dashboard)/onboarding/page.tsx: -------------------------------------------------------------------------------- 1 | import { auth } from "@clerk/nextjs"; 2 | 3 | import { Onboarding } from "./multi-step-form"; 4 | 5 | export const runtime = "edge"; 6 | 7 | export default function OnboardingPage() { 8 | const { orgId, userId } = auth(); 9 | 10 | return ( 11 | <> 12 | 13 | 14 |
15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(marketing)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from "react"; 2 | import type { ReactNode } from "react"; 3 | import Link from "next/link"; 4 | import { auth } from "@clerk/nextjs"; 5 | 6 | import { buttonVariants } from "@acme/ui/button"; 7 | import * as Icons from "@acme/ui/icons"; 8 | 9 | import { siteConfig } from "~/app/config"; 10 | import { SiteFooter } from "~/components/footer"; 11 | import { MobileDropdown } from "~/components/mobile-nav"; 12 | import { MainNav } from "../(dashboard)/_components/main-nav"; 13 | 14 | export default function MarketingLayout(props: { children: ReactNode }) { 15 | return ( 16 |
17 | 32 | 33 |
{props.children}
34 | 35 |
36 | ); 37 | } 38 | 39 | function DashboardLink() { 40 | const { userId, orgId } = auth(); 41 | 42 | if (!userId) { 43 | return ( 44 | 45 | Sign In 46 | 47 | 48 | ); 49 | } 50 | 51 | return ( 52 | 56 | Dashboard 57 | 58 | 59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(marketing)/pricing/page.tsx: -------------------------------------------------------------------------------- 1 | import { toDecimal } from "dinero.js"; 2 | import { CheckCircle2 } from "lucide-react"; 3 | import { Balancer } from "react-wrap-balancer"; 4 | 5 | import { 6 | Card, 7 | CardDescription, 8 | CardFooter, 9 | CardHeader, 10 | CardTitle, 11 | } from "@acme/ui/card"; 12 | 13 | import { currencySymbol } from "~/lib/currency"; 14 | import type { RouterOutputs } from "~/trpc/server"; 15 | import { api } from "~/trpc/server"; 16 | import { SubscribeNow } from "./subscribe-now"; 17 | 18 | // FIXME: Run this in Edge runtime - currently got some weird transforming error with Dinero.js + Superjson 19 | // export const runtime = "edge"; 20 | 21 | export default async function PricingPage() { 22 | const plans = await api.stripe.plans.query(); 23 | 24 | return ( 25 |
26 |
27 |

Pricing

28 | 29 | Simple pricing for all your needs. No hidden fees, no surprises. 30 | 31 | 32 |
33 | {plans.map((plan) => ( 34 | 35 | ))} 36 |
37 |
38 |
39 | ); 40 | } 41 | 42 | function PricingCard(props: { 43 | plan: RouterOutputs["stripe"]["plans"][number]; 44 | }) { 45 | return ( 46 | 47 | 48 | {props.plan.name} 49 |
50 | {toDecimal( 51 | props.plan.price, 52 | ({ value, currency }) => `${currencySymbol(currency.code)}${value}`, 53 | )} 54 | / month 55 |
{" "} 56 | {props.plan.description} 57 |
58 | 59 |
    60 | {props.plan.preFeatures && ( 61 |
  • {props.plan.preFeatures}
  • 62 | )} 63 | {props.plan.features.map((feature) => ( 64 |
  • 65 | 66 | {feature} 67 |
  • 68 | ))} 69 |
70 | 71 | 72 | 73 | 74 |
75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(marketing)/pricing/subscribe-now.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRouter } from "next/navigation"; 4 | import { useSession } from "@clerk/nextjs"; 5 | 6 | import { Button } from "@acme/ui/button"; 7 | 8 | import { api } from "~/trpc/client"; 9 | 10 | export function SubscribeNow(props: { planId: string }) { 11 | const router = useRouter(); 12 | const session = useSession(); 13 | 14 | return ( 15 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(marketing)/privacy/page.mdx: -------------------------------------------------------------------------------- 1 | export const runtime = "edge"; 2 | 3 |
4 | 5 | # Privacy 6 | 7 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec bibendum pretium congue. Vivamus hendrerit, sem id fringilla accumsan, ex ex suscipit tellus, vitae volutpat enim velit vel erat. Quisque scelerisque maximus rutrum. Curabitur non suscipit augue, sit amet lobortis libero. Sed semper justo sit amet congue semper. Cras imperdiet ultricies faucibus. Sed placerat, urna ut vehicula aliquet, libero urna imperdiet odio, in condimentum felis arcu sit amet nibh. Curabitur eu lectus sit amet est iaculis posuere at a orci. In gravida nunc vel orci pharetra, id vulputate ligula placerat. Praesent fringilla massa ac urna tempus, eu venenatis turpis aliquam. Pellentesque nec dapibus velit. Aenean sagittis nisi purus, vitae vehicula sapien mattis sit amet. Etiam commodo sit amet quam eleifend commodo. Nunc blandit commodo urna at porta. 8 | 9 | Sed sed turpis justo. Aenean congue nisi ligula, id vulputate nibh ullamcorper ac. Maecenas molestie cursus blandit. In dolor erat, venenatis rutrum porta nec, placerat ut velit. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Fusce placerat, purus sit amet accumsan condimentum, lorem tortor semper felis, vel maximus risus nibh vel nisi. Nam elit diam, sagittis id egestas non, volutpat vel arcu. Ut congue at urna vitae vehicula. Curabitur nec maximus magna. 10 | 11 | Ut sed elementum nisi. Morbi vitae faucibus nisi. Nulla magna purus, blandit sed congue et, posuere ut est. Ut scelerisque risus est, ac tincidunt risus dignissim et. Praesent in elementum sapien. Proin tortor augue, tempus vel sapien non, commodo laoreet ante. Integer id semper turpis. Sed a lobortis orci, sed vulputate odio. Suspendisse eget eros a nibh dictum euismod. Sed congue, dolor vel finibus malesuada, risus massa accumsan nisi, in tincidunt ante nibh et velit. 12 | 13 | Praesent scelerisque lorem quis erat tempus rutrum vitae at enim. Nam vestibulum diam nec euismod sagittis. Mauris ac metus congue, aliquam nibh sit amet, accumsan ex. Nulla vitae efficitur felis, quis hendrerit lacus. Donec id arcu fermentum, commodo enim in, accumsan nisl. Fusce euismod faucibus velit, ac vulputate ex faucibus et. Donec imperdiet egestas ornare. Aenean sit amet varius erat, vel ullamcorper dui. Curabitur nisl mauris, egestas eget ipsum eget, semper aliquet justo. Donec a commodo nunc. Duis ac ullamcorper arcu, tristique molestie enim. Donec tincidunt gravida eros, ut viverra nunc tristique ut. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; 14 | 15 | Morbi rutrum libero vel suscipit dapibus. Suspendisse pellentesque et mauris vel mattis. Nunc non suscipit est. Nullam id enim accumsan, commodo magna vel, elementum lectus. Integer ut orci dolor. Interdum et malesuada fames ac ante ipsum primis in faucibus. Etiam rutrum dignissim metus, in eleifend sapien dictum at. Praesent ac libero purus. Cras vel tortor in lectus placerat porttitor in in massa. Vestibulum dui turpis, cursus vitae posuere eget, accumsan ac nisi. Duis suscipit tortor augue, at iaculis metus consequat eget. Duis id sapien arcu. 16 | 17 |
18 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(marketing)/terms/page.mdx: -------------------------------------------------------------------------------- 1 | export const runtime = "edge"; 2 | 3 |
4 | 5 | # Terms 6 | 7 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec bibendum pretium congue. Vivamus hendrerit, sem id fringilla accumsan, ex ex suscipit tellus, vitae volutpat enim velit vel erat. Quisque scelerisque maximus rutrum. Curabitur non suscipit augue, sit amet lobortis libero. Sed semper justo sit amet congue semper. Cras imperdiet ultricies faucibus. Sed placerat, urna ut vehicula aliquet, libero urna imperdiet odio, in condimentum felis arcu sit amet nibh. Curabitur eu lectus sit amet est iaculis posuere at a orci. In gravida nunc vel orci pharetra, id vulputate ligula placerat. Praesent fringilla massa ac urna tempus, eu venenatis turpis aliquam. Pellentesque nec dapibus velit. Aenean sagittis nisi purus, vitae vehicula sapien mattis sit amet. Etiam commodo sit amet quam eleifend commodo. Nunc blandit commodo urna at porta. 8 | 9 | Sed sed turpis justo. Aenean congue nisi ligula, id vulputate nibh ullamcorper ac. Maecenas molestie cursus blandit. In dolor erat, venenatis rutrum porta nec, placerat ut velit. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Fusce placerat, purus sit amet accumsan condimentum, lorem tortor semper felis, vel maximus risus nibh vel nisi. Nam elit diam, sagittis id egestas non, volutpat vel arcu. Ut congue at urna vitae vehicula. Curabitur nec maximus magna. 10 | 11 | Ut sed elementum nisi. Morbi vitae faucibus nisi. Nulla magna purus, blandit sed congue et, posuere ut est. Ut scelerisque risus est, ac tincidunt risus dignissim et. Praesent in elementum sapien. Proin tortor augue, tempus vel sapien non, commodo laoreet ante. Integer id semper turpis. Sed a lobortis orci, sed vulputate odio. Suspendisse eget eros a nibh dictum euismod. Sed congue, dolor vel finibus malesuada, risus massa accumsan nisi, in tincidunt ante nibh et velit. 12 | 13 | Praesent scelerisque lorem quis erat tempus rutrum vitae at enim. Nam vestibulum diam nec euismod sagittis. Mauris ac metus congue, aliquam nibh sit amet, accumsan ex. Nulla vitae efficitur felis, quis hendrerit lacus. Donec id arcu fermentum, commodo enim in, accumsan nisl. Fusce euismod faucibus velit, ac vulputate ex faucibus et. Donec imperdiet egestas ornare. Aenean sit amet varius erat, vel ullamcorper dui. Curabitur nisl mauris, egestas eget ipsum eget, semper aliquet justo. Donec a commodo nunc. Duis ac ullamcorper arcu, tristique molestie enim. Donec tincidunt gravida eros, ut viverra nunc tristique ut. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; 14 | 15 | Morbi rutrum libero vel suscipit dapibus. Suspendisse pellentesque et mauris vel mattis. Nunc non suscipit est. Nullam id enim accumsan, commodo magna vel, elementum lectus. Integer ut orci dolor. Interdum et malesuada fames ac ante ipsum primis in faucibus. Etiam rutrum dignissim metus, in eleifend sapien dictum at. Praesent ac libero purus. Cras vel tortor in lectus placerat porttitor in in massa. Vestibulum dui turpis, cursus vitae posuere eget, accumsan ac nisi. Duis suscipit tortor augue, at iaculis metus consequat eget. Duis id sapien arcu. 16 | 17 |
18 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/api/trpc/edge/[trpc]/route.ts: -------------------------------------------------------------------------------- 1 | import type { NextRequest } from "next/server"; 2 | import { getAuth } from "@clerk/nextjs/server"; 3 | import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; 4 | 5 | import { createTRPCContext } from "@acme/api"; 6 | import { edgeRouter } from "@acme/api/edge"; 7 | 8 | export const runtime = "edge"; 9 | 10 | const createContext = async (req: NextRequest) => { 11 | return createTRPCContext({ 12 | headers: req.headers, 13 | auth: getAuth(req), 14 | req, 15 | }); 16 | }; 17 | 18 | const handler = (req: NextRequest) => 19 | fetchRequestHandler({ 20 | endpoint: "/api/trpc/edge", 21 | router: edgeRouter, 22 | req: req, 23 | createContext: () => createContext(req), 24 | onError: ({ error, path }) => { 25 | console.log("Error in tRPC handler (edge) on path", path); 26 | console.error(error); 27 | }, 28 | }); 29 | 30 | export { handler as GET, handler as POST }; 31 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/api/trpc/lambda/[trpc]/route.ts: -------------------------------------------------------------------------------- 1 | import type { NextRequest } from "next/server"; 2 | import { getAuth } from "@clerk/nextjs/server"; 3 | import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; 4 | 5 | import { createTRPCContext } from "@acme/api"; 6 | import { lambdaRouter } from "@acme/api/lambda"; 7 | 8 | // Stripe is incompatible with Edge runtimes due to using Node.js events 9 | // export const runtime = "edge"; 10 | 11 | const createContext = async (req: NextRequest) => { 12 | return createTRPCContext({ 13 | headers: req.headers, 14 | auth: getAuth(req), 15 | req, 16 | }); 17 | }; 18 | 19 | const handler = (req: NextRequest) => 20 | fetchRequestHandler({ 21 | endpoint: "/api/trpc/lambda", 22 | router: lambdaRouter, 23 | req: req, 24 | createContext: () => createContext(req), 25 | onError: ({ error, path }) => { 26 | console.log("Error in tRPC handler (lambda) on path", path); 27 | console.error(error); 28 | }, 29 | }); 30 | 31 | export { handler as GET, handler as POST }; 32 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/api/webhooks/stripe/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import type { NextRequest } from "next/server"; 3 | 4 | import { handleEvent, stripe } from "@acme/stripe"; 5 | 6 | import { env } from "~/env.mjs"; 7 | 8 | export async function POST(req: NextRequest) { 9 | const payload = await req.text(); 10 | const signature = req.headers.get("Stripe-Signature")!; 11 | 12 | try { 13 | const event = stripe.webhooks.constructEvent( 14 | payload, 15 | signature, 16 | env.STRIPE_WEBHOOK_SECRET, 17 | ); 18 | 19 | await handleEvent(event); 20 | 21 | console.log("✅ Handled Stripe Event", event.type); 22 | return NextResponse.json({ received: true }, { status: 200 }); 23 | } catch (error) { 24 | const message = error instanceof Error ? error.message : "Unknown error"; 25 | console.log(`❌ Error when handling Stripe Event: ${message}`); 26 | return NextResponse.json({ error: message }, { status: 400 }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "react-image-crop/dist/ReactCrop.css"; 2 | import "~/styles/globals.css"; 3 | 4 | import { Inter } from "next/font/google"; 5 | import LocalFont from "next/font/local"; 6 | import { ClerkProvider } from "@clerk/nextjs"; 7 | import { Analytics } from "@vercel/analytics/react"; 8 | 9 | import { cn } from "@acme/ui"; 10 | import { Toaster } from "@acme/ui/toaster"; 11 | 12 | import { TailwindIndicator } from "~/components/tailwind-indicator"; 13 | import { ThemeProvider } from "~/components/theme-provider"; 14 | import { siteConfig } from "./config"; 15 | 16 | const fontSans = Inter({ 17 | subsets: ["latin"], 18 | variable: "--font-sans", 19 | }); 20 | const fontCal = LocalFont({ 21 | src: "../styles/calsans.ttf", 22 | variable: "--font-cal", 23 | }); 24 | 25 | export const metadata = { 26 | title: { 27 | default: siteConfig.name, 28 | template: `%s - ${siteConfig.name}`, 29 | }, 30 | description: siteConfig.description, 31 | icons: { 32 | icon: "/favicon.ico", 33 | }, 34 | openGraph: { 35 | images: [{ url: "/opengraph-image.png" }], 36 | }, 37 | twitter: { 38 | card: "summary_large_image", 39 | title: siteConfig.name, 40 | description: siteConfig.description, 41 | images: [{ url: "https://acme-corp-lib.vercel.app/opengraph-image.png" }], 42 | creator: "@jullerino", 43 | }, 44 | metadataBase: new URL("https://acme-corp.jumr.dev"), 45 | }; 46 | 47 | export default function RootLayout(props: { children: React.ReactNode }) { 48 | return ( 49 | <> 50 | 51 | 52 | 59 | 60 | {props.children} 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /apps/nextjs/src/components/footer.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from "next/dynamic"; 2 | import Link from "next/link"; 3 | 4 | import { cn } from "@acme/ui"; 5 | import { Button } from "@acme/ui/button"; 6 | import * as Icons from "@acme/ui/icons"; 7 | 8 | import { siteConfig } from "~/app/config"; 9 | 10 | const ThemeToggle = dynamic(() => import("~/components/theme-toggle"), { 11 | ssr: false, 12 | loading: () => ( 13 | 23 | ), 24 | }); 25 | 26 | export function SiteFooter(props: { className?: string }) { 27 | return ( 28 | 81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /apps/nextjs/src/components/mobile-nav.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import Link from "next/link"; 5 | 6 | import { Button } from "@acme/ui/button"; 7 | import * as Icons from "@acme/ui/icons"; 8 | import { Popover, PopoverContent, PopoverTrigger } from "@acme/ui/popover"; 9 | import { ScrollArea } from "@acme/ui/scroll-area"; 10 | 11 | import { Search } from "~/app/(dashboard)/_components/search"; 12 | import { navItems, siteConfig } from "~/app/config"; 13 | import ThemeToggle from "./theme-toggle"; 14 | 15 | export function MobileDropdown() { 16 | const [isOpen, setIsOpen] = React.useState(false); 17 | 18 | React.useEffect(() => { 19 | if (isOpen) { 20 | document.body.classList.add("overflow-hidden"); 21 | } else { 22 | document.body.classList.remove("overflow-hidden"); 23 | } 24 | }, [isOpen]); 25 | 26 | return ( 27 | 28 | 29 | 38 | 39 | 40 | 41 | 42 | {navItems.map((item) => ( 43 | 49 | {item.title} 50 | 51 | ))} 52 | 53 |
54 | 55 |
56 |
57 |
58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /apps/nextjs/src/components/tailwind-indicator.tsx: -------------------------------------------------------------------------------- 1 | export function TailwindIndicator() { 2 | if (process.env.NODE_ENV === "production") return null; 3 | 4 | return ( 5 |
6 |
xs
7 |
8 | sm 9 |
10 |
md
11 |
lg
12 |
xl
13 |
2xl
14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /apps/nextjs/src/components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ThemeProvider as NextThemeProvider } from "next-themes"; 4 | 5 | export const ThemeProvider = NextThemeProvider; 6 | -------------------------------------------------------------------------------- /apps/nextjs/src/components/theme-toggle.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { useTheme } from "next-themes"; 5 | 6 | import { Button } from "@acme/ui/button"; 7 | import { 8 | DropdownMenu, 9 | DropdownMenuContent, 10 | DropdownMenuItem, 11 | DropdownMenuTrigger, 12 | } from "@acme/ui/dropdown-menu"; 13 | import * as Icons from "@acme/ui/icons"; 14 | 15 | export default function ThemeToggle(props: { 16 | align?: "center" | "start" | "end"; 17 | side?: "top" | "bottom"; 18 | }) { 19 | const { setTheme, theme } = useTheme(); 20 | 21 | const triggerIcon = { 22 | light: , 23 | dark: , 24 | system: , 25 | }[theme as "light" | "dark" | "system"]; 26 | 27 | return ( 28 | 29 | 30 | 39 | 40 | 41 | setTheme("light")}> 42 | 43 | Light 44 | 45 | setTheme("dark")}> 46 | 47 | Dark 48 | 49 | setTheme("system")}> 50 | 51 | System 52 | 53 | 54 | 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /apps/nextjs/src/env.mjs: -------------------------------------------------------------------------------- 1 | import { createEnv } from "@t3-oss/env-nextjs"; 2 | import { z } from "zod"; 3 | 4 | export const env = createEnv({ 5 | shared: { 6 | NODE_ENV: z.enum(["development", "test", "production"]), 7 | }, 8 | server: { 9 | DATABASE_URL: z.string().url(), 10 | CLERK_SECRET_KEY: z.string().optional(), 11 | STRIPE_WEBHOOK_SECRET: z.string(), 12 | }, 13 | client: { 14 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: z.string().min(1), 15 | }, 16 | // Client side variables gets destructured here due to Next.js static analysis 17 | // Shared ones are also included here for good measure since the behavior has been inconsistent 18 | experimental__runtimeEnv: { 19 | NODE_ENV: process.env.NODE_ENV, 20 | 21 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: 22 | process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY, 23 | }, 24 | skipValidation: 25 | !!process.env.SKIP_ENV_VALIDATION || 26 | process.env.npm_lifecycle_event === "lint", 27 | }); 28 | -------------------------------------------------------------------------------- /apps/nextjs/src/lib/currency.ts: -------------------------------------------------------------------------------- 1 | export const currencySymbol = (curr: string) => 2 | ({ 3 | USD: "$", 4 | EUR: "€", 5 | GBP: "£", 6 | })[curr] ?? curr; 7 | -------------------------------------------------------------------------------- /apps/nextjs/src/lib/project-guard.ts: -------------------------------------------------------------------------------- 1 | import { notFound } from "next/navigation"; 2 | 3 | import { db } from "@acme/db"; 4 | 5 | export async function userCanAccess(projectId: string) { 6 | if (!projectId.startsWith("project_")) { 7 | notFound(); 8 | } 9 | 10 | // see if project exists 11 | const project = await db 12 | .selectFrom("Project") 13 | .select("id") 14 | .where("id", "=", projectId) 15 | .executeTakeFirst(); 16 | 17 | if (!project) { 18 | notFound(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /apps/nextjs/src/lib/use-debounce.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | export function useDebounce(value: T, delay: number) { 4 | const [debouncedValue, setDebouncedValue] = useState(value); 5 | 6 | useEffect(() => { 7 | const timeoutId = setTimeout(() => { 8 | setDebouncedValue(value); 9 | }, delay); 10 | 11 | return () => { 12 | clearTimeout(timeoutId); 13 | }; 14 | }, [value, delay]); 15 | 16 | return debouncedValue; 17 | } 18 | -------------------------------------------------------------------------------- /apps/nextjs/src/lib/zod-form.tsx: -------------------------------------------------------------------------------- 1 | import { zodResolver } from "@hookform/resolvers/zod"; 2 | import { useForm } from "react-hook-form"; 3 | import type { UseFormProps } from "react-hook-form"; 4 | import type { ZodType } from "zod"; 5 | 6 | export function useZodForm( 7 | props: Omit, "resolver"> & { 8 | schema: TSchema; 9 | }, 10 | ) { 11 | const form = useForm({ 12 | ...props, 13 | resolver: zodResolver(props.schema, undefined), 14 | }); 15 | 16 | return form; 17 | } 18 | -------------------------------------------------------------------------------- /apps/nextjs/src/mdx-components.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { Route } from "next"; 3 | import Link from "next/link"; 4 | import type { MDXComponents } from "mdx/types"; 5 | 6 | // This file is required to use MDX in `app` directory. 7 | export function useMDXComponents(components: MDXComponents): MDXComponents { 8 | return { 9 | // Allows customizing built-in components, e.g. to add styling. 10 | h1: (props) => ( 11 |

12 | {props.children} 13 |

14 | ), 15 | h2: (props) => ( 16 |

20 | {props.children} 21 |

22 | ), 23 | h3: (props) => ( 24 |

25 | {props.children} 26 |

27 | ), 28 | h4: (props) => ( 29 |

30 | {props.children} 31 |

32 | ), 33 | p: (props) => ( 34 |

35 | ), 36 | a: ({ children, href }) => { 37 | const isExternal = href?.startsWith("http"); 38 | const Component = isExternal ? "a" : Link; 39 | return ( 40 | 44 | {children} 45 | 46 | ); 47 | }, 48 | ul: (props) =>

    , 49 | code: (props) => ( 50 | 54 | ), 55 | // eslint-disable-next-line @next/next/no-img-element 56 | img: (props) => {props.alt}, 57 | 58 | // Pass through all other components. 59 | ...components, 60 | }; 61 | } 62 | -------------------------------------------------------------------------------- /apps/nextjs/src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { authMiddleware, clerkClient } from "@clerk/nextjs"; 3 | 4 | export default authMiddleware({ 5 | signInUrl: "/signin", 6 | publicRoutes: [ 7 | "/", 8 | "/opengraph-image.png", 9 | "/signin(.*)", 10 | "/sso-callback(.*)", 11 | "/terms(.*)", 12 | "/pricing(.*)", 13 | "/privacy(.*)", 14 | "/api(.*)", 15 | ], 16 | async afterAuth(auth, req) { 17 | if (auth.isPublicRoute) { 18 | // Don't do anything for public routes 19 | return NextResponse.next(); 20 | } 21 | 22 | const url = new URL(req.nextUrl.origin); 23 | const parts = req.nextUrl.pathname.split("/").filter(Boolean); 24 | 25 | if (!auth.userId) { 26 | // User is not signed in 27 | url.pathname = "/signin"; 28 | return NextResponse.redirect(url); 29 | } 30 | 31 | if (req.nextUrl.pathname === "/dashboard") { 32 | // /dashboard should redirect to the user's dashboard 33 | // use their current workspace, i.e. /:orgId or /:userId 34 | url.pathname = `/${auth.orgId ?? auth.userId}`; 35 | return NextResponse.redirect(url); 36 | } 37 | 38 | /** 39 | * TODO: I'd prefer not showing the ID in the URL but 40 | * a more friendly looking slug. For example, 41 | * /org_foo34213 -> /foo 42 | * /user_bar123/project_acm234231sfsdfa -> /bar/baz 43 | */ 44 | 45 | /** 46 | * TODO: Decide if redirects should 404 or redirect to / 47 | */ 48 | 49 | const workspaceId = parts[0]; 50 | const isOrg = workspaceId?.startsWith("org_"); 51 | if (isOrg && auth.orgId !== workspaceId) { 52 | // User is accessing an org that's not their active one 53 | // Check if they have access to it 54 | const orgs = await clerkClient.users.getOrganizationMembershipList({ 55 | userId: auth.userId, 56 | }); 57 | const hasAccess = orgs.some((org) => org.id === workspaceId); 58 | if (!hasAccess) { 59 | url.pathname = `/`; 60 | return NextResponse.redirect(url); 61 | } 62 | 63 | // User has access to the org, let them pass. 64 | // TODO: Set the active org to the one they're accessing 65 | // so that we don't need to do this client-side. 66 | // This is currently not possible with Clerk but will be. 67 | return NextResponse.next(); 68 | } 69 | 70 | const isUser = workspaceId?.startsWith("user_"); 71 | if (isUser && auth.userId !== workspaceId) { 72 | // User is accessing a user that's not them 73 | url.pathname = `/`; 74 | return NextResponse.redirect(url); 75 | } 76 | 77 | return NextResponse.next(); 78 | }, 79 | }); 80 | 81 | export const config = { 82 | matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"], 83 | }; 84 | -------------------------------------------------------------------------------- /apps/nextjs/src/styles/calsans.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juliusmarminge/acme-corp/90a28878e07f15517fdb0892a5c7ecb364364b14/apps/nextjs/src/styles/calsans.ttf -------------------------------------------------------------------------------- /apps/nextjs/src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0deg 0% 100%; 8 | --foreground: 222.2deg 47.4% 11.2%; 9 | 10 | --muted: 210deg 40% 96.1%; 11 | --muted-foreground: 215.4deg 16.3% 46.9%; 12 | 13 | --popover: 0deg 0% 100%; 14 | --popover-foreground: 222.2deg 47.4% 11.2%; 15 | 16 | --border: 214.3deg 31.8% 91.4%; 17 | --input: 214.3deg 31.8% 91.4%; 18 | 19 | --card: 0deg 0% 100%; 20 | --card-foreground: 222.2deg 47.4% 11.2%; 21 | 22 | --primary: 222.2deg 47.4% 11.2%; 23 | --primary-foreground: 210deg 40% 98%; 24 | 25 | --secondary: 210deg 40% 96.1%; 26 | --secondary-foreground: 222.2deg 47.4% 11.2%; 27 | 28 | --accent: 210deg 40% 96.1%; 29 | --accent-foreground: 222.2deg 47.4% 11.2%; 30 | 31 | --destructive: 0deg 100% 50%; 32 | --destructive-foreground: 210deg 40% 98%; 33 | 34 | --ring: 215deg 20.2% 65.1%; 35 | 36 | --radius: 0.5rem; 37 | } 38 | 39 | .dark { 40 | --background: 224 71% 4%; 41 | --foreground: 213 31% 91%; 42 | 43 | --muted: 223 47% 11%; 44 | --muted-foreground: 215.4 16.3% 56.9%; 45 | 46 | --accent: 216 34% 17%; 47 | --accent-foreground: 210 40% 98%; 48 | 49 | --popover: 224 71% 4%; 50 | --popover-foreground: 215 20.2% 65.1%; 51 | 52 | --border: 216 34% 17%; 53 | --input: 216 34% 17%; 54 | 55 | --card: 224 71% 4%; 56 | --card-foreground: 213 31% 91%; 57 | 58 | --primary: 210 40% 98%; 59 | --primary-foreground: 222.2 47.4% 1.2%; 60 | 61 | --secondary: 222.2 47.4% 11.2%; 62 | --secondary-foreground: 210 40% 98%; 63 | 64 | --destructive: 0 63% 31%; 65 | --destructive-foreground: 210 40% 98%; 66 | 67 | --ring: 216 34% 17%; 68 | 69 | --radius: 0.5rem; 70 | } 71 | } 72 | 73 | @layer base { 74 | body { 75 | @apply bg-background text-foreground; 76 | font-feature-settings: "rlig" 1, "calt" 1; 77 | } 78 | 79 | .container { 80 | @apply max-sm:px-4; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /apps/nextjs/src/trpc/client.ts: -------------------------------------------------------------------------------- 1 | import { createTRPCClient, loggerLink } from "@trpc/client"; 2 | 3 | import type { AppRouter } from "@acme/api"; 4 | 5 | import { endingLink, transformer } from "./shared"; 6 | 7 | export const api = createTRPCClient({ 8 | transformer: transformer, 9 | links: [ 10 | loggerLink({ 11 | enabled: (opts) => 12 | process.env.NODE_ENV === "development" || 13 | (opts.direction === "down" && opts.result instanceof Error), 14 | }), 15 | endingLink({ 16 | headers: { 17 | "x-trpc-source": "client", 18 | }, 19 | }), 20 | ], 21 | }); 22 | 23 | export { type RouterInputs, type RouterOutputs } from "@acme/api"; 24 | -------------------------------------------------------------------------------- /apps/nextjs/src/trpc/server.ts: -------------------------------------------------------------------------------- 1 | import { headers } from "next/headers"; 2 | import { createTRPCClient, loggerLink } from "@trpc/client"; 3 | 4 | import type { AppRouter } from "@acme/api"; 5 | 6 | import { endingLink, transformer } from "./shared"; 7 | 8 | export const api = createTRPCClient({ 9 | transformer: transformer, 10 | links: [ 11 | loggerLink({ 12 | enabled: (opts) => 13 | process.env.NODE_ENV === "development" || 14 | (opts.direction === "down" && opts.result instanceof Error), 15 | }), 16 | endingLink({ 17 | headers: () => { 18 | const h = new Map(headers()); 19 | h.delete("connection"); 20 | h.delete("transfer-encoding"); 21 | h.set("x-trpc-source", "server"); 22 | return Object.fromEntries(h.entries()); 23 | }, 24 | }), 25 | ], 26 | }); 27 | 28 | export { type RouterInputs, type RouterOutputs } from "@acme/api"; 29 | -------------------------------------------------------------------------------- /apps/nextjs/src/trpc/shared.ts: -------------------------------------------------------------------------------- 1 | import type { HTTPBatchLinkOptions, HTTPHeaders, TRPCLink } from "@trpc/client"; 2 | import { httpBatchLink } from "@trpc/client"; 3 | 4 | import type { AppRouter } from "@acme/api"; 5 | 6 | export { transformer } from "@acme/api/transformer"; 7 | 8 | const getBaseUrl = () => { 9 | if (typeof window !== "undefined") return ""; 10 | const vc = process.env.VERCEL_URL; 11 | if (vc) return `https://${vc}`; 12 | return `http://localhost:3000`; 13 | }; 14 | 15 | const lambdas = ["ingestion"]; 16 | 17 | export const endingLink = (opts?: { 18 | headers?: HTTPHeaders | (() => HTTPHeaders); 19 | }) => 20 | ((runtime) => { 21 | const sharedOpts = { 22 | headers: opts?.headers, 23 | } satisfies Partial; 24 | 25 | const edgeLink = httpBatchLink({ 26 | ...sharedOpts, 27 | url: `${getBaseUrl()}/api/trpc/edge`, 28 | })(runtime); 29 | const lambdaLink = httpBatchLink({ 30 | ...sharedOpts, 31 | url: `${getBaseUrl()}/api/trpc/lambda`, 32 | })(runtime); 33 | 34 | return (ctx) => { 35 | const path = ctx.op.path.split(".") as [string, ...string[]]; 36 | const endpoint = lambdas.includes(path[0]) ? "lambda" : "edge"; 37 | 38 | const newCtx = { 39 | ...ctx, 40 | op: { ...ctx.op, path: path.join(".") }, 41 | }; 42 | return endpoint === "edge" ? edgeLink(newCtx) : lambdaLink(newCtx); 43 | }; 44 | }) satisfies TRPCLink; 45 | -------------------------------------------------------------------------------- /apps/nextjs/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | import baseConfig from "@acme/tailwind-config"; 4 | 5 | export default { 6 | // We need to append the path to the UI package to the content array so that 7 | // those classes are included correctly. 8 | content: [...baseConfig.content, "../../packages/ui/src/**/*.{ts,tsx}"], 9 | presets: [baseConfig], 10 | } satisfies Config; 11 | -------------------------------------------------------------------------------- /apps/nextjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@acme/tsconfig/base.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "~/*": ["./src/*"], 7 | }, 8 | "plugins": [ 9 | { 10 | "name": "next", 11 | }, 12 | ], 13 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json", 14 | }, 15 | "include": ["next-env.d.ts", ".next/types/**/*.ts", "*.ts", "*.mjs", "src"], 16 | "exclude": ["node_modules"], 17 | } 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-t3-turbo", 3 | "private": true, 4 | "engines": { 5 | "node": ">=v18.17.1" 6 | }, 7 | "packageManager": "pnpm@8.6.12", 8 | "scripts": { 9 | "build": "turbo build", 10 | "clean": "git clean -xdf node_modules dist .next", 11 | "clean:workspaces": "turbo clean", 12 | "db:generate": "turbo db:generate", 13 | "db:push": "turbo db:push db:generate", 14 | "db:studio": "pnpm -F db studio", 15 | "dev": "cross-env FORCE_COLOR=1 turbo dev --parallel", 16 | "dev:web": "turbo dev --parallel --filter !@acme/expo --filter !@acme/db", 17 | "format": "turbo format --continue -- --cache --cache-location='node_modules/.cache/.prettiercache' --ignore-path='../../.gitignore'", 18 | "format:fix": "turbo format --continue -- --write --cache --cache-location='node_modules/.cache/.prettiercache' --ignore-path='../../.gitignore'", 19 | "lint": "turbo lint --continue -- --cache --cache-location 'node_modules/.cache/.eslintcache' && manypkg check", 20 | "lint:fix": "turbo lint --continue -- --fix --cache --cache-location 'node_modules/.cache/.eslintcache' && manypkg fix", 21 | "typecheck": "turbo typecheck" 22 | }, 23 | "dependencies": { 24 | "@acme/prettier-config": "^0.1.0", 25 | "@manypkg/cli": "^0.21.2", 26 | "@turbo/gen": "^1.11.3", 27 | "cross-env": "^7.0.3", 28 | "prettier": "^3.2.4", 29 | "turbo": "^1.11.3", 30 | "typescript": "^5.3.3" 31 | }, 32 | "prettier": "@acme/prettier-config" 33 | } 34 | -------------------------------------------------------------------------------- /packages/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@acme/api", 3 | "private": true, 4 | "version": "0.1.0", 5 | "exports": { 6 | ".": "./src/index.ts", 7 | "./env": "./src/env.mjs", 8 | "./edge": "./src/edge.ts", 9 | "./lambda": "./src/lambda.ts", 10 | "./transformer": "./src/transformer.ts", 11 | "./validators": "./src/validators.ts" 12 | }, 13 | "typesVersions": { 14 | "*": { 15 | "*": [ 16 | "src/*" 17 | ] 18 | } 19 | }, 20 | "license": "MIT", 21 | "scripts": { 22 | "clean": "rm -rf .turbo node_modules", 23 | "lint": "eslint .", 24 | "format": "prettier --check \"**/*.{mjs,ts,json}\"", 25 | "typecheck": "tsc --noEmit" 26 | }, 27 | "dependencies": { 28 | "@acme/db": "^0.1.0", 29 | "@acme/stripe": "^0.1.0", 30 | "@clerk/nextjs": "^4.29.4", 31 | "@dinero.js/currencies": "2.0.0-alpha.14", 32 | "@t3-oss/env-nextjs": "^0.7.3", 33 | "@trpc/client": "next", 34 | "@trpc/server": "next", 35 | "dinero.js": "2.0.0-alpha.14", 36 | "superjson": "2.2.1", 37 | "zod": "^3.22.4", 38 | "zod-form-data": "^2.0.2" 39 | }, 40 | "devDependencies": { 41 | "@acme/eslint-config": "^0.2.0", 42 | "@acme/prettier-config": "^0.1.0", 43 | "@acme/tsconfig": "^0.1.0", 44 | "eslint": "^8.56.0", 45 | "prettier": "^3.2.4", 46 | "typescript": "^5.3.3" 47 | }, 48 | "eslintConfig": { 49 | "root": true, 50 | "extends": [ 51 | "@acme/eslint-config/base" 52 | ] 53 | }, 54 | "prettier": "@acme/prettier-config" 55 | } 56 | -------------------------------------------------------------------------------- /packages/api/src/edge.ts: -------------------------------------------------------------------------------- 1 | import { authRouter } from "./router/auth"; 2 | import { organizationsRouter } from "./router/organizations"; 3 | import { projectRouter } from "./router/project"; 4 | import { stripeRouter } from "./router/stripe"; 5 | import { createTRPCRouter } from "./trpc"; 6 | 7 | // Deployed to /trpc/edge/** 8 | export const edgeRouter = createTRPCRouter({ 9 | project: projectRouter, 10 | auth: authRouter, 11 | stripe: stripeRouter, 12 | organization: organizationsRouter, 13 | }); 14 | -------------------------------------------------------------------------------- /packages/api/src/env.mjs: -------------------------------------------------------------------------------- 1 | import { createEnv } from "@t3-oss/env-nextjs"; 2 | import * as z from "zod"; 3 | 4 | export const env = createEnv({ 5 | shared: {}, 6 | server: { 7 | NEXTJS_URL: z.preprocess( 8 | (str) => 9 | process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : str, 10 | process.env.VERCEL_URL ? z.string().min(1) : z.string().url(), 11 | ), 12 | }, 13 | // Client side variables gets destructured here due to Next.js static analysis 14 | // Shared ones are also included here for good measure since the behavior has been inconsistent 15 | experimental__runtimeEnv: {}, 16 | skipValidation: 17 | !!process.env.SKIP_ENV_VALIDATION || 18 | process.env.npm_lifecycle_event === "lint", 19 | }); 20 | -------------------------------------------------------------------------------- /packages/api/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server"; 2 | 3 | import type { AppRouter } from "./root"; 4 | 5 | export { createTRPCContext, createInnerTRPCContext } from "./trpc"; 6 | 7 | // TODO: Maybe just export `createAction` instead of the whole `trpc` object? 8 | export { t } from "./trpc"; 9 | 10 | export type { AppRouter } from "./root"; 11 | export { appRouter } from "./root"; 12 | /** 13 | * Inference helpers for input types 14 | * @example type HelloInput = RouterInputs['example']['hello'] 15 | **/ 16 | export type RouterInputs = inferRouterInputs; 17 | 18 | /** 19 | * Inference helpers for output types 20 | * @example type HelloOutput = RouterOutputs['example']['hello'] 21 | **/ 22 | export type RouterOutputs = inferRouterOutputs; 23 | -------------------------------------------------------------------------------- /packages/api/src/lambda.ts: -------------------------------------------------------------------------------- 1 | import { ingestionRouter } from "./router/ingestion"; 2 | import { createTRPCRouter } from "./trpc"; 3 | 4 | // Deployed to /trpc/lambda/** 5 | export const lambdaRouter = createTRPCRouter({ 6 | ingestion: ingestionRouter, 7 | }); 8 | -------------------------------------------------------------------------------- /packages/api/src/root.ts: -------------------------------------------------------------------------------- 1 | import { edgeRouter } from "./edge"; 2 | import { lambdaRouter } from "./lambda"; 3 | import { mergeRouters } from "./trpc"; 4 | 5 | // Used to provide a good DX with a single client 6 | // Then, a custom link is used to generate the correct URL for the request 7 | export const appRouter = mergeRouters(edgeRouter, lambdaRouter); 8 | export type AppRouter = typeof appRouter; 9 | -------------------------------------------------------------------------------- /packages/api/src/router/auth.ts: -------------------------------------------------------------------------------- 1 | import { clerkClient } from "@clerk/nextjs"; 2 | 3 | import { createTRPCRouter, protectedProcedure } from "../trpc"; 4 | 5 | export const authRouter = createTRPCRouter({ 6 | mySubscription: protectedProcedure.query(async (opts) => { 7 | const customer = await opts.ctx.db 8 | .selectFrom("Customer") 9 | .select(["plan", "endsAt"]) 10 | .where("clerkUserId", "=", opts.ctx.auth.userId) 11 | .executeTakeFirst(); 12 | 13 | if (!customer) return null; 14 | 15 | return { plan: customer.plan ?? null, endsAt: customer.endsAt ?? null }; 16 | }), 17 | listOrganizations: protectedProcedure.query(async (opts) => { 18 | const memberships = await clerkClient.users.getOrganizationMembershipList({ 19 | userId: opts.ctx.auth.userId, 20 | }); 21 | 22 | return memberships.map(({ organization }) => ({ 23 | id: organization.id, 24 | name: organization.name, 25 | image: organization.imageUrl, 26 | })); 27 | }), 28 | }); 29 | -------------------------------------------------------------------------------- /packages/api/src/router/ingestion.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { zfd } from "zod-form-data"; 3 | 4 | import { genId } from "@acme/db"; 5 | 6 | import { 7 | createTRPCRouter, 8 | protectedApiFormDataProcedure, 9 | protectedProcedure, 10 | } from "../trpc"; 11 | 12 | globalThis.File = File; 13 | 14 | const myFileValidator = z.preprocess( 15 | // @ts-expect-error - this is a hack. not sure why it's needed since it should already be a File 16 | (file: File) => 17 | new File([file], file.name, { 18 | type: file.type, 19 | lastModified: file.lastModified, 20 | }), 21 | zfd.file(z.instanceof(File)), 22 | ); 23 | 24 | /** 25 | * FIXME: Not all of these have to run on lambda, just the upload one 26 | */ 27 | 28 | export const ingestionRouter = createTRPCRouter({ 29 | byId: protectedProcedure 30 | .input(z.object({ id: z.string() })) 31 | .query(async (opts) => { 32 | const ingestion = await opts.ctx.db 33 | .selectFrom("Ingestion") 34 | .select(["id", "createdAt", "hash", "schema", "origin", "parent"]) 35 | .where("id", "=", opts.input.id) 36 | .executeTakeFirstOrThrow(); 37 | 38 | return ingestion; 39 | }), 40 | 41 | list: protectedProcedure 42 | .input( 43 | z.object({ 44 | projectId: z.string(), 45 | limit: z.number().optional(), 46 | }), 47 | ) 48 | .query(async (opts) => { 49 | let query = opts.ctx.db 50 | .selectFrom("Ingestion") 51 | .select(["id", "createdAt", "hash"]) 52 | .where("projectId", "=", opts.input.projectId); 53 | 54 | if (opts.input.limit) { 55 | query = query.limit(opts.input.limit).orderBy("createdAt", "desc"); 56 | } 57 | const ingestions = await query.execute(); 58 | 59 | return ingestions.map((ingestion) => ({ 60 | ...ingestion, 61 | adds: Math.floor(Math.random() * 10), 62 | subs: Math.floor(Math.random() * 10), 63 | })); 64 | }), 65 | upload: protectedApiFormDataProcedure 66 | .input( 67 | zfd.formData({ 68 | hash: zfd.text(), 69 | parent: zfd.text().optional(), 70 | origin: zfd.text(), 71 | schema: myFileValidator, 72 | }), 73 | ) 74 | .mutation(async (opts) => { 75 | const fileContent = await opts.input.schema.text(); 76 | 77 | const id = "ingest_" + genId(); 78 | await opts.ctx.db 79 | .insertInto("Ingestion") 80 | .values({ 81 | id, 82 | projectId: opts.ctx.apiKey.projectId, 83 | hash: opts.input.hash, 84 | parent: opts.input.parent, 85 | origin: opts.input.origin, 86 | schema: fileContent, 87 | apiKeyId: opts.ctx.apiKey.id, 88 | }) 89 | .executeTakeFirst(); 90 | 91 | return { status: "ok" }; 92 | }), 93 | }); 94 | -------------------------------------------------------------------------------- /packages/api/src/router/organizations.ts: -------------------------------------------------------------------------------- 1 | import { clerkClient } from "@clerk/nextjs"; 2 | import { TRPCError } from "@trpc/server"; 3 | import * as z from "zod"; 4 | 5 | import { 6 | createTRPCRouter, 7 | protectedAdminProcedure, 8 | protectedOrgProcedure, 9 | } from "../trpc"; 10 | import { inviteOrgMemberSchema } from "../validators"; 11 | 12 | export const organizationsRouter = createTRPCRouter({ 13 | listMembers: protectedOrgProcedure.query(async (opts) => { 14 | const { orgId } = opts.ctx.auth; 15 | 16 | const members = 17 | await clerkClient.organizations.getOrganizationMembershipList({ 18 | organizationId: orgId, 19 | }); 20 | 21 | return members.map((member) => ({ 22 | id: member.id, 23 | email: member.publicUserData?.identifier ?? "", 24 | role: member.role, 25 | joinedAt: member.createdAt, 26 | avatarUrl: member.publicUserData?.imageUrl, 27 | name: [ 28 | member.publicUserData?.firstName, 29 | member.publicUserData?.lastName, 30 | ].join(" "), 31 | })); 32 | }), 33 | 34 | deleteMember: protectedAdminProcedure 35 | .input(z.object({ userId: z.string() })) 36 | .mutation(async (opts) => { 37 | const { orgId } = opts.ctx.auth; 38 | 39 | try { 40 | const member = 41 | await clerkClient.organizations.deleteOrganizationMembership({ 42 | organizationId: orgId, 43 | userId: opts.input.userId, 44 | }); 45 | 46 | return { memberName: member.publicUserData?.firstName }; 47 | } catch (e) { 48 | console.log("Error deleting member", e); 49 | throw new TRPCError({ 50 | code: "NOT_FOUND", 51 | message: "User not found", 52 | }); 53 | } 54 | }), 55 | 56 | inviteMember: protectedAdminProcedure 57 | .input(inviteOrgMemberSchema) 58 | .mutation(async (opts) => { 59 | const { orgId } = opts.ctx.auth; 60 | 61 | const { email } = opts.input; 62 | const users = await clerkClient.users.getUserList({ 63 | emailAddress: [email], 64 | }); 65 | const user = users[0]; 66 | 67 | if (users.length === 0 || !user) { 68 | throw new TRPCError({ 69 | code: "NOT_FOUND", 70 | message: "User not found", 71 | }); 72 | } 73 | 74 | if (users.length > 1) { 75 | throw new TRPCError({ 76 | code: "BAD_REQUEST", 77 | message: "Multiple users found with that email address", 78 | }); 79 | } 80 | 81 | const member = 82 | await clerkClient.organizations.createOrganizationMembership({ 83 | organizationId: orgId, 84 | userId: user.id, 85 | role: opts.input.role, 86 | }); 87 | 88 | const { firstName, lastName } = member.publicUserData ?? {}; 89 | return { name: [firstName, lastName].join(" ") }; 90 | }), 91 | 92 | deleteOrganization: protectedAdminProcedure.mutation(async (opts) => { 93 | const { orgId } = opts.ctx.auth; 94 | 95 | await clerkClient.organizations.deleteOrganization(orgId); 96 | }), 97 | }); 98 | -------------------------------------------------------------------------------- /packages/api/src/transformer.ts: -------------------------------------------------------------------------------- 1 | import { dinero } from "dinero.js"; 2 | import type { Dinero, DineroSnapshot } from "dinero.js"; 3 | import superjson from "superjson"; 4 | import type { JSONValue } from "superjson/dist/types"; 5 | 6 | /** 7 | * TODO: Maybe put this in a shared package that can be safely shared between `api`, `nextjs` and `expo` packages 8 | */ 9 | superjson.registerCustom( 10 | { 11 | isApplicable: (val): val is Dinero => { 12 | try { 13 | // if this doesn't crash we're kinda sure it's a Dinero instance 14 | (val as Dinero).calculator.add(1, 2); 15 | return true; 16 | } catch { 17 | return false; 18 | } 19 | }, 20 | serialize: (val) => { 21 | return val.toJSON() as JSONValue; 22 | }, 23 | deserialize: (val) => { 24 | return dinero(val as DineroSnapshot); 25 | }, 26 | }, 27 | "Dinero", 28 | ); 29 | 30 | export const transformer = superjson; 31 | -------------------------------------------------------------------------------- /packages/api/src/validators.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | 3 | import { PLANS } from "@acme/stripe/plans"; 4 | 5 | /** 6 | * Shared validators used in both the frontend and backend 7 | */ 8 | 9 | export const createProjectSchema = z.object({ 10 | name: z.string().min(5, "Name must be at least 5 characters"), 11 | url: z.string().url("Must be a valid URL").optional(), 12 | }); 13 | export type CreateProject = z.infer; 14 | 15 | export const renameProjectSchema = z.object({ 16 | projectId: z.string(), 17 | name: z.string().min(5, "Name must be at least 5 characters"), 18 | }); 19 | export type RenameProject = z.infer; 20 | 21 | export const purchaseOrgSchema = z.object({ 22 | orgName: z.string().min(5, "Name must be at least 5 characters"), 23 | planId: z.string().refine( 24 | (str) => 25 | Object.values(PLANS) 26 | .map((p) => p.priceId) 27 | .includes(str), 28 | "Invalid planId", 29 | ), 30 | }); 31 | export type PurchaseOrg = z.infer; 32 | 33 | export const createApiKeySchema = z.object({ 34 | projectId: z.string(), 35 | name: z.string(), 36 | expiresAt: z.date().optional(), 37 | }); 38 | export type CreateApiKey = z.infer; 39 | 40 | export const MEMBERSHIP = { 41 | Member: "basic_member", 42 | Admin: "admin", 43 | } as const; 44 | 45 | export const inviteOrgMemberSchema = z.object({ 46 | email: z.string().email(), 47 | role: z.nativeEnum(MEMBERSHIP), 48 | }); 49 | export type InviteOrgMember = z.infer; 50 | 51 | export const transferToOrgSchema = z.object({ 52 | projectId: z.string(), 53 | orgId: z.string(), 54 | }); 55 | export type TransferToOrg = z.infer; 56 | -------------------------------------------------------------------------------- /packages/api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@acme/tsconfig/base.json", 3 | "compilerOptions": { 4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json", 5 | }, 6 | "include": ["src"], 7 | "exclude": ["node_modules"], 8 | } 9 | -------------------------------------------------------------------------------- /packages/db/index.ts: -------------------------------------------------------------------------------- 1 | // Generated by prisma/post-generate.ts 2 | 3 | import { Kysely } from "kysely"; 4 | import { PlanetScaleDialect } from "kysely-planetscale"; 5 | import { customAlphabet } from "nanoid"; 6 | 7 | import type { DB } from "./prisma/types"; 8 | 9 | export { jsonArrayFrom, jsonObjectFrom } from "kysely/helpers/postgres"; 10 | 11 | export * from "./prisma/types"; 12 | export * from "./prisma/enums"; 13 | 14 | export const db = new Kysely({ 15 | dialect: new PlanetScaleDialect({ 16 | url: process.env.DATABASE_URL, 17 | }), 18 | }); 19 | 20 | // Use custom alphabet without special chars for less chaotic, copy-able URLs 21 | // Will not collide for a long long time: https://zelark.github.io/nano-id-cc/ 22 | export const genId = customAlphabet("0123456789abcdefghijklmnopqrstuvwxyz", 16); 23 | -------------------------------------------------------------------------------- /packages/db/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@acme/db", 3 | "private": true, 4 | "version": "0.1.0", 5 | "exports": { 6 | ".": "./index.ts" 7 | }, 8 | "license": "MIT", 9 | "scripts": { 10 | "clean": "rm -rf .turbo node_modules", 11 | "db:generate": "pnpm with-env prisma generate", 12 | "db:push": "pnpm with-env prisma db push --skip-generate", 13 | "studio": "pnpm with-env prisma studio --port 5556", 14 | "format": "prisma format && prettier --check \"**/*.{mjs,ts,json}\"", 15 | "lint": "eslint .", 16 | "typecheck": "tsc --noEmit", 17 | "with-env": "dotenv -e ../../.env.local --" 18 | }, 19 | "dependencies": { 20 | "@planetscale/database": "^1.14.0", 21 | "kysely": "^0.27.2", 22 | "kysely-planetscale": "^1.4.0", 23 | "nanoid": "^5.0.4" 24 | }, 25 | "devDependencies": { 26 | "@acme/eslint-config": "^0.2.0", 27 | "@acme/prettier-config": "^0.1.0", 28 | "@acme/tsconfig": "^0.1.0", 29 | "dotenv-cli": "^7.3.0", 30 | "eslint": "^8.56.0", 31 | "prettier": "^3.2.4", 32 | "prisma": "^5.8.1", 33 | "prisma-kysely": "^1.7.1", 34 | "typescript": "^5.3.3" 35 | }, 36 | "eslintConfig": { 37 | "extends": [ 38 | "@acme/eslint-config/base" 39 | ], 40 | "rules": { 41 | "@typescript-eslint/consistent-type-definitions": "off" 42 | } 43 | }, 44 | "prettier": "@acme/prettier-config" 45 | } 46 | -------------------------------------------------------------------------------- /packages/db/prisma/enums.ts: -------------------------------------------------------------------------------- 1 | export const ProjectTier = { 2 | FREE: "FREE", 3 | PRO: "PRO", 4 | } as const; 5 | export type ProjectTier = (typeof ProjectTier)[keyof typeof ProjectTier]; 6 | export const SubscriptionPlan = { 7 | FREE: "FREE", 8 | STANDARD: "STANDARD", 9 | PRO: "PRO", 10 | } as const; 11 | export type SubscriptionPlan = 12 | (typeof SubscriptionPlan)[keyof typeof SubscriptionPlan]; 13 | -------------------------------------------------------------------------------- /packages/db/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator kysely { 2 | provider = "prisma-kysely" 3 | output = "." 4 | enumFileName = "enums.ts" 5 | } 6 | 7 | datasource db { 8 | provider = "mysql" 9 | url = env("DATABASE_URL") 10 | relationMode = "prisma" 11 | } 12 | 13 | enum ProjectTier { 14 | FREE 15 | PRO 16 | } 17 | 18 | model Project { 19 | id String @id @db.VarChar(30) // prefix_ + nanoid (16) 20 | createdAt DateTime @default(now()) 21 | 22 | // A project is tied to a Clerk User or Organization 23 | organizationId String? @db.VarChar(36) // uuid v4 24 | userId String? @db.VarChar(36) // uuid v4 25 | 26 | name String 27 | tier ProjectTier @default(FREE) 28 | url String? 29 | 30 | @@index([organizationId]) 31 | @@index([userId]) 32 | } 33 | 34 | enum SubscriptionPlan { 35 | FREE 36 | STANDARD 37 | PRO 38 | } 39 | 40 | model Customer { 41 | id String @id @db.VarChar(30) // prefix_ + nanoid (16) 42 | stripeId String @unique 43 | subscriptionId String? 44 | clerkUserId String 45 | clerkOrganizationId String? 46 | name String? 47 | plan SubscriptionPlan? 48 | paidUntil DateTime? 49 | endsAt DateTime? 50 | 51 | @@index([clerkUserId]) 52 | } 53 | 54 | model ApiKey { 55 | id String @id @db.VarChar(30) // prefix_ + nanoid (16) 56 | createdAt DateTime @default(now()) 57 | expiresAt DateTime? 58 | lastUsed DateTime? 59 | revokedAt DateTime? 60 | 61 | projectId String @db.VarChar(30) // prefix_ + nanoid (16) 62 | clerkUserId String @db.VarChar(36) // uuid v4 63 | 64 | name String @default("Secret Key") 65 | key String @unique 66 | 67 | @@index([projectId]) 68 | } 69 | 70 | model Ingestion { 71 | id String @id @db.VarChar(30) // prefix_ + nanoid (16) 72 | createdAt DateTime @default(now()) 73 | 74 | projectId String @db.VarChar(30) // prefix_ + nanoid (16) 75 | apiKeyId String @db.VarChar(30) // prefix_ + nanoid (16) 76 | 77 | schema Json 78 | hash String @db.VarChar(40) // sha1 79 | parent String? @db.VarChar(40) // sha1 80 | origin String @db.VarChar(100) 81 | 82 | @@index([projectId]) 83 | } 84 | -------------------------------------------------------------------------------- /packages/db/prisma/types.ts: -------------------------------------------------------------------------------- 1 | import type { ColumnType } from "kysely"; 2 | 3 | import type { ProjectTier, SubscriptionPlan } from "./enums"; 4 | 5 | export type Generated = 6 | T extends ColumnType 7 | ? ColumnType 8 | : ColumnType; 9 | export type Timestamp = ColumnType; 10 | 11 | export type ApiKey = { 12 | id: string; 13 | createdAt: Generated; 14 | expiresAt: Timestamp | null; 15 | lastUsed: Timestamp | null; 16 | revokedAt: Timestamp | null; 17 | projectId: string; 18 | clerkUserId: string; 19 | name: Generated; 20 | key: string; 21 | }; 22 | export type Customer = { 23 | id: string; 24 | stripeId: string; 25 | subscriptionId: string | null; 26 | clerkUserId: string; 27 | clerkOrganizationId: string | null; 28 | name: string | null; 29 | plan: SubscriptionPlan | null; 30 | paidUntil: Timestamp | null; 31 | endsAt: Timestamp | null; 32 | }; 33 | export type Ingestion = { 34 | id: string; 35 | createdAt: Generated; 36 | projectId: string; 37 | apiKeyId: string; 38 | schema: unknown; 39 | hash: string; 40 | parent: string | null; 41 | origin: string; 42 | }; 43 | export type Project = { 44 | id: string; 45 | createdAt: Generated; 46 | organizationId: string | null; 47 | userId: string | null; 48 | name: string; 49 | tier: Generated; 50 | url: string | null; 51 | }; 52 | export type DB = { 53 | ApiKey: ApiKey; 54 | Customer: Customer; 55 | Ingestion: Ingestion; 56 | Project: Project; 57 | }; 58 | -------------------------------------------------------------------------------- /packages/db/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@acme/tsconfig/base.json", 3 | "compilerOptions": { 4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json", 5 | }, 6 | "include": ["*.ts", "prisma", "src"], 7 | "exclude": ["node_modules"], 8 | } 9 | -------------------------------------------------------------------------------- /packages/stripe/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@acme/stripe", 3 | "private": true, 4 | "version": "0.1.0", 5 | "exports": { 6 | ".": "./src/index.ts", 7 | "./plans": "./src/plans.ts", 8 | "./env": "./src/env.mjs" 9 | }, 10 | "typesVersions": { 11 | "*": { 12 | "*": [ 13 | "src/*" 14 | ] 15 | } 16 | }, 17 | "license": "MIT", 18 | "scripts": { 19 | "clean": "rm -rf .turbo node_modules", 20 | "dev": "stripe listen --forward-to localhost:3000/api/webhooks/stripe", 21 | "lint": "eslint .", 22 | "format": "prettier --check \"**/*.{mjs,ts,json}\"", 23 | "typecheck": "tsc --noEmit" 24 | }, 25 | "dependencies": { 26 | "@acme/db": "^0.1.0", 27 | "@clerk/nextjs": "^4.29.4", 28 | "@t3-oss/env-nextjs": "^0.7.3", 29 | "stripe": "^14.13.0" 30 | }, 31 | "devDependencies": { 32 | "@acme/eslint-config": "^0.2.0", 33 | "@acme/prettier-config": "^0.1.0", 34 | "@acme/tsconfig": "^0.1.0", 35 | "eslint": "^8.56.0", 36 | "prettier": "^3.2.4", 37 | "typescript": "^5.3.3" 38 | }, 39 | "eslintConfig": { 40 | "extends": [ 41 | "@acme/eslint-config/base" 42 | ] 43 | }, 44 | "prettier": "@acme/prettier-config" 45 | } 46 | -------------------------------------------------------------------------------- /packages/stripe/src/env.mjs: -------------------------------------------------------------------------------- 1 | import { createEnv } from "@t3-oss/env-nextjs"; 2 | import * as z from "zod"; 3 | 4 | export const env = createEnv({ 5 | shared: {}, 6 | server: { 7 | NEXTJS_URL: z.preprocess( 8 | (str) => 9 | process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : str, 10 | process.env.VERCEL_URL ? z.string().min(1) : z.string().url(), 11 | ), 12 | 13 | STRIPE_API_KEY: z.string(), 14 | }, 15 | client: { 16 | NEXT_PUBLIC_STRIPE_STD_PRODUCT_ID: z.string(), 17 | NEXT_PUBLIC_STRIPE_STD_MONTHLY_PRICE_ID: z.string(), 18 | NEXT_PUBLIC_STRIPE_PRO_PRODUCT_ID: z.string(), 19 | NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PRICE_ID: z.string(), 20 | }, 21 | // Client side variables gets destructured here due to Next.js static analysis 22 | // Shared ones are also included here for good measure since the behavior has been inconsistent 23 | experimental__runtimeEnv: { 24 | NEXT_PUBLIC_STRIPE_STD_PRODUCT_ID: 25 | process.env.NEXT_PUBLIC_STRIPE_STD_PRODUCT_ID, 26 | NEXT_PUBLIC_STRIPE_STD_MONTHLY_PRICE_ID: 27 | process.env.NEXT_PUBLIC_STRIPE_STD_MONTHLY_PRICE_ID, 28 | NEXT_PUBLIC_STRIPE_PRO_PRODUCT_ID: 29 | process.env.NEXT_PUBLIC_STRIPE_PRO_PRODUCT_ID, 30 | NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PRICE_ID: 31 | process.env.NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PRICE_ID, 32 | }, 33 | skipValidation: 34 | !!process.env.SKIP_ENV_VALIDATION || 35 | process.env.npm_lifecycle_event === "lint", 36 | }); 37 | -------------------------------------------------------------------------------- /packages/stripe/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Stripe } from "stripe"; 2 | 3 | import { env } from "./env.mjs"; 4 | 5 | export * from "./plans"; 6 | export * from "./webhooks"; 7 | 8 | export type { Stripe }; 9 | 10 | export const stripe = new Stripe(env.STRIPE_API_KEY, { 11 | apiVersion: "2023-10-16", 12 | typescript: true, 13 | }); 14 | -------------------------------------------------------------------------------- /packages/stripe/src/plans.ts: -------------------------------------------------------------------------------- 1 | import { SubscriptionPlan } from "@acme/db"; 2 | 3 | import { env } from "./env.mjs"; 4 | 5 | interface PlanInfo { 6 | key: SubscriptionPlan; 7 | name: string; 8 | description: string; 9 | preFeatures?: string; 10 | features: string[]; 11 | priceId: string; 12 | } 13 | 14 | export const PLANS: Record = { 15 | STANDARD: { 16 | key: SubscriptionPlan.STANDARD, 17 | name: "Standard", 18 | description: "For individuals", 19 | features: ["Invite up to 1 team member", "Lorem ipsum dolor sit amet"], 20 | priceId: env.NEXT_PUBLIC_STRIPE_STD_MONTHLY_PRICE_ID, 21 | }, 22 | PRO: { 23 | key: SubscriptionPlan.PRO, 24 | name: "Pro", 25 | description: "For teams", 26 | preFeatures: "Everything in standard, plus", 27 | features: ["Invite up to 5 team members", "Unlimited projects"], 28 | priceId: env.NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PRICE_ID, 29 | }, 30 | FREE: { 31 | key: SubscriptionPlan.FREE, 32 | name: "Free", 33 | description: "For individuals", 34 | features: ["Invite up to 1 team member", "Lorem ipsum dolor sit amet"], 35 | priceId: "no-id-necessary", 36 | }, 37 | }; 38 | 39 | export function stripePriceToSubscriptionPlan(priceId: string | undefined) { 40 | return Object.values(PLANS).find((plan) => plan.priceId === priceId); 41 | } 42 | -------------------------------------------------------------------------------- /packages/stripe/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@acme/tsconfig/base.json", 3 | "compilerOptions": { 4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json", 5 | }, 6 | "include": ["*.ts", "src"], 7 | "exclude": ["node_modules"], 8 | } 9 | -------------------------------------------------------------------------------- /packages/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@acme/ui", 3 | "private": true, 4 | "version": "0.1.0", 5 | "scripts": { 6 | "clean": "git clean -xdf .turbo node_modules", 7 | "format": "prettier --check \"**/*.{ts,tsx}\"", 8 | "lint": "eslint .", 9 | "typecheck": "tsc --noEmit" 10 | }, 11 | "dependencies": { 12 | "@radix-ui/react-avatar": "^1.0.4", 13 | "@radix-ui/react-checkbox": "^1.0.4", 14 | "@radix-ui/react-dialog": "^1.0.5", 15 | "@radix-ui/react-dropdown-menu": "^2.0.6", 16 | "@radix-ui/react-label": "^2.0.2", 17 | "@radix-ui/react-popover": "^1.0.7", 18 | "@radix-ui/react-scroll-area": "^1.0.5", 19 | "@radix-ui/react-select": "^2.0.0", 20 | "@radix-ui/react-slot": "^1.0.2", 21 | "@radix-ui/react-tabs": "^1.0.4", 22 | "@radix-ui/react-toast": "^1.1.5", 23 | "class-variance-authority": "^0.7.0", 24 | "clsx": "^2.1.0", 25 | "cmdk": "^0.2.0", 26 | "lucide-react": "0.307.0", 27 | "tailwind-merge": "^2.2.0", 28 | "zod": "^3.22.4" 29 | }, 30 | "peerDependencies": { 31 | "@tanstack/react-table": "^8.10.7", 32 | "react": "^18.2.0", 33 | "react-day-picker": "^8.10.0", 34 | "react-dom": "^18.2.0", 35 | "react-hook-form": "^7.45.4", 36 | "tailwindcss": "3.4.1", 37 | "tailwindcss-animate": "^1.0.7" 38 | }, 39 | "devDependencies": { 40 | "@acme/eslint-config": "0.2.0", 41 | "@acme/prettier-config": "0.1.0", 42 | "@acme/tailwind-config": "0.1.0", 43 | "@acme/tsconfig": "0.1.0", 44 | "@tanstack/react-table": "^8.11.3", 45 | "@types/react": "^18.2.48", 46 | "@types/react-dom": "^18.2.18", 47 | "date-fns": "^3.2.0", 48 | "eslint": "^8.56.0", 49 | "prettier": "^3.2.4", 50 | "react": "18.2.0", 51 | "react-day-picker": "^8.10.0", 52 | "react-dom": "18.2.0", 53 | "react-hook-form": "^7.49.2", 54 | "tailwindcss": "3.4.1", 55 | "tailwindcss-animate": "^1.0.7", 56 | "typescript": "^5.3.3" 57 | }, 58 | "eslintConfig": { 59 | "root": true, 60 | "extends": [ 61 | "@acme/eslint-config/base", 62 | "@acme/eslint-config/react" 63 | ] 64 | }, 65 | "prettier": "@acme/prettier-config", 66 | "exports": { 67 | ".": "./src/index.ts", 68 | "./avatar": "./src/avatar.tsx", 69 | "./button": "./src/button.tsx", 70 | "./calendar": "./src/calendar.tsx", 71 | "./card": "./src/card.tsx", 72 | "./checkbox": "./src/checkbox.tsx", 73 | "./command": "./src/command.tsx", 74 | "./data-table": "./src/data-table.tsx", 75 | "./dialog": "./src/dialog.tsx", 76 | "./dropdown-menu": "./src/dropdown-menu.tsx", 77 | "./form": "./src/form.tsx", 78 | "./icons": "./src/icons.tsx", 79 | "./input": "./src/input.tsx", 80 | "./label": "./src/label.tsx", 81 | "./popover": "./src/popover.tsx", 82 | "./scroll-area": "./src/scroll-area.tsx", 83 | "./select": "./src/select.tsx", 84 | "./sheet": "./src/sheet.tsx", 85 | "./table": "./src/table.tsx", 86 | "./tabs": "./src/tabs.tsx", 87 | "./toaster": "./src/toaster.tsx", 88 | "./use-toast": "./src/use-toast.tsx" 89 | }, 90 | "typesVersions": { 91 | "*": { 92 | "*": [ 93 | "src/*" 94 | ] 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /packages/ui/src/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"; 5 | 6 | import { cn } from "./utils/cn"; 7 | 8 | const Avatar = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )); 21 | Avatar.displayName = AvatarPrimitive.Root.displayName; 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )); 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName; 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )); 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; 49 | 50 | export { Avatar, AvatarImage, AvatarFallback }; 51 | -------------------------------------------------------------------------------- /packages/ui/src/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva } from "class-variance-authority"; 4 | import type { VariantProps } from "class-variance-authority"; 5 | 6 | import { cn } from "./utils/cn"; 7 | 8 | const buttonVariants = cva( 9 | "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 10 | { 11 | variants: { 12 | variant: { 13 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-10 px-4 py-2", 25 | sm: "h-9 rounded-md px-3", 26 | lg: "h-11 rounded-md px-8", 27 | icon: "h-10 w-10", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | }, 35 | ); 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean; 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button"; 46 | return ( 47 | 52 | ); 53 | }, 54 | ); 55 | Button.displayName = "Button"; 56 | 57 | export { Button, buttonVariants }; 58 | -------------------------------------------------------------------------------- /packages/ui/src/calendar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { ChevronLeft, ChevronRight } from "lucide-react"; 5 | import { DayPicker } from "react-day-picker"; 6 | 7 | import { buttonVariants } from "./button"; 8 | import { cn } from "./utils/cn"; 9 | 10 | export type { DateRange } from "react-day-picker"; 11 | export type CalendarProps = React.ComponentProps; 12 | 13 | function Calendar({ 14 | className, 15 | classNames, 16 | showOutsideDays = true, 17 | ...props 18 | }: CalendarProps) { 19 | return ( 20 | ( 57 | 58 | ), 59 | IconRight: ({ ...props }) => ( 60 | 61 | ), 62 | }} 63 | {...props} 64 | /> 65 | ); 66 | } 67 | Calendar.displayName = "Calendar"; 68 | 69 | export { Calendar }; 70 | -------------------------------------------------------------------------------- /packages/ui/src/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "./utils/cn"; 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
    17 | )); 18 | Card.displayName = "Card"; 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
    29 | )); 30 | CardHeader.displayName = "CardHeader"; 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

    44 | {props.children} 45 |

    46 | )); 47 | CardTitle.displayName = "CardTitle"; 48 | 49 | const CardDescription = React.forwardRef< 50 | HTMLParagraphElement, 51 | React.HTMLAttributes 52 | >(({ className, ...props }, ref) => ( 53 |

    58 | )); 59 | CardDescription.displayName = "CardDescription"; 60 | 61 | const CardContent = React.forwardRef< 62 | HTMLDivElement, 63 | React.HTMLAttributes 64 | >(({ className, ...props }, ref) => ( 65 |

    66 | )); 67 | CardContent.displayName = "CardContent"; 68 | 69 | const CardFooter = React.forwardRef< 70 | HTMLDivElement, 71 | React.HTMLAttributes 72 | >(({ className, ...props }, ref) => ( 73 |
    78 | )); 79 | CardFooter.displayName = "CardFooter"; 80 | 81 | export { 82 | Card, 83 | CardHeader, 84 | CardFooter, 85 | CardTitle, 86 | CardDescription, 87 | CardContent, 88 | }; 89 | -------------------------------------------------------------------------------- /packages/ui/src/checkbox.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; 5 | import { Check } from "lucide-react"; 6 | 7 | import { cn } from "./utils/cn"; 8 | 9 | const Checkbox = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => ( 13 | 21 | 24 | 25 | 26 | 27 | )); 28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName; 29 | 30 | export { Checkbox }; 31 | -------------------------------------------------------------------------------- /packages/ui/src/data-table.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import type { ColumnDef } from "@tanstack/react-table"; 4 | import { 5 | flexRender, 6 | getCoreRowModel, 7 | useReactTable, 8 | } from "@tanstack/react-table"; 9 | 10 | import { 11 | Table, 12 | TableBody, 13 | TableCell, 14 | TableHead, 15 | TableHeader, 16 | TableRow, 17 | } from "./table"; 18 | 19 | interface DataTableProps { 20 | columns: ColumnDef[]; 21 | data: TData[]; 22 | } 23 | 24 | export function DataTable({ 25 | columns, 26 | data, 27 | }: DataTableProps) { 28 | const table = useReactTable({ 29 | data, 30 | columns, 31 | getCoreRowModel: getCoreRowModel(), 32 | }); 33 | 34 | return ( 35 |
    36 | 37 | 38 | {table.getHeaderGroups().map((headerGroup) => ( 39 | 40 | {headerGroup.headers.map((header) => { 41 | return ( 42 | 43 | {header.isPlaceholder 44 | ? null 45 | : flexRender( 46 | header.column.columnDef.header, 47 | header.getContext(), 48 | )} 49 | 50 | ); 51 | })} 52 | 53 | ))} 54 | 55 | 56 | {table.getRowModel().rows?.length ? ( 57 | table.getRowModel().rows.map((row) => ( 58 | 62 | {row.getVisibleCells().map((cell) => ( 63 | 64 | {flexRender(cell.column.columnDef.cell, cell.getContext())} 65 | 66 | ))} 67 | 68 | )) 69 | ) : ( 70 | 71 | 72 | No results. 73 | 74 | 75 | )} 76 | 77 |
    78 |
    79 | ); 80 | } 81 | -------------------------------------------------------------------------------- /packages/ui/src/index.ts: -------------------------------------------------------------------------------- 1 | export { cn } from "./utils/cn"; 2 | -------------------------------------------------------------------------------- /packages/ui/src/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "./utils/cn"; 4 | 5 | export type InputProps = React.InputHTMLAttributes; 6 | 7 | const Input = React.forwardRef( 8 | ({ className, type, ...props }, ref) => { 9 | return ( 10 | 19 | ); 20 | }, 21 | ); 22 | Input.displayName = "Input"; 23 | 24 | export { Input }; 25 | -------------------------------------------------------------------------------- /packages/ui/src/label.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as LabelPrimitive from "@radix-ui/react-label"; 5 | import { cva } from "class-variance-authority"; 6 | import type { VariantProps } from "class-variance-authority"; 7 | 8 | import { cn } from "./utils/cn"; 9 | 10 | const labelVariants = cva( 11 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", 12 | ); 13 | 14 | const Label = React.forwardRef< 15 | React.ElementRef, 16 | React.ComponentPropsWithoutRef & 17 | VariantProps 18 | >(({ className, ...props }, ref) => ( 19 | 24 | )); 25 | Label.displayName = LabelPrimitive.Root.displayName; 26 | 27 | export { Label }; 28 | -------------------------------------------------------------------------------- /packages/ui/src/popover.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as PopoverPrimitive from "@radix-ui/react-popover"; 5 | 6 | import { cn } from "./utils/cn"; 7 | 8 | const Popover = PopoverPrimitive.Root; 9 | 10 | const PopoverTrigger = PopoverPrimitive.Trigger; 11 | 12 | const PopoverContent = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef 15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 16 | 17 | 27 | 28 | )); 29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName; 30 | 31 | export { Popover, PopoverTrigger, PopoverContent }; 32 | -------------------------------------------------------------------------------- /packages/ui/src/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"; 5 | 6 | import { cn } from "./utils/cn"; 7 | 8 | const ScrollArea = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, children, ...props }, ref) => ( 12 | 17 | 18 | {children} 19 | 20 | 21 | 22 | 23 | )); 24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; 25 | 26 | const ScrollBar = React.forwardRef< 27 | React.ElementRef, 28 | React.ComponentPropsWithoutRef 29 | >(({ className, orientation = "vertical", ...props }, ref) => ( 30 | 43 | 44 | 45 | )); 46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName; 47 | 48 | export { ScrollArea, ScrollBar }; 49 | -------------------------------------------------------------------------------- /packages/ui/src/table.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "./utils/cn"; 4 | 5 | const Table = React.forwardRef< 6 | HTMLTableElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
    10 | 15 | 16 | )); 17 | Table.displayName = "Table"; 18 | 19 | const TableHeader = React.forwardRef< 20 | HTMLTableSectionElement, 21 | React.HTMLAttributes 22 | >(({ className, ...props }, ref) => ( 23 | 24 | )); 25 | TableHeader.displayName = "TableHeader"; 26 | 27 | const TableBody = React.forwardRef< 28 | HTMLTableSectionElement, 29 | React.HTMLAttributes 30 | >(({ className, ...props }, ref) => ( 31 | 36 | )); 37 | TableBody.displayName = "TableBody"; 38 | 39 | const TableFooter = React.forwardRef< 40 | HTMLTableSectionElement, 41 | React.HTMLAttributes 42 | >(({ className, ...props }, ref) => ( 43 | 48 | )); 49 | TableFooter.displayName = "TableFooter"; 50 | 51 | const TableRow = React.forwardRef< 52 | HTMLTableRowElement, 53 | React.HTMLAttributes & { disabled?: boolean } 54 | >(({ className, disabled, ...props }, ref) => ( 55 | 64 | )); 65 | TableRow.displayName = "TableRow"; 66 | 67 | const TableHead = React.forwardRef< 68 | HTMLTableCellElement, 69 | React.ThHTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
    79 | )); 80 | TableHead.displayName = "TableHead"; 81 | 82 | const TableCell = React.forwardRef< 83 | HTMLTableCellElement, 84 | React.TdHTMLAttributes 85 | >(({ className, ...props }, ref) => ( 86 | 91 | )); 92 | TableCell.displayName = "TableCell"; 93 | 94 | const TableCaption = React.forwardRef< 95 | HTMLTableCaptionElement, 96 | React.HTMLAttributes 97 | >(({ className, ...props }, ref) => ( 98 |
    103 | )); 104 | TableCaption.displayName = "TableCaption"; 105 | 106 | export { 107 | Table, 108 | TableHeader, 109 | TableBody, 110 | TableFooter, 111 | TableHead, 112 | TableRow, 113 | TableCell, 114 | TableCaption, 115 | }; 116 | -------------------------------------------------------------------------------- /packages/ui/src/tabs.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as TabsPrimitive from "@radix-ui/react-tabs"; 5 | 6 | import { cn } from "./utils/cn"; 7 | 8 | const Tabs = TabsPrimitive.Root; 9 | 10 | const TabsList = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, ...props }, ref) => ( 14 | 22 | )); 23 | TabsList.displayName = TabsPrimitive.List.displayName; 24 | 25 | const TabsTrigger = React.forwardRef< 26 | React.ElementRef, 27 | React.ComponentPropsWithoutRef 28 | >(({ className, ...props }, ref) => ( 29 | 37 | )); 38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; 39 | 40 | const TabsContent = React.forwardRef< 41 | React.ElementRef, 42 | React.ComponentPropsWithoutRef 43 | >(({ className, ...props }, ref) => ( 44 | 52 | )); 53 | TabsContent.displayName = TabsPrimitive.Content.displayName; 54 | 55 | export { Tabs, TabsList, TabsTrigger, TabsContent }; 56 | -------------------------------------------------------------------------------- /packages/ui/src/toaster.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Toast, 5 | ToastClose, 6 | ToastDescription, 7 | ToastProvider, 8 | ToastTitle, 9 | ToastViewport, 10 | } from "./toast"; 11 | import { useToast } from "./use-toast"; 12 | 13 | export function Toaster() { 14 | const { toasts } = useToast(); 15 | 16 | return ( 17 | 18 | {toasts.map(function ({ id, title, description, action, ...props }) { 19 | return ( 20 | 21 |
    22 | {title && {title}} 23 | {description && ( 24 | {description} 25 | )} 26 |
    27 | {action} 28 | 29 |
    30 | ); 31 | })} 32 | 33 |
    34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /packages/ui/src/utils/cn.ts: -------------------------------------------------------------------------------- 1 | import { clsx } from "clsx"; 2 | import type { ClassValue } from "clsx"; 3 | import { twMerge } from "tailwind-merge"; 4 | 5 | export function cn(...inputs: ClassValue[]) { 6 | return twMerge(clsx(inputs)); 7 | } 8 | -------------------------------------------------------------------------------- /packages/ui/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is not used for any compilation purpose, it is only used 3 | * for Tailwind Intellisense & Autocompletion in the source files 4 | */ 5 | import type { Config } from "tailwindcss"; 6 | 7 | import baseConfig from "@acme/tailwind-config"; 8 | 9 | export default { 10 | content: baseConfig.content, 11 | presets: [baseConfig], 12 | } satisfies Config; 13 | -------------------------------------------------------------------------------- /packages/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@acme/tsconfig/base.json", 3 | "compilerOptions": { 4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" 5 | }, 6 | "include": ["*.ts", "src"], 7 | "exclude": ["node_modules"] 8 | } 9 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - apps/* 3 | - packages/* 4 | - tooling/* 5 | -------------------------------------------------------------------------------- /tooling/eslint/base.js: -------------------------------------------------------------------------------- 1 | /** @type {import("eslint").Linter.Config} */ 2 | const config = { 3 | extends: [ 4 | "turbo", 5 | "eslint:recommended", 6 | "plugin:@typescript-eslint/recommended-type-checked", 7 | "plugin:@typescript-eslint/stylistic-type-checked", 8 | "prettier", 9 | ], 10 | env: { 11 | es2022: true, 12 | node: true, 13 | }, 14 | parser: "@typescript-eslint/parser", 15 | parserOptions: { 16 | project: true, 17 | }, 18 | plugins: ["@typescript-eslint", "import"], 19 | rules: { 20 | "turbo/no-undeclared-env-vars": "off", 21 | "@typescript-eslint/no-unused-vars": [ 22 | "error", 23 | { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }, 24 | ], 25 | "@typescript-eslint/consistent-type-imports": [ 26 | "warn", 27 | { prefer: "type-imports", fixStyle: "separate-type-imports" }, 28 | ], 29 | "@typescript-eslint/no-misused-promises": [ 30 | 2, 31 | { checksVoidReturn: { attributes: false } }, 32 | ], 33 | "import/consistent-type-specifier-style": ["error", "prefer-top-level"], 34 | }, 35 | ignorePatterns: [ 36 | "**/.eslintrc.cjs", 37 | "**/*.config.js", 38 | "**/*.config.cjs", 39 | ".next", 40 | "dist", 41 | "pnpm-lock.yaml", 42 | ], 43 | reportUnusedDisableDirectives: true, 44 | }; 45 | 46 | module.exports = config; 47 | -------------------------------------------------------------------------------- /tooling/eslint/nextjs.js: -------------------------------------------------------------------------------- 1 | /** @type {import('eslint').Linter.Config} */ 2 | const config = { 3 | extends: ["plugin:@next/next/recommended"], 4 | rules: { 5 | "@next/next/no-html-link-for-pages": "off", 6 | }, 7 | }; 8 | 9 | module.exports = config; 10 | -------------------------------------------------------------------------------- /tooling/eslint/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@acme/eslint-config", 3 | "version": "0.2.0", 4 | "private": true, 5 | "license": "MIT", 6 | "files": [ 7 | "./base.js", 8 | "./nextjs.js", 9 | "./react.js" 10 | ], 11 | "scripts": { 12 | "clean": "rm -rf .turbo node_modules", 13 | "lint": "eslint .", 14 | "format": "prettier --check \"**/*.{js,json}\"", 15 | "typecheck": "tsc --noEmit" 16 | }, 17 | "dependencies": { 18 | "@next/eslint-plugin-next": "^14.1.0", 19 | "@types/eslint": "^8.56.2", 20 | "@typescript-eslint/eslint-plugin": "^6.19.0", 21 | "@typescript-eslint/parser": "^6.19.0", 22 | "eslint-config-prettier": "^9.1.0", 23 | "eslint-config-turbo": "^1.11.3", 24 | "eslint-plugin-import": "^2.29.1", 25 | "eslint-plugin-jsx-a11y": "^6.8.0", 26 | "eslint-plugin-react": "^7.33.2", 27 | "eslint-plugin-react-hooks": "^4.6.0" 28 | }, 29 | "devDependencies": { 30 | "@acme/prettier-config": "^0.1.0", 31 | "@acme/tsconfig": "^0.1.0", 32 | "eslint": "^8.56.0", 33 | "typescript": "^5.3.3" 34 | }, 35 | "eslintConfig": { 36 | "root": true, 37 | "extends": [ 38 | "./base.js" 39 | ] 40 | }, 41 | "prettier": "@acme/prettier-config" 42 | } 43 | -------------------------------------------------------------------------------- /tooling/eslint/react.js: -------------------------------------------------------------------------------- 1 | /** @type {import('eslint').Linter.Config} */ 2 | const config = { 3 | extends: [ 4 | "plugin:react/recommended", 5 | "plugin:react-hooks/recommended", 6 | "plugin:jsx-a11y/recommended", 7 | ], 8 | rules: { 9 | "react/prop-types": "off", 10 | }, 11 | globals: { 12 | React: "writable", 13 | }, 14 | settings: { 15 | react: { 16 | version: "detect", 17 | }, 18 | }, 19 | env: { 20 | browser: true, 21 | }, 22 | }; 23 | 24 | module.exports = config; 25 | -------------------------------------------------------------------------------- /tooling/eslint/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@acme/tsconfig/base.json", 3 | "compilerOptions": { 4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json", 5 | }, 6 | "include": ["."], 7 | "exclude": ["node_modules"], 8 | } 9 | -------------------------------------------------------------------------------- /tooling/prettier/index.mjs: -------------------------------------------------------------------------------- 1 | /** @typedef {import("prettier").Config} PrettierConfig */ 2 | /** @typedef {import("prettier-plugin-tailwindcss").PluginOptions} TailwindConfig */ 3 | /** @typedef {import("@ianvs/prettier-plugin-sort-imports").PluginConfig} SortImportsConfig */ 4 | 5 | /** @type { PrettierConfig | SortImportsConfig | TailwindConfig } */ 6 | const config = { 7 | plugins: [ 8 | "@ianvs/prettier-plugin-sort-imports", 9 | "prettier-plugin-tailwindcss", 10 | ], 11 | tailwindConfig: "../../tooling/tailwind", 12 | importOrder: [ 13 | "^(react/(.*)$)|^(react$)|^(react-native(.*)$)", 14 | "^(next/(.*)$)|^(next$)", 15 | "^(expo(.*)$)|^(expo$)", 16 | "", 17 | "", 18 | "^@acme/(.*)$", 19 | "", 20 | "^~/", 21 | "^[../]", 22 | "^[./]", 23 | ], 24 | importOrderParserPlugins: ["typescript", "jsx", "decorators-legacy"], 25 | importOrderTypeScriptVersion: "4.4.0", 26 | }; 27 | 28 | export default config; 29 | -------------------------------------------------------------------------------- /tooling/prettier/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@acme/prettier-config", 3 | "private": true, 4 | "version": "0.1.0", 5 | "main": "index.mjs", 6 | "scripts": { 7 | "clean": "rm -rf .turbo node_modules", 8 | "format": "prettier --check \"**/*.{mjs,json}\"", 9 | "typecheck": "tsc --noEmit" 10 | }, 11 | "dependencies": { 12 | "@ianvs/prettier-plugin-sort-imports": "^4.1.1", 13 | "prettier": "^3.2.4", 14 | "prettier-plugin-tailwindcss": "^0.5.11" 15 | }, 16 | "devDependencies": { 17 | "@acme/tsconfig": "^0.1.0", 18 | "typescript": "^5.3.3" 19 | }, 20 | "prettier": "@acme/prettier-config" 21 | } 22 | -------------------------------------------------------------------------------- /tooling/prettier/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@acme/tsconfig/base.json", 3 | "compilerOptions": { 4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json", 5 | }, 6 | "include": ["."], 7 | "exclude": ["node_modules"], 8 | } 9 | -------------------------------------------------------------------------------- /tooling/tailwind/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@acme/tailwind-config", 3 | "version": "0.1.0", 4 | "main": "index.ts", 5 | "license": "MIT", 6 | "files": [ 7 | "index.ts", 8 | "postcss.js" 9 | ], 10 | "scripts": { 11 | "clean": "rm -rf .turbo node_modules", 12 | "lint": "eslint .", 13 | "format": "prettier --check \"**/*.{js,ts,json}\"", 14 | "typecheck": "tsc --noEmit" 15 | }, 16 | "dependencies": { 17 | "autoprefixer": "^10.4.17", 18 | "postcss": "8.4.33", 19 | "tailwindcss": "3.4.1" 20 | }, 21 | "devDependencies": { 22 | "@acme/eslint-config": "^0.2.0", 23 | "@acme/prettier-config": "^0.1.0", 24 | "@acme/tsconfig": "^0.1.0", 25 | "eslint": "^8.56.0", 26 | "prettier": "^3.2.4", 27 | "typescript": "^5.3.3" 28 | }, 29 | "eslintConfig": { 30 | "root": true, 31 | "extends": [ 32 | "@acme/eslint-config/base" 33 | ] 34 | }, 35 | "prettier": "@acme/prettier-config" 36 | } 37 | -------------------------------------------------------------------------------- /tooling/tailwind/postcss.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /tooling/tailwind/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@acme/tsconfig/base.json", 3 | "compilerOptions": { 4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json", 5 | }, 6 | "include": ["."], 7 | "exclude": ["node_modules"], 8 | } 9 | -------------------------------------------------------------------------------- /tooling/typescript/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "target": "ES2017", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "checkJs": true, 8 | "skipLibCheck": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noEmit": true, 12 | "esModuleInterop": true, 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "jsx": "preserve", 18 | "incremental": true, 19 | "noUncheckedIndexedAccess": true 20 | }, 21 | "exclude": ["node_modules", "build", "dist", ".next", ".expo"] 22 | } 23 | -------------------------------------------------------------------------------- /tooling/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@acme/tsconfig", 3 | "private": true, 4 | "version": "0.1.0", 5 | "files": [ 6 | "base.json" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "checkJs": true, 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve", 17 | "incremental": true, 18 | "noUncheckedIndexedAccess": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turborepo.org/schema.json", 3 | "globalDependencies": ["**/.env"], 4 | "pipeline": { 5 | "topo": { 6 | "dependsOn": ["^topo"] 7 | }, 8 | "build": { 9 | "dependsOn": ["^build", "^db:generate"], 10 | "outputs": [".next/**", "!.next/cache/**", "next-env.d.ts", ".expo/**"] 11 | }, 12 | "db:generate": { 13 | "inputs": ["prisma/schema.prisma"], 14 | "outputs": ["prisma/**"] 15 | }, 16 | "db:push": { 17 | "inputs": ["prisma/schema.prisma"], 18 | "cache": false 19 | }, 20 | "dev": { 21 | "persistent": true, 22 | "cache": false 23 | }, 24 | "format": { 25 | "outputs": ["node_modules/.cache/.prettiercache"], 26 | "outputMode": "new-only" 27 | }, 28 | "lint": { 29 | "dependsOn": ["^topo"], 30 | "outputs": ["node_modules/.cache/.eslintcache"] 31 | }, 32 | "typecheck": { 33 | "dependsOn": ["^topo"], 34 | "outputs": ["node_modules/.cache/tsbuildinfo.json"] 35 | }, 36 | "clean": { 37 | "cache": false 38 | }, 39 | "//#clean": { 40 | "cache": false 41 | } 42 | }, 43 | "globalEnv": [ 44 | "CLERK_SECRET_KEY", 45 | "DATABASE_URL", 46 | "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY", 47 | "NEXTJS_URL", 48 | "SKIP_ENV_VALIDATION", 49 | "STRIPE_API_KEY", 50 | "STRIPE_WEBHOOK_SECRET", 51 | "NEXT_PUBLIC_STRIPE_STD_PRODUCT_ID", 52 | "NEXT_PUBLIC_STRIPE_STD_MONTHLY_PRICE_ID", 53 | "NEXT_PUBLIC_STRIPE_PRO_PRODUCT_ID", 54 | "NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PRICE_ID" 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /turbo/generators/config.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from "node:child_process"; 2 | import type { PackageJson, PlopTypes } from "@turbo/gen"; 3 | 4 | export default function generator(plop: PlopTypes.NodePlopAPI): void { 5 | plop.setGenerator("init", { 6 | description: "Generate a new package for the Acme Monorepo", 7 | prompts: [ 8 | { 9 | type: "input", 10 | name: "name", 11 | message: 12 | "What is the name of the package? (You can skip the `@acme/` prefix)", 13 | }, 14 | { 15 | type: "input", 16 | name: "deps", 17 | message: 18 | "Enter a space separated list of dependencies you would like to install", 19 | }, 20 | ], 21 | actions: [ 22 | (answers) => { 23 | if ("name" in answers && typeof answers.name === "string") { 24 | if (answers.name.startsWith("@acme/")) { 25 | answers.name = answers.name.replace("@acme/", ""); 26 | } 27 | } 28 | return "Config sanitized"; 29 | }, 30 | { 31 | type: "add", 32 | path: "packages/{{ name }}/package.json", 33 | templateFile: "templates/package.json.hbs", 34 | }, 35 | { 36 | type: "add", 37 | path: "packages/{{ name }}/tsconfig.json", 38 | templateFile: "templates/tsconfig.json.hbs", 39 | }, 40 | { 41 | type: "add", 42 | path: "packages/{{ name }}/index.ts", 43 | template: "export * from './src';", 44 | }, 45 | { 46 | type: "add", 47 | path: "packages/{{ name }}/src/index.ts", 48 | template: "export const name = '{{ name }}';", 49 | }, 50 | { 51 | type: "modify", 52 | path: "packages/{{ name }}/package.json", 53 | async transform(content, answers) { 54 | const pkg = JSON.parse(content) as PackageJson; 55 | for (const dep of answers.deps.split(" ").filter(Boolean)) { 56 | const version = await fetch( 57 | `https://registry.npmjs.org/-/package/${dep}/dist-tags`, 58 | ) 59 | .then((res) => res.json()) 60 | .then((json) => json.latest); 61 | pkg.dependencies![dep] = `^${version}`; 62 | } 63 | return JSON.stringify(pkg, null, 2); 64 | }, 65 | }, 66 | async (answers) => { 67 | /** 68 | * Install deps and format everything 69 | */ 70 | execSync("pnpm manypkg fix", { 71 | stdio: "inherit", 72 | }); 73 | execSync( 74 | `pnpm prettier --write packages/${ 75 | (answers as { name: string }).name 76 | }/** --list-different`, 77 | ); 78 | return "Package scaffolded"; 79 | }, 80 | ], 81 | }); 82 | } 83 | -------------------------------------------------------------------------------- /turbo/generators/templates/package.json.hbs: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@acme/{{ name }}", 3 | "private": true, 4 | "version": "0.1.0", 5 | "exports": { 6 | ".": "./index.ts" 7 | }, 8 | "typesVersions": { 9 | "*": { 10 | "*": [ 11 | "src/*" 12 | ] 13 | } 14 | } 15 | "license": "MIT", 16 | "scripts": { 17 | "clean": "rm -rf .turbo node_modules", 18 | "lint": "eslint .", 19 | "format": "prettier --check \"**/*.{mjs,ts,md,json}\"", 20 | "typecheck": "tsc --noEmit" 21 | }, 22 | "dependencies": { 23 | }, 24 | "devDependencies": { 25 | "@acme/eslint-config": "0.2.0", 26 | "@acme/prettier-config": "^0.1.0", 27 | "@acme/tsconfig": "^0.1.0", 28 | "eslint": "^8.56.0", 29 | "prettier": "^3.1.1", 30 | "typescript": "^5.3.3" 31 | }, 32 | "eslintConfig": { 33 | "extends": [ 34 | "@acme/eslint-config/base" 35 | ] 36 | }, 37 | "prettier": "@acme/prettier-config" 38 | } 39 | -------------------------------------------------------------------------------- /turbo/generators/templates/tsconfig.json.hbs: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@acme/tsconfig/base.json", 3 | "compilerOptions": { 4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" 5 | }, 6 | "include": ["."], 7 | "exclude": ["node_modules"] 8 | } 9 | --------------------------------------------------------------------------------