├── .env.example ├── .github ├── DISCUSSION_TEMPLATE │ └── ideas.yml ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── config.yml ├── renovate.json └── workflows │ └── ci.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .vscode ├── create-t3-turbo.code-workspace ├── extensions.json ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── TODO.md ├── apps ├── expo │ ├── .env.example │ ├── .expo-shared │ │ └── assets.json │ ├── .vscode │ │ └── launch.json │ ├── app.config.ts │ ├── assets │ │ ├── icon-dark.png │ │ └── icon.png │ ├── babel.config.js │ ├── components.json │ ├── eas.json │ ├── eslint.config.mjs │ ├── metro.config.js │ ├── package.json │ ├── src │ │ ├── app │ │ │ ├── +not-found.tsx │ │ │ ├── _layout.tsx │ │ │ ├── index.tsx │ │ │ ├── post │ │ │ │ └── [id].tsx │ │ │ └── profile.tsx │ │ ├── components │ │ │ ├── header.tsx │ │ │ ├── icons.tsx │ │ │ ├── theme-toggle.tsx │ │ │ └── ui │ │ │ │ ├── avatar.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── card.tsx │ │ │ │ ├── dialog.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── label.tsx │ │ │ │ ├── skeleton.tsx │ │ │ │ └── text.tsx │ │ ├── lib │ │ │ ├── android-navigation-bar.tsx │ │ │ ├── constants.ts │ │ │ ├── icons │ │ │ │ ├── X.tsx │ │ │ │ └── iconWithClassName.ts │ │ │ ├── use-color-scheme.tsx │ │ │ └── utils.ts │ │ ├── styles.css │ │ ├── types │ │ │ └── nativewind-env.d.ts │ │ └── utils │ │ │ ├── api.tsx │ │ │ ├── auth.ts │ │ │ └── supabase.ts │ ├── tailwind.config.ts │ └── tsconfig.json └── nextjs │ ├── .vscode │ └── launch.json │ ├── README.md │ ├── eslint.config.js │ ├── next.config.js │ ├── package.json │ ├── postcss.config.cjs │ ├── public │ ├── favicon.ico │ ├── icon-dark.svg │ ├── icon-light.svg │ └── images │ │ └── hero.jpg │ ├── src │ ├── app │ │ ├── _components │ │ │ ├── landing │ │ │ │ ├── faq │ │ │ │ │ └── faq.tsx │ │ │ │ ├── features │ │ │ │ │ └── features.tsx │ │ │ │ ├── footer │ │ │ │ │ └── footer.tsx │ │ │ │ ├── header │ │ │ │ │ └── header.tsx │ │ │ │ ├── hero │ │ │ │ │ └── hero.tsx │ │ │ │ └── index.ts │ │ │ ├── motion-wrap.tsx │ │ │ └── smooth-scroll.tsx │ │ ├── api │ │ │ └── trpc │ │ │ │ └── [trpc] │ │ │ │ └── route.ts │ │ ├── auth │ │ │ ├── _components │ │ │ │ ├── back-button.tsx │ │ │ │ ├── card-wrapper.tsx │ │ │ │ ├── form-error.tsx │ │ │ │ ├── form-success.tsx │ │ │ │ ├── header.tsx │ │ │ │ ├── sign-in-form.tsx │ │ │ │ ├── sign-out-button.tsx │ │ │ │ ├── sign-up-form.tsx │ │ │ │ └── social.tsx │ │ │ ├── actions.ts │ │ │ ├── callback │ │ │ │ └── route.ts │ │ │ ├── confirm │ │ │ │ └── route.ts │ │ │ ├── layout.tsx │ │ │ ├── signin │ │ │ │ └── page.tsx │ │ │ └── signup │ │ │ │ └── page.tsx │ │ ├── dashboard │ │ │ ├── _components │ │ │ │ ├── auth-showcase.tsx │ │ │ │ ├── layout │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── logo.tsx │ │ │ │ │ └── nav.tsx │ │ │ │ ├── posts.tsx │ │ │ │ └── user-avatar.tsx │ │ │ ├── layout.tsx │ │ │ ├── page.tsx │ │ │ └── settings │ │ │ │ ├── _components │ │ │ │ └── sidebar-nav.tsx │ │ │ │ ├── account │ │ │ │ ├── account-form.tsx │ │ │ │ └── page.tsx │ │ │ │ ├── appearance │ │ │ │ ├── appearance-form.tsx │ │ │ │ └── page.tsx │ │ │ │ ├── display │ │ │ │ ├── display-form.tsx │ │ │ │ └── page.tsx │ │ │ │ ├── layout.tsx │ │ │ │ ├── notifications │ │ │ │ ├── notifications-form.tsx │ │ │ │ └── page.tsx │ │ │ │ ├── page.tsx │ │ │ │ └── profile │ │ │ │ ├── page.tsx │ │ │ │ └── profile-form.tsx │ │ ├── globals.css │ │ ├── layout.tsx │ │ └── page.tsx │ ├── config │ │ └── routes.ts │ ├── env.ts │ ├── lib │ │ ├── lenis │ │ │ └── index.ts │ │ └── safe-action.ts │ ├── middleware.ts │ ├── trpc │ │ ├── query-client.ts │ │ ├── react.tsx │ │ └── server.ts │ └── utils │ │ └── supabase │ │ ├── client.ts │ │ ├── middleware.ts │ │ └── server.ts │ ├── tailwind.config.ts │ └── tsconfig.json ├── package.json ├── packages ├── api │ ├── eslint.config.js │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── root.ts │ │ ├── router │ │ │ ├── auth.ts │ │ │ └── post.ts │ │ └── trpc.ts │ └── tsconfig.json ├── db │ ├── drizzle.config.ts │ ├── eslint.config.js │ ├── migrations │ │ ├── 0000_right_karnak.sql │ │ └── meta │ │ │ ├── 0000_snapshot.json │ │ │ └── _journal.json │ ├── package.json │ ├── src │ │ ├── client.ts │ │ ├── index.ts │ │ ├── lib │ │ │ └── utils.ts │ │ ├── migrate.ts │ │ └── schema │ │ │ ├── _table.ts │ │ │ ├── auth.ts │ │ │ ├── index.ts │ │ │ ├── post.ts │ │ │ └── profile.ts │ └── tsconfig.json ├── ui │ ├── components.json │ ├── eslint.config.js │ ├── package.json │ ├── src │ │ ├── accordion.tsx │ │ ├── avatar.tsx │ │ ├── badge.tsx │ │ ├── breadcrumb.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── card.tsx │ │ ├── checkbox.tsx │ │ ├── command.tsx │ │ ├── dialog.tsx │ │ ├── dropdown-menu.tsx │ │ ├── form.tsx │ │ ├── index.ts │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── popover.tsx │ │ ├── radio-group.tsx │ │ ├── resizable.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── sonner.tsx │ │ ├── switch.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── theme.tsx │ │ └── tooltip.tsx │ ├── tailwind.config.ts │ └── tsconfig.json └── validators │ ├── eslint.config.js │ ├── package.json │ ├── src │ └── index.ts │ └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── scripts └── update-shadui.sh ├── tooling ├── eslint │ ├── base.js │ ├── nextjs.js │ ├── package.json │ ├── react.js │ ├── tsconfig.json │ └── types.d.ts ├── github │ ├── package.json │ └── setup │ │ └── action.yml ├── prettier │ ├── index.js │ ├── package.json │ └── tsconfig.json ├── tailwind │ ├── base.ts │ ├── eslint.config.js │ ├── native.ts │ ├── package.json │ ├── tsconfig.json │ └── web.ts └── typescript │ ├── base.json │ ├── internal-package.json │ └── package.json ├── turbo.json ├── turbo └── generators │ ├── config.ts │ └── templates │ ├── eslint.config.js.hbs │ ├── package.json.hbs │ └── tsconfig.json.hbs └── vercel.json /.env.example: -------------------------------------------------------------------------------- 1 | # Since .env 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 | # The Supavisor URL is used to connect to your Supabase database 8 | POSTGRES_URL="postgres://postgres.[USERNAME]:[PASSWORD]@aws-0-eu-central-1.pooler.supabase.com:6543/postgres?workaround=supabase-pooler.vercel" 9 | 10 | NEXT_PUBLIC_SUPABASE_URL="https://YOUR_PROJECT_REF.supabase.co" 11 | NEXT_PUBLIC_SUPABASE_ANON_KEY="YOUR_ANON_KEY" 12 | -------------------------------------------------------------------------------- /.github/DISCUSSION_TEMPLATE/ideas.yml: -------------------------------------------------------------------------------- 1 | body: 2 | - type: markdown 3 | attributes: 4 | value: | 5 | Thank you for taking the time to file a feature request. Please fill out this form as completely as possible. 6 | - type: textarea 7 | attributes: 8 | label: Describe the feature you'd like to request 9 | description: Please describe the feature as clear and concise as possible. Remember to add context as to why you believe this feature is needed. 10 | validations: 11 | required: true 12 | - type: textarea 13 | attributes: 14 | label: Describe the solution you'd like to see 15 | description: Please describe the solution you would like to see. Adding example usage is a good way to provide context. 16 | validations: 17 | required: true 18 | - type: textarea 19 | attributes: 20 | label: Additional information 21 | description: Add any other information related to the feature here. If your feature request is related to any issues or discussions, link them here. 22 | -------------------------------------------------------------------------------- /.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/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: Ask a question 3 | url: https://github.com/t3-oss/create-t3-turbo/discussions 4 | about: Ask questions and discuss with other community members 5 | - name: Feature request 6 | url: https://github.com/t3-oss/create-t3-turbo/discussions/new?category=ideas 7 | about: Feature requests should be opened as discussions 8 | -------------------------------------------------------------------------------- /.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 | "automerge": true 13 | } 14 | -------------------------------------------------------------------------------- /.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 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} 13 | 14 | # You can leverage Vercel Remote Caching with Turbo to speed up your builds 15 | # @link https://turborepo.org/docs/core-concepts/remote-caching#remote-caching-on-vercel-builds 16 | env: 17 | FORCE_COLOR: 3 18 | TURBO_TEAM: ${{ vars.TURBO_TEAM }} 19 | TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} 20 | 21 | jobs: 22 | lint: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v4 26 | 27 | - name: Setup 28 | uses: ./tooling/github/setup 29 | 30 | - name: Copy env 31 | shell: bash 32 | run: cp .env.example .env 33 | 34 | - name: Lint 35 | run: pnpm lint && pnpm lint:ws 36 | 37 | format: 38 | runs-on: ubuntu-latest 39 | steps: 40 | - uses: actions/checkout@v4 41 | 42 | - name: Setup 43 | uses: ./tooling/github/setup 44 | 45 | - name: Format 46 | run: pnpm format 47 | 48 | typecheck: 49 | runs-on: ubuntu-latest 50 | steps: 51 | - uses: actions/checkout@v4 52 | 53 | - name: Setup 54 | uses: ./tooling/github/setup 55 | 56 | - name: Typecheck 57 | run: pnpm typecheck 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # testing 9 | coverage 10 | 11 | # next.js 12 | .next/ 13 | out/ 14 | next-env.d.ts 15 | 16 | apps/nextjs/core 17 | 18 | # nitro 19 | .nitro/ 20 | .output/ 21 | 22 | # expo 23 | .expo/ 24 | expo-env.d.ts 25 | apps/expo/.gitignore 26 | apps/expo/ios 27 | apps/expo/android 28 | 29 | apps/expo/core 30 | 31 | # production 32 | build 33 | 34 | # misc 35 | .DS_Store 36 | *.pem 37 | 38 | # debug 39 | npm-debug.log* 40 | yarn-debug.log* 41 | yarn-error.log* 42 | .pnpm-debug.log* 43 | 44 | # local env files 45 | .env 46 | .env*.local 47 | 48 | # vercel 49 | .vercel 50 | 51 | # typescript 52 | *.tsbuildinfo 53 | dist/ 54 | 55 | # turbo 56 | .turbo 57 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | node-linker=hoisted 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20.12 2 | -------------------------------------------------------------------------------- /.vscode/create-t3-turbo.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "name": "Next.js", 5 | "path": "../apps/nextjs", 6 | }, 7 | { 8 | "name": "Expo", 9 | "path": "../apps/expo", 10 | }, 11 | { 12 | "name": "tRPC", 13 | "path": "../packages/api", 14 | }, 15 | { 16 | "name": "db", 17 | "path": "../packages/db", 18 | }, 19 | { 20 | "name": "Root", 21 | "path": "../", 22 | }, 23 | ], 24 | "extensions": { 25 | "recommendations": ["joshx.workspace-terminals"], 26 | }, 27 | "settings": { 28 | "search.useIgnoreFiles": true, 29 | // use root .gitignore to ignore files from search 30 | "search.useParentIgnoreFiles": true, 31 | }, 32 | "launch": { 33 | "version": "0.2.0", 34 | "configurations": [], 35 | "compounds": [ 36 | { 37 | "name": "Launch Next.js and Expo", 38 | // from apps/nextjs/.vscode/launch.json and apps/expo/.vscode/launch.json 39 | "configurations": ["Debug Next.js", "Debug Expo app"], 40 | }, 41 | ], 42 | }, 43 | } 44 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "bradlc.vscode-tailwindcss", 4 | "dbaeumer.vscode-eslint", 5 | "esbenp.prettier-vscode", 6 | "expo.vscode-expo-tools", 7 | "yoavbls.pretty-ts-errors" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.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 | "name": "Expo", 14 | "type": "expo", 15 | "request": "attach", 16 | "projectRoot": "${workspaceFolder}/apps/expo", 17 | "bundlerPort": "8081", 18 | "bundlerHost": "127.0.0.1" 19 | } 20 | ], 21 | "compounds": [ 22 | { 23 | "name": "Launch Next.js and Expo", 24 | "configurations": ["Next.js", "Expo"] 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.workingDirectories": [ 3 | { 4 | "mode": "auto" 5 | } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # Todos 2 | 3 | - [x] Landing Page 4 | - [ ] Update README with latest information 5 | - [x] Authentication System 6 | - [ ] Add zod to create post 7 | - [ ] Remove the limit for the posts fetching 8 | - [ ] File Upload Functionality 9 | - [ ] Folders & Files Management 10 | - [ ] Profile Management -------------------------------------------------------------------------------- /apps/expo/.env.example: -------------------------------------------------------------------------------- 1 | EXPO_PUBLIC_SUPABASE_URL="https://YOUR_PROJECT_REF.supabase.co" 2 | EXPO_PUBLIC_SUPABASE_ANON_KEY="YOUR_ANON_KEY" 3 | -------------------------------------------------------------------------------- /apps/expo/.expo-shared/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true, 3 | "40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true 4 | } 5 | -------------------------------------------------------------------------------- /apps/expo/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "expo", 9 | "request": "attach", 10 | "name": "Debug Expo app", 11 | "projectRoot": "${workspaceFolder}", 12 | "bundlerPort": "8081", 13 | "bundlerHost": "127.0.0.1" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /apps/expo/app.config.ts: -------------------------------------------------------------------------------- 1 | import type { ConfigContext, ExpoConfig } from "@expo/config"; 2 | 3 | export default ({ config }: ConfigContext): ExpoConfig => ({ 4 | ...config, 5 | name: "expo", 6 | slug: "expo", 7 | scheme: "expo", 8 | version: "0.1.0", 9 | orientation: "portrait", 10 | icon: "./assets/icon.png", 11 | userInterfaceStyle: "automatic", 12 | splash: { 13 | image: "./assets/icon.png", 14 | resizeMode: "contain", 15 | backgroundColor: "#18181A", 16 | }, 17 | updates: { 18 | fallbackToCacheTimeout: 0, 19 | }, 20 | assetBundlePatterns: ["**/*"], 21 | ios: { 22 | bundleIdentifier: "your.bundle.identifier", 23 | supportsTablet: true, 24 | usesAppleSignIn: true, 25 | config: { 26 | usesNonExemptEncryption: false, 27 | }, 28 | }, 29 | android: { 30 | package: "your.bundle.identifier", 31 | adaptiveIcon: { 32 | foregroundImage: "./assets/icon.png", 33 | backgroundColor: "#18181A", 34 | }, 35 | }, 36 | experiments: { 37 | tsconfigPaths: true, 38 | typedRoutes: true, 39 | }, 40 | // extra: { 41 | // eas: { 42 | // projectId: "your-project-id", 43 | // }, 44 | // }, 45 | plugins: ["expo-router", "expo-secure-store", "expo-apple-authentication"], 46 | }); 47 | -------------------------------------------------------------------------------- /apps/expo/assets/icon-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techwithanirudh/create-t3-turbo-supabase/a24a1dc38df1facdd80366f4528b42157ef03fab/apps/expo/assets/icon-dark.png -------------------------------------------------------------------------------- /apps/expo/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techwithanirudh/create-t3-turbo-supabase/a24a1dc38df1facdd80366f4528b42157ef03fab/apps/expo/assets/icon.png -------------------------------------------------------------------------------- /apps/expo/babel.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import("@babel/core").ConfigFunction} */ 2 | module.exports = (api) => { 3 | api.cache(true); 4 | return { 5 | presets: [ 6 | ["babel-preset-expo", { jsxImportSource: "nativewind" }], 7 | "nativewind/babel", 8 | ], 9 | plugins: ["react-native-reanimated/plugin"], 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /apps/expo/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "platforms": "universal", 3 | "aliases": { 4 | "components": "~/components", 5 | "lib": "~/lib" 6 | } 7 | } -------------------------------------------------------------------------------- /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/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import baseConfig from "@acme/eslint-config/base"; 2 | import reactConfig from "@acme/eslint-config/react"; 3 | 4 | /** @type {import('typescript-eslint').Config} */ 5 | export default [ 6 | { 7 | ignores: [".expo/**", "expo-plugins/**"], 8 | }, 9 | ...baseConfig, 10 | ...reactConfig, 11 | ]; 12 | -------------------------------------------------------------------------------- /apps/expo/metro.config.js: -------------------------------------------------------------------------------- 1 | // Learn more: https://docs.expo.dev/guides/monorepos/ 2 | const { getDefaultConfig } = require("expo/metro-config"); 3 | const { FileStore } = require("metro-cache"); 4 | const { withNativeWind } = require("nativewind/metro"); 5 | 6 | const path = require("path"); 7 | 8 | module.exports = withTurborepoManagedCache( 9 | withMonorepoPaths( 10 | withNativeWind(getDefaultConfig(__dirname), { 11 | input: "./src/styles.css", 12 | configPath: "./tailwind.config.ts", 13 | }), 14 | ), 15 | ); 16 | 17 | /** 18 | * Add the monorepo paths to the Metro config. 19 | * This allows Metro to resolve modules from the monorepo. 20 | * 21 | * @see https://docs.expo.dev/guides/monorepos/#modify-the-metro-config 22 | * @param {import('expo/metro-config').MetroConfig} config 23 | * @returns {import('expo/metro-config').MetroConfig} 24 | */ 25 | function withMonorepoPaths(config) { 26 | const projectRoot = __dirname; 27 | const workspaceRoot = path.resolve(projectRoot, "../.."); 28 | 29 | // #1 - Watch all files in the monorepo 30 | config.watchFolders = [workspaceRoot]; 31 | 32 | // #2 - Resolve modules within the project's `node_modules` first, then all monorepo modules 33 | config.resolver.nodeModulesPaths = [ 34 | path.resolve(projectRoot, "node_modules"), 35 | path.resolve(workspaceRoot, "node_modules"), 36 | ]; 37 | 38 | return config; 39 | } 40 | 41 | /** 42 | * Move the Metro cache to the `node_modules/.cache/metro` folder. 43 | * This repository configured Turborepo to use this cache location as well. 44 | * If you have any environment variables, you can configure Turborepo to invalidate it when needed. 45 | * 46 | * @see https://turbo.build/repo/docs/reference/configuration#env 47 | * @param {import('expo/metro-config').MetroConfig} config 48 | * @returns {import('expo/metro-config').MetroConfig} 49 | */ 50 | function withTurborepoManagedCache(config) { 51 | config.cacheStores = [ 52 | new FileStore({ root: path.join(__dirname, "node_modules/.cache/metro") }), 53 | ]; 54 | return config; 55 | } 56 | -------------------------------------------------------------------------------- /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", 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 | "ui:add": "pnpm dlx @react-native-reusables/cli@latest add" 17 | }, 18 | "dependencies": { 19 | "@bacons/text-decoder": "^0.0.0", 20 | "@expo/metro-config": "^0.18.3", 21 | "@react-native-async-storage/async-storage": "1.23.1", 22 | "@rn-primitives/avatar": "^1.0.3", 23 | "@rn-primitives/dialog": "^1.0.3", 24 | "@rn-primitives/label": "^1.0.3", 25 | "@rn-primitives/slot": "^1.0.3", 26 | "@rn-primitives/types": "^1.0.3", 27 | "@shopify/flash-list": "1.6.4", 28 | "@supabase/auth-helpers-react": "^0.4.2", 29 | "@supabase/supabase-js": "^2.43.1", 30 | "@tanstack/react-query": "^5.49.2", 31 | "@trpc/client": "11.0.0-rc.441", 32 | "@trpc/react-query": "11.0.0-rc.441", 33 | "@trpc/server": "11.0.0-rc.441", 34 | "class-variance-authority": "^0.7.0", 35 | "clsx": "^2.1.1", 36 | "expo": "~51.0.14", 37 | "expo-apple-authentication": "~6.4.1", 38 | "expo-constants": "~16.0.1", 39 | "expo-crypto": "~13.0.2", 40 | "expo-dev-client": "~4.0.19", 41 | "expo-image": "~1.12.9", 42 | "expo-linking": "~6.3.1", 43 | "expo-navigation-bar": "^3.0.6", 44 | "expo-router": "~3.5.11", 45 | "expo-secure-store": "^13.0.1", 46 | "expo-splash-screen": "~0.27.4", 47 | "expo-status-bar": "~1.12.1", 48 | "expo-web-browser": "^13.0.3", 49 | "lucide-react-native": "^0.399.0", 50 | "nativewind": "~4.0.36", 51 | "react": "18.3.1", 52 | "react-dom": "18.3.1", 53 | "react-native": "~0.74.2", 54 | "react-native-css-interop": "~0.0.36", 55 | "react-native-gesture-handler": "~2.16.2", 56 | "react-native-reanimated": "~3.10.1", 57 | "react-native-safe-area-context": "~4.10.1", 58 | "react-native-screens": "~3.31.1", 59 | "react-native-svg": "15.2.0", 60 | "react-native-url-polyfill": "^1.3.0", 61 | "superjson": "2.2.1", 62 | "tailwind-merge": "^2.3.0", 63 | "tailwindcss-animate": "^1.0.7" 64 | }, 65 | "devDependencies": { 66 | "@acme/api": "workspace:*", 67 | "@acme/eslint-config": "workspace:*", 68 | "@acme/prettier-config": "workspace:*", 69 | "@acme/tailwind-config": "workspace:*", 70 | "@acme/tsconfig": "workspace:*", 71 | "@babel/core": "^7.24.7", 72 | "@babel/preset-env": "^7.24.7", 73 | "@babel/runtime": "^7.24.7", 74 | "@expo/config-plugins": "^8.0.4", 75 | "@types/babel__core": "^7.20.5", 76 | "@types/react": "^18.3.3", 77 | "eslint": "^9.6.0", 78 | "prettier": "^3.3.2", 79 | "tailwindcss": "^3.4.4", 80 | "typescript": "^5.5.3" 81 | }, 82 | "prettier": "@acme/prettier-config" 83 | } 84 | -------------------------------------------------------------------------------- /apps/expo/src/app/+not-found.tsx: -------------------------------------------------------------------------------- 1 | import { View } from "react-native"; 2 | import { Link, Stack } from "expo-router"; 3 | 4 | import { Text } from "~/components/ui/text"; 5 | 6 | export default function NotFoundScreen() { 7 | return ( 8 | <> 9 | 10 | 11 | This screen doesn't exist. 12 | 13 | 14 | Go to home screen! 15 | 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /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 }); 10 | 11 | if (!data) return null; 12 | 13 | return ( 14 | 15 | 16 | 17 | 18 | {data.title} 19 | 20 | {data.content} 21 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /apps/expo/src/components/header.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | import { Button, Text, TouchableOpacity } from "react-native"; 3 | import { useRouter } from "expo-router"; 4 | import { Ionicons } from "@expo/vector-icons"; 5 | 6 | export function AuthAvatar() { 7 | // const user = useUser(); 8 | const router = useRouter(); 9 | // const profileImage = user?.user_metadata.avatar_url as string | undefined; 10 | 11 | return 31 | 34 | 35 | 36 | 37 | Hero 45 | 46 | 47 | ); 48 | } -------------------------------------------------------------------------------- /apps/nextjs/src/app/_components/landing/index.ts: -------------------------------------------------------------------------------- 1 | import Header from './header/header'; 2 | import Hero from './hero/hero'; 3 | import Features from './features/features'; 4 | import FAQ from './faq/faq'; 5 | import Footer from './footer/footer'; 6 | 7 | export { Header, Hero, Features, FAQ, Footer }; -------------------------------------------------------------------------------- /apps/nextjs/src/app/_components/motion-wrap.tsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 'use client'; 4 | import type { ReactNode } from 'react'; 5 | import React from 'react'; 6 | import type { MotionProps } from 'framer-motion'; 7 | import { motion } from 'framer-motion'; 8 | import { cn } from '@acme/ui'; 9 | 10 | type MotionWrapProps = { 11 | children: ReactNode; 12 | className?: string; 13 | id?: string; 14 | } & MotionProps; 15 | 16 | const MotionWrap: React.FC = ({ 17 | children, 18 | className, 19 | ...props 20 | }) => { 21 | return ( 22 | 28 | {children} 29 | 30 | ); 31 | }; 32 | 33 | export default MotionWrap; 34 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/_components/smooth-scroll.tsx: -------------------------------------------------------------------------------- 1 | 2 | // according to a github issue it is not reccomended to render this in a layout rather, have a wrapper component which does it each render 3 | // https://github.com/darkroomengineering/lenis/issues/319 4 | 'use client'; 5 | 6 | import React, { useEffect } from 'react'; 7 | import { ReactLenis, useLenis } from '~/lib/lenis'; 8 | 9 | interface LenisProps { 10 | children: React.ReactNode; 11 | } 12 | 13 | function SmoothScroll({ children }: LenisProps) { 14 | const lenis = useLenis(() => { 15 | // called every scroll 16 | }); 17 | 18 | useEffect(() => { 19 | document.addEventListener('DOMContentLoaded', () => { 20 | lenis?.stop(); 21 | lenis?.start(); 22 | }); 23 | }, []); 24 | 25 | return ( 26 | 32 | {children} 33 | 34 | ); 35 | } 36 | 37 | export default SmoothScroll; -------------------------------------------------------------------------------- /apps/nextjs/src/app/api/trpc/[trpc]/route.ts: -------------------------------------------------------------------------------- 1 | import type { NextRequest } from "next/server"; 2 | import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; 3 | 4 | import { appRouter, createTRPCContext } from "@acme/api"; 5 | 6 | import { createClient } from "~/utils/supabase/server"; 7 | 8 | export const runtime = "edge"; 9 | 10 | /** 11 | * Configure basic CORS headers 12 | * You should extend this to match your needs 13 | */ 14 | function setCorsHeaders(res: Response) { 15 | res.headers.set("Access-Control-Allow-Origin", "*"); 16 | res.headers.set("Access-Control-Request-Method", "*"); 17 | res.headers.set("Access-Control-Allow-Methods", "OPTIONS, GET, POST"); 18 | res.headers.set("Access-Control-Allow-Headers", "*"); 19 | } 20 | 21 | export function OPTIONS() { 22 | const response = new Response(null, { 23 | status: 204, 24 | }); 25 | setCorsHeaders(response); 26 | return response; 27 | } 28 | 29 | const handler = async (req: NextRequest) => { 30 | const supabase = createClient(); 31 | 32 | const response = await fetchRequestHandler({ 33 | endpoint: "/api/trpc", 34 | router: appRouter, 35 | req, 36 | createContext: () => createTRPCContext({ headers: req.headers, supabase }), 37 | onError({ error, path }) { 38 | console.error(`>>> tRPC Error on '${path}'`, error); 39 | }, 40 | }); 41 | 42 | setCorsHeaders(response); 43 | return response; 44 | }; 45 | 46 | export { handler as GET, handler as POST }; 47 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/auth/_components/back-button.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | interface BackButtonProps { 4 | label: string; 5 | linkLabel: string; 6 | href: string; 7 | } 8 | 9 | export const BackButton = ({ label, linkLabel, href }: BackButtonProps) => { 10 | return ( 11 |
12 | {label} 13 | 14 | {linkLabel} 15 | 16 |
17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/auth/_components/card-wrapper.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@acme/ui"; 2 | 3 | import { BackButton } from "~/app/auth/_components/back-button"; 4 | import { Social } from "~/app/auth/_components/social"; 5 | 6 | interface CardWrapperProps { 7 | children: React.ReactNode; 8 | backButtonLabel: string; 9 | backButtonLinkLabel: string; 10 | backButtonHref: string; 11 | showSocial?: boolean; 12 | showCredentials?: boolean; 13 | className?: string; 14 | } 15 | 16 | export const CardWrapper = ({ 17 | children, 18 | backButtonLabel, 19 | backButtonLinkLabel, 20 | backButtonHref, 21 | showSocial, 22 | showCredentials, 23 | className, 24 | }: CardWrapperProps) => { 25 | return ( 26 |
27 | {showCredentials && ( 28 | <> 29 |
{children}
30 |
31 |
32 | 33 |
34 |
35 | 36 | Or continue with 37 | 38 |
39 |
40 | 41 | )} 42 | 43 | {showSocial && } 44 | 45 | 50 |
51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/auth/_components/form-error.tsx: -------------------------------------------------------------------------------- 1 | import { AlertTriangle } from "lucide-react"; 2 | 3 | interface FormErrorProps { 4 | message?: string; 5 | } 6 | 7 | export const FormError = ({ message }: FormErrorProps) => { 8 | if (!message) { 9 | return null; 10 | } 11 | 12 | return ( 13 |
14 | 15 |

{message}

16 |
17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/auth/_components/form-success.tsx: -------------------------------------------------------------------------------- 1 | import { CheckCircle } from "lucide-react"; 2 | 3 | interface FormSuccessProps { 4 | message?: string; 5 | } 6 | 7 | export const FormSuccess = ({ message }: FormSuccessProps) => { 8 | if (!message) { 9 | return null; 10 | } 11 | 12 | return ( 13 |
14 | 15 |

{message}

16 |
17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/auth/_components/header.tsx: -------------------------------------------------------------------------------- 1 | interface HeaderProps { 2 | label: string; 3 | } 4 | 5 | export const Header = ({ label }: HeaderProps) => { 6 | return ( 7 |
8 |

T3 Turbo x Supabase

9 |

{label}

10 |
11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/auth/_components/sign-in-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useAction } from "next-safe-action/hooks"; 4 | 5 | import type { SignIn } from "@acme/validators"; 6 | import { Button } from "@acme/ui/button"; 7 | import { 8 | Form, 9 | FormControl, 10 | FormField, 11 | FormItem, 12 | FormMessage, 13 | } from "@acme/ui/form"; 14 | import { zodResolver } from "@hookform/resolvers/zod" 15 | import { useForm } from "react-hook-form" 16 | import { Input } from "@acme/ui/input"; 17 | import { SignInSchema } from "@acme/validators"; 18 | 19 | import { FormError } from "~/app/auth/_components/form-error"; 20 | import { signInWithPassword } from "~/app/auth/actions"; 21 | 22 | import { LoaderCircleIcon } from "lucide-react"; 23 | 24 | export const SignInForm = () => { 25 | const form = useForm({ 26 | resolver: zodResolver(SignInSchema), 27 | defaultValues: { 28 | email: "", 29 | password: "", 30 | }, 31 | }); 32 | 33 | const { execute, result, status } = useAction(signInWithPassword); 34 | 35 | const onSubmit = (values: SignIn) => { 36 | execute(values); 37 | }; 38 | 39 | return ( 40 |
41 | 42 |
43 | ( 47 | 48 | {/* Email address */} 49 | 50 | 56 | 57 | 58 | 59 | )} 60 | /> 61 | 62 | ( 66 | 67 | {/* Password */} 68 | 69 | 75 | 76 | 77 | 78 | )} 79 | /> 80 |
81 | 82 | 83 | 84 | 92 | 93 | 94 | ); 95 | }; 96 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/auth/_components/sign-out-button.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@acme/ui/button"; 2 | 3 | import { signOut } from "~/app/auth/actions"; 4 | 5 | export const SignOutButton = () => { 6 | return ( 7 |
8 | 9 |
10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/auth/_components/sign-up-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useAction } from "next-safe-action/hooks"; 4 | 5 | import type { SignUp } from "@acme/validators"; 6 | import { Button } from "@acme/ui/button"; 7 | import { 8 | Form, 9 | FormControl, 10 | FormField, 11 | FormItem, 12 | FormMessage, 13 | } from "@acme/ui/form"; 14 | import { zodResolver } from "@hookform/resolvers/zod" 15 | import { useForm } from "react-hook-form" 16 | import { Input } from "@acme/ui/input"; 17 | import { SignUpSchema } from "@acme/validators"; 18 | 19 | import { FormError } from "~/app/auth/_components/form-error"; 20 | import { FormSuccess } from "~/app/auth/_components/form-success"; 21 | import { signUp } from "~/app/auth/actions"; 22 | 23 | import { LoaderCircleIcon } from "lucide-react"; 24 | 25 | export const SignUpForm = () => { 26 | const form = useForm({ 27 | resolver: zodResolver(SignUpSchema), 28 | defaultValues: { 29 | email: "", 30 | password: "", 31 | }, 32 | }); 33 | 34 | const { execute, result, status } = useAction(signUp); 35 | 36 | const onSubmit = (values: SignUp) => { 37 | execute(values); 38 | }; 39 | 40 | return ( 41 |
42 | 43 |
44 | ( 48 | 49 | {/* Email address */} 50 | 51 | 57 | 58 | 59 | 60 | )} 61 | /> 62 | 63 | ( 67 | 68 | {/* Password */} 69 | 70 | 76 | 77 | 78 | 79 | )} 80 | /> 81 |
82 | 83 | {status === "hasSucceeded" && ( 84 | 85 | )} 86 | 87 | 88 | 96 | 97 | 98 | ); 99 | }; 100 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/auth/_components/social.tsx: -------------------------------------------------------------------------------- 1 | import { Github } from "lucide-react"; 2 | import { Button } from "@acme/ui/button"; 3 | import { signInWithGithub } from "~/app/auth/actions"; 4 | 5 | export const Social = () => { 6 | return ( 7 |
8 | 19 |
20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/auth/actions.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { revalidatePath } from "next/cache"; 4 | import { headers } from "next/headers"; 5 | import { redirect } from "next/navigation"; 6 | 7 | import { SignInSchema, SignUpSchema } from "@acme/validators"; 8 | 9 | import { DEFAULT_LOGIN_REDIRECT } from "~/config/routes"; 10 | import { action } from "~/lib/safe-action"; 11 | import { createClient } from "~/utils/supabase/server"; 12 | 13 | export const signInWithPassword = action( 14 | SignInSchema, 15 | async ({ email, password }) => { 16 | const supabase = createClient(); 17 | 18 | const { error } = await supabase.auth.signInWithPassword({ 19 | email, 20 | password, 21 | }); 22 | 23 | if (error) throw error; 24 | 25 | revalidatePath("/", "layout"); 26 | redirect(DEFAULT_LOGIN_REDIRECT); 27 | }, 28 | ); 29 | 30 | export const signUp = action(SignUpSchema, async ({ email, password }) => { 31 | const origin = headers().get("origin"); 32 | const supabase = createClient(); 33 | 34 | const redirectUrl = `${origin}/auth/confirm?next=${encodeURIComponent(DEFAULT_LOGIN_REDIRECT)}`; 35 | 36 | const { error, data } = await supabase.auth.signUp({ 37 | email, 38 | password, 39 | options: { 40 | emailRedirectTo: redirectUrl, 41 | }, 42 | }); 43 | 44 | // User already exists, so fake data is returned. See https://supabase.com/docs/reference/javascript/auth-signup 45 | if (data.user?.identities && data.user.identities.length === 0) { 46 | throw new Error("An error occurred. Please try again."); 47 | } 48 | 49 | if (error) throw error; 50 | return data.user; 51 | }); 52 | 53 | export const signInWithGithub = async () => { 54 | const origin = headers().get("origin"); 55 | const supabase = createClient(); 56 | 57 | const redirectUrl = `${origin}/auth/callback?next=${encodeURIComponent(DEFAULT_LOGIN_REDIRECT)}`; 58 | 59 | const res = await supabase.auth.signInWithOAuth({ 60 | provider: "github", 61 | options: { redirectTo: redirectUrl }, 62 | }); 63 | 64 | if (res.data.url) redirect(res.data.url); 65 | throw res.error; 66 | }; 67 | 68 | export const signOut = async () => { 69 | const supabase = createClient(); 70 | await supabase.auth.signOut(); 71 | redirect("/"); 72 | }; 73 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/auth/callback/route.ts: -------------------------------------------------------------------------------- 1 | import type { NextRequest } from "next/server"; 2 | import { NextResponse } from "next/server"; 3 | 4 | import { createClient } from "~/utils/supabase/server"; 5 | 6 | export async function GET(request: NextRequest) { 7 | const { searchParams, origin } = new URL(request.url); 8 | const code = searchParams.get("code"); 9 | // if "next" is in param, use it as the redirect URL 10 | const next = searchParams.get("next") ?? "/"; 11 | 12 | if (code) { 13 | const supabase = createClient(); 14 | 15 | const { error } = await supabase.auth.exchangeCodeForSession(code); 16 | if (!error) { 17 | return NextResponse.redirect(`${origin}${next}`); 18 | } 19 | } 20 | 21 | // return the user to an error page with instructions 22 | return NextResponse.redirect(`${origin}/auth/auth-code-error`); 23 | } 24 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/auth/confirm/route.ts: -------------------------------------------------------------------------------- 1 | import type { EmailOtpType } from "@supabase/supabase-js"; 2 | import type { NextRequest } from "next/server"; 3 | import { NextResponse } from "next/server"; 4 | 5 | import { createClient } from "~/utils/supabase/server"; 6 | 7 | export async function GET(request: NextRequest) { 8 | const { searchParams } = new URL(request.url); 9 | const token_hash = searchParams.get("token_hash"); 10 | const type = searchParams.get("type") as EmailOtpType | null; 11 | const next = searchParams.get("next") ?? "/"; 12 | 13 | const redirectTo = request.nextUrl.clone(); 14 | redirectTo.pathname = next; 15 | redirectTo.searchParams.delete("token_hash"); 16 | redirectTo.searchParams.delete("type"); 17 | 18 | if (token_hash && type) { 19 | const supabase = createClient(); 20 | 21 | const { error } = await supabase.auth.verifyOtp({ 22 | type, 23 | token_hash, 24 | }); 25 | if (!error) { 26 | redirectTo.searchParams.delete("next"); 27 | return NextResponse.redirect(redirectTo); 28 | } 29 | } 30 | 31 | // return the user to an error page with some instructions 32 | redirectTo.pathname = "/error"; 33 | return NextResponse.redirect(redirectTo); 34 | } 35 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/auth/layout.tsx: -------------------------------------------------------------------------------- 1 | import { ThemeToggle } from "@acme/ui/theme"; 2 | 3 | export default function AuthLayout(props: { children: React.ReactNode }) { 4 | return ( 5 |
6 | {props.children} 7 |
8 | 9 |
10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/auth/signin/page.tsx: -------------------------------------------------------------------------------- 1 | import type {Metadata} from "next"; 2 | import Link from "next/link"; 3 | import { NotebookTextIcon } from "lucide-react"; 4 | 5 | import { cn } from "@acme/ui"; 6 | import { buttonVariants } from "@acme/ui/button"; 7 | 8 | import { CardWrapper } from "~/app/auth/_components/card-wrapper"; 9 | import { SignInForm } from "~/app/auth/_components/sign-in-form"; 10 | 11 | import { createClient } from "~/utils/supabase/server"; 12 | import { redirect } from 'next/navigation'; 13 | 14 | export const metadata: Metadata = { 15 | title: "Sign In", 16 | description: "Sign In to Posts Buddy", 17 | }; 18 | 19 | export default async function SignInPage() { 20 | const supabase = createClient(); 21 | const user = await supabase.auth.getUser(); 22 | 23 | if (!user.error ?? user.data.user) redirect('/dashboard') 24 | 25 | return ( 26 | <> 27 |
28 | 35 | Sign Up 36 | 37 |
38 |
39 | 43 | 44 | Notes Buddy 45 | 46 |
47 | {/*
48 |

49 | “This library has saved me countless hours of work and 50 | helped me deliver stunning designs to my clients faster than 51 | ever before.” 52 |

53 |
Sofia Davis
54 |
*/} 55 |
56 |
57 |
58 |
59 |
60 |

61 | Welcome back 62 |

63 |

64 | Enter your email below to login to your account 65 |

66 |
67 | 74 | 75 | 76 | {/*

77 | By clicking continue, you agree to our{" "} 78 | 82 | Terms of Service 83 | {" "} 84 | and{" "} 85 | 89 | Privacy Policy 90 | 91 | . 92 |

*/} 93 |
94 |
95 |
96 | 97 | ); 98 | } 99 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/auth/signup/page.tsx: -------------------------------------------------------------------------------- 1 | import type {Metadata} from "next"; 2 | import Link from "next/link"; 3 | import { NotebookTextIcon } from "lucide-react"; 4 | 5 | import { cn } from "@acme/ui"; 6 | import { buttonVariants } from "@acme/ui/button"; 7 | 8 | import { CardWrapper } from "~/app/auth/_components/card-wrapper"; 9 | import { SignUpForm } from "~/app/auth/_components/sign-up-form"; 10 | 11 | 12 | export const metadata: Metadata = { 13 | title: 'Sign Up', 14 | description: 'Sign Up to Posts Buddy' 15 | }; 16 | 17 | export default function SignUpPage() { 18 | return ( 19 | <> 20 |
21 | 28 | Sign In 29 | 30 |
31 |
32 | 36 | 37 | Notes Buddy 38 | 39 |
40 | {/*
41 |

42 | “This library has saved me countless hours of work and 43 | helped me deliver stunning designs to my clients faster than 44 | ever before.” 45 |

46 |
Sofia Davis
47 |
*/} 48 |
49 |
50 |
51 |
52 |
53 |

54 | Create an account 55 |

56 |

57 | Enter your email below to create your account 58 |

59 |
60 | 67 | 68 | 69 | {/*

70 | By clicking continue, you agree to our{" "} 71 | 75 | Terms of Service 76 | {" "} 77 | and{" "} 78 | 82 | Privacy Policy 83 | 84 | . 85 |

*/} 86 |
87 |
88 |
89 | 90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/dashboard/_components/auth-showcase.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | import { Button } from "@acme/ui/button"; 4 | 5 | import { SignOutButton } from "~/app/auth/_components/sign-out-button"; 6 | import { DEFAULT_AUTH_ROUTE } from "~/config/routes"; 7 | import { createClient } from "~/utils/supabase/server"; 8 | 9 | export async function AuthShowcase() { 10 | const supabase = createClient(); 11 | const user = await supabase.auth.getUser(); 12 | 13 | if (user.error ?? !user.data.user) { 14 | return ( 15 | 18 | ); 19 | } 20 | 21 | return ( 22 |
23 |

24 | Logged in as {user.data.user.email} 25 |

26 | 27 | 28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/dashboard/_components/layout/logo.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { NotebookTextIcon } from "lucide-react"; 5 | import { useTheme } from "next-themes"; 6 | 7 | import { cn } from "@acme/ui"; 8 | import { Button } from "@acme/ui/button"; 9 | import { 10 | DropdownMenu, 11 | DropdownMenuContent, 12 | DropdownMenuItem, 13 | DropdownMenuLabel, 14 | DropdownMenuSeparator, 15 | DropdownMenuTrigger, 16 | } from "@acme/ui/dropdown-menu"; 17 | 18 | interface LogoProps { 19 | isCollapsed: boolean; 20 | } 21 | 22 | export function Logo({ isCollapsed }: LogoProps) { 23 | const { setTheme } = useTheme(); 24 | 25 | return ( 26 |
32 | 33 | 34 | 41 | 42 | 43 | Theme 44 | 45 | setTheme("light")}> 46 | Light 47 | 48 | setTheme("dark")}> 49 | Dark 50 | 51 | setTheme("system")}> 52 | System 53 | 54 | 55 | 56 |
57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/dashboard/_components/layout/nav.tsx: -------------------------------------------------------------------------------- 1 | 2 | "use client" 3 | 4 | import Link from "next/link" 5 | import type { LucideIcon } from "lucide-react" 6 | 7 | import { cn } from "@acme/ui" 8 | import { buttonVariants } from "@acme/ui/button" 9 | import { 10 | Tooltip, 11 | TooltipContent, 12 | TooltipTrigger, 13 | } from "@acme/ui/tooltip" 14 | 15 | export interface NavProps { 16 | isCollapsed: boolean 17 | links: { 18 | title: string 19 | href: string; 20 | label?: string 21 | icon: LucideIcon 22 | variant: "default" | "ghost" 23 | }[] 24 | } 25 | 26 | export function Nav({ links, isCollapsed }: NavProps) { 27 | return ( 28 |
32 | 87 |
88 | ) 89 | } 90 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/dashboard/_components/user-avatar.tsx: -------------------------------------------------------------------------------- 1 | 2 | import * as React from 'react'; 3 | 4 | import { Button } from '@acme/ui/button'; 5 | import { 6 | DropdownMenu, 7 | DropdownMenuContent, 8 | DropdownMenuGroup, 9 | DropdownMenuItem, 10 | DropdownMenuLabel, 11 | DropdownMenuSeparator, 12 | DropdownMenuTrigger 13 | } from '@acme/ui/dropdown-menu'; 14 | import { Avatar, AvatarFallback, AvatarImage } from '@acme/ui/avatar'; 15 | 16 | import Link from 'next/link'; 17 | 18 | import { CreditCardIcon, LogOutIcon, UserIcon } from 'lucide-react'; 19 | import { signOut } from '~/app/auth/actions'; 20 | 21 | import type { UserResponse } from '@supabase/supabase-js'; 22 | 23 | interface UserAvatarProps { 24 | user: UserResponse; 25 | } 26 | 27 | export default function UserAvatar({ user }: UserAvatarProps) { 28 | if (user.error ?? !user.data.user) return null; 29 | const session = user.data; 30 | 31 | async function handleSignOut() { 32 | const res = await signOut(); 33 | 34 | // const title = 35 | // typeof res?.message === 'string' && res?.message != '' 36 | // ? res.message 37 | // : 'Oops! Something went wrong. Please try again later.'; 38 | // toast(title); 39 | // if (res?.success && res?.redirect && typeof res.redirect === 'string') 40 | // redirect(res?.redirect); 41 | } 42 | 43 | // yes, this is from stackoverflow... 44 | function getInitials(name: string) { 45 | const names = name.split(' '); 46 | const initials = names.map(x => x.charAt(0).toUpperCase()) 47 | if (initials.length > 1) { 48 | return `${initials[0]}${initials[initials.length - 1]}`; 49 | } else { 50 | return initials[0]; 51 | } 52 | } 53 | 54 | return ( 55 | 56 | 57 | 70 | 71 | 72 | 73 |
74 |

75 | {session.user.user_metadata.full_name ?? 'User'} 76 |

77 |

78 | {session.user.email ?? 'user@example.com'} 79 |

80 |
81 |
82 | 83 | 84 | 85 | 86 | 87 | Profile 88 | 89 | 90 | 91 | {/* TODO: Build this XD */} 92 | 93 | 94 | Billing 95 | 96 | 97 | 98 | 99 | 100 | Log out 101 | 102 | 103 |
104 |
105 | ); 106 | } -------------------------------------------------------------------------------- /apps/nextjs/src/app/dashboard/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { cookies } from "next/headers"; 3 | import { redirect } from "next/navigation"; 4 | 5 | import { createClient } from "~/utils/supabase/server"; 6 | import { Layout } from "./_components/layout"; 7 | 8 | export const metadata: Metadata = { 9 | title: { 10 | default: "Dashboard - Posts Buddy", 11 | template: `%s - Dashboard - Posts Buddy`, 12 | }, 13 | description: "Posts Buddy: A powerful notes sharing website.", 14 | }; 15 | 16 | export default async function DashboardLayout({ 17 | children, 18 | }: Readonly<{ 19 | children: React.ReactNode; 20 | }>) { 21 | const supabase = createClient(); 22 | const user = await supabase.auth.getUser(); 23 | 24 | const layout = cookies().get("react-resizable-panels:layout"); 25 | const collapsed = cookies().get("react-resizable-panels:collapsed"); 26 | 27 | const defaultLayout = layout 28 | ? (JSON.parse(layout.value) as number[]) 29 | : undefined; 30 | const defaultCollapsed = collapsed 31 | ? (JSON.parse(collapsed.value) as boolean) 32 | : false; 33 | 34 | if (user.error ?? !user.data.user) redirect("/auth/signin"); 35 | 36 | return ( 37 | 43 | {children} 44 | 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from "react"; 2 | 3 | import { 4 | Breadcrumb, 5 | BreadcrumbEllipsis, 6 | BreadcrumbItem, 7 | BreadcrumbLink, 8 | BreadcrumbList, 9 | BreadcrumbPage, 10 | BreadcrumbSeparator, 11 | } from "@acme/ui/breadcrumb"; 12 | 13 | import { api, HydrateClient } from "~/trpc/server"; 14 | import { AuthShowcase } from "./_components/auth-showcase"; 15 | import { 16 | CreatePostForm, 17 | PostCardSkeleton, 18 | PostList, 19 | } from "./_components/posts"; 20 | 21 | import { ScrollArea } from "@acme/ui/scroll-area" 22 | 23 | export const runtime = "edge"; 24 | 25 | export default function HomePage() { 26 | // You can await this here if you don't want to show Suspense fallback below 27 | void api.post.all.prefetch(); 28 | 29 | return ( 30 | 31 |
32 | 33 | 34 | 35 | 38 | 39 | 40 | 41 |
42 | } 43 | > 44 | 45 | 46 | 47 |
48 | 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/dashboard/settings/_components/sidebar-nav.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 | import { buttonVariants } from '@acme/ui/button'; 8 | 9 | interface SidebarNavProps extends React.HTMLAttributes { 10 | items: { 11 | href: string; 12 | title: string; 13 | }[]; 14 | } 15 | 16 | export function SidebarNav({ className, items, ...props }: SidebarNavProps) { 17 | const pathname = usePathname(); 18 | 19 | return ( 20 | 43 | ); 44 | } -------------------------------------------------------------------------------- /apps/nextjs/src/app/dashboard/settings/account/page.tsx: -------------------------------------------------------------------------------- 1 | import { Separator } from '@acme/ui/separator'; 2 | import { AccountForm } from '~/app/dashboard/settings/account/account-form'; 3 | 4 | export default function SettingsAccountPage() { 5 | return ( 6 |
7 |
8 |

Account

9 |

10 | Update your account settings. Set your preferred language and 11 | timezone. 12 |

13 |
14 | 15 | 16 |
17 | ); 18 | } -------------------------------------------------------------------------------- /apps/nextjs/src/app/dashboard/settings/appearance/page.tsx: -------------------------------------------------------------------------------- 1 | import { Separator } from '@acme/ui/separator'; 2 | import { AppearanceForm } from '~/app/dashboard/settings/appearance/appearance-form'; 3 | 4 | export default function SettingsAppearancePage() { 5 | return ( 6 |
7 |
8 |

Appearance

9 |

10 | Customize the appearance of the app. Automatically switch between day 11 | and night themes. 12 |

13 |
14 | 15 | 16 |
17 | ); 18 | } -------------------------------------------------------------------------------- /apps/nextjs/src/app/dashboard/settings/display/page.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { Separator } from '@acme/ui/separator'; 3 | import { DisplayForm } from '~/app/dashboard/settings/display/display-form'; 4 | 5 | export default function SettingsDisplayPage() { 6 | return ( 7 |
8 |
9 |

Display

10 |

11 | Turn items on or off to control what's displayed in the app. 12 |

13 |
14 | 15 | 16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/dashboard/settings/layout.tsx: -------------------------------------------------------------------------------- 1 | 2 | import type {Metadata} from 'next'; 3 | 4 | import { Separator } from '@acme/ui/separator'; 5 | import { SidebarNav } from '~/app/dashboard/settings/_components/sidebar-nav'; 6 | 7 | export const metadata: Metadata = { 8 | title: 'Settings', 9 | description: 'Settings for Anikode Studio. Customize your preferences and configure your site settings here.' 10 | }; 11 | 12 | const sidebarNavItems = [ 13 | { 14 | title: 'Profile', 15 | href: '/dashboard/settings/profile' 16 | }, 17 | { 18 | title: 'Account', 19 | href: '/dashboard/settings/account' 20 | }, 21 | { 22 | title: 'Appearance', 23 | href: '/dashboard/settings/appearance' 24 | }, 25 | { 26 | title: 'Notifications', 27 | href: '/dashboard/settings/notifications' 28 | }, 29 | { 30 | title: 'Display', 31 | href: '/dashboard/settings/display' 32 | } 33 | ]; 34 | 35 | interface SettingsLayoutProps { 36 | children: React.ReactNode; 37 | } 38 | 39 | export default function SettingsLayout({ children }: SettingsLayoutProps) { 40 | return ( 41 | <> 42 |
43 |
44 |

Settings

45 |

46 | Manage your account settings and set e-mail preferences. 47 |

48 |
49 | 50 |
51 | {/* remove mx-4 for aside */} 52 | 55 |
{children}
56 |
57 |
58 | 59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/dashboard/settings/notifications/page.tsx: -------------------------------------------------------------------------------- 1 | import { Separator } from '@acme/ui/separator'; 2 | import { NotificationsForm } from '~/app/dashboard/settings/notifications/notifications-form'; 3 | 4 | export default function SettingsNotificationsPage() { 5 | return ( 6 |
7 |
8 |

Notifications

9 |

10 | Configure how you receive notifications. 11 |

12 |
13 | 14 | 15 |
16 | ); 17 | } -------------------------------------------------------------------------------- /apps/nextjs/src/app/dashboard/settings/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from 'next/navigation'; 2 | 3 | export default function SettingsProfilePage() { 4 | redirect('/dashboard/settings/profile'); 5 | 6 | return ( 7 |
8 | This URL has changed. You are being redirected shortly. 9 |
10 | ); 11 | } -------------------------------------------------------------------------------- /apps/nextjs/src/app/dashboard/settings/profile/page.tsx: -------------------------------------------------------------------------------- 1 | import { Separator } from '@acme/ui/separator'; 2 | import { ProfileForm } from '~/app/dashboard/settings/profile/profile-form'; 3 | 4 | export default function SettingsProfilePage() { 5 | return ( 6 |
7 |
8 |

Profile

9 |

10 | This is how others will see you on the site. 11 |

12 |
13 | 14 | 15 |
16 | ); 17 | } -------------------------------------------------------------------------------- /apps/nextjs/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 240 10% 3.9%; 9 | --card: 0 0% 100%; 10 | --card-foreground: 240 10% 3.9%; 11 | --popover: 0 0% 100%; 12 | --popover-foreground: 240 10% 3.9%; 13 | --primary: 327 66% 69%; 14 | --primary-foreground: 337 65.5% 17.1%; 15 | --secondary: 240 4.8% 95.9%; 16 | --secondary-foreground: 240 5.9% 10%; 17 | --muted: 240 4.8% 95.9%; 18 | --muted-foreground: 240 3.8% 46.1%; 19 | --accent: 240 4.8% 95.9%; 20 | --accent-foreground: 240 5.9% 10%; 21 | --destructive: 0 72.22% 50.59%; 22 | --destructive-foreground: 0 0% 98%; 23 | --border: 240 5.9% 90%; 24 | --input: 240 5.9% 90%; 25 | --ring: 240 5% 64.9%; 26 | --radius: 0.5rem; 27 | } 28 | 29 | .dark { 30 | --background: 240 10% 3.9%; 31 | --foreground: 0 0% 98%; 32 | --card: 240 10% 3.9%; 33 | --card-foreground: 0 0% 98%; 34 | --popover: 240 10% 3.9%; 35 | --popover-foreground: 0 0% 98%; 36 | --primary: 327 66% 69%; 37 | --primary-foreground: 337 65.5% 17.1%; 38 | --secondary: 240 3.7% 15.9%; 39 | --secondary-foreground: 0 0% 98%; 40 | --muted: 240 3.7% 15.9%; 41 | --muted-foreground: 240 5% 64.9%; 42 | --accent: 240 3.7% 15.9%; 43 | --accent-foreground: 0 0% 98%; 44 | --destructive: 0 62.8% 30.6%; 45 | --destructive-foreground: 0 85.7% 97.3%; 46 | --border: 240 3.7% 15.9%; 47 | --input: 240 3.7% 15.9%; 48 | --ring: 240 4.9% 83.9%; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata, Viewport } from "next"; 2 | import { GeistMono } from "geist/font/mono"; 3 | import { GeistSans } from "geist/font/sans"; 4 | 5 | import { cn } from "@acme/ui"; 6 | import { Toaster } from "@acme/ui/sonner"; 7 | import { ThemeProvider } from "@acme/ui/theme"; 8 | 9 | import { TRPCReactProvider } from "~/trpc/react"; 10 | 11 | import "~/app/globals.css"; 12 | 13 | import { env } from "~/env"; 14 | 15 | export const metadata: Metadata = { 16 | metadataBase: new URL( 17 | env.VERCEL_ENV === "production" 18 | ? "https://turbo.t3.gg" 19 | : "http://localhost:3000", 20 | ), 21 | title: "T3 Turbo x Supabase", 22 | description: "Simple monorepo with shared backend for web & mobile apps", 23 | openGraph: { 24 | title: "T3 Turbo x Supabase", 25 | description: "Simple monorepo with shared backend for web & mobile apps", 26 | url: "https://github.com/supabase-community/create-t3-turbo", 27 | siteName: "T3 Turbo x Supabase", 28 | }, 29 | twitter: { 30 | card: "summary_large_image", 31 | site: "@jullerino", 32 | creator: "@jullerino", 33 | }, 34 | }; 35 | 36 | export const viewport: Viewport = { 37 | themeColor: [ 38 | { media: "(prefers-color-scheme: light)", color: "white" }, 39 | { media: "(prefers-color-scheme: dark)", color: "black" }, 40 | ], 41 | }; 42 | 43 | export default function RootLayout(props: { children: React.ReactNode }) { 44 | return ( 45 | 46 | 53 | 54 | {props.children} 55 | {/*
56 | 57 |
*/} 58 | 59 |
60 | 61 | 62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { FAQ, Features, Footer, Header, Hero } from "./_components/landing"; 2 | import SmoothScroll from "./_components/smooth-scroll"; 3 | 4 | export default function HomePage() { 5 | return ( 6 | 7 |
8 |
9 |
10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 | ); 18 | } -------------------------------------------------------------------------------- /apps/nextjs/src/config/routes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents an array of routes that are publicly accessible. 3 | * Users do not need to be logged in to access these routes. 4 | * @type {string[]} publicRoutes - The public routes. 5 | */ 6 | export const publicRoutes = ["/"]; 7 | 8 | /** 9 | * Represents an array of routes that require authentication. 10 | * Users must be logged in to access these routes. 11 | * @type {string[]} protectedRoutes - The protected routes. 12 | */ 13 | export const protectedRoutes = ["/dashboard"]; 14 | 15 | /** 16 | * Represents an array of routes used for authentication. 17 | * Logged in users will be redirected to DEFAULT_LOGIN_REDIRECT when accessing these routes. 18 | * @type {string[]} authRoutes - The authentication routes. 19 | */ 20 | export const authRoutes = [ 21 | "/auth/signin", 22 | "/auth/signup", 23 | "/auth/callback", 24 | "/auth/confirm", 25 | ]; 26 | 27 | /** 28 | * The default path to which users are redirected after logging in. 29 | * @type {string} DEFAULT_LOGIN_REDIRECT - The default login redirect path. 30 | */ 31 | export const DEFAULT_LOGIN_REDIRECT = "/dashboard"; 32 | 33 | /** 34 | * The default path to which users are redirected to authenticate. 35 | * @type {string} DEFAULT_AUTH_ROUTE - The default auth route. 36 | */ 37 | export const DEFAULT_AUTH_ROUTE = "/auth/signin"; 38 | -------------------------------------------------------------------------------- /apps/nextjs/src/env.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-properties */ 2 | import { createEnv } from "@t3-oss/env-nextjs"; 3 | import { vercel } from "@t3-oss/env-nextjs/presets"; 4 | import { z } from "zod"; 5 | 6 | export const env = createEnv({ 7 | extends: [vercel()], 8 | shared: { 9 | NODE_ENV: z 10 | .enum(["development", "test", "production"]) 11 | .default("development"), 12 | }, 13 | /** 14 | * Specify your server-side environment variables schema here. This way you can ensure the app isn't 15 | * built with invalid env vars. 16 | */ 17 | server: { 18 | POSTGRES_URL: z.string().url(), 19 | }, 20 | /** 21 | * Specify your client-side environment variables schema here. 22 | * For them to be exposed to the client, prefix them with `NEXT_PUBLIC_`. 23 | */ 24 | client: { 25 | NEXT_PUBLIC_SUPABASE_URL: z.string().url(), 26 | NEXT_PUBLIC_SUPABASE_ANON_KEY: z.string().min(1), 27 | }, 28 | /** 29 | * Destructure all variables from `process.env` to make sure they aren't tree-shaken away. 30 | */ 31 | experimental__runtimeEnv: { 32 | NODE_ENV: process.env.NODE_ENV, 33 | NEXT_PUBLIC_SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL, 34 | NEXT_PUBLIC_SUPABASE_ANON_KEY: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY, 35 | }, 36 | skipValidation: 37 | !!process.env.CI || 38 | !!process.env.SKIP_ENV_VALIDATION || 39 | process.env.npm_lifecycle_event === "lint", 40 | }); 41 | -------------------------------------------------------------------------------- /apps/nextjs/src/lib/lenis/index.ts: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | export * from 'lenis/react'; -------------------------------------------------------------------------------- /apps/nextjs/src/lib/safe-action.ts: -------------------------------------------------------------------------------- 1 | import { createSafeActionClient } from "next-safe-action"; 2 | 3 | import { createClient } from "~/utils/supabase/server"; 4 | 5 | export const action = createSafeActionClient(); 6 | 7 | export const authAction = createSafeActionClient({ 8 | async middleware() { 9 | const supabase = createClient(); 10 | const { data, error } = await supabase.auth.getUser(); 11 | 12 | if (error ?? !data.user) { 13 | throw new Error("Unauthorized"); 14 | } 15 | 16 | return { user: data.user }; 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /apps/nextjs/src/middleware.ts: -------------------------------------------------------------------------------- 1 | import type { NextRequest } from "next/server"; 2 | 3 | import { updateSession } from "~/utils/supabase/middleware"; 4 | 5 | export async function middleware(request: NextRequest) { 6 | return await updateSession(request); 7 | } 8 | 9 | // Read more: https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher 10 | // Clerk matcher: https://clerk.com/docs/references/nextjs/auth-middleware 11 | export const config = { 12 | matcher: ["/((?!.+\\.[\\w]+$|_next).*)", "/", "/(api|trpc)(.*)"], 13 | }; 14 | -------------------------------------------------------------------------------- /apps/nextjs/src/trpc/query-client.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defaultShouldDehydrateQuery, 3 | QueryClient, 4 | } from "@tanstack/react-query"; 5 | import SuperJSON from "superjson"; 6 | 7 | export const createQueryClient = () => 8 | new QueryClient({ 9 | defaultOptions: { 10 | queries: { 11 | // With SSR, we usually want to set some default staleTime 12 | // above 0 to avoid refetching immediately on the client 13 | staleTime: 30 * 1000, 14 | }, 15 | dehydrate: { 16 | serializeData: SuperJSON.serialize, 17 | shouldDehydrateQuery: (query) => 18 | defaultShouldDehydrateQuery(query) || 19 | query.state.status === "pending", 20 | }, 21 | hydrate: { 22 | deserializeData: SuperJSON.deserialize, 23 | }, 24 | }, 25 | }); 26 | -------------------------------------------------------------------------------- /apps/nextjs/src/trpc/react.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import type { QueryClient } from "@tanstack/react-query"; 4 | import { useState } from "react"; 5 | import { QueryClientProvider } from "@tanstack/react-query"; 6 | import { loggerLink, unstable_httpBatchStreamLink } from "@trpc/client"; 7 | import { createTRPCReact } from "@trpc/react-query"; 8 | import SuperJSON from "superjson"; 9 | 10 | import type { AppRouter } from "@acme/api"; 11 | 12 | import { env } from "~/env"; 13 | import { createQueryClient } from "./query-client"; 14 | 15 | let clientQueryClientSingleton: QueryClient | undefined = undefined; 16 | const getQueryClient = () => { 17 | if (typeof window === "undefined") { 18 | // Server: always make a new query client 19 | return createQueryClient(); 20 | } else { 21 | // Browser: use singleton pattern to keep the same query client 22 | return (clientQueryClientSingleton ??= createQueryClient()); 23 | } 24 | }; 25 | 26 | export const api = createTRPCReact(); 27 | 28 | export function TRPCReactProvider(props: { children: React.ReactNode }) { 29 | const queryClient = getQueryClient(); 30 | 31 | const [trpcClient] = useState(() => 32 | api.createClient({ 33 | links: [ 34 | loggerLink({ 35 | enabled: (op) => 36 | env.NODE_ENV === "development" || 37 | (op.direction === "down" && op.result instanceof Error), 38 | }), 39 | unstable_httpBatchStreamLink({ 40 | transformer: SuperJSON, 41 | url: getBaseUrl() + "/api/trpc", 42 | headers() { 43 | const headers = new Headers(); 44 | headers.set("x-trpc-source", "nextjs-react"); 45 | return headers; 46 | }, 47 | }), 48 | ], 49 | }), 50 | ); 51 | 52 | return ( 53 | 54 | 55 | {props.children} 56 | 57 | 58 | ); 59 | } 60 | 61 | const getBaseUrl = () => { 62 | if (typeof window !== "undefined") return window.location.origin; 63 | if (env.VERCEL_URL) return `https://${env.VERCEL_URL}`; 64 | // eslint-disable-next-line no-restricted-properties 65 | return `http://localhost:${process.env.PORT ?? 3000}`; 66 | }; 67 | -------------------------------------------------------------------------------- /apps/nextjs/src/trpc/server.ts: -------------------------------------------------------------------------------- 1 | import { cache } from "react"; 2 | import { headers } from "next/headers"; 3 | import { createHydrationHelpers } from "@trpc/react-query/rsc"; 4 | 5 | import type { AppRouter } from "@acme/api"; 6 | import { createCaller, createTRPCContext } from "@acme/api"; 7 | import { createClient } from "~/utils/supabase/server"; 8 | 9 | import { createQueryClient } from "./query-client"; 10 | 11 | /** 12 | * This wraps the `createTRPCContext` helper and provides the required context for the tRPC API when 13 | * handling a tRPC call from a React Server Component. 14 | */ 15 | const createContext = cache(async () => { 16 | const heads = new Headers(headers()); 17 | heads.set("x-trpc-source", "rsc"); 18 | 19 | const supabase = createClient(); 20 | 21 | return createTRPCContext({ 22 | supabase, 23 | headers: heads, 24 | }); 25 | }); 26 | 27 | const getQueryClient = cache(createQueryClient); 28 | const caller = createCaller(createContext); 29 | 30 | export const { trpc: api, HydrateClient } = createHydrationHelpers( 31 | caller, 32 | getQueryClient, 33 | ); 34 | -------------------------------------------------------------------------------- /apps/nextjs/src/utils/supabase/client.ts: -------------------------------------------------------------------------------- 1 | import { createBrowserClient } from "@supabase/ssr"; 2 | 3 | import { env } from "~/env"; 4 | 5 | export function createClient() { 6 | return createBrowserClient( 7 | env.NEXT_PUBLIC_SUPABASE_URL, 8 | env.NEXT_PUBLIC_SUPABASE_ANON_KEY, 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /apps/nextjs/src/utils/supabase/middleware.ts: -------------------------------------------------------------------------------- 1 | import type { CookieOptions } from "@supabase/ssr"; 2 | import type { NextRequest } from "next/server"; 3 | import { NextResponse } from "next/server"; 4 | import { createServerClient } from "@supabase/ssr"; 5 | 6 | import { 7 | authRoutes, 8 | DEFAULT_AUTH_ROUTE, 9 | DEFAULT_LOGIN_REDIRECT, 10 | protectedRoutes, 11 | } from "~/config/routes"; 12 | import { env } from "~/env"; 13 | 14 | export async function updateSession(request: NextRequest) { 15 | let response = NextResponse.next({ 16 | request: { 17 | headers: request.headers, 18 | }, 19 | }); 20 | 21 | const supabase = createServerClient( 22 | env.NEXT_PUBLIC_SUPABASE_URL, 23 | env.NEXT_PUBLIC_SUPABASE_ANON_KEY, 24 | { 25 | cookies: { 26 | get(name: string) { 27 | return request.cookies.get(name)?.value; 28 | }, 29 | set(name: string, value: string, options: CookieOptions) { 30 | request.cookies.set({ 31 | name, 32 | value, 33 | ...options, 34 | }); 35 | response = NextResponse.next({ 36 | request: { 37 | headers: request.headers, 38 | }, 39 | }); 40 | response.cookies.set({ 41 | name, 42 | value, 43 | ...options, 44 | }); 45 | }, 46 | remove(name: string, options: CookieOptions) { 47 | request.cookies.set({ 48 | name, 49 | value: "", 50 | ...options, 51 | }); 52 | response = NextResponse.next({ 53 | request: { 54 | headers: request.headers, 55 | }, 56 | }); 57 | response.cookies.set({ 58 | name, 59 | value: "", 60 | ...options, 61 | }); 62 | }, 63 | }, 64 | }, 65 | ); 66 | 67 | // Get user 68 | const { data, error } = await supabase.auth.getUser(); 69 | 70 | // If protected route and user is not authenticated, redirect to login 71 | const isProtectedRoute = protectedRoutes.includes(request.nextUrl.pathname); 72 | 73 | if (isProtectedRoute && (error ?? !data.user)) { 74 | const url = new URL(DEFAULT_AUTH_ROUTE, request.url); 75 | return NextResponse.redirect(url); 76 | } 77 | 78 | // Forward authed user to DEFAULT_LOGIN_REDIRECT if auth route 79 | const isAuthRoute = authRoutes.includes(request.nextUrl.pathname); 80 | 81 | if (isAuthRoute && data.user) { 82 | const url = new URL(DEFAULT_LOGIN_REDIRECT, request.url); 83 | return NextResponse.redirect(url); 84 | } 85 | 86 | // Proceed as normal 87 | return response; 88 | } 89 | -------------------------------------------------------------------------------- /apps/nextjs/src/utils/supabase/server.ts: -------------------------------------------------------------------------------- 1 | import type { CookieOptions } from "@supabase/ssr"; 2 | import { cookies } from "next/headers"; 3 | import { createServerClient } from "@supabase/ssr"; 4 | 5 | import { env } from "~/env"; 6 | 7 | export function createClient() { 8 | const cookieStore = cookies(); 9 | 10 | return createServerClient( 11 | env.NEXT_PUBLIC_SUPABASE_URL, 12 | env.NEXT_PUBLIC_SUPABASE_ANON_KEY, 13 | { 14 | cookies: { 15 | get(name: string) { 16 | return cookieStore.get(name)?.value; 17 | }, 18 | set(name: string, value: string, options: CookieOptions) { 19 | try { 20 | cookieStore.set({ name, value, ...options }); 21 | } catch (error) { 22 | // The `set` method was called from a Server Component. 23 | // This can be ignored if you have middleware refreshing 24 | // user sessions. 25 | } 26 | }, 27 | remove(name: string, options: CookieOptions) { 28 | try { 29 | cookieStore.set({ name, value: "", ...options }); 30 | } catch (error) { 31 | // The `delete` method was called from a Server Component. 32 | // This can be ignored if you have middleware refreshing 33 | // user sessions. 34 | } 35 | }, 36 | }, 37 | }, 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /apps/nextjs/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | import { fontFamily } from "tailwindcss/defaultTheme"; 3 | 4 | import baseConfig from "@acme/tailwind-config/web"; 5 | 6 | export default { 7 | // We need to append the path to the UI package to the content array so that 8 | // those classes are included correctly. 9 | content: [...baseConfig.content, "../../packages/ui/**/*.{ts,tsx}"], 10 | presets: [baseConfig], 11 | theme: { 12 | extend: { 13 | fontFamily: { 14 | sans: ["var(--font-geist-sans)", ...fontFamily.sans], 15 | mono: ["var(--font-geist-mono)", ...fontFamily.mono], 16 | }, 17 | }, 18 | }, 19 | } satisfies Config; 20 | -------------------------------------------------------------------------------- /apps/nextjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@acme/tsconfig/base.json", 3 | "compilerOptions": { 4 | "lib": ["es2022", "dom", "dom.iterable"], 5 | "jsx": "preserve", 6 | "baseUrl": ".", 7 | "paths": { 8 | "~/*": ["./src/*"] 9 | }, 10 | "plugins": [{ "name": "next" }], 11 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json", 12 | "module": "esnext" 13 | }, 14 | "include": [".", ".next/types/**/*.ts"], 15 | "exclude": ["node_modules"] 16 | } 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-t3-turbo", 3 | "private": true, 4 | "engines": { 5 | "node": ">=20.12.0" 6 | }, 7 | "packageManager": "pnpm@9.2.0", 8 | "scripts": { 9 | "build": "turbo run build", 10 | "clean": "git clean -xdf node_modules", 11 | "clean:workspaces": "turbo run clean", 12 | "db:push": "turbo -F @acme/db push", 13 | "db:studio": "turbo -F @acme/db studio", 14 | "dev": "turbo watch dev", 15 | "dev:next": "turbo watch dev -F @acme/nextjs...", 16 | "format": "turbo run format --continue -- --cache --cache-location node_modules/.cache/.prettiercache", 17 | "format:fix": "turbo run format --continue -- --write --cache --cache-location node_modules/.cache/.prettiercache", 18 | "lint": "turbo run lint --continue -- --cache --cache-location node_modules/.cache/.eslintcache", 19 | "lint:fix": "turbo run lint --continue -- --fix --cache --cache-location node_modules/.cache/.eslintcache", 20 | "lint:ws": "pnpm dlx sherif@latest", 21 | "postinstall": "pnpm lint:ws", 22 | "typecheck": "turbo run typecheck", 23 | "ui-add": "turbo run ui-add" 24 | }, 25 | "devDependencies": { 26 | "@acme/prettier-config": "workspace:*", 27 | "@turbo/gen": "^2.0.6", 28 | "prettier": "^3.3.2", 29 | "turbo": "^2.0.6", 30 | "typescript": "^5.5.3" 31 | }, 32 | "prettier": "@acme/prettier-config" 33 | } 34 | -------------------------------------------------------------------------------- /packages/api/eslint.config.js: -------------------------------------------------------------------------------- 1 | import baseConfig from "@acme/eslint-config/base"; 2 | 3 | /** @type {import('typescript-eslint').Config} */ 4 | export default [ 5 | { 6 | ignores: ["dist/**"], 7 | }, 8 | ...baseConfig, 9 | ]; 10 | -------------------------------------------------------------------------------- /packages/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@acme/api", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "exports": { 7 | ".": { 8 | "types": "./dist/index.d.ts", 9 | "default": "./src/index.ts" 10 | } 11 | }, 12 | "license": "MIT", 13 | "scripts": { 14 | "build": "tsc", 15 | "dev": "tsc --watch", 16 | "clean": "rm -rf .turbo node_modules", 17 | "format": "prettier --check . --ignore-path ../../.gitignore", 18 | "lint": "eslint", 19 | "typecheck": "tsc --noEmit --emitDeclarationOnly false" 20 | }, 21 | "dependencies": { 22 | "@acme/db": "workspace:*", 23 | "@acme/validators": "workspace:*", 24 | "@supabase/supabase-js": "^2.43.1", 25 | "@trpc/server": "11.0.0-rc.441", 26 | "nanoid": "^5.0.7", 27 | "superjson": "2.2.1", 28 | "zod": "^3.23.8" 29 | }, 30 | "devDependencies": { 31 | "@acme/eslint-config": "workspace:*", 32 | "@acme/prettier-config": "workspace:*", 33 | "@acme/tsconfig": "workspace:*", 34 | "eslint": "^9.6.0", 35 | "prettier": "^3.3.2", 36 | "typescript": "^5.5.3" 37 | }, 38 | "prettier": "@acme/prettier-config" 39 | } 40 | -------------------------------------------------------------------------------- /packages/api/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server"; 2 | 3 | import type { AppRouter } from "./root"; 4 | import { appRouter } from "./root"; 5 | import { createCallerFactory, createTRPCContext } from "./trpc"; 6 | 7 | /** 8 | * Create a server-side caller for the tRPC API 9 | * @example 10 | * const trpc = createCaller(createContext); 11 | * const res = await trpc.Post.all(); 12 | * ^? Post[] 13 | */ 14 | const createCaller = createCallerFactory(appRouter); 15 | 16 | /** 17 | * Inference helpers for input types 18 | * @example 19 | * type PostByIdInput = RouterInputs['post']['byId'] 20 | * ^? { id: number } 21 | **/ 22 | type RouterInputs = inferRouterInputs; 23 | 24 | /** 25 | * Inference helpers for output types 26 | * @example 27 | * type AllPostsOutput = RouterOutputs['post']['all'] 28 | * ^? Post[] 29 | **/ 30 | type RouterOutputs = inferRouterOutputs; 31 | 32 | export { createTRPCContext, appRouter, createCaller }; 33 | export type { AppRouter, RouterInputs, RouterOutputs }; 34 | -------------------------------------------------------------------------------- /packages/api/src/root.ts: -------------------------------------------------------------------------------- 1 | import { authRouter } from "./router/auth"; 2 | import { postRouter } from "./router/post"; 3 | import { createTRPCRouter } from "./trpc"; 4 | 5 | export const appRouter = createTRPCRouter({ 6 | auth: authRouter, 7 | post: postRouter, 8 | }); 9 | 10 | // export type definition of API 11 | export type AppRouter = typeof appRouter; 12 | -------------------------------------------------------------------------------- /packages/api/src/router/auth.ts: -------------------------------------------------------------------------------- 1 | import type { TRPCRouterRecord } from "@trpc/server"; 2 | 3 | import { protectedProcedure, publicProcedure } from "../trpc"; 4 | 5 | export const authRouter = { 6 | me: publicProcedure.query(({ ctx }) => { 7 | return ctx.user; 8 | }), 9 | getSecretMessage: protectedProcedure.query(() => { 10 | return "you can see this secret message!"; 11 | }), 12 | } satisfies TRPCRouterRecord; 13 | -------------------------------------------------------------------------------- /packages/api/src/router/post.ts: -------------------------------------------------------------------------------- 1 | import type { TRPCRouterRecord } from "@trpc/server"; 2 | import { TRPCError } from "@trpc/server"; 3 | import { z } from "zod"; 4 | 5 | import { desc, eq } from "@acme/db"; 6 | import { CreatePostSchema, Post } from "@acme/db/schema"; 7 | 8 | import { protectedProcedure, publicProcedure } from "../trpc"; 9 | 10 | export const postRouter = { 11 | all: publicProcedure.query(({ ctx }) => { 12 | return ctx.db.query.Post.findMany({ 13 | with: { author: true }, 14 | orderBy: desc(Post.id), 15 | limit: 10, 16 | }); 17 | }), 18 | 19 | byId: publicProcedure 20 | .input(z.object({ id: z.string() })) 21 | .query(({ ctx, input }) => { 22 | return ctx.db.query.Post.findFirst({ 23 | with: { author: true }, 24 | where: eq(Post.id, input.id), 25 | }); 26 | }), 27 | 28 | create: protectedProcedure 29 | .input(CreatePostSchema) 30 | .mutation(async ({ ctx, input }) => { 31 | const { title, content } = input; 32 | const userId = ctx.user.id; 33 | 34 | return ctx.db.insert(Post).values({ 35 | authorId: userId, 36 | title, 37 | content, 38 | }); 39 | }), 40 | 41 | delete: protectedProcedure 42 | .input(z.string()) 43 | .mutation(async ({ ctx, input }) => { 44 | const data = await ctx.db.query.Post.findFirst({ 45 | where: eq(Post.id, input), 46 | }); 47 | 48 | if (data?.authorId !== ctx.user.id) { 49 | throw new TRPCError({ 50 | code: "UNAUTHORIZED", 51 | message: "Only the author is allowed to delete the post", 52 | }); 53 | } 54 | 55 | return ctx.db.delete(Post).where(eq(Post.id, input)); 56 | }), 57 | } satisfies TRPCRouterRecord; 58 | -------------------------------------------------------------------------------- /packages/api/src/trpc.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS: 3 | * 1. You want to modify request context (see Part 1) 4 | * 2. You want to create a new middleware or type of procedure (see Part 3) 5 | * 6 | * tl;dr - this is where all the tRPC server stuff is created and plugged in. 7 | * The pieces you will need to use are documented accordingly near the end 8 | */ 9 | import type { SupabaseClient } from "@supabase/supabase-js"; 10 | import { initTRPC, TRPCError } from "@trpc/server"; 11 | import superjson from "superjson"; 12 | import { ZodError } from "zod"; 13 | 14 | import { db } from "@acme/db/client"; 15 | 16 | /** 17 | * 1. CONTEXT 18 | * 19 | * This section defines the "contexts" that are available in the backend API. 20 | * 21 | * These allow you to access things when processing a request, like the database, the session, etc. 22 | * 23 | * This helper generates the "internals" for a tRPC context. The API handler and RSC clients each 24 | * wrap this and provides the required context. 25 | * 26 | * @see https://trpc.io/docs/server/context 27 | */ 28 | export const createTRPCContext = async (opts: { 29 | headers: Headers; 30 | supabase: SupabaseClient; 31 | }) => { 32 | const supabase = opts.supabase; 33 | 34 | // React Native will pass their token through headers, 35 | // browsers will have the session cookie set 36 | const token = opts.headers.get("authorization"); 37 | 38 | const user = token 39 | ? await supabase.auth.getUser(token) 40 | : await supabase.auth.getUser(); 41 | 42 | const source = opts.headers.get("x-trpc-source") ?? "unknown"; 43 | console.log(">>> tRPC Request from", source, "by", user.data.user?.email); 44 | 45 | return { 46 | user: user.data.user, 47 | db, 48 | }; 49 | }; 50 | 51 | /** 52 | * 2. INITIALIZATION 53 | * 54 | * This is where the trpc api is initialized, connecting the context and 55 | * transformer 56 | */ 57 | const t = initTRPC.context().create({ 58 | transformer: superjson, 59 | errorFormatter: ({ shape, error }) => ({ 60 | ...shape, 61 | data: { 62 | ...shape.data, 63 | zodError: error.cause instanceof ZodError ? error.cause.flatten() : null, 64 | }, 65 | }), 66 | }); 67 | 68 | /** 69 | * Create a server-side caller 70 | * @see https://trpc.io/docs/server/server-side-calls 71 | */ 72 | export const createCallerFactory = t.createCallerFactory; 73 | 74 | /** 75 | * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) 76 | * 77 | * These are the pieces you use to build your tRPC API. You should import these 78 | * a lot in the /src/server/api/routers folder 79 | */ 80 | 81 | /** 82 | * This is how you create new routers and subrouters in your tRPC API 83 | * @see https://trpc.io/docs/router 84 | */ 85 | export const createTRPCRouter = t.router; 86 | 87 | /** 88 | * Public (unauthed) procedure 89 | * 90 | * This is the base piece you use to build new queries and mutations on your 91 | * tRPC API. It does not guarantee that a user querying is authorized, but you 92 | * can still access user session data if they are logged in 93 | */ 94 | export const publicProcedure = t.procedure; 95 | 96 | /** 97 | * Protected (authenticated) procedure 98 | * 99 | * If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies 100 | * the session is valid and guarantees `ctx.session.user` is not null. 101 | * 102 | * @see https://trpc.io/docs/procedures 103 | */ 104 | export const protectedProcedure = t.procedure.use(({ ctx, next }) => { 105 | if (!ctx.user?.id) { 106 | throw new TRPCError({ code: "UNAUTHORIZED" }); 107 | } 108 | return next({ 109 | ctx: { 110 | // infers the `user` as non-nullable 111 | user: ctx.user, 112 | }, 113 | }); 114 | }); 115 | -------------------------------------------------------------------------------- /packages/api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@acme/tsconfig/internal-package.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" 6 | }, 7 | "include": ["src"], 8 | "exclude": ["node_modules"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/db/drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "drizzle-kit"; 2 | 3 | import { env } from "./src/client"; 4 | 5 | const nonPoolingUrl = env.POSTGRES_URL.replace(":6543", ":5432"); 6 | 7 | export default { 8 | schema: "./src/schema", 9 | schemaFilter: ["public"], 10 | out: "./migrations", 11 | dialect: "postgresql", 12 | dbCredentials: { url: nonPoolingUrl }, 13 | tablesFilter: ["t3turbo_*"], 14 | } satisfies Config; 15 | -------------------------------------------------------------------------------- /packages/db/eslint.config.js: -------------------------------------------------------------------------------- 1 | import baseConfig, { restrictEnvAccess } from "@acme/eslint-config/base"; 2 | 3 | /** @type {import('typescript-eslint').Config} */ 4 | export default [ 5 | { 6 | ignores: ["dist/**"], 7 | }, 8 | ...baseConfig, 9 | ...restrictEnvAccess, 10 | ]; 11 | -------------------------------------------------------------------------------- /packages/db/migrations/0000_right_karnak.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS "auth"."users" ( 2 | "id" uuid PRIMARY KEY NOT NULL 3 | ); 4 | --> statement-breakpoint 5 | CREATE TABLE IF NOT EXISTS "t3turbo_profile" ( 6 | "id" uuid PRIMARY KEY NOT NULL, 7 | "name" varchar(256) NOT NULL, 8 | "image" varchar(256), 9 | "email" varchar(256) 10 | ); 11 | --> statement-breakpoint 12 | CREATE TABLE IF NOT EXISTS "t3turbo_post" ( 13 | "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, 14 | "name" varchar(256) NOT NULL, 15 | "content" text NOT NULL, 16 | "author_id" uuid NOT NULL, 17 | "created_at" timestamp DEFAULT now() NOT NULL, 18 | "updatedAt" timestamp with time zone 19 | ); 20 | --> statement-breakpoint 21 | DO $$ BEGIN 22 | ALTER TABLE "t3turbo_profile" ADD CONSTRAINT "t3turbo_profile_id_users_id_fk" FOREIGN KEY ("id") REFERENCES "auth"."users"("id") ON DELETE cascade ON UPDATE no action; 23 | EXCEPTION 24 | WHEN duplicate_object THEN null; 25 | END $$; 26 | --> statement-breakpoint 27 | DO $$ BEGIN 28 | ALTER TABLE "t3turbo_post" ADD CONSTRAINT "t3turbo_post_author_id_t3turbo_profile_id_fk" FOREIGN KEY ("author_id") REFERENCES "public"."t3turbo_profile"("id") ON DELETE no action ON UPDATE no action; 29 | EXCEPTION 30 | WHEN duplicate_object THEN null; 31 | END $$; 32 | -------------------------------------------------------------------------------- /packages/db/migrations/meta/0000_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "cdc5fe6f-b85f-4925-84bb-e550ff6f1f2a", 3 | "prevId": "00000000-0000-0000-0000-000000000000", 4 | "version": "6", 5 | "dialect": "postgresql", 6 | "tables": { 7 | "auth.users": { 8 | "name": "users", 9 | "schema": "auth", 10 | "columns": { 11 | "id": { 12 | "name": "id", 13 | "type": "uuid", 14 | "primaryKey": true, 15 | "notNull": true 16 | } 17 | }, 18 | "indexes": {}, 19 | "foreignKeys": {}, 20 | "compositePrimaryKeys": {}, 21 | "uniqueConstraints": {} 22 | }, 23 | "public.t3turbo_profile": { 24 | "name": "t3turbo_profile", 25 | "schema": "", 26 | "columns": { 27 | "id": { 28 | "name": "id", 29 | "type": "uuid", 30 | "primaryKey": true, 31 | "notNull": true 32 | }, 33 | "name": { 34 | "name": "name", 35 | "type": "varchar(256)", 36 | "primaryKey": false, 37 | "notNull": true 38 | }, 39 | "image": { 40 | "name": "image", 41 | "type": "varchar(256)", 42 | "primaryKey": false, 43 | "notNull": false 44 | }, 45 | "email": { 46 | "name": "email", 47 | "type": "varchar(256)", 48 | "primaryKey": false, 49 | "notNull": false 50 | } 51 | }, 52 | "indexes": {}, 53 | "foreignKeys": { 54 | "t3turbo_profile_id_users_id_fk": { 55 | "name": "t3turbo_profile_id_users_id_fk", 56 | "tableFrom": "t3turbo_profile", 57 | "tableTo": "users", 58 | "schemaTo": "auth", 59 | "columnsFrom": ["id"], 60 | "columnsTo": ["id"], 61 | "onDelete": "cascade", 62 | "onUpdate": "no action" 63 | } 64 | }, 65 | "compositePrimaryKeys": {}, 66 | "uniqueConstraints": {} 67 | }, 68 | "public.t3turbo_post": { 69 | "name": "t3turbo_post", 70 | "schema": "", 71 | "columns": { 72 | "id": { 73 | "name": "id", 74 | "type": "uuid", 75 | "primaryKey": true, 76 | "notNull": true, 77 | "default": "gen_random_uuid()" 78 | }, 79 | "name": { 80 | "name": "name", 81 | "type": "varchar(256)", 82 | "primaryKey": false, 83 | "notNull": true 84 | }, 85 | "content": { 86 | "name": "content", 87 | "type": "text", 88 | "primaryKey": false, 89 | "notNull": true 90 | }, 91 | "author_id": { 92 | "name": "author_id", 93 | "type": "uuid", 94 | "primaryKey": false, 95 | "notNull": true 96 | }, 97 | "created_at": { 98 | "name": "created_at", 99 | "type": "timestamp", 100 | "primaryKey": false, 101 | "notNull": true, 102 | "default": "now()" 103 | }, 104 | "updatedAt": { 105 | "name": "updatedAt", 106 | "type": "timestamp with time zone", 107 | "primaryKey": false, 108 | "notNull": false 109 | } 110 | }, 111 | "indexes": {}, 112 | "foreignKeys": { 113 | "t3turbo_post_author_id_t3turbo_profile_id_fk": { 114 | "name": "t3turbo_post_author_id_t3turbo_profile_id_fk", 115 | "tableFrom": "t3turbo_post", 116 | "tableTo": "t3turbo_profile", 117 | "columnsFrom": ["author_id"], 118 | "columnsTo": ["id"], 119 | "onDelete": "no action", 120 | "onUpdate": "no action" 121 | } 122 | }, 123 | "compositePrimaryKeys": {}, 124 | "uniqueConstraints": {} 125 | } 126 | }, 127 | "enums": {}, 128 | "schemas": {}, 129 | "_meta": { 130 | "columns": {}, 131 | "schemas": {}, 132 | "tables": {} 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /packages/db/migrations/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "6", 3 | "dialect": "postgresql", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "6", 8 | "when": 1715583558178, 9 | "tag": "0000_right_karnak", 10 | "breakpoints": true 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /packages/db/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@acme/db", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "exports": { 7 | ".": { 8 | "types": "./dist/index.d.ts", 9 | "default": "./src/index.ts" 10 | }, 11 | "./client": { 12 | "types": "./dist/client.d.ts", 13 | "default": "./src/client.ts" 14 | }, 15 | "./schema": { 16 | "types": "./dist/schema/index.d.ts", 17 | "default": "./src/schema/index.ts" 18 | } 19 | }, 20 | "license": "MIT", 21 | "scripts": { 22 | "build": "tsc", 23 | "dev": "tsc --watch", 24 | "clean": "rm -rf .turbo node_modules", 25 | "format": "prettier --check . --ignore-path ../../.gitignore", 26 | "lint": "eslint", 27 | "push": "pnpm with-env drizzle-kit push", 28 | "studio": "pnpm with-env drizzle-kit studio", 29 | "typecheck": "tsc --noEmit --emitDeclarationOnly false", 30 | "with-env": "dotenv -e ../../.env --" 31 | }, 32 | "dependencies": { 33 | "@t3-oss/env-core": "^0.10.1", 34 | "@vercel/postgres": "^0.9.0", 35 | "drizzle-orm": "^0.31.2", 36 | "drizzle-zod": "^0.5.1", 37 | "zod": "^3.23.8" 38 | }, 39 | "devDependencies": { 40 | "@acme/eslint-config": "workspace:*", 41 | "@acme/prettier-config": "workspace:*", 42 | "@acme/tsconfig": "workspace:*", 43 | "dotenv-cli": "^7.4.2", 44 | "drizzle-kit": "^0.22.8", 45 | "eslint": "^9.6.0", 46 | "prettier": "^3.3.2", 47 | "typescript": "^5.5.3" 48 | }, 49 | "prettier": "@acme/prettier-config" 50 | } 51 | -------------------------------------------------------------------------------- /packages/db/src/client.ts: -------------------------------------------------------------------------------- 1 | import { createEnv } from "@t3-oss/env-core"; 2 | import { sql } from "@vercel/postgres"; 3 | import { drizzle } from "drizzle-orm/vercel-postgres"; 4 | import { z } from "zod"; 5 | 6 | import * as schema from "./schema"; 7 | 8 | export const env = createEnv({ 9 | server: { 10 | POSTGRES_URL: z.string().url(), 11 | }, 12 | // eslint-disable-next-line no-restricted-properties 13 | runtimeEnv: process.env, 14 | emptyStringAsUndefined: true, 15 | }); 16 | 17 | export const db = drizzle(sql, { schema }); 18 | -------------------------------------------------------------------------------- /packages/db/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "drizzle-orm/sql"; 2 | export { alias } from "drizzle-orm/pg-core"; 3 | -------------------------------------------------------------------------------- /packages/db/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | export const timestamps: { createdAt: true; updatedAt: true } = { 2 | createdAt: true, 3 | updatedAt: true, 4 | }; 5 | -------------------------------------------------------------------------------- /packages/db/src/migrate.ts: -------------------------------------------------------------------------------- 1 | import { sql } from "@vercel/postgres"; 2 | import { drizzle } from "drizzle-orm/vercel-postgres"; 3 | import { migrate } from "drizzle-orm/vercel-postgres/migrator"; 4 | 5 | import { env } from "./client"; 6 | 7 | const runMigrate = async () => { 8 | if (!env.POSTGRES_URL) { 9 | throw new Error("POSTGRES_URL is not defined"); 10 | } 11 | 12 | const db = drizzle(sql); 13 | 14 | console.log("⏳ Running migrations..."); 15 | 16 | const start = Date.now(); 17 | 18 | await migrate(db, { migrationsFolder: "migrations" }); 19 | 20 | const end = Date.now(); 21 | 22 | console.log("✅ Migrations completed in", end - start, "ms"); 23 | 24 | process.exit(0); 25 | }; 26 | 27 | runMigrate().catch((err) => { 28 | console.error("❌ Migration failed"); 29 | console.error(err); 30 | process.exit(1); 31 | }); 32 | -------------------------------------------------------------------------------- /packages/db/src/schema/_table.ts: -------------------------------------------------------------------------------- 1 | import { pgTableCreator } from "drizzle-orm/pg-core"; 2 | 3 | /** 4 | * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same 5 | * database instance for multiple projects. 6 | * 7 | * @see https://orm.drizzle.team/docs/goodies#multi-project-schema 8 | */ 9 | export const createTable = pgTableCreator((name) => `t3turbo_${name}`); 10 | -------------------------------------------------------------------------------- /packages/db/src/schema/auth.ts: -------------------------------------------------------------------------------- 1 | import { pgSchema, uuid } from "drizzle-orm/pg-core"; 2 | 3 | const authSchema = pgSchema("auth"); 4 | 5 | export const Users = authSchema.table("users", { 6 | id: uuid("id").primaryKey(), 7 | }); 8 | 9 | // Used to setup foreign key from profile.id to auth.users table in Supabase 10 | -------------------------------------------------------------------------------- /packages/db/src/schema/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./auth"; 2 | export * from "./profile"; 3 | export * from "./post"; 4 | -------------------------------------------------------------------------------- /packages/db/src/schema/post.ts: -------------------------------------------------------------------------------- 1 | import { relations, sql } from "drizzle-orm"; 2 | import { text, timestamp, uuid, varchar } from "drizzle-orm/pg-core"; 3 | import { createInsertSchema } from "drizzle-zod"; 4 | import { z } from "zod"; 5 | 6 | import { timestamps } from "../lib/utils"; 7 | import { createTable } from "./_table"; 8 | import { Profile } from "./profile"; 9 | 10 | export const Post = createTable("post", { 11 | id: uuid("id").primaryKey().defaultRandom(), 12 | title: varchar("name", { length: 256 }).notNull(), 13 | content: text("content").notNull(), 14 | authorId: uuid("author_id") 15 | .notNull() 16 | .references(() => Profile.id), 17 | createdAt: timestamp("created_at") 18 | .default(sql`now()`) 19 | .notNull(), 20 | updatedAt: timestamp("updatedAt", { 21 | mode: "date", 22 | withTimezone: true, 23 | }).$onUpdateFn(() => sql`now()`), 24 | }); 25 | 26 | export const PostRelations = relations(Post, ({ one }) => ({ 27 | author: one(Profile, { fields: [Post.authorId], references: [Profile.id] }), 28 | })); 29 | 30 | export const CreatePostSchema = createInsertSchema(Post, { 31 | title: z.string().max(256), 32 | content: z.string().max(256), 33 | }).omit({ 34 | id: true, 35 | authorId: true, 36 | ...timestamps, 37 | }); 38 | -------------------------------------------------------------------------------- /packages/db/src/schema/profile.ts: -------------------------------------------------------------------------------- 1 | import { relations } from "drizzle-orm"; 2 | import { uuid, varchar } from "drizzle-orm/pg-core"; 3 | 4 | import { createTable } from "./_table"; 5 | import { Users } from "./auth"; 6 | import { Post } from "./post"; 7 | 8 | export const Profile = createTable("profile", { 9 | // Matches id from auth.users table in Supabase 10 | id: uuid("id") 11 | .primaryKey() 12 | .references(() => Users.id, { onDelete: "cascade" }), 13 | name: varchar("name", { length: 256 }).notNull(), 14 | image: varchar("image", { length: 256 }), 15 | email: varchar("email", { length: 256 }), 16 | }); 17 | 18 | export const ProfileRelations = relations(Profile, ({ many }) => ({ 19 | posts: many(Post), 20 | })); 21 | -------------------------------------------------------------------------------- /packages/db/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@acme/tsconfig/internal-package.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" 6 | }, 7 | "include": ["src"], 8 | "exclude": ["node_modules"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/ui/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "./tailwind.config.ts", 8 | "css": "unused.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "utils": "@acme/ui", 14 | "components": "src/", 15 | "ui": "src/" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/ui/eslint.config.js: -------------------------------------------------------------------------------- 1 | import baseConfig from "@acme/eslint-config/base"; 2 | import reactConfig from "@acme/eslint-config/react"; 3 | 4 | /** @type {import('typescript-eslint').Config} */ 5 | export default [ 6 | { 7 | ignores: [], 8 | }, 9 | ...baseConfig, 10 | ...reactConfig, 11 | ]; 12 | -------------------------------------------------------------------------------- /packages/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@acme/ui", 3 | "private": true, 4 | "version": "0.1.0", 5 | "type": "module", 6 | "exports": { 7 | ".": "./src/index.ts", 8 | "./*": [ 9 | "./src/*.tsx", 10 | "./src/*.ts" 11 | ] 12 | }, 13 | "license": "MIT", 14 | "scripts": { 15 | "clean": "rm -rf .turbo node_modules", 16 | "format": "prettier --check . --ignore-path ../../.gitignore", 17 | "lint": "eslint", 18 | "typecheck": "tsc --noEmit --emitDeclarationOnly false", 19 | "ui-add": "pnpm dlx shadcn-ui add && prettier src --write --list-different" 20 | }, 21 | "dependencies": { 22 | "@hookform/resolvers": "^3.3.4", 23 | "@radix-ui/react-accordion": "^1.2.0", 24 | "@radix-ui/react-avatar": "^1.1.0", 25 | "@radix-ui/react-checkbox": "^1.1.0", 26 | "@radix-ui/react-dialog": "^1.1.0", 27 | "@radix-ui/react-dropdown-menu": "^2.0.6", 28 | "@radix-ui/react-icons": "^1.3.0", 29 | "@radix-ui/react-label": "^2.0.2", 30 | "@radix-ui/react-popover": "^1.1.0", 31 | "@radix-ui/react-radio-group": "^1.2.0", 32 | "@radix-ui/react-scroll-area": "^1.1.0", 33 | "@radix-ui/react-select": "^2.1.0", 34 | "@radix-ui/react-separator": "^1.1.0", 35 | "@radix-ui/react-slot": "^1.0.2", 36 | "@radix-ui/react-switch": "^1.1.0", 37 | "@radix-ui/react-tabs": "^1.1.0", 38 | "@radix-ui/react-tooltip": "^1.1.0", 39 | "class-variance-authority": "^0.7.0", 40 | "cmdk": "^1.0.0", 41 | "date-fns": "^3.6.0", 42 | "next-themes": "^0.3.0", 43 | "react-day-picker": "^8.10.1", 44 | "react-hook-form": "^7.51.4", 45 | "react-resizable-panels": "^2.0.19", 46 | "sonner": "^1.4.41", 47 | "tailwind-merge": "^2.3.0", 48 | "tailwindcss-animate": "^1.0.7", 49 | "vaul": "^0.9.1" 50 | }, 51 | "devDependencies": { 52 | "@acme/eslint-config": "workspace:*", 53 | "@acme/prettier-config": "workspace:*", 54 | "@acme/tailwind-config": "workspace:*", 55 | "@acme/tsconfig": "workspace:*", 56 | "@types/react": "^18.3.3", 57 | "eslint": "^9.6.0", 58 | "prettier": "^3.3.2", 59 | "react": "18.3.1", 60 | "tailwindcss": "^3.4.4", 61 | "typescript": "^5.5.3", 62 | "zod": "^3.23.8" 63 | }, 64 | "peerDependencies": { 65 | "react": "18.3.1", 66 | "zod": "^3.23.8" 67 | }, 68 | "prettier": "@acme/prettier-config" 69 | } 70 | -------------------------------------------------------------------------------- /packages/ui/src/accordion.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as AccordionPrimitive from "@radix-ui/react-accordion"; 5 | import { ChevronDownIcon } from "@radix-ui/react-icons"; 6 | 7 | import { cn } from "@acme/ui"; 8 | 9 | const Accordion = AccordionPrimitive.Root; 10 | 11 | const AccordionItem = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, ...props }, ref) => ( 15 | 20 | )); 21 | AccordionItem.displayName = "AccordionItem"; 22 | 23 | const AccordionTrigger = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, children, ...props }, ref) => ( 27 | 28 | svg]:rotate-180", 32 | className, 33 | )} 34 | {...props} 35 | > 36 | {children} 37 | 38 | 39 | 40 | )); 41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; 42 | 43 | const AccordionContent = React.forwardRef< 44 | React.ElementRef, 45 | React.ComponentPropsWithoutRef 46 | >(({ className, children, ...props }, ref) => ( 47 | 52 |
{children}
53 |
54 | )); 55 | AccordionContent.displayName = AccordionPrimitive.Content.displayName; 56 | 57 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; 58 | -------------------------------------------------------------------------------- /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 "@acme/ui"; 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/badge.tsx: -------------------------------------------------------------------------------- 1 | import type { VariantProps } from "class-variance-authority"; 2 | import * as React from "react"; 3 | import { cva } from "class-variance-authority"; 4 | 5 | import { cn } from "@acme/ui"; 6 | 7 | const badgeVariants = cva( 8 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", 14 | secondary: 15 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 16 | destructive: 17 | "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", 18 | outline: "text-foreground", 19 | }, 20 | }, 21 | defaultVariants: { 22 | variant: "default", 23 | }, 24 | }, 25 | ); 26 | 27 | export interface BadgeProps 28 | extends React.HTMLAttributes, 29 | VariantProps {} 30 | 31 | function Badge({ className, variant, ...props }: BadgeProps) { 32 | return ( 33 |
34 | ); 35 | } 36 | 37 | export { Badge, badgeVariants }; 38 | -------------------------------------------------------------------------------- /packages/ui/src/breadcrumb.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { ChevronRightIcon, DotsHorizontalIcon } from "@radix-ui/react-icons"; 3 | import { Slot } from "@radix-ui/react-slot"; 4 | 5 | import { cn } from "@acme/ui"; 6 | 7 | const Breadcrumb = React.forwardRef< 8 | HTMLElement, 9 | React.ComponentPropsWithoutRef<"nav"> & { 10 | separator?: React.ReactNode; 11 | } 12 | >(({ ...props }, ref) =>