├── .cursor └── rules │ ├── 1up-cursor-rules.mdc │ ├── create-rules.mdc │ ├── database-schema.mdc │ ├── mobile-app-cursor-rules.mdc │ ├── mobile-app-toast.mdc │ ├── mobile-backend-communication.mdc │ └── trpc-backend-procedures.mdc ├── .env.example ├── .github ├── DISCUSSION_TEMPLATE │ └── ideas.yml ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── config.yml ├── renovate.json └── workflows │ └── ci.yml ├── .gitignore ├── .infisical.json ├── .npmrc ├── .nvmrc ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── CLAUDE.md ├── LICENSE ├── README.md ├── apps ├── backend │ ├── .gitignore │ ├── README.md │ ├── api │ │ ├── ai │ │ │ ├── chat.ts │ │ │ └── functions.ts │ │ └── index.ts │ ├── package.json │ ├── public │ │ └── favicon.ico │ ├── tsconfig.json │ ├── tsup.config.ts │ └── vercel.json ├── mobile │ ├── .gitignore │ ├── .prettierignore │ ├── app.config.ts │ ├── assets │ │ ├── app │ │ │ ├── icon-light.png │ │ │ ├── splash-dark.png │ │ │ └── splash-light.png │ │ └── fonts │ │ │ └── Monocraft.ttf │ ├── babel.config.js │ ├── eas.json │ ├── eslint.config.mjs │ ├── index.ts │ ├── ios │ │ ├── .gitignore │ │ ├── .xcode.env │ │ ├── Podfile │ │ ├── Podfile.lock │ │ ├── Podfile.properties.json │ │ ├── Potential.xcodeproj │ │ │ ├── project.pbxproj │ │ │ └── xcshareddata │ │ │ │ └── xcschemes │ │ │ │ └── Potential.xcscheme │ │ ├── Potential.xcworkspace │ │ │ └── contents.xcworkspacedata │ │ └── Potential │ │ │ ├── AppDelegate.swift │ │ │ ├── Images.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── App-Icon-1024x1024@1x.png │ │ │ │ ├── App-Icon-dark-1024x1024@1x.png │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── SplashScreenBackground.colorset │ │ │ │ └── Contents.json │ │ │ └── SplashScreenLogo.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── dark_image.png │ │ │ │ ├── dark_image@2x.png │ │ │ │ ├── dark_image@3x.png │ │ │ │ ├── image.png │ │ │ │ ├── image@2x.png │ │ │ │ └── image@3x.png │ │ │ ├── Info.plist │ │ │ ├── Potential-Bridging-Header.h │ │ │ ├── Potential.entitlements │ │ │ ├── PrivacyInfo.xcprivacy │ │ │ ├── SplashScreen.storyboard │ │ │ └── Supporting │ │ │ └── Expo.plist │ ├── metro.config.js │ ├── nativewind-env.d.ts │ ├── package.json │ ├── phosphor-react-native.d.ts │ ├── polyfills.ts │ ├── prebuild.js │ ├── src │ │ ├── app │ │ │ ├── (home) │ │ │ │ ├── [trackableId].tsx │ │ │ │ ├── _layout.tsx │ │ │ │ ├── delete.tsx │ │ │ │ ├── experiment.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── log.tsx │ │ │ │ ├── new.tsx │ │ │ │ ├── profile.tsx │ │ │ │ ├── settings.tsx │ │ │ │ ├── stats.tsx │ │ │ │ └── updates.tsx │ │ │ ├── _layout.tsx │ │ │ ├── index.tsx │ │ │ ├── login.tsx │ │ │ ├── otp.tsx │ │ │ └── post-login-redirect.tsx │ │ ├── components │ │ │ ├── app │ │ │ │ ├── audioRecorder.tsx │ │ │ │ ├── camera.tsx │ │ │ │ ├── experimentsDemo.tsx │ │ │ │ ├── logStepWrapper.tsx │ │ │ │ ├── trackableLogStep.tsx │ │ │ │ ├── trackablesHeader.tsx │ │ │ │ └── userHeader.tsx │ │ │ ├── branding │ │ │ │ ├── heart-sprite.tsx │ │ │ │ └── logo.tsx │ │ │ ├── loading.tsx │ │ │ ├── trackables │ │ │ │ ├── TemplateConfigForm.tsx │ │ │ │ ├── TemplateList.tsx │ │ │ │ ├── TrackableCard.tsx │ │ │ │ ├── TrackableSection.tsx │ │ │ │ ├── TrackablesContainer.tsx │ │ │ │ ├── displays │ │ │ │ │ ├── CheckboxDisplay.tsx │ │ │ │ │ ├── MeasureDisplay.tsx │ │ │ │ │ ├── RangeDisplay.tsx │ │ │ │ │ ├── RatingDisplay.tsx │ │ │ │ │ ├── TextDisplay.tsx │ │ │ │ │ └── index.ts │ │ │ │ ├── inputs │ │ │ │ │ ├── CheckboxInput.tsx │ │ │ │ │ ├── MeasureInput.tsx │ │ │ │ │ ├── RangeInput.tsx │ │ │ │ │ ├── RatingInput.tsx │ │ │ │ │ ├── TextInput.tsx │ │ │ │ │ └── index.ts │ │ │ │ └── new.tsx │ │ │ └── ui │ │ │ │ ├── button.tsx │ │ │ │ ├── card.tsx │ │ │ │ ├── checkbox.tsx │ │ │ │ ├── dropdown.tsx │ │ │ │ ├── image-picker-uploader.tsx │ │ │ │ ├── image-upload.tsx │ │ │ │ ├── image-view.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── long-text.tsx │ │ │ │ ├── number-input.tsx │ │ │ │ ├── otp.tsx │ │ │ │ ├── picker.tsx │ │ │ │ ├── rating-display.tsx │ │ │ │ ├── rating.tsx │ │ │ │ ├── request-permission.tsx │ │ │ │ ├── slider.tsx │ │ │ │ ├── text.tsx │ │ │ │ ├── textarea.tsx │ │ │ │ └── touchable-opacity.tsx │ │ ├── hooks │ │ │ └── usePushNotificationSync.ts │ │ ├── lib │ │ │ └── hooks │ │ │ │ └── usePermission.ts │ │ ├── styles.css │ │ ├── types │ │ │ └── trackables.ts │ │ └── utils │ │ │ ├── api.ts │ │ │ ├── auth-client.ts │ │ │ ├── base-url.ts │ │ │ ├── date.ts │ │ │ ├── images │ │ │ ├── image-processing.ts │ │ │ └── image-upload.ts │ │ │ └── ui.ts │ ├── tailwind.config.ts │ ├── tsconfig.json │ └── turbo.json └── web │ ├── README.md │ ├── components.json │ ├── eslint.config.js │ ├── next.config.js │ ├── package.json │ ├── postcss.config.cjs │ ├── public │ ├── images │ │ ├── branding │ │ │ ├── heart │ │ │ │ └── sprite │ │ │ │ │ ├── 1-1.svg │ │ │ │ │ ├── 1-10.svg │ │ │ │ │ ├── 1-11.svg │ │ │ │ │ ├── 1-12.svg │ │ │ │ │ ├── 1-13.svg │ │ │ │ │ ├── 1-14.svg │ │ │ │ │ ├── 1-15.svg │ │ │ │ │ ├── 1-16.svg │ │ │ │ │ ├── 1-17.svg │ │ │ │ │ ├── 1-18.svg │ │ │ │ │ ├── 1-2.svg │ │ │ │ │ ├── 1-3.svg │ │ │ │ │ ├── 1-4.svg │ │ │ │ │ ├── 1-5.svg │ │ │ │ │ ├── 1-6.svg │ │ │ │ │ ├── 1-7.svg │ │ │ │ │ ├── 1-8.svg │ │ │ │ │ └── 1-9.svg │ │ │ ├── logo-1up-dark.svg │ │ │ └── logo-ph-dark.svg │ │ └── home │ │ │ ├── flag_icon.png │ │ │ ├── flask_icon.png │ │ │ ├── growth_icon.png │ │ │ ├── omar.jpg │ │ │ └── pulse_icon.png │ └── pitch.pdf │ ├── src │ ├── app │ │ ├── _components │ │ │ └── branding │ │ │ │ ├── heart-sprite.tsx │ │ │ │ └── logo.tsx │ │ ├── actions │ │ │ └── subscribe.ts │ │ ├── apple-icon.png │ │ ├── components │ │ │ ├── button.tsx │ │ │ ├── dialog.tsx │ │ │ ├── divider.tsx │ │ │ ├── email-signup-dialog.tsx │ │ │ ├── input.tsx │ │ │ ├── pulsingUnderline.tsx │ │ │ ├── theme.tsx │ │ │ └── toast.tsx │ │ ├── favicon.ico │ │ ├── globals.css │ │ ├── icon.png │ │ ├── layout.tsx │ │ ├── not-found.tsx │ │ ├── page.tsx │ │ ├── refund │ │ │ └── page.tsx │ │ ├── start │ │ │ └── page.tsx │ │ ├── success │ │ │ └── page.tsx │ │ ├── terms │ │ │ └── page.tsx │ │ └── ui │ │ │ └── NavMenu.tsx │ ├── env.ts │ └── lib │ │ └── utils.ts │ ├── tailwind.config.ts │ ├── tsconfig.json │ └── turbo.json ├── docs └── image-storage.md ├── package.json ├── packages ├── ai │ ├── eslint.config.js │ ├── package.json │ ├── src │ │ ├── calls │ │ │ ├── index.ts │ │ │ └── newTrackableChat.ts │ │ ├── functions │ │ │ ├── createConsumptionTrackables.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── prompts.ts │ │ └── tools │ │ │ ├── generateConsumptionTrackables.ts │ │ │ ├── generateNewNonConsumptionTrackable.ts │ │ │ ├── getUserTrackables.ts │ │ │ └── index.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── auth │ ├── eslint.config.js │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── options.ts │ │ └── validator.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── consts │ ├── eslint.config.js │ ├── package.json │ ├── src │ │ ├── auth.ts │ │ ├── base.ts │ │ ├── colors.ts │ │ ├── index.ts │ │ ├── integrations.ts │ │ ├── prompts.ts │ │ ├── trackables.ts │ │ └── users.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── db │ ├── drizzle.config.ts │ ├── eslint.config.js │ ├── package.json │ ├── src │ │ ├── client.ts │ │ ├── columns │ │ │ ├── custom │ │ │ │ ├── color.ts │ │ │ │ ├── integration.ts │ │ │ │ ├── trackable.ts │ │ │ │ ├── typeId.ts │ │ │ │ └── user.ts │ │ │ └── timestamps.ts │ │ ├── index.ts │ │ ├── schema.ts │ │ └── schema │ │ │ ├── aiLogs.ts │ │ │ ├── auth.ts │ │ │ ├── ingredients.ts │ │ │ ├── integrations.ts │ │ │ ├── logs.ts │ │ │ └── users.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── email │ ├── eslint.config.js │ ├── package.json │ ├── src │ │ ├── emails │ │ │ ├── auth-otp.tsx │ │ │ └── bug-report.tsx │ │ └── index.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── env │ ├── eslint.config.js │ ├── package.json │ ├── src │ │ ├── client.ts │ │ ├── index.ts │ │ └── server.ts │ └── tsconfig.json ├── local-docker │ ├── docker-compose.yml │ └── package.json ├── storage │ ├── eslint.config.js │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── templates │ ├── README.md │ ├── eslint.config.js │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── templates │ │ │ ├── body.ts │ │ │ ├── consumption.ts │ │ │ ├── index.ts │ │ │ ├── mind.ts │ │ │ └── templates-registry.ts │ │ ├── types.ts │ │ ├── types │ │ │ └── templates.ts │ │ ├── upgrades.ts │ │ └── utils.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── trpc │ ├── eslint.config.js │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── root.ts │ │ ├── router │ │ │ ├── log.ts │ │ │ ├── storage.ts │ │ │ ├── trackables.ts │ │ │ └── user │ │ │ │ ├── account.ts │ │ │ │ ├── index.ts │ │ │ │ └── profile.ts │ │ ├── trpc.ts │ │ └── utils │ │ │ ├── ai │ │ │ └── generateNewTrackables.ts │ │ │ └── xpPoints.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── tsconfig │ ├── base.json │ ├── internal-package.json │ └── package.json ├── ui │ ├── components.json │ ├── eslint.config.js │ ├── package.json │ ├── src │ │ ├── button.tsx │ │ ├── dropdown-menu.tsx │ │ ├── form.tsx │ │ ├── index.ts │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── theme.tsx │ │ └── toast.tsx │ ├── tsconfig.json │ └── tsup.config.ts ├── utils │ ├── eslint.config.js │ ├── package.json │ ├── src │ │ ├── convert.ts │ │ ├── index.ts │ │ ├── ms.ts │ │ └── typeid.ts │ ├── tsconfig.json │ └── tsup.config.ts └── validators │ ├── eslint.config.js │ ├── package.json │ ├── src │ └── index.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── patches └── better-auth@1.2.5.patch ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── tooling ├── eslint │ ├── base.js │ ├── nextjs.js │ ├── owneridcolumn.js │ ├── package.json │ ├── react.js │ ├── rn.js │ ├── tsconfig.json │ ├── typeid.js │ └── types.d.ts ├── github │ ├── package.json │ └── setup │ │ └── action.yml ├── prettier │ ├── index.js │ ├── package.json │ └── tsconfig.json └── tailwind │ ├── base.ts │ ├── eslint.config.js │ ├── native.ts │ ├── package.json │ ├── tsconfig.json │ └── web.ts ├── turbo.json └── turbo └── generators ├── config.ts └── templates ├── eslint.config.js.hbs ├── package.json.hbs └── tsconfig.json.hbs /.cursor/rules/create-rules.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: 4 | alwaysApply: false 5 | --- 6 | # Cursor Rules Location 7 | 8 | How to add new cursor rules to the project 9 | 10 | 1. Always place rule files in PROJECT_ROOT/.cursor/rules/: 11 | ``` 12 | .cursor/rules/ 13 | ├── your-rule-name.mdc 14 | ├── another-rule.mdc 15 | └── ... 16 | ``` 17 | 18 | 2. Follow the naming convention: 19 | - Use kebab-case for filenames 20 | - Always use .mdc extension 21 | - Make names descriptive of the rule's purpose 22 | 23 | 3. Directory structure: 24 | ``` 25 | PROJECT_ROOT/ 26 | ├── .cursor/ 27 | │ └── rules/ 28 | │ ├── your-rule-name.mdc 29 | │ └── ... 30 | └── ... 31 | ``` 32 | 33 | 4. Never place rule files: 34 | - In the project root 35 | - In subdirectories outside .cursor/rules 36 | - In any other location 37 | 38 | 5. Cursor rules have the following structure: 39 | 40 | ``` 41 | --- 42 | description: Short description of the rule's purpose 43 | globs: optional/path/pattern/**/* 44 | alwaysApply: false 45 | --- 46 | # Rule Title 47 | 48 | Main content explaining the rule with markdown formatting. 49 | 50 | 1. Step-by-step instructions 51 | 2. Code examples 52 | 3. Guidelines 53 | 54 | Example: 55 | ```typescript 56 | // Good example 57 | function goodExample() { 58 | // Implementation following guidelines 59 | } 60 | 61 | // Bad example 62 | function badExample() { 63 | // Implementation not following guidelines 64 | } 65 | ``` -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # General 2 | BASE_URL=https://localhost 3 | NODE_ENV=development 4 | 5 | # Mobile App 6 | 7 | # Web App 8 | NEXT_PUBLIC_URL=http://localhost:3000 9 | 10 | # Backend 11 | BACKEND_URL=http://localhost:3100 12 | EXPO_PUBLIC_BACKEND_URL= 13 | NEXT_PUBLIC_BACKEND_URL=${BACKEND_URL} 14 | # generate using openssl rand -hex 32 15 | AUTH_SECRET=secretsecretsecret 16 | 17 | 18 | 19 | # Db 20 | DB_HOST=localhost:3900 21 | DB_USERNAME=root 22 | DB_PASSWORD=planetscale 23 | DB_DATABASE=planetscale 24 | DB_MIGRATION_URL="mysql://root:@localhost:3306/planetscale" 25 | 26 | # S3 27 | 28 | STORAGE_S3_BUCKET_UPLOADS=uploads 29 | STORAGE_S3_BUCKET_AVATARS=avatars 30 | STORAGE_S3_REGION=us-east-1 31 | STORAGE_S3_ENDPOINT= 32 | STORAGE_S3_ACCESS_KEY_ID=minioroot 33 | STORAGE_S3_SECRET_ACCESS_KEY=minioroot 34 | -------------------------------------------------------------------------------- /.github/DISCUSSION_TEMPLATE/ideas.yml: -------------------------------------------------------------------------------- 1 | body: 2 | - type: markdown 3 | attributes: 4 | value: | 5 | Thank you for taking the time to file a feature request. Please fill out this form as completely as possible. 6 | - type: textarea 7 | attributes: 8 | label: Describe the feature you'd like to request 9 | description: Please describe the feature as clear and concise as possible. Remember to add context as to why you believe this feature is needed. 10 | validations: 11 | required: true 12 | - type: textarea 13 | attributes: 14 | label: Describe the solution you'd like to see 15 | description: Please describe the solution you would like to see. Adding example usage is a good way to provide context. 16 | validations: 17 | required: true 18 | - type: textarea 19 | attributes: 20 | label: Additional information 21 | description: Add any other information related to the feature here. If your feature request is related to any issues or discussions, link them here. 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: 🐞 Bug Report 2 | description: Create a bug report to help us improve 3 | title: "bug: " 4 | labels: ["🐞❔ unconfirmed bug"] 5 | body: 6 | - type: textarea 7 | attributes: 8 | label: Provide environment information 9 | description: | 10 | Run this command in your project root and paste the results in a code block: 11 | ```bash 12 | npx envinfo --system --binaries 13 | ``` 14 | validations: 15 | required: true 16 | - type: textarea 17 | attributes: 18 | label: Describe the bug 19 | description: A clear and concise description of the bug, as well as what you expected to happen when encountering it. 20 | validations: 21 | required: true 22 | - type: input 23 | attributes: 24 | label: Link to reproduction 25 | description: Please provide a link to a reproduction of the bug. Issues without a reproduction repo may be ignored. 26 | validations: 27 | required: true 28 | - type: textarea 29 | attributes: 30 | label: To reproduce 31 | description: Describe how to reproduce your bug. Steps, code snippets, reproduction repos etc. 32 | validations: 33 | required: true 34 | - type: textarea 35 | attributes: 36 | label: Additional information 37 | description: Add any other information related to the bug here, screenshots if applicable. 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: Ask a question 3 | url: https://github.com/t3-oss/create-t3-turbo/discussions 4 | about: Ask questions and discuss with other community members 5 | - name: Feature request 6 | url: https://github.com/t3-oss/create-t3-turbo/discussions/new?category=ideas 7 | about: Feature requests should be opened as discussions 8 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ], 6 | "packageRules": [ 7 | { 8 | "matchPackagePatterns": [ 9 | "^@potential/" 10 | ], 11 | "enabled": false 12 | } 13 | ], 14 | "updateInternalDeps": true, 15 | "rangeStrategy": "bump", 16 | "automerge": true, 17 | "npm": { 18 | "fileMatch": [ 19 | "(^|/)package\\.json$", 20 | "(^|/)package\\.json\\.hbs$" 21 | ] 22 | } 23 | } -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: ["*"] 6 | push: 7 | branches: ["main"] 8 | merge_group: 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} 13 | 14 | # You can leverage Vercel Remote Caching with Turbo to speed up your builds 15 | # @link https://turborepo.org/docs/core-concepts/remote-caching#remote-caching-on-vercel-builds 16 | env: 17 | FORCE_COLOR: 3 18 | TURBO_TEAM: ${{ vars.TURBO_TEAM }} 19 | TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} 20 | 21 | jobs: 22 | lint: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v4 26 | 27 | - name: Setup 28 | uses: ./tooling/github/setup 29 | 30 | - name: Copy env 31 | shell: bash 32 | run: cp .env.example .env 33 | 34 | - name: Lint 35 | run: pnpm lint && pnpm lint:ws 36 | 37 | format: 38 | runs-on: ubuntu-latest 39 | steps: 40 | - uses: actions/checkout@v4 41 | 42 | - name: Setup 43 | uses: ./tooling/github/setup 44 | 45 | - name: Format 46 | run: pnpm format 47 | 48 | typecheck: 49 | runs-on: ubuntu-latest 50 | steps: 51 | - uses: actions/checkout@v4 52 | 53 | - name: Setup 54 | uses: ./tooling/github/setup 55 | 56 | - name: Typecheck 57 | run: pnpm typecheck 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # testing 9 | coverage 10 | 11 | # next.js 12 | .next/ 13 | out/ 14 | next-env.d.ts 15 | 16 | # nitro 17 | .nitro/ 18 | .output/ 19 | 20 | # expo 21 | .expo/ 22 | expo-env.d.ts 23 | apps/expo/.gitignore 24 | apps/expo/ios 25 | apps/expo/android 26 | *.ipa 27 | 28 | # production 29 | build 30 | 31 | # misc 32 | .DS_Store 33 | *.pem 34 | 35 | # debug 36 | npm-debug.log* 37 | yarn-debug.log* 38 | yarn-error.log* 39 | .pnpm-debug.log* 40 | 41 | # local env files 42 | .env 43 | .env*.local 44 | 45 | # vercel 46 | .vercel 47 | 48 | # typescript 49 | dist/ 50 | .cache 51 | tsup.config.bundled_*.* 52 | 53 | # turbo 54 | .turbo 55 | -------------------------------------------------------------------------------- /.infisical.json: -------------------------------------------------------------------------------- 1 | { 2 | "workspaceId": "eae89b9a-6468-4687-a5e9-53d7502160ef", 3 | "defaultEnvironment": "", 4 | "gitBranchToEnvironmentMapping": null 5 | } -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | node-linker=hoisted 2 | link-workspace-packages=true 3 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 22.12 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "expo.vscode-expo-tools", 5 | "esbenp.prettier-vscode", 6 | "yoavbls.pretty-ts-errors", 7 | "bradlc.vscode-tailwindcss" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Next.js", 6 | "type": "node-terminal", 7 | "request": "launch", 8 | "command": "pnpm dev", 9 | "cwd": "${workspaceFolder}/apps/nextjs", 10 | "skipFiles": ["/**"], 11 | "sourceMaps": true, 12 | "sourceMapPathOverrides": { 13 | "/turbopack/[project]/*": "${webRoot}/*" //https://github.com/vercel/next.js/issues/62008 14 | } 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.workingDirectories": [ 3 | { 4 | "mode": "auto" 5 | } 6 | ], 7 | "editor.formatOnSave": true, 8 | "editor.codeActionsOnSave": { 9 | "source.fixAll": "explicit", 10 | "source.organizeImports": "explicit" 11 | }, 12 | 13 | "eslint.useFlatConfig": true, 14 | "cSpell.words": ["mediumint", "monocraft", "Pressable", "trackables"] 15 | } 16 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # CLAUDE.md - PotentialHealth Project Guidelines 2 | 3 | ## Build, Lint, Test Commands 4 | 5 | - **Build:** `pnpm build` (builds all packages) 6 | - **Lint:** `pnpm lint` or `pnpm lint:fix` (add `--fix` to auto-fix issues) 7 | - **Typecheck:** `pnpm typecheck` (runs TypeScript type checking) 8 | - **Dev:** `pnpm dev` (start development servers) 9 | - **iOS:** `pnpm ios` (run mobile app on iOS simulator) 10 | 11 | ## Code Style Guidelines 12 | 13 | - Use **functional and declarative programming** patterns; avoid classes 14 | - Use **TypeScript** with proper interfaces and types 15 | - Use **Zod** for schema validation and type inference 16 | - Use lowercase with dashes for directory names (e.g., `components/auth-wizard`) 17 | - Favor **named exports** for components and functions 18 | - Structure files: exported component, subcomponents, helpers, types 19 | - Use descriptive variable names with auxiliary verbs (e.g., isLoading, hasError) 20 | - Implement guard clauses to handle error conditions early 21 | - Use **early returns** for error conditions 22 | - Backend: All queries should use **tRPC** and **Drizzle ORM** 23 | - All data exchanged with the backend should be validated using Zod schemas 24 | - UI: Use Tailwind CSS, Shadcn UI (web), Nativewind (mobile) 25 | - Minimize `useEffect` and `setState`, favor React Server Components when possible 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Potential Health 2 | 3 | Your personal Ai Health Coach 4 | Helping you live Longer, Sharper and Better 5 | 6 | We are currently focusing on our iOS Build 7 | 8 | to run in dev mode: 9 | 10 | 1. Ensure you have XCode installed on your system with a version of iPhone IOS downloaded 11 | 2. run `pnpm install` in the root of the repo 12 | 3. go into `apps/mobile` 13 | 4. run `npx pod-install` 14 | 5. run `pnpm ios` 15 | 6. this will take a while to build the app on your machine, and should start an iOS simulator after 16 | 17 | Join the discord for discussions 18 | 19 | [Join the waitlist](https://potentialhealth.io) 20 | [Follow on X](https://x.com/potential_health) 21 | [Chat on Discord](https://discord.gg/tKmSyU8GBT) 22 | -------------------------------------------------------------------------------- /apps/backend/.gitignore: -------------------------------------------------------------------------------- 1 | # dev 2 | .vercel/ 3 | .yarn/ 4 | !.yarn/releases 5 | .vscode/* 6 | !.vscode/launch.json 7 | !.vscode/*.code-snippets 8 | .idea/workspace.xml 9 | .idea/usage.statistics.xml 10 | .idea/shelf 11 | 12 | # deps 13 | node_modules/ 14 | 15 | # env 16 | .env 17 | .env.production 18 | 19 | # logs 20 | logs/ 21 | *.log 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | pnpm-debug.log* 26 | lerna-debug.log* 27 | 28 | # misc 29 | .DS_Store 30 | -------------------------------------------------------------------------------- /apps/backend/README.md: -------------------------------------------------------------------------------- 1 | Backend 2 | -------------------------------------------------------------------------------- /apps/backend/api/ai/chat.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "hono"; 2 | import { createDataStreamResponse } from "ai"; 3 | import { Hono } from "hono"; 4 | 5 | import type { CloudTypeId } from "@potential/utils"; 6 | import { createNewTrackableChatStream } from "@potential/ai"; 7 | 8 | import type { AppContext } from "../index"; 9 | 10 | const ai = new Hono(); 11 | 12 | async function saveToDatabase({ 13 | content, 14 | chatId, 15 | }: { 16 | content: string; 17 | chatId: CloudTypeId<"chat"> | undefined; 18 | }) { 19 | console.log("Saving to database:", content); 20 | 21 | await new Promise((resolve) => setTimeout(resolve, 100)); 22 | console.log("Saved to database."); 23 | } 24 | 25 | ai.post("/chat", async (c: Context) => { 26 | const { messages, chatId } = await c.req.json(); 27 | const { user } = c.get("auth"); 28 | const db = c.get("db"); 29 | 30 | return createDataStreamResponse({ 31 | execute: (dataStream) => { 32 | let accumulatedResponse = ""; 33 | 34 | const result = createNewTrackableChatStream({ 35 | messages, 36 | userId: user!.id, 37 | chatId, 38 | }); 39 | 40 | result.mergeIntoDataStream(dataStream); 41 | }, 42 | onError: (error) => { 43 | console.error("Error during AI stream:", error); 44 | // Error messages are masked by default for security reasons. 45 | // If you want to expose the error message to the client, you can do so here: 46 | return error instanceof Error ? error.message : String(error); 47 | }, 48 | }); 49 | }); 50 | 51 | export default ai; 52 | -------------------------------------------------------------------------------- /apps/backend/api/ai/functions.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/un/potential/7789ad8ca77a5c2960c0a7ca6c4c452cf95d6be0/apps/backend/api/ai/functions.ts -------------------------------------------------------------------------------- /apps/backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@potential/backend", 3 | "type": "module", 4 | "scripts": { 5 | "dev": "NODE_ENV=development ENVIRONMENT=development pnpm with-env tsx watch api/index.ts --clear-screen=false", 6 | "build": "tsup", 7 | "start": "node .output/index.js", 8 | "start:local": "pnpm with-env node .output/index.js", 9 | "lint": "eslint .", 10 | "typecheck": "tsc --noEmit", 11 | "with-env": "dotenv -e ../../.env --" 12 | }, 13 | "dependencies": { 14 | "@ai-sdk/openai": "catalog:", 15 | "@hono/node-server": "^1.14.1", 16 | "@hono/trpc-server": "^0.3.4", 17 | "@hono/zod-validator": "^0.5.0", 18 | "@potential/ai": "workspace:*", 19 | "@potential/auth": "workspace:*", 20 | "@potential/consts": "workspace:*", 21 | "@potential/db": "workspace:*", 22 | "@potential/env": "workspace:*", 23 | "@potential/storage": "workspace:*", 24 | "@potential/templates": "workspace:*", 25 | "@potential/trpc": "workspace:*", 26 | "@potential/utils": "workspace:*", 27 | "@react-email/components": "^0.0.38", 28 | "ai": "catalog:", 29 | "better-auth": "catalog:", 30 | "hono": "^4.7.8", 31 | "nanoid": "^5.1.5", 32 | "react": "catalog:react19", 33 | "react-dom": "catalog:react19", 34 | "resend": "^4.5.0", 35 | "zod": "catalog:" 36 | }, 37 | "devDependencies": { 38 | "@potential/eslint-config": "workspace:*", 39 | "@potential/tsconfig": "workspace:*", 40 | "tsup": "^8.2.4" 41 | } 42 | } -------------------------------------------------------------------------------- /apps/backend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/un/potential/7789ad8ca77a5c2960c0a7ca6c4c452cf95d6be0/apps/backend/public/favicon.ico -------------------------------------------------------------------------------- /apps/backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "@potential/tsconfig/base.json", 4 | "compilerOptions": { 5 | "jsx": "preserve", 6 | "baseUrl": ".", 7 | "paths": { 8 | "~/*": ["./src/*"] 9 | }, 10 | "noUncheckedIndexedAccess": true, 11 | "skipDefaultLibCheck": true, 12 | "skipLibCheck": true, 13 | "verbatimModuleSyntax": true, 14 | "lib": ["ESNext"], 15 | "moduleResolution": "Bundler", 16 | "alwaysStrict": true, 17 | "strict": true, 18 | "module": "ESNext", 19 | "target": "ESNext" 20 | }, 21 | "include": ["api/**/*", ".next/types/**/*.ts"], 22 | "exclude": ["node_modules", "dist"] 23 | } 24 | -------------------------------------------------------------------------------- /apps/backend/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | entry: ["api/index.ts"], 5 | outDir: ".output", 6 | format: "esm", 7 | target: "esnext", 8 | clean: true, 9 | bundle: true, 10 | treeshake: true, 11 | noExternal: [/^@potential\/.*/], 12 | minify: false, 13 | keepNames: true, 14 | banner: { 15 | js: [ 16 | `import { createRequire } from 'module';`, 17 | `const require = createRequire(import.meta.url);`, 18 | ].join("\n"), 19 | }, 20 | esbuildOptions: (options) => { 21 | options.legalComments = "none"; 22 | }, 23 | }); 24 | -------------------------------------------------------------------------------- /apps/backend/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "functions": { 3 | "api/index.ts": { 4 | "includeFiles": "../../../packages/**" 5 | } 6 | }, 7 | "rewrites": [ 8 | { 9 | "source": "/api/(.*)", 10 | "destination": "/api/index.ts" 11 | }, 12 | { 13 | "source": "/api", 14 | "destination": "/api/index.ts" 15 | } 16 | ], 17 | "redirects": [ 18 | { 19 | "source": "/", 20 | "destination": "/api", 21 | "permanent": false 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /apps/mobile/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb 3 | # The following patterns were generated by expo-cli 4 | 5 | expo-env.d.ts 6 | # @end expo-cli 7 | 8 | *.ipa 9 | *.apk 10 | -------------------------------------------------------------------------------- /apps/mobile/.prettierignore: -------------------------------------------------------------------------------- 1 | nativewind-env.d.ts 2 | -------------------------------------------------------------------------------- /apps/mobile/assets/app/icon-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/un/potential/7789ad8ca77a5c2960c0a7ca6c4c452cf95d6be0/apps/mobile/assets/app/icon-light.png -------------------------------------------------------------------------------- /apps/mobile/assets/app/splash-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/un/potential/7789ad8ca77a5c2960c0a7ca6c4c452cf95d6be0/apps/mobile/assets/app/splash-dark.png -------------------------------------------------------------------------------- /apps/mobile/assets/app/splash-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/un/potential/7789ad8ca77a5c2960c0a7ca6c4c452cf95d6be0/apps/mobile/assets/app/splash-light.png -------------------------------------------------------------------------------- /apps/mobile/assets/fonts/Monocraft.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/un/potential/7789ad8ca77a5c2960c0a7ca6c4c452cf95d6be0/apps/mobile/assets/fonts/Monocraft.ttf -------------------------------------------------------------------------------- /apps/mobile/babel.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import("@babel/core").ConfigFunction} */ 2 | module.exports = (api) => { 3 | api.cache(true); 4 | return { 5 | presets: [ 6 | [ 7 | "babel-preset-expo", 8 | { jsxImportSource: "nativewind", runtime: "automatic" }, 9 | ], 10 | "nativewind/babel", 11 | ], 12 | plugins: ["react-native-reanimated/plugin"], 13 | }; 14 | }; 15 | -------------------------------------------------------------------------------- /apps/mobile/eas.json: -------------------------------------------------------------------------------- 1 | { 2 | "cli": { 3 | "version": ">= 4.1.2", 4 | "appVersionSource": "remote", 5 | "requireCommit": true 6 | }, 7 | "build": { 8 | "base": { 9 | "node": "22.14.0", 10 | "pnpm": "9.15.4", 11 | "ios": { 12 | "resourceClass": "m-medium" 13 | }, 14 | "channel": "base" 15 | // "prebuildCommand": "pnpm prebuild &&" 16 | }, 17 | "development": { 18 | "extends": "base", 19 | "developmentClient": true, 20 | "distribution": "internal", 21 | "channel": "development", 22 | "env": { 23 | "APP_VARIANT": "development" 24 | } 25 | }, 26 | "preview": { 27 | "extends": "base", 28 | "distribution": "internal", 29 | "env": { 30 | "APP_VARIANT": "development" 31 | }, 32 | "ios": { 33 | "simulator": true 34 | }, 35 | "channel": "preview" 36 | }, 37 | "prod-dev": { 38 | "extends": "base", 39 | "channel": "production", 40 | "env": { 41 | "APP_VARIANT": "development" 42 | } 43 | }, 44 | "production": { 45 | "extends": "base", 46 | "channel": "production" 47 | } 48 | }, 49 | "submit": { 50 | "production": { 51 | "ios": { 52 | "companyName": "Unproprietary Corporation" 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /apps/mobile/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import baseConfig from "@potential/eslint-config/base"; 2 | import reactConfig from "@potential/eslint-config/react"; 3 | import rnConfig from "@potential/eslint-config/rn"; 4 | 5 | /** @type {import('typescript-eslint').Config} */ 6 | export default [ 7 | { 8 | ignores: [".expo/**", "expo-plugins/**"], 9 | }, 10 | ...baseConfig, 11 | ...reactConfig, 12 | ...rnConfig, 13 | ]; 14 | -------------------------------------------------------------------------------- /apps/mobile/index.ts: -------------------------------------------------------------------------------- 1 | import "expo-router/entry"; 2 | -------------------------------------------------------------------------------- /apps/mobile/ios/.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | project.xcworkspace 24 | .xcode.env.local 25 | 26 | # Bundle artifacts 27 | *.jsbundle 28 | 29 | # CocoaPods 30 | /Pods/ 31 | -------------------------------------------------------------------------------- /apps/mobile/ios/.xcode.env: -------------------------------------------------------------------------------- 1 | # This `.xcode.env` file is versioned and is used to source the environment 2 | # used when running script phases inside Xcode. 3 | # To customize your local environment, you can create an `.xcode.env.local` 4 | # file that is not versioned. 5 | 6 | # NODE_BINARY variable contains the PATH to the node executable. 7 | # 8 | # Customize the NODE_BINARY variable here. 9 | # For example, to use nvm with brew, add the following line 10 | # . "$(brew --prefix nvm)/nvm.sh" --no-use 11 | export NODE_BINARY=$(command -v node) 12 | -------------------------------------------------------------------------------- /apps/mobile/ios/Podfile.properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo.jsEngine": "hermes", 3 | "EX_DEV_CLIENT_NETWORK_INSPECTOR": "true", 4 | "newArchEnabled": "true" 5 | } 6 | -------------------------------------------------------------------------------- /apps/mobile/ios/Potential.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /apps/mobile/ios/Potential/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/un/potential/7789ad8ca77a5c2960c0a7ca6c4c452cf95d6be0/apps/mobile/ios/Potential/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png -------------------------------------------------------------------------------- /apps/mobile/ios/Potential/Images.xcassets/AppIcon.appiconset/App-Icon-dark-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/un/potential/7789ad8ca77a5c2960c0a7ca6c4c452cf95d6be0/apps/mobile/ios/Potential/Images.xcassets/AppIcon.appiconset/App-Icon-dark-1024x1024@1x.png -------------------------------------------------------------------------------- /apps/mobile/ios/Potential/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "filename": "App-Icon-1024x1024@1x.png", 5 | "idiom": "universal", 6 | "platform": "ios", 7 | "size": "1024x1024" 8 | }, 9 | { 10 | "filename": "App-Icon-dark-1024x1024@1x.png", 11 | "idiom": "universal", 12 | "platform": "ios", 13 | "size": "1024x1024", 14 | "appearances": [ 15 | { 16 | "appearance": "luminosity", 17 | "value": "dark" 18 | } 19 | ] 20 | } 21 | ], 22 | "info": { 23 | "version": 1, 24 | "author": "expo" 25 | } 26 | } -------------------------------------------------------------------------------- /apps/mobile/ios/Potential/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "expo" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /apps/mobile/ios/Potential/Images.xcassets/SplashScreenBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors": [ 3 | { 4 | "color": { 5 | "components": { 6 | "alpha": "1.000", 7 | "blue": "0.937254901960784", 8 | "green": "0.941176470588235", 9 | "red": "0.945098039215686" 10 | }, 11 | "color-space": "srgb" 12 | }, 13 | "idiom": "universal" 14 | }, 15 | { 16 | "color": { 17 | "components": { 18 | "alpha": "1.000", 19 | "blue": "0.129411764705882", 20 | "green": "0.133333333333333", 21 | "red": "0.133333333333333" 22 | }, 23 | "color-space": "srgb" 24 | }, 25 | "idiom": "universal", 26 | "appearances": [ 27 | { 28 | "appearance": "luminosity", 29 | "value": "dark" 30 | } 31 | ] 32 | } 33 | ], 34 | "info": { 35 | "version": 1, 36 | "author": "expo" 37 | } 38 | } -------------------------------------------------------------------------------- /apps/mobile/ios/Potential/Images.xcassets/SplashScreenLogo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "idiom": "universal", 5 | "filename": "image.png", 6 | "scale": "1x" 7 | }, 8 | { 9 | "idiom": "universal", 10 | "filename": "image@2x.png", 11 | "scale": "2x" 12 | }, 13 | { 14 | "idiom": "universal", 15 | "filename": "image@3x.png", 16 | "scale": "3x" 17 | }, 18 | { 19 | "idiom": "universal", 20 | "appearances": [ 21 | { 22 | "appearance": "luminosity", 23 | "value": "dark" 24 | } 25 | ], 26 | "scale": "1x", 27 | "filename": "dark_image.png" 28 | }, 29 | { 30 | "idiom": "universal", 31 | "appearances": [ 32 | { 33 | "appearance": "luminosity", 34 | "value": "dark" 35 | } 36 | ], 37 | "scale": "2x", 38 | "filename": "dark_image@2x.png" 39 | }, 40 | { 41 | "idiom": "universal", 42 | "appearances": [ 43 | { 44 | "appearance": "luminosity", 45 | "value": "dark" 46 | } 47 | ], 48 | "scale": "3x", 49 | "filename": "dark_image@3x.png" 50 | } 51 | ], 52 | "info": { 53 | "version": 1, 54 | "author": "expo" 55 | } 56 | } -------------------------------------------------------------------------------- /apps/mobile/ios/Potential/Images.xcassets/SplashScreenLogo.imageset/dark_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/un/potential/7789ad8ca77a5c2960c0a7ca6c4c452cf95d6be0/apps/mobile/ios/Potential/Images.xcassets/SplashScreenLogo.imageset/dark_image.png -------------------------------------------------------------------------------- /apps/mobile/ios/Potential/Images.xcassets/SplashScreenLogo.imageset/dark_image@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/un/potential/7789ad8ca77a5c2960c0a7ca6c4c452cf95d6be0/apps/mobile/ios/Potential/Images.xcassets/SplashScreenLogo.imageset/dark_image@2x.png -------------------------------------------------------------------------------- /apps/mobile/ios/Potential/Images.xcassets/SplashScreenLogo.imageset/dark_image@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/un/potential/7789ad8ca77a5c2960c0a7ca6c4c452cf95d6be0/apps/mobile/ios/Potential/Images.xcassets/SplashScreenLogo.imageset/dark_image@3x.png -------------------------------------------------------------------------------- /apps/mobile/ios/Potential/Images.xcassets/SplashScreenLogo.imageset/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/un/potential/7789ad8ca77a5c2960c0a7ca6c4c452cf95d6be0/apps/mobile/ios/Potential/Images.xcassets/SplashScreenLogo.imageset/image.png -------------------------------------------------------------------------------- /apps/mobile/ios/Potential/Images.xcassets/SplashScreenLogo.imageset/image@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/un/potential/7789ad8ca77a5c2960c0a7ca6c4c452cf95d6be0/apps/mobile/ios/Potential/Images.xcassets/SplashScreenLogo.imageset/image@2x.png -------------------------------------------------------------------------------- /apps/mobile/ios/Potential/Images.xcassets/SplashScreenLogo.imageset/image@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/un/potential/7789ad8ca77a5c2960c0a7ca6c4c452cf95d6be0/apps/mobile/ios/Potential/Images.xcassets/SplashScreenLogo.imageset/image@3x.png -------------------------------------------------------------------------------- /apps/mobile/ios/Potential/Potential-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | -------------------------------------------------------------------------------- /apps/mobile/ios/Potential/Potential.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /apps/mobile/ios/Potential/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyAccessedAPITypes 6 | 7 | 8 | NSPrivacyAccessedAPIType 9 | NSPrivacyAccessedAPICategoryFileTimestamp 10 | NSPrivacyAccessedAPITypeReasons 11 | 12 | C617.1 13 | 0A2A.1 14 | 3B52.1 15 | 16 | 17 | 18 | NSPrivacyAccessedAPIType 19 | NSPrivacyAccessedAPICategoryUserDefaults 20 | NSPrivacyAccessedAPITypeReasons 21 | 22 | CA92.1 23 | 24 | 25 | 26 | NSPrivacyAccessedAPIType 27 | NSPrivacyAccessedAPICategorySystemBootTime 28 | NSPrivacyAccessedAPITypeReasons 29 | 30 | 35F9.1 31 | 32 | 33 | 34 | NSPrivacyAccessedAPIType 35 | NSPrivacyAccessedAPICategoryDiskSpace 36 | NSPrivacyAccessedAPITypeReasons 37 | 38 | E174.1 39 | 85F4.1 40 | 41 | 42 | 43 | NSPrivacyCollectedDataTypes 44 | 45 | NSPrivacyTracking 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /apps/mobile/ios/Potential/Supporting/Expo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | EXUpdatesCheckOnLaunch 6 | ALWAYS 7 | EXUpdatesEnabled 8 | 9 | EXUpdatesLaunchWaitMs 10 | 0 11 | EXUpdatesRuntimeVersion 12 | 1.0.0 13 | EXUpdatesURL 14 | https://u.expo.dev/f406fd9a-e9b0-4fd9-93f0-d2533b773a05 15 | 16 | -------------------------------------------------------------------------------- /apps/mobile/nativewind-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // NOTE: This file should not be edited and should be committed with your source code. It is generated by NativeWind. 4 | -------------------------------------------------------------------------------- /apps/mobile/phosphor-react-native.d.ts: -------------------------------------------------------------------------------- 1 | declare module "phosphor-react-native" { 2 | // Re-export all named exports from the actual ESM module. 3 | // This should bring in types for icons like CaretLeft, X, etc., 4 | // if "phosphor-react-native/lib/module" has its own type definitions. 5 | export * from "phosphor-react-native/lib/typescript/index"; 6 | 7 | // Now, include your specific definitions for IconContext. 8 | // These will either add to or override what's exported from "phosphor-react-native/lib/module" 9 | // when you import from "phosphor-react-native". 10 | 11 | // export interface IconContextProps { 12 | // color?: string; 13 | // size?: string | number; 14 | // weight?: "thin" | "light" | "regular" | "bold" | "fill" | "duotone"; 15 | // mirrored?: boolean; 16 | // } 17 | 18 | // export interface IconContext { 19 | // Provider: React.Provider; 20 | // Consumer: React.Consumer; 21 | // } 22 | 23 | // export const IconContext: IconContext; 24 | } 25 | -------------------------------------------------------------------------------- /apps/mobile/polyfills.ts: -------------------------------------------------------------------------------- 1 | import { Platform } from "react-native"; 2 | import structuredCloneImpl from "@ungap/structured-clone"; 3 | 4 | if (Platform.OS !== "web") { 5 | const setupPolyfills = async (): Promise => { 6 | // @ts-expect-error - Dynamic import types are hard to manage 7 | const polyfillFunctions = await import( 8 | "react-native/Libraries/Utilities/PolyfillFunctions" 9 | ); 10 | // @ts-expect-error - Dynamic import types are hard to manage 11 | const streamsTextEncoding = await import( 12 | "@stardazed/streams-text-encoding" 13 | ); 14 | 15 | const polyfillGlobal = polyfillFunctions.polyfillGlobal as ( 16 | name: string, 17 | getValue: () => unknown, 18 | ) => void; 19 | const TextEncoderStream = 20 | streamsTextEncoding.TextEncoderStream as typeof globalThis.TextEncoderStream; 21 | const TextDecoderStream = 22 | streamsTextEncoding.TextDecoderStream as typeof globalThis.TextDecoderStream; 23 | 24 | if (!("structuredClone" in global)) { 25 | polyfillGlobal("structuredClone", () => structuredCloneImpl); 26 | } 27 | 28 | polyfillGlobal("TextEncoderStream", () => TextEncoderStream); 29 | polyfillGlobal("TextDecoderStream", () => TextDecoderStream); 30 | }; 31 | 32 | void setupPolyfills(); 33 | } 34 | 35 | export {}; 36 | -------------------------------------------------------------------------------- /apps/mobile/prebuild.js: -------------------------------------------------------------------------------- 1 | import { execSync } from "child_process"; 2 | import path from "path"; 3 | import { fileURLToPath } from "url"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = path.dirname(__filename); 7 | 8 | // Get command line arguments 9 | const args = process.argv.slice(2); 10 | console.log("📝 Received arguments:", args); 11 | 12 | console.log("🚀 Starting prebuild process..."); 13 | 14 | try { 15 | // Change to root directory 16 | const rootDir = path.join(__dirname, "../.."); 17 | console.log(`📂 Changing to root directory: ${rootDir}`); 18 | process.chdir(rootDir); 19 | 20 | // Run commands 21 | console.log("📦 Installing dependencies..."); 22 | execSync("pnpm install", { stdio: "inherit" }); 23 | 24 | console.log("🔨 Building workspace packages..."); 25 | execSync("pnpm build", { stdio: "inherit" }); 26 | 27 | // Change back to mobile directory 28 | const mobileDir = path.join(__dirname); 29 | console.log(`📂 Returning to mobile directory: ${mobileDir}`); 30 | process.chdir(mobileDir); 31 | 32 | console.log("✅ Prebuild completed successfully!"); 33 | } catch (error) { 34 | console.error("❌ Prebuild failed:", error.message); 35 | process.exit(1); 36 | } 37 | -------------------------------------------------------------------------------- /apps/mobile/src/app/(home)/profile.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ScrollView, View } from "react-native"; 3 | import { SafeAreaView } from "react-native-safe-area-context"; 4 | import { useRouter } from "expo-router"; 5 | 6 | import { Button } from "~/components/ui/button"; 7 | import { Text } from "~/components/ui/text"; 8 | import { authClient, doAuthLogout } from "~/utils/auth-client"; 9 | 10 | export default function Profile() { 11 | const { data: session } = authClient.useSession(); 12 | const router = useRouter(); 13 | async function handleLogout() { 14 | await doAuthLogout(); 15 | router.replace("../../login"); 16 | } 17 | 18 | return ( 19 | 20 | 21 | 22 | 23 | Settings 24 | 25 | 26 | 27 | User Information 28 | Name: {session?.user.name ?? "Not available"} 29 | Email: {session?.user.email ?? "Not available"} 30 | 31 | 32 | 35 | 41 | 42 | 43 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /apps/mobile/src/app/(home)/settings.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Switch, View } from "react-native"; 3 | import { SafeAreaView } from "react-native-safe-area-context"; 4 | import { useColorScheme } from "nativewind"; 5 | 6 | import { Text } from "~/components/ui/text"; 7 | 8 | export default function Settings() { 9 | //! TODO: add delete account 10 | const { colorScheme, toggleColorScheme } = useColorScheme(); 11 | const isDarkMode = colorScheme === "dark"; 12 | 13 | const handleToggleTheme = () => { 14 | toggleColorScheme(); 15 | }; 16 | 17 | return ( 18 | 19 | 20 | 21 | Settings 22 | 23 | 24 | 25 | 26 | Dark Mode 27 | 33 | 34 | 35 | 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /apps/mobile/src/app/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { View } from "react-native"; 3 | import { SafeAreaView } from "react-native-safe-area-context"; 4 | import { Stack, useRouter } from "expo-router"; 5 | 6 | import { Loading } from "~/components/loading"; 7 | import { Text } from "~/components/ui/text"; 8 | import { authClient } from "~/utils/auth-client"; 9 | 10 | export default function Index() { 11 | const { data: session, isPending } = authClient.useSession(); 12 | const router = useRouter(); 13 | 14 | useEffect(() => { 15 | // Wait 2 seconds before redirecting 16 | const timer = setTimeout(() => { 17 | if (!isPending) { 18 | if (session?.user.id) { 19 | // User is logged in, redirect to dashboard 20 | 21 | router.replace("/post-login-redirect"); 22 | } else { 23 | // User is not logged in, redirect to login 24 | router.replace("/login"); 25 | } 26 | } 27 | // hacky workaround due to "route not found" error when splash screen is hidden, otherwise set to 2000 28 | }, 4000); 29 | 30 | return () => clearTimeout(timer); 31 | }, [session, isPending, router]); 32 | 33 | return ( 34 | 35 | {/* Changes page title visible on the header */} 36 | 37 | 38 | 39 | 40 | 41 | Loading{" "} 42 | 46 | your 47 | {" "} 48 | health potential 49 | 50 | 51 | 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /apps/mobile/src/components/app/trackablesHeader.tsx: -------------------------------------------------------------------------------- 1 | import type { NativeStackHeaderProps } from "@react-navigation/native-stack"; 2 | import { View } from "react-native"; 3 | import { SafeAreaView } from "react-native-safe-area-context"; 4 | 5 | import { Text } from "../ui/text"; 6 | 7 | export default function TrackablesHeader({ 8 | props, 9 | }: { 10 | props: NativeStackHeaderProps; 11 | }) { 12 | return ( 13 | 17 | {props.options.headerLeft ? ( 18 | {props.options.headerLeft({})} 19 | ) : ( 20 | 21 | )} 22 | 23 | 24 | {props.options.title && ( 25 | 26 | {props.options.title} 27 | 28 | )} 29 | 30 | {props.options.headerRight ? ( 31 | {props.options.headerRight({})} 32 | ) : ( 33 | 34 | )} 35 | 36 | ); 37 | } 38 | 39 | function EmptyButton() { 40 | return ; 41 | } 42 | -------------------------------------------------------------------------------- /apps/mobile/src/components/app/userHeader.tsx: -------------------------------------------------------------------------------- 1 | import { View } from "react-native"; 2 | import { SafeAreaView } from "react-native-safe-area-context"; 3 | import { Link } from "expo-router"; 4 | import { useQuery } from "@tanstack/react-query"; 5 | import { Bug, Fire, Gear, Star } from "phosphor-react-native"; 6 | 7 | import { trpc } from "~/utils/api"; 8 | import { iconColor } from "~/utils/ui"; 9 | import { Text } from "../ui/text"; 10 | 11 | export default function UserHeader() { 12 | // get user profile 13 | const { data: profileData } = useQuery( 14 | trpc.user.profile.getUserProfileOverview.queryOptions(), 15 | ); 16 | return ( 17 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | {profileData?.xpTotal.toString().split(".")[0]} 32 | 33 | 34 | 35 | 36 | 37 | {profileData?.streakCurrentDays} 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /apps/mobile/src/components/loading.tsx: -------------------------------------------------------------------------------- 1 | import type { VariantProps } from "class-variance-authority"; 2 | import React from "react"; 3 | import { View } from "react-native"; 4 | import { cva } from "class-variance-authority"; 5 | 6 | import { cn } from "~/utils/ui"; 7 | import { AnimatedHeart } from "./branding/heart-sprite"; 8 | 9 | const loadingVariants = cva("flex-1 items-center justify-center", { 10 | variants: { 11 | size: { 12 | tiny: "size-3 max-h-3 min-h-3 min-w-3 max-w-3", 13 | sm: "size-4 max-h-4 min-h-4 min-w-4 max-w-4", 14 | md: "size-6 max-h-6 min-h-6 min-w-6 max-w-6", 15 | lg: "size-9 max-h-9 min-h-9 min-w-9 max-w-9", 16 | xl: "size-12 max-h-12 min-h-12 min-w-12 max-w-12", 17 | "2xl": "size-16 max-h-16 min-h-16 min-w-16 max-w-16", 18 | "3xl": "size-32 max-h-32 min-h-32 min-w-32 max-w-32", 19 | mega: "size-64 max-h-64 min-h-64 min-w-64 max-w-64", 20 | }, 21 | }, 22 | defaultVariants: { 23 | size: "md", 24 | }, 25 | }); 26 | 27 | export const Loading: React.FC< 28 | VariantProps & { bpm?: number } 29 | > = ({ size, bpm = 42 }) => { 30 | return ( 31 | 32 | 33 | 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /apps/mobile/src/components/trackables/TemplateConfigForm.tsx: -------------------------------------------------------------------------------- 1 | import { View } from "react-native"; 2 | 3 | import type { BaseTemplate, TrackableConfigWithMeta } from "@potential/templates"; 4 | 5 | import { Button } from "~/components/ui/button"; 6 | import { Input } from "~/components/ui/input"; 7 | import { Text } from "~/components/ui/text"; 8 | 9 | interface TemplateConfigFormProps { 10 | template: BaseTemplate; 11 | onSubmit: (data: { 12 | name: string; 13 | description?: string; 14 | customConfig: TrackableConfigWithMeta; 15 | }) => void; 16 | onCancel: () => void; 17 | } 18 | 19 | export function TemplateConfigForm({ 20 | template, 21 | onSubmit, 22 | onCancel, 23 | }: TemplateConfigFormProps) { 24 | return ( 25 | 26 | 27 | Configure {template.name} 28 | 29 | 30 | 31 | 32 | Name 33 | 38 | 39 | 40 | 41 | Description 42 | 48 | 49 | 50 | {/* Render different config inputs based on template.defaultConfig.type */} 51 | 55 | 56 | 57 | 60 | 63 | 64 | 65 | 66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /apps/mobile/src/components/trackables/TrackableSection.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { View } from "react-native"; 3 | 4 | import type { TrackableCustomConfig } from "@potential/consts"; 5 | 6 | import { TrackableCard } from "./TrackableCard"; 7 | 8 | interface Trackable { 9 | id: string; 10 | name: string; 11 | description: string | null; 12 | type: string; 13 | subType: string; 14 | configType: string; 15 | customConfig: TrackableCustomConfig; 16 | } 17 | 18 | interface TrackableSectionProps { 19 | title: string; 20 | trackables: Trackable[]; 21 | } 22 | 23 | export function TrackableSection({ trackables }: TrackableSectionProps) { 24 | if (trackables.length === 0) { 25 | return null; 26 | } 27 | 28 | return ( 29 | 30 | {trackables.map((trackable) => ( 31 | 32 | ))} 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /apps/mobile/src/components/trackables/displays/CheckboxDisplay.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { View } from "react-native"; 3 | 4 | import type { Log, Trackable } from "~/types/trackables"; 5 | import { Checkbox } from "~/components/ui/checkbox"; 6 | import { Text } from "~/components/ui/text"; 7 | 8 | interface CheckboxDisplayProps { 9 | checked: boolean; 10 | label?: string; 11 | size?: "sm" | "md" | "lg"; 12 | color?: "accent" | "default"; 13 | } 14 | 15 | export function CheckboxDisplay({ 16 | checked, 17 | label = "Completed", 18 | size = "md", 19 | }: CheckboxDisplayProps) { 20 | const textSize = 21 | size === "sm" ? "text-sm" : size === "lg" ? "text-lg" : "text-base"; 22 | 23 | const checkboxSize = size === "sm" ? "sm" : size === "lg" ? "lg" : "default"; 24 | 25 | return ( 26 | 27 | {label} 28 | 29 | 30 | ); 31 | } 32 | 33 | export function getCheckboxValueFromLog( 34 | log: Log, 35 | trackable: Trackable, 36 | size: "sm" | "md" | "lg" = "sm", 37 | ): React.ReactNode { 38 | if (log.checked === null) return null; 39 | 40 | let label = "Completed"; 41 | if (trackable.customConfig.type === "checkbox") { 42 | label = trackable.customConfig.checkboxName || "Completed"; 43 | } 44 | 45 | return ( 46 | 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /apps/mobile/src/components/trackables/displays/RangeDisplay.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { View } from "react-native"; 3 | 4 | import type { Log } from "~/types/trackables"; 5 | import { Text } from "~/components/ui/text"; 6 | 7 | interface RangeDisplayProps { 8 | value: number; 9 | min?: number; 10 | max?: number; 11 | unit?: string; 12 | size?: "sm" | "md" | "lg"; 13 | } 14 | 15 | export function RangeDisplay({ 16 | value, 17 | min = 0, 18 | max = 100, 19 | unit = "", 20 | size = "md", 21 | }: RangeDisplayProps) { 22 | const titleSize = 23 | size === "sm" ? "text-base" : size === "lg" ? "text-xl" : "text-lg"; 24 | 25 | // Calculate position as a percentage 26 | const position = Math.min( 27 | 100, 28 | Math.max(0, ((value - min) / (max - min)) * 100), 29 | ); 30 | 31 | return ( 32 | 33 | 34 | 35 | {value} 36 | 37 | /{max} 38 | 39 | 40 | {unit && ( 41 | 44 | {unit} 45 | 46 | )} 47 | 48 | 49 | {/* Range bar visualization */} 50 | 51 | 55 | 56 | 57 | ); 58 | } 59 | 60 | export function getRangeValueFromLog( 61 | log: Log, 62 | min = 0, 63 | max = 100, 64 | unit?: string, 65 | size: "sm" | "md" | "lg" = "sm", 66 | ): React.ReactNode { 67 | if (log.numericValue === null) return null; 68 | return ( 69 | 76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /apps/mobile/src/components/trackables/displays/RatingDisplay.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import type { Log } from "~/types/trackables"; 4 | import { RatingDisplay as BaseRatingDisplay } from "~/components/ui/rating-display"; 5 | 6 | interface RatingDisplayWrapperProps { 7 | value: number; 8 | ratingMax?: 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10; 9 | ratingIcon?: string; 10 | ratingEmoji?: string; 11 | size?: "sm" | "md" | "lg"; 12 | } 13 | 14 | export function RatingDisplayWrapper({ 15 | value, 16 | ratingMax = 5, 17 | ratingIcon, 18 | ratingEmoji, 19 | size = "md", 20 | }: RatingDisplayWrapperProps) { 21 | // Ensure ratingMax is between 2 and 10 22 | const safeRatingMax = Math.max(2, Math.min(10, ratingMax)) as 23 | | 2 24 | | 3 25 | | 4 26 | | 5 27 | | 6 28 | | 7 29 | | 8 30 | | 9 31 | | 10; 32 | 33 | return ( 34 | 41 | ); 42 | } 43 | 44 | export function getRatingValueFromLog( 45 | log: Log, 46 | ratingMax?: 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10, 47 | ratingIcon?: string, 48 | ratingEmoji?: string, 49 | size: "sm" | "md" | "lg" = "sm", 50 | ): React.ReactNode { 51 | if (log.numericValue === null) return null; 52 | return ( 53 | 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /apps/mobile/src/components/trackables/displays/TextDisplay.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import type { Log } from "~/types/trackables"; 4 | import { Text } from "~/components/ui/text"; 5 | import { cn } from "~/utils/ui"; 6 | 7 | interface TextDisplayProps { 8 | value: string; 9 | isLong?: boolean; 10 | size?: "sm" | "md" | "lg"; 11 | shortened?: boolean; 12 | } 13 | 14 | export function TextDisplay({ 15 | value, 16 | isLong = false, 17 | size = "md", 18 | shortened = false, 19 | }: TextDisplayProps) { 20 | const textSize = 21 | size === "sm" ? "text-sm" : size === "lg" ? "text-lg" : "text-base"; 22 | 23 | if (isLong) { 24 | return ( 25 | 31 | {value} 32 | 33 | ); 34 | } 35 | 36 | // For short text 37 | return ( 38 | 39 | {value} 40 | 41 | ); 42 | } 43 | 44 | export function getTextValueFromLog( 45 | log: Log, 46 | isLong = false, 47 | size: "sm" | "md" | "lg" = "sm", 48 | shortened = false, 49 | ): React.ReactNode { 50 | if (!log.textValue) return null; 51 | return ( 52 | 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /apps/mobile/src/components/trackables/inputs/CheckboxInput.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { View } from "react-native"; 3 | 4 | import { Checkbox } from "~/components/ui/checkbox"; 5 | import { Text } from "~/components/ui/text"; 6 | 7 | interface CheckboxInputProps { 8 | checked: boolean; 9 | onCheckedChange: (checked: boolean) => void; 10 | label?: string; 11 | size?: "sm" | "md" | "lg"; 12 | } 13 | 14 | /** 15 | * Input component for checkbox trackables 16 | */ 17 | export function CheckboxInput({ 18 | checked, 19 | onCheckedChange, 20 | label = "Completed", 21 | size = "md", 22 | }: CheckboxInputProps) { 23 | const textSize = 24 | size === "sm" ? "text-sm" : size === "lg" ? "text-lg" : "text-base"; 25 | 26 | return ( 27 | 28 | 29 | {label} 30 | 31 | ); 32 | } 33 | 34 | /** 35 | * Higher-level function to get a checkbox input component 36 | */ 37 | export function getCheckboxInputForLog( 38 | checked: boolean, 39 | onCheckedChange: (checked: boolean) => void, 40 | label?: string, 41 | ): React.ReactNode { 42 | return ( 43 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /apps/mobile/src/components/trackables/inputs/RangeInput.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { View } from "react-native"; 3 | 4 | import { Slider } from "~/components/ui/slider"; 5 | import { Text } from "~/components/ui/text"; 6 | 7 | interface RangeInputProps { 8 | value: number; 9 | onValueChange: (value: number) => void; 10 | min?: number; 11 | max?: number; 12 | unit?: string; 13 | minLabel?: string; 14 | maxLabel?: string; 15 | size?: "sm" | "md" | "lg"; 16 | } 17 | 18 | /** 19 | * Input component for range trackables 20 | */ 21 | export function RangeInput({ 22 | value, 23 | onValueChange, 24 | min = 0, 25 | max = 100, 26 | unit = "", 27 | minLabel, 28 | maxLabel, 29 | // We're not using size directly, but keeping it in the interface for consistency 30 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 31 | size = "md", 32 | }: RangeInputProps) { 33 | return ( 34 | 35 | 36 | {minLabel && {minLabel}} 37 | 38 | {value} 39 | {unit ? ` ${unit}` : ""} 40 | 41 | {maxLabel && {maxLabel}} 42 | 43 | 44 | 53 | 54 | ); 55 | } 56 | 57 | /** 58 | * Higher-level function to get a range input component 59 | */ 60 | export function getRangeInputForLog( 61 | value: number, 62 | onValueChange: (value: number) => void, 63 | min?: number, 64 | max?: number, 65 | unit?: string, 66 | minLabel?: string, 67 | maxLabel?: string, 68 | ): React.ReactNode { 69 | return ( 70 | 79 | ); 80 | } 81 | -------------------------------------------------------------------------------- /apps/mobile/src/components/trackables/inputs/TextInput.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { View } from "react-native"; 3 | 4 | import { Input } from "~/components/ui/input"; 5 | import { LongText } from "~/components/ui/long-text"; 6 | import { Text } from "~/components/ui/text"; 7 | 8 | interface TextInputProps { 9 | value: string; 10 | onChangeText: (text: string) => void; 11 | isLong?: boolean; 12 | placeholder?: string; 13 | label?: string; 14 | maxLength?: number; 15 | size?: "sm" | "md" | "lg"; 16 | } 17 | 18 | /** 19 | * Input component for text trackables (both short and long text) 20 | */ 21 | export function TextInput({ 22 | value, 23 | onChangeText, 24 | isLong = false, 25 | placeholder = "Enter text...", 26 | label, 27 | maxLength, 28 | // We're not using size here, but keeping it in the props for consistency 29 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 30 | size = "md", 31 | }: TextInputProps) { 32 | return ( 33 | 34 | {label && {label}} 35 | 36 | {isLong ? ( 37 | 43 | ) : ( 44 | 50 | )} 51 | 52 | ); 53 | } 54 | 55 | /** 56 | * Higher-level function to get a text input component 57 | */ 58 | export function getTextInputForLog( 59 | value: string, 60 | onChangeText: (text: string) => void, 61 | isLong = false, 62 | placeholder?: string, 63 | label?: string, 64 | maxLength?: number, 65 | ): React.ReactNode { 66 | return ( 67 | 75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /apps/mobile/src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import type { ViewProps } from "react-native"; 2 | import React from "react"; 3 | import { View } from "react-native"; 4 | 5 | import { cn } from "~/utils/ui"; 6 | 7 | interface CardProps extends ViewProps { 8 | children: React.ReactNode; 9 | className?: string; 10 | } 11 | 12 | export function Card({ children, className, ...props }: CardProps) { 13 | return ( 14 | 21 | {children} 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /apps/mobile/src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import type { TextInputProps } from "react-native"; 2 | import * as React from "react"; 3 | import { TextInput, View } from "react-native"; 4 | 5 | import { cn } from "~/utils/ui"; 6 | import { Text } from "./text"; 7 | 8 | export type InputProps = TextInputProps & { 9 | label?: string; 10 | helperText?: string; 11 | error?: string | object[]; 12 | }; 13 | 14 | const Input = React.forwardRef, InputProps>( 15 | ( 16 | { className, placeholderClassName, label, helperText, error, ...props }, 17 | ref, 18 | ) => { 19 | return ( 20 | 21 | {label && ( 22 | 23 | {label} 24 | 25 | )} 26 | 36 | {helperText && !error && ( 37 | 38 | {helperText} 39 | 40 | )} 41 | {error && typeof error === "string" && ( 42 | 43 | {error} 44 | 45 | )} 46 | {error && error.length > 0 && ( 47 | 48 | {typeof error[0] === "string" ? error[0] : error[0].message} 49 | 50 | )} 51 | 52 | ); 53 | }, 54 | ); 55 | 56 | Input.displayName = "Input"; 57 | 58 | export { Input }; 59 | -------------------------------------------------------------------------------- /apps/mobile/src/components/ui/long-text.tsx: -------------------------------------------------------------------------------- 1 | import type { TextInputProps } from "react-native"; 2 | import * as React from "react"; 3 | import { TextInput, View } from "react-native"; 4 | 5 | import type { InputProps } from "./input"; 6 | import { cn } from "~/utils/ui"; 7 | import { Text } from "./text"; 8 | 9 | const LongText = React.forwardRef< 10 | React.ElementRef, 11 | TextInputProps & InputProps 12 | >( 13 | ( 14 | { className, placeholderClassName, label, helperText, error, ...props }, 15 | ref, 16 | ) => { 17 | return ( 18 | 19 | {label && ( 20 | 21 | {label} 22 | 23 | )} 24 | 36 | {helperText && !error && ( 37 | 38 | {helperText} 39 | 40 | )} 41 | {error && typeof error === "string" && ( 42 | 43 | {error} 44 | 45 | )} 46 | {error && error.length > 0 && ( 47 | 48 | {typeof error[0] === "string" ? error[0] : error[0].message} 49 | 50 | )} 51 | 52 | ); 53 | }, 54 | ); 55 | 56 | LongText.displayName = "LongText"; 57 | 58 | export { LongText }; 59 | -------------------------------------------------------------------------------- /apps/mobile/src/components/ui/picker.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { View } from "react-native"; 3 | import { Picker as RNPicker } from "@react-native-picker/picker"; 4 | import { useColorScheme } from "nativewind"; 5 | 6 | import { cn } from "~/utils/ui"; 7 | 8 | export interface PickerOption { 9 | value: string; 10 | label: string; 11 | } 12 | 13 | export interface PickerProps { 14 | items: PickerOption[]; 15 | value: string; 16 | onChange: (value: string) => void; 17 | className?: string; 18 | } 19 | 20 | export function Picker({ items, value, onChange, className }: PickerProps) { 21 | const { colorScheme } = useColorScheme(); 22 | return ( 23 | 24 | 33 | {items.map((option) => ( 34 | 39 | ))} 40 | 41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /apps/mobile/src/components/ui/request-permission.tsx: -------------------------------------------------------------------------------- 1 | import { View } from "react-native"; 2 | 3 | import type { PermissionType } from "../../lib/hooks/usePermission"; 4 | import { usePermission } from "../../lib/hooks/usePermission"; 5 | import { Button } from "./button"; 6 | import { Text } from "./text"; 7 | 8 | interface RequestPermissionProps { 9 | permission: PermissionType; 10 | title?: string; 11 | description?: string; 12 | buttonText?: string; 13 | onPermissionGranted?: () => void; 14 | } 15 | 16 | export function RequestPermission({ 17 | permission, 18 | title = "Permission Required", 19 | description = `We need your permission to use the ${permission}`, 20 | buttonText = `Grant ${permission} Permission`, 21 | onPermissionGranted, 22 | }: RequestPermissionProps) { 23 | const { requestPermission } = usePermission(permission); 24 | 25 | const handleRequestPermission = async () => { 26 | const granted = await requestPermission(); 27 | if (granted && onPermissionGranted) { 28 | onPermissionGranted(); 29 | } 30 | }; 31 | 32 | return ( 33 | 34 | {title} 35 | {description} 36 | 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /apps/mobile/src/components/ui/text.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @potential/no-direct-rn-import */ 2 | import type { SlottableTextProps, TextRef } from "@rn-primitives/types"; 3 | import type { VariantProps } from "class-variance-authority"; 4 | import * as React from "react"; 5 | import { Text as RNText } from "react-native"; 6 | import * as Slot from "@rn-primitives/slot"; 7 | import { cva } from "class-variance-authority"; 8 | 9 | import { cn } from "~/utils/ui"; 10 | 11 | const textVariants = cva("web:select-text text-sand-12 text-base", { 12 | variants: { 13 | type: { 14 | paragraph: "font-['IBMPlexSans-Regular']", 15 | title: "font-['IBMPlexSerif-Italic'] italic", 16 | }, 17 | }, 18 | defaultVariants: { 19 | type: "paragraph", 20 | }, 21 | }); 22 | 23 | type TextVariants = VariantProps; 24 | 25 | const TextClassContext = React.createContext(undefined); 26 | 27 | const Text = React.forwardRef( 28 | ({ className, asChild = false, type, ...props }, ref) => { 29 | const textClass = React.useContext(TextClassContext); 30 | const Component = asChild ? Slot.Text : RNText; 31 | return ( 32 | 37 | ); 38 | }, 39 | ); 40 | Text.displayName = "Text"; 41 | 42 | export { Text, TextClassContext }; 43 | -------------------------------------------------------------------------------- /apps/mobile/src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import type { TextInputProps } from "react-native"; 2 | import * as React from "react"; 3 | import { TextInput, View } from "react-native"; 4 | 5 | import type { InputProps } from "./input"; 6 | import { cn } from "~/utils/ui"; 7 | import { Text } from "./text"; 8 | 9 | const Textarea = React.forwardRef< 10 | React.ElementRef, 11 | TextInputProps & InputProps 12 | >( 13 | ( 14 | { 15 | className, 16 | multiline = true, 17 | numberOfLines = 4, 18 | placeholderClassName, 19 | label, 20 | helperText, 21 | error, 22 | ...props 23 | }, 24 | ref, 25 | ) => { 26 | return ( 27 | 28 | {label && ( 29 | 30 | {label} 31 | 32 | )} 33 | 46 | {helperText && !error && ( 47 | 48 | {helperText} 49 | 50 | )} 51 | {error && typeof error === "string" && ( 52 | 53 | {error} 54 | 55 | )} 56 | {error && error.length > 0 && ( 57 | 58 | {typeof error[0] === "string" ? error[0] : error[0].message} 59 | 60 | )} 61 | 62 | ); 63 | }, 64 | ); 65 | 66 | Textarea.displayName = "Textarea"; 67 | 68 | export { Textarea }; 69 | -------------------------------------------------------------------------------- /apps/mobile/src/components/ui/touchable-opacity.tsx: -------------------------------------------------------------------------------- 1 | import type { TouchableOpacityProps } from "react-native"; 2 | import React from "react"; 3 | import { TouchableOpacity as RNTouchableOpacity } from "react-native"; 4 | 5 | export const TouchableOpacity = ({ 6 | children, 7 | ...props 8 | }: TouchableOpacityProps) => { 9 | return {children}; 10 | }; 11 | -------------------------------------------------------------------------------- /apps/mobile/src/styles.css: -------------------------------------------------------------------------------- 1 | @import "radi-color-css/dist/colors.css"; 2 | 3 | @tailwind base; 4 | @tailwind components; 5 | @tailwind utilities; 6 | 7 | :root { 8 | --background: var(--mint-5); 9 | --foreground: var(--mint-12); 10 | --primary: var(--mint-9); 11 | --primary-foreground: var(--mint-12); 12 | --secondary: var(--mint-4); 13 | --secondary-foreground: var(--mint-12); 14 | --muted: var(--mint-3); 15 | --muted-foreground: var(--mint-11); 16 | --accent: var(--mint-4); 17 | --accent-foreground: var(--mint-12); 18 | --destructive: var(--red-9); 19 | --destructive-foreground: var(--red-12); 20 | --border: var(--mint-6); 21 | --input: var(--mint-6); 22 | --ring: var(--mint-7); 23 | --radius: 0.5rem; 24 | } 25 | -------------------------------------------------------------------------------- /apps/mobile/src/types/trackables.ts: -------------------------------------------------------------------------------- 1 | import type { TrackableCustomConfig } from "@potential/consts"; 2 | 3 | export type TrackableType = 4 | | "measure" 5 | | "checkbox" 6 | | "range" 7 | | "rating" 8 | | "shortText" 9 | | "longText"; 10 | 11 | export interface Log { 12 | id: string; 13 | createdAt: Date | string; 14 | updatedAt?: Date | string; 15 | loggedAt?: Date | string; 16 | ownerId?: string; 17 | trackableId?: string; 18 | parentLogId?: string | null; 19 | checked: boolean | null; 20 | numericValue: number | null; 21 | textValue: string | null; 22 | jsonValue?: { 23 | imageIds?: string[]; 24 | } | null; 25 | source?: string; 26 | } 27 | 28 | export interface Trackable { 29 | id: string; 30 | name: string; 31 | configType: string; 32 | customConfig: TrackableCustomConfig; 33 | } 34 | -------------------------------------------------------------------------------- /apps/mobile/src/utils/api.ts: -------------------------------------------------------------------------------- 1 | import { QueryClient } from "@tanstack/react-query"; 2 | import { createTRPCClient, httpBatchLink, loggerLink } from "@trpc/client"; 3 | import { createTRPCOptionsProxy } from "@trpc/tanstack-react-query"; 4 | import { toast } from "sonner-native"; 5 | import superjson from "superjson"; 6 | 7 | import type { AppRouter } from "@potential/trpc"; 8 | 9 | import { authClient } from "./auth-client"; 10 | import { getApiUrl } from "./base-url"; 11 | 12 | export const getAuthHeaders = () => { 13 | const headers = new Map(); 14 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call 15 | const cookies = authClient.getCookie(); 16 | if (cookies) { 17 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument 18 | headers.set("Cookie", cookies); 19 | } 20 | return Object.fromEntries(headers); 21 | }; 22 | 23 | export const queryClient = new QueryClient({ 24 | defaultOptions: { 25 | queries: { 26 | retry: false, 27 | refetchOnWindowFocus: true, 28 | }, 29 | mutations: { 30 | onError: (err) => toast.error(err.message), 31 | }, 32 | }, 33 | }); 34 | 35 | export const trpcClient = createTRPCClient({ 36 | links: [ 37 | loggerLink({ 38 | enabled: (opts) => 39 | process.env.NODE_ENV === "development" || 40 | (opts.direction === "down" && opts.result instanceof Error), 41 | colorMode: "ansi", 42 | }), 43 | httpBatchLink({ 44 | transformer: superjson, 45 | url: `${getApiUrl()}/trpc`, 46 | headers: getAuthHeaders, 47 | }), 48 | ], 49 | }); 50 | 51 | export const trpc = createTRPCOptionsProxy({ 52 | client: trpcClient, 53 | queryClient, 54 | }); 55 | 56 | export { type RouterInputs, type RouterOutputs } from "@potential/trpc"; 57 | -------------------------------------------------------------------------------- /apps/mobile/src/utils/auth-client.ts: -------------------------------------------------------------------------------- 1 | import { expoClient } from "@better-auth/expo/client"; 2 | import type { BetterAuthClientPlugin } from "better-auth"; 3 | import { emailOTPClient } from "better-auth/client/plugins"; 4 | import { createAuthClient } from "better-auth/react"; 5 | import * as SecureStore from "expo-secure-store"; 6 | 7 | import { getApiUrl } from "./base-url"; 8 | 9 | const authUrl = getApiUrl(); 10 | 11 | export const authClient = createAuthClient({ 12 | baseURL: authUrl + "/auth", 13 | 14 | plugins: [ 15 | emailOTPClient(), 16 | expoClient({ 17 | scheme: "potential", 18 | storagePrefix: "potential", 19 | storage: SecureStore, 20 | }) as unknown as BetterAuthClientPlugin, 21 | ], 22 | }); 23 | 24 | export const doAuthLogout = async () => { 25 | await authClient.signOut(); 26 | await SecureStore.deleteItemAsync("potential_cookie"); 27 | await SecureStore.deleteItemAsync("potential_session_data"); 28 | return; 29 | }; 30 | -------------------------------------------------------------------------------- /apps/mobile/src/utils/base-url.ts: -------------------------------------------------------------------------------- 1 | import Constants from "expo-constants"; 2 | 3 | export const getApiUrl = (): string => { 4 | interface ExpoEnv { 5 | EXPO_PUBLIC_BACKEND_URL?: string; 6 | } 7 | 8 | // get backend api url from env 9 | const env = Constants.expoConfig?.extra?.env as ExpoEnv | undefined; 10 | 11 | if ( 12 | env?.EXPO_PUBLIC_BACKEND_URL && 13 | env.EXPO_PUBLIC_BACKEND_URL !== "" && 14 | env.EXPO_PUBLIC_BACKEND_URL.length > 0 15 | ) { 16 | return env.EXPO_PUBLIC_BACKEND_URL; 17 | } 18 | 19 | // if backend env is not set, assume localhost, fetch and use IP address 20 | 21 | const debuggerHost = Constants.expoConfig?.hostUri; 22 | const localhost = debuggerHost?.split(":")[0]; 23 | 24 | if (!localhost) { 25 | throw new Error( 26 | "Failed to get localhost. Please point to your production server.", 27 | ); 28 | } 29 | return `http://${localhost}:3100`; 30 | }; 31 | -------------------------------------------------------------------------------- /apps/mobile/src/utils/date.ts: -------------------------------------------------------------------------------- 1 | import { duration } from "@potential/utils"; 2 | 3 | export const timeAgoText = ({ date }: { date: Date }) => { 4 | const timeBetweenInputAndNow = 5 | new Date().valueOf() - new Date(date).valueOf(); 6 | 7 | const timeAgo = duration(timeBetweenInputAndNow, { parts: 1 }); 8 | const timeAgoLabel = timeAgo.toString().split(" ")[1]; 9 | const timeAgoValue = timeAgo.toString().split(".")[0]; 10 | 11 | return `${timeAgoValue} ${timeAgoLabel} ago`; 12 | }; 13 | -------------------------------------------------------------------------------- /apps/mobile/src/utils/ui.ts: -------------------------------------------------------------------------------- 1 | import type { ClassValue } from "clsx"; 2 | import { clsx } from "clsx"; 3 | import { colorScheme } from "nativewind"; 4 | import { twMerge } from "tailwind-merge"; 5 | 6 | export function cn(...inputs: ClassValue[]) { 7 | return twMerge(clsx(inputs)); 8 | } 9 | 10 | export function iconColor({ 11 | inverse, 12 | lightColor, 13 | darkColor, 14 | }: { inverse?: boolean; lightColor?: boolean; darkColor?: boolean } = {}) { 15 | const lightColorValue = "#f9f9f8"; 16 | const darkColorValue = "#191918"; 17 | 18 | if (lightColor) return lightColorValue; 19 | if (darkColor) return darkColorValue; 20 | 21 | const theme = colorScheme.get(); 22 | 23 | return !inverse 24 | ? theme === "dark" 25 | ? lightColorValue 26 | : darkColorValue 27 | : theme === "dark" 28 | ? darkColorValue 29 | : lightColorValue; 30 | } 31 | -------------------------------------------------------------------------------- /apps/mobile/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | // @ts-expect-error - no types 3 | import nativewind from "nativewind/preset"; 4 | import { radixColors, tailwindSafelist } from "radi-color-css"; 5 | 6 | export default { 7 | content: ["./src/**/*.{ts,tsx}"], 8 | presets: [nativewind], 9 | safelist: [ 10 | { 11 | ...tailwindSafelist, 12 | }, 13 | ], 14 | theme: { 15 | // Override the base theme completely instead of extending it 16 | colors: { 17 | ...radixColors, 18 | border: "var(--sand-6)", 19 | input: "var(--sand-6)", 20 | ring: "var(--sand-6)", 21 | background: "var(--sand-2)", 22 | foreground: "var(--sand-12)", 23 | primary: { 24 | DEFAULT: "var(--sand-12)", 25 | foreground: "var(--sand-1)", 26 | }, 27 | secondary: { 28 | DEFAULT: "var(--sand-9)", 29 | foreground: "var(--sand-1)", 30 | }, 31 | destructive: { 32 | DEFAULT: "var(--red-9)", 33 | foreground: "var(--red-1)", 34 | }, 35 | muted: { 36 | DEFAULT: "var(--sand-10)", 37 | foreground: "var(--sand-1)", 38 | }, 39 | accent: { 40 | DEFAULT: "var(--orange-9)", 41 | foreground: "var(--orange-1)", 42 | }, 43 | popover: { 44 | DEFAULT: "var(--popover)", 45 | foreground: "var(--popover-foreground)", 46 | }, 47 | card: { 48 | DEFAULT: "var(--card)", 49 | foreground: "var(--card-foreground)", 50 | }, 51 | }, 52 | extend: { 53 | keyframes: { 54 | "accordion-down": { 55 | from: { height: "0" }, 56 | to: { height: "var(--radix-accordion-content-height)" }, 57 | }, 58 | "accordion-up": { 59 | from: { height: "var(--radix-accordion-content-height)" }, 60 | to: { height: "0" }, 61 | }, 62 | }, 63 | animation: { 64 | "accordion-down": "accordion-down 0.2s ease-out", 65 | "accordion-up": "accordion-up 0.2s ease-out", 66 | }, 67 | }, 68 | plugins: [require("tailwindcss-animate")], 69 | }, 70 | } satisfies Config; 71 | -------------------------------------------------------------------------------- /apps/mobile/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@potential/tsconfig/base.json"], 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "~/*": ["./src/*"] 7 | }, 8 | "jsx": "react-native", 9 | "checkJs": false, 10 | "moduleSuffixes": [".ios", ".android", ".native", ""] 11 | }, 12 | "include": [ 13 | "src", 14 | "*.ts", 15 | "*.js", 16 | ".expo/types/**/*.ts", 17 | "expo-env.d.ts", 18 | "nativewind-env.d.ts", 19 | "phosphor-react-native.d.ts" 20 | ], 21 | "exclude": ["node_modules"] 22 | } 23 | -------------------------------------------------------------------------------- /apps/mobile/turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turborepo.org/schema.json", 3 | "extends": ["//"], 4 | "tasks": { 5 | "dev": { 6 | "persistent": true, 7 | "interactive": true 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /apps/web/README.md: -------------------------------------------------------------------------------- 1 | # Create T3 App 2 | 3 | This is a [T3 Stack](https://create.t3.gg/) project bootstrapped with `create-t3-app`. 4 | 5 | ## What's next? How do I make an app with this? 6 | 7 | We try to keep this project as simple as possible, so you can start with just the scaffolding we set up for you, and add additional things later when they become necessary. 8 | 9 | If you are not familiar with the different technologies used in this project, please refer to the respective docs. If you still are in the wind, please join our [Discord](https://t3.gg/discord) and ask for help. 10 | 11 | - [Next.js](https://nextjs.org) 12 | - [NextAuth.js](https://next-auth.js.org) 13 | - [Drizzle](https://orm.drizzle.team) 14 | - [Tailwind CSS](https://tailwindcss.com) 15 | - [tRPC](https://trpc.io) 16 | 17 | ## Learn More 18 | 19 | To learn more about the [T3 Stack](https://create.t3.gg/), take a look at the following resources: 20 | 21 | - [Documentation](https://create.t3.gg/) 22 | - [Learn the T3 Stack](https://create.t3.gg/en/faq#what-learning-resources-are-currently-available) — Check out these awesome tutorials 23 | 24 | You can check out the [create-t3-app GitHub repository](https://github.com/t3-oss/create-t3-app) — your feedback and contributions are welcome! 25 | 26 | ## How do I deploy this? 27 | 28 | Follow our deployment guides for [Vercel](https://create.t3.gg/en/deployment/vercel) and [Docker](https://create.t3.gg/en/deployment/docker) for more information. 29 | -------------------------------------------------------------------------------- /apps/web/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "~/components", 15 | "utils": "~/lib/utils", 16 | "ui": "~/components/ui", 17 | "lib": "~/lib", 18 | "hooks": "~/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /apps/web/eslint.config.js: -------------------------------------------------------------------------------- 1 | import baseConfig, { restrictEnvAccess } from "@potential/eslint-config/base"; 2 | import nextjsConfig from "@potential/eslint-config/nextjs"; 3 | import reactConfig from "@potential/eslint-config/react"; 4 | 5 | /** @type {import('typescript-eslint').Config} */ 6 | export default [ 7 | { 8 | ignores: [".next/**"], 9 | }, 10 | ...baseConfig, 11 | ...reactConfig, 12 | ...nextjsConfig, 13 | ...restrictEnvAccess, 14 | ]; 15 | -------------------------------------------------------------------------------- /apps/web/next.config.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from "url"; 2 | import createJiti from "jiti"; 3 | 4 | // Import env files to validate at build time. Use jiti so we can load .ts files in here. 5 | createJiti(fileURLToPath(import.meta.url))("./src/env"); 6 | 7 | /** @type {import("next").NextConfig} */ 8 | const config = { 9 | /** Enables hot reloading for local packages without a build step */ 10 | transpilePackages: [ 11 | "@potential/api", 12 | "@potential/auth", 13 | "@potential/db", 14 | // "@potential/ui", // Temporarily removed 15 | "@potential/validators", 16 | ], 17 | 18 | /** We already do linting and typechecking as separate tasks in CI */ 19 | eslint: { ignoreDuringBuilds: true }, 20 | typescript: { ignoreBuildErrors: true }, 21 | experimental: { 22 | optimizePackageImports: ["@phosphor-icons/react"], 23 | }, 24 | }; 25 | 26 | export default config; 27 | -------------------------------------------------------------------------------- /apps/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@potential/web", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "build": "pnpm with-env next build", 8 | "clean": "git clean -xdf .cache .next .turbo node_modules", 9 | "dev": "pnpm with-env next dev --turbo", 10 | "format": "prettier --check . --ignore-path ../../.gitignore", 11 | "lint": "eslint", 12 | "start": "pnpm with-env next start", 13 | "typecheck": "tsc --noEmit", 14 | "with-env": "dotenv -e ../../.env --" 15 | }, 16 | "dependencies": { 17 | "@phosphor-icons/react": "^2.1.7", 18 | "@potential/db": "workspace:*", 19 | "@radix-ui/colors": "^3.0.0", 20 | "@radix-ui/react-dialog": "1.1.13", 21 | "@radix-ui/react-slot": "^1.2.0", 22 | "@t3-oss/env-nextjs": "^0.13.4", 23 | "@tanstack/react-query": "catalog:", 24 | "@trpc/client": "catalog:", 25 | "@trpc/server": "catalog:", 26 | "@trpc/tanstack-react-query": "catalog:", 27 | "class-variance-authority": "^0.7.1", 28 | "clsx": "^2.1.1", 29 | "geist": "^1.4.1", 30 | "lucide-react": "^0.507.0", 31 | "next": "^15.3.2", 32 | "react": "catalog:react19", 33 | "react-dom": "catalog:react19", 34 | "resend": "^4.5.0", 35 | "superjson": "2.2.2", 36 | "tailwind-merge": "catalog:", 37 | "tw-animate-css": "^1.2.9", 38 | "zod": "catalog:" 39 | }, 40 | "devDependencies": { 41 | "@potential/eslint-config": "workspace:*", 42 | "@potential/prettier-config": "workspace:*", 43 | "@potential/tailwind-config": "workspace:*", 44 | "@potential/tsconfig": "workspace:*", 45 | "@types/node": "^22.15.3", 46 | "@types/react": "catalog:react19", 47 | "@types/react-dom": "catalog:react19", 48 | "dotenv-cli": "^8.0.0", 49 | "eslint": "catalog:", 50 | "jiti": "^2.4.2", 51 | "prettier": "catalog:", 52 | "tailwindcss": "catalog:", 53 | "typescript": "catalog:" 54 | }, 55 | "prettier": "@potential/prettier-config" 56 | } -------------------------------------------------------------------------------- /apps/web/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /apps/web/public/images/home/flag_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/un/potential/7789ad8ca77a5c2960c0a7ca6c4c452cf95d6be0/apps/web/public/images/home/flag_icon.png -------------------------------------------------------------------------------- /apps/web/public/images/home/flask_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/un/potential/7789ad8ca77a5c2960c0a7ca6c4c452cf95d6be0/apps/web/public/images/home/flask_icon.png -------------------------------------------------------------------------------- /apps/web/public/images/home/growth_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/un/potential/7789ad8ca77a5c2960c0a7ca6c4c452cf95d6be0/apps/web/public/images/home/growth_icon.png -------------------------------------------------------------------------------- /apps/web/public/images/home/omar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/un/potential/7789ad8ca77a5c2960c0a7ca6c4c452cf95d6be0/apps/web/public/images/home/omar.jpg -------------------------------------------------------------------------------- /apps/web/public/images/home/pulse_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/un/potential/7789ad8ca77a5c2960c0a7ca6c4c452cf95d6be0/apps/web/public/images/home/pulse_icon.png -------------------------------------------------------------------------------- /apps/web/public/pitch.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/un/potential/7789ad8ca77a5c2960c0a7ca6c4c452cf95d6be0/apps/web/public/pitch.pdf -------------------------------------------------------------------------------- /apps/web/src/app/_components/branding/logo.tsx: -------------------------------------------------------------------------------- 1 | import type { VariantProps } from "class-variance-authority"; 2 | import * as React from "react"; 3 | import Image from "next/image"; 4 | import { cva } from "class-variance-authority"; 5 | 6 | import { cn } from "~/lib/utils"; 7 | 8 | const logoVariants = cva("relative flex flex-col items-center justify-center", { 9 | variants: { 10 | size: { 11 | tiny: "size-3 max-h-3 min-h-3 min-w-3 max-w-3", 12 | sm: "size-4 max-h-4 min-h-4 min-w-4 max-w-4", 13 | md: "size-6 max-h-6 min-h-6 min-w-6 max-w-6", 14 | lg: "size-9 max-h-9 min-h-9 min-w-9 max-w-9", 15 | xl: "size-12 max-h-12 min-h-12 min-w-12 max-w-12", 16 | "2xl": "size-16 max-h-16 min-h-16 min-w-16 max-w-16", 17 | "3xl": "size-32 max-h-32 min-h-32 min-w-32 max-w-32", 18 | mega: "size-64 max-h-64 min-h-64 min-w-64 max-w-64", 19 | }, 20 | }, 21 | defaultVariants: { 22 | size: "3xl", 23 | }, 24 | }); 25 | 26 | export const Logo: React.FC> = ({ size }) => ( 27 |
28 | 1up 29 |
30 | ); 31 | -------------------------------------------------------------------------------- /apps/web/src/app/actions/subscribe.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { Resend } from "resend"; 4 | 5 | import { env } from "~/env"; 6 | 7 | const resend = new Resend(env.RESEND_API_KEY); 8 | 9 | export async function subscribe(formData: FormData) { 10 | const email = formData.get("email") as string; 11 | 12 | if (!email) { 13 | return { error: "Email is required" }; 14 | } 15 | 16 | try { 17 | await resend.contacts.create({ 18 | email, 19 | unsubscribed: false, 20 | audienceId: env.RESEND_AUDIENCE_ID, 21 | }); 22 | 23 | return { success: true }; 24 | } catch (error) { 25 | console.error("Error subscribing:", error); 26 | return { error: "Failed to subscribe" }; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /apps/web/src/app/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/un/potential/7789ad8ca77a5c2960c0a7ca6c4c452cf95d6be0/apps/web/src/app/apple-icon.png -------------------------------------------------------------------------------- /apps/web/src/app/components/divider.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "../../lib/utils"; 2 | 3 | interface DividerProps { 4 | className?: string; 5 | } 6 | 7 | export function Divider({ className }: DividerProps) { 8 | return ( 9 |
15 | . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 | . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 | . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 | . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /apps/web/src/app/components/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "~/lib/utils"; 4 | 5 | const Input = React.forwardRef>( 6 | ({ className, type, ...props }, ref) => { 7 | return ( 8 | 17 | ); 18 | }, 19 | ); 20 | Input.displayName = "Input"; 21 | 22 | export { Input }; 23 | -------------------------------------------------------------------------------- /apps/web/src/app/components/pulsingUnderline.tsx: -------------------------------------------------------------------------------- 1 | const PulsingUnderline = ({ 2 | children, 3 | className = "", 4 | }: { 5 | children: React.ReactNode; 6 | className?: string; 7 | }) => { 8 | return ( 9 | 10 | {children} 11 | 12 | 13 | ); 14 | }; 15 | 16 | export default PulsingUnderline; 17 | -------------------------------------------------------------------------------- /apps/web/src/app/components/theme.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { MoonIcon, SunIcon } from "@radix-ui/react-icons"; 4 | import { ThemeProvider, useTheme } from "next-themes"; 5 | 6 | import { Button } from "./button"; 7 | 8 | function ThemeToggle() { 9 | const { setTheme, theme } = useTheme(); 10 | 11 | return ( 12 | 24 | // 25 | // 26 | // 34 | // 35 | // 36 | // setTheme("light")}> 37 | // Light 38 | // 39 | // setTheme("dark")}> 40 | // Dark 41 | // 42 | // setTheme("system")}> 43 | // System 44 | // 45 | // 46 | // 47 | ); 48 | } 49 | 50 | export { ThemeProvider, ThemeToggle, useTheme }; 51 | -------------------------------------------------------------------------------- /apps/web/src/app/components/toast.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useTheme } from "next-themes"; 4 | import { Toaster as Sonner, toast } from "sonner"; 5 | 6 | type ToasterProps = React.ComponentProps; 7 | 8 | const Toaster = ({ ...props }: ToasterProps) => { 9 | const { theme = "system" } = useTheme(); 10 | 11 | return ( 12 | 28 | ); 29 | }; 30 | 31 | export { Toaster, toast }; 32 | -------------------------------------------------------------------------------- /apps/web/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/un/potential/7789ad8ca77a5c2960c0a7ca6c4c452cf95d6be0/apps/web/src/app/favicon.ico -------------------------------------------------------------------------------- /apps/web/src/app/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/un/potential/7789ad8ca77a5c2960c0a7ca6c4c452cf95d6be0/apps/web/src/app/icon.png -------------------------------------------------------------------------------- /apps/web/src/app/not-found.tsx: -------------------------------------------------------------------------------- 1 | export default function NotFound() { 2 | return ( 3 |
4 |

404 - Page Not Found

5 |

Sorry, the page you are looking for does not exist.

6 |
7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /apps/web/src/app/refund/page.tsx: -------------------------------------------------------------------------------- 1 | export default function SuccessPage() { 2 | return ( 3 |
4 |
5 |

6 | 100% Refund within 90 days 7 |

8 | 9 |

10 | Potential Health isn't what you thought it was? 11 |

12 |

13 | We'll refund you 100% of your purchase within 90 days. 14 |

15 |

16 | Please tell us why you want a refund at{" "} 17 | omar@potentialhealth.io 18 | and we'll get you sorted. 19 |

20 |

21 | Keep in mind: It could take a few days to process refunds. 22 |

23 |
24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /apps/web/src/app/success/page.tsx: -------------------------------------------------------------------------------- 1 | export default function SuccessPage() { 2 | return ( 3 |
4 |
5 |

6 | Welcome to Potential Health 7 |

8 | 9 |

10 | You will shortly be added to our TestFlight group. 11 |

12 |

13 | This is currently a manual process and may take up to 24 hours 14 | (usually done within 1 hour). 15 |

16 |

17 | If you do not receive an email within 24 hours, please contact us at{" "} 18 | 19 | omar@potentialhealth.io 20 | 21 |

22 |
23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /apps/web/src/app/terms/page.tsx: -------------------------------------------------------------------------------- 1 | export default function SuccessPage() { 2 | return ( 3 |
4 |
5 |

6 | 100% Refund within 90 days 7 |

8 | 9 |

10 | Potential Health isn't what you thought it was? 11 |

12 |

13 | We'll refund you 100% of your purchase within 90 days. 14 |

15 |

16 | Please tell us why you want a refund at{" "} 17 | omar@potentialhealth.io 18 | and we'll get you sorted. 19 |

20 |

21 | Keep in mind: It could take a few days to process refunds. 22 |

23 |
24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /apps/web/src/env.ts: -------------------------------------------------------------------------------- 1 | import { createEnv } from "@t3-oss/env-nextjs"; 2 | import { vercel } from "@t3-oss/env-nextjs/presets-zod"; 3 | import { z } from "zod"; 4 | 5 | export const env = createEnv({ 6 | extends: [vercel()], 7 | shared: { 8 | NODE_ENV: z 9 | .enum(["development", "production", "test"]) 10 | .default("development"), 11 | }, 12 | server: { 13 | BASE_URL: z.string(), 14 | EXPO_PUBLIC_BACKEND_URL: z.string(), 15 | AUTH_SECRET: z.string(), 16 | RESEND_API_KEY: z.string(), 17 | RESEND_AUDIENCE_ID: z.string(), 18 | }, 19 | client: { 20 | NEXT_PUBLIC_URL: z.string(), 21 | NEXT_PUBLIC_BACKEND_URL: z.string(), 22 | }, 23 | experimental__runtimeEnv: { 24 | NODE_ENV: process.env.NODE_ENV, 25 | NEXT_PUBLIC_URL: process.env.NEXT_PUBLIC_URL, 26 | NEXT_PUBLIC_BACKEND_URL: process.env.NEXT_PUBLIC_BACKEND_URL, 27 | }, 28 | skipValidation: 29 | !!process.env.CI || process.env.npm_lifecycle_event === "lint", 30 | }); 31 | -------------------------------------------------------------------------------- /apps/web/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /apps/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@potential/tsconfig/base.json", 3 | "compilerOptions": { 4 | "lib": ["ES2022", "dom", "dom.iterable"], 5 | "jsx": "preserve", 6 | "baseUrl": ".", 7 | "paths": { 8 | "~/*": ["./src/*"] 9 | }, 10 | "plugins": [{ "name": "next" }], 11 | "module": "esnext" 12 | }, 13 | "include": [".", ".next/types/**/*.ts"], 14 | "exclude": ["node_modules"] 15 | } 16 | -------------------------------------------------------------------------------- /apps/web/turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turborepo.org/schema.json", 3 | "extends": ["//"], 4 | "tasks": { 5 | "build": { 6 | "dependsOn": ["^build"], 7 | "outputs": [".next/**", "!.next/cache/**", "next-env.d.ts"] 8 | }, 9 | "dev": { 10 | "persistent": true 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/ai/eslint.config.js: -------------------------------------------------------------------------------- 1 | import baseConfig from "@potential/eslint-config/base"; 2 | import ownerIdColumnRule from "@potential/eslint-config/owneridcolumn"; 3 | 4 | /** @type {import('typescript-eslint').Config} */ 5 | export default [ 6 | { 7 | ignores: ["dist/**"], 8 | }, 9 | ...baseConfig, 10 | ...ownerIdColumnRule, 11 | ]; 12 | -------------------------------------------------------------------------------- /packages/ai/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@potential/ai", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "main": "./dist/index.js", 7 | "types": "./dist/index.d.ts", 8 | "exports": { 9 | ".": { 10 | "types": "./dist/index.d.ts", 11 | "import": "./dist/index.js" 12 | } 13 | }, 14 | "files": [ 15 | "dist" 16 | ], 17 | "scripts": { 18 | "build": "tsup", 19 | "dev": "tsup --watch", 20 | "clean": "git clean -xdf .cache .turbo dist node_modules", 21 | "format": "prettier --check . --ignore-path ../../.gitignore", 22 | "lint": "eslint", 23 | "typecheck": "tsc --noEmit --emitDeclarationOnly false", 24 | "with-env": "dotenv -e ../../.env --" 25 | }, 26 | "dependencies": { 27 | "@ai-sdk/openai": "catalog:", 28 | "@potential/consts": "workspace:*", 29 | "@potential/db": "workspace:*", 30 | "@potential/env": "workspace:*", 31 | "@potential/templates": "workspace:*", 32 | "@potential/utils": "workspace:*", 33 | "ai": "catalog:", 34 | "zod": "catalog:" 35 | }, 36 | "devDependencies": { 37 | "@potential/eslint-config": "workspace:*", 38 | "@potential/prettier-config": "workspace:*", 39 | "@potential/tsconfig": "workspace:*", 40 | "dotenv-cli": "^8.0.0", 41 | "eslint": "catalog:", 42 | "prettier": "catalog:", 43 | "typescript": "catalog:" 44 | }, 45 | "prettier": "@potential/prettier-config" 46 | } -------------------------------------------------------------------------------- /packages/ai/src/calls/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./newTrackableChat"; 2 | -------------------------------------------------------------------------------- /packages/ai/src/calls/newTrackableChat.ts: -------------------------------------------------------------------------------- 1 | import type { CoreMessage } from "ai"; 2 | import { openai } from "@ai-sdk/openai"; 3 | import { streamText } from "ai"; 4 | 5 | import type { CloudTypeId } from "@potential/utils"; 6 | 7 | import { PROMPTS } from "../prompts"; 8 | import { 9 | generateConsumptionTrackables, 10 | generateNewNonConsumptionTrackable, 11 | getUserTrackablesWithLastLogTimestamp, 12 | } from "../tools"; 13 | 14 | interface CreateNewTrackableChatStreamParams { 15 | messages: CoreMessage[]; 16 | userId: CloudTypeId<"user">; 17 | } 18 | 19 | export const createNewTrackableChatStream = ({ 20 | messages, 21 | userId, 22 | }: CreateNewTrackableChatStreamParams) => 23 | streamText({ 24 | model: openai("gpt-4o-mini"), 25 | maxSteps: 10, 26 | toolCallStreaming: true, 27 | messages, 28 | system: PROMPTS.CHAT.SYSTEM, 29 | tools: { 30 | getUserExistingTrackables: getUserTrackablesWithLastLogTimestamp({ 31 | userId: userId, 32 | }), 33 | generateNewNonConsumptionTrackable: generateNewNonConsumptionTrackable(), 34 | generateConsumptionTrackables: generateConsumptionTrackables({ 35 | userId: userId, 36 | }), 37 | }, 38 | }); 39 | -------------------------------------------------------------------------------- /packages/ai/src/functions/createConsumptionTrackables.ts: -------------------------------------------------------------------------------- 1 | import type { InferInsertModel } from "@potential/db"; 2 | import type { CloudTypeId } from "@potential/utils"; 3 | import { and, db, eq, trackables } from "@potential/db"; 4 | import { TRACKABLE_TEMPLATES } from "@potential/templates"; 5 | import { cloudTypeIdGenerator } from "@potential/utils"; 6 | 7 | export async function createConsumptionTrackables({ 8 | userId, 9 | }: { 10 | userId: CloudTypeId<"user">; 11 | }) { 12 | console.log("🍕 CREATING CONSUMPTION TRACKABLES FUNCTION"); 13 | const userTrackables = await db.query.trackables.findMany({ 14 | where: and( 15 | eq(trackables.ownerId, userId), 16 | eq(trackables.type, "consumption"), 17 | ), 18 | columns: { 19 | id: true, 20 | }, 21 | }); 22 | if (userTrackables.length > 0) { 23 | return; 24 | } 25 | 26 | const templates = TRACKABLE_TEMPLATES.consumption; 27 | 28 | const newTrackablesToInsertArray: InferInsertModel[] = []; 29 | for (const template of Object.values(templates)) { 30 | newTrackablesToInsertArray.push({ 31 | id: cloudTypeIdGenerator("trackable"), 32 | ownerId: userId, 33 | name: template.name, 34 | description: template.description, 35 | color: null, 36 | configType: template.defaultConfig.type, 37 | public: false, 38 | type: "consumption", 39 | subType: template.subType, 40 | customConfig: template.defaultConfig, 41 | }); 42 | } 43 | console.log("🍕 INSERTING CONSUMPTION TRACKABLES", { 44 | newTrackablesToInsertArray, 45 | }); 46 | await db.insert(trackables).values(newTrackablesToInsertArray); 47 | 48 | return; 49 | } 50 | -------------------------------------------------------------------------------- /packages/ai/src/functions/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./createConsumptionTrackables"; 2 | -------------------------------------------------------------------------------- /packages/ai/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./calls"; 2 | export * from "./functions"; 3 | export * from "./prompts"; 4 | export * from "./tools"; 5 | -------------------------------------------------------------------------------- /packages/ai/src/tools/generateConsumptionTrackables.ts: -------------------------------------------------------------------------------- 1 | import { tool } from "ai"; 2 | import { z } from "zod"; 3 | 4 | import type { CloudTypeId } from "@potential/utils"; 5 | 6 | import { createConsumptionTrackables } from "../functions"; 7 | 8 | export const generateConsumptionTrackables = ({ 9 | userId, 10 | }: { 11 | userId: CloudTypeId<"user">; 12 | }) => 13 | tool({ 14 | description: 15 | "Generate a list of trackable items related to food and drink consumption. This is a special tool that should only be called if the user does not already have trackables of type 'consumption'.", 16 | parameters: z.object({}), 17 | execute: async () => { 18 | console.log("🍕 GENERATE CONSUMPTION TRACKABLES"); 19 | await createConsumptionTrackables({ userId }); 20 | return { completed: true }; 21 | }, 22 | }); 23 | -------------------------------------------------------------------------------- /packages/ai/src/tools/generateNewNonConsumptionTrackable.ts: -------------------------------------------------------------------------------- 1 | import { openai } from "@ai-sdk/openai"; 2 | import { generateObject, tool } from "ai"; 3 | import { z } from "zod"; 4 | 5 | import { PROMPTS } from "../prompts"; 6 | 7 | export const generateNewNonConsumptionTrackable = () => 8 | tool({ 9 | description: 10 | "Generate a new trackable data schema for the user. DO NOT CALL THIS TOOL IF YOU WANT TO GENERATE TRACKABLES RELATED TO CONSUMPTION. INSTEAD CALL generateConsumptionTrackables TOOL.", 11 | parameters: z.object({ 12 | description: z 13 | .string() 14 | .describe( 15 | "A description of the trackable to generate with a single goal in mind.", 16 | ), 17 | }), 18 | execute: async ({ description }: { description: string }) => { 19 | const { object: newTrackable } = await generateObject({ 20 | // issue with thinking models and optional fields in structured output: https://github.com/vercel/ai/issues/4662 21 | model: openai("gpt-4o-mini"), 22 | output: "array", 23 | schema: 24 | PROMPTS.TOOLS.GENERATE_NEW_NON_CONSUMPTION_TRACKABLE.EXECUTE.PROMPT 25 | .SCHEMA, 26 | system: 27 | PROMPTS.TOOLS.GENERATE_NEW_NON_CONSUMPTION_TRACKABLE.EXECUTE.PROMPT 28 | .SYSTEM, 29 | prompt: description, 30 | }); 31 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 32 | (newTrackable as any[]).forEach((trackable: any) => { 33 | console.log("TRACKABLE", { 34 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 35 | trackable, 36 | }); 37 | }); 38 | return { trackable: newTrackable }; 39 | }, 40 | }); 41 | -------------------------------------------------------------------------------- /packages/ai/src/tools/getUserTrackables.ts: -------------------------------------------------------------------------------- 1 | import { tool } from "ai"; 2 | import { z } from "zod"; 3 | 4 | import type { CloudTypeId } from "@potential/utils"; 5 | import { db, desc, eq, trackableLogs, trackables } from "@potential/db"; 6 | 7 | export const getUserTrackablesWithLastLogTimestamp = ({ 8 | userId, 9 | }: { 10 | userId: CloudTypeId<"user">; 11 | }) => 12 | tool({ 13 | description: 14 | "Get a list of existing data items the user is already tracking and the last time they entered data for it.", 15 | parameters: z.object({}), 16 | execute: async () => { 17 | const userTrackables = await db.query.trackables.findMany({ 18 | where: eq(trackables.ownerId, userId), 19 | 20 | columns: { 21 | id: true, 22 | name: true, 23 | description: true, 24 | type: true, 25 | subType: true, 26 | subTypeCustomName: true, 27 | }, 28 | with: { 29 | logs: { 30 | limit: 1, 31 | orderBy: desc(trackableLogs.createdAt), 32 | columns: { 33 | createdAt: true, 34 | }, 35 | }, 36 | }, 37 | }); 38 | const consumptionTrackables = userTrackables.filter( 39 | (trackable) => trackable.type === "consumption", 40 | ); 41 | const nonConsumptionTrackables = userTrackables.filter( 42 | (trackable) => trackable.type !== "consumption", 43 | ); 44 | return { 45 | trackables: nonConsumptionTrackables, 46 | hasConsumptionTrackables: consumptionTrackables.length > 0, 47 | }; 48 | }, 49 | }); 50 | -------------------------------------------------------------------------------- /packages/ai/src/tools/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./generateConsumptionTrackables"; 2 | export * from "./generateNewNonConsumptionTrackable"; 3 | export * from "./getUserTrackables"; 4 | -------------------------------------------------------------------------------- /packages/ai/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@potential/tsconfig/internal-package.json", 3 | "compilerOptions": { 4 | "emitDeclarationOnly": false, 5 | "rootDir": "src" 6 | }, 7 | "include": ["src"], 8 | "exclude": ["node_modules", "dist"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/ai/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | entry: ["src/index.ts", "src/client.ts", "src/schema.ts"], 5 | outDir: "dist", 6 | platform: "node", 7 | format: ["esm"], 8 | external: [ 9 | "@ai-sdk/openai", 10 | "@potential/db", 11 | "@potential/consts", 12 | "@potential/env", 13 | "@potential/templates", 14 | "@potential/utils", 15 | "ai", 16 | "zod", 17 | ], 18 | dts: true, 19 | splitting: false, 20 | sourcemap: true, 21 | clean: false, 22 | bundle: true, 23 | }); 24 | -------------------------------------------------------------------------------- /packages/auth/eslint.config.js: -------------------------------------------------------------------------------- 1 | import baseConfig from "@potential/eslint-config/base"; 2 | 3 | /** @type {import('typescript-eslint').Config} */ 4 | export default [ 5 | { 6 | ignores: ["dist/**"], 7 | }, 8 | ...baseConfig, 9 | ]; 10 | -------------------------------------------------------------------------------- /packages/auth/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@potential/auth", 3 | "private": true, 4 | "type": "module", 5 | "main": "./dist/index.js", 6 | "module": "./dist/index.js", 7 | "types": "./dist/index.d.ts", 8 | "exports": { 9 | ".": { 10 | "types": "./dist/index.d.ts", 11 | "import": "./dist/index.js" 12 | } 13 | }, 14 | "files": [ 15 | "dist" 16 | ], 17 | "scripts": { 18 | "build": "tsup", 19 | "dev": "tsup --watch", 20 | "clean": "git clean -xdf .cache .turbo dist node_modules", 21 | "check": "tsc --noEmit", 22 | "auth:generate": "drizzle-kit push" 23 | }, 24 | "dependencies": { 25 | "@better-auth/expo": "catalog:", 26 | "@potential/consts": "workspace:*", 27 | "@potential/db": "workspace:*", 28 | "@potential/email": "workspace:*", 29 | "@potential/env": "workspace:*", 30 | "@potential/utils": "workspace:*", 31 | "@potential/validators": "workspace:*", 32 | "better-auth": "catalog:", 33 | "dotenv": "^16.5.0", 34 | "zod": "catalog:" 35 | }, 36 | "peerDependencies": { 37 | "@react-email/components": "^0.0.38", 38 | "react": "catalog:react19", 39 | "react-dom": "catalog:react19", 40 | "resend": "^4.5.0" 41 | }, 42 | "devDependencies": { 43 | "@potential/prettier-config": "workspace:*", 44 | "@potential/tsconfig": "workspace:*", 45 | "eslint": "catalog:", 46 | "prettier": "catalog:", 47 | "tsup": "^8.2.4", 48 | "tsx": "^4.19.4", 49 | "typescript": "catalog:" 50 | }, 51 | "prettier": "@potential/prettier-config" 52 | } -------------------------------------------------------------------------------- /packages/auth/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@potential/tsconfig/internal-package.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | "module": "ESNext", 7 | "target": "ESNext", 8 | "moduleResolution": "bundler", 9 | "allowImportingTsExtensions": true, 10 | "isolatedModules": true, 11 | "jsx": "react-jsx", 12 | "strict": true 13 | }, 14 | "include": ["src"], 15 | "exclude": ["node_modules", "dist"] 16 | } 17 | -------------------------------------------------------------------------------- /packages/auth/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | entry: ["src/index.ts"], 5 | outDir: "dist", 6 | platform: "node", // Auth logic is typically server-side or neutral 7 | format: ["esm"], 8 | external: [ 9 | "@better-auth/expo", 10 | "@potential/consts", 11 | "@potential/db", 12 | "@potential/email", 13 | "@potential/env", 14 | "@potential/utils", 15 | "@potential/validators", 16 | "better-auth", 17 | "dotenv", 18 | "zod", 19 | ], 20 | dts: true, 21 | splitting: false, 22 | sourcemap: true, 23 | clean: false, 24 | bundle: true, 25 | minify: false, 26 | treeshake: true, 27 | }); 28 | -------------------------------------------------------------------------------- /packages/consts/eslint.config.js: -------------------------------------------------------------------------------- 1 | import baseConfig from "@potential/eslint-config/base"; 2 | import typeidConfig from "@potential/eslint-config/typeid"; 3 | 4 | /** @type {import('typescript-eslint').Config} */ 5 | export default [ 6 | { 7 | ignores: ["dist/**"], 8 | }, 9 | ...baseConfig, 10 | ...typeidConfig, 11 | ]; 12 | -------------------------------------------------------------------------------- /packages/consts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@potential/consts", 3 | "private": true, 4 | "version": "0.3.0", 5 | "main": "./dist/index.js", 6 | "module": "./dist/index.mjs", 7 | "react-native": "./dist/index.js", 8 | "types": "./dist/index.d.ts", 9 | "exports": { 10 | ".": { 11 | "types": "./dist/index.d.ts", 12 | "require": "./dist/index.js", 13 | "import": "./dist/index.mjs" 14 | } 15 | }, 16 | "files": [ 17 | "dist" 18 | ], 19 | "scripts": { 20 | "build": "tsup", 21 | "dev": "tsup --watch", 22 | "prepare": "pnpm exec tsup", 23 | "clean": "git clean -xdf .cache .turbo dist node_modules", 24 | "typecheck": "tsc --noEmit" 25 | }, 26 | "dependencies": { 27 | "@potential/tsconfig": "workspace:*", 28 | "tsup": "^8.2.4", 29 | "zod": "catalog:" 30 | }, 31 | "devDependencies": { 32 | "@potential/prettier-config": "workspace:*", 33 | "@potential/utils": "workspace:*", 34 | "eslint": "catalog:", 35 | "prettier": "catalog:", 36 | "typescript": "catalog:" 37 | }, 38 | "prettier": "@potential/prettier-config" 39 | } -------------------------------------------------------------------------------- /packages/consts/src/auth.ts: -------------------------------------------------------------------------------- 1 | export enum UserRoles { 2 | SUPER_ADMIN = "super_admin", 3 | ADMIN = "admin", 4 | COMMUNITY_ADMIN = "community_admin", 5 | CREATOR = "creator", 6 | USER = "user", 7 | } 8 | -------------------------------------------------------------------------------- /packages/consts/src/colors.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const COLORS = { 4 | bronze: "Bronze", 5 | gold: "Gold", 6 | brown: "Brown", 7 | orange: "Orange", 8 | tomato: "Tomato", 9 | red: "Red", 10 | ruby: "Ruby", 11 | crimson: "Crimson", 12 | pink: "Pink", 13 | plum: "Plum", 14 | purple: "Purple", 15 | violet: "Violet", 16 | iris: "Iris", 17 | indigo: "Indigo", 18 | blue: "Blue", 19 | cyan: "Cyan", 20 | teal: "Teal", 21 | jade: "Jade", 22 | green: "Green", 23 | grass: "Grass", 24 | } as const; 25 | 26 | export type ColorsMap = typeof COLORS; 27 | export type ColorsKey = keyof ColorsMap; 28 | export type ColorsValues = ColorsMap[ColorsKey]; 29 | 30 | export const colorsArray = Object.values(COLORS); 31 | export const colorsSchema = z 32 | .enum(Object.keys(COLORS) as [ColorsKey, ...ColorsKey[]]) 33 | .describe( 34 | "The color of the trackable item. This might change the color of the trackable item in the UI. Possible values are:" + 35 | "Bronze" + 36 | " " + 37 | "Gold" + 38 | " " + 39 | "Brown" + 40 | " " + 41 | "Orange" + 42 | " " + 43 | "Tomato" + 44 | " " + 45 | "Red" + 46 | " " + 47 | "Ruby" + 48 | " " + 49 | "Crimson" + 50 | " " + 51 | "Pink" + 52 | " " + 53 | "Plum" + 54 | " " + 55 | "Purple" + 56 | " " + 57 | "Violet" + 58 | " " + 59 | "Iris" + 60 | " " + 61 | "Indigo" + 62 | " " + 63 | "Blue" + 64 | " " + 65 | "Cyan" + 66 | " " + 67 | "Teal" + 68 | " " + 69 | "Jade" + 70 | " " + 71 | "Green" + 72 | " " + 73 | "Grass", 74 | ); 75 | export type ColorsSchema = z.infer; 76 | 77 | export function getColorDisplayValue(key: ColorsKey): string { 78 | return COLORS[key]; 79 | } 80 | 81 | export function getRandomColor(): ColorsKey { 82 | const keys = Object.keys(COLORS) as ColorsKey[]; 83 | return keys[Math.floor(Math.random() * keys.length)] ?? "red"; 84 | } 85 | -------------------------------------------------------------------------------- /packages/consts/src/index.ts: -------------------------------------------------------------------------------- 1 | export { CONSTS } from "./base"; 2 | export type { ConstsTypes } from "./base"; 3 | export { PROMPTS } from "./prompts"; 4 | export type { TrackableCustomConfig } from "./trackables"; 5 | -------------------------------------------------------------------------------- /packages/consts/src/integrations.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const INTEGRATIONS = { 4 | "1up_AIR": "1upAir", 5 | oura: "Oura Ring", 6 | ultrahuman: "Ultrahuman Ring", 7 | whoop: "Whoop Band", 8 | } as const; 9 | 10 | export type IntegrationsMap = typeof INTEGRATIONS; 11 | export type IntegrationsKey = keyof IntegrationsMap; 12 | export type IntegrationsValues = IntegrationsMap[IntegrationsKey]; 13 | 14 | export const integrationsSchema = z.enum( 15 | Object.keys(INTEGRATIONS) as [IntegrationsKey, ...IntegrationsKey[]], 16 | ); 17 | export type IntegrationsSchema = z.infer; 18 | 19 | export function getIntegrationDisplayValue(key: IntegrationsKey): string { 20 | return INTEGRATIONS[key]; 21 | } 22 | 23 | export const INTEGRATION_ACCESS_MODE = { 24 | local: "Local Device", 25 | api: "API", 26 | webhook: "Webhook", 27 | manual: "Manual", 28 | } as const; 29 | 30 | export type IntegrationAccessModeMap = typeof INTEGRATION_ACCESS_MODE; 31 | export type IntegrationAccessModeKey = keyof IntegrationAccessModeMap; 32 | export type IntegrationAccessModeValues = 33 | IntegrationAccessModeMap[IntegrationAccessModeKey]; 34 | 35 | export const integrationAccessModeSchema = z.enum( 36 | Object.keys(INTEGRATION_ACCESS_MODE) as [ 37 | IntegrationAccessModeKey, 38 | ...IntegrationAccessModeKey[], 39 | ], 40 | ); 41 | export type IntegrationAccessModeSchema = z.infer< 42 | typeof integrationAccessModeSchema 43 | >; 44 | 45 | export function getIntegrationAccessModeDisplayValue( 46 | key: IntegrationAccessModeKey, 47 | ): string { 48 | return INTEGRATION_ACCESS_MODE[key]; 49 | } 50 | -------------------------------------------------------------------------------- /packages/consts/src/users.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const GENDER_AT_BIRTH = { 4 | male: "Male", 5 | female: "Female", 6 | other: "Other", 7 | } as const; 8 | 9 | export type GenderAtBirthMap = typeof GENDER_AT_BIRTH; 10 | export type GenderAtBirthKey = keyof GenderAtBirthMap; 11 | export type GenderAtBirthValues = GenderAtBirthMap[GenderAtBirthKey]; 12 | 13 | export const genderAtBirthSchema = z.enum( 14 | Object.keys(GENDER_AT_BIRTH) as [GenderAtBirthKey, ...GenderAtBirthKey[]], 15 | ); 16 | export type GenderAtBirthSchema = z.infer; 17 | 18 | export function getGenderAtBirthDisplayValue(key: GenderAtBirthKey): string { 19 | return GENDER_AT_BIRTH[key]; 20 | } 21 | -------------------------------------------------------------------------------- /packages/consts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@potential/tsconfig/internal-package.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | "module": "ESNext", 7 | "target": "ESNext", 8 | "moduleResolution": "bundler", 9 | "isolatedModules": true, 10 | "strict": true 11 | }, 12 | "include": ["src"], 13 | "exclude": ["node_modules", "dist"] 14 | } 15 | -------------------------------------------------------------------------------- /packages/consts/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | entry: ["src/index.ts"], 5 | format: ["cjs", "esm"], 6 | dts: true, 7 | clean: false, 8 | splitting: false, 9 | sourcemap: true, 10 | minify: false, 11 | treeshake: true, 12 | }); 13 | -------------------------------------------------------------------------------- /packages/db/drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "drizzle-kit"; 2 | 3 | if (!process.env.DB_MIGRATION_URL) { 4 | throw new Error("Missing DB_MIGRATION_URL"); 5 | } 6 | 7 | export default { 8 | schema: "./src/schema/", 9 | dialect: "mysql", 10 | dbCredentials: { url: process.env.DB_MIGRATION_URL }, 11 | casing: "snake_case", 12 | } satisfies Config; 13 | -------------------------------------------------------------------------------- /packages/db/eslint.config.js: -------------------------------------------------------------------------------- 1 | import baseConfig from "@potential/eslint-config/base"; 2 | import ownerIdColumnRule from "@potential/eslint-config/owneridcolumn"; 3 | 4 | /** @type {import('typescript-eslint').Config} */ 5 | export default [ 6 | { 7 | ignores: ["dist/**"], 8 | }, 9 | ...baseConfig, 10 | ...ownerIdColumnRule, 11 | ]; 12 | -------------------------------------------------------------------------------- /packages/db/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@potential/db", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "main": "./dist/index.js", 7 | "types": "./dist/index.d.ts", 8 | "exports": { 9 | ".": { 10 | "types": "./dist/index.d.ts", 11 | "import": "./dist/index.js" 12 | }, 13 | "./client": { 14 | "types": "./dist/client.d.ts", 15 | "import": "./dist/client.js" 16 | }, 17 | "./schema": { 18 | "types": "./dist/schema.d.ts", 19 | "import": "./dist/schema.js" 20 | } 21 | }, 22 | "files": [ 23 | "dist" 24 | ], 25 | "scripts": { 26 | "build": "tsup", 27 | "dev": "tsup --watch", 28 | "clean": "git clean -xdf .cache .turbo dist node_modules", 29 | "format": "prettier --check . --ignore-path ../../.gitignore", 30 | "lint": "eslint", 31 | "push": "dotenv -e ../../.env -- drizzle-kit push", 32 | "studio": "pnpm with-env drizzle-kit studio", 33 | "typecheck": "tsc --noEmit --emitDeclarationOnly false", 34 | "with-env": "dotenv -e ../../.env --" 35 | }, 36 | "dependencies": { 37 | "@planetscale/database": "^1.19.0", 38 | "@potential/consts": "workspace:*", 39 | "@potential/env": "workspace:*", 40 | "@potential/utils": "workspace:*", 41 | "drizzle-orm": "catalog:", 42 | "drizzle-zod": "catalog:", 43 | "zod": "catalog:" 44 | }, 45 | "devDependencies": { 46 | "@potential/eslint-config": "workspace:*", 47 | "@potential/prettier-config": "workspace:*", 48 | "@potential/tsconfig": "workspace:*", 49 | "dotenv-cli": "^8.0.0", 50 | "drizzle-kit": "catalog:", 51 | "eslint": "catalog:", 52 | "prettier": "catalog:", 53 | "typescript": "catalog:" 54 | }, 55 | "prettier": "@potential/prettier-config" 56 | } -------------------------------------------------------------------------------- /packages/db/src/client.ts: -------------------------------------------------------------------------------- 1 | import { Client } from "@planetscale/database"; 2 | import { drizzle } from "drizzle-orm/planetscale-serverless"; 3 | 4 | import { serverEnv } from "@potential/env"; 5 | 6 | import * as schema from "./schema"; 7 | 8 | const host = serverEnv.database.DB_HOST; 9 | const username = serverEnv.database.DB_USERNAME; 10 | const password = serverEnv.database.DB_PASSWORD; 11 | const database = serverEnv.database.DB_DATABASE; 12 | 13 | if (!host || !username || !password || !database) { 14 | throw new Error("Missing DB_HOST, DB_USERNAME, DB_PASSWORD, or DB_DATABASE"); 15 | } 16 | 17 | const client = new Client({ 18 | host, 19 | username, 20 | password, 21 | fetch: (url, init) => { 22 | if (init) { 23 | delete init.cache; 24 | } 25 | const u = new URL(url); 26 | // set protocol to http if localhost for CI testing 27 | if (u.host.includes("localhost") || u.host.includes("127.0.0.1")) { 28 | u.protocol = "http"; 29 | } 30 | return fetch(u, init); 31 | }, 32 | }); 33 | 34 | export const db = drizzle({ 35 | client, 36 | schema, 37 | }); 38 | -------------------------------------------------------------------------------- /packages/db/src/columns/custom/color.ts: -------------------------------------------------------------------------------- 1 | import type { SQL } from "drizzle-orm"; 2 | import { sql } from "drizzle-orm"; 3 | import { customType } from "drizzle-orm/mysql-core"; 4 | 5 | import type { ConstsTypes } from "@potential/consts"; 6 | import { CONSTS } from "@potential/consts"; 7 | 8 | // Colors 9 | export const colorsColumn = (columnName: string) => 10 | customType<{ 11 | data: ConstsTypes["COLORS"]["TYPES"]["KEY"]; 12 | driverData: string; 13 | }>({ 14 | dataType() { 15 | return "varchar(64)"; 16 | }, 17 | fromDriver(value: string): ConstsTypes["COLORS"]["TYPES"]["KEY"] { 18 | if (!(value in CONSTS.COLORS.TYPES)) { 19 | throw new Error(`Invalid color key: ${value}`); 20 | } 21 | return value as ConstsTypes["COLORS"]["TYPES"]["KEY"]; 22 | }, 23 | toDriver(value: ConstsTypes["COLORS"]["TYPES"]["KEY"]): SQL { 24 | return sql.raw(`'${value}'`); 25 | }, 26 | })(columnName); 27 | -------------------------------------------------------------------------------- /packages/db/src/columns/custom/integration.ts: -------------------------------------------------------------------------------- 1 | import type { SQL } from "drizzle-orm"; 2 | import { sql } from "drizzle-orm"; 3 | import { customType } from "drizzle-orm/mysql-core"; 4 | 5 | import type { ConstsTypes } from "@potential/consts"; 6 | import { CONSTS } from "@potential/consts"; 7 | 8 | // Integrations 9 | 10 | export const integrationColumn = (columnName: string) => 11 | customType<{ 12 | data: ConstsTypes["INTEGRATIONS"]["TYPES"]["KEY"]; 13 | driverData: string; 14 | }>({ 15 | dataType() { 16 | return "varchar(64)"; 17 | }, 18 | fromDriver(value: string): ConstsTypes["INTEGRATIONS"]["TYPES"]["KEY"] { 19 | if (!(value in CONSTS.INTEGRATIONS.TYPES)) { 20 | throw new Error(`Invalid integration type key: ${value}`); 21 | } 22 | return value as ConstsTypes["INTEGRATIONS"]["TYPES"]["KEY"]; 23 | }, 24 | toDriver(value: ConstsTypes["INTEGRATIONS"]["TYPES"]["KEY"]): SQL { 25 | return sql.raw(`'${value}'`); 26 | }, 27 | })(columnName); 28 | 29 | // Integration Access Mode 30 | export const integrationAccessModeColumn = (columnName: string) => 31 | customType<{ 32 | data: ConstsTypes["INTEGRATIONS"]["ACCESS_MODE"]["KEY"]; 33 | driverData: string; 34 | }>({ 35 | dataType() { 36 | return "varchar(64)"; 37 | }, 38 | fromDriver( 39 | value: string, 40 | ): ConstsTypes["INTEGRATIONS"]["ACCESS_MODE"]["KEY"] { 41 | if (!(value in CONSTS.INTEGRATIONS.ACCESS_MODE)) { 42 | throw new Error(`Invalid integration access mode key: ${value}`); 43 | } 44 | return value as ConstsTypes["INTEGRATIONS"]["ACCESS_MODE"]["KEY"]; 45 | }, 46 | toDriver( 47 | value: ConstsTypes["INTEGRATIONS"]["ACCESS_MODE"]["KEY"], 48 | ): SQL { 49 | return sql.raw(`'${value}'`); 50 | }, 51 | })(columnName); 52 | -------------------------------------------------------------------------------- /packages/db/src/columns/custom/typeId.ts: -------------------------------------------------------------------------------- 1 | import type { SQL } from "drizzle-orm"; 2 | import { sql } from "drizzle-orm"; 3 | import { customType } from "drizzle-orm/mysql-core"; 4 | 5 | import type { CloudIdTypePrefixNames, CloudTypeId } from "@potential/utils"; 6 | import { 7 | cloudTypeIdFromUUIDBytes, 8 | cloudTypeIdToUUIDBytes, 9 | } from "@potential/utils"; 10 | 11 | // TypeId Column 12 | function bytesToHex(bytes: Uint8Array): string { 13 | let hex = ""; 14 | for (const byte of bytes) { 15 | hex += ("0" + (byte & 0xff).toString(16)).slice(-2); 16 | } 17 | return hex; 18 | } 19 | 20 | export const typeIdColumn = ( 21 | typeName: T, 22 | columnName: string, 23 | ) => 24 | customType<{ data: CloudTypeId; driverData: string }>({ 25 | dataType() { 26 | return "binary(16)"; 27 | }, 28 | fromDriver(input: string): CloudTypeId { 29 | const typedId = cloudTypeIdFromUUIDBytes( 30 | typeName, 31 | new Uint8Array(input as unknown as ArrayBuffer), 32 | ); 33 | return typedId as CloudTypeId; 34 | }, 35 | toDriver(input: CloudTypeId): SQL { 36 | const buffer = new Uint8Array(cloudTypeIdToUUIDBytes(input).uuid); 37 | const hex = bytesToHex(buffer); 38 | return sql.raw(`x'${hex}'`); 39 | }, 40 | })(columnName); 41 | -------------------------------------------------------------------------------- /packages/db/src/columns/custom/user.ts: -------------------------------------------------------------------------------- 1 | import type { ConstsTypes } from "@potential/consts"; 2 | import type { SQL } from "drizzle-orm"; 3 | import { CONSTS } from "@potential/consts"; 4 | import { sql } from "drizzle-orm"; 5 | import { customType } from "drizzle-orm/mysql-core"; 6 | 7 | // Colors 8 | export const genderAtBirthColumn = (columnName: string) => 9 | customType<{ 10 | data: ConstsTypes["USERS"]["GENDER_AT_BIRTH"]["KEY"]; 11 | driverData: string; 12 | }>({ 13 | dataType() { 14 | return "varchar(64)"; 15 | }, 16 | fromDriver(value: string): ConstsTypes["USERS"]["GENDER_AT_BIRTH"]["KEY"] { 17 | if (!(value in CONSTS.USERS.GENDER_AT_BIRTH)) { 18 | throw new Error(`Invalid gender at birth key: ${value}`); 19 | } 20 | return value as ConstsTypes["USERS"]["GENDER_AT_BIRTH"]["KEY"]; 21 | }, 22 | toDriver( 23 | value: ConstsTypes["USERS"]["GENDER_AT_BIRTH"]["KEY"], 24 | ): SQL { 25 | return sql.raw(`'${value}'`); 26 | }, 27 | })(columnName); 28 | -------------------------------------------------------------------------------- /packages/db/src/columns/timestamps.ts: -------------------------------------------------------------------------------- 1 | import { timestamp } from "drizzle-orm/mysql-core"; 2 | 3 | export const timestamps = { 4 | create: { 5 | createdAt: timestamp("createdAt").notNull().defaultNow(), 6 | }, 7 | createUpdate: { 8 | createdAt: timestamp("createdAt").notNull().defaultNow(), 9 | updatedAt: timestamp("updatedAt").notNull().defaultNow().onUpdateNow(), 10 | }, 11 | createUpdateLogged: { 12 | createdAt: timestamp("createdAt").notNull().defaultNow(), 13 | updatedAt: timestamp("updatedAt").notNull().defaultNow().onUpdateNow(), 14 | loggedAt: timestamp("loggedAt").notNull().defaultNow(), 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /packages/db/src/index.ts: -------------------------------------------------------------------------------- 1 | export type { InferInsertModel, InferSelectModel } from "drizzle-orm"; 2 | export { alias } from "drizzle-orm/mysql-core"; 3 | export * from "drizzle-orm/sql"; 4 | export * from "./client"; 5 | export * from "./schema"; 6 | -------------------------------------------------------------------------------- /packages/db/src/schema.ts: -------------------------------------------------------------------------------- 1 | export * from "./schema/aiLogs"; 2 | export * from "./schema/auth"; 3 | export * from "./schema/ingredients"; 4 | export * from "./schema/integrations"; 5 | export * from "./schema/logs"; 6 | export * from "./schema/users"; 7 | -------------------------------------------------------------------------------- /packages/db/src/schema/aiLogs.ts: -------------------------------------------------------------------------------- 1 | import { relations } from "drizzle-orm"; 2 | import { json, mysqlEnum, mysqlTable, text } from "drizzle-orm/mysql-core"; 3 | 4 | import type { CloudTypeId } from "@potential/utils/typeid"; 5 | import { cloudTypeIdGenerator } from "@potential/utils/typeid"; 6 | 7 | import { typeIdColumn } from "../columns/custom/typeId"; 8 | import { timestamps } from "../columns/timestamps"; 9 | import { users } from "./auth"; 10 | import { trackableLogs } from "./logs"; 11 | 12 | export const aiInputLogs = mysqlTable("ai_input_logs", { 13 | id: typeIdColumn("aiInputLog", "id") 14 | .primaryKey() 15 | .$default(() => cloudTypeIdGenerator("aiInputLog")), 16 | ownerId: typeIdColumn("user", "user_id").notNull(), 17 | inputType: mysqlEnum("inputType", ["text", "voice", "image"]).notNull(), 18 | text: text("text"), 19 | voiceBlob: json("imageIds").$type[]>(), 20 | imageIds: json("imageIds").$type[]>(), 21 | resultAiLog: text("resultAiLog"), 22 | resultFriendlyText: text("resultFriendlyText"), 23 | resultLogId: typeIdColumn("trackableLog", "trackableLogId"), 24 | ...timestamps.createUpdateLogged, 25 | }); 26 | 27 | export const aiInputLogsRelations = relations(aiInputLogs, ({ one }) => ({ 28 | owner: one(users, { 29 | fields: [aiInputLogs.ownerId], 30 | references: [users.id], 31 | }), 32 | resultLog: one(trackableLogs, { 33 | fields: [aiInputLogs.resultLogId], 34 | references: [trackableLogs.id], 35 | }), 36 | })); 37 | -------------------------------------------------------------------------------- /packages/db/src/schema/ingredients.ts: -------------------------------------------------------------------------------- 1 | import { relations } from "drizzle-orm"; 2 | import { mysqlTable, text, varchar } from "drizzle-orm/mysql-core"; 3 | 4 | import { cloudTypeIdGenerator } from "@potential/utils/typeid"; 5 | 6 | import { typeIdColumn } from "../columns/custom/typeId"; 7 | import { timestamps } from "../columns/timestamps"; 8 | import { users } from "./auth"; 9 | import { trackableLogs } from "./logs"; 10 | 11 | // Ingredient library 12 | export const ingredients = mysqlTable("ingredients", { 13 | id: typeIdColumn("ingredientLibrary", "id") 14 | .primaryKey() 15 | .$default(() => cloudTypeIdGenerator("ingredientLibrary")), 16 | ownerId: typeIdColumn("user", "user_id").notNull(), 17 | name: varchar("name", { length: 128 }).notNull(), 18 | description: text("description"), 19 | ...timestamps.createUpdate, 20 | }); 21 | 22 | export const ingredientsRelations = relations(ingredients, ({ one, many }) => ({ 23 | owner: one(users, { 24 | fields: [ingredients.ownerId], 25 | references: [users.id], 26 | }), 27 | logs: many(ingredientLogs), 28 | })); 29 | 30 | export const ingredientLogs = mysqlTable("ingredientLogs", { 31 | id: typeIdColumn("ingredientLog", "id") 32 | .primaryKey() 33 | .$default(() => cloudTypeIdGenerator("ingredientLog")), 34 | ingredientId: typeIdColumn("ingredientLibrary", "id").notNull(), 35 | logId: typeIdColumn("trackableLog", "id").notNull(), 36 | ownerId: typeIdColumn("user", "user_id").notNull(), 37 | ...timestamps.createUpdateLogged, 38 | }); 39 | 40 | export const ingredientLogsRelations = relations(ingredientLogs, ({ one }) => ({ 41 | ingredient: one(ingredients, { 42 | fields: [ingredientLogs.ingredientId], 43 | references: [ingredients.id], 44 | }), 45 | log: one(trackableLogs, { 46 | fields: [ingredientLogs.logId], 47 | references: [trackableLogs.id], 48 | }), 49 | })); 50 | -------------------------------------------------------------------------------- /packages/db/src/schema/integrations.ts: -------------------------------------------------------------------------------- 1 | import { relations } from "drizzle-orm"; 2 | import { json, mysqlTable, timestamp } from "drizzle-orm/mysql-core"; 3 | 4 | import { cloudTypeIdGenerator } from "@potential/utils/typeid"; 5 | 6 | import { 7 | integrationAccessModeColumn, 8 | integrationColumn, 9 | } from "../columns/custom/integration"; 10 | import { typeIdColumn } from "../columns/custom/typeId"; 11 | import { timestamps } from "../columns/timestamps"; 12 | import { users } from "./auth"; 13 | 14 | // No need to import "./user" as we already have users imported from "./auth" 15 | 16 | type IntegrationAccessData = Record[]; 17 | 18 | export const integrations = mysqlTable("integrations", { 19 | id: typeIdColumn("integration", "id") 20 | .primaryKey() 21 | .$default(() => cloudTypeIdGenerator("integration")), 22 | ownerId: typeIdColumn("user", "user_id").notNull(), 23 | type: integrationColumn("type"), 24 | accessMode: integrationAccessModeColumn("accessMode"), 25 | accessData: json("accessData") 26 | .$type() 27 | .default([ 28 | { 29 | accessToken: "", 30 | refreshToken: "", 31 | expiresAt: "", 32 | url: "", 33 | }, 34 | ]), 35 | lastSync: timestamp("last_sync"), 36 | ...timestamps.createUpdate, 37 | }); 38 | 39 | export const integrationsRelations = relations(integrations, ({ one }) => ({ 40 | owner: one(users, { 41 | fields: [integrations.ownerId], 42 | references: [users.id], 43 | }), 44 | })); 45 | -------------------------------------------------------------------------------- /packages/db/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@potential/tsconfig/internal-package.json", 3 | "compilerOptions": { 4 | "emitDeclarationOnly": false, 5 | "rootDir": "src" 6 | }, 7 | "include": ["src"], 8 | "exclude": ["node_modules", "dist"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/db/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | entry: ["src/index.ts", "src/client.ts", "src/schema.ts"], 5 | outDir: "dist", 6 | platform: "node", 7 | format: ["esm"], 8 | external: [ 9 | "drizzle-orm", 10 | "drizzle-zod", 11 | "@planetscale/database", 12 | "@potential/consts", 13 | "@potential/env", 14 | "@potential/utils", 15 | "zod", 16 | ], 17 | dts: true, 18 | splitting: false, 19 | sourcemap: true, 20 | clean: false, 21 | bundle: true, 22 | }); 23 | -------------------------------------------------------------------------------- /packages/email/eslint.config.js: -------------------------------------------------------------------------------- 1 | import baseConfig from "@potential/eslint-config/base"; 2 | 3 | /** @type {import('typescript-eslint').Config} */ 4 | export default [ 5 | { 6 | ignores: ["dist/**"], 7 | }, 8 | ...baseConfig, 9 | ]; 10 | -------------------------------------------------------------------------------- /packages/email/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@potential/email", 3 | "private": true, 4 | "type": "module", 5 | "main": "./dist/index.js", 6 | "module": "./dist/index.js", 7 | "types": "./dist/index.d.ts", 8 | "exports": { 9 | ".": { 10 | "types": "./dist/index.d.ts", 11 | "import": "./dist/index.js" 12 | }, 13 | "./emails/*": { 14 | "types": "./dist/emails/*.d.ts", 15 | "import": "./dist/emails/*.js" 16 | } 17 | }, 18 | "files": [ 19 | "dist" 20 | ], 21 | "scripts": { 22 | "build": "tsup", 23 | "dev": "tsup --watch", 24 | "check": "tsc --noEmit" 25 | }, 26 | "dependencies": { 27 | "@potential/consts": "workspace:*", 28 | "@potential/env": "workspace:*", 29 | "@react-email/components": "^0.0.38", 30 | "dotenv": "^16.5.0", 31 | "react": "catalog:react19", 32 | "react-dom": "catalog:react19", 33 | "resend": "^4.5.0" 34 | }, 35 | "peerDependencies": { 36 | "@react-email/components": "^0.0.38", 37 | "react": "catalog:react19", 38 | "react-dom": "catalog:react19", 39 | "resend": "^4.5.0" 40 | }, 41 | "devDependencies": { 42 | "@potential/prettier-config": "workspace:*", 43 | "@potential/tsconfig": "workspace:*", 44 | "eslint": "catalog:", 45 | "prettier": "catalog:", 46 | "react-email": "^4.0.11", 47 | "tsx": "^4.19.4", 48 | "typescript": "catalog:" 49 | }, 50 | "prettier": "@potential/prettier-config" 51 | } -------------------------------------------------------------------------------- /packages/email/src/emails/bug-report.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { 3 | Body, 4 | Container, 5 | Head, 6 | Html, 7 | Preview, 8 | Section, 9 | Tailwind, 10 | Text, 11 | } from "@react-email/components"; 12 | 13 | export const BugReportEmail = ({ 14 | message, 15 | userEmail, 16 | }: { 17 | message: string; 18 | userEmail: string; 19 | }) => { 20 | return ( 21 | 22 | 23 | Bug Report from {userEmail} 24 | 25 | 26 | 27 |
28 | 29 | Yo Omar! someone reported a bug on Potential Health 30 | 31 | {message} 32 |
33 |
34 | 35 |
36 | 37 | ); 38 | }; 39 | 40 | export default BugReportEmail; 41 | -------------------------------------------------------------------------------- /packages/email/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@potential/tsconfig/internal-package.json", 3 | "compilerOptions": { 4 | "lib": ["ES2022", "dom", "dom.iterable"], 5 | "jsx": "react", 6 | "rootDir": "src", 7 | "esModuleInterop": true, 8 | "emitDeclarationOnly": false 9 | }, 10 | "include": ["src"], 11 | "exclude": ["node_modules", "dist"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/email/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | entry: ["src/index.ts", "src/emails/*.ts"], 5 | outDir: "dist", 6 | platform: "node", 7 | format: ["esm"], 8 | external: [ 9 | "@potential/consts", 10 | "@potential/env", 11 | "@react-email/components", 12 | "dotenv", 13 | "react", 14 | "react-dom", 15 | "resend", 16 | ], 17 | dts: true, // Generate .d.ts files 18 | splitting: false, // Usually better for libraries, but can be true if preferred 19 | sourcemap: true, 20 | clean: false, // Clean output directory before build 21 | bundle: true, // Explicitly set, though often default 22 | }); 23 | -------------------------------------------------------------------------------- /packages/env/eslint.config.js: -------------------------------------------------------------------------------- 1 | import baseConfig from "@potential/eslint-config/base"; 2 | 3 | /** @type {import('typescript-eslint').Config} */ 4 | export default [ 5 | { 6 | ignores: [], 7 | }, 8 | ...baseConfig, 9 | ]; 10 | -------------------------------------------------------------------------------- /packages/env/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@potential/env", 3 | "private": true, 4 | "version": "0.1.0", 5 | "type": "module", 6 | "main": "./dist/index.js", 7 | "types": "./dist/index.d.ts", 8 | "exports": { 9 | ".": { 10 | "import": "./dist/index.js", 11 | "types": "./dist/index.d.ts" 12 | } 13 | }, 14 | "files": [ 15 | "dist" 16 | ], 17 | "scripts": { 18 | "dev": "tsup src/index.ts --out-dir dist --platform node --format esm --external @t3-oss/env-core --external zod --dts --no-splitting --sourcemap --watch", 19 | "build": "tsup src/index.ts --out-dir dist --platform node --format esm --external @t3-oss/env-core --external zod --dts --no-splitting --sourcemap --clean", 20 | "clean": "git clean -xdf .cache .turbo dist node_modules", 21 | "format": "prettier --check . --ignore-path ../../.gitignore", 22 | "lint": "eslint", 23 | "typecheck": "tsc --noEmit --emitDeclarationOnly false" 24 | }, 25 | "devDependencies": { 26 | "@potential/eslint-config": "workspace:*", 27 | "@potential/prettier-config": "workspace:*", 28 | "@potential/tsconfig": "workspace:*", 29 | "eslint": "catalog:", 30 | "prettier": "catalog:", 31 | "typescript": "catalog:" 32 | }, 33 | "prettier": "@potential/prettier-config", 34 | "dependencies": { 35 | "@t3-oss/env-core": "^0.13.4", 36 | "zod": "catalog:" 37 | } 38 | } -------------------------------------------------------------------------------- /packages/env/src/client.ts: -------------------------------------------------------------------------------- 1 | import { createEnv } from "@t3-oss/env-core"; 2 | import { z } from "zod"; 3 | 4 | export const shared = createEnv({ 5 | clientPrefix: "PUBLIC_", 6 | client: { 7 | PUBLIC_BACKEND_URL: z.string().min(1).optional(), 8 | }, 9 | runtimeEnv: process.env, 10 | }); 11 | 12 | export const expo = createEnv({ 13 | clientPrefix: "EXPO_PUBLIC_", 14 | client: { 15 | EXPO_PUBLIC_BACKEND_URL: z.string().optional(), 16 | }, 17 | runtimeEnv: process.env, 18 | }); 19 | 20 | export const web = createEnv({ 21 | clientPrefix: "NEXT_PUBLIC_", 22 | client: { 23 | NEXT_PUBLIC_URL: z.string().min(1), 24 | NEXT_PUBLIC_BACKEND_URL: z.string().min(1), 25 | }, 26 | runtimeEnv: process.env, 27 | }); 28 | 29 | export const clientEnv = { 30 | expo, 31 | shared, 32 | web, 33 | }; 34 | -------------------------------------------------------------------------------- /packages/env/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./client"; 2 | export * from "./server"; 3 | -------------------------------------------------------------------------------- /packages/env/src/server.ts: -------------------------------------------------------------------------------- 1 | import { createEnv } from "@t3-oss/env-core"; 2 | import { z } from "zod"; 3 | 4 | const shared = createEnv({ 5 | server: { 6 | NODE_ENV: z.enum(["development", "production"]), 7 | VERCEL: z.string().optional(), 8 | BASE_URL: z.string().min(1), 9 | BACKEND_URL: z.string().min(1), 10 | }, 11 | runtimeEnv: process.env, 12 | }); 13 | 14 | const ai = createEnv({ 15 | server: { 16 | OPENAI_API_KEY: z.string().min(1), 17 | }, 18 | runtimeEnv: process.env, 19 | }); 20 | const auth = createEnv({ 21 | server: { 22 | AUTH_SECRET: z.string().min(1), 23 | }, 24 | runtimeEnv: process.env, 25 | }); 26 | 27 | const database = createEnv({ 28 | server: { 29 | DB_HOST: z.string().min(1), 30 | DB_USERNAME: z.string().min(1), 31 | DB_PASSWORD: z.string().min(1), 32 | DB_DATABASE: z.string().min(1), 33 | DB_MIGRATION_URL: z.string().min(1), 34 | }, 35 | runtimeEnv: process.env, 36 | }); 37 | 38 | const email = createEnv({ 39 | server: { 40 | RESEND_API_KEY: z.string().optional(), 41 | }, 42 | runtimeEnv: process.env, 43 | }); 44 | 45 | const storage = createEnv({ 46 | server: { 47 | STORAGE_S3_BUCKET_UPLOADS: z.string().min(1), 48 | STORAGE_S3_BUCKET_AVATARS: z.string().min(1), 49 | STORAGE_S3_REGION: z.string().min(1), 50 | STORAGE_S3_ENDPOINT: z.string().optional(), 51 | STORAGE_S3_ACCESS_KEY_ID: z.string().min(1), 52 | STORAGE_S3_SECRET_ACCESS_KEY: z.string().min(1), 53 | }, 54 | runtimeEnv: process.env, 55 | }); 56 | 57 | export const serverEnv = { 58 | auth, 59 | ai, 60 | database, 61 | email, 62 | shared, 63 | storage, 64 | }; 65 | -------------------------------------------------------------------------------- /packages/env/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "esModuleInterop": true, 5 | "skipLibCheck": true, 6 | "target": "ES2022", 7 | "lib": ["ES2022"], 8 | "allowJs": true, 9 | "resolveJsonModule": true, 10 | "moduleDetection": "force", 11 | "isolatedModules": true, 12 | "strict": true, 13 | "noUncheckedIndexedAccess": true, 14 | "module": "Preserve", 15 | "moduleResolution": "Bundler", 16 | 17 | "declaration": true, 18 | "declarationMap": true, 19 | "rootDir": "src", 20 | "outDir": "dist" 21 | }, 22 | "include": ["src/**/*.ts"], 23 | "files": ["src/index.ts", "src/client.ts", "src/server.ts"], 24 | "exclude": ["node_modules", "dist", ".cache"] 25 | } 26 | -------------------------------------------------------------------------------- /packages/local-docker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@potential/local-docker", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "dev:docker": "pnpm docker:up", 6 | "docker:up": "docker compose up", 7 | "docker:stop": "docker compose stop", 8 | "docker:clean": "docker compose down -v" 9 | }, 10 | "node": "20" 11 | } -------------------------------------------------------------------------------- /packages/storage/eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import baseConfig from "@potential/eslint-config/base.js"; 3 | 4 | /** @type {import("eslint").Linter.Config} */ 5 | export default [ 6 | ...baseConfig, 7 | ]; -------------------------------------------------------------------------------- /packages/storage/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@potential/storage", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "main": "./dist/index.js", 7 | "module": "./dist/index.js", 8 | "types": "./dist/index.d.ts", 9 | "exports": { 10 | ".": { 11 | "types": "./dist/index.d.ts", 12 | "import": "./dist/index.js" 13 | } 14 | }, 15 | "files": [ 16 | "dist" 17 | ], 18 | "scripts": { 19 | "build": "tsup src/index.ts --format esm --dts --external @aws-sdk/client-s3 --external @aws-sdk/s3-request-presigner --external @potential/env --external @potential/tsconfig --external date-fns --external zod", 20 | "clean": "git clean -xdf .cache .turbo dist node_modules", 21 | "dev": "tsup src/index.ts --format esm --dts --external @aws-sdk/client-s3 --external @aws-sdk/s3-request-presigner --external @potential/env --external @potential/tsconfig --external date-fns --external zod --watch", 22 | "format": "prettier --check . --ignore-path ../../.gitignore", 23 | "lint": "eslint", 24 | "typecheck": "tsc --noEmit --emitDeclarationOnly false" 25 | }, 26 | "dependencies": { 27 | "@aws-sdk/client-s3": "^3.802.0", 28 | "@aws-sdk/s3-request-presigner": "^3.802.0", 29 | "@potential/env": "workspace:*", 30 | "@potential/tsconfig": "workspace:*", 31 | "date-fns": "^4.1.0", 32 | "zod": "catalog:" 33 | }, 34 | "devDependencies": { 35 | "@potential/eslint-config": "workspace:*", 36 | "tsup": "^8.2.4" 37 | } 38 | } -------------------------------------------------------------------------------- /packages/storage/src/index.ts: -------------------------------------------------------------------------------- 1 | import { S3Client } from "@aws-sdk/client-s3"; 2 | import { networkInterfaces } from "os"; 3 | 4 | import { serverEnv } from "@potential/env"; 5 | 6 | // dynamically work out the endpoint based on the environment or localhost ip 7 | const getS3Endpoint = (): string => { 8 | const endpointEnv = serverEnv.storage.STORAGE_S3_ENDPOINT; 9 | if (endpointEnv && endpointEnv.length > 0) { 10 | console.log("💿 using S3 endpoint from env", { endpointEnv }); 11 | return endpointEnv; 12 | } 13 | 14 | const localhostIP = () => { 15 | const nets = networkInterfaces(); 16 | for (const name of Object.keys(nets)) { 17 | for (const net of nets[name] ?? []) { 18 | // Skip over non-IPv4 and internal (i.e. 127.0.0.1) addresses 19 | if (net.family === "IPv4" && !net.internal) { 20 | return net.address; 21 | } 22 | } 23 | } 24 | return null; 25 | }; 26 | 27 | const ip = localhostIP(); 28 | if (!ip) { 29 | console.error( 30 | "🚨 💿 Failed to get the s3 endpoint from ENV or via local IP", 31 | ); 32 | throw new Error( 33 | "Failed to get localhost. Please point to your production server.", 34 | ); 35 | } 36 | console.log("💿 using S3 endpoint from IP address", { ip }); 37 | 38 | return `http://${ip}:3902`; 39 | }; 40 | 41 | export const s3Client = new S3Client({ 42 | region: serverEnv.storage.STORAGE_S3_REGION, 43 | endpoint: getS3Endpoint(), 44 | forcePathStyle: true, 45 | credentials: { 46 | accessKeyId: serverEnv.storage.STORAGE_S3_ACCESS_KEY_ID, 47 | secretAccessKey: serverEnv.storage.STORAGE_S3_SECRET_ACCESS_KEY, 48 | }, 49 | }); 50 | 51 | export { GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3"; 52 | export { getSignedUrl } from "@aws-sdk/s3-request-presigner"; 53 | 54 | -------------------------------------------------------------------------------- /packages/storage/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@potential/tsconfig/internal-package.json", 3 | "compilerOptions": { 4 | "jsx": "react-jsx", 5 | "baseUrl": ".", 6 | "emitDeclarationOnly": false, 7 | "rootDir": "src" 8 | }, 9 | "include": ["src"], 10 | "exclude": ["node_modules", "dist"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/templates/README.md: -------------------------------------------------------------------------------- 1 | # @potential/templates 2 | 3 | This package contains all trackable templates and template groups for the 1up health tracking app. 4 | 5 | ## Structure 6 | 7 | - `/types` - Type definitions for templates 8 | - `/trackables` - Individual trackable templates organized by type 9 | - `/groups` - Template groups for creating multiple trackables together 10 | - `/utils` - Utility functions for working with templates 11 | 12 | ## Adding New Templates 13 | 14 | 1. Choose the appropriate category in `/templates` 15 | 2. Follow the pattern in the template file 16 | 3. Export your templates using the `defineTemplate` helper 17 | 18 | Example: 19 | 20 | ```typescript 21 | // /trackables/body/custom-template.ts 22 | export const customTemplates = [ 23 | defineTemplate({ 24 | id: "custom-measurement", 25 | version: 1, 26 | name: "Custom Measurement", 27 | // ... template definition 28 | }), 29 | ]; 30 | ``` 31 | 32 | ## Adding Template Groups 33 | 34 | 1. Choose or create an appropriate category in `/groups` 35 | 2. Create your group definition 36 | 3. Add it to the groups index 37 | -------------------------------------------------------------------------------- /packages/templates/eslint.config.js: -------------------------------------------------------------------------------- 1 | import baseConfig from "@potential/eslint-config/base"; 2 | 3 | /** @type {import('typescript-eslint').Config} */ 4 | export default [ 5 | { 6 | ignores: [], 7 | }, 8 | ...baseConfig, 9 | ]; 10 | -------------------------------------------------------------------------------- /packages/templates/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@potential/templates", 3 | "private": true, 4 | "version": "0.1.0", 5 | "main": "./dist/index.js", 6 | "module": "./dist/index.mjs", 7 | "react-native": "./dist/index.js", 8 | "types": "./dist/index.d.ts", 9 | "exports": { 10 | ".": { 11 | "types": "./dist/index.d.ts", 12 | "require": "./dist/index.js", 13 | "import": "./dist/index.mjs" 14 | } 15 | }, 16 | "files": [ 17 | "dist" 18 | ], 19 | "scripts": { 20 | "build": "tsup", 21 | "prepare": "pnpm exec tsup", 22 | "clean": "git clean -xdf .cache .turbo dist node_modules", 23 | "dev:package": "tsup --watch", 24 | "format": "prettier --check . --ignore-path ../../.gitignore", 25 | "lint": "eslint", 26 | "typecheck": "tsc --noEmit --emitDeclarationOnly false" 27 | }, 28 | "dependencies": { 29 | "@potential/consts": "workspace:*", 30 | "@potential/tsconfig": "workspace:*", 31 | "@potential/utils": "workspace:*", 32 | "tsup": "^8.2.4", 33 | "zod": "catalog:" 34 | }, 35 | "devDependencies": { 36 | "@potential/eslint-config": "workspace:*", 37 | "@potential/prettier-config": "workspace:*", 38 | "eslint": "catalog:", 39 | "prettier": "catalog:", 40 | "typescript": "catalog:" 41 | }, 42 | "prettier": "@potential/prettier-config" 43 | } -------------------------------------------------------------------------------- /packages/templates/src/index.ts: -------------------------------------------------------------------------------- 1 | // packages/templates/src/index.ts 2 | export * from "./templates"; 3 | export * from "./types"; 4 | export * from "./upgrades"; 5 | export * from "./utils"; 6 | -------------------------------------------------------------------------------- /packages/templates/src/templates/index.ts: -------------------------------------------------------------------------------- 1 | // packages/templates/src/templates/index.ts 2 | import { bodyTemplates } from "./body"; 3 | import { consumptionTemplates } from "./consumption"; 4 | import { mindTemplates } from "./mind"; 5 | import { TRACKABLE_TEMPLATES } from "./templates-registry"; 6 | 7 | // Populate the registry 8 | TRACKABLE_TEMPLATES.body = bodyTemplates; 9 | TRACKABLE_TEMPLATES.mind = mindTemplates; 10 | TRACKABLE_TEMPLATES.consumption = consumptionTemplates; 11 | 12 | export * from "./body"; 13 | export * from "./consumption"; 14 | export * from "./mind"; 15 | 16 | export { TRACKABLE_TEMPLATES }; 17 | -------------------------------------------------------------------------------- /packages/templates/src/templates/templates-registry.ts: -------------------------------------------------------------------------------- 1 | import type { TrackableTemplateRegistry } from "../types"; 2 | 3 | export const TRACKABLE_TEMPLATES: TrackableTemplateRegistry = { 4 | body: {}, // We populate this later 5 | sleep: {}, 6 | consumption: {}, 7 | supplement: {}, 8 | medication: {}, 9 | energy: {}, 10 | blood: {}, 11 | activity: {}, 12 | mind: {}, 13 | symptom: {}, 14 | custom: {}, 15 | } as const; 16 | -------------------------------------------------------------------------------- /packages/templates/src/types.ts: -------------------------------------------------------------------------------- 1 | // packages/templates/src/types.ts 2 | import { z } from "zod"; 3 | 4 | import type { ConstsTypes, TrackableCustomConfig } from "@potential/consts"; 5 | import { CONSTS } from "@potential/consts"; 6 | 7 | type TrackableSubTypesKey = ConstsTypes["TRACKABLE"]["SUB_TYPES"]["KEY"]; 8 | type TrackableTypesKey = ConstsTypes["TRACKABLE"]["TYPES"]["KEY"]; 9 | 10 | // Core template types 11 | export interface TrackableConfigMetadata { 12 | templateId?: string; 13 | templateVersion?: number; 14 | isCustomized?: boolean; 15 | } 16 | 17 | export type TrackableConfigWithMeta = TrackableCustomConfig & { 18 | _meta?: TrackableConfigMetadata; 19 | }; 20 | 21 | export const baseTemplateSchema = z.object({ 22 | id: z.string(), 23 | version: z.number(), 24 | name: z.string(), 25 | description: z.string().optional(), 26 | aiDescriptionHelper: z.string(), 27 | recommended: z.boolean().default(false), 28 | featured: z.boolean().default(false), 29 | uses: z.number().default(0), 30 | defaultConfig: z.custom(), 31 | type: CONSTS.TRACKABLE.TYPES_SCHEMA, 32 | subType: CONSTS.TRACKABLE.SUB_TYPES_SCHEMA, 33 | }); 34 | 35 | export type BaseTemplate = z.infer; 36 | 37 | export type TrackableTemplateRegistry = Record< 38 | TrackableTypesKey, 39 | Partial> 40 | >; 41 | 42 | export interface TrackableTemplateGroup { 43 | id: string; 44 | version: number; 45 | name: string; 46 | description?: string; 47 | aiDescriptionHelper: string; 48 | recommended?: boolean; 49 | featured?: boolean; 50 | uses?: number; 51 | templates: { 52 | templateId: string; 53 | overrides?: { 54 | name?: string; 55 | description?: string; 56 | defaultConfig?: Partial; 57 | }; 58 | }[]; 59 | category?: string; 60 | tags?: string[]; 61 | } 62 | -------------------------------------------------------------------------------- /packages/templates/src/types/templates.ts: -------------------------------------------------------------------------------- 1 | // packages/templates/src/types/templates.ts 2 | import { z } from "zod"; 3 | 4 | import type { ConstsTypes, TrackableCustomConfig } from "@potential/consts"; 5 | 6 | type TrackableSubTypesKey = ConstsTypes["TRACKABLE"]["SUB_TYPES"]["KEY"]; 7 | type TrackableTypesKey = ConstsTypes["TRACKABLE"]["TYPES"]["KEY"]; 8 | 9 | export interface TrackableConfigMetadata { 10 | templateId?: string; 11 | templateVersion?: number; 12 | isCustomized?: boolean; 13 | } 14 | 15 | export type TrackableConfigWithMeta = TrackableCustomConfig & { 16 | _meta?: TrackableConfigMetadata; 17 | }; 18 | 19 | export const baseTemplateSchema = z.object({ 20 | id: z.string(), 21 | version: z.number(), 22 | name: z.string(), 23 | description: z.string().optional(), 24 | icon: z.string().optional(), 25 | recommended: z.boolean().default(false), 26 | defaultConfig: z.custom(), 27 | }); 28 | 29 | export type BaseTemplate = z.infer; 30 | 31 | export type TrackableTemplateRegistry = Record< 32 | TrackableTypesKey, 33 | Partial> 34 | >; 35 | -------------------------------------------------------------------------------- /packages/templates/src/upgrades.ts: -------------------------------------------------------------------------------- 1 | // packages/templates/src/upgrades.ts 2 | import type { TrackableConfigWithMeta } from "./types"; 3 | 4 | // Define upgrade paths for templates 5 | export const templateUpgradePaths: Record< 6 | string, 7 | Record< 8 | number, 9 | { 10 | toVersion: number; 11 | upgradeFn: (config: TrackableConfigWithMeta) => TrackableConfigWithMeta; 12 | } 13 | > 14 | > = { 15 | "body-fat-percentage": { 16 | 1: { 17 | toVersion: 2, 18 | upgradeFn: (config) => ({ 19 | ...config, 20 | measureMin: 2, 21 | measureMax: 50, 22 | _meta: { 23 | ...config._meta, 24 | templateVersion: 2, 25 | }, 26 | }), 27 | }, 28 | }, 29 | // Add other template upgrade paths here 30 | }; 31 | -------------------------------------------------------------------------------- /packages/templates/src/utils.ts: -------------------------------------------------------------------------------- 1 | // packages/templates/src/utils.ts 2 | import type { ConstsTypes } from "@potential/consts"; 3 | 4 | import { TRACKABLE_TEMPLATES } from "./templates/templates-registry"; 5 | import type { BaseTemplate, TrackableConfigWithMeta } from "./types"; 6 | 7 | // Helper function to create templates with type checking 8 | export function defineTemplate(template: BaseTemplate): BaseTemplate { 9 | return template; 10 | } 11 | 12 | // Template retrieval and manipulation functions 13 | export function getTemplateById(id: string): BaseTemplate | undefined { 14 | for (const typeTemplates of Object.values(TRACKABLE_TEMPLATES)) { 15 | const found = typeTemplates[id as keyof typeof typeTemplates]; 16 | if (found) return found; 17 | } 18 | return undefined; 19 | } 20 | 21 | export function getTemplateVersion( 22 | templateId: string, 23 | version: number, 24 | ): BaseTemplate | undefined { 25 | const template = getTemplateById(templateId); 26 | if (template?.version === version) { 27 | return template; 28 | } 29 | return undefined; 30 | } 31 | 32 | export function hasTemplateUpdates(config: TrackableConfigWithMeta): boolean { 33 | if (!config._meta?.templateId || !config._meta.templateVersion) { 34 | return false; 35 | } 36 | 37 | const currentTemplate = getTemplateById(config._meta.templateId); 38 | return currentTemplate 39 | ? currentTemplate.version > config._meta.templateVersion 40 | : false; 41 | } 42 | 43 | export function filterTemplatesByType( 44 | type: ConstsTypes["TRACKABLE"]["TYPES"]["KEY"], 45 | ): BaseTemplate[] { 46 | const templates: BaseTemplate[] = []; 47 | for (const typeTemplates of Object.values(TRACKABLE_TEMPLATES[type])) { 48 | templates.push(typeTemplates); 49 | } 50 | return templates; 51 | } 52 | -------------------------------------------------------------------------------- /packages/templates/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@potential/tsconfig/internal-package.json", 3 | "compilerOptions": { 4 | "emitDeclarationOnly": false, 5 | "rootDir": "src" 6 | }, 7 | "include": ["src"], 8 | "exclude": ["node_modules", "dist"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/templates/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | entry: ["src/index.ts"], 5 | format: ["cjs", "esm"], 6 | dts: true, 7 | splitting: false, 8 | sourcemap: true, 9 | clean: false, 10 | treeshake: true, 11 | external: [ 12 | "@potential/consts", 13 | "@potential/utils", 14 | "@potential/tsconfig", // Added as it's a devDependency that might be used by tsconfig.json 15 | "zod", 16 | ], 17 | }); 18 | -------------------------------------------------------------------------------- /packages/trpc/eslint.config.js: -------------------------------------------------------------------------------- 1 | import baseConfig from "@potential/eslint-config/base"; 2 | 3 | /** @type {import('typescript-eslint').Config} */ 4 | export default [ 5 | { 6 | ignores: ["dist/**"], 7 | }, 8 | ...baseConfig, 9 | ]; 10 | -------------------------------------------------------------------------------- /packages/trpc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@potential/trpc", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "main": "./dist/index.js", 7 | "module": "./dist/index.js", 8 | "types": "./dist/index.d.ts", 9 | "exports": { 10 | ".": { 11 | "types": "./dist/index.d.ts", 12 | "import": "./dist/index.js" 13 | }, 14 | "./root": { 15 | "types": "./dist/root.d.ts", 16 | "import": "./dist/root.js" 17 | }, 18 | "./trpc": { 19 | "types": "./dist/trpc.d.ts", 20 | "import": "./dist/trpc.js" 21 | } 22 | }, 23 | "files": [ 24 | "dist" 25 | ], 26 | "scripts": { 27 | "dev": "tsup --watch", 28 | "build": "tsup --clean", 29 | "clean": "git clean -xdf .cache .turbo dist node_modules", 30 | "format": "prettier --check . --ignore-path ../../.gitignore", 31 | "lint": "eslint", 32 | "typecheck": "tsc --noEmit --project tsconfig.json" 33 | }, 34 | "dependencies": { 35 | "@ai-sdk/openai": "^1.3.22", 36 | "@potential/auth": "workspace:*", 37 | "@potential/consts": "workspace:*", 38 | "@potential/db": "workspace:*", 39 | "@potential/email": "workspace:*", 40 | "@potential/env": "workspace:*", 41 | "@potential/storage": "workspace:*", 42 | "@potential/utils": "workspace:*", 43 | "@potential/validators": "workspace:*", 44 | "@trpc/server": "catalog:", 45 | "ai": "catalog:", 46 | "better-auth": "catalog:", 47 | "superjson": "2.2.2", 48 | "zod": "catalog:" 49 | }, 50 | "devDependencies": { 51 | "@potential/eslint-config": "workspace:*", 52 | "@potential/prettier-config": "workspace:*", 53 | "@potential/tsconfig": "workspace:*", 54 | "eslint": "catalog:", 55 | "prettier": "catalog:", 56 | "tsup": "^8.2.4", 57 | "typescript": "catalog:" 58 | }, 59 | "prettier": "@potential/prettier-config" 60 | } -------------------------------------------------------------------------------- /packages/trpc/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server"; 2 | 3 | import type { AppRouter } from "./root"; 4 | import type { TrpcContext } from "./trpc"; 5 | import { appRouter } from "./root"; 6 | import { createCallerFactory } from "./trpc"; 7 | 8 | const createCaller = createCallerFactory(appRouter); 9 | 10 | type RouterInputs = inferRouterInputs; 11 | 12 | type RouterOutputs = inferRouterOutputs; 13 | 14 | export { appRouter, createCaller }; 15 | export type { AppRouter, RouterInputs, RouterOutputs, TrpcContext }; 16 | -------------------------------------------------------------------------------- /packages/trpc/src/root.ts: -------------------------------------------------------------------------------- 1 | import { logRouter } from "./router/log"; 2 | import { storageRouter } from "./router/storage"; 3 | import { trackablesRouter } from "./router/trackables"; 4 | import { userRouter } from "./router/user"; 5 | import { createTRPCRouter } from "./trpc"; 6 | 7 | export const appRouter = createTRPCRouter({ 8 | user: userRouter, 9 | log: logRouter, 10 | trackables: trackablesRouter, 11 | storage: storageRouter, 12 | }); 13 | 14 | export type AppRouter = typeof appRouter; 15 | -------------------------------------------------------------------------------- /packages/trpc/src/router/storage.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | import { serverEnv } from "@potential/env"; 4 | import { getSignedUrl, PutObjectCommand, s3Client } from "@potential/storage"; 5 | import { cloudTypeIdGenerator } from "@potential/utils"; 6 | 7 | import { createTRPCRouter, protectedProcedure } from "../trpc"; 8 | 9 | export const storageRouter = createTRPCRouter({ 10 | getUploadPresignedUrl: protectedProcedure 11 | .input( 12 | z.object({ 13 | fileType: z.string(), 14 | }), 15 | ) 16 | .mutation(async ({ ctx, input }) => { 17 | const userId = ctx.auth.user.id; 18 | const { fileType } = input; 19 | 20 | const uploadId = cloudTypeIdGenerator("userUpload"); 21 | const command = new PutObjectCommand({ 22 | Bucket: serverEnv.storage.STORAGE_S3_BUCKET_UPLOADS, 23 | Key: `${userId}/${uploadId}`, 24 | ContentType: fileType, 25 | }); 26 | const signedUrl = await getSignedUrl(s3Client, command, { 27 | expiresIn: 3600, 28 | }); 29 | 30 | console.log("🔥 trpc storage getUploadPresignedUrls", { signedUrl }); 31 | 32 | return { 33 | id: uploadId, 34 | url: signedUrl, 35 | }; 36 | }), 37 | }); 38 | -------------------------------------------------------------------------------- /packages/trpc/src/router/user/index.ts: -------------------------------------------------------------------------------- 1 | import { accountRouter } from "./account"; 2 | import { profileRouter } from "./profile"; 3 | 4 | export const userRouter = { 5 | profile: profileRouter, 6 | account: accountRouter, 7 | }; 8 | -------------------------------------------------------------------------------- /packages/trpc/src/utils/ai/generateNewTrackables.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | import { CONSTS } from "@potential/consts"; 4 | 5 | export const newTrackablesSchema = z.object({ 6 | trackableName: z.string(), 7 | trackableDescription: z.string(), 8 | trackableConfig: CONSTS.TRACKABLE.CONFIG.CONFIG_SCHEMA, 9 | name: z.string().max(32), 10 | description: z.string().max(255), 11 | color: z.union([CONSTS.COLORS.SCHEMA, z.string()]), 12 | type: CONSTS.TRACKABLE.TYPES_SCHEMA, 13 | subType: CONSTS.TRACKABLE.SUB_TYPES_SCHEMA, 14 | subTypeCustomName: z.string().max(64), 15 | configType: CONSTS.TRACKABLE.CONFIG.TYPES_SCHEMA, 16 | }); 17 | -------------------------------------------------------------------------------- /packages/trpc/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@potential/tsconfig/internal-package.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "rootDir": "src", 6 | "composite": false 7 | }, 8 | "include": ["src"], 9 | "exclude": ["node_modules", "dist"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/trpc/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | entry: ["src/index.ts", "src/root.ts", "src/trpc.ts"], 5 | outDir: "dist", 6 | platform: "node", // Auth logic is typically server-side or neutral 7 | format: ["esm"], 8 | dts: true, // tsup handles DTS 9 | external: [ 10 | "@ai-sdk/anthropic", 11 | "@potential/auth", 12 | "@potential/consts", 13 | "@potential/db", 14 | "@potential/email", 15 | "@potential/env", 16 | "@potential/storage", 17 | "@potential/utils", 18 | "@potential/validators", 19 | "@trpc/server", 20 | "ai", 21 | "better-auth", 22 | "superjson", 23 | "zod", 24 | ], 25 | tsconfig: "tsconfig.json", // Explicitly point to tsconfig 26 | splitting: false, // Common for libraries 27 | sourcemap: true, // For JS sourcemaps by tsup 28 | clean: false, // Set to false 29 | bundle: true, 30 | // No onSuccess hook needed here, scripts will handle build sequence 31 | }); 32 | -------------------------------------------------------------------------------- /packages/tsconfig/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "esModuleInterop": true, 5 | "skipLibCheck": true, 6 | "target": "ES2022", 7 | "lib": [ 8 | "ES2022", 9 | "dom" 10 | ], 11 | "allowJs": true, 12 | "resolveJsonModule": true, 13 | "moduleDetection": "force", 14 | "isolatedModules": true, 15 | "incremental": true, 16 | "disableSourceOfProjectReferenceRedirect": true, 17 | "tsBuildInfoFile": "${configDir}/.cache/tsbuildinfo.json", 18 | "strict": true, 19 | "noUncheckedIndexedAccess": true, 20 | "checkJs": true, 21 | "module": "Preserve", 22 | "moduleResolution": "Bundler", 23 | "noEmit": true 24 | }, 25 | "exclude": [ 26 | "node_modules", 27 | "build", 28 | "dist", 29 | ".next", 30 | ".expo" 31 | ] 32 | } -------------------------------------------------------------------------------- /packages/tsconfig/internal-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "./base.json", 4 | "compilerOptions": { 5 | "declaration": true, 6 | "declarationMap": true, 7 | "emitDeclarationOnly": false, 8 | "noEmit": false, 9 | "outDir": "${configDir}/dist" 10 | } 11 | } -------------------------------------------------------------------------------- /packages/tsconfig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@potential/tsconfig", 3 | "private": true, 4 | "version": "0.1.0", 5 | "files": [ 6 | "*.json" 7 | ] 8 | } -------------------------------------------------------------------------------- /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": "../../tooling/tailwind/web.ts", 8 | "css": "unused.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "utils": "@potential/ui", 14 | "components": "src/", 15 | "ui": "src/" 16 | } 17 | } -------------------------------------------------------------------------------- /packages/ui/eslint.config.js: -------------------------------------------------------------------------------- 1 | import baseConfig from "@potential/eslint-config/base"; 2 | import reactConfig from "@potential/eslint-config/react"; 3 | 4 | /** @type {import('typescript-eslint').Config} */ 5 | export default [ 6 | { 7 | ignores: ["dist/**"], 8 | }, 9 | ...baseConfig, 10 | ...reactConfig, 11 | ]; 12 | -------------------------------------------------------------------------------- /packages/ui/src/button.tsx: -------------------------------------------------------------------------------- 1 | import type { VariantProps } from "class-variance-authority"; 2 | import * as React from "react"; 3 | import { cva } from "class-variance-authority"; 4 | import { Slot } from "radix-ui"; 5 | 6 | import { cn } from "./index"; 7 | 8 | const buttonVariants = cva( 9 | "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", 10 | { 11 | variants: { 12 | variant: { 13 | primary: 14 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 15 | destructive: 16 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 17 | outline: 18 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 19 | secondary: 20 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 21 | ghost: "hover:bg-accent hover:text-accent-foreground", 22 | link: "text-primary underline-offset-4 hover:underline", 23 | }, 24 | size: { 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | md: "h-9 px-4 py-2", 27 | lg: "h-10 rounded-md px-8", 28 | icon: "size-9", 29 | }, 30 | }, 31 | defaultVariants: { 32 | variant: "primary", 33 | size: "md", 34 | }, 35 | }, 36 | ); 37 | 38 | interface ButtonProps 39 | extends React.ButtonHTMLAttributes, 40 | VariantProps { 41 | asChild?: boolean; 42 | } 43 | 44 | const Button = React.forwardRef( 45 | ({ className, variant, size, asChild = false, ...props }, ref) => { 46 | const Comp = asChild ? Slot.Slot : "button"; 47 | return ( 48 | 53 | ); 54 | }, 55 | ); 56 | Button.displayName = "Button"; 57 | 58 | export { Button, buttonVariants }; 59 | -------------------------------------------------------------------------------- /packages/ui/src/index.ts: -------------------------------------------------------------------------------- 1 | import { cx } from "class-variance-authority"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | const cn = (...inputs: Parameters) => twMerge(cx(inputs)); 5 | 6 | export { cn }; 7 | -------------------------------------------------------------------------------- /packages/ui/src/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "./index"; 4 | 5 | type InputProps = React.InputHTMLAttributes; 6 | 7 | const Input = React.forwardRef( 8 | ({ className, type, ...props }, ref) => { 9 | return ( 10 | 19 | ); 20 | }, 21 | ); 22 | Input.displayName = "Input"; 23 | 24 | export { Input }; 25 | -------------------------------------------------------------------------------- /packages/ui/src/label.tsx: -------------------------------------------------------------------------------- 1 | import type { VariantProps } from "class-variance-authority"; 2 | import * as React from "react"; 3 | import { cva } from "class-variance-authority"; 4 | import { Label as LabelPrimitive } from "radix-ui"; 5 | 6 | import { cn } from "./index"; 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/theme.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { MoonIcon, SunIcon } from "@radix-ui/react-icons"; 4 | import { ThemeProvider, useTheme } from "next-themes"; 5 | 6 | import { Button } from "./button"; 7 | 8 | function ThemeToggle() { 9 | const { setTheme, theme } = useTheme(); 10 | 11 | return ( 12 | 24 | // 25 | // 26 | // 34 | // 35 | // 36 | // setTheme("light")}> 37 | // Light 38 | // 39 | // setTheme("dark")}> 40 | // Dark 41 | // 42 | // setTheme("system")}> 43 | // System 44 | // 45 | // 46 | // 47 | ); 48 | } 49 | 50 | export { ThemeProvider, ThemeToggle, useTheme }; 51 | -------------------------------------------------------------------------------- /packages/ui/src/toast.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useTheme } from "next-themes"; 4 | import { Toaster as Sonner, toast } from "sonner"; 5 | 6 | type ToasterProps = React.ComponentProps; 7 | 8 | const Toaster = ({ ...props }: ToasterProps) => { 9 | const { theme = "system" } = useTheme(); 10 | 11 | return ( 12 | 28 | ); 29 | }; 30 | 31 | export { Toaster, toast }; 32 | -------------------------------------------------------------------------------- /packages/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@potential/tsconfig/internal-package.json", 3 | "compilerOptions": { 4 | "lib": ["ES2022", "dom", "dom.iterable"], 5 | "jsx": "react-jsx", 6 | "rootDir": "src", 7 | "emitDeclarationOnly": false 8 | }, 9 | "include": ["src"], 10 | "exclude": ["node_modules", "dist"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/ui/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | entry: [ 5 | "src/index.ts", 6 | "src/button.tsx", 7 | "src/dropdown-menu.tsx", 8 | "src/form.tsx", 9 | "src/input.tsx", 10 | "src/label.tsx", 11 | "src/theme.tsx", 12 | "src/toast.tsx", 13 | ], 14 | outDir: "dist", 15 | platform: "browser", 16 | format: ["esm"], 17 | external: [ 18 | "@hookform/resolvers", 19 | "@radix-ui/react-icons", 20 | "class-variance-authority", 21 | "next-themes", 22 | "radix-ui", // Or specific subpaths like "@radix-ui/react-slot" if used 23 | "react-hook-form", 24 | "sonner", 25 | "tailwind-merge", 26 | "react", 27 | "zod", 28 | ], 29 | dts: true, 30 | splitting: false, // Keep components bundled per entry point 31 | sourcemap: true, 32 | clean: false, 33 | bundle: true, 34 | // esbuildOptions(options) { // If Radix UI components need 'use client' 35 | // options.banner = { 36 | // js: '"use client";', 37 | // }; 38 | // }, 39 | }); 40 | -------------------------------------------------------------------------------- /packages/utils/eslint.config.js: -------------------------------------------------------------------------------- 1 | import baseConfig from '@potential/eslint-config/base'; 2 | import typeidConfig from '@potential/eslint-config/typeid'; 3 | 4 | /** @type {import('typescript-eslint').Config} */ 5 | export default [ 6 | { 7 | ignores: ['dist/**'] 8 | }, 9 | ...baseConfig, 10 | ...typeidConfig 11 | ]; 12 | -------------------------------------------------------------------------------- /packages/utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@potential/utils", 3 | "version": "1.0.0", 4 | "description": "", 5 | "types": "./dist/index.d.ts", 6 | "main": "./dist/index.js", 7 | "module": "./dist/index.mjs", 8 | "react-native": "./dist/index.js", 9 | "files": [ 10 | "dist" 11 | ], 12 | "scripts": { 13 | "build": "tsup", 14 | "prepare": "pnpm exec tsup", 15 | "check": "tsc --noEmit", 16 | "dev": "tsup --watch" 17 | }, 18 | "exports": { 19 | ".": { 20 | "types": "./dist/index.d.ts", 21 | "require": "./dist/index.js", 22 | "import": "./dist/index.mjs" 23 | }, 24 | "./typeid": { 25 | "types": "./dist/typeid.d.ts", 26 | "require": "./dist/typeid.js", 27 | "import": "./dist/typeid.mjs" 28 | }, 29 | "./ms": { 30 | "types": "./dist/ms.d.ts", 31 | "require": "./dist/ms.js", 32 | "import": "./dist/ms.mjs" 33 | }, 34 | "./convert": { 35 | "types": "./dist/convert.d.ts", 36 | "require": "./dist/convert.js", 37 | "import": "./dist/convert.mjs" 38 | } 39 | }, 40 | "keywords": [], 41 | "author": "", 42 | "license": "ISC", 43 | "engines": { 44 | "node": ">=20", 45 | "pnpm": ">=8" 46 | }, 47 | "dependencies": { 48 | "@potential/tsconfig": "workspace:*", 49 | "convert-units": "3.0.0-beta.7", 50 | "itty-time": "^2.0.1", 51 | "nanoid": "^5.1.5", 52 | "tsup": "^8.2.4", 53 | "typeid-js": "^1.2.0", 54 | "zod": "catalog:" 55 | }, 56 | "devDependencies": { 57 | "@potential/eslint-config": "workspace:*" 58 | } 59 | } -------------------------------------------------------------------------------- /packages/utils/src/convert.ts: -------------------------------------------------------------------------------- 1 | import configureMeasurements from "convert-units"; 2 | import length from "convert-units/definitions/length"; 3 | import mass from "convert-units/definitions/mass"; 4 | import volume from "convert-units/definitions/volume"; 5 | 6 | /* 7 | `configureMeasurements` is a closure that accepts a directory 8 | of measures and returns a factory function (`convert`) that uses 9 | only those measures. 10 | */ 11 | 12 | /* 13 | convert(1).from('l').to('ml'); 14 | convert(1).from('lb').to('kg'); 15 | convert(1).from('oz').to('g'); 16 | */ 17 | export const convert = configureMeasurements({ 18 | volume, 19 | // @ts-expect-error beta version type issues - check in or after beta 8 20 | mass, 21 | // @ts-expect-error beta version type issues - check in or after beta 8 22 | length, 23 | }); 24 | -------------------------------------------------------------------------------- /packages/utils/src/index.ts: -------------------------------------------------------------------------------- 1 | // Main exports 2 | export * from "itty-time"; 3 | export * from "./convert"; 4 | export * from "./ms"; 5 | export * from "./typeid"; 6 | // Add any future files here automatically by using a similar pattern 7 | -------------------------------------------------------------------------------- /packages/utils/src/ms.ts: -------------------------------------------------------------------------------- 1 | export * from "itty-time"; 2 | -------------------------------------------------------------------------------- /packages/utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@potential/tsconfig/base.json", 3 | "compilerOptions": { 4 | "target": "ES2020", 5 | "lib": ["ES2020"], 6 | "module": "ESNext", 7 | "moduleResolution": "bundler", 8 | "outDir": "./dist", 9 | "declaration": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "esModuleInterop": true, 13 | "skipLibCheck": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "types": ["eslint"], 16 | "rootDir": "src", 17 | "emitDeclarationOnly": false, 18 | "noEmit": false 19 | }, 20 | "include": ["src"], 21 | "exclude": ["node_modules", "dist"] 22 | } 23 | -------------------------------------------------------------------------------- /packages/utils/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | entry: [ 5 | "src/index.ts", 6 | "src/typeid.ts", 7 | "src/ms.ts", 8 | "src/convert.ts", 9 | "src/uiColors.ts", 10 | "src/storage.ts", 11 | ], 12 | format: ["cjs", "esm"], 13 | dts: true, 14 | splitting: false, // Recommended for packages to avoid chunks 15 | sourcemap: true, 16 | clean: false, 17 | treeshake: true, 18 | external: [ 19 | "@potential/tsconfig", 20 | "convert-units", 21 | "itty-time", 22 | "nanoid", 23 | "typeid-js", 24 | "zod", 25 | ], 26 | }); 27 | -------------------------------------------------------------------------------- /packages/validators/eslint.config.js: -------------------------------------------------------------------------------- 1 | import baseConfig from "@potential/eslint-config/base"; 2 | 3 | /** @type {import('typescript-eslint').Config} */ 4 | export default [ 5 | { 6 | ignores: ["dist/**"], 7 | }, 8 | ...baseConfig, 9 | ]; 10 | -------------------------------------------------------------------------------- /packages/validators/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@potential/validators", 3 | "private": true, 4 | "version": "0.1.0", 5 | "type": "module", 6 | "main": "./dist/index.js", 7 | "types": "./dist/index.d.ts", 8 | "exports": { 9 | ".": { 10 | "types": "./dist/index.d.ts", 11 | "import": "./dist/index.js" 12 | } 13 | }, 14 | "files": [ 15 | "dist" 16 | ], 17 | "scripts": { 18 | "build": "tsup", 19 | "clean": "git clean -xdf .cache .turbo dist node_modules", 20 | "dev": "tsup --watch", 21 | "format": "prettier --check . --ignore-path ../../.gitignore", 22 | "lint": "eslint", 23 | "typecheck": "tsc --noEmit --emitDeclarationOnly false" 24 | }, 25 | "dependencies": { 26 | "zod": "catalog:" 27 | }, 28 | "devDependencies": { 29 | "@potential/eslint-config": "workspace:*", 30 | "@potential/prettier-config": "workspace:*", 31 | "@potential/tsconfig": "workspace:*", 32 | "eslint": "catalog:", 33 | "prettier": "catalog:", 34 | "typescript": "catalog:" 35 | }, 36 | "prettier": "@potential/prettier-config" 37 | } -------------------------------------------------------------------------------- /packages/validators/src/index.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const unused = z.string().describe( 4 | `This lib is currently not used as we use drizzle-zod for simple schemas 5 | But as your application grows and you need other validators to share 6 | with back and frontend, you can put them in here 7 | `, 8 | ); 9 | 10 | export const usernameSchema = z 11 | .string() 12 | .min(2, { 13 | message: `Must be at least 2 characters long`, 14 | }) 15 | .max(32, { 16 | message: "Must be less than 32 characters", 17 | }) 18 | .regex(/^[a-zA-Z0-9]*$/, { 19 | message: "Only letters and numbers", 20 | }); 21 | -------------------------------------------------------------------------------- /packages/validators/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@potential/tsconfig/internal-package.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | "composite": true 7 | }, 8 | "include": ["src"], 9 | "exclude": ["node_modules", "dist"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/validators/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | entry: ["src/index.ts"], 5 | outDir: "dist", 6 | platform: "neutral", // Good for libraries usable in browser and node 7 | format: ["esm"], 8 | external: ["zod"], 9 | dts: true, 10 | splitting: false, 11 | sourcemap: true, 12 | clean: true, 13 | bundle: true, 14 | }); 15 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - apps/* 3 | - packages/* 4 | - tooling/* 5 | 6 | catalog: 7 | # AI 8 | "ai": ^4.3.16 9 | "@ai-sdk/openai": ^1.3.22 10 | 11 | # Auth 12 | "better-auth": 1.2.5 13 | "@better-auth/expo": 1.2.5 14 | 15 | # Dev tooling 16 | eslint: ^9.19.0 17 | prettier: ^3.4.2 18 | tailwindcss: ^3.4.15 19 | typescript: ^5.7.3 20 | 21 | # Misc 22 | zod: ^3.24.1 23 | "@hookform/resolvers": ^4.1.2 24 | "drizzle-orm": ^0.40.0 25 | "tailwind-merge": ^3.0.2 26 | "drizzle-kit": ^0.30.5 27 | "drizzle-zod": "^0.6.1" 28 | "react-hook-form": "^7.54.2" 29 | "@tanstack/react-form": "^1.1.0" 30 | 31 | # Tanstack & tRPC 32 | "@tanstack/react-query": ^5.67.1 33 | "@trpc/client": ^11.0.0-rc.824 34 | "@trpc/tanstack-react-query": ^11.0.0-rc.824 35 | "@trpc/server": ^11.0.0-rc.824 36 | 37 | catalogs: 38 | react19: 39 | react: 19.0.0 40 | react-dom: 19.0.0 41 | "@types/react": ^19.0.0 42 | "@types/react-dom": ^19.0.0 43 | -------------------------------------------------------------------------------- /tooling/eslint/nextjs.js: -------------------------------------------------------------------------------- 1 | import nextPlugin from "@next/eslint-plugin-next"; 2 | 3 | /** @type {Awaited} */ 4 | export default [ 5 | { 6 | files: ["**/*.ts", "**/*.tsx"], 7 | plugins: { 8 | "@next/next": nextPlugin, 9 | }, 10 | rules: { 11 | ...nextPlugin.configs.recommended.rules, 12 | ...nextPlugin.configs["core-web-vitals"].rules, 13 | // TypeError: context.getAncestors is not a function 14 | "@next/next/no-duplicate-head": "off", 15 | }, 16 | }, 17 | ]; 18 | -------------------------------------------------------------------------------- /tooling/eslint/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@potential/eslint-config", 3 | "private": true, 4 | "version": "0.3.0", 5 | "type": "module", 6 | "exports": { 7 | "./base": "./base.js", 8 | "./nextjs": "./nextjs.js", 9 | "./react": "./react.js", 10 | "./typeid": "./typeid.js", 11 | "./owneridcolumn": "./owneridcolumn.js", 12 | "./rn": "./rn.js" 13 | }, 14 | "scripts": { 15 | "clean": "git clean -xdf .cache .turbo node_modules", 16 | "format": "prettier --check . --ignore-path ../../.gitignore", 17 | "typecheck": "tsc --noEmit" 18 | }, 19 | "dependencies": { 20 | "@eslint/compat": "^1.2.9", 21 | "@next/eslint-plugin-next": "^15.3.1", 22 | "eslint-plugin-import": "^2.31.0", 23 | "eslint-plugin-jsx-a11y": "^6.10.2", 24 | "eslint-plugin-react": "^7.37.5", 25 | "eslint-plugin-react-compiler": "19.1.0-rc.1", 26 | "eslint-plugin-react-hooks": "^5.2.0", 27 | "eslint-plugin-turbo": "^2.5.2", 28 | "typescript-eslint": "^8.31.1" 29 | }, 30 | "devDependencies": { 31 | "@potential/prettier-config": "workspace:*", 32 | "@potential/tsconfig": "workspace:*", 33 | "eslint": "catalog:", 34 | "prettier": "catalog:", 35 | "typescript": "catalog:" 36 | }, 37 | "prettier": "@potential/prettier-config" 38 | } -------------------------------------------------------------------------------- /tooling/eslint/react.js: -------------------------------------------------------------------------------- 1 | import reactPlugin from "eslint-plugin-react"; 2 | import compilerPlugin from "eslint-plugin-react-compiler"; 3 | import hooksPlugin from "eslint-plugin-react-hooks"; 4 | 5 | /** @type {Awaited} */ 6 | export default [ 7 | { 8 | files: ["**/*.ts", "**/*.tsx"], 9 | plugins: { 10 | react: reactPlugin, 11 | "react-compiler": compilerPlugin, 12 | "react-hooks": hooksPlugin, 13 | }, 14 | rules: { 15 | ...reactPlugin.configs["jsx-runtime"].rules, 16 | ...hooksPlugin.configs.recommended.rules, 17 | "react-compiler/react-compiler": "error", 18 | }, 19 | languageOptions: { 20 | globals: { 21 | React: "writable", 22 | }, 23 | }, 24 | }, 25 | ]; 26 | -------------------------------------------------------------------------------- /tooling/eslint/rn.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview ESLint rule to ensure importing from UI lib instead of react native 3 | */ 4 | 5 | /** @type {import('eslint').Rule.RuleModule} */ 6 | const rnImportRule = { 7 | meta: { 8 | type: "problem", 9 | docs: { 10 | description: "Ensure that importing from UI lib instead of react native", 11 | category: "Best Practices", 12 | recommended: true, 13 | }, 14 | messages: { 15 | wrongImport: 16 | "Import from UI lib! This component should not be imported from react native", 17 | }, 18 | schema: [], 19 | }, 20 | create(context) { 21 | // Components that should not be imported from react-native 22 | const restrictedComponents = [ 23 | "Text", 24 | // Add more components as needed 25 | ]; 26 | 27 | return { 28 | ImportDeclaration(node) { 29 | // Check if importing from react-native 30 | if (node.source.value !== "react-native") { 31 | return; 32 | } 33 | 34 | // Check each imported specifier 35 | node.specifiers.forEach((specifier) => { 36 | if ( 37 | specifier.type === "ImportSpecifier" && 38 | specifier.imported.type === "Identifier" && 39 | restrictedComponents.includes(specifier.imported.name) 40 | ) { 41 | context.report({ 42 | node: specifier, 43 | messageId: "wrongImport", 44 | }); 45 | } 46 | }); 47 | }, 48 | }; 49 | }, 50 | }; 51 | 52 | /** @type {import('@eslint/eslintrc').FlatConfig[]} */ 53 | export default [ 54 | { 55 | files: ["**/*.tsx", "**/*.ts", "**/*.jsx", "**/*.js"], 56 | plugins: { 57 | "@potential": { 58 | rules: { 59 | "no-direct-rn-import": rnImportRule, 60 | }, 61 | }, 62 | }, 63 | rules: { 64 | "@potential/no-direct-rn-import": "error", 65 | }, 66 | }, 67 | ]; 68 | -------------------------------------------------------------------------------- /tooling/eslint/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@potential/tsconfig/base.json", 3 | "include": ["."], 4 | "exclude": ["node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /tooling/eslint/types.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Since the ecosystem hasn't fully migrated to ESLint's new FlatConfig system yet, 3 | * we "need" to type some of the plugins manually :( 4 | */ 5 | 6 | declare module "eslint-plugin-import" { 7 | import type { Linter, Rule } from "eslint"; 8 | 9 | export const configs: { 10 | recommended: { rules: Linter.RulesRecord }; 11 | }; 12 | export const rules: Record; 13 | } 14 | 15 | declare module "eslint-plugin-react" { 16 | import type { Linter, Rule } from "eslint"; 17 | 18 | export const configs: { 19 | recommended: { rules: Linter.RulesRecord }; 20 | all: { rules: Linter.RulesRecord }; 21 | "jsx-runtime": { rules: Linter.RulesRecord }; 22 | }; 23 | export const rules: Record; 24 | } 25 | 26 | declare module "eslint-plugin-react-compiler" {} 27 | 28 | declare module "eslint-plugin-react-hooks" { 29 | import type { Linter, Rule } from "eslint"; 30 | 31 | export const configs: { 32 | recommended: { 33 | rules: { 34 | "rules-of-hooks": Linter.RuleEntry; 35 | "exhaustive-deps": Linter.RuleEntry; 36 | }; 37 | }; 38 | }; 39 | export const rules: Record; 40 | } 41 | 42 | declare module "@next/eslint-plugin-next" { 43 | import type { Linter, Rule } from "eslint"; 44 | 45 | export const configs: { 46 | recommended: { rules: Linter.RulesRecord }; 47 | "core-web-vitals": { rules: Linter.RulesRecord }; 48 | }; 49 | export const rules: Record; 50 | } 51 | -------------------------------------------------------------------------------- /tooling/github/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@potential/github" 3 | } -------------------------------------------------------------------------------- /tooling/github/setup/action.yml: -------------------------------------------------------------------------------- 1 | name: "Setup and install" 2 | description: "Common setup steps for Actions" 3 | 4 | runs: 5 | using: composite 6 | steps: 7 | - uses: pnpm/action-setup@v4 8 | - uses: actions/setup-node@v4 9 | with: 10 | node-version-file: ".nvmrc" 11 | cache: "pnpm" 12 | 13 | - shell: bash 14 | run: pnpm add -g turbo 15 | 16 | - shell: bash 17 | run: pnpm install 18 | -------------------------------------------------------------------------------- /tooling/prettier/index.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from "node:url"; 2 | 3 | /** @typedef {import("prettier").Config} PrettierConfig */ 4 | /** @typedef {import("prettier-plugin-tailwindcss").PluginOptions} TailwindConfig */ 5 | /** @typedef {import("@ianvs/prettier-plugin-sort-imports").PluginConfig} SortImportsConfig */ 6 | 7 | /** @type { PrettierConfig | SortImportsConfig | TailwindConfig } */ 8 | const config = { 9 | plugins: [ 10 | "@ianvs/prettier-plugin-sort-imports", 11 | "prettier-plugin-tailwindcss", 12 | ], 13 | tailwindConfig: fileURLToPath( 14 | new URL("../../tooling/tailwind/web.ts", import.meta.url), 15 | ), 16 | tailwindFunctions: ["cn", "cva"], 17 | importOrder: [ 18 | "", 19 | "^(react/(.*)$)|^(react$)|^(react-native(.*)$)", 20 | "^(next/(.*)$)|^(next$)", 21 | "^(expo(.*)$)|^(expo$)", 22 | "", 23 | "", 24 | "^@potential", 25 | "^@potential/(.*)$", 26 | "", 27 | "^[.|..|~]", 28 | "^~/", 29 | "^[../]", 30 | "^[./]", 31 | ], 32 | importOrderParserPlugins: ["typescript", "jsx", "decorators-legacy"], 33 | importOrderTypeScriptVersion: "4.4.0", 34 | overrides: [ 35 | { 36 | files: "*.json.hbs", 37 | options: { 38 | parser: "json", 39 | }, 40 | }, 41 | { 42 | files: "*.js.hbs", 43 | options: { 44 | parser: "babel", 45 | }, 46 | }, 47 | ], 48 | }; 49 | 50 | export default config; 51 | -------------------------------------------------------------------------------- /tooling/prettier/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@potential/prettier-config", 3 | "private": true, 4 | "version": "0.1.0", 5 | "type": "module", 6 | "exports": { 7 | ".": "./index.js" 8 | }, 9 | "scripts": { 10 | "clean": "git clean -xdf .cache .turbo node_modules", 11 | "format": "prettier --check . --ignore-path ../../.gitignore", 12 | "typecheck": "tsc --noEmit" 13 | }, 14 | "dependencies": { 15 | "@ianvs/prettier-plugin-sort-imports": "^4.4.1", 16 | "prettier": "catalog:", 17 | "prettier-plugin-tailwindcss": "^0.6.11" 18 | }, 19 | "devDependencies": { 20 | "@potential/tsconfig": "workspace:*", 21 | "@types/node": "^22.15.3", 22 | "typescript": "catalog:" 23 | }, 24 | "prettier": "@potential/prettier-config" 25 | } -------------------------------------------------------------------------------- /tooling/prettier/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@potential/tsconfig/base.json", 3 | "include": ["."], 4 | "exclude": ["node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /tooling/tailwind/base.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | export default { 4 | darkMode: ["class"], 5 | content: ["src/**/*.{ts,tsx}"], 6 | theme: { 7 | extend: { 8 | colors: { 9 | border: "hsl(var(--border))", 10 | input: "hsl(var(--input))", 11 | ring: "hsl(var(--ring))", 12 | background: "hsl(var(--background))", 13 | foreground: "hsl(var(--foreground))", 14 | primary: { 15 | DEFAULT: "hsl(var(--primary))", 16 | foreground: "hsl(var(--primary-foreground))", 17 | }, 18 | secondary: { 19 | DEFAULT: "hsl(var(--secondary))", 20 | foreground: "hsl(var(--secondary-foreground))", 21 | }, 22 | destructive: { 23 | DEFAULT: "hsl(var(--destructive))", 24 | foreground: "hsl(var(--destructive-foreground))", 25 | }, 26 | muted: { 27 | DEFAULT: "hsl(var(--muted))", 28 | foreground: "hsl(var(--muted-foreground))", 29 | }, 30 | accent: { 31 | DEFAULT: "hsl(var(--accent))", 32 | foreground: "hsl(var(--accent-foreground))", 33 | }, 34 | popover: { 35 | DEFAULT: "hsl(var(--popover))", 36 | foreground: "hsl(var(--popover-foreground))", 37 | }, 38 | card: { 39 | DEFAULT: "hsl(var(--card))", 40 | foreground: "hsl(var(--card-foreground))", 41 | }, 42 | }, 43 | borderColor: { 44 | DEFAULT: "hsl(var(--border))", 45 | }, 46 | }, 47 | }, 48 | } satisfies Config; 49 | -------------------------------------------------------------------------------- /tooling/tailwind/eslint.config.js: -------------------------------------------------------------------------------- 1 | // FIXME: This kinda stinks... 2 | /// 3 | 4 | import baseConfig from "@potential/eslint-config/base"; 5 | 6 | export default [...baseConfig]; 7 | -------------------------------------------------------------------------------- /tooling/tailwind/native.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | import base from "./base"; 4 | 5 | export default { 6 | content: base.content, 7 | presets: [base], 8 | theme: {}, 9 | } satisfies Config; 10 | -------------------------------------------------------------------------------- /tooling/tailwind/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@potential/tailwind-config", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "exports": { 7 | "./native": "./native.ts", 8 | "./web": "./web.ts" 9 | }, 10 | "scripts": { 11 | "clean": "git clean -xdf .cache .turbo node_modules", 12 | "format": "prettier --check . --ignore-path ../../.gitignore", 13 | "lint": "eslint", 14 | "typecheck": "tsc --noEmit" 15 | }, 16 | "dependencies": { 17 | "postcss": "^8.5.3", 18 | "tailwindcss": "catalog:", 19 | "tailwindcss-animate": "^1.0.7" 20 | }, 21 | "devDependencies": { 22 | "@potential/eslint-config": "workspace:*", 23 | "@potential/prettier-config": "workspace:*", 24 | "@potential/tsconfig": "workspace:*", 25 | "@types/node": "^22.15.3", 26 | "eslint": "catalog:", 27 | "prettier": "catalog:", 28 | "typescript": "catalog:" 29 | }, 30 | "prettier": "@potential/prettier-config" 31 | } -------------------------------------------------------------------------------- /tooling/tailwind/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@potential/tsconfig/base.json", 3 | "include": ["."], 4 | "exclude": ["node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /tooling/tailwind/web.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | import animate from "tailwindcss-animate"; 3 | 4 | import base from "./base"; 5 | 6 | export default { 7 | content: base.content, 8 | presets: [base], 9 | theme: { 10 | container: { 11 | center: true, 12 | padding: "2rem", 13 | screens: { 14 | "2xl": "1400px", 15 | }, 16 | }, 17 | extend: { 18 | borderRadius: { 19 | lg: "var(--radius)", 20 | md: "calc(var(--radius) - 2px)", 21 | sm: "calc(var(--radius) - 4px)", 22 | }, 23 | keyframes: { 24 | "accordion-down": { 25 | from: { height: "0" }, 26 | to: { height: "var(--radix-accordion-content-height)" }, 27 | }, 28 | "accordion-up": { 29 | from: { height: "var(--radix-accordion-content-height)" }, 30 | to: { height: "0" }, 31 | }, 32 | }, 33 | animation: { 34 | "accordion-down": "accordion-down 0.2s ease-out", 35 | "accordion-up": "accordion-up 0.2s ease-out", 36 | }, 37 | }, 38 | }, 39 | plugins: [animate], 40 | } satisfies Config; 41 | -------------------------------------------------------------------------------- /turbo/generators/templates/eslint.config.js.hbs: -------------------------------------------------------------------------------- 1 | import baseConfig from "@potential/eslint-config/base"; 2 | 3 | /** @type {import('typescript-eslint').Config} */ 4 | export default [ 5 | { 6 | ignores: [], 7 | }, 8 | ...baseConfig, 9 | ]; 10 | -------------------------------------------------------------------------------- /turbo/generators/templates/package.json.hbs: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@potential/{{ name }}", 3 | "private": true, 4 | "version": "0.1.0", 5 | "type": "module", 6 | "exports": { 7 | ".": "./src/index.ts" 8 | }, 9 | "scripts": { 10 | "build": "tsc", 11 | "clean": "git clean -xdf .cache .turbo dist node_modules", 12 | "dev": "tsc", 13 | "format": "prettier --check . --ignore-path ../../.gitignore", 14 | "lint": "eslint", 15 | "typecheck": "tsc --noEmit --emitDeclarationOnly false" 16 | }, 17 | "devDependencies": { 18 | "@potential/eslint-config": "workspace:*", 19 | "@potential/prettier-config": "workspace:*", 20 | "@potential/tsconfig": "workspace:*", 21 | "eslint": "catalog:", 22 | "prettier": "catalog:", 23 | "typescript": "catalog:" 24 | }, 25 | "prettier": "@potential/prettier-config" 26 | } 27 | -------------------------------------------------------------------------------- /turbo/generators/templates/tsconfig.json.hbs: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@potential/tsconfig/internal-package.json", 3 | "compilerOptions": {}, 4 | "include": ["*.ts", "src"], 5 | "exclude": ["node_modules"] 6 | } 7 | --------------------------------------------------------------------------------