├── .changeset
├── README.md
└── config.json
├── .github
└── workflows
│ ├── pull_request.yaml
│ └── release.yml
├── .gitignore
├── .husky
└── pre-commit
├── .idea
├── .gitignore
├── inspectionProfiles
│ └── Project_Default.xml
├── jsLibraryMappings.xml
├── modules.xml
├── mudd-app.iml
└── vcs.xml
├── .npmrc
├── .prettierrc
├── .vscode
├── extensions.json
└── settings.json
├── README.md
├── apps
├── app
│ ├── .env.local.example
│ ├── .eslintrc.js
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── lib
│ │ └── supabase
│ │ │ └── server.ts
│ ├── middleware.ts
│ ├── next.config.js
│ ├── package.json
│ ├── postcss.config.js
│ ├── public
│ │ ├── apple-touch-icon.png
│ │ ├── favicon-16x16.png
│ │ ├── favicon.ico
│ │ ├── next.svg
│ │ └── vercel.svg
│ ├── src
│ │ ├── app
│ │ │ ├── (splash)
│ │ │ │ ├── layout.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── app
│ │ │ │ ├── actions
│ │ │ │ │ └── user.ts
│ │ │ │ ├── layout.tsx
│ │ │ │ ├── page.tsx
│ │ │ │ └── settings
│ │ │ │ │ ├── appearance
│ │ │ │ │ └── page.tsx
│ │ │ │ │ ├── layout.tsx
│ │ │ │ │ └── page.tsx
│ │ │ ├── auth
│ │ │ │ ├── (public)
│ │ │ │ │ ├── forgot-password
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ ├── layout.tsx
│ │ │ │ │ ├── login
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ └── register
│ │ │ │ │ │ └── page.tsx
│ │ │ │ ├── actions
│ │ │ │ │ └── index.ts
│ │ │ │ ├── callback
│ │ │ │ │ └── route.ts
│ │ │ │ ├── layout.tsx
│ │ │ │ ├── reset-password
│ │ │ │ │ └── page.tsx
│ │ │ │ └── welcome
│ │ │ │ │ └── page.tsx
│ │ │ ├── experimental
│ │ │ │ └── page.tsx
│ │ │ └── layout.tsx
│ │ ├── components
│ │ │ ├── form
│ │ │ │ ├── AppForm.tsx
│ │ │ │ ├── FormFieldset.tsx
│ │ │ │ ├── FormInputField.tsx
│ │ │ │ └── FormSelectField.tsx
│ │ │ └── layouts
│ │ │ │ ├── AppNavbar.tsx
│ │ │ │ ├── Navbar.tsx
│ │ │ │ ├── ThemeProvider.tsx
│ │ │ │ └── ThemeToggle.tsx
│ │ └── modules
│ │ │ ├── auth
│ │ │ ├── actions.ts
│ │ │ ├── components
│ │ │ │ ├── ForgotPasswordForm.tsx
│ │ │ │ ├── LoginAuthForm.tsx
│ │ │ │ ├── LoginWithEmailAndPasswordAuthForm.tsx
│ │ │ │ ├── LoginWithEmailAuthForm.tsx
│ │ │ │ ├── RegisterWithEmailAndPasswordAuthForm.tsx
│ │ │ │ ├── ResetPasswordForm.tsx
│ │ │ │ └── UpdateUserForm.tsx
│ │ │ ├── styles
│ │ │ │ └── auth.css
│ │ │ └── validations
│ │ │ │ ├── EmailFormSchema.ts
│ │ │ │ ├── LoginWithEmailAndPasswordSchema.ts
│ │ │ │ ├── RegisterWithEmailAndPasswordSchema.ts
│ │ │ │ ├── ResetPasswordSchema.ts
│ │ │ │ └── index.ts
│ │ │ ├── dashboard
│ │ │ └── components
│ │ │ │ └── UserWelcomeCard.tsx
│ │ │ ├── settings
│ │ │ └── components
│ │ │ │ ├── AppearanceForm.tsx
│ │ │ │ └── SidebarNav.tsx
│ │ │ └── user
│ │ │ └── components
│ │ │ └── ProfileIconMenu.tsx
│ ├── supabase
│ │ ├── .gitignore
│ │ ├── config.toml
│ │ ├── migrations
│ │ │ └── 20240324185402_remote_schema.sql
│ │ └── seed.sql
│ ├── tailwind.config.js
│ └── tsconfig.json
└── docs
│ ├── .eslintrc.cjs
│ ├── .gitignore
│ ├── .storybook
│ ├── main.js
│ └── preview.jsx
│ ├── package.json
│ ├── postcss.config.cjs
│ ├── stories
│ ├── Mixins
│ │ ├── colors.mdx
│ │ ├── icons.mdx
│ │ └── spacings.mdx
│ ├── button.stories.tsx
│ ├── dialog.stories.tsx
│ ├── input.stories.tsx
│ └── toast.stories.tsx
│ ├── tailwind.config.js
│ ├── tokens.ts
│ └── tsconfig.json
├── package.json
├── packages
├── eslint-config-custom
│ ├── README.md
│ ├── library.js
│ ├── next.js
│ ├── package.json
│ ├── react-internal.js
│ └── storybook.js
├── tailwind-config
│ ├── .eslintrc.js
│ ├── package.json
│ └── tailwind.config.js
├── tsconfig
│ ├── base.json
│ ├── nextjs.json
│ ├── package.json
│ └── react-library.json
└── ui
│ ├── .eslintrc.js
│ ├── CHANGELOG.md
│ ├── components.json
│ ├── generate-tokens.js
│ ├── index.tsx
│ ├── package.json
│ ├── src
│ ├── components
│ │ ├── magicui
│ │ │ └── border-beam.tsx
│ │ └── ui
│ │ │ ├── button.tsx
│ │ │ ├── card.tsx
│ │ │ ├── dialog.tsx
│ │ │ ├── dropdown-menu.tsx
│ │ │ ├── form.tsx
│ │ │ ├── icons.tsx
│ │ │ ├── input.tsx
│ │ │ ├── label.tsx
│ │ │ ├── select.tsx
│ │ │ ├── separator.tsx
│ │ │ ├── skeleton.tsx
│ │ │ ├── toast.tsx
│ │ │ ├── toaster.tsx
│ │ │ └── use-toast.ts
│ └── lib
│ │ └── utils.ts
│ ├── styles
│ └── globals.css
│ ├── tailwind.config.js
│ ├── tsconfig.json
│ └── vite.config.ts
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── public
└── launchpad-logo-md.png
├── supabase
├── .gitignore
├── config.toml
└── seed.sql
├── tsconfig.json
└── turbo.json
/.changeset/README.md:
--------------------------------------------------------------------------------
1 | # Changesets
2 |
3 | Hello and welcome! This folder has been automatically generated by
4 | `@changesets/cli`, a build tool that works with multi-package repos, or
5 | single-package repos to help you version and publish your code. You can find the
6 | full documentation for it
7 | [in our repository](https://github.com/changesets/changesets)
8 |
9 | We have a quick list of common questions to get you started engaging with this
10 | project in
11 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
12 |
--------------------------------------------------------------------------------
/.changeset/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
3 | "changelog": [
4 | "@changesets/changelog-github",
5 | { "repo": "JadRizk/miniature-launchpad" }
6 | ],
7 | "commit": false,
8 | "fixed": [],
9 | "linked": [],
10 | "access": "restricted",
11 | "baseBranch": "main",
12 | "updateInternalDependencies": "patch",
13 | "ignore": [],
14 | "privatePackages": {
15 | "version": true,
16 | "tag": true
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/.github/workflows/pull_request.yaml:
--------------------------------------------------------------------------------
1 | name: Pull Request
2 |
3 | on: [pull_request]
4 |
5 | jobs:
6 | pull_request:
7 | runs-on: ubuntu-latest
8 | timeout-minutes: 15
9 |
10 | env:
11 | NEXT_APP_URL: test
12 | SUPABASE_URL: test
13 | SUPABASE_ANON_KEY: test
14 |
15 | steps:
16 | - name: ⬇️ Checkout Repo
17 | uses: actions/checkout@v4
18 |
19 | - name: 🥡 Install PNPM
20 | uses: pnpm/action-setup@v3
21 | with:
22 | version: 8
23 |
24 | - name: 🟢 Setup Node.js 20.x
25 | uses: actions/setup-node@v4
26 | with:
27 | node-version: 20
28 | cache: 'pnpm'
29 |
30 | - name: 🧩 Install Dependencies
31 | run: pnpm install
32 |
33 | - name: 🔍 Run Linting
34 | run: pnpm run lint
35 |
36 | - name: 📝 Run Formatting
37 | run: pnpm run format
38 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: 🚀 Release
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | concurrency: ${{ github.workflow }}-${{ github.ref }}
9 |
10 | jobs:
11 | release:
12 | name: 🚀 Release
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: ⬇️ Checkout Repo
16 | uses: actions/checkout@v4
17 |
18 | - name: 🥡 Install PNPM
19 | uses: pnpm/action-setup@v3
20 | with:
21 | version: 8
22 |
23 | - name: 🟢 Setup Node.js 20.x
24 | uses: actions/setup-node@v4
25 | with:
26 | node-version: 20
27 | cache: 'pnpm'
28 |
29 | - name: 🧩 Install Dependencies
30 | run: pnpm install
31 |
32 | - name: 📣 Create Release Pull Request
33 | id: changesets
34 | uses: changesets/action@v1
35 | with:
36 | title: 'chore(release): version packages 🦋'
37 | publish: pnpm release
38 | version: pnpm version-packages
39 | commit: 'chore(release): version packages 🦋 [skip ci]'
40 | env:
41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
42 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
43 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.toptal.com/developers/gitignore/api/webstorm,nextjs,node,turbo
2 | # Edit at https://www.toptal.com/developers/gitignore?templates=webstorm,nextjs,node,turbo
3 |
4 | ### NextJS ###
5 | # dependencies
6 | /node_modules
7 | /.pnp
8 | .pnp.js
9 |
10 | # testing
11 | /coverage
12 |
13 | # next.js
14 | /.next/
15 | /out/
16 |
17 | # production
18 | /build
19 |
20 | # misc
21 | .DS_Store
22 | *.pem
23 |
24 | # debug
25 | npm-debug.log*
26 | yarn-debug.log*
27 | yarn-error.log*
28 | .pnpm-debug.log*
29 |
30 | # local env files
31 | .env*.local
32 |
33 | # vercel
34 | .vercel
35 |
36 | # typescript
37 | *.tsbuildinfo
38 | next-env.d.ts
39 |
40 | ### Node ###
41 | # Logs
42 | logs
43 | *.log
44 | lerna-debug.log*
45 |
46 | # Diagnostic reports (https://nodejs.org/api/report.html)
47 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
48 |
49 | # Runtime data
50 | pids
51 | *.pid
52 | *.seed
53 | *.pid.lock
54 |
55 | # Directory for instrumented libs generated by jscoverage/JSCover
56 | lib-cov
57 |
58 | # Coverage directory used by tools like istanbul
59 | coverage
60 | *.lcov
61 |
62 | # nyc test coverage
63 | .nyc_output
64 |
65 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
66 | .grunt
67 |
68 | # Bower dependency directory (https://bower.io/)
69 | bower_components
70 |
71 | # node-waf configuration
72 | .lock-wscript
73 |
74 | # Compiled binary addons (https://nodejs.org/api/addons.html)
75 | build/Release
76 |
77 | # Dependency directories
78 | node_modules/
79 | jspm_packages/
80 |
81 | # Snowpack dependency directory (https://snowpack.dev/)
82 | web_modules/
83 |
84 | # TypeScript cache
85 |
86 | # Optional npm cache directory
87 | .npm
88 |
89 | # Optional eslint cache
90 | .eslintcache
91 |
92 | # Optional stylelint cache
93 | .stylelintcache
94 |
95 | # Microbundle cache
96 | .rpt2_cache/
97 | .rts2_cache_cjs/
98 | .rts2_cache_es/
99 | .rts2_cache_umd/
100 |
101 | # Optional REPL history
102 | .node_repl_history
103 |
104 | # Output of 'npm pack'
105 | *.tgz
106 |
107 | # Yarn Integrity file
108 | .yarn-integrity
109 |
110 | # dotenv environment variable files
111 | .env
112 | .env.development.local
113 | .env.test.local
114 | .env.production.local
115 | .env.local
116 |
117 | # parcel-bundler cache (https://parceljs.org/)
118 | .cache
119 | .parcel-cache
120 |
121 | # Next.js build output
122 | .next
123 | out
124 |
125 | # Nuxt.js build / generate output
126 | .nuxt
127 | dist
128 |
129 | # Gatsby files
130 | .cache/
131 | # Comment in the public line in if your project uses Gatsby and not Next.js
132 | # https://nextjs.org/blog/next-9-1#public-directory-support
133 | # public
134 |
135 | # vuepress build output
136 | .vuepress/dist
137 |
138 | # vuepress v2.x temp and cache directory
139 | .temp
140 |
141 | # Docusaurus cache and generated files
142 | .docusaurus
143 |
144 | # Serverless directories
145 | .serverless/
146 |
147 | # FuseBox cache
148 | .fusebox/
149 |
150 | # DynamoDB Local files
151 | .dynamodb/
152 |
153 | # TernJS port file
154 | .tern-port
155 |
156 | # Stores VSCode versions used for testing VSCode extensions
157 | .vscode-test
158 |
159 | # yarn v2
160 | .yarn/cache
161 | .yarn/unplugged
162 | .yarn/build-state.yml
163 | .yarn/install-state.gz
164 | .pnp.*
165 |
166 | ### Node Patch ###
167 | # Serverless Webpack directories
168 | .webpack/
169 |
170 | # Optional stylelint cache
171 |
172 | # SvelteKit build / generate output
173 | .svelte-kit
174 |
175 | ### Turbo ###
176 | # Turborepo task cache
177 | .turbo
178 |
179 | ### WebStorm ###
180 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
181 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
182 |
183 | # User-specific stuff
184 | .idea/**/workspace.xml
185 | .idea/**/tasks.xml
186 | .idea/**/usage.statistics.xml
187 | .idea/**/dictionaries
188 | .idea/**/shelf
189 |
190 | # AWS User-specific
191 | .idea/**/aws.xml
192 |
193 | # Generated files
194 | .idea/**/contentModel.xml
195 |
196 | # Sensitive or high-churn files
197 | .idea/**/dataSources/
198 | .idea/**/dataSources.ids
199 | .idea/**/dataSources.local.xml
200 | .idea/**/sqlDataSources.xml
201 | .idea/**/dynamic.xml
202 | .idea/**/uiDesigner.xml
203 | .idea/**/dbnavigator.xml
204 |
205 | # Gradle
206 | .idea/**/gradle.xml
207 | .idea/**/libraries
208 |
209 | # Gradle and Maven with auto-import
210 | # When using Gradle or Maven with auto-import, you should exclude module files,
211 | # since they will be recreated, and may cause churn. Uncomment if using
212 | # auto-import.
213 | # .idea/artifacts
214 | # .idea/compiler.xml
215 | # .idea/jarRepositories.xml
216 | # .idea/modules.xml
217 | # .idea/*.iml
218 | # .idea/modules
219 | # *.iml
220 | # *.ipr
221 |
222 | # CMake
223 | cmake-build-*/
224 |
225 | # Mongo Explorer plugin
226 | .idea/**/mongoSettings.xml
227 |
228 | # File-based project format
229 | *.iws
230 |
231 | # IntelliJ
232 | out/
233 |
234 | # mpeltonen/sbt-idea plugin
235 | .idea_modules/
236 |
237 | # JIRA plugin
238 | atlassian-ide-plugin.xml
239 |
240 | # Cursive Clojure plugin
241 | .idea/replstate.xml
242 |
243 | # SonarLint plugin
244 | .idea/sonarlint/
245 |
246 | # Crashlytics plugin (for Android Studio and IntelliJ)
247 | com_crashlytics_export_strings.xml
248 | crashlytics.properties
249 | crashlytics-build.properties
250 | fabric.properties
251 |
252 | # Editor-based Rest Client
253 | .idea/httpRequests
254 |
255 | # Android studio 3.1+ serialized cache file
256 | .idea/caches/build_file_checksums.ser
257 |
258 | ### WebStorm Patch ###
259 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
260 |
261 | # *.iml
262 | # modules.xml
263 | # .idea/misc.xml
264 | # *.ipr
265 | .idea
266 |
267 | # Sonarlint plugin
268 | # https://plugins.jetbrains.com/plugin/7973-sonarlint
269 | .idea/**/sonarlint/
270 |
271 | # SonarQube Plugin
272 | # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
273 | .idea/**/sonarIssues.xml
274 |
275 | # Markdown Navigator plugin
276 | # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
277 | .idea/**/markdown-navigator.xml
278 | .idea/**/markdown-navigator-enh.xml
279 | .idea/**/markdown-navigator/
280 |
281 | # Cache file creation bug
282 | # See https://youtrack.jetbrains.com/issue/JBR-2257
283 | .idea/$CACHE_FILE$
284 |
285 | # CodeStream plugin
286 | # https://plugins.jetbrains.com/plugin/12206-codestream
287 | .idea/codestream.xml
288 |
289 | # Azure Toolkit for IntelliJ plugin
290 | # https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
291 | .idea/**/azureSettings.xml
292 |
293 | # End of https://www.toptal.com/developers/gitignore/api/webstorm,nextjs,node,turbo
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | pnpm exec lint-staged
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/jsLibraryMappings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/mudd-app.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | auto-install-peers = true
2 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "avoid",
3 | "bracketSpacing": true,
4 | "htmlWhitespaceSensitivity": "css",
5 | "insertPragma": false,
6 | "jsxBracketSameLine": false,
7 | "jsxSingleQuote": true,
8 | "printWidth": 80,
9 | "proseWrap": "always",
10 | "quoteProps": "as-needed",
11 | "requirePragma": false,
12 | "semi": true,
13 | "singleQuote": true,
14 | "tabWidth": 2,
15 | "trailingComma": "all",
16 | "useTabs": false
17 | }
18 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["denoland.vscode-deno"]
3 | }
4 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "deno.enablePaths": ["supabase/functions"],
3 | "deno.lint": true,
4 | "deno.unstable": true,
5 | "[typescript]": {
6 | "editor.defaultFormatter": "denoland.vscode-deno"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # Launchpad
6 |
7 | 
8 | 
9 | 
10 | 
11 | 
12 | 
13 | 
14 | 
15 | 
16 | 
17 | 
18 | 
19 |
20 | ---
21 |
22 | Welcome to **Launchpad**, a turbocharged monorepo equipped with a Next.js 14
23 | application, a comprehensive Storybook-hosted component library, and essential
24 | configuration packages for ESLint, Tailwind CSS, and TypeScript. Designed to
25 | streamline the development of scalable and secure web applications,
26 | **Launchpad** offers a robust suite of tools and a customizable UI component
27 | library to enhance your development workflow.
28 |
29 | - [Launchpad](#launchpad)
30 | - [🚀 Getting Started](#-getting-started)
31 | - [Installation](#installation)
32 | - [Environment Setup](#environment-setup)
33 | - [Useful Commands](#useful-commands)
34 | - [📦 Apps \& Packages](#-apps--packages)
35 | - [🛠 Tools Powering This Repository](#-tools-powering-this-repository)
36 | - [🧑💻 Setting Up Supabase Locally](#-setting-up-supabase-locally)
37 | - [Prerequisites](#prerequisites)
38 | - [Installation](#installation-1)
39 | - [Configuration](#configuration)
40 | - [Useful Commands](#useful-commands-1)
41 | - [🧱 Components](#-components)
42 | - [🔄 Versioning \& Publishing Packages](#-versioning--publishing-packages)
43 | - [🔧 Generating Changesets](#-generating-changesets)
44 | - [📚 Further Documentation and Resources](#-further-documentation-and-resources)
45 |
46 | ## 🚀 Getting Started
47 |
48 | To get started ensure you have the necessary tools installed -PNPM & Node
49 | (v21+)-, and then clone this repository:
50 |
51 | ### Installation
52 |
53 | ```sh
54 | # Clone repo locally
55 | git clone https://github.com/JadRizk/miniature-launchpad.git
56 | cd miniature-launchpad
57 |
58 | # Install dependencies
59 | pnpm install
60 | ```
61 |
62 | ### Environment Setup
63 |
64 | Before you start the application, you must configure the environment variables
65 | for Supabase authentication and Umami analytics. Create a .env.local file at the
66 | root of your project and include the following:
67 |
68 | ```sh
69 | SUPABASE_URL=your_supabase_url
70 | SUPABASE_ANON_KEY=your_supabase_anon_key
71 |
72 | UMAMI_SCRIPT_URL=your_umami_script_url
73 | ```
74 |
75 | For more information on setting up Supabase, visit the
76 | [Supabase Documentation](https://supabase.com/).
77 |
78 | ### Useful Commands
79 |
80 | - `pnpm build` - Build all packages, including the Storybook site
81 | - `pnpm dev` - Run all packages locally and preview with Storybook
82 | - `pnpm lint` - Lint all packages
83 | - `pnpm changeset` - Generate a changeset
84 | - `pnpm clean` - Clean up all `node_modules` and `dist` folders (runs each
85 | package's clean script)
86 |
87 | ## 📦 Apps & Packages
88 |
89 | Launchpad's Turborepo structure is designed to maximize modularity and
90 | efficiency across multiple projects. Each app and package is tailored to
91 | specific aspects of development, ensuring a cohesive and scalable ecosystem:
92 |
93 | - **`apps/app`**: Our main Next.js 14 application, serving as the face of your
94 | digital presence. It integrates Supabase for robust authentication and
95 | leverages our unified component library to ensure a seamless user experience.
96 | - **`apps/docs`**: A dedicated Storybook-powered site for documenting and
97 | showcasing UI components. It offers an interactive platform for developers to
98 | view and test component designs in isolation.
99 | - **`packages/ui`**: The core of our UI development, this package includes
100 | reusable React components that ensure consistency and quality across all
101 | applications and projects within the repository.
102 | - **`packages/tailwind-config`**: Provides a centralized Tailwind CSS
103 | configuration to maintain a uniform styling framework throughout all our
104 | applications and components.
105 | - **`packages/typescript-config`**: Manages shared TypeScript configurations,
106 | facilitating consistent coding practices and error-free compilation across the
107 | monorepo.
108 | - **`packages/eslint-config`**: Delivers a pre-set ESLint configuration designed
109 | to enforce stringent code quality and style guidelines for all JavaScript and
110 | TypeScript codebases.
111 |
112 | ## 🛠 Tools Powering This Repository
113 |
114 | Launchpad leverages a suite of top-tier development tools designed to enhance
115 | productivity and maintain high standards across the development lifecycle:
116 |
117 | - 🏎 [**Turborepo**](https://turbo.build/repo) — A high-performance build system
118 | tailored for efficient management of monorepos.
119 | - 🚀 [**Next.js 14**](https://nextjs.org/) — A React framework for building
120 | user-friendly and scalable web applications.
121 | - 🔐 [**Supabase Authentication**](https://supabase.com/auth) — Provides robust
122 | authentication solutions, enabling secure and scalable user management.
123 | - 📖 [**Storybook**](https://storybook.js.org/) — A sandboxed environment for
124 | developing and isolating UI components, powered by Vite for near-instant
125 | feedback.
126 | - 🔠 [**TypeScript**](https://www.typescriptlang.org/) — Enhances JavaScript
127 | with static types to improve predictability and maintainability of code.
128 | - 🌊 [**Tailwind CSS**](https://tailwindcss.com/) — A utility-first CSS
129 | framework for rapidly building custom designs.
130 | - 🌐 [**Shadcn**](https://github.com/shadcn) — A modern toolchain for front-end
131 | development, focusing on performance and developer experience.
132 | - 🔍 [**ESLint**](https://eslint.org/) — A linter tool to identify and fix
133 | problems in JavaScript and TypeScript code, enforcing code quality standards.
134 | - ✨ [**Prettier**](https://prettier.io) — An opinionated code formatter that
135 | ensures consistency in code style.
136 | - 🏷️ [**Changesets**](https://github.com/changesets/changesets) — Manages
137 | versioning and changelog generation, streamlining the release process.
138 | - ⚙️ [**GitHub Actions**](https://github.com/features/actions) — Automates
139 | workflows for continuous integration and deployment, enhancing development
140 | pipelines.
141 | - 📊 [**Umami Analytics**](https://umami.is/) — A simple, privacy-focused
142 | alternative to Google Analytics, providing insights into user interactions
143 | without compromising privacy.
144 |
145 | **Note**: Each package and app is 100%
146 | [TypeScript](https://www.typescriptlang.org/). Workspaces enables us to "hoist"
147 | dependencies that are shared between packages to the root `package.json`. This
148 | means smaller `node_modules` folders and a better local dev experience. To
149 | install a dependency for the entire monorepo, use the `-w` workspaces flag with
150 | `pnpm add`.
151 |
152 | ## 🧑💻 Setting Up Supabase Locally
153 |
154 | Setting up Supabase locally can significantly streamline your development
155 | workflow. Follow these steps to get started with Supabase CLI for local
156 | development:
157 |
158 | ### Prerequisites
159 |
160 | Ensure you have Docker installed on your machine, as Supabase relies on Docker
161 | containers to run the local development environment.
162 |
163 | ### Installation
164 |
165 | 1. **Install Supabase CLI**: First, install the Supabase CLI by running the
166 | following command:
167 |
168 | ```sh
169 | # Install Supabase CLI
170 | npm install -g supabase
171 | ```
172 |
173 | 2. **Start Supabase**: Navigate to your project's root directory and initialize
174 | Supabase:
175 |
176 | ```sh
177 | # Initialize Supabase in your project
178 | supabase init
179 | ```
180 |
181 | 3. **Start the Supabase local development environment**: Run the following
182 | command to start the local Supabase environment:
183 |
184 | ```sh
185 | # Start Supabase local environment
186 | supabase start
187 | ```
188 |
189 | This command will spin up the necessary Docker containers for Supabase,
190 | including the database, authentication, and storage services.
191 |
192 | ### Configuration
193 |
194 | 1. **Configure Environment Variables**: Update your `.env.local` file to use the
195 | local Supabase instance. Replace `your_supabase_url` and
196 | `your_supabase_anon_key` with the values provided by the local Supabase
197 | environment:
198 |
199 | ```sh
200 | NEXT_PUBLIC_SUPABASE_URL=http://localhost:54321
201 | NEXT_PUBLIC_SUPABASE_ANON_KEY=your_local_anon_key
202 | ```
203 |
204 | 2. **Migrate Database**: To apply database migrations, use the following
205 | command:
206 |
207 | ```sh
208 | # Apply database migrations
209 | supabase db push
210 | ```
211 |
212 | This command ensures your local database schema matches your project
213 | requirements.
214 |
215 | ### Useful Commands
216 |
217 | - `supabase start` - Start the local Supabase development environment.
218 | - `supabase stop` - Stop the local Supabase environment.
219 | - `supabase db push` - Apply database migrations.
220 | - `supabase db reset` - Reset the local database to its initial state.
221 |
222 | For more detailed information on setting up and using Supabase locally, visit
223 | the
224 | [Supabase CLI Local Development Guide](https://supabase.com/docs/guides/cli/local-development).
225 |
226 | ## 🧱 Components
227 |
228 | Each file inside of `packages/src/components` is a component inside our design
229 | system. For example:
230 |
231 | ```tsx:acme-core/src/Button.tsx
232 | import * as React from 'react';
233 |
234 | export interface ButtonProps {
235 | children: React.ReactNode;
236 | }
237 |
238 | export function Button(props: ButtonProps) {
239 | return ;
240 | }
241 |
242 | Button.displayName = 'Button';
243 | ```
244 |
245 | When adding a new file, ensure the component is also exported from the entry
246 | `index.tsx` file:
247 |
248 | ```tsx:packages/src/components/index.tsx
249 | export * from './Button';
250 | ```
251 |
252 | ## 🔄 Versioning & Publishing Packages
253 |
254 | The project uses [Changesets](https://github.com/changesets/changesets) for
255 | version management, changelog generation, and npm publishing. The workflow is
256 | designed to automate and streamline the release process, ensuring reliable and
257 | consistent package updates.
258 |
259 | ### 🔧 Generating Changesets
260 |
261 | When you make changes that should be tracked in the changelog, follow these
262 | steps:
263 |
264 | 1. **Start the Changeset**: Run the following command in your terminal:
265 | `pnpm changeset`
266 | 2. **Select Packages**: The CLI will prompt you to select the packages affected
267 | by your changes. Ensure you create one changeset per repository to keep the
268 | changelogs separate.
269 | 3. **Version Bumps**: Decide on the version bump for the selected packages. It's
270 | crucial to follow semantic versioning best practices.
271 | 4. **Summarize Changes**: Write a clear summary of what changes have been made;
272 | this summary will be included in the changelog.
273 | 5. **Review and Confirm**: Confirm the details of your changeset. A Markdown
274 | file will then be created in the changeset folder, listing the packages and
275 | summarizing the changes.
276 |
277 | ## 📚 Further Documentation and Resources
278 |
279 | To help you maximize your use of **Launchpad**, here are some valuable resources
280 | and documentation links:
281 |
282 | - [Next.js Documentation](https://nextjs.org/docs) - Learn about Next.js
283 | features and API.
284 | - [Supabase Documentation](https://supabase.com/docs) - Comprehensive guides and
285 | API references for using Supabase for authentication and database services.
286 | - [Storybook Documentation](https://storybook.js.org/docs) - Explore Storybook
287 | to better understand how to build and test UI components in isolation.
288 | - [Tailwind CSS Documentation](https://tailwindcss.com/docs) - Get to know how
289 | to style your applications efficiently using Tailwind CSS.
290 | - [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/intro.html) -
291 | Deepen your knowledge of TypeScript and how to use it effectively in your
292 | projects.
293 | - [ESLint User Guide](https://eslint.org/docs/user-guide) - Configure ESLint to
294 | enforce code quality standards in your JavaScript and TypeScript code.
295 | - [Prettier Documentation](https://prettier.io/docs/en/index.html) - Learn how
296 | to use Prettier for automatic code formatting.
297 | - [Turborepo Guide](https://turborepo.org/docs) - Understand how to use
298 | Turborepo to manage your monorepo efficiently.
299 |
300 | These resources provide extensive information and best practices that can boost
301 | your development process and enhance your understanding of the technologies
302 | integrated into **Launchpad**.
303 |
304 | ---
305 |
--------------------------------------------------------------------------------
/apps/app/.env.local.example:
--------------------------------------------------------------------------------
1 | NEXT_APP_URL=your-app-url
2 |
3 | # Supabase
4 | SUPABASE_URL=your-project-url
5 | SUPABASE_ANON_KEY=your-anon-key
6 |
7 | #Umami
8 | UMAMI_WEBSITE_ID=your-website-id
9 |
10 |
--------------------------------------------------------------------------------
/apps/app/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ["custom/next"],
3 | };
4 |
--------------------------------------------------------------------------------
/apps/app/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
38 | ./supabase
39 |
--------------------------------------------------------------------------------
/apps/app/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # app
2 |
3 | ## 0.0.3
4 |
5 | ### Patch Changes
6 |
7 | - [`f577fdc`](https://github.com/JadRizk/turborepo-launchpad/commit/f577fdcd5ab04413456ae1885cce8c0577a0a6e3)
8 | Thanks [@JadRizk](https://github.com/JadRizk)! - Refactor landing page
9 |
10 | - Updated dependencies
11 | [[`f577fdc`](https://github.com/JadRizk/turborepo-launchpad/commit/f577fdcd5ab04413456ae1885cce8c0577a0a6e3)]:
12 | - ui@0.0.2
13 |
14 | ## 0.0.2
15 |
16 | ### Patch Changes
17 |
18 | - [#19](https://github.com/JadRizk/turborepo-launchpad/pull/19)
19 | [`547690f`](https://github.com/JadRizk/turborepo-launchpad/commit/547690f3ad58252028fc8129c20a75f2d197f388)
20 | Thanks [@JadRizk](https://github.com/JadRizk)! - Fix local authentication
21 | issue
22 |
23 | ## 0.0.1
24 |
25 | ### Patch Changes
26 |
27 | - [#13](https://github.com/JadRizk/miniature-launchpad/pull/13)
28 | [`2290e14`](https://github.com/JadRizk/miniature-launchpad/commit/2290e14ede8fbaa169a61282934da798064c0bfe)
29 | Thanks [@JadRizk](https://github.com/JadRizk)! - 🚀 Initial Log
30 |
31 | - Updated dependencies
32 | [[`2290e14`](https://github.com/JadRizk/miniature-launchpad/commit/2290e14ede8fbaa169a61282934da798064c0bfe)]:
33 | - ui@0.0.1
34 |
--------------------------------------------------------------------------------
/apps/app/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with
2 | [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
3 |
4 | ## Getting Started
5 |
6 | First, run the development server:
7 |
8 | ```bash
9 | npm run dev
10 | # or
11 | yarn dev
12 | # or
13 | pnpm dev
14 | # or
15 | bun dev
16 | ```
17 |
18 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the
19 | result.
20 |
21 | You can start editing the page by modifying `app/page.tsx`. The page
22 | auto-updates as you edit the file.
23 |
24 | This project uses
25 | [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to
26 | automatically optimize and load Inter, a custom Google Font.
27 |
28 | ## Learn More
29 |
30 | To learn more about Next.js, take a look at the following resources:
31 |
32 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js
33 | features and API.
34 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
35 |
36 | You can check out
37 | [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your
38 | feedback and contributions are welcome!
39 |
40 | ## Deploy on Vercel
41 |
42 | The easiest way to deploy your Next.js app is to use the
43 | [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme)
44 | from the creators of Next.js.
45 |
46 | Check out our
47 | [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more
48 | details.
49 |
--------------------------------------------------------------------------------
/apps/app/lib/supabase/server.ts:
--------------------------------------------------------------------------------
1 | 'use server';
2 |
3 | import { cookies } from 'next/headers';
4 | import type { CookieOptions } from '@supabase/ssr';
5 | import { createServerClient } from '@supabase/ssr';
6 |
7 | async function createSupabaseServerClient() {
8 | const cookieStore = cookies();
9 | return createServerClient(
10 | process.env.SUPABASE_URL!,
11 | process.env.SUPABASE_ANON_KEY!,
12 | {
13 | cookies: {
14 | get(name: string) {
15 | return cookieStore.get(name)?.value;
16 | },
17 | set(name: string, value: string, options: CookieOptions) {
18 | cookieStore.set({ name, value, ...options });
19 | },
20 | remove(name: string, options: CookieOptions) {
21 | cookieStore.set({ name, ...options });
22 | },
23 | },
24 | },
25 | );
26 | }
27 |
28 | export default createSupabaseServerClient;
29 |
--------------------------------------------------------------------------------
/apps/app/middleware.ts:
--------------------------------------------------------------------------------
1 | import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs';
2 | import type { NextRequest } from 'next/server';
3 | import { NextResponse } from 'next/server';
4 |
5 | export async function middleware(req: NextRequest) {
6 | const res = NextResponse.next();
7 |
8 | const supabase = createMiddlewareClient({ req, res });
9 |
10 | await supabase.auth.getSession();
11 |
12 | return res;
13 | }
14 |
--------------------------------------------------------------------------------
/apps/app/next.config.js:
--------------------------------------------------------------------------------
1 | const getEnv = key => {
2 | const value = process.env[key];
3 | if (!value) throw Error(`Missing environment variable: ${key}`);
4 | return value;
5 | };
6 |
7 | // TODO: refactor to use the exported envs. Used now as a check
8 | module.exports = {
9 | reactStrictMode: true,
10 | transpilePackages: ['ui', 'tailwind-config'],
11 | experimental: {
12 | typedRoutes: true,
13 | },
14 | publicRuntimeConfig: {
15 | appUrl: getEnv('NEXT_APP_URL'),
16 | supaBaseUrl: getEnv('SUPABASE_URL'),
17 | supaKey: getEnv('SUPABASE_ANON_KEY'),
18 | },
19 | };
20 |
--------------------------------------------------------------------------------
/apps/app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "app",
3 | "version": "0.0.3",
4 | "private": false,
5 | "scripts": {
6 | "dev": "NODE_OPTIONS='--inspect' next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@hookform/error-message": "^2.0.1",
13 | "@hookform/resolvers": "^3.3.4",
14 | "@next/eslint-plugin-next": "^12.3.0",
15 | "@radix-ui/react-toast": "^1.1.5",
16 | "@supabase/auth-helpers-nextjs": "^0.8.7",
17 | "@supabase/ssr": "^0.0.10",
18 | "@supabase/supabase-js": "^2.38.5",
19 | "next": "14.0.3",
20 | "next-themes": "^0.2.1",
21 | "react": "^18",
22 | "react-confetti": "^6.1.0",
23 | "react-dom": "^18",
24 | "react-hook-form": "^7.48.2",
25 | "ui": "workspace:*",
26 | "zod": "^3.22.4"
27 | },
28 | "devDependencies": {
29 | "@types/node": "^20",
30 | "@types/react": "^18",
31 | "@types/react-dom": "^18",
32 | "autoprefixer": "^10.0.1",
33 | "date-fns": "^4.1.0",
34 | "eslint-config-custom": "workspace:*",
35 | "postcss": "^8",
36 | "tailwind-config": "workspace:*",
37 | "tailwindcss": "^3.3.0",
38 | "tsconfig": "workspace:*",
39 | "typescript": "^5"
40 | },
41 | "packageManager": "pnpm@9.1.1"
42 | }
43 |
--------------------------------------------------------------------------------
/apps/app/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/apps/app/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JadRizk/turborepo-launchpad/cacd6cb5565725e47a593c9fc609c31d137d7f15/apps/app/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/apps/app/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JadRizk/turborepo-launchpad/cacd6cb5565725e47a593c9fc609c31d137d7f15/apps/app/public/favicon-16x16.png
--------------------------------------------------------------------------------
/apps/app/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JadRizk/turborepo-launchpad/cacd6cb5565725e47a593c9fc609c31d137d7f15/apps/app/public/favicon.ico
--------------------------------------------------------------------------------
/apps/app/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/app/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/app/src/app/(splash)/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { ReactNode } from 'react';
2 | import { redirect } from 'next/navigation';
3 | import { NavBar } from '../../components/layouts/Navbar';
4 | import { ThemeToggle } from '../../components/layouts/ThemeToggle';
5 | import { getCurrentUser } from '../app/actions/user';
6 |
7 | const LandingLayout = async ({ children }: { children: ReactNode }) => {
8 | const user = await getCurrentUser();
9 |
10 | if (user) return redirect('/app');
11 | return (
12 |
13 |
16 |
{children}
17 |
25 |
26 | );
27 | };
28 |
29 | export default LandingLayout;
30 |
--------------------------------------------------------------------------------
/apps/app/src/app/(splash)/page.tsx:
--------------------------------------------------------------------------------
1 | import type { NextPage } from 'next';
2 | import Link from 'next/link';
3 | import { buttonVariants, cn } from 'ui';
4 |
5 | const Page: NextPage = () => {
6 | return (
7 |
8 |
9 |
10 | Build Faster. Scale Smarter.
11 |
12 |
13 | Launchpad is the monorepo framework designed for speed, power, and
14 | flexibility—so you can focus on what matters.
15 |
16 |
20 | Get Started
21 |
22 |
23 |
24 | );
25 | };
26 |
27 | export default Page;
28 |
--------------------------------------------------------------------------------
/apps/app/src/app/app/actions/user.ts:
--------------------------------------------------------------------------------
1 | 'use server';
2 |
3 | import createSupabaseServerClient from '../../../../lib/supabase/server';
4 |
5 | export async function getCurrentUser() {
6 | const supabase = await createSupabaseServerClient();
7 |
8 | const { data } = await supabase.auth.getSession();
9 |
10 | return data.session?.user;
11 | }
12 |
13 | export async function signOut() {
14 | const supabase = await createSupabaseServerClient();
15 |
16 | await supabase.auth.signOut();
17 | }
18 |
--------------------------------------------------------------------------------
/apps/app/src/app/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { ReactNode } from 'react';
2 | import { redirect } from 'next/navigation';
3 | import { AppNavBar } from '../../components/layouts/AppNavbar';
4 | import { getCurrentUser } from './actions/user';
5 |
6 | const AppLayout = async ({ children }: { children: ReactNode }) => {
7 | const user = await getCurrentUser();
8 |
9 | if (!user) return redirect('/auth/login');
10 |
11 | return (
12 |
22 | );
23 | };
24 |
25 | export default AppLayout;
26 |
--------------------------------------------------------------------------------
/apps/app/src/app/app/page.tsx:
--------------------------------------------------------------------------------
1 | import { Suspense } from 'react';
2 | import { UserWelcomeCard } from '../../modules/dashboard/components/UserWelcomeCard';
3 | import { getCurrentUser } from './actions/user';
4 | import { Skeleton } from 'ui';
5 |
6 | const Page = async () => {
7 | const user = await getCurrentUser();
8 |
9 | return (
10 |
11 |
12 | }>
13 |
14 |
15 |
16 |
17 | );
18 | };
19 |
20 | export default Page;
21 |
--------------------------------------------------------------------------------
/apps/app/src/app/app/settings/appearance/page.tsx:
--------------------------------------------------------------------------------
1 | import type { FC } from 'react';
2 | import { Separator } from 'ui/src/components/ui/separator';
3 | import { AppearanceForm } from '../../../../modules/settings/components/AppearanceForm';
4 |
5 | const AppearancePage: FC = () => {
6 | return (
7 |
8 |
9 |
Appearance
10 |
11 | Customize the appearance of the app. Automatically switch between day
12 | and night themes.
13 |
14 |
15 |
16 |
17 |
18 | );
19 | };
20 |
21 | export default AppearancePage;
22 |
--------------------------------------------------------------------------------
/apps/app/src/app/app/settings/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { ReactNode } from 'react';
2 | import { Separator } from 'ui/src/components/ui/separator';
3 | import { SidebarNav } from '../../../modules/settings/components/SidebarNav';
4 |
5 | const sidebarNavItems = [
6 | {
7 | title: 'Profile',
8 | href: '/app/settings',
9 | },
10 | {
11 | title: 'Appearance',
12 | href: '/app/settings/appearance',
13 | },
14 | ];
15 |
16 | const SettingsLayout = async ({ children }: { children: ReactNode }) => {
17 | return (
18 |
19 |
20 |
Settings
21 |
22 | Manage your account settings and set e-mail preferences.
23 |
24 |
25 |
26 |
27 |
30 |
{children}
31 |
32 |
33 | );
34 | };
35 |
36 | export default SettingsLayout;
37 |
--------------------------------------------------------------------------------
/apps/app/src/app/app/settings/page.tsx:
--------------------------------------------------------------------------------
1 | import { Separator } from 'ui/src/components/ui/separator';
2 | import { UpdateUserForm } from '../../../modules/auth/components/UpdateUserForm';
3 | import { getCurrentUser } from '../actions/user';
4 |
5 | const SettingsPage = async () => {
6 | const user = await getCurrentUser();
7 |
8 | return (
9 |
10 |
11 |
Profile
12 |
13 | This is how others will see you on the site.
14 |
15 |
16 |
17 |
18 |
19 | );
20 | };
21 |
22 | export default SettingsPage;
23 |
--------------------------------------------------------------------------------
/apps/app/src/app/auth/(public)/forgot-password/page.tsx:
--------------------------------------------------------------------------------
1 | import type { NextPage } from 'next';
2 | import Link from 'next/link';
3 | import { Icons } from 'ui';
4 | import { ForgotPasswordForm } from '../../../../modules/auth/components/ForgotPasswordForm';
5 |
6 | const Page: NextPage = () => {
7 | return (
8 |
9 |
10 |
11 |
12 | Forgot Your Password?
13 |
14 |
15 | No worries! Enter your email below to reset your password.
16 |
17 |
18 |
19 |
20 | Remembered your password?{' '}
21 |
25 | Sign in here
26 |
27 |
28 |
29 | );
30 | };
31 |
32 | export default Page;
33 |
--------------------------------------------------------------------------------
/apps/app/src/app/auth/(public)/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { ReactNode } from 'react';
2 | import React from 'react';
3 | import { redirect } from 'next/navigation';
4 | import { getCurrentUser } from '../../app/actions/user';
5 |
6 | const PublicAuthLayout = async ({ children }: { children: ReactNode }) => {
7 | const user = await getCurrentUser();
8 |
9 | if (user) return redirect('/app');
10 |
11 | return <> {children} >;
12 | };
13 |
14 | export default PublicAuthLayout;
15 |
--------------------------------------------------------------------------------
/apps/app/src/app/auth/(public)/login/page.tsx:
--------------------------------------------------------------------------------
1 | import type { NextPage } from 'next';
2 | import Link from 'next/link';
3 | import { Icons } from 'ui';
4 | import { LoginAuthForm } from '../../../../modules/auth/components/LoginAuthForm';
5 |
6 | const Page: NextPage = () => {
7 | return (
8 |
9 |
10 |
11 |
Welcome back
12 |
13 | Enter your email to sign in to your account
14 |
15 |
16 |
17 |
18 |
22 | Don't have an account? Sign Up
23 |
24 |
25 |
26 | );
27 | };
28 |
29 | export default Page;
30 |
--------------------------------------------------------------------------------
/apps/app/src/app/auth/(public)/register/page.tsx:
--------------------------------------------------------------------------------
1 | import type { NextPage } from 'next';
2 | import Link from 'next/link';
3 | import { Icons } from 'ui';
4 | import { RegisterWithEmailAndPasswordAuthForm } from '../../../../modules/auth/components/RegisterWithEmailAndPasswordAuthForm';
5 |
6 | const Page: NextPage = () => {
7 | return (
8 |
9 |
10 |
11 |
Register now!
12 |
13 | Enter your email to sign in to your account
14 |
15 |
16 |
17 |
18 |
22 | Have an account? Sign In
23 |
24 |
25 |
26 | );
27 | };
28 |
29 | export default Page;
30 |
--------------------------------------------------------------------------------
/apps/app/src/app/auth/actions/index.ts:
--------------------------------------------------------------------------------
1 | 'use server';
2 |
3 | import createSupabaseServerClient from '../../../../lib/supabase/server';
4 |
5 | export async function signUpWithEmailAndPassword(values: {
6 | email: string;
7 | password: string;
8 | }) {
9 | const supabase = await createSupabaseServerClient();
10 |
11 | const { data, error } = await supabase.auth.signUp({
12 | email: values.email,
13 | password: values.password,
14 | options: {
15 | emailRedirectTo: `${process.env.NEXT_APP_URL!}/auth/callback`,
16 | },
17 | });
18 |
19 | return { data, error };
20 | }
21 |
22 | export async function signInWithEmailAndPassword(values: {
23 | email: string;
24 | password: string;
25 | }) {
26 | const supabase = await createSupabaseServerClient();
27 |
28 | return supabase.auth.signInWithPassword(values);
29 | }
30 |
31 | export const signInWithRecoveryToken = async (code: string) => {
32 | const supabase = await createSupabaseServerClient();
33 |
34 | return supabase.auth.exchangeCodeForSession(code);
35 | };
36 |
37 | export async function signInWithEmail(email: string) {
38 | const supabase = await createSupabaseServerClient();
39 |
40 | // signup users if not available
41 | return supabase.auth.signInWithOtp({
42 | email,
43 | options: {
44 | emailRedirectTo: `${process.env.NEXT_APP_URL!}/auth/welcome`,
45 | },
46 | });
47 | }
48 |
49 | // Todo: Add loginWithGithub
50 |
51 | export async function resetPasswordForEmail(email: string) {
52 | const supabase = await createSupabaseServerClient();
53 |
54 | return supabase.auth.resetPasswordForEmail(email, {
55 | redirectTo: `${process.env.NEXT_APP_URL!}/auth/reset-password`,
56 | });
57 | }
58 |
59 | export async function updatePassword(password: string) {
60 | const supabase = await createSupabaseServerClient();
61 |
62 | return supabase.auth.updateUser({
63 | password,
64 | });
65 | }
66 |
--------------------------------------------------------------------------------
/apps/app/src/app/auth/callback/route.ts:
--------------------------------------------------------------------------------
1 | import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs';
2 | import { cookies } from 'next/headers';
3 | import { NextResponse } from 'next/server';
4 | import type { NextRequest } from 'next/server';
5 |
6 | export async function GET(request: NextRequest) {
7 | const requestUrl = new URL(request.url);
8 | const code = requestUrl.searchParams.get('code');
9 |
10 | if (code) {
11 | const cookieStore = cookies();
12 | const supabase = createRouteHandlerClient({
13 | cookies: () => cookieStore,
14 | });
15 | await supabase.auth.exchangeCodeForSession(code);
16 | }
17 |
18 | // URL to redirect to after sign in process completes
19 | return NextResponse.redirect(
20 | process.env.NEXT_APP_URL ?? `http://localhost:3000/`,
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/apps/app/src/app/auth/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { ReactNode } from 'react';
2 | import React, { Suspense } from 'react';
3 | import Link from 'next/link';
4 | import { buttonVariants, cn, Icons } from 'ui';
5 |
6 | const AuthLayout = async ({ children }: { children: ReactNode }) => {
7 | return (
8 |
9 |
16 |
17 |
18 |
19 |
20 | {children}
21 |
22 |
23 | );
24 | };
25 |
26 | export default AuthLayout;
27 |
--------------------------------------------------------------------------------
/apps/app/src/app/auth/reset-password/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import type { NextPage } from 'next';
4 | import { Icons, toast } from 'ui';
5 | import { useRouter, useSearchParams } from 'next/navigation';
6 | import { useEffect } from 'react';
7 | import { ResetPasswordForm } from '../../../modules/auth/components/ResetPasswordForm';
8 | import { signInWithRecoveryToken } from '../actions';
9 |
10 | const Page: NextPage = () => {
11 | const searchParams = useSearchParams();
12 | const { push } = useRouter();
13 | const token = searchParams.get('code');
14 |
15 | useEffect(() => {
16 | const handleCodeAuth = async () => {
17 | if (!token) {
18 | push('/');
19 | return;
20 | }
21 |
22 | const { error } = await signInWithRecoveryToken(token);
23 |
24 | if (error) {
25 | toast({ title: error.message, variant: 'destructive' });
26 | push('/');
27 | }
28 | };
29 |
30 | void handleCodeAuth();
31 | }, [push, token]);
32 |
33 | return (
34 |
35 |
36 |
37 |
38 | Reset Your Password
39 |
40 |
41 | Please enter your new password below.
42 |
43 |
44 |
45 |
46 | );
47 | };
48 |
49 | export default Page;
50 |
--------------------------------------------------------------------------------
/apps/app/src/app/auth/welcome/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import type { NextPage } from 'next';
4 | import { Icons, toast } from 'ui';
5 | import { useRouter, useSearchParams } from 'next/navigation';
6 | import { useEffect } from 'react';
7 | import ReactConfetti from 'react-confetti';
8 | import { signInWithRecoveryToken } from '../actions';
9 |
10 | const Page: NextPage = () => {
11 | const searchParams = useSearchParams();
12 | const { push } = useRouter();
13 | const code = searchParams.get('code');
14 |
15 | useEffect(() => {
16 | const handleCodeAuth = async () => {
17 | if (!code) {
18 | push('/');
19 | return;
20 | }
21 |
22 | const { error } = await signInWithRecoveryToken(code);
23 |
24 | if (error) {
25 | toast({ title: error.message, variant: 'destructive' });
26 | }
27 |
28 | setTimeout(() => {
29 | push('/');
30 | }, 5000);
31 | };
32 |
33 | void handleCodeAuth();
34 | }, [code, push]);
35 |
36 | return (
37 | <>
38 | {typeof window !== 'undefined' && window.innerHeight ? (
39 |
40 | ) : null}
41 |
42 |
43 |
44 |
45 | Welcome to New Beginnings!
46 |
47 |
48 | Embark on a journey of discovery and innovation with us. Let's
49 | create something extraordinary together.
50 |
51 |
52 |
53 | >
54 | );
55 | };
56 |
57 | export default Page;
58 |
--------------------------------------------------------------------------------
/apps/app/src/app/experimental/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useState } from 'react';
4 | import { Button, cn, Icons } from 'ui';
5 | import type { NextPage } from 'next';
6 | import { RegisterWithEmailAndPasswordAuthForm } from '../../modules/auth/components/RegisterWithEmailAndPasswordAuthForm';
7 | import '../../modules/auth/styles/auth.css';
8 | import { LoginAuthForm } from '../../modules/auth/components/LoginAuthForm';
9 |
10 | const Page: NextPage = () => {
11 | const [signUpMode, setSignUpMode] = useState(false);
12 |
13 | return (
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | Register now!
23 |
24 |
25 | Enter your email to sign in to your account
26 |
27 |
28 |
29 |
30 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | Welcome back
47 |
48 |
49 | Enter your email to sign in to your account
50 |
51 |
52 |
53 |
54 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | );
69 | };
70 |
71 | export default Page;
72 |
--------------------------------------------------------------------------------
/apps/app/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from 'next';
2 | import { Oswald } from 'next/font/google';
3 | import 'ui/styles/globals.css';
4 | import React from 'react';
5 | import { Toaster } from 'ui';
6 | import { ThemeProvider } from '../components/layouts/ThemeProvider';
7 | import Script from 'next/script';
8 |
9 | const inter = Oswald({ subsets: ['latin'] });
10 |
11 | // TODO: update the site metadata
12 | export const metadata: Metadata = {
13 | title: 'Launchpad',
14 | description: 'Turbo app starter',
15 | icons: {
16 | icon: '/favicon.ico',
17 | shortcut: '/favicon-16x16.png',
18 | apple: '/apple-touch-icon.png',
19 | },
20 | };
21 |
22 | // TODO: Add global providers over here
23 | const RootLayout = ({ children }: { children: React.ReactNode }) => (
24 |
25 |
26 |
33 | {children}
34 |
35 |
36 |
37 |
42 |
43 | );
44 |
45 | export default RootLayout;
46 |
--------------------------------------------------------------------------------
/apps/app/src/components/form/AppForm.tsx:
--------------------------------------------------------------------------------
1 | import type { DefaultValues, FieldValues } from 'react-hook-form';
2 | import { FormProvider, useForm } from 'react-hook-form';
3 | import type { Schema } from 'zod';
4 | import { zodResolver } from '@hookform/resolvers/zod';
5 | import type { ReactNode } from 'react';
6 |
7 | export type AppFormProps = {
8 | onSubmit: (input: T) => void;
9 | shouldUnregister?: boolean;
10 | defaultValues?: DefaultValues;
11 | children: ReactNode;
12 | schema: Schema;
13 | };
14 |
15 | export function AppForm({
16 | defaultValues,
17 | onSubmit,
18 | children,
19 | schema,
20 | shouldUnregister = true,
21 | }: AppFormProps) {
22 | const methods = useForm({
23 | mode: 'onChange',
24 | shouldUseNativeValidation: false,
25 | defaultValues,
26 | resolver: zodResolver(schema),
27 | shouldUnregister,
28 | delayError: 2000,
29 | });
30 |
31 | const handleSubmit = methods.handleSubmit(onSubmit);
32 |
33 | return (
34 |
35 |
38 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/apps/app/src/components/form/FormFieldset.tsx:
--------------------------------------------------------------------------------
1 | import type { ReactNode } from 'react';
2 | import { cn } from 'ui';
3 | import { ErrorMessage } from '@hookform/error-message';
4 | import type { FieldValues, Path } from 'react-hook-form';
5 | import { useFormContext } from 'react-hook-form';
6 | import { FormField, FormItem, FormLabel } from 'ui/src/components/ui/form';
7 |
8 | export type FormFieldsetProps = {
9 | path: Path;
10 | label?: string;
11 | children?: ReactNode;
12 | };
13 |
14 | export function FormFieldset({
15 | path,
16 | label,
17 | children,
18 | }: FormFieldsetProps) {
19 | const {
20 | control,
21 | formState: { errors },
22 | } = useFormContext();
23 |
24 | return (
25 | (
29 |
30 | {label}
31 | {children}
32 |
33 | (
37 |
46 | {message}
47 |
48 | )}
49 | />
50 |
51 | )}
52 | />
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/apps/app/src/components/form/FormInputField.tsx:
--------------------------------------------------------------------------------
1 | import type { InputProps } from 'ui';
2 | import { Input } from 'ui';
3 | import type { FieldValues, Path } from 'react-hook-form';
4 | import { useFormContext, useController } from 'react-hook-form';
5 | import { FormFieldset } from './FormFieldset';
6 |
7 | export type FormInputFieldProps = Omit<
8 | InputProps,
9 | 'id' | 'name' | 'invalid'
10 | > & {
11 | path: Path;
12 | label?: string;
13 | };
14 |
15 | export function FormInputField({
16 | path,
17 | type = 'text',
18 | placeholder,
19 | label,
20 | onChange,
21 | disabled,
22 | onBlur,
23 | children,
24 | ...rest
25 | }: FormInputFieldProps) {
26 | const { register, control } = useFormContext();
27 |
28 | const { fieldState } = useController({
29 | name: path,
30 | control,
31 | });
32 |
33 | return (
34 | label={label} path={path}>
35 | {
43 | if (!value) return null;
44 | if (
45 | type === 'number' ||
46 | rest.inputMode === 'numeric' ||
47 | rest.inputMode === 'decimal'
48 | ) {
49 | const output = parseFloat(value);
50 | return isNaN(output) ? undefined : output;
51 | }
52 | return value;
53 | },
54 | onChange,
55 | onBlur,
56 | })}
57 | autoCapitalize='none'
58 | autoCorrect='off'
59 | disabled={disabled}
60 | >
61 | {children}
62 |
63 |
64 | );
65 | }
66 |
--------------------------------------------------------------------------------
/apps/app/src/components/form/FormSelectField.tsx:
--------------------------------------------------------------------------------
1 | import type { FieldValues, Path } from 'react-hook-form';
2 | import { useFormContext, useController } from 'react-hook-form';
3 | import { FormFieldset } from './FormFieldset';
4 | import {
5 | Select,
6 | SelectContent,
7 | SelectItem,
8 | SelectTrigger,
9 | SelectValue,
10 | } from 'ui';
11 |
12 | export type FormSelectFieldProps = {
13 | path: Path;
14 | label?: string;
15 | options: { label: string; value: string }[];
16 | disabled?: boolean;
17 | };
18 |
19 | export function FormSelectField({
20 | path,
21 | label,
22 | options,
23 | disabled,
24 | }: FormSelectFieldProps) {
25 | const { control, setValue } = useFormContext();
26 |
27 | const {
28 | field: { value },
29 | fieldState,
30 | } = useController({ name: path, control });
31 |
32 | return (
33 | label={label} path={path}>
34 |
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/apps/app/src/components/layouts/AppNavbar.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import type { FC, ReactNode } from 'react';
4 | import { Icons } from 'ui';
5 | import { ProfileIconMenu } from '../../modules/user/components/ProfileIconMenu';
6 |
7 | interface AppNavbarProps {
8 | children?: ReactNode;
9 | }
10 |
11 | export const AppNavBar: FC = () => {
12 | return (
13 |
24 | );
25 | };
26 |
--------------------------------------------------------------------------------
/apps/app/src/components/layouts/Navbar.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import type { FC, ReactNode } from 'react';
4 | import { useState } from 'react';
5 | import { Button, buttonVariants, cn, Icons } from 'ui';
6 | import { useRouter } from 'next/navigation';
7 |
8 | interface NavbarProps {
9 | children?: ReactNode;
10 | }
11 |
12 | export const NavBar: FC = () => {
13 | const { push } = useRouter();
14 |
15 | const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
16 |
17 | const toggleMobileMenu = () => {
18 | setIsMobileMenuOpen(!isMobileMenuOpen);
19 | };
20 |
21 | const onLoginClick = () => {
22 | push('/auth/login');
23 | };
24 |
25 | return (
26 |
77 | );
78 | };
79 |
--------------------------------------------------------------------------------
/apps/app/src/components/layouts/ThemeProvider.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import { ThemeProvider as NextThemesProvider } from 'next-themes';
5 | import { type ThemeProviderProps } from 'next-themes/dist/types';
6 |
7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
8 | return {children};
9 | }
10 |
--------------------------------------------------------------------------------
/apps/app/src/components/layouts/ThemeToggle.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import { useTheme } from 'next-themes';
5 | import type { FC } from 'react';
6 | import {
7 | Button,
8 | DropdownMenu,
9 | DropdownMenuContent,
10 | DropdownMenuItem,
11 | DropdownMenuTrigger,
12 | Icons,
13 | } from 'ui';
14 |
15 | export const ThemeToggle: FC = () => {
16 | const { setTheme } = useTheme();
17 |
18 | return (
19 |
20 |
21 |
26 |
27 |
28 | {
30 | setTheme('light');
31 | }}
32 | >
33 | Light
34 |
35 | {
37 | setTheme('dark');
38 | }}
39 | >
40 | Dark
41 |
42 | {
44 | setTheme('system');
45 | }}
46 | >
47 | System
48 |
49 |
50 |
51 | );
52 | };
53 |
--------------------------------------------------------------------------------
/apps/app/src/modules/auth/actions.ts:
--------------------------------------------------------------------------------
1 | 'use server';
2 |
3 | import createSupabaseServerClient from '../../../lib/supabase/server';
4 |
5 | export async function updateUser(values: { phone: string }) {
6 | const supabase = await createSupabaseServerClient();
7 |
8 | return supabase.auth.updateUser(values);
9 | }
10 |
--------------------------------------------------------------------------------
/apps/app/src/modules/auth/components/ForgotPasswordForm.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import type { FC } from 'react';
4 | import { useState } from 'react';
5 | import { Button, useToast } from 'ui';
6 | import { useRouter } from 'next/navigation';
7 | import { resetPasswordForEmail } from '../../../app/auth/actions';
8 | import { AppForm } from '../../../components/form/AppForm';
9 | import type { EmailFormValues } from '../validations';
10 | import { emailFormSchema } from '../validations';
11 | import { FormInputField } from '../../../components/form/FormInputField';
12 |
13 | export const ForgotPasswordForm: FC = () => {
14 | const [isLoading, setIsLoading] = useState(false);
15 | const { push } = useRouter();
16 | const { toast } = useToast();
17 |
18 | const onSubmit = async ({ email }: EmailFormValues) => {
19 | setIsLoading(true);
20 |
21 | const { error } = await resetPasswordForEmail(email);
22 |
23 | try {
24 | if (error) {
25 | toast({ title: error.message, variant: 'destructive' });
26 | return;
27 | }
28 |
29 | toast({
30 | title: 'Check Your Email',
31 | description:
32 | "We've sent a password reset link to your email. Please click the link to set a new password.",
33 | });
34 | push('/auth/login');
35 | } catch (err) {
36 | toast({
37 | title: 'Error',
38 | description: 'An unexpected error occurred. Please try again.',
39 | variant: 'destructive',
40 | });
41 | } finally {
42 | setIsLoading(false);
43 | }
44 | };
45 |
46 | return (
47 |
48 |
49 |
50 |
51 | label='Email'
52 | path='email'
53 | placeholder='name@example.com'
54 | />
55 |
56 |
59 |
60 |
61 |
62 | );
63 | };
64 |
--------------------------------------------------------------------------------
/apps/app/src/modules/auth/components/LoginAuthForm.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import type { FC } from 'react';
4 | import { Suspense, lazy, useState } from 'react';
5 | import { buttonVariants, cn, Icons, Skeleton } from 'ui';
6 |
7 | enum AuthFormType {
8 | EmailAndPassword = 'emailAndPassword',
9 | MagicLink = 'magicLink',
10 | }
11 |
12 | const LoginWithEmailAndPasswordAuthForm = lazy(
13 | () => import('./LoginWithEmailAndPasswordAuthForm'),
14 | );
15 | const LoginWithEmailAuthForm = lazy(() => import('./LoginWithEmailAuthForm')); // Assuming you also have a component for this
16 |
17 | const authFormMapper: Record = {
18 | emailAndPassword: ,
19 | magicLink: ,
20 | };
21 |
22 | export const LoginAuthFormLoading: FC = () => {
23 | return (
24 |
25 |
26 |
27 |
28 | );
29 | };
30 |
31 | export const LoginAuthForm: FC = () => {
32 | const [currentForm, setCurrentForm] = useState(
33 | AuthFormType.EmailAndPassword,
34 | );
35 |
36 | const isMagicLinkForm = currentForm === AuthFormType.MagicLink;
37 |
38 | return (
39 |
40 |
}>
41 | {authFormMapper[currentForm]}
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | Or continue with
52 |
53 |
54 |
55 |
56 |
69 |
77 |
78 |
79 |
80 | );
81 | };
82 |
--------------------------------------------------------------------------------
/apps/app/src/modules/auth/components/LoginWithEmailAndPasswordAuthForm.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import type { FC } from 'react';
4 | import { useState } from 'react';
5 | import { Button, useToast } from 'ui';
6 | import { useRouter } from 'next/navigation';
7 | import Link from 'next/link';
8 | import { signInWithEmailAndPassword } from '../../../app/auth/actions';
9 | import { FormInputField } from '../../../components/form/FormInputField';
10 | import { AppForm } from '../../../components/form/AppForm';
11 | import type { LoginEmailAndPasswordFormValues } from '../validations';
12 | import { loginWithEmailAndPasswordSchema } from '../validations';
13 |
14 | const LoginWithEmailAndPasswordAuthForm: FC = () => {
15 | const [isLoading, setIsLoading] = useState(false);
16 | const { push } = useRouter();
17 | const { toast } = useToast();
18 |
19 | const onSubmit = async (formValues: LoginEmailAndPasswordFormValues) => {
20 | setIsLoading(true);
21 |
22 | const { error } = await signInWithEmailAndPassword(formValues);
23 |
24 | try {
25 | if (error) {
26 | toast({ title: error.message, variant: 'destructive' });
27 | return;
28 | }
29 |
30 | toast({ title: 'Login successful!' });
31 | push('/app');
32 | } catch (err) {
33 | toast({
34 | title: 'Error',
35 | description: 'An unexpected error occurred. Please try again.',
36 | variant: 'destructive',
37 | });
38 | } finally {
39 | setIsLoading(false);
40 | }
41 | };
42 |
43 | return (
44 |
45 |
46 |
47 |
48 | label='Email'
49 | path='email'
50 | placeholder='name@example.com'
51 | />
52 |
53 |
54 |
55 | label='Password'
56 | path='password'
57 | placeholder='********'
58 | type='password'
59 | />
60 |
61 |
65 | Forgot your password?
66 |
67 |
68 |
69 |
72 |
73 |
74 |
75 | );
76 | };
77 | export default LoginWithEmailAndPasswordAuthForm;
78 |
--------------------------------------------------------------------------------
/apps/app/src/modules/auth/components/LoginWithEmailAuthForm.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import type { FC } from 'react';
4 | import { useState } from 'react';
5 | import { Button, useToast } from 'ui';
6 | import { useRouter } from 'next/navigation';
7 | import { signInWithEmail } from '../../../app/auth/actions';
8 | import { FormInputField } from '../../../components/form/FormInputField';
9 | import { AppForm } from '../../../components/form/AppForm';
10 | import type { EmailFormValues } from '../validations';
11 | import { emailFormSchema } from '../validations';
12 |
13 | const LoginWithEmailAuthForm: FC = () => {
14 | const [isLoading, setIsLoading] = useState(false);
15 | const { toast } = useToast();
16 | const { push } = useRouter();
17 |
18 | const onSubmit = async ({ email }: EmailFormValues) => {
19 | setIsLoading(true);
20 |
21 | const { error } = await signInWithEmail(email);
22 |
23 | try {
24 | if (error) {
25 | toast({ title: error.message, variant: 'destructive' });
26 | return;
27 | }
28 |
29 | toast({
30 | title: 'Check Your Email',
31 | description: "We've sent a magic link to your email!",
32 | });
33 | push('/');
34 | } catch (err) {
35 | toast({
36 | title: 'Error',
37 | description: 'An unexpected error occurred. Please try again.',
38 | variant: 'destructive',
39 | });
40 | } finally {
41 | setIsLoading(false);
42 | }
43 | };
44 |
45 | return (
46 |
47 |
48 |
49 |
50 | label='Email'
51 | path='email'
52 | placeholder='name@example.com'
53 | />
54 |
57 |
58 |
59 |
60 | );
61 | };
62 | export default LoginWithEmailAuthForm;
63 |
--------------------------------------------------------------------------------
/apps/app/src/modules/auth/components/RegisterWithEmailAndPasswordAuthForm.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import type { FC } from 'react';
4 | import { useState } from 'react';
5 | import { Button, buttonVariants, cn, Icons, useToast } from 'ui';
6 | import { useRouter } from 'next/navigation';
7 | import { signUpWithEmailAndPassword } from '../../../app/auth/actions';
8 | import { AppForm } from '../../../components/form/AppForm';
9 | import { FormInputField } from '../../../components/form/FormInputField';
10 | import type { RegisterEmailAndPasswordFormValues } from '../validations';
11 | import { registerWithEmailAndPasswordSchema } from '../validations';
12 |
13 | export const RegisterWithEmailAndPasswordAuthForm: FC = () => {
14 | const [isLoading, setIsLoading] = useState(false);
15 | const { push } = useRouter();
16 | const { toast } = useToast();
17 |
18 | const onSubmit = async ({
19 | email,
20 | password,
21 | }: RegisterEmailAndPasswordFormValues) => {
22 | setIsLoading(true);
23 |
24 | const { error } = await signUpWithEmailAndPassword({ email, password });
25 |
26 | try {
27 | if (error) {
28 | toast({ title: error.message, variant: 'destructive' });
29 | return;
30 | }
31 |
32 | toast({ title: 'User created!' });
33 | push('/auth/login');
34 | } catch (err) {
35 | toast({
36 | title: 'Error',
37 | description: 'An unexpected error occurred. Please try again.',
38 | variant: 'destructive',
39 | });
40 | } finally {
41 | setIsLoading(false);
42 | }
43 | };
44 |
45 | return (
46 |
47 |
48 |
49 |
50 | label='Email'
51 | path='email'
52 | placeholder='name@example.com'
53 | />
54 |
55 | label='Password'
56 | path='password'
57 | placeholder='********'
58 | type='password'
59 | />
60 |
61 | label='Confirm password'
62 | path='confirmPassword'
63 | placeholder='********'
64 | type='password'
65 | />
66 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | Or continue with
78 |
79 |
80 |
81 |
89 |
90 | );
91 | };
92 |
--------------------------------------------------------------------------------
/apps/app/src/modules/auth/components/ResetPasswordForm.tsx:
--------------------------------------------------------------------------------
1 | import type { FC } from 'react';
2 | import React, { useState } from 'react';
3 | import { Button, useToast } from 'ui';
4 | import { useRouter } from 'next/navigation';
5 | import { updatePassword } from '../../../app/auth/actions';
6 | import { AppForm } from '../../../components/form/AppForm';
7 | import type { ResetPasswordFormValues } from '../validations/ResetPasswordSchema';
8 | import { resetPasswordSchema } from '../validations/ResetPasswordSchema';
9 | import { FormInputField } from '../../../components/form/FormInputField';
10 |
11 | export const ResetPasswordForm: FC = () => {
12 | const [isLoading, setIsLoading] = useState(false);
13 | const { push } = useRouter();
14 | const { toast } = useToast();
15 |
16 | const onSubmit = async ({ password }: ResetPasswordFormValues) => {
17 | setIsLoading(true);
18 | const { error } = await updatePassword(password);
19 |
20 | try {
21 | if (error) {
22 | toast({
23 | title: error.message,
24 | variant: 'destructive',
25 | });
26 | return;
27 | }
28 |
29 | toast({
30 | title: 'Password updated!',
31 | description: 'Your password has been successfully updated.',
32 | });
33 | push('/');
34 | } catch (err) {
35 | toast({
36 | title: 'Error',
37 | description: 'An unexpected error occurred. Please try again.',
38 | variant: 'destructive',
39 | });
40 | } finally {
41 | setIsLoading(false);
42 | }
43 | };
44 |
45 | return (
46 |
47 |
48 |
49 |
50 | label='Password'
51 | path='password'
52 | placeholder='********'
53 | type='password'
54 | />
55 |
56 | label='Confirm password'
57 | path='confirmPassword'
58 | placeholder='********'
59 | type='password'
60 | />
61 |
64 |
65 |
66 |
67 | );
68 | };
69 |
--------------------------------------------------------------------------------
/apps/app/src/modules/auth/components/UpdateUserForm.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import type { FC } from 'react';
4 | import { useState } from 'react';
5 | import { Button, useToast } from 'ui';
6 | import { z } from 'zod';
7 | import type { User } from '@supabase/auth-helpers-nextjs';
8 | import { AppForm } from '../../../components/form/AppForm';
9 | import { FormInputField } from '../../../components/form/FormInputField';
10 | import { updateUser } from '../actions';
11 | import { format } from 'date-fns';
12 |
13 | const updateUserFormSchema = z.object({
14 | phone: z
15 | .string({
16 | invalid_type_error: 'Phone is required',
17 | required_error: 'Phone is required.',
18 | })
19 | .regex(
20 | /^\+?\d+$/,
21 | 'Phone number must be a string of numbers and may start with a plus',
22 | ),
23 | });
24 |
25 | type UpdateUserFormValues = z.infer;
26 |
27 | export const UpdateUserForm: FC<{ user: User | undefined }> = ({ user }) => {
28 | const [isLoading, setIsLoading] = useState(false);
29 | const { toast } = useToast();
30 | const onSubmit = async (data: UpdateUserFormValues) => {
31 | setIsLoading(true);
32 |
33 | const { error } = await updateUser(data);
34 |
35 | try {
36 | if (error) {
37 | toast({ title: error.message, variant: 'destructive' });
38 | return;
39 | }
40 |
41 | toast({ title: 'User updated!' });
42 | } catch (err) {
43 | toast({
44 | title: 'Error',
45 | description: 'An unexpected error occurred. Please try again.',
46 | variant: 'destructive',
47 | });
48 | } finally {
49 | setIsLoading(false);
50 | }
51 | };
52 |
53 | return (
54 |
59 |
60 |
67 |
68 |
69 |
70 |
81 |
82 |
85 |
86 |
87 | );
88 | };
89 |
--------------------------------------------------------------------------------
/apps/app/src/modules/auth/styles/auth.css:
--------------------------------------------------------------------------------
1 | /* Base container style */
2 | .container {
3 | position: relative;
4 | width: 100%;
5 | min-height: 100vh;
6 | overflow: hidden;
7 | }
8 |
9 | /* Styling for the large circular gradient background */
10 | .container:before {
11 | content: '';
12 | position: absolute;
13 | height: 2000px;
14 | width: 2000px;
15 | top: -10%;
16 | right: 48%;
17 | transform: translateY(-50%);
18 | transition: 1.8s ease-in-out;
19 | border-radius: 50%;
20 | z-index: 6;
21 | background: linear-gradient(
22 | 103deg,
23 | rgba(0, 210, 255, 1) 0%,
24 | rgba(146, 141, 171, 1) 100%
25 | );
26 | }
27 |
28 | /* Container for forms */
29 | .forms-container {
30 | position: absolute;
31 | width: 100%;
32 | height: 100%;
33 | top: 0;
34 | left: 0;
35 | }
36 |
37 | /* Styling for sign-in and sign-up sections */
38 | .signin-signup {
39 | position: absolute;
40 | top: 50%;
41 | transform: translate(-50%, -50%);
42 | left: 75%;
43 | width: 50%;
44 | transition: 1s 0.7s ease-in-out;
45 | display: grid;
46 | grid-template-columns: 1fr;
47 | z-index: 5;
48 | }
49 |
50 | /* Default styles for forms */
51 | .sign-up-form, .sign-in-form {
52 | transition: opacity 0.5s ease, transform 0.5s ease;
53 | display: none;
54 | opacity: 0;
55 | z-index: 1;
56 | }
57 |
58 | /* Visible form styles */
59 | .sign-in-form {
60 | display: block;
61 | opacity: 1;
62 | z-index: 2;
63 | }
64 |
65 | /* Toggle styles for sign-up mode */
66 | .container.sign-up-mode:before {
67 | transform: translate(100%, -50%);
68 | right: 52%;
69 | }
70 |
71 | .container.sign-up-mode .signin-signup {
72 | left: 25%;
73 | }
74 |
75 | .container.sign-up-mode .sign-up-form {
76 | display: block;
77 | opacity: 1;
78 | z-index: 2;
79 | }
80 |
81 | .container.sign-up-mode .sign-in-form {
82 | display: none;
83 | opacity: 0;
84 | z-index: 1;
85 | }
86 |
87 | /* Responsive styles for smaller screens */
88 | @media (max-width: 870px) {
89 | .container:before {
90 | width: 1500px;
91 | height: 1500px;
92 | transform: translateX(-50%);
93 | left: 30%;
94 | bottom: 68%;
95 | transition: 2s ease-in-out;
96 | }
97 |
98 | .signin-signup {
99 | width: 100%;
100 | top: 95%;
101 | transform: translate(-50%, -100%);
102 | transition: 1s 0.8s ease-in-out;
103 | left: 50%;
104 | }
105 |
106 | .container.sign-up-mode:before {
107 | transform: translate(-50%, 100%);
108 | bottom: 32%;
109 | }
110 |
111 | .container.sign-up-mode .signin-signup {
112 | top: 5%;
113 | transform: translate(-50%, 0);
114 | }
115 | }
116 |
117 | @media (max-width: 570px) {
118 | form {
119 | padding: 0 1.5rem;
120 | }
121 |
122 | .container:before {
123 | bottom: 72%;
124 | left: 50%;
125 | }
126 |
127 | .container.sign-up-mode:before {
128 | bottom: 28%;
129 | left: 50%;
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/apps/app/src/modules/auth/validations/EmailFormSchema.ts:
--------------------------------------------------------------------------------
1 | import type { ZodType } from 'zod';
2 | import { z } from 'zod';
3 |
4 | export type EmailFormValues = {
5 | email: string;
6 | };
7 |
8 | export const emailFormSchema: ZodType = z.object({
9 | email: z
10 | .string({
11 | invalid_type_error: 'Email is required',
12 | required_error: 'Email is required.',
13 | })
14 | .email({
15 | message: 'Invalid email.',
16 | }),
17 | });
18 |
--------------------------------------------------------------------------------
/apps/app/src/modules/auth/validations/LoginWithEmailAndPasswordSchema.ts:
--------------------------------------------------------------------------------
1 | import type { ZodType } from 'zod';
2 | import { z } from 'zod';
3 |
4 | export type LoginEmailAndPasswordFormValues = {
5 | email: string;
6 | password: string;
7 | };
8 |
9 | export const loginWithEmailAndPasswordSchema: ZodType =
10 | z.object({
11 | email: z
12 | .string({
13 | invalid_type_error: 'Email is required',
14 | required_error: 'Email is required.',
15 | })
16 | .email({
17 | message: 'Invalid email.',
18 | }),
19 | password: z.string({
20 | required_error: 'Password is required.',
21 | invalid_type_error: 'Password is required.',
22 | }),
23 | });
24 |
--------------------------------------------------------------------------------
/apps/app/src/modules/auth/validations/RegisterWithEmailAndPasswordSchema.ts:
--------------------------------------------------------------------------------
1 | import type { ZodType } from 'zod';
2 | import { z } from 'zod';
3 |
4 | export type RegisterEmailAndPasswordFormValues = {
5 | email: string;
6 | password: string;
7 | confirmPassword: string;
8 | };
9 |
10 | export const registerWithEmailAndPasswordSchema: ZodType =
11 | z
12 | .object({
13 | email: z
14 | .string({
15 | invalid_type_error: 'Email is required',
16 | required_error: 'Email is required.',
17 | })
18 | .email({
19 | message: 'Invalid email.',
20 | }),
21 | password: z
22 | .string({
23 | required_error: 'Password is required.',
24 | invalid_type_error: 'Password is required.',
25 | })
26 | .min(8, { message: 'Password must be at least 8 characters long.' })
27 | .regex(/[a-z]/, {
28 | message: 'Password must contain at least one lowercase letter.',
29 | })
30 | .regex(/[A-Z]/, {
31 | message: 'Password must contain at least one uppercase letter.',
32 | })
33 | .regex(/[0-9]/, {
34 | message: 'Password must contain at least one number.',
35 | })
36 | .regex(/[\W_]/, {
37 | message: 'Password must contain at least one special character.',
38 | }),
39 | confirmPassword: z.string({
40 | required_error: 'Confirmation password is required.',
41 | invalid_type_error: 'Confirmation password is required.',
42 | }),
43 | })
44 | .refine(values => values.password === values.confirmPassword, {
45 | message: 'Passwords must match!',
46 | path: ['confirmPassword'],
47 | });
48 |
--------------------------------------------------------------------------------
/apps/app/src/modules/auth/validations/ResetPasswordSchema.ts:
--------------------------------------------------------------------------------
1 | import type { ZodType } from 'zod';
2 | import { z } from 'zod';
3 |
4 | export type ResetPasswordFormValues = {
5 | password: string;
6 | confirmPassword: string;
7 | };
8 |
9 | export const resetPasswordSchema: ZodType = z
10 | .object({
11 | password: z
12 | .string({
13 | required_error: 'Password is required.',
14 | invalid_type_error: 'Password is required.',
15 | })
16 | .min(8, { message: 'Password must be at least 8 characters long.' })
17 | .regex(/[a-z]/, {
18 | message: 'Password must contain at least one lowercase letter.',
19 | })
20 | .regex(/[A-Z]/, {
21 | message: 'Password must contain at least one uppercase letter.',
22 | })
23 | .regex(/[0-9]/, {
24 | message: 'Password must contain at least one number.',
25 | })
26 | .regex(/[\W_]/, {
27 | message: 'Password must contain at least one special character.',
28 | }),
29 | confirmPassword: z.string({
30 | required_error: 'Confirmation password is required.',
31 | invalid_type_error: 'Confirmation password is required.',
32 | }),
33 | })
34 | .refine(values => values.password === values.confirmPassword, {
35 | message: 'Passwords must match!',
36 | path: ['confirmPassword'],
37 | });
38 |
--------------------------------------------------------------------------------
/apps/app/src/modules/auth/validations/index.ts:
--------------------------------------------------------------------------------
1 | export * from './LoginWithEmailAndPasswordSchema';
2 | export * from './RegisterWithEmailAndPasswordSchema';
3 | export * from './EmailFormSchema';
4 |
--------------------------------------------------------------------------------
/apps/app/src/modules/dashboard/components/UserWelcomeCard.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import type { FC } from 'react';
4 | import type { User } from '@supabase/auth-helpers-nextjs';
5 | import { formatDistanceToNow } from 'date-fns';
6 | import { BorderBeam, Card, CardContent, CardHeader, CardTitle } from 'ui';
7 |
8 | export const UserWelcomeCard: FC<{ user: User | undefined }> = ({ user }) => {
9 | const userName = user?.user_metadata?.name || user?.email || 'Awesome Human';
10 | const accountCreatedAt = user?.created_at ? new Date(user.created_at) : null;
11 |
12 | const membershipDuration = accountCreatedAt
13 | ? formatDistanceToNow(accountCreatedAt, { addSuffix: true })
14 | : 'since the dawn of time';
15 |
16 | return (
17 |
18 |
19 |
20 | Welcome back, {userName}! 🎉
21 |
22 |
23 |
24 |
25 | You've started this amazing journey {membershipDuration}..
26 |
27 |
28 | Keep being awesome, and let’s build something great today! 💡
29 |
30 |
31 |
32 |
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/apps/app/src/modules/settings/components/AppearanceForm.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useState } from 'react';
4 | import { z } from 'zod';
5 | import { Button, useToast } from 'ui';
6 | import type { FC } from 'react';
7 | import { useTheme } from 'next-themes';
8 | import { AppForm } from '../../../components/form/AppForm';
9 | import { FormSelectField } from '../../../components/form/FormSelectField';
10 |
11 | const appearanceFormSchema = z.object({
12 | theme: z.enum(['light', 'dark', 'system'], {
13 | required_error: 'Please select a theme.',
14 | }),
15 | });
16 |
17 | type AppearanceFormValues = z.infer;
18 |
19 | export const AppearanceForm: FC = () => {
20 | const { theme, setTheme } = useTheme();
21 | const [isLoading, setIsLoading] = useState(false);
22 | const { toast } = useToast();
23 |
24 | const onSubmit = async (data: AppearanceFormValues) => {
25 | setIsLoading(true);
26 | try {
27 | setTheme(data.theme);
28 |
29 | toast({
30 | title: 'Preferences updated!',
31 | description: `Theme set to ${data.theme}.`,
32 | });
33 | } catch (err) {
34 | toast({
35 | title: 'Error',
36 | description: 'Failed to update theme. Please try again.',
37 | variant: 'destructive',
38 | });
39 | } finally {
40 | setIsLoading(false);
41 | }
42 | };
43 |
44 | return (
45 |
50 |
51 |
60 |
61 |
64 |
65 |
66 |
67 | );
68 | };
69 |
--------------------------------------------------------------------------------
/apps/app/src/modules/settings/components/SidebarNav.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import Link from 'next/link';
4 | import { usePathname } from 'next/navigation';
5 | import { buttonVariants, cn } from 'ui';
6 |
7 | interface SidebarNavProps extends React.HTMLAttributes {
8 | items: {
9 | href: string;
10 | title: string;
11 | }[];
12 | }
13 |
14 | export function SidebarNav({ items }: SidebarNavProps) {
15 | const pathname = usePathname();
16 |
17 | return (
18 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/apps/app/src/modules/user/components/ProfileIconMenu.tsx:
--------------------------------------------------------------------------------
1 | import type { FC } from 'react';
2 | import {
3 | Button,
4 | DropdownMenu,
5 | DropdownMenuContent,
6 | DropdownMenuGroup,
7 | DropdownMenuItem,
8 | DropdownMenuSeparator,
9 | DropdownMenuTrigger,
10 | Icons,
11 | } from 'ui';
12 | import * as React from 'react';
13 | import { useRouter } from 'next/navigation';
14 | import { signOut } from '../../../app/app/actions/user';
15 |
16 | export const ProfileIconMenu: FC = () => {
17 | const { push } = useRouter();
18 |
19 | const onLogout = async () => {
20 | await signOut();
21 | };
22 |
23 | return (
24 |
25 |
26 |
29 |
30 |
31 |
32 | {
34 | push('/app/settings');
35 | }}
36 | >
37 | Settings
38 |
39 |
40 |
41 | Log out
42 |
43 |
44 | );
45 | };
46 |
--------------------------------------------------------------------------------
/apps/app/supabase/.gitignore:
--------------------------------------------------------------------------------
1 | # Supabase
2 | .branches
3 | .temp
4 | .env
5 |
--------------------------------------------------------------------------------
/apps/app/supabase/config.toml:
--------------------------------------------------------------------------------
1 | # A string used to distinguish different Supabase projects on the same host. Defaults to the
2 | # working directory name when running `supabase init`.
3 | project_id = "app"
4 |
5 | [api]
6 | enabled = true
7 | # Port to use for the API URL.
8 | port = 54321
9 | # Schemas to expose in your API. Tables, views and stored procedures in this schema will get API
10 | # endpoints. public and storage are always included.
11 | schemas = ["public", "storage", "graphql_public"]
12 | # Extra schemas to add to the search_path of every request. public is always included.
13 | extra_search_path = ["public", "extensions"]
14 | # The maximum number of rows returns from a view, table, or stored procedure. Limits payload size
15 | # for accidental or malicious requests.
16 | max_rows = 1000
17 |
18 | [db]
19 | # Port to use for the local database URL.
20 | port = 54322
21 | # Port used by db diff command to initialize the shadow database.
22 | shadow_port = 54320
23 | # The database major version to use. This has to be the same as your remote database's. Run `SHOW
24 | # server_version;` on the remote database to check.
25 | major_version = 15
26 |
27 | [db.pooler]
28 | enabled = false
29 | # Port to use for the local connection pooler.
30 | port = 54329
31 | # Specifies when a server connection can be reused by other clients.
32 | # Configure one of the supported pooler modes: `transaction`, `session`.
33 | pool_mode = "transaction"
34 | # How many server connections to allow per user/database pair.
35 | default_pool_size = 20
36 | # Maximum number of client connections allowed.
37 | max_client_conn = 100
38 |
39 | [realtime]
40 | enabled = true
41 | # Bind realtime via either IPv4 or IPv6. (default: IPv6)
42 | # ip_version = "IPv6"
43 | # The maximum length in bytes of HTTP request headers. (default: 4096)
44 | # max_header_length = 4096
45 |
46 | [studio]
47 | enabled = true
48 | # Port to use for Supabase Studio.
49 | port = 54323
50 | # External URL of the API server that frontend connects to.
51 | api_url = "http://127.0.0.1"
52 | # OpenAI API Key to use for Supabase AI in the Supabase Studio.
53 | openai_api_key = "env(OPENAI_API_KEY)"
54 |
55 | # Email testing server. Emails sent with the local dev setup are not actually sent - rather, they
56 | # are monitored, and you can view the emails that would have been sent from the web interface.
57 | [inbucket]
58 | enabled = true
59 | # Port to use for the email testing server web interface.
60 | port = 54324
61 | # Uncomment to expose additional ports for testing user applications that send emails.
62 | # smtp_port = 54325
63 | # pop3_port = 54326
64 |
65 | [storage]
66 | enabled = true
67 | # The maximum file size allowed (e.g. "5MB", "500KB").
68 | file_size_limit = "50MiB"
69 |
70 | [auth]
71 | enabled = true
72 | # The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
73 | # in emails.
74 | site_url = "http://127.0.0.1:3000"
75 | # A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
76 | additional_redirect_urls = ["https://127.0.0.1:3000/**", "http://127.0.0.1:3000/auth/welcome", "http://localhost:3000/auth/reset-password"]
77 | # How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
78 | jwt_expiry = 3600
79 | # If disabled, the refresh token will never expire.
80 | enable_refresh_token_rotation = true
81 | # Allows refresh tokens to be reused after expiry, up to the specified interval in seconds.
82 | # Requires enable_refresh_token_rotation = true.
83 | refresh_token_reuse_interval = 10
84 | # Allow/disallow new user signups to your project.
85 | enable_signup = true
86 | # Allow/disallow testing manual linking of accounts
87 | enable_manual_linking = false
88 |
89 | [auth.email]
90 | # Allow/disallow new user signups via email to your project.
91 | enable_signup = true
92 | # If enabled, a user will be required to confirm any email change on both the old, and new email
93 | # addresses. If disabled, only the new email is required to confirm.
94 | double_confirm_changes = true
95 | # If enabled, users need to confirm their email address before signing in.
96 | enable_confirmations = false
97 |
98 | # Uncomment to customize email template
99 | # [auth.email.template.invite]
100 | # subject = "You have been invited"
101 | # content_path = "./supabase/templates/invite.html"
102 |
103 | [auth.sms]
104 | # Allow/disallow new user signups via SMS to your project.
105 | enable_signup = true
106 | # If enabled, users need to confirm their phone number before signing in.
107 | enable_confirmations = false
108 | # Template for sending OTP to users
109 | template = "Your code is {{ .Code }} ."
110 |
111 | # Use pre-defined map of phone number to OTP for testing.
112 | [auth.sms.test_otp]
113 | # 4152127777 = "123456"
114 |
115 | # This hook runs before a token is issued and allows you to add additional claims based on the authentication method used.
116 | [auth.hook.custom_access_token]
117 | # enabled = true
118 | # uri = "pg-functions:////"
119 |
120 |
121 | # Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`.
122 | [auth.sms.twilio]
123 | enabled = false
124 | account_sid = ""
125 | message_service_sid = ""
126 | # DO NOT commit your Twilio auth token to git. Use environment variable substitution instead:
127 | auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)"
128 |
129 | # Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`,
130 | # `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`,
131 | # `twitter`, `slack`, `spotify`, `workos`, `zoom`.
132 | [auth.external.apple]
133 | enabled = false
134 | client_id = ""
135 | # DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead:
136 | secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)"
137 | # Overrides the default auth redirectUrl.
138 | redirect_uri = ""
139 | # Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure,
140 | # or any other third-party OIDC providers.
141 | url = ""
142 |
143 | [analytics]
144 | enabled = false
145 | port = 54327
146 | vector_port = 54328
147 | # Configure one of the supported backends: `postgres`, `bigquery`.
148 | backend = "postgres"
149 |
150 | # Experimental features may be deprecated any time
151 | [experimental]
152 | # Configures Postgres storage engine to use OrioleDB (S3)
153 | orioledb_version = ""
154 | # Configures S3 bucket URL, eg. .s3-.amazonaws.com
155 | s3_host = "env(S3_HOST)"
156 | # Configures S3 bucket region, eg. us-east-1
157 | s3_region = "env(S3_REGION)"
158 | # Configures AWS_ACCESS_KEY_ID for S3 bucket
159 | s3_access_key = "env(S3_ACCESS_KEY)"
160 | # Configures AWS_SECRET_ACCESS_KEY for S3 bucket
161 | s3_secret_key = "env(S3_SECRET_KEY)"
162 |
--------------------------------------------------------------------------------
/apps/app/supabase/migrations/20240324185402_remote_schema.sql:
--------------------------------------------------------------------------------
1 |
2 | SET statement_timeout = 0;
3 | SET lock_timeout = 0;
4 | SET idle_in_transaction_session_timeout = 0;
5 | SET client_encoding = 'UTF8';
6 | SET standard_conforming_strings = on;
7 | SELECT pg_catalog.set_config('search_path', '', false);
8 | SET check_function_bodies = false;
9 | SET xmloption = content;
10 | SET client_min_messages = warning;
11 | SET row_security = off;
12 |
13 | CREATE EXTENSION IF NOT EXISTS "pgsodium" WITH SCHEMA "pgsodium";
14 |
15 | CREATE SCHEMA IF NOT EXISTS "public";
16 |
17 | ALTER SCHEMA "public" OWNER TO "pg_database_owner";
18 |
19 | COMMENT ON SCHEMA "public" IS 'standard public schema';
20 |
21 | CREATE EXTENSION IF NOT EXISTS "pg_graphql" WITH SCHEMA "graphql";
22 |
23 | CREATE EXTENSION IF NOT EXISTS "pg_stat_statements" WITH SCHEMA "extensions";
24 |
25 | CREATE EXTENSION IF NOT EXISTS "pgcrypto" WITH SCHEMA "extensions";
26 |
27 | CREATE EXTENSION IF NOT EXISTS "pgjwt" WITH SCHEMA "extensions";
28 |
29 | CREATE EXTENSION IF NOT EXISTS "supabase_vault" WITH SCHEMA "vault";
30 |
31 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA "extensions";
32 |
33 | CREATE OR REPLACE FUNCTION "public"."handle_new_user"() RETURNS "trigger"
34 | LANGUAGE "plpgsql" SECURITY DEFINER
35 | SET "search_path" TO 'public'
36 | AS $$
37 | begin
38 | insert into public.users (id)
39 | values (new.id);
40 | return new;
41 | end;
42 | $$;
43 |
44 | ALTER FUNCTION "public"."handle_new_user"() OWNER TO "postgres";
45 |
46 | SET default_tablespace = '';
47 |
48 | SET default_table_access_method = "heap";
49 |
50 | CREATE TABLE IF NOT EXISTS "public"."users" (
51 | "id" "uuid" NOT NULL,
52 | "username" "text"
53 | );
54 |
55 | ALTER TABLE "public"."users" OWNER TO "postgres";
56 |
57 | ALTER TABLE ONLY "public"."users"
58 | ADD CONSTRAINT "profiles_pkey" PRIMARY KEY ("id");
59 |
60 | ALTER TABLE ONLY "public"."users"
61 | ADD CONSTRAINT "profiles_id_fkey" FOREIGN KEY ("id") REFERENCES "auth"."users"("id") ON DELETE CASCADE;
62 |
63 | CREATE POLICY "Profiles are viewable by users who created them." ON "public"."users" FOR SELECT USING (("auth"."uid"() = "id"));
64 |
65 | ALTER TABLE "public"."users" ENABLE ROW LEVEL SECURITY;
66 |
67 | GRANT USAGE ON SCHEMA "public" TO "postgres";
68 | GRANT USAGE ON SCHEMA "public" TO "anon";
69 | GRANT USAGE ON SCHEMA "public" TO "authenticated";
70 | GRANT USAGE ON SCHEMA "public" TO "service_role";
71 |
72 | GRANT ALL ON FUNCTION "public"."handle_new_user"() TO "anon";
73 | GRANT ALL ON FUNCTION "public"."handle_new_user"() TO "authenticated";
74 | GRANT ALL ON FUNCTION "public"."handle_new_user"() TO "service_role";
75 |
76 | GRANT ALL ON TABLE "public"."users" TO "anon";
77 | GRANT ALL ON TABLE "public"."users" TO "authenticated";
78 | GRANT ALL ON TABLE "public"."users" TO "service_role";
79 |
80 | ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "postgres";
81 | ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "anon";
82 | ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "authenticated";
83 | ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "service_role";
84 |
85 | ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "postgres";
86 | ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "anon";
87 | ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "authenticated";
88 | ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "service_role";
89 |
90 | ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "postgres";
91 | ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "anon";
92 | ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "authenticated";
93 | ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "service_role";
94 |
95 | RESET ALL;
96 |
--------------------------------------------------------------------------------
/apps/app/supabase/seed.sql:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JadRizk/turborepo-launchpad/cacd6cb5565725e47a593c9fc609c31d137d7f15/apps/app/supabase/seed.sql
--------------------------------------------------------------------------------
/apps/app/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | const sharedConfig = require("tailwind-config");
3 | module.exports = {
4 | ...sharedConfig,
5 | content: [
6 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
8 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
9 | "../../packages/ui/**/*.{js,ts,jsx,tsx,mdx}",
10 | ],
11 | };
--------------------------------------------------------------------------------
/apps/app/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tsconfig/nextjs.json",
3 | "compilerOptions": {
4 | "plugins": [{ "name": "next" }]
5 | },
6 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/apps/docs/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | /** @type {import("eslint").Linter.Config} */
2 | module.exports = {
3 | extends: ['custom/storybook.js'],
4 | };
5 |
--------------------------------------------------------------------------------
/apps/docs/.gitignore:
--------------------------------------------------------------------------------
1 | /storybook-static
--------------------------------------------------------------------------------
/apps/docs/.storybook/main.js:
--------------------------------------------------------------------------------
1 | import { dirname, join, resolve } from 'path';
2 |
3 | function getAbsolutePath(value) {
4 | return dirname(require.resolve(join(value, 'package.json')));
5 | }
6 |
7 | const config = {
8 | stories: [
9 | '../stories/*.stories.tsx',
10 | '../stories/**/*.stories.tsx',
11 | '../stories/**/*.mdx',
12 | ],
13 | addons: [
14 | getAbsolutePath('@storybook/addon-links'),
15 | getAbsolutePath('@storybook/addon-essentials'),
16 | getAbsolutePath('@storybook/addon-themes'),
17 | ],
18 | framework: {
19 | name: getAbsolutePath('@storybook/react-vite'),
20 | options: {},
21 | },
22 |
23 | core: {},
24 |
25 | async viteFinal(config, { configType }) {
26 | return {
27 | ...config,
28 | define: { 'process.env': {} },
29 | resolve: {
30 | alias: [
31 | {
32 | find: 'ui',
33 | replacement: resolve(__dirname, '../../../packages/ui'),
34 | },
35 | ],
36 | },
37 | };
38 | },
39 |
40 | docs: {
41 | autodocs: true,
42 | },
43 | };
44 |
45 | export default config;
46 |
--------------------------------------------------------------------------------
/apps/docs/.storybook/preview.jsx:
--------------------------------------------------------------------------------
1 | import 'ui/styles/globals.css';
2 | import { withThemeByClassName } from '@storybook/addon-themes';
3 |
4 | export default {
5 | decorators: [
6 | withThemeByClassName({
7 | themes: {
8 | light: 'light',
9 | dark: 'dark',
10 | },
11 | defaultTheme: 'light',
12 | }),
13 | ],
14 | parameters: {
15 | actions: { argTypesRegex: '^on[A-Z].*' },
16 | controls: { expanded: true },
17 | layout: 'centered',
18 | },
19 | };
20 |
--------------------------------------------------------------------------------
/apps/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "docs",
3 | "version": "0.0.1",
4 | "type": "module",
5 | "private": true,
6 | "scripts": {
7 | "dev": "storybook dev -p 6006",
8 | "build": "storybook build --docs",
9 | "preview-storybook": "serve storybook-static",
10 | "clean": "rm -rf .turbo && rm -rf node_modules",
11 | "lint": "eslint ./stories/*.stories.tsx"
12 | },
13 | "dependencies": {
14 | "react": "^18.2.0",
15 | "react-dom": "^18.2.0"
16 | },
17 | "devDependencies": {
18 | "@storybook/blocks": "^8.5.8",
19 | "@storybook/addon-actions": "^8.5.8",
20 | "@storybook/addon-docs": "^8.5.8",
21 | "@storybook/addon-essentials": "^8.5.8",
22 | "@storybook/addon-links": "^8.5.8",
23 | "@storybook/addon-themes": "^8.5.8",
24 | "@storybook/react": "^8.5.8",
25 | "@storybook/react-vite": "^8.5.8",
26 | "@types/react": "^19.0.10",
27 | "@types/react-dom": "^19.0.4",
28 | "@vitejs/plugin-react": "^4.2.1",
29 | "autoprefixer": "^10.4.19",
30 | "eslint": "^8.57.0",
31 | "eslint-config-custom": "workspace:*",
32 | "eslint-plugin-mdx": "^3.1.5",
33 | "serve": "^14.2.1",
34 | "storybook": "^8.5.8",
35 | "tailwind-config": "workspace:*",
36 | "tailwindcss": "^3.3.0",
37 | "tsconfig": "workspace:*",
38 | "typescript": "^5.3.3",
39 | "ui": "workspace:*",
40 | "vite": "^5.1.4"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/apps/docs/postcss.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/apps/docs/stories/Mixins/colors.mdx:
--------------------------------------------------------------------------------
1 | import { Meta, ColorPalette, ColorItem } from '@storybook/blocks';
2 | import { customColors, tailwindColors } from '../../tokens';
3 |
4 |
5 |
6 | # Colors
7 |
8 | This story displays the color palette for the design tokens.
9 |
10 | ## Custom colors
11 |
12 |
13 | {Object.entries(customColors).map(([colorName, colorValue]) => (
14 |
24 | ))}
25 |
26 |
27 | ## Tailwind Colors
28 |
29 |
30 | {Object.entries(tailwindColors).map(([colorName, colorValue]) => (
31 |
41 | ))}
42 |
43 |
--------------------------------------------------------------------------------
/apps/docs/stories/Mixins/icons.mdx:
--------------------------------------------------------------------------------
1 | import { Meta } from '@storybook/addon-docs/blocks';
2 | import { IconGallery, IconItem } from '@storybook/addon-docs';
3 | import { Icons } from 'ui';
4 |
5 |
6 |
7 | # Icons
8 |
9 |
10 | {Object.entries(Icons).map(([name, IconComponent]) => (
11 |
12 |
13 |
14 | ))}
15 |
16 |
--------------------------------------------------------------------------------
/apps/docs/stories/Mixins/spacings.mdx:
--------------------------------------------------------------------------------
1 | import { Meta } from '@storybook/blocks';
2 | import { spacing } from '../../tokens.ts';
3 |
4 |
5 |
6 | # Spacing Tokens
7 |
8 | This story displays the spacing tokens defined in your Tailwind configuration.
9 | Each token is listed with its name, size, pixel value, and a visual preview.
10 |
11 | ## Table of Spacing Tokens
12 |
13 |
21 |
29 |
30 | Name |
31 | Size |
32 | Pixels |
33 | Preview |
34 |
35 |
36 |
37 | {Object.entries(spacing).map(([key, value]) => {
38 | const pixels =
39 | parseFloat(value) * (String(value).endsWith('rem') ? 16 : 1);
40 | return (
41 |
42 | {key} |
43 | {value} |
44 | {pixels}px |
45 |
46 |
61 | |
62 |
63 | );
64 | })}
65 |
66 |
67 |
--------------------------------------------------------------------------------
/apps/docs/stories/button.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 | import { Button } from 'ui';
3 |
4 | const meta = {
5 | title: 'Components/Button',
6 | component: Button,
7 | argTypes: {
8 | variant: {
9 | control: { type: 'select' },
10 | options: [
11 | 'default',
12 | 'destructive',
13 | 'outline',
14 | 'secondary',
15 | 'ghost',
16 | 'link',
17 | ],
18 | },
19 | size: {
20 | control: { type: 'select' },
21 | options: ['default', 'sm', 'lg', 'icon'],
22 | },
23 | asChild: {
24 | control: { type: 'boolean' },
25 | },
26 | },
27 | parameters: {
28 | layout: 'centered',
29 | docs: {
30 | description: {
31 | component:
32 | 'A versatile button component with various styles and sizes.',
33 | },
34 | },
35 | },
36 | tags: ['autodocs'],
37 | } satisfies Meta;
38 |
39 | export default meta;
40 |
41 | type Story = StoryObj;
42 |
43 | export const Default: Story = {
44 | args: {
45 | children: 'Button',
46 | variant: 'default',
47 | size: 'default',
48 | },
49 | parameters: {
50 | docs: {
51 | description: {
52 | story: 'The default button style with standard size and appearance.',
53 | },
54 | },
55 | },
56 | };
57 |
58 | export const Variants: Story = {
59 | render: args => (
60 |
61 |
64 |
67 |
70 |
73 |
76 |
79 |
80 | ),
81 | parameters: {
82 | docs: {
83 | description: {
84 | story:
85 | 'Showcases all available button variants: default, destructive, outline, secondary, ghost, and link.',
86 | },
87 | },
88 | },
89 | };
90 |
91 | export const Sizes: Story = {
92 | render: args => (
93 |
94 |
97 |
100 |
103 |
106 |
107 | ),
108 | parameters: {
109 | docs: {
110 | description: {
111 | story:
112 | 'Demonstrates the different button sizes: small, default, large, and icon.',
113 | },
114 | },
115 | },
116 | };
117 |
--------------------------------------------------------------------------------
/apps/docs/stories/dialog.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 | import {
3 | Dialog,
4 | DialogTrigger,
5 | DialogContent,
6 | DialogHeader,
7 | DialogTitle,
8 | DialogDescription,
9 | DialogClose,
10 | Button,
11 | } from 'ui';
12 |
13 | const meta: Meta = {
14 | title: 'Components/Dialog',
15 | component: Dialog,
16 | argTypes: {
17 | open: { control: 'boolean' },
18 | },
19 | };
20 |
21 | export default meta;
22 |
23 | type Story = StoryObj;
24 |
25 | export const Default: Story = {
26 | render: args => (
27 |
47 | ),
48 | };
49 |
--------------------------------------------------------------------------------
/apps/docs/stories/input.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 | import type { InputProps } from 'ui';
3 | import { Input } from 'ui';
4 |
5 | const meta: Meta = {
6 | title: 'Components/Input',
7 | component: Input,
8 | args: {
9 | type: 'text',
10 | placeholder: 'Enter text...',
11 | invalid: false,
12 | },
13 | } satisfies Meta;
14 |
15 | export default meta;
16 |
17 | type Story = StoryObj;
18 |
19 | export const Default: Story = {};
20 |
21 | export const Invalid: Story = {
22 | args: {
23 | invalid: true,
24 | placeholder: 'Invalid input',
25 | },
26 | };
27 |
28 | export const Disabled: Story = {
29 | args: {
30 | disabled: true,
31 | placeholder: 'Disabled input',
32 | },
33 | };
34 |
--------------------------------------------------------------------------------
/apps/docs/stories/toast.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 | import type { FC } from 'react';
3 | import type { ToastProps } from 'ui';
4 | import { Button, Toast, Toaster, useToast } from 'ui';
5 |
6 | export default {
7 | title: 'Components/Toast',
8 | component: Toast,
9 | parameters: {
10 | layout: 'centered',
11 | },
12 | argTypes: {
13 | title: {
14 | control: 'text',
15 | defaultValue: 'This is a toast message',
16 | },
17 | variant: {
18 | control: 'select',
19 | options: ['default', 'destructive'],
20 | defaultValue: 'default',
21 | },
22 | },
23 | } as Meta;
24 |
25 | type ToastStory = StoryObj;
26 |
27 | const DefaultToast: FC = ({ title, variant }) => {
28 | const { toast } = useToast();
29 |
30 | return (
31 |
32 |
35 |
36 |
37 | );
38 | };
39 | export const SingleToast: ToastStory = {
40 | name: 'SingleToast',
41 | args: { title: 'This is a toast message', variant: 'default' },
42 | render: args => ,
43 | };
44 |
45 | SingleToast.args = {
46 | title: 'This is a toast message',
47 | variant: 'default',
48 | };
49 |
--------------------------------------------------------------------------------
/apps/docs/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 |
3 | const sharedConfig = require('tailwind-config');
4 |
5 | module.exports = {
6 | ...sharedConfig,
7 | content: ['./**/*.{js,ts,jsx,tsx,mdx}'],
8 | };
9 |
--------------------------------------------------------------------------------
/apps/docs/tokens.ts:
--------------------------------------------------------------------------------
1 |
2 | export const customColors = {
3 | "border": "hsl(var(--border))",
4 | "input": "hsl(var(--input))",
5 | "ring": "hsl(var(--ring))",
6 | "background": "hsl(var(--background))",
7 | "foreground": "hsl(var(--foreground))",
8 | "primary": {
9 | "DEFAULT": "hsl(var(--primary))",
10 | "foreground": "hsl(var(--primary-foreground))"
11 | },
12 | "secondary": {
13 | "DEFAULT": "hsl(var(--secondary))",
14 | "foreground": "hsl(var(--secondary-foreground))"
15 | },
16 | "destructive": {
17 | "DEFAULT": "hsl(var(--destructive))",
18 | "foreground": "hsl(var(--destructive-foreground))"
19 | },
20 | "muted": {
21 | "DEFAULT": "hsl(var(--muted))",
22 | "foreground": "hsl(var(--muted-foreground))"
23 | },
24 | "accent": {
25 | "DEFAULT": "hsl(var(--accent))",
26 | "foreground": "hsl(var(--accent-foreground))"
27 | },
28 | "popover": {
29 | "DEFAULT": "hsl(var(--popover))",
30 | "foreground": "hsl(var(--popover-foreground))"
31 | },
32 | "card": {
33 | "DEFAULT": "hsl(var(--card))",
34 | "foreground": "hsl(var(--card-foreground))"
35 | },
36 | "reefGreen": {
37 | "100": "var(--reefGreen-100)",
38 | "200": "var(--reefGreen-200)",
39 | "300": "var(--reefGreen-300)",
40 | "400": "var(--reefGreen-400)",
41 | "500": "var(--reefGreen-500)",
42 | "600": "var(--reefGreen-600)",
43 | "700": "var(--reefGreen-700)",
44 | "800": "var(--reefGreen-800)",
45 | "900": "var(--reefGreen-900)",
46 | "DEFAULT": "var(--reefGreen-500)"
47 | }
48 | };
49 | export const tailwindColors = {
50 | "inherit": "inherit",
51 | "current": "currentColor",
52 | "transparent": "transparent",
53 | "black": "#000",
54 | "white": "#fff",
55 | "slate": {
56 | "50": "#f8fafc",
57 | "100": "#f1f5f9",
58 | "200": "#e2e8f0",
59 | "300": "#cbd5e1",
60 | "400": "#94a3b8",
61 | "500": "#64748b",
62 | "600": "#475569",
63 | "700": "#334155",
64 | "800": "#1e293b",
65 | "900": "#0f172a",
66 | "950": "#020617"
67 | },
68 | "gray": {
69 | "50": "#f9fafb",
70 | "100": "#f3f4f6",
71 | "200": "#e5e7eb",
72 | "300": "#d1d5db",
73 | "400": "#9ca3af",
74 | "500": "#6b7280",
75 | "600": "#4b5563",
76 | "700": "#374151",
77 | "800": "#1f2937",
78 | "900": "#111827",
79 | "950": "#030712"
80 | },
81 | "zinc": {
82 | "50": "#fafafa",
83 | "100": "#f4f4f5",
84 | "200": "#e4e4e7",
85 | "300": "#d4d4d8",
86 | "400": "#a1a1aa",
87 | "500": "#71717a",
88 | "600": "#52525b",
89 | "700": "#3f3f46",
90 | "800": "#27272a",
91 | "900": "#18181b",
92 | "950": "#09090b"
93 | },
94 | "neutral": {
95 | "50": "#fafafa",
96 | "100": "#f5f5f5",
97 | "200": "#e5e5e5",
98 | "300": "#d4d4d4",
99 | "400": "#a3a3a3",
100 | "500": "#737373",
101 | "600": "#525252",
102 | "700": "#404040",
103 | "800": "#262626",
104 | "900": "#171717",
105 | "950": "#0a0a0a"
106 | },
107 | "stone": {
108 | "50": "#fafaf9",
109 | "100": "#f5f5f4",
110 | "200": "#e7e5e4",
111 | "300": "#d6d3d1",
112 | "400": "#a8a29e",
113 | "500": "#78716c",
114 | "600": "#57534e",
115 | "700": "#44403c",
116 | "800": "#292524",
117 | "900": "#1c1917",
118 | "950": "#0c0a09"
119 | },
120 | "red": {
121 | "50": "#fef2f2",
122 | "100": "#fee2e2",
123 | "200": "#fecaca",
124 | "300": "#fca5a5",
125 | "400": "#f87171",
126 | "500": "#ef4444",
127 | "600": "#dc2626",
128 | "700": "#b91c1c",
129 | "800": "#991b1b",
130 | "900": "#7f1d1d",
131 | "950": "#450a0a"
132 | },
133 | "orange": {
134 | "50": "#fff7ed",
135 | "100": "#ffedd5",
136 | "200": "#fed7aa",
137 | "300": "#fdba74",
138 | "400": "#fb923c",
139 | "500": "#f97316",
140 | "600": "#ea580c",
141 | "700": "#c2410c",
142 | "800": "#9a3412",
143 | "900": "#7c2d12",
144 | "950": "#431407"
145 | },
146 | "amber": {
147 | "50": "#fffbeb",
148 | "100": "#fef3c7",
149 | "200": "#fde68a",
150 | "300": "#fcd34d",
151 | "400": "#fbbf24",
152 | "500": "#f59e0b",
153 | "600": "#d97706",
154 | "700": "#b45309",
155 | "800": "#92400e",
156 | "900": "#78350f",
157 | "950": "#451a03"
158 | },
159 | "yellow": {
160 | "50": "#fefce8",
161 | "100": "#fef9c3",
162 | "200": "#fef08a",
163 | "300": "#fde047",
164 | "400": "#facc15",
165 | "500": "#eab308",
166 | "600": "#ca8a04",
167 | "700": "#a16207",
168 | "800": "#854d0e",
169 | "900": "#713f12",
170 | "950": "#422006"
171 | },
172 | "lime": {
173 | "50": "#f7fee7",
174 | "100": "#ecfccb",
175 | "200": "#d9f99d",
176 | "300": "#bef264",
177 | "400": "#a3e635",
178 | "500": "#84cc16",
179 | "600": "#65a30d",
180 | "700": "#4d7c0f",
181 | "800": "#3f6212",
182 | "900": "#365314",
183 | "950": "#1a2e05"
184 | },
185 | "green": {
186 | "50": "#f0fdf4",
187 | "100": "#dcfce7",
188 | "200": "#bbf7d0",
189 | "300": "#86efac",
190 | "400": "#4ade80",
191 | "500": "#22c55e",
192 | "600": "#16a34a",
193 | "700": "#15803d",
194 | "800": "#166534",
195 | "900": "#14532d",
196 | "950": "#052e16"
197 | },
198 | "emerald": {
199 | "50": "#ecfdf5",
200 | "100": "#d1fae5",
201 | "200": "#a7f3d0",
202 | "300": "#6ee7b7",
203 | "400": "#34d399",
204 | "500": "#10b981",
205 | "600": "#059669",
206 | "700": "#047857",
207 | "800": "#065f46",
208 | "900": "#064e3b",
209 | "950": "#022c22"
210 | },
211 | "teal": {
212 | "50": "#f0fdfa",
213 | "100": "#ccfbf1",
214 | "200": "#99f6e4",
215 | "300": "#5eead4",
216 | "400": "#2dd4bf",
217 | "500": "#14b8a6",
218 | "600": "#0d9488",
219 | "700": "#0f766e",
220 | "800": "#115e59",
221 | "900": "#134e4a",
222 | "950": "#042f2e"
223 | },
224 | "cyan": {
225 | "50": "#ecfeff",
226 | "100": "#cffafe",
227 | "200": "#a5f3fc",
228 | "300": "#67e8f9",
229 | "400": "#22d3ee",
230 | "500": "#06b6d4",
231 | "600": "#0891b2",
232 | "700": "#0e7490",
233 | "800": "#155e75",
234 | "900": "#164e63",
235 | "950": "#083344"
236 | },
237 | "sky": {
238 | "50": "#f0f9ff",
239 | "100": "#e0f2fe",
240 | "200": "#bae6fd",
241 | "300": "#7dd3fc",
242 | "400": "#38bdf8",
243 | "500": "#0ea5e9",
244 | "600": "#0284c7",
245 | "700": "#0369a1",
246 | "800": "#075985",
247 | "900": "#0c4a6e",
248 | "950": "#082f49"
249 | },
250 | "blue": {
251 | "50": "#eff6ff",
252 | "100": "#dbeafe",
253 | "200": "#bfdbfe",
254 | "300": "#93c5fd",
255 | "400": "#60a5fa",
256 | "500": "#3b82f6",
257 | "600": "#2563eb",
258 | "700": "#1d4ed8",
259 | "800": "#1e40af",
260 | "900": "#1e3a8a",
261 | "950": "#172554"
262 | },
263 | "indigo": {
264 | "50": "#eef2ff",
265 | "100": "#e0e7ff",
266 | "200": "#c7d2fe",
267 | "300": "#a5b4fc",
268 | "400": "#818cf8",
269 | "500": "#6366f1",
270 | "600": "#4f46e5",
271 | "700": "#4338ca",
272 | "800": "#3730a3",
273 | "900": "#312e81",
274 | "950": "#1e1b4b"
275 | },
276 | "violet": {
277 | "50": "#f5f3ff",
278 | "100": "#ede9fe",
279 | "200": "#ddd6fe",
280 | "300": "#c4b5fd",
281 | "400": "#a78bfa",
282 | "500": "#8b5cf6",
283 | "600": "#7c3aed",
284 | "700": "#6d28d9",
285 | "800": "#5b21b6",
286 | "900": "#4c1d95",
287 | "950": "#2e1065"
288 | },
289 | "purple": {
290 | "50": "#faf5ff",
291 | "100": "#f3e8ff",
292 | "200": "#e9d5ff",
293 | "300": "#d8b4fe",
294 | "400": "#c084fc",
295 | "500": "#a855f7",
296 | "600": "#9333ea",
297 | "700": "#7e22ce",
298 | "800": "#6b21a8",
299 | "900": "#581c87",
300 | "950": "#3b0764"
301 | },
302 | "fuchsia": {
303 | "50": "#fdf4ff",
304 | "100": "#fae8ff",
305 | "200": "#f5d0fe",
306 | "300": "#f0abfc",
307 | "400": "#e879f9",
308 | "500": "#d946ef",
309 | "600": "#c026d3",
310 | "700": "#a21caf",
311 | "800": "#86198f",
312 | "900": "#701a75",
313 | "950": "#4a044e"
314 | },
315 | "pink": {
316 | "50": "#fdf2f8",
317 | "100": "#fce7f3",
318 | "200": "#fbcfe8",
319 | "300": "#f9a8d4",
320 | "400": "#f472b6",
321 | "500": "#ec4899",
322 | "600": "#db2777",
323 | "700": "#be185d",
324 | "800": "#9d174d",
325 | "900": "#831843",
326 | "950": "#500724"
327 | },
328 | "rose": {
329 | "50": "#fff1f2",
330 | "100": "#ffe4e6",
331 | "200": "#fecdd3",
332 | "300": "#fda4af",
333 | "400": "#fb7185",
334 | "500": "#f43f5e",
335 | "600": "#e11d48",
336 | "700": "#be123c",
337 | "800": "#9f1239",
338 | "900": "#881337",
339 | "950": "#4c0519"
340 | }
341 | };
342 | export const spacing = {
343 | "0": "0px",
344 | "1": "0.25rem",
345 | "2": "0.5rem",
346 | "3": "0.75rem",
347 | "4": "1rem",
348 | "5": "1.25rem",
349 | "6": "1.5rem",
350 | "7": "1.75rem",
351 | "8": "2rem",
352 | "9": "2.25rem",
353 | "10": "2.5rem",
354 | "11": "2.75rem",
355 | "12": "3rem",
356 | "14": "3.5rem",
357 | "16": "4rem",
358 | "20": "5rem",
359 | "24": "6rem",
360 | "28": "7rem",
361 | "32": "8rem",
362 | "36": "9rem",
363 | "40": "10rem",
364 | "44": "11rem",
365 | "48": "12rem",
366 | "52": "13rem",
367 | "56": "14rem",
368 | "60": "15rem",
369 | "64": "16rem",
370 | "72": "18rem",
371 | "80": "20rem",
372 | "96": "24rem",
373 | "px": "1px",
374 | "0.5": "0.125rem",
375 | "1.5": "0.375rem",
376 | "2.5": "0.625rem",
377 | "3.5": "0.875rem"
378 | };
379 | export const typography = {
380 | "sans": [
381 | "var(--font-sans)",
382 | "ui-sans-serif",
383 | "system-ui",
384 | "-apple-system",
385 | "BlinkMacSystemFont",
386 | "\"Segoe UI\"",
387 | "Roboto",
388 | "\"Helvetica Neue\"",
389 | "Arial",
390 | "\"Noto Sans\"",
391 | "sans-serif",
392 | "\"Apple Color Emoji\"",
393 | "\"Segoe UI Emoji\"",
394 | "\"Segoe UI Symbol\"",
395 | "\"Noto Color Emoji\""
396 | ],
397 | "serif": [
398 | "ui-serif",
399 | "Georgia",
400 | "Cambria",
401 | "\"Times New Roman\"",
402 | "Times",
403 | "serif"
404 | ],
405 | "mono": [
406 | "ui-monospace",
407 | "SFMono-Regular",
408 | "Menlo",
409 | "Monaco",
410 | "Consolas",
411 | "\"Liberation Mono\"",
412 | "\"Courier New\"",
413 | "monospace"
414 | ],
415 | "oswald": [
416 | "Oswald",
417 | "ui-sans-serif",
418 | "system-ui",
419 | "-apple-system",
420 | "BlinkMacSystemFont",
421 | "\"Segoe UI\"",
422 | "Roboto",
423 | "\"Helvetica Neue\"",
424 | "Arial",
425 | "\"Noto Sans\"",
426 | "sans-serif",
427 | "\"Apple Color Emoji\"",
428 | "\"Segoe UI Emoji\"",
429 | "\"Segoe UI Symbol\"",
430 | "\"Noto Color Emoji\""
431 | ],
432 | "heading": [
433 | "var(--font-heading)",
434 | "ui-sans-serif",
435 | "system-ui",
436 | "-apple-system",
437 | "BlinkMacSystemFont",
438 | "\"Segoe UI\"",
439 | "Roboto",
440 | "\"Helvetica Neue\"",
441 | "Arial",
442 | "\"Noto Sans\"",
443 | "sans-serif",
444 | "\"Apple Color Emoji\"",
445 | "\"Segoe UI Emoji\"",
446 | "\"Segoe UI Symbol\"",
447 | "\"Noto Color Emoji\""
448 | ]
449 | };
450 | export const borderRadius = {
451 | "none": "0px",
452 | "sm": "calc(var(--radius) - 4px)",
453 | "DEFAULT": "0.25rem",
454 | "md": "calc(var(--radius) - 2px)",
455 | "lg": "var(--radius)",
456 | "xl": "0.75rem",
457 | "2xl": "1rem",
458 | "3xl": "1.5rem",
459 | "full": "9999px"
460 | };
461 |
--------------------------------------------------------------------------------
/apps/docs/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tsconfig/react-library.json",
3 | "include": ["."],
4 | "exclude": ["dist", "build", "node_modules"]
5 | }
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "launchpad",
3 | "private": true,
4 | "scripts": {
5 | "build": "turbo run build",
6 | "dev": "turbo run dev",
7 | "lint": "turbo run lint",
8 | "format": "prettier --write \"**/*.{ts,tsx,md}\"",
9 | "changeset": "changeset",
10 | "version-packages": "changeset version",
11 | "clean": "turbo run clean && rm -rf node_modules",
12 | "release": "turbo run build --filter=docs^..."
13 | },
14 | "devDependencies": {
15 | "eslint": "^8.48.0",
16 | "husky": "^9.0.11",
17 | "lint-staged": "^15.2.2",
18 | "prettier": "^3.0.3",
19 | "tsconfig": "workspace:*",
20 | "turbo": "^2.4.2"
21 | },
22 | "lint-staged": {
23 | "**/*": "prettier --write \"**/*.{ts,tsx,md}\" --ignore-unknown"
24 | },
25 | "packageManager": "pnpm@9.1.1",
26 | "dependencies": {
27 | "@changesets/changelog-github": "^0.5.0",
28 | "@changesets/cli": "^2.27.1"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/packages/eslint-config-custom/README.md:
--------------------------------------------------------------------------------
1 | # `@turbo/eslint-config`
2 |
3 | Collection of internal eslint configurations.
4 |
--------------------------------------------------------------------------------
/packages/eslint-config-custom/library.js:
--------------------------------------------------------------------------------
1 | const { resolve } = require("node:path");
2 |
3 | const project = resolve(process.cwd(), "tsconfig.json");
4 |
5 | /*
6 | * This is a custom ESLint configuration for use with
7 | * typescript packages.
8 | *
9 | * This config extends the Vercel Engineering Style Guide.
10 | * For more information, see https://github.com/vercel/style-guide
11 | *
12 | */
13 |
14 | module.exports = {
15 | extends: [
16 | "@vercel/style-guide/eslint/node",
17 | "@vercel/style-guide/eslint/typescript",
18 | ].map(require.resolve),
19 | parserOptions: {
20 | project,
21 | },
22 | globals: {
23 | React: true,
24 | JSX: true,
25 | },
26 | settings: {
27 | "import/resolver": {
28 | typescript: {
29 | project,
30 | },
31 | },
32 | },
33 | rules: {
34 | "import/no-extraneous-dependencies": "off",
35 | "@typescript-eslint/explicit-function-return-type": "off",
36 | },
37 | ignorePatterns: ["node_modules/", "dist/"],
38 | };
39 |
--------------------------------------------------------------------------------
/packages/eslint-config-custom/next.js:
--------------------------------------------------------------------------------
1 | const { resolve } = require('node:path');
2 |
3 | const project = resolve(process.cwd(), 'tsconfig.json');
4 |
5 | /*
6 | * This is a custom ESLint configuration for use with
7 | * Next.js apps.
8 | *
9 | * This config extends the Vercel Engineering Style Guide.
10 | * For more information, see https://github.com/vercel/style-guide
11 | *
12 | */
13 |
14 | module.exports = {
15 | extends: [
16 | '@vercel/style-guide/eslint/node',
17 | '@vercel/style-guide/eslint/browser',
18 | '@vercel/style-guide/eslint/typescript',
19 | '@vercel/style-guide/eslint/react',
20 | '@vercel/style-guide/eslint/next',
21 | 'eslint-config-turbo',
22 | ].map(require.resolve),
23 | parserOptions: {
24 | project,
25 | },
26 | globals: {
27 | React: true,
28 | JSX: true,
29 | },
30 | settings: {
31 | 'import/resolver': {
32 | typescript: {
33 | project,
34 | },
35 | },
36 | },
37 | ignorePatterns: ['node_modules/', 'dist/'],
38 | rules: {
39 | 'import/no-extraneous-dependencies': 'off',
40 | 'import/no-default-export': 'off',
41 | 'unicorn/filename-case': 'off',
42 | 'react/button-has-type': 'off',
43 |
44 | 'react/jsx-pascal-case': 'off',
45 | 'react/function-component-definition': 'off',
46 |
47 | '@typescript-eslint/consistent-type-definitions': 'off',
48 | '@typescript-eslint/no-unsafe-argument': 'off',
49 | '@typescript-eslint/no-unsafe-call': 'off',
50 | '@typescript-eslint/explicit-function-return-type': 'off',
51 | '@typescript-eslint/no-non-null-assertion': 'off',
52 | '@typescript-eslint/unbound-method': 'off',
53 | '@typescript-eslint/no-misused-promises': 'off',
54 | '@typescript-eslint/require-await': 'off',
55 | '@typescript-eslint/no-unsafe-return': 'off',
56 | '@typescript-eslint/prefer-optional-chain': 'off',
57 |
58 | 'turbo/no-undeclared-env-vars': 'off',
59 | },
60 | };
61 |
--------------------------------------------------------------------------------
/packages/eslint-config-custom/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "eslint-config-custom",
3 | "license": "MIT",
4 | "version": "0.0.0",
5 | "private": true,
6 | "files": [
7 | "library.js",
8 | "next.js",
9 | "react-internal.js",
10 | "storybook.js"
11 | ],
12 | "devDependencies": {
13 | "@vercel/style-guide": "^5.0.0",
14 | "eslint-config-turbo": "^1.10.12",
15 | "typescript": "^5.2.2",
16 | "eslint-plugin-mdx": "^3.1.5",
17 | "eslint-plugin-storybook": "^0.8.0"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/packages/eslint-config-custom/react-internal.js:
--------------------------------------------------------------------------------
1 | const { resolve } = require("node:path");
2 |
3 | const project = resolve(process.cwd(), "tsconfig.json");
4 |
5 | /*
6 | * This is a custom ESLint configuration for use with
7 | * internal (bundled by their consumer) libraries
8 | * that utilize React.
9 | *
10 | * This config extends the Vercel Engineering Style Guide.
11 | * For more information, see https://github.com/vercel/style-guide
12 | *
13 | */
14 |
15 | module.exports = {
16 | extends: [
17 | "@vercel/style-guide/eslint/browser",
18 | "@vercel/style-guide/eslint/typescript",
19 | "@vercel/style-guide/eslint/react",
20 | ].map(require.resolve),
21 | parserOptions: {
22 | project,
23 | },
24 | globals: {
25 | JSX: true,
26 | },
27 | settings: {
28 | "import/resolver": {
29 | typescript: {
30 | project,
31 | },
32 | },
33 | },
34 | ignorePatterns: ["node_modules/", "dist/", ".eslintrc.js"],
35 |
36 | rules: {
37 | // add specific rules configurations here
38 | },
39 | };
40 |
--------------------------------------------------------------------------------
/packages/eslint-config-custom/storybook.js:
--------------------------------------------------------------------------------
1 | const { resolve } = require('node:path');
2 |
3 | const project = resolve(process.cwd(), 'tsconfig.json');
4 |
5 | /*
6 | * This is a custom ESLint configuration for use with
7 | * typescript packages.
8 | *
9 | * This config extends the Vercel Engineering Style Guide.
10 | * For more information, see https://github.com/vercel/style-guide
11 | *
12 | */
13 |
14 | module.exports = {
15 | extends: [
16 | 'plugin:storybook/recommended',
17 | 'plugin:mdx/recommended',
18 | ...[
19 | '@vercel/style-guide/eslint/node',
20 | '@vercel/style-guide/eslint/typescript',
21 | '@vercel/style-guide/eslint/browser',
22 | '@vercel/style-guide/eslint/react',
23 | ].map(require.resolve),
24 | ],
25 | parserOptions: {
26 | project,
27 | },
28 | globals: {
29 | React: true,
30 | JSX: true,
31 | },
32 | settings: {
33 | 'import/resolver': {
34 | typescript: {
35 | project,
36 | },
37 | },
38 | },
39 | ignorePatterns: ['node_modules/', 'dist/'],
40 | // add rules configurations here
41 | rules: {
42 | 'import/no-default-export': 'off',
43 | 'react/function-component-definition': 'off',
44 | '@typescript-eslint/no-unsafe-assignment': 'off',
45 | },
46 | };
47 |
--------------------------------------------------------------------------------
/packages/tailwind-config/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ["custom/library"],
3 | };
4 |
--------------------------------------------------------------------------------
/packages/tailwind-config/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tailwind-config",
3 | "version": "0.0.0",
4 | "private": true,
5 | "main": "tailwind.config.js",
6 | "scripts": {
7 | "clean": "rm -rf .turbo && rm -rf node_modules"
8 | },
9 | "devDependencies": {
10 | "@tailwindcss/typography": "^0.5.10",
11 | "eslint": "^8.28.0",
12 | "eslint-config-custom": "workspace:*",
13 | "tailwindcss": "^3.3.0"
14 | },
15 | "dependencies": {
16 | "tailwindcss-animate": "^1.0.7"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/packages/tailwind-config/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | const { fontFamily } = require('tailwindcss/defaultTheme');
3 |
4 | module.exports = {
5 | darkMode: ['class'],
6 | theme: {
7 | container: {
8 | center: true,
9 | padding: '2rem',
10 | screens: {
11 | '2xl': '1400px',
12 | },
13 | },
14 | extend: {
15 | colors: {
16 | border: 'hsl(var(--border))',
17 | input: 'hsl(var(--input))',
18 | ring: 'hsl(var(--ring))',
19 | background: 'hsl(var(--background))',
20 | foreground: 'hsl(var(--foreground))',
21 | primary: {
22 | DEFAULT: 'hsl(var(--primary))',
23 | foreground: 'hsl(var(--primary-foreground))',
24 | },
25 | secondary: {
26 | DEFAULT: 'hsl(var(--secondary))',
27 | foreground: 'hsl(var(--secondary-foreground))',
28 | },
29 | destructive: {
30 | DEFAULT: 'hsl(var(--destructive))',
31 | foreground: 'hsl(var(--destructive-foreground))',
32 | },
33 | muted: {
34 | DEFAULT: 'hsl(var(--muted))',
35 | foreground: 'hsl(var(--muted-foreground))',
36 | },
37 | accent: {
38 | DEFAULT: 'hsl(var(--accent))',
39 | foreground: 'hsl(var(--accent-foreground))',
40 | },
41 | popover: {
42 | DEFAULT: 'hsl(var(--popover))',
43 | foreground: 'hsl(var(--popover-foreground))',
44 | },
45 | card: {
46 | DEFAULT: 'hsl(var(--card))',
47 | foreground: 'hsl(var(--card-foreground))',
48 | },
49 | reefGreen: {
50 | DEFAULT: 'var(--reefGreen-500)',
51 | 100: 'var(--reefGreen-100)',
52 | 200: 'var(--reefGreen-200)',
53 | 300: 'var(--reefGreen-300)',
54 | 400: 'var(--reefGreen-400)',
55 | 500: 'var(--reefGreen-500)',
56 | 600: 'var(--reefGreen-600)',
57 | 700: 'var(--reefGreen-700)',
58 | 800: 'var(--reefGreen-800)',
59 | 900: 'var(--reefGreen-900)',
60 | },
61 | },
62 | borderRadius: {
63 | lg: `var(--radius)`,
64 | md: `calc(var(--radius) - 2px)`,
65 | sm: 'calc(var(--radius) - 4px)',
66 | },
67 | fontFamily: {
68 | oswald: ['Oswald', ...fontFamily.sans],
69 | sans: ['var(--font-sans)', ...fontFamily.sans],
70 | heading: ['var(--font-heading)', ...fontFamily.sans],
71 | },
72 | keyframes: {
73 | 'accordion-down': {
74 | from: { height: 0 },
75 | to: { height: 'var(--radix-accordion-content-height)' },
76 | },
77 | 'accordion-up': {
78 | from: { height: 'var(--radix-accordion-content-height)' },
79 | to: { height: 0 },
80 | },
81 | },
82 | animation: {
83 | 'accordion-down': 'accordion-down 0.2s ease-out',
84 | 'accordion-up': 'accordion-up 0.2s ease-out',
85 | },
86 | },
87 | },
88 | plugins: [require('tailwindcss-animate'), require('@tailwindcss/typography')],
89 | };
90 |
--------------------------------------------------------------------------------
/packages/tsconfig/base.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "Default",
4 | "compilerOptions": {
5 | "esModuleInterop": true,
6 | "skipLibCheck": true,
7 | "target": "es2022",
8 | "resolveJsonModule": true,
9 | "isolatedModules": true,
10 | "moduleDetection": "force",
11 | "strict": true,
12 | "noUncheckedIndexedAccess": true,
13 | "moduleResolution": "Bundler",
14 | "module": "ESNext",
15 | "noEmit": true,
16 | "lib": ["es2022", "dom", "dom.iterable"],
17 | },
18 | "exclude": ["node_modules"]
19 | }
20 |
--------------------------------------------------------------------------------
/packages/tsconfig/nextjs.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es6",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "plugins": [
18 | {
19 | "name": "next"
20 | }
21 | ]
22 | },
23 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
24 | "exclude": ["node_modules"]
25 | }
--------------------------------------------------------------------------------
/packages/tsconfig/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tsconfig",
3 | "version": "0.0.0",
4 | "private": true,
5 | "license": "MIT",
6 | "publishConfig": {
7 | "access": "public"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/packages/tsconfig/react-library.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "React Library",
4 | "extends": "./base.json",
5 | "compilerOptions": {
6 | "jsx": "react-jsx",
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/packages/ui/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ["custom/library"],
3 | };
4 |
--------------------------------------------------------------------------------
/packages/ui/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # ui
2 |
3 | ## 0.0.2
4 |
5 | ### Patch Changes
6 |
7 | - [`f577fdc`](https://github.com/JadRizk/turborepo-launchpad/commit/f577fdcd5ab04413456ae1885cce8c0577a0a6e3)
8 | Thanks [@JadRizk](https://github.com/JadRizk)! - Add Card and Select component
9 | form Shadcn. And Border bean component from magicUI
10 |
11 | ## 0.0.1
12 |
13 | ### Patch Changes
14 |
15 | - [#13](https://github.com/JadRizk/miniature-launchpad/pull/13)
16 | [`2290e14`](https://github.com/JadRizk/miniature-launchpad/commit/2290e14ede8fbaa169a61282934da798064c0bfe)
17 | Thanks [@JadRizk](https://github.com/JadRizk)! - 🚀 Initial Log
18 |
--------------------------------------------------------------------------------
/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.js",
8 | "css": "styles/globals.css",
9 | "baseColor": "stone",
10 | "cssVariables": true
11 | },
12 | "aliases": {
13 | "components": "src/components",
14 | "utils": "src/lib/utils"
15 | }
16 | }
--------------------------------------------------------------------------------
/packages/ui/generate-tokens.js:
--------------------------------------------------------------------------------
1 | const fs = require('node:fs');
2 | const resolveConfig = require('tailwindcss/resolveConfig');
3 | const defaultColors = require('tailwindcss/colors');
4 | const tailwindConfig = require('./tailwind.config.js');
5 |
6 | const fullConfig = resolveConfig(tailwindConfig);
7 |
8 | // Separate custom colors from Tailwind default colors
9 | const customColors = {};
10 | const tailwindColors = {};
11 |
12 | // Separates custom colors from Tailwind default colors.
13 | Object.entries(fullConfig.theme.colors).forEach(([key, value]) => {
14 | if (defaultColors[key] === value) {
15 | tailwindColors[key] = value;
16 | } else {
17 | customColors[key] = value;
18 | }
19 | });
20 |
21 | const tokens = {
22 | customColors,
23 | tailwindColors,
24 | spacing: fullConfig.theme.spacing,
25 | typography: fullConfig.theme.fontFamily,
26 | borderRadius: fullConfig.theme.borderRadius,
27 | };
28 |
29 | // Generate TypeScript content
30 | const tsContent = `
31 | export const customColors = ${JSON.stringify(tokens.customColors, null, 2)};
32 | export const tailwindColors = ${JSON.stringify(tokens.tailwindColors, null, 2)};
33 | export const spacing = ${JSON.stringify(tokens.spacing, null, 2)};
34 | export const typography = ${JSON.stringify(tokens.typography, null, 2)};
35 | export const borderRadius = ${JSON.stringify(tokens.borderRadius, null, 2)};
36 | `;
37 |
38 | // Save tokens to a TypeScript file
39 | fs.writeFileSync('../../apps/docs/tokens.ts', tsContent);
40 |
41 | // eslint-disable-next-line no-console -- On success, log a message.
42 | console.log('✅ Tokens generated successfully!');
43 |
--------------------------------------------------------------------------------
/packages/ui/index.tsx:
--------------------------------------------------------------------------------
1 | // shadcn component exports
2 | export * from './src/components/ui/button';
3 | export * from './src/components/ui/icons';
4 | export * from './src/components/ui/dropdown-menu';
5 | export * from './src/components/ui/dialog';
6 | export * from './src/components/ui/input';
7 | export * from './src/components/ui/label';
8 | export * from './src/components/ui/skeleton';
9 | export * from './src/components/ui/toast';
10 | export * from './src/components/ui/toaster';
11 | export * from './src/components/ui/select';
12 | export * from './src/components/ui/card';
13 | export * from './src/components/magicui/border-beam';
14 |
15 | export * from './src/components/ui/use-toast';
16 |
17 | export { cn } from './src/lib/utils';
18 |
--------------------------------------------------------------------------------
/packages/ui/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ui",
3 | "version": "0.0.2",
4 | "main": "./index.tsx",
5 | "types": "./index.tsx",
6 | "license": "MIT",
7 | "scripts": {
8 | "lint": "eslint .",
9 | "generate:tokens": "node generate-tokens.js",
10 | "generate:component": "turbo gen react-component"
11 | },
12 | "devDependencies": {
13 | "@turbo/gen": "^1.10.12",
14 | "@types/node": "^20.5.2",
15 | "@types/react": "^18.2.0",
16 | "@types/react-dom": "^18.2.0",
17 | "eslint-config-custom": "workspace:*",
18 | "motion": "^12.4.7",
19 | "react": "^18.2.0",
20 | "tailwind-config": "workspace:*",
21 | "tailwindcss": "^3.3.0",
22 | "tsconfig": "workspace:*",
23 | "typescript": "^5.2.2"
24 | },
25 | "dependencies": {
26 | "@hookform/resolvers": "^3.3.4",
27 | "@radix-ui/react-accordion": "^1.1.2",
28 | "@radix-ui/react-dialog": "^1.0.5",
29 | "@radix-ui/react-dropdown-menu": "^2.0.6",
30 | "@radix-ui/react-icons": "^1.3.0",
31 | "@radix-ui/react-label": "^2.0.2",
32 | "@radix-ui/react-radio-group": "^1.1.3",
33 | "@radix-ui/react-select": "^2.1.6",
34 | "@radix-ui/react-separator": "^1.0.3",
35 | "@radix-ui/react-slot": "^1.0.2",
36 | "@radix-ui/react-toast": "^1.1.5",
37 | "class-variance-authority": "^0.7.0",
38 | "clsx": "^2.0.0",
39 | "lucide-react": "^0.292.0",
40 | "react-hook-form": "^7.48.2",
41 | "tailwind-merge": "^2.0.0",
42 | "tailwindcss-animate": "^1.0.7",
43 | "zod": "^3.22.4"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/packages/ui/src/components/magicui/border-beam.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import type { MotionStyle, Transition } from 'motion/react';
4 | import { motion } from 'motion/react';
5 | import { cn } from '../../lib/utils';
6 |
7 | interface BorderBeamProps {
8 | /**
9 | * The size of the border beam.
10 | */
11 | size?: number;
12 | /**
13 | * The duration of the border beam.
14 | */
15 | duration?: number;
16 | /**
17 | * The delay of the border beam.
18 | */
19 | delay?: number;
20 | /**
21 | * The color of the border beam from.
22 | */
23 | colorFrom?: string;
24 | /**
25 | * The color of the border beam to.
26 | */
27 | colorTo?: string;
28 | /**
29 | * The motion transition of the border beam.
30 | */
31 | transition?: Transition;
32 | /**
33 | * The class name of the border beam.
34 | */
35 | className?: string;
36 | /**
37 | * The style of the border beam.
38 | */
39 | style?: React.CSSProperties;
40 | /**
41 | * Whether to reverse the animation direction.
42 | */
43 | reverse?: boolean;
44 | /**
45 | * The initial offset position (0-100).
46 | */
47 | initialOffset?: number;
48 | }
49 |
50 | export const BorderBeam = ({
51 | className,
52 | size = 50,
53 | delay = 0,
54 | duration = 6,
55 | colorFrom = '#ffaa40',
56 | colorTo = '#9c40ff',
57 | transition,
58 | style,
59 | reverse = false,
60 | initialOffset = 0,
61 | }: BorderBeamProps) => {
62 | return (
63 |
64 |
95 |
96 | );
97 | };
98 |
--------------------------------------------------------------------------------
/packages/ui/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Slot } from '@radix-ui/react-slot';
3 | import { cva, type VariantProps } from 'class-variance-authority';
4 | import { cn } from '../../lib/utils';
5 | import { Icons } from './icons';
6 |
7 | const buttonVariants = cva(
8 | 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
14 | primary:
15 | 'bg-reefGreen-500 text-primary dark:text-primary-foreground shadow hover:bg-reefGreen-400 ',
16 | destructive:
17 | 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
18 | outline:
19 | 'border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground',
20 | secondary:
21 | 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
22 | ghost: 'hover:bg-accent hover:text-accent-foreground',
23 | link: 'text-primary underline-offset-4 hover:underline',
24 | },
25 | size: {
26 | default: 'h-9 px-4 py-2',
27 | sm: 'h-8 rounded-md px-3 text-xs',
28 | lg: 'h-10 rounded-md px-8',
29 | icon: 'h-9 w-9',
30 | },
31 | },
32 | defaultVariants: {
33 | variant: 'default',
34 | size: 'default',
35 | },
36 | },
37 | );
38 |
39 | export interface ButtonProps
40 | extends React.ButtonHTMLAttributes,
41 | VariantProps {
42 | asChild?: boolean;
43 | loading?: boolean;
44 | }
45 |
46 | const Button = React.forwardRef(
47 | (
48 | {
49 | className,
50 | variant,
51 | size,
52 | asChild = false,
53 | loading = false,
54 | children,
55 | ...props
56 | },
57 | ref,
58 | ) => {
59 | const Comp = asChild ? Slot : 'button';
60 | return (
61 |
70 | {loading ? (
71 | <>
72 | Please wait
73 | >
74 | ) : (
75 | children
76 | )}
77 |
78 | );
79 | },
80 | );
81 |
82 | Button.displayName = 'Button';
83 |
84 | export { Button, buttonVariants };
85 |
--------------------------------------------------------------------------------
/packages/ui/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { cn } from '../../lib/utils';
3 |
4 | const Card = React.forwardRef<
5 | HTMLDivElement,
6 | React.HTMLAttributes
7 | >(({ className, ...props }, ref) => (
8 |
16 | ));
17 | Card.displayName = 'Card';
18 |
19 | const CardHeader = React.forwardRef<
20 | HTMLDivElement,
21 | React.HTMLAttributes
22 | >(({ className, ...props }, ref) => (
23 |
28 | ));
29 | CardHeader.displayName = 'CardHeader';
30 |
31 | const CardTitle = React.forwardRef<
32 | HTMLDivElement,
33 | React.HTMLAttributes
34 | >(({ className, ...props }, ref) => (
35 |
40 | ));
41 | CardTitle.displayName = 'CardTitle';
42 |
43 | const CardDescription = React.forwardRef<
44 | HTMLDivElement,
45 | React.HTMLAttributes
46 | >(({ className, ...props }, ref) => (
47 |
52 | ));
53 | CardDescription.displayName = 'CardDescription';
54 |
55 | const CardContent = React.forwardRef<
56 | HTMLDivElement,
57 | React.HTMLAttributes
58 | >(({ className, ...props }, ref) => (
59 |
60 | ));
61 | CardContent.displayName = 'CardContent';
62 |
63 | const CardFooter = React.forwardRef<
64 | HTMLDivElement,
65 | React.HTMLAttributes
66 | >(({ className, ...props }, ref) => (
67 |
72 | ));
73 | CardFooter.displayName = 'CardFooter';
74 |
75 | export {
76 | Card,
77 | CardHeader,
78 | CardFooter,
79 | CardTitle,
80 | CardDescription,
81 | CardContent,
82 | };
83 |
--------------------------------------------------------------------------------
/packages/ui/src/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as DialogPrimitive from '@radix-ui/react-dialog';
5 | import { Cross2Icon } from '@radix-ui/react-icons';
6 | import { cn } from '../../lib/utils';
7 |
8 | const Dialog = DialogPrimitive.Root;
9 |
10 | const DialogTrigger = DialogPrimitive.Trigger;
11 |
12 | const DialogPortal = DialogPrimitive.Portal;
13 |
14 | const DialogClose = DialogPrimitive.Close;
15 |
16 | const DialogOverlay = React.forwardRef<
17 | React.ElementRef,
18 | React.ComponentPropsWithoutRef
19 | >(({ className, ...props }, ref) => (
20 |
28 | ));
29 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
30 |
31 | const DialogContent = React.forwardRef<
32 | React.ElementRef,
33 | React.ComponentPropsWithoutRef
34 | >(({ className, children, ...props }, ref) => (
35 |
36 |
37 |
45 | {children}
46 |
47 |
48 | Close
49 |
50 |
51 |
52 | ));
53 | DialogContent.displayName = DialogPrimitive.Content.displayName;
54 |
55 | const DialogHeader = ({
56 | className,
57 | ...props
58 | }: React.HTMLAttributes) => (
59 |
66 | );
67 | DialogHeader.displayName = 'DialogHeader';
68 |
69 | const DialogFooter = ({
70 | className,
71 | ...props
72 | }: React.HTMLAttributes) => (
73 |
80 | );
81 | DialogFooter.displayName = 'DialogFooter';
82 |
83 | const DialogTitle = React.forwardRef<
84 | React.ElementRef,
85 | React.ComponentPropsWithoutRef
86 | >(({ className, ...props }, ref) => (
87 |
95 | ));
96 | DialogTitle.displayName = DialogPrimitive.Title.displayName;
97 |
98 | const DialogDescription = React.forwardRef<
99 | React.ElementRef,
100 | React.ComponentPropsWithoutRef
101 | >(({ className, ...props }, ref) => (
102 |
107 | ));
108 | DialogDescription.displayName = DialogPrimitive.Description.displayName;
109 |
110 | export {
111 | Dialog,
112 | DialogPortal,
113 | DialogOverlay,
114 | DialogTrigger,
115 | DialogClose,
116 | DialogContent,
117 | DialogHeader,
118 | DialogFooter,
119 | DialogTitle,
120 | DialogDescription,
121 | };
122 |
--------------------------------------------------------------------------------
/packages/ui/src/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
5 | import {
6 | CheckIcon,
7 | ChevronRightIcon,
8 | DotFilledIcon,
9 | } from '@radix-ui/react-icons';
10 | import { cn } from '../../lib/utils';
11 |
12 | const DropdownMenu = DropdownMenuPrimitive.Root;
13 |
14 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
15 |
16 | const DropdownMenuGroup = DropdownMenuPrimitive.Group;
17 |
18 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
19 |
20 | const DropdownMenuSub = DropdownMenuPrimitive.Sub;
21 |
22 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
23 |
24 | const DropdownMenuSubTrigger = React.forwardRef<
25 | React.ElementRef,
26 | React.ComponentPropsWithoutRef & {
27 | inset?: boolean;
28 | }
29 | >(({ className, inset, children, ...props }, ref) => (
30 |
39 | {children}
40 |
41 |
42 | ));
43 | DropdownMenuSubTrigger.displayName =
44 | DropdownMenuPrimitive.SubTrigger.displayName;
45 |
46 | const DropdownMenuSubContent = React.forwardRef<
47 | React.ElementRef,
48 | React.ComponentPropsWithoutRef
49 | >(({ className, ...props }, ref) => (
50 |
58 | ));
59 | DropdownMenuSubContent.displayName =
60 | DropdownMenuPrimitive.SubContent.displayName;
61 |
62 | const DropdownMenuContent = React.forwardRef<
63 | React.ElementRef,
64 | React.ComponentPropsWithoutRef
65 | >(({ className, sideOffset = 4, ...props }, ref) => (
66 |
67 |
77 |
78 | ));
79 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
80 |
81 | const DropdownMenuItem = React.forwardRef<
82 | React.ElementRef,
83 | React.ComponentPropsWithoutRef & {
84 | inset?: boolean;
85 | }
86 | >(({ className, inset, ...props }, ref) => (
87 |
96 | ));
97 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
98 |
99 | const DropdownMenuCheckboxItem = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, children, checked, ...props }, ref) => (
103 |
112 |
113 |
114 |
115 |
116 |
117 | {children}
118 |
119 | ));
120 | DropdownMenuCheckboxItem.displayName =
121 | DropdownMenuPrimitive.CheckboxItem.displayName;
122 |
123 | const DropdownMenuRadioItem = React.forwardRef<
124 | React.ElementRef,
125 | React.ComponentPropsWithoutRef
126 | >(({ className, children, ...props }, ref) => (
127 |
135 |
136 |
137 |
138 |
139 |
140 | {children}
141 |
142 | ));
143 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
144 |
145 | const DropdownMenuLabel = React.forwardRef<
146 | React.ElementRef,
147 | React.ComponentPropsWithoutRef & {
148 | inset?: boolean;
149 | }
150 | >(({ className, inset, ...props }, ref) => (
151 |
160 | ));
161 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
162 |
163 | const DropdownMenuSeparator = React.forwardRef<
164 | React.ElementRef,
165 | React.ComponentPropsWithoutRef
166 | >(({ className, ...props }, ref) => (
167 |
172 | ));
173 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
174 |
175 | const DropdownMenuShortcut = ({
176 | className,
177 | ...props
178 | }: React.HTMLAttributes) => {
179 | return (
180 |
184 | );
185 | };
186 | DropdownMenuShortcut.displayName = 'DropdownMenuShortcut';
187 |
188 | export {
189 | DropdownMenu,
190 | DropdownMenuTrigger,
191 | DropdownMenuContent,
192 | DropdownMenuItem,
193 | DropdownMenuCheckboxItem,
194 | DropdownMenuRadioItem,
195 | DropdownMenuLabel,
196 | DropdownMenuSeparator,
197 | DropdownMenuShortcut,
198 | DropdownMenuGroup,
199 | DropdownMenuPortal,
200 | DropdownMenuSub,
201 | DropdownMenuSubContent,
202 | DropdownMenuSubTrigger,
203 | DropdownMenuRadioGroup,
204 | };
205 |
--------------------------------------------------------------------------------
/packages/ui/src/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import type * as LabelPrimitive from '@radix-ui/react-label';
3 | import { Slot } from '@radix-ui/react-slot';
4 | import type { ControllerProps, FieldPath, FieldValues } from 'react-hook-form';
5 | import { Controller, FormProvider, useFormContext } from 'react-hook-form';
6 | import { cn } from '../../lib/utils';
7 | import { Label } from './label';
8 |
9 | const Form = FormProvider;
10 |
11 | interface FormFieldContextValue<
12 | TFieldValues extends FieldValues = FieldValues,
13 | TName extends FieldPath = FieldPath,
14 | > {
15 | name: TName;
16 | }
17 |
18 | const FormFieldContext = React.createContext(
19 | {} as FormFieldContextValue,
20 | );
21 |
22 | const FormField = <
23 | TFieldValues extends FieldValues = FieldValues,
24 | TName extends FieldPath = FieldPath,
25 | >({
26 | ...props
27 | }: ControllerProps) => {
28 | return (
29 |
30 |
31 |
32 | );
33 | };
34 |
35 | const useFormField = () => {
36 | const fieldContext = React.useContext(FormFieldContext);
37 | const itemContext = React.useContext(FormItemContext);
38 | const { getFieldState, formState } = useFormContext();
39 |
40 | const fieldState = getFieldState(fieldContext.name, formState);
41 |
42 | const { id } = itemContext;
43 |
44 | return {
45 | id,
46 | name: fieldContext.name,
47 | formItemId: `${id}-form-item`,
48 | formDescriptionId: `${id}-form-item-description`,
49 | formMessageId: `${id}-form-item-message`,
50 | ...fieldState,
51 | };
52 | };
53 |
54 | interface FormItemContextValue {
55 | id: string;
56 | }
57 |
58 | const FormItemContext = React.createContext(
59 | {} as FormItemContextValue,
60 | );
61 |
62 | const FormItem = React.forwardRef<
63 | HTMLDivElement,
64 | React.HTMLAttributes
65 | >(({ className, ...props }, ref) => {
66 | const id = React.useId();
67 |
68 | return (
69 |
70 |
71 |
72 | );
73 | });
74 | FormItem.displayName = 'FormItem';
75 |
76 | const FormLabel = React.forwardRef<
77 | React.ElementRef,
78 | React.ComponentPropsWithoutRef
79 | >(({ className, ...props }, ref) => {
80 | const { error, formItemId } = useFormField();
81 |
82 | return (
83 |
89 | );
90 | });
91 | FormLabel.displayName = 'FormLabel';
92 |
93 | const FormControl = React.forwardRef<
94 | React.ElementRef,
95 | React.ComponentPropsWithoutRef
96 | >(({ ...props }, ref) => {
97 | const { error, formItemId, formDescriptionId, formMessageId } =
98 | useFormField();
99 |
100 | return (
101 |
110 | );
111 | });
112 | FormControl.displayName = 'FormControl';
113 |
114 | const FormDescription = React.forwardRef<
115 | HTMLParagraphElement,
116 | React.HTMLAttributes
117 | >(({ className, ...props }, ref) => {
118 | const { formDescriptionId } = useFormField();
119 |
120 | return (
121 |
127 | );
128 | });
129 | FormDescription.displayName = 'FormDescription';
130 |
131 | const FormMessage = React.forwardRef<
132 | HTMLParagraphElement,
133 | React.HTMLAttributes
134 | >(({ className, children, ...props }, ref) => {
135 | const { error, formMessageId } = useFormField();
136 | const body = error ? String(error.message) : children;
137 |
138 | if (!body) {
139 | return null;
140 | }
141 |
142 | return (
143 |
149 | {body}
150 |
151 | );
152 | });
153 | FormMessage.displayName = 'FormMessage';
154 |
155 | export {
156 | useFormField,
157 | Form,
158 | FormItem,
159 | FormLabel,
160 | FormControl,
161 | FormDescription,
162 | FormMessage,
163 | FormField,
164 | };
165 |
--------------------------------------------------------------------------------
/packages/ui/src/components/ui/icons.tsx:
--------------------------------------------------------------------------------
1 | import type { LucideIcon } from 'lucide-react';
2 | import {
3 | ShieldCheck,
4 | ChevronLeft,
5 | ChevronRight,
6 | Github,
7 | Loader2,
8 | Menu,
9 | Moon,
10 | Rocket,
11 | SunMedium,
12 | User,
13 | X,
14 | } from 'lucide-react';
15 |
16 | export type Icon = LucideIcon;
17 |
18 | export const Icons = {
19 | logo: Rocket,
20 | close: X,
21 | Spinner: Loader2,
22 | ChevronLeft,
23 | ChevronRight,
24 | User,
25 | Sun: SunMedium,
26 | Moon,
27 | Github,
28 | Menu,
29 | ShieldCheck,
30 | };
31 |
--------------------------------------------------------------------------------
/packages/ui/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { cn } from '../../lib/utils';
3 |
4 | export type InputProps = React.InputHTMLAttributes & {
5 | invalid?: boolean;
6 | };
7 |
8 | const Input = React.forwardRef(
9 | ({ className, invalid, type, ...props }, ref) => {
10 | return (
11 |
21 | );
22 | },
23 | );
24 | Input.displayName = 'Input';
25 |
26 | export { Input };
27 |
--------------------------------------------------------------------------------
/packages/ui/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as LabelPrimitive from '@radix-ui/react-label';
5 | import { cva, type VariantProps } from 'class-variance-authority';
6 | import { cn } from '../../lib/utils';
7 |
8 | const labelVariants = cva(
9 | 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
10 | );
11 |
12 | const Label = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef &
15 | VariantProps
16 | >(({ className, ...props }, ref) => (
17 |
22 | ));
23 | Label.displayName = LabelPrimitive.Root.displayName;
24 |
25 | export { Label };
26 |
--------------------------------------------------------------------------------
/packages/ui/src/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as SelectPrimitive from '@radix-ui/react-select';
5 | import {
6 | CheckIcon,
7 | ChevronDownIcon,
8 | ChevronUpIcon,
9 | } from '@radix-ui/react-icons';
10 | import { cn } from '../../lib/utils';
11 |
12 | const Select = SelectPrimitive.Root;
13 |
14 | const SelectGroup = SelectPrimitive.Group;
15 |
16 | const SelectValue = SelectPrimitive.Value;
17 |
18 | const SelectTrigger = React.forwardRef<
19 | React.ElementRef,
20 | React.ComponentPropsWithoutRef
21 | >(({ className, children, ...props }, ref) => (
22 | span]:line-clamp-1',
26 | className,
27 | )}
28 | {...props}
29 | >
30 | {children}
31 |
32 |
33 |
34 |
35 | ));
36 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
37 |
38 | const SelectScrollUpButton = React.forwardRef<
39 | React.ElementRef,
40 | React.ComponentPropsWithoutRef
41 | >(({ className, ...props }, ref) => (
42 |
50 |
51 |
52 | ));
53 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
54 |
55 | const SelectScrollDownButton = React.forwardRef<
56 | React.ElementRef,
57 | React.ComponentPropsWithoutRef
58 | >(({ className, ...props }, ref) => (
59 |
67 |
68 |
69 | ));
70 | SelectScrollDownButton.displayName =
71 | SelectPrimitive.ScrollDownButton.displayName;
72 |
73 | const SelectContent = React.forwardRef<
74 | React.ElementRef,
75 | React.ComponentPropsWithoutRef
76 | >(({ className, children, position = 'popper', ...props }, ref) => (
77 |
78 |
89 |
90 |
97 | {children}
98 |
99 |
100 |
101 |
102 | ));
103 | SelectContent.displayName = SelectPrimitive.Content.displayName;
104 |
105 | const SelectLabel = React.forwardRef<
106 | React.ElementRef,
107 | React.ComponentPropsWithoutRef
108 | >(({ className, ...props }, ref) => (
109 |
114 | ));
115 | SelectLabel.displayName = SelectPrimitive.Label.displayName;
116 |
117 | const SelectItem = React.forwardRef<
118 | React.ElementRef,
119 | React.ComponentPropsWithoutRef
120 | >(({ className, children, ...props }, ref) => (
121 |
129 |
130 |
131 |
132 |
133 |
134 | {children}
135 |
136 | ));
137 | SelectItem.displayName = SelectPrimitive.Item.displayName;
138 |
139 | const SelectSeparator = React.forwardRef<
140 | React.ElementRef,
141 | React.ComponentPropsWithoutRef
142 | >(({ className, ...props }, ref) => (
143 |
148 | ));
149 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
150 |
151 | export {
152 | Select,
153 | SelectGroup,
154 | SelectValue,
155 | SelectTrigger,
156 | SelectContent,
157 | SelectLabel,
158 | SelectItem,
159 | SelectSeparator,
160 | SelectScrollUpButton,
161 | SelectScrollDownButton,
162 | };
163 |
--------------------------------------------------------------------------------
/packages/ui/src/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as SeparatorPrimitive from '@radix-ui/react-separator';
5 | import { cn } from '../../lib/utils';
6 |
7 | const Separator = React.forwardRef<
8 | React.ElementRef,
9 | React.ComponentPropsWithoutRef
10 | >(
11 | (
12 | { className, orientation = 'horizontal', decorative = true, ...props },
13 | ref,
14 | ) => (
15 |
26 | ),
27 | );
28 | Separator.displayName = SeparatorPrimitive.Root.displayName;
29 |
30 | export { Separator };
31 |
--------------------------------------------------------------------------------
/packages/ui/src/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '../../lib/utils';
2 |
3 | function Skeleton({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) {
7 | return (
8 |
12 | );
13 | }
14 |
15 | export { Skeleton };
16 |
--------------------------------------------------------------------------------
/packages/ui/src/components/ui/toast.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import { Cross2Icon } from '@radix-ui/react-icons';
5 | import * as ToastPrimitives from '@radix-ui/react-toast';
6 | import { cva, type VariantProps } from 'class-variance-authority';
7 | import { cn } from '../../lib/utils';
8 |
9 | const ToastProvider = ToastPrimitives.Provider;
10 |
11 | const ToastViewport = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, ...props }, ref) => (
15 |
23 | ));
24 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
25 |
26 | const toastVariants = cva(
27 | 'group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full',
28 | {
29 | variants: {
30 | variant: {
31 | default: 'border bg-background text-foreground',
32 | destructive:
33 | 'destructive group border-destructive bg-destructive text-destructive-foreground',
34 | },
35 | },
36 | defaultVariants: {
37 | variant: 'default',
38 | },
39 | },
40 | );
41 |
42 | const Toast = React.forwardRef<
43 | React.ElementRef,
44 | React.ComponentPropsWithoutRef &
45 | VariantProps
46 | >(({ className, variant, ...props }, ref) => {
47 | return (
48 |
53 | );
54 | });
55 | Toast.displayName = ToastPrimitives.Root.displayName;
56 |
57 | const ToastAction = React.forwardRef<
58 | React.ElementRef,
59 | React.ComponentPropsWithoutRef
60 | >(({ className, ...props }, ref) => (
61 |
69 | ));
70 | ToastAction.displayName = ToastPrimitives.Action.displayName;
71 |
72 | const ToastClose = React.forwardRef<
73 | React.ElementRef,
74 | React.ComponentPropsWithoutRef
75 | >(({ className, ...props }, ref) => (
76 |
85 |
86 |
87 | ));
88 | ToastClose.displayName = ToastPrimitives.Close.displayName;
89 |
90 | const ToastTitle = React.forwardRef<
91 | React.ElementRef,
92 | React.ComponentPropsWithoutRef
93 | >(({ className, ...props }, ref) => (
94 |
99 | ));
100 | ToastTitle.displayName = ToastPrimitives.Title.displayName;
101 |
102 | const ToastDescription = React.forwardRef<
103 | React.ElementRef,
104 | React.ComponentPropsWithoutRef
105 | >(({ className, ...props }, ref) => (
106 |
111 | ));
112 | ToastDescription.displayName = ToastPrimitives.Description.displayName;
113 |
114 | type ToastProps = React.ComponentPropsWithoutRef;
115 |
116 | type ToastActionElement = React.ReactElement;
117 |
118 | export {
119 | type ToastProps,
120 | type ToastActionElement,
121 | ToastProvider,
122 | ToastViewport,
123 | Toast,
124 | ToastTitle,
125 | ToastDescription,
126 | ToastClose,
127 | ToastAction,
128 | };
129 |
--------------------------------------------------------------------------------
/packages/ui/src/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useToast } from './use-toast';
4 | import {
5 | Toast,
6 | ToastClose,
7 | ToastDescription,
8 | ToastProvider,
9 | ToastTitle,
10 | ToastViewport,
11 | } from './toast';
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast();
15 |
16 | return (
17 |
18 | {toasts.map(({ id, title, description, action, ...props }) => {
19 | return (
20 |
21 |
22 | {title && {title}}
23 | {description && (
24 | {description}
25 | )}
26 |
27 | {action}
28 |
29 |
30 | );
31 | })}
32 |
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/packages/ui/src/components/ui/use-toast.ts:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | // Inspired by react-hot-toast library
4 | import * as React from 'react';
5 | import type { ToastActionElement, ToastProps } from './toast';
6 |
7 | const TOAST_LIMIT = 1;
8 | const TOAST_REMOVE_DELAY = 1000000;
9 |
10 | type ToasterToast = ToastProps & {
11 | id: string;
12 | title?: React.ReactNode;
13 | description?: React.ReactNode;
14 | action?: ToastActionElement;
15 | };
16 |
17 | const actionTypes = {
18 | ADD_TOAST: 'ADD_TOAST',
19 | UPDATE_TOAST: 'UPDATE_TOAST',
20 | DISMISS_TOAST: 'DISMISS_TOAST',
21 | REMOVE_TOAST: 'REMOVE_TOAST',
22 | } as const;
23 |
24 | let count = 0;
25 |
26 | function genId() {
27 | count = (count + 1) % Number.MAX_SAFE_INTEGER;
28 | return count.toString();
29 | }
30 |
31 | type ActionType = typeof actionTypes;
32 |
33 | type Action =
34 | | {
35 | type: ActionType['ADD_TOAST'];
36 | toast: ToasterToast;
37 | }
38 | | {
39 | type: ActionType['UPDATE_TOAST'];
40 | toast: Partial;
41 | }
42 | | {
43 | type: ActionType['DISMISS_TOAST'];
44 | toastId?: ToasterToast['id'];
45 | }
46 | | {
47 | type: ActionType['REMOVE_TOAST'];
48 | toastId?: ToasterToast['id'];
49 | };
50 |
51 | interface ToastState {
52 | toasts: ToasterToast[];
53 | }
54 |
55 | const toastTimeouts = new Map>();
56 |
57 | const addToRemoveQueue = (toastId: string) => {
58 | if (toastTimeouts.has(toastId)) {
59 | return;
60 | }
61 |
62 | const timeout = setTimeout(() => {
63 | toastTimeouts.delete(toastId);
64 | dispatch({
65 | type: 'REMOVE_TOAST',
66 | toastId,
67 | });
68 | }, TOAST_REMOVE_DELAY);
69 |
70 | toastTimeouts.set(toastId, timeout);
71 | };
72 |
73 | export const reducer = (state: ToastState, action: Action): ToastState => {
74 | switch (action.type) {
75 | case 'ADD_TOAST':
76 | return {
77 | ...state,
78 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
79 | };
80 |
81 | case 'UPDATE_TOAST':
82 | return {
83 | ...state,
84 | toasts: state.toasts.map(t =>
85 | t.id === action.toast.id ? { ...t, ...action.toast } : t,
86 | ),
87 | };
88 |
89 | case 'DISMISS_TOAST': {
90 | const { toastId } = action;
91 |
92 | // ! Side effects ! - This could be extracted into a dismissToast() action,
93 | // but I'll keep it here for simplicity
94 | if (toastId) {
95 | addToRemoveQueue(toastId);
96 | } else {
97 | state.toasts.forEach(item => {
98 | addToRemoveQueue(item.id);
99 | });
100 | }
101 |
102 | return {
103 | ...state,
104 | toasts: state.toasts.map(t =>
105 | t.id === toastId || toastId === undefined
106 | ? {
107 | ...t,
108 | open: false,
109 | }
110 | : t,
111 | ),
112 | };
113 | }
114 | case 'REMOVE_TOAST':
115 | if (action.toastId === undefined) {
116 | return {
117 | ...state,
118 | toasts: [],
119 | };
120 | }
121 | return {
122 | ...state,
123 | toasts: state.toasts.filter(t => t.id !== action.toastId),
124 | };
125 | }
126 | };
127 |
128 | const listeners: ((state: ToastState) => void)[] = [];
129 |
130 | let memoryState: ToastState = { toasts: [] };
131 |
132 | function dispatch(action: Action) {
133 | memoryState = reducer(memoryState, action);
134 | listeners.forEach(listener => {
135 | listener(memoryState);
136 | });
137 | }
138 |
139 | type Toast = Omit;
140 |
141 | function toast({ ...props }: Toast) {
142 | const id = genId();
143 |
144 | const update = (values: ToasterToast) => {
145 | dispatch({
146 | type: 'UPDATE_TOAST',
147 | toast: { ...values, id },
148 | });
149 | };
150 | const dismiss = () => {
151 | dispatch({ type: 'DISMISS_TOAST', toastId: id });
152 | };
153 |
154 | dispatch({
155 | type: 'ADD_TOAST',
156 | toast: {
157 | ...props,
158 | id,
159 | open: true,
160 | onOpenChange: open => {
161 | if (!open) dismiss();
162 | },
163 | },
164 | });
165 |
166 | return {
167 | id,
168 | dismiss,
169 | update,
170 | };
171 | }
172 |
173 | function useToast() {
174 | const [state, setState] = React.useState(memoryState);
175 |
176 | React.useEffect(() => {
177 | listeners.push(setState);
178 | return () => {
179 | const index = listeners.indexOf(setState);
180 | if (index > -1) {
181 | listeners.splice(index, 1);
182 | }
183 | };
184 | }, [state]);
185 |
186 | return {
187 | ...state,
188 | toast,
189 | dismiss: (toastId?: string) => {
190 | dispatch({ type: 'DISMISS_TOAST', toastId });
191 | },
192 | };
193 | }
194 |
195 | export { useToast, toast };
196 |
--------------------------------------------------------------------------------
/packages/ui/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import type { ClassValue } from 'clsx';
2 | import { clsx } from 'clsx';
3 | import { twMerge } from 'tailwind-merge';
4 |
5 | export function cn(...inputs: ClassValue[]) {
6 | return twMerge(clsx(inputs));
7 | }
8 |
--------------------------------------------------------------------------------
/packages/ui/styles/globals.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Manrope:wght@200..800&family=Oswald:wght@200..700&display=swap');
2 |
3 | @tailwind base;
4 | @tailwind components;
5 | @tailwind utilities;
6 |
7 | @layer base {
8 | :root {
9 | --background: 0 0% 100%;
10 | --foreground: 20 14.3% 4.1%;
11 |
12 | --card: 0 0% 100%;
13 | --card-foreground: 20 14.3% 4.1%;
14 |
15 | --popover: 0 0% 100%;
16 | --popover-foreground: 20 14.3% 4.1%;
17 |
18 | --primary: 24 9.8% 10%;
19 | --primary-foreground: 60 9.1% 97.8%;
20 |
21 | --secondary: 60 4.8% 95.9%;
22 | --secondary-foreground: 24 9.8% 10%;
23 |
24 | --muted: 60 4.8% 95.9%;
25 | --muted-foreground: 25 5.3% 44.7%;
26 |
27 | --accent: 60 4.8% 95.9%;
28 | --accent-foreground: 24 9.8% 10%;
29 |
30 | --destructive: 0 84.2% 60.2%;
31 | --destructive-foreground: 60 9.1% 97.8%;
32 |
33 | --border: 20 5.9% 90%;
34 | --input: 20 5.9% 90%;
35 | --ring: 20 14.3% 4.1%;
36 |
37 | --radius: 0.5rem;
38 | }
39 |
40 | .dark {
41 | --background: 20 14.3% 4.1%;
42 | --foreground: 60 9.1% 97.8%;
43 |
44 | --card: 20 14.3% 4.1%;
45 | --card-foreground: 60 9.1% 97.8%;
46 |
47 | --popover: 20 14.3% 4.1%;
48 | --popover-foreground: 60 9.1% 97.8%;
49 |
50 | --primary: 60 9.1% 97.8%;
51 | --primary-foreground: 24 9.8% 10%;
52 |
53 | --secondary: 12 6.5% 15.1%;
54 | --secondary-foreground: 60 9.1% 97.8%;
55 |
56 | --muted: 12 6.5% 15.1%;
57 | --muted-foreground: 24 5.4% 63.9%;
58 |
59 | --accent: 12 6.5% 15.1%;
60 | --accent-foreground: 60 9.1% 97.8%;
61 |
62 | --destructive: 0 62.8% 30.6%;
63 | --destructive-foreground: 60 9.1% 97.8%;
64 |
65 | --border: 12 6.5% 15.1%;
66 | --input: 12 6.5% 15.1%;
67 | --ring: 24 5.7% 82.9%;
68 | }
69 | }
70 |
71 | @layer base {
72 | * {
73 | @apply border-border;
74 |
75 | --reefGreen-100: #e0fddb;
76 | --reefGreen-200: #c0fbb6;
77 | --reefGreen-300: #a1f992;
78 | --reefGreen-400: #81f76d;
79 | --reefGreen-500: #62f549;
80 | --reefGreen-600: #4ec43a;
81 | --reefGreen-700: #3b932c;
82 | --reefGreen-800: #27621d;
83 | --reefGreen-900: #14310f;
84 |
85 | @apply font-oswald;
86 | }
87 | body {
88 | @apply bg-background text-foreground;
89 | }
90 | }
91 |
92 |
93 |
94 | @layer base {
95 | * {
96 | @apply border-border outline-ring/50;
97 | }
98 | body {
99 | @apply bg-background text-foreground;
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/packages/ui/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 |
3 | const sharedConfig = require("tailwind-config");
4 |
5 | module.exports = {
6 | ...sharedConfig,
7 | content: ["./**/*.{js,ts,jsx,tsx,mdx}"],
8 | };
--------------------------------------------------------------------------------
/packages/ui/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tsconfig/react-library.json",
3 | "include": ["."],
4 | "exclude": ["dist", "build", "node_modules"]
5 | }
6 |
--------------------------------------------------------------------------------
/packages/ui/vite.config.ts:
--------------------------------------------------------------------------------
1 | // vite.config.ts
2 |
3 | // This file is intentionally left empty to trick ShadCN into recognizing
4 | // this project as a Vite-based setup. Some tools check for the existence
5 | // of vite.config.ts to determine the build system. Since we're not actually
6 | // using Vite, we don't need any configurations here.
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - 'apps/**'
3 | - 'packages/**'
4 |
--------------------------------------------------------------------------------
/public/launchpad-logo-md.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JadRizk/turborepo-launchpad/cacd6cb5565725e47a593c9fc609c31d137d7f15/public/launchpad-logo-md.png
--------------------------------------------------------------------------------
/supabase/.gitignore:
--------------------------------------------------------------------------------
1 | # Supabase
2 | .branches
3 | .temp
4 | .env
5 |
--------------------------------------------------------------------------------
/supabase/config.toml:
--------------------------------------------------------------------------------
1 | # A string used to distinguish different Supabase projects on the same host. Defaults to the
2 | # working directory name when running `supabase init`.
3 | project_id = "miniature-launchpad"
4 |
5 | [api]
6 | enabled = true
7 | # Port to use for the API URL.
8 | port = 54321
9 | # Schemas to expose in your API. Tables, views and stored procedures in this schema will get API
10 | # endpoints. public and storage are always included.
11 | schemas = ["public", "storage", "graphql_public"]
12 | # Extra schemas to add to the search_path of every request. public is always included.
13 | extra_search_path = ["public", "extensions"]
14 | # The maximum number of rows returns from a view, table, or stored procedure. Limits payload size
15 | # for accidental or malicious requests.
16 | max_rows = 1000
17 |
18 | [db]
19 | # Port to use for the local database URL.
20 | port = 54322
21 | # Port used by db diff command to initialize the shadow database.
22 | shadow_port = 54320
23 | # The database major version to use. This has to be the same as your remote database's. Run `SHOW
24 | # server_version;` on the remote database to check.
25 | major_version = 15
26 |
27 | [db.pooler]
28 | enabled = false
29 | # Port to use for the local connection pooler.
30 | port = 54329
31 | # Specifies when a server connection can be reused by other clients.
32 | # Configure one of the supported pooler modes: `transaction`, `session`.
33 | pool_mode = "transaction"
34 | # How many server connections to allow per user/database pair.
35 | default_pool_size = 20
36 | # Maximum number of client connections allowed.
37 | max_client_conn = 100
38 |
39 | [realtime]
40 | enabled = true
41 | # Bind realtime via either IPv4 or IPv6. (default: IPv6)
42 | # ip_version = "IPv6"
43 | # The maximum length in bytes of HTTP request headers. (default: 4096)
44 | # max_header_length = 4096
45 |
46 | [studio]
47 | enabled = true
48 | # Port to use for Supabase Studio.
49 | port = 54323
50 | # External URL of the API server that frontend connects to.
51 | api_url = "http://127.0.0.1"
52 | # OpenAI API Key to use for Supabase AI in the Supabase Studio.
53 | openai_api_key = "env(OPENAI_API_KEY)"
54 |
55 | # Email testing server. Emails sent with the local dev setup are not actually sent - rather, they
56 | # are monitored, and you can view the emails that would have been sent from the web interface.
57 | [inbucket]
58 | enabled = true
59 | # Port to use for the email testing server web interface.
60 | port = 54324
61 | # Uncomment to expose additional ports for testing user applications that send emails.
62 | # smtp_port = 54325
63 | # pop3_port = 54326
64 |
65 | [storage]
66 | enabled = true
67 | # The maximum file size allowed (e.g. "5MB", "500KB").
68 | file_size_limit = "50MiB"
69 |
70 | [auth]
71 | enabled = true
72 | # The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
73 | # in emails.
74 | site_url = "http://127.0.0.1:3000"
75 | # A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
76 | additional_redirect_urls = ["http://127.0.0.1:3000/**", "http://localhost:3000/auth/*"]
77 | # How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
78 | jwt_expiry = 3600
79 | # If disabled, the refresh token will never expire.
80 | enable_refresh_token_rotation = true
81 | # Allows refresh tokens to be reused after expiry, up to the specified interval in seconds.
82 | # Requires enable_refresh_token_rotation = true.
83 | refresh_token_reuse_interval = 10
84 | # Allow/disallow new user signups to your project.
85 | enable_signup = true
86 | # Allow/disallow testing manual linking of accounts
87 | enable_manual_linking = false
88 |
89 | [auth.email]
90 | # Allow/disallow new user signups via email to your project.
91 | enable_signup = true
92 | # If enabled, a user will be required to confirm any email change on both the old, and new email
93 | # addresses. If disabled, only the new email is required to confirm.
94 | double_confirm_changes = true
95 | # If enabled, users need to confirm their email address before signing in.
96 | enable_confirmations = false
97 |
98 | # Uncomment to customize email template
99 | # [auth.email.template.invite]
100 | # subject = "You have been invited"
101 | # content_path = "./supabase/templates/invite.html"
102 |
103 | [auth.sms]
104 | # Allow/disallow new user signups via SMS to your project.
105 | enable_signup = true
106 | # If enabled, users need to confirm their phone number before signing in.
107 | enable_confirmations = false
108 | # Template for sending OTP to users
109 | template = "Your code is {{ .Code }} ."
110 |
111 | # Use pre-defined map of phone number to OTP for testing.
112 | [auth.sms.test_otp]
113 | # 4152127777 = "123456"
114 |
115 | # This hook runs before a token is issued and allows you to add additional claims based on the authentication method used.
116 | [auth.hook.custom_access_token]
117 | # enabled = true
118 | # uri = "pg-functions:////"
119 |
120 |
121 | # Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`.
122 | [auth.sms.twilio]
123 | enabled = false
124 | account_sid = ""
125 | message_service_sid = ""
126 | # DO NOT commit your Twilio auth token to git. Use environment variable substitution instead:
127 | auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)"
128 |
129 | # Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`,
130 | # `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`,
131 | # `twitter`, `slack`, `spotify`, `workos`, `zoom`.
132 | [auth.external.apple]
133 | enabled = false
134 | client_id = ""
135 | # DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead:
136 | secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)"
137 | # Overrides the default auth redirectUrl.
138 | redirect_uri = ""
139 | # Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure,
140 | # or any other third-party OIDC providers.
141 | url = ""
142 |
143 | [analytics]
144 | enabled = false
145 | port = 54327
146 | vector_port = 54328
147 | # Configure one of the supported backends: `postgres`, `bigquery`.
148 | backend = "postgres"
149 |
150 | # Experimental features may be deprecated any time
151 | [experimental]
152 | # Configures Postgres storage engine to use OrioleDB (S3)
153 | orioledb_version = ""
154 | # Configures S3 bucket URL, eg. .s3-.amazonaws.com
155 | s3_host = "env(S3_HOST)"
156 | # Configures S3 bucket region, eg. us-east-1
157 | s3_region = "env(S3_REGION)"
158 | # Configures AWS_ACCESS_KEY_ID for S3 bucket
159 | s3_access_key = "env(S3_ACCESS_KEY)"
160 | # Configures AWS_SECRET_ACCESS_KEY for S3 bucket
161 | s3_secret_key = "env(S3_SECRET_KEY)"
162 |
--------------------------------------------------------------------------------
/supabase/seed.sql:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JadRizk/turborepo-launchpad/cacd6cb5565725e47a593c9fc609c31d137d7f15/supabase/seed.sql
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tsconfig/base.json"
3 | }
4 |
--------------------------------------------------------------------------------
/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://turbo.build/schema.json",
3 | "globalDependencies": ["**/.env.*local"],
4 | "tasks": {
5 | "build": {
6 | "dependsOn": ["^build"],
7 | "outputs": [".next/**", "!.next/cache/**"]
8 | },
9 | "lint": {},
10 | "dev": {
11 | "cache": false,
12 | "persistent": true
13 | },
14 | "app#build": {
15 | "dependsOn": ["^build"],
16 | "env": ["NEXT_APP_URL", "SUPABASE_URL", "SUPABASE_ANON_KEY"],
17 | "outputs": [".next/**"]
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------