├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── cli ├── .gitignore ├── README.md ├── cli.js └── package.json ├── web ├── .eslintrc.json ├── .gitignore ├── README.md ├── components.json ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.js ├── public │ ├── expo-icon.svg │ ├── expo-router-icon.svg │ ├── expo-router.svg │ ├── favicon.ico │ ├── github.svg │ ├── image.png │ ├── next.svg │ ├── prisma-icon.svg │ ├── trpc-icon.svg │ ├── typescript-icon.svg │ ├── uni-stack-banner.png │ ├── unistack-logo.svg │ ├── vercel.svg │ ├── x-twitter.svg │ └── x.svg ├── src │ ├── components │ │ ├── homepage.tsx │ │ ├── magicui │ │ │ ├── retro-grid.tsx │ │ │ ├── shimmer-button.tsx │ │ │ └── text-reveal.tsx │ │ └── ui │ │ │ ├── button.tsx │ │ │ ├── card.tsx │ │ │ ├── hover-effect.tsx │ │ │ ├── my-footer.tsx │ │ │ ├── star-us.tsx │ │ │ └── tooltip.tsx │ ├── lib │ │ └── utils.ts │ ├── pages │ │ ├── _app.tsx │ │ ├── _document.tsx │ │ ├── api │ │ │ └── hello.ts │ │ └── index.tsx │ ├── styles │ │ └── globals.css │ └── utils │ │ └── cn.ts ├── tailwind.config.ts ├── tsconfig.json └── yarn.lock ├── with-gluestack ├── .env.sample ├── .gitignore ├── app.json ├── babel.config.js ├── package.json ├── providers.tsx ├── src │ ├── app │ │ ├── +html.tsx │ │ ├── _layout.tsx │ │ ├── api │ │ │ └── trpc │ │ │ │ └── [trpc]+api.ts │ │ └── index.tsx │ ├── prisma │ │ ├── dev.db │ │ ├── migrations │ │ │ ├── 20240201150705_init │ │ │ │ └── migration.sql │ │ │ └── migration_lock.toml │ │ └── schema.prisma │ ├── providers.tsx │ ├── trpc-server │ │ ├── db.ts │ │ ├── routers │ │ │ └── _app.ts │ │ └── trpc.ts │ └── utils │ │ └── trpc.ts └── tsconfig.json ├── with-nativewind ├── .env.sample ├── .gitignore ├── app.json ├── babel.config.js ├── bun.lockb ├── global.d.ts ├── metro.config.js ├── package.json ├── src │ ├── app │ │ ├── _layout.tsx │ │ ├── api │ │ │ └── trpc │ │ │ │ └── [trpc]+api.ts │ │ └── index.tsx │ ├── global.css │ ├── prisma │ │ ├── dev.db │ │ ├── migrations │ │ │ ├── 20240201150705_init │ │ │ │ └── migration.sql │ │ │ └── migration_lock.toml │ │ └── schema.prisma │ ├── providers.tsx │ ├── trpc-server │ │ ├── db.ts │ │ ├── routers │ │ │ └── _app.ts │ │ └── trpc.ts │ └── utils │ │ └── trpc.ts ├── tailwind.config.js └── tsconfig.json ├── with-tamagui-lab ├── .env.sample ├── .gitignore ├── app.json ├── babel.config.js ├── metro.config.js ├── package.json ├── src │ ├── app │ │ ├── +html.tsx │ │ ├── _layout.tsx │ │ ├── api │ │ │ └── trpc │ │ │ │ └── [trpc]+api.ts │ │ └── index.tsx │ ├── prisma │ │ ├── dev.db │ │ ├── migrations │ │ │ ├── 20240201150705_init │ │ │ │ └── migration.sql │ │ │ └── migration_lock.toml │ │ └── schema.prisma │ ├── providers.tsx │ ├── trpc-server │ │ ├── db.ts │ │ ├── routers │ │ │ └── _app.ts │ │ └── trpc.ts │ └── utils │ │ └── trpc.ts ├── tamagui-web.css ├── tamagui.config.ts └── tsconfig.json └── with-tamagui ├── .env.sample ├── .gitignore ├── app.json ├── babel.config.js ├── biome.json ├── metro.config.js ├── package.json ├── src ├── app │ ├── +html.tsx │ ├── _layout.tsx │ ├── api │ │ └── trpc │ │ │ └── [trpc]+api.ts │ └── index.tsx ├── assets │ ├── fonts │ │ └── SpaceMono-Regular.ttf │ └── images │ │ ├── adaptive-icon.png │ │ ├── favicon.png │ │ ├── icon.png │ │ └── splash.png ├── constants │ └── Colors.ts ├── prisma │ ├── dev.db │ ├── migrations │ │ ├── 20240201150705_init │ │ │ └── migration.sql │ │ └── migration_lock.toml │ └── schema.prisma ├── providers.tsx ├── trpc-server │ ├── db.ts │ ├── routers │ │ └── _app.ts │ └── trpc.ts └── utils │ └── trpc.ts ├── tamagui-web.css ├── tamagui.config.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | node_modules/ 3 | .expo/ 4 | dist/ 5 | npm-debug.* 6 | *.jks 7 | *.p8 8 | *.p12 9 | *.key 10 | *.mobileprovision 11 | *.orig.* 12 | web-build/ 13 | 14 | # macOS 15 | .DS_Store 16 | .idea 17 | **/yarn.lock 18 | **/.env 19 | create-uni-app-cli/ 20 | supabase/ 21 | 22 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are always welcome, no matter how large or small! 4 | 5 | We want this community to be friendly and respectful to each other. Please follow it in all your interactions with the project. Before contributing, please read the [code of conduct](./CODE_OF_CONDUCT.md). 6 | 7 | ## Development workflow 8 | 9 | This project is a monorepo managed using [Yarn workspaces](https://yarnpkg.com/features/workspaces). It contains the following packages: 10 | 11 | - The library package in the root directory. 12 | - An example app in the `example/` directory. 13 | 14 | To get started with the project, run `yarn` in the root directory to install the required dependencies for each package: 15 | 16 | ```sh 17 | yarn 18 | ``` 19 | 20 | > Since the project relies on Yarn workspaces, you cannot use [`npm`](https://github.com/npm/cli) for development. 21 | 22 | The [example app](/example/) demonstrates usage of the library. You need to run it to test any changes you make. 23 | 24 | It is configured to use the local version of the library, so any changes you make to the library's source code will be reflected in the example app. Changes to the library's JavaScript code will be reflected in the example app without a rebuild, but native code changes will require a rebuild of the example app. 25 | 26 | You can use various commands from the root directory to work with the project. 27 | 28 | To start the packager: 29 | 30 | ```sh 31 | yarn example start 32 | ``` 33 | 34 | To run the example app on Android: 35 | 36 | ```sh 37 | yarn example android 38 | ``` 39 | 40 | To run the example app on iOS: 41 | 42 | ```sh 43 | yarn example ios 44 | ``` 45 | 46 | To run the example app on Web: 47 | 48 | ```sh 49 | yarn example web 50 | ``` 51 | 52 | Make sure your code passes TypeScript and ESLint. Run the following to verify: 53 | 54 | ```sh 55 | yarn typecheck 56 | yarn lint 57 | ``` 58 | 59 | To fix formatting errors, run the following: 60 | 61 | ```sh 62 | yarn lint --fix 63 | ``` 64 | 65 | Remember to add tests for your change if possible. Run the unit tests by: 66 | 67 | ```sh 68 | yarn test 69 | ``` 70 | 71 | ### Commit message convention 72 | 73 | We follow the [conventional commits specification](https://www.conventionalcommits.org/en) for our commit messages: 74 | 75 | - `fix`: bug fixes, e.g. fix crash due to deprecated method. 76 | - `feat`: new features, e.g. add new method to the module. 77 | - `refactor`: code refactor, e.g. migrate from class components to hooks. 78 | - `docs`: changes into documentation, e.g. add usage example for the module.. 79 | - `test`: adding or updating tests, e.g. add integration tests using detox. 80 | - `chore`: tooling changes, e.g. change CI config. 81 | 82 | Our pre-commit hooks verify that your commit message matches this format when committing. 83 | 84 | ### Linting and tests 85 | 86 | [ESLint](https://eslint.org/), [Prettier](https://prettier.io/), [TypeScript](https://www.typescriptlang.org/) 87 | 88 | We use [TypeScript](https://www.typescriptlang.org/) for type checking, [ESLint](https://eslint.org/) with [Prettier](https://prettier.io/) for linting and formatting the code, and [Jest](https://jestjs.io/) for testing. 89 | 90 | Our pre-commit hooks verify that the linter and tests pass when committing. 91 | 92 | ### Publishing to npm 93 | 94 | We use [release-it](https://github.com/release-it/release-it) to make it easier to publish new versions. It handles common tasks like bumping version based on semver, creating tags and releases etc. 95 | 96 | To publish new versions, run the following: 97 | 98 | ```sh 99 | yarn release 100 | ``` 101 | 102 | ### Scripts 103 | 104 | The `package.json` file contains various scripts for common tasks: 105 | 106 | - `yarn`: setup project by installing dependencies and pods - run with `POD_INSTALL=0` to skip installing pods. 107 | - `yarn typecheck`: type-check files with TypeScript. 108 | - `yarn lint`: lint files with ESLint. 109 | - `yarn test`: run unit tests with Jest. 110 | - `yarn example start`: start the Metro server for the example app. 111 | - `yarn example android`: run the example app on Android. 112 | - `yarn example ios`: run the example app on iOS. 113 | 114 | ### Sending a pull request 115 | 116 | > **Working on your first pull request?** You can learn how from this _free_ series: [How to Contribute to an Open Source Project on GitHub](https://app.egghead.io/playlists/how-to-contribute-to-an-open-source-project-on-github). 117 | 118 | When you're sending a pull request: 119 | 120 | - Prefer small pull requests focused on one change. 121 | - Verify that linters and tests are passing. 122 | - Review the documentation to make sure it looks good. 123 | - Follow the pull request template when opening a pull request. 124 | - For pull requests that change the API or implementation, discuss with maintainers first by opening an issue. 125 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Rodrigo Figueroa 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [️⚛💻📱UNI STACK](https://www.uni-stack.dev/) 2 | 3 | ### typesafe setup to build fullstack expo universal native apps. 4 | 5 | --- 6 | 7 | ## Usage 8 | 9 | ### Starter installation 10 | 11 | Create a new project with option to select your prefered UI library with: 12 | 13 | ```sh 14 | npx create-uni-app 15 | ``` 16 | 17 | ### Set .env file 18 | 19 | Rename .env.sample to .env 20 | 21 | ```sh 22 | mv .env.sample .env 23 | ``` 24 | 25 | ### Configure your Prisma Client 26 | 27 | ⚠️ you will get a `TypeError: ExpoResponse is not a constructor` error if you don't do this before running the server 28 | 29 | ```sh 30 | npm run prisma:generate 31 | ``` 32 | 33 | ### Run web app and API 34 | 35 | ```sh 36 | npm run web 37 | ``` 38 | 39 | This spins up the web app with expo router API routes 40 | 41 | ### Run native apps 42 | 43 | In a separate terminal run 44 | 45 | ```sh 46 | npm run start 47 | ``` 48 | 49 | In the same window hit I in your keyboard to launch IOS simulator or A for Android emulator 50 | 51 | ## Tech stack 52 | 53 | - Expo v50 54 | - Typescript v5 55 | - Prisma v5.9 56 | - tRPC v11 57 | - Expo Router v3 58 | - UI 59 | - Nativewind v4 60 | - Tamagui 61 | - gluestack-ui 62 | - SQLite database 63 | 64 | ## Prisma 65 | 66 | > SQLite database comes with two tables `users`, and `posts` for you to try out the setup. 67 | 68 | To generate and instantiate Prisma Client run: 69 | 70 | ```sh 71 | npm run prisma:generate 72 | ``` 73 | 74 | The db push command pushes the state of your Prisma schema file to the database without using migrations. 75 | 76 | ```sh 77 | npm run prisma:push 78 | ``` 79 | 80 | Explore and manipulate your data with a web UI 81 | 82 | ```sh 83 | npm run prisma:studio 84 | ``` 85 | 86 | ## Contributing 87 | 88 | See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow. 89 | 90 | ## About Author 91 | 92 | ### Rodrigo Figueroa 93 | 94 | Follow Rodrigo Figueroa, creator of `uni-stack` for updates on the project and universal app development on [x/twitter](https://twitter.com/bidah) 95 | 96 | ## Credits 97 | 98 | - [Nader Dabit](https://x.com/dabit3) for [RN-AI](https://github.com/dabit3/react-native-ai) and using it's CLI setup as a template to build uni-stack CLI. 99 | - [Nishan](https://x.com/nishanbende) for [expo-trpc](https://github.com/intergalacticspacehighway/expo-trpc) starter that served as inspiration to build this setup. 100 | - [Nate Birdman](https://x.com/natebirdman) from [Tamagui](https://tamagui.dev/) for testing this modified version of the Tamagui Expo Router starter and fixing various issues. 101 | - [Sanket Sahu](https://x.com/sanketsahu) from [Gluestack](https://gluestack.io/) for being the first to join the conversation about the project and offer his support. 102 | 103 | ## License 104 | 105 | MIT 106 | -------------------------------------------------------------------------------- /cli/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | rn-ai 4 | .DS_Store 5 | bun.lockb 6 | -------------------------------------------------------------------------------- /cli/README.md: -------------------------------------------------------------------------------- 1 | # Create UNI app 2 | 3 | ``` 4 | npx create-uni-app 5 | ``` 6 | -------------------------------------------------------------------------------- /cli/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import chalk from "chalk"; 4 | import path from "path"; 5 | import fs from "fs"; 6 | import { Command } from "commander"; 7 | import select from "@inquirer/select"; 8 | import { input } from "@inquirer/prompts"; 9 | import { execa, execaCommand } from "execa"; 10 | import ora from "ora"; 11 | import childProcess from "child_process"; 12 | 13 | const log = console.log; 14 | const program = new Command(); 15 | const green = chalk.green; 16 | 17 | const repoUrl = "https://github.com/bidah/uni-stack.git"; 18 | 19 | const isYarnInstalled = () => { 20 | try { 21 | childProcess.execSync("yarn --version"); 22 | return true; 23 | } catch { 24 | return false; 25 | } 26 | }; 27 | 28 | const isBunInstalled = () => { 29 | try { 30 | childProcess.execSync("bun --version"); 31 | return true; 32 | } catch (err) { 33 | return false; 34 | } 35 | }; 36 | 37 | async function main() { 38 | const spinner = ora({ 39 | text: "Creating codebase", 40 | }); 41 | try { 42 | const kebabRegez = /^([a-z]+)(-[a-z0-9]+)*$/; 43 | 44 | program 45 | .name("Create uni app") 46 | .description( 47 | "Typesafe setup to build fullstack expo universal native apps" 48 | ); 49 | 50 | program.parse(process.argv); 51 | 52 | const args = program.args; 53 | let appName = args[0]; 54 | 55 | if (!appName || !kebabRegez.test(args[0])) { 56 | appName = await input({ 57 | message: "Enter your app name", 58 | default: "uni-app", 59 | validate: (d) => { 60 | if (!kebabRegez.test(d)) { 61 | return "please enter your app name in the format of my-app-name"; 62 | } 63 | return true; 64 | }, 65 | }); 66 | } 67 | 68 | const uiLibrary = await select({ 69 | message: "Which UI library do you want to use?", 70 | choices: [ 71 | { name: "Tamagui", value: "tamagui" }, 72 | { name: "Nativewind v4", value: "nativewind" }, 73 | { name: "gluestack-ui", value: "gluestack" }, 74 | ], 75 | default: "Nativewind v4", 76 | }); 77 | 78 | spinner.start(); 79 | await execa("git", ["clone", repoUrl, appName]); 80 | try { 81 | if (uiLibrary === "gluestack") { 82 | await execa("rm", ["-r", `${appName}/with-nativewind`]); 83 | await execa("rm", ["-r", `${appName}/with-tamagui`]); 84 | } else if (uiLibrary === "nativewind") { 85 | await execa("rm", ["-r", `${appName}/with-gluestack`]); 86 | await execa("rm", ["-r", `${appName}/with-tamagui`]); 87 | } else if (uiLibrary === "tamagui") { 88 | await execa("rm", ["-r", `${appName}/with-gluestack`]); 89 | await execa("rm", ["-r", `${appName}/with-nativewind`]); 90 | } 91 | await execa("rm", ["-r", `${appName}/web`]); 92 | await execa("rm", ["-r", `${appName}/cli`]); 93 | await execa("rm", ["-rf", `${appName}/.git`]); 94 | } catch (err) {} 95 | 96 | const pwd = process.cwd(); 97 | 98 | await execaCommand(`mv ${pwd}/${appName}/with-${uiLibrary} ${pwd}`); 99 | await execaCommand(`rm -rf ${appName}`); 100 | await execaCommand(`mv with-${uiLibrary} ${appName}`); 101 | 102 | let packageJson = fs.readFileSync(`${appName}/package.json`, "utf8"); 103 | const packageObj = JSON.parse(packageJson); 104 | packageObj.name = appName; 105 | packageJson = JSON.stringify(packageObj, null, 2); 106 | fs.writeFileSync(`${appName}/package.json`, packageJson); 107 | 108 | spinner.text = ""; 109 | process.chdir(path.join(process.cwd(), appName)); 110 | 111 | spinner.text = ""; 112 | let appStartCommand = ""; 113 | let packageManager = ""; 114 | 115 | if (isBunInstalled()) { 116 | spinner.text = "Installing app dependencies"; 117 | await execaCommand("bun install").pipeStdout(process.stdout); 118 | spinner.text = ""; 119 | appStartCommand = "bun start"; 120 | packageManager = "bun"; 121 | console.log("\n"); 122 | } else if (isYarnInstalled()) { 123 | await execaCommand("yarn").pipeStdout(process.stdout); 124 | appStartCommand = "yarn start"; 125 | packageManager = "yarn"; 126 | } else { 127 | spinner.text = "Installing app dependencies"; 128 | await execa("npm", ["install", "--verbose"]).pipeStdout(process.stdout); 129 | spinner.text = ""; 130 | appStartCommand = "npm start"; 131 | packageManager = "npm"; 132 | } 133 | 134 | spinner.stop(); 135 | process.chdir("../"); 136 | log( 137 | `${green.bold( 138 | "Success!" 139 | )} Created fullstack Expo universal native app setup for ${appName} \n` 140 | ); 141 | 142 | log( 143 | `To get started rename .env.sample to .env ${chalk.cyan( 144 | "mv .env.sample .env" 145 | )} \n` 146 | ); 147 | 148 | log( 149 | `Configure your Prisma Client with: ${chalk.cyan( 150 | packageManager + " " + "run prisma:generate" 151 | )} \n` 152 | ); 153 | log( 154 | `⚠️ You will get a "TypeError: ExpoResponse is not a constructor" error if you don't do this before running the server \n` 155 | ); 156 | log( 157 | `Next run ${chalk.cyan( 158 | packageManager + " " + "web" 159 | )} to spin up the web app with the expo router API routes\n ` 160 | ); 161 | log( 162 | `In the same window hit I in your keyboard to launch IOS simulator or A for Android emulator\n` 163 | ); 164 | log("For more details on how to use the setup visit:"); 165 | log("https://github.com/bidah/uni-stack or http://uni-stack.dev"); 166 | 167 | log(""); 168 | log("┌─────────────────────────────────────────────────┐"); 169 | log("│ Follow ROFI @ http://x.com/bidah for updates on │"); 170 | log("│ the project and universal app development. │"); 171 | log("└─────────────────────────────────────────────────┘"); 172 | 173 | log(""); 174 | 175 | log("┌─────────────────────────────────────────────────┐"); 176 | log("│ Universal State of Mind newsletter │"); 177 | log("│ subscribe → http://dub.sh/usom │"); 178 | log("└─────────────────────────────────────────────────┘"); 179 | 180 | log(""); 181 | } catch (err) { 182 | log("Error: ", err); 183 | log("\n"); 184 | if (err.exitCode == 128) { 185 | log("Error: directory already exists."); 186 | } 187 | spinner.stop(); 188 | } 189 | } 190 | main(); 191 | -------------------------------------------------------------------------------- /cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-uni-app", 3 | "version": "0.0.19", 4 | "description": "Typesafe setup to build fullstack React Native universal apps", 5 | "type": "module", 6 | "bin": "cli.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [ 11 | "React Native", 12 | "Expo", 13 | "Prisma", 14 | "tRPC", 15 | "universal apps" 16 | ], 17 | "author": "Rodrigo Figueroa", 18 | "license": "MIT", 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/bidah/uni-stack" 22 | }, 23 | "dependencies": { 24 | "@inquirer/prompts": "^3.1.1", 25 | "@inquirer/select": "^2.0.0", 26 | "chalk": "^5.3.0", 27 | "cli-spinners": "^2.9.0", 28 | "commander": "^11.0.0", 29 | "execa": "^8.0.1", 30 | "ora": "^7.0.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /web/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 20 | 21 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. 22 | 23 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 24 | 25 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 26 | 27 | ## Learn More 28 | 29 | To learn more about Next.js, take a look at the following resources: 30 | 31 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 32 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 33 | 34 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 35 | 36 | ## Deploy on Vercel 37 | 38 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 39 | 40 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 41 | -------------------------------------------------------------------------------- /web/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/styles/globals.css", 9 | "baseColor": "gray", 10 | "cssVariables": false 11 | }, 12 | "aliases": { 13 | "utils": "@/lib/utils", 14 | "components": "@/components" 15 | } 16 | } -------------------------------------------------------------------------------- /web/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | webpack(config) { 5 | // Grab the existing rule that handles SVG imports 6 | const fileLoaderRule = config.module.rules.find((rule) => 7 | rule.test?.test?.(".svg") 8 | ); 9 | 10 | config.module.rules.push( 11 | // Reapply the existing rule, but only for svg imports ending in ?url 12 | { 13 | ...fileLoaderRule, 14 | test: /\.svg$/i, 15 | resourceQuery: /url/, // *.svg?url 16 | }, 17 | // Convert all other *.svg imports to React components 18 | { 19 | test: /\.svg$/i, 20 | issuer: fileLoaderRule.issuer, 21 | resourceQuery: { not: [...fileLoaderRule.resourceQuery.not, /url/] }, // exclude if *.svg?url 22 | use: ["@svgr/webpack"], 23 | } 24 | ); 25 | 26 | // Modify the file loader rule to ignore *.svg, since we have it handled now. 27 | fileLoaderRule.exclude = /\.svg$/i; 28 | 29 | return config; 30 | }, 31 | }; 32 | 33 | export default nextConfig; 34 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@radix-ui/react-icons": "^1.3.0", 13 | "@radix-ui/react-slot": "^1.0.2", 14 | "@radix-ui/react-tooltip": "^1.0.7", 15 | "@vercel/analytics": "^1.2.2", 16 | "class-variance-authority": "^0.7.0", 17 | "clsx": "^2.1.0", 18 | "framer-motion": "^11.0.5", 19 | "lucide-react": "^0.321.0", 20 | "next": "14.1.0", 21 | "react": "^18", 22 | "react-dom": "^18", 23 | "react-twc": "^1.4.1", 24 | "tailwind-merge": "^2.2.1", 25 | "tailwindcss-animate": "^1.0.7" 26 | }, 27 | "devDependencies": { 28 | "@svgr/webpack": "^8.1.0", 29 | "@types/node": "^20", 30 | "@types/react": "^18", 31 | "@types/react-dom": "^18", 32 | "autoprefixer": "^10.0.1", 33 | "eslint": "^8", 34 | "eslint-config-next": "14.1.0", 35 | "postcss": "^8", 36 | "tailwindcss": "^3.3.0", 37 | "typescript": "^5" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /web/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /web/public/expo-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/public/expo-router-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /web/public/expo-router.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bidah/uni-stack/96d62ca4c6d598a7887ab6d97cf50d3a5cd4bae1/web/public/favicon.ico -------------------------------------------------------------------------------- /web/public/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/public/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bidah/uni-stack/96d62ca4c6d598a7887ab6d97cf50d3a5cd4bae1/web/public/image.png -------------------------------------------------------------------------------- /web/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/public/prisma-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /web/public/trpc-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/public/typescript-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | TypeScript icon -------------------------------------------------------------------------------- /web/public/uni-stack-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bidah/uni-stack/96d62ca4c6d598a7887ab6d97cf50d3a5cd4bae1/web/public/uni-stack-banner.png -------------------------------------------------------------------------------- /web/public/unistack-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /web/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/public/x-twitter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/public/x.svg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bidah/uni-stack/96d62ca4c6d598a7887ab6d97cf50d3a5cd4bae1/web/public/x.svg -------------------------------------------------------------------------------- /web/src/components/homepage.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import Image from "next/image"; 3 | import { CardTitle, CardHeader, CardContent, Card } from "@/components/ui/card"; 4 | import { Button } from "@/components/ui/button"; 5 | import { HoverEffect } from "./ui/hover-effect"; 6 | import { 7 | Tooltip, 8 | TooltipContent, 9 | TooltipProvider, 10 | TooltipTrigger, 11 | } from "./ui/tooltip"; 12 | import RetroGrid from "./magicui/retro-grid"; 13 | import TextRevealByWord from "./magicui/text-reveal"; 14 | import ShimmerButton from "./magicui/shimmer-button"; 15 | import { useState } from "react"; 16 | import { MyFooter } from "./ui/my-footer"; 17 | import StarUs from "./ui/star-us"; 18 | import UnistackLogo from "../../public/unistack-logo.svg"; 19 | 20 | const items = [ 21 | { 22 | title: "Expo v50", 23 | description: 24 | "Framework for making universal native apps with React Native. This Expo setup is configured to be multi platform. It runs on Android, iOS, and the web.", 25 | link: "https://expo.dev", 26 | svg: "expo-icon", 27 | }, 28 | { 29 | title: "Expo Router v3", 30 | description: 31 | "Expo Router is a versatile router for React Native and web apps based on file-system routing that also support API routes.", 32 | link: "https://docs.expo.dev/router/introduction/", 33 | svg: "expo-router-icon", 34 | }, 35 | { 36 | title: "Prisma v5.9", 37 | description: 38 | "Prisma enhances the developer experience in working with your database, offering an intuitive data model, automated migrations, type-safety, and auto-completion.", 39 | link: "https://www.prisma.io", 40 | svg: "prisma-icon", 41 | }, 42 | { 43 | title: "tRPC v11", 44 | description: 45 | "End-to-end typesafe APIs. tRPC is like using an SDK for your API's server code, giving you confidence in your endpoints.", 46 | link: "https://trpc.io/", 47 | svg: "trpc-icon", 48 | }, 49 | { 50 | title: "Nativewind v4 • Tamagui • gluestack-ui", 51 | description: 52 | "Cherry pick your UI driver. We have 3 options which bring in the best configurations for your app to have a strong theme setup and UI.", 53 | links: [ 54 | { 55 | title: "Nativewind", 56 | link: "https://www.nativewind.dev/v4/overview", 57 | }, 58 | { 59 | title: "Tamagui", 60 | link: "http://www.tamagui.dev", 61 | }, 62 | { 63 | title: "gluestack-ui", 64 | link: "https://gluestack.io/ui/docs/overview/introduction", 65 | }, 66 | ], 67 | }, 68 | { 69 | title: "Typescript v5", 70 | description: 71 | "TypeScript improves codebase by adding static typing, enhanced tooling, better scalability, and easier collaboration, resulting in more maintainable and reliable software. ", 72 | link: "https://www.typescriptlang.org", 73 | svg: "typescript-icon", 74 | }, 75 | ]; 76 | export function Homepage() { 77 | const [copied, setCopied] = useState(false); 78 | 79 | const handleCopyClick = () => { 80 | navigator.clipboard.writeText("npx create-uni-app@latest"); 81 | setCopied(true); 82 | setTimeout(() => setCopied(false), 2000); // Reset after 2 seconds 83 | }; 84 | 85 | return ( 86 |
90 |
91 |
92 | 93 |

UNI STACK

94 |
95 | {typeof window !== "undefined" && window.innerWidth > 768 && } 96 |
97 | 102 | SVG Image 108 | 109 | 114 | SVG Image 115 | 116 |
117 |
118 | 119 |
120 | 121 |
122 |
123 |
124 |

125 | 126 | ️⚛ 127 | 💻 128 | 📱 129 | 130 | 131 | typesafe setup to build fullstack React Native universal apps 132 |

133 | 134 |
135 | 136 |

npx create-uni-app@latest

137 | {copied ? : } 138 |
139 |
140 | {/*

141 | Running everywhere using Expo, Trpc, Prisma and Nativewind. 142 |

*/} 143 |
144 |
145 |
146 |
147 | 148 |
149 |
150 | 151 |
152 |
153 | 154 |
155 | {/* */} 156 | 157 |
158 | {/*
159 |
160 |
161 |

162 | get started now 163 |

164 |
165 | npx create-uni-app 166 | 167 | 168 | 169 | 170 | 179 | 180 | 181 | Copy to clipboard 182 | 183 | 184 | 185 |
186 |
187 |
188 |
*/} 189 | 190 | {/*
191 |
192 | dashboard 199 | 200 |
201 |
*/} 202 | 203 | {/* hello there */} 204 | 217 |
218 | ); 219 | } 220 | 221 | function TickIcon(props) { 222 | return ( 223 | 235 | 236 | 237 | ); 238 | } 239 | 240 | function CopyIcon(props) { 241 | return ( 242 | 254 | 255 | 256 | 257 | ); 258 | } 259 | -------------------------------------------------------------------------------- /web/src/components/magicui/retro-grid.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | 3 | export default function RetroGrid({ className }: { className?: string }) { 4 | return ( 5 |
11 | {/* Grid */} 12 |
13 |
26 |
27 | 28 | {/* Background Gradient */} 29 |
30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /web/src/components/magicui/shimmer-button.tsx: -------------------------------------------------------------------------------- 1 | import React, { type CSSProperties } from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | export interface ShimmerButtonProps 6 | extends React.ButtonHTMLAttributes { 7 | shimmerColor?: string; 8 | shimmerSize?: string; 9 | borderRadius?: string; 10 | shimmerDuration?: string; 11 | background?: string; 12 | className?: string; 13 | children?: React.ReactNode; 14 | } 15 | 16 | const ShimmerButton = React.forwardRef( 17 | ( 18 | { 19 | shimmerColor = "#ffffff", 20 | shimmerSize = "0.05em", 21 | shimmerDuration = "3s", 22 | borderRadius = "100px", 23 | background = "rgba(0, 0, 1)", 24 | className, 25 | children, 26 | ...props 27 | }, 28 | ref 29 | ) => { 30 | return ( 31 | 92 | ); 93 | } 94 | ); 95 | 96 | ShimmerButton.displayName = "ShimmerButton"; 97 | 98 | export default ShimmerButton; 99 | -------------------------------------------------------------------------------- /web/src/components/magicui/text-reveal.tsx: -------------------------------------------------------------------------------- 1 | import { type FC, type ReactNode, useRef } from "react"; 2 | import { motion, useScroll, useTransform } from "framer-motion"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | interface TextRevealByWordProps { 7 | text: string; 8 | className?: string; 9 | } 10 | 11 | export const TextRevealByWord: FC = ({ 12 | text, 13 | className, 14 | }) => { 15 | const targetRef = useRef(null); 16 | 17 | const { scrollYProgress } = useScroll({ 18 | target: targetRef, 19 | }); 20 | const words = text.split(" "); 21 | 22 | return ( 23 |
24 |
29 |

35 | {words.map((word, i) => { 36 | const start = i / words.length; 37 | const end = start + 1 / words.length; 38 | return ( 39 | 40 | {word} 41 | 42 | ); 43 | })} 44 |

45 |
46 |
47 | ); 48 | }; 49 | 50 | interface WordProps { 51 | children: ReactNode; 52 | progress: any; 53 | range: [number, number]; 54 | } 55 | 56 | const Word: FC = ({ children, progress, range }) => { 57 | const opacity = useTransform(progress, range, [0, 1]); 58 | return ( 59 | 60 | {children} 61 | 62 | {children} 63 | 64 | 65 | ); 66 | }; 67 | 68 | export default TextRevealByWord; 69 | -------------------------------------------------------------------------------- /web/src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 ring-offset-gray-950 focus-visible:ring-gray-300", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-gray-900 text-gray-50 hover:bg-gray-900/90 bg-gray-50 text-gray-900 hover:bg-gray-50/90", 14 | destructive: 15 | "bg-red-500 text-gray-50 hover:bg-red-500/90 bg-red-900 text-gray-50 hover:bg-red-900/90", 16 | outline: 17 | "border border-gray-200 bg-white hover:bg-gray-100 hover:text-gray-900 border-gray-800 bg-gray-950 hover:bg-gray-800 hover:text-gray-50", 18 | secondary: 19 | "bg-gray-100 text-gray-900 hover:bg-gray-100/80 bg-gray-800 text-gray-50 hover:bg-gray-800/80", 20 | ghost: 21 | "hover:bg-gray-100 hover:text-gray-900 hover:bg-gray-800 hover:text-gray-50", 22 | link: "text-gray-900 underline-offset-4 hover:underline text-gray-50", 23 | }, 24 | size: { 25 | default: "h-10 px-4 py-2", 26 | sm: "h-9 rounded-md px-3", 27 | lg: "h-11 rounded-md px-8", 28 | icon: "h-10 w-10", 29 | }, 30 | }, 31 | defaultVariants: { 32 | variant: "default", 33 | size: "default", 34 | }, 35 | } 36 | ); 37 | 38 | export 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 : "button"; 47 | return ( 48 | 53 | ); 54 | } 55 | ); 56 | Button.displayName = "Button"; 57 | 58 | export { Button, buttonVariants }; 59 | -------------------------------------------------------------------------------- /web/src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )); 18 | Card.displayName = "Card"; 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )); 30 | CardHeader.displayName = "CardHeader"; 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

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

52 | )); 53 | CardDescription.displayName = "CardDescription"; 54 | 55 | const CardContent = React.forwardRef< 56 | HTMLDivElement, 57 | React.HTMLAttributes 58 | >(({ className, ...props }, ref) => ( 59 |

64 | )); 65 | CardContent.displayName = "CardContent"; 66 | 67 | const CardIcon = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )); 77 | CardIcon.displayName = "CardIcon"; 78 | 79 | const CardFooter = React.forwardRef< 80 | HTMLDivElement, 81 | React.HTMLAttributes 82 | >(({ className, ...props }, ref) => ( 83 |
88 | )); 89 | CardFooter.displayName = "CardFooter"; 90 | 91 | export { 92 | CardIcon, 93 | Card, 94 | CardHeader, 95 | CardFooter, 96 | CardTitle, 97 | CardDescription, 98 | CardContent, 99 | }; 100 | -------------------------------------------------------------------------------- /web/src/components/ui/hover-effect.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/utils/cn"; 2 | import { AnimatePresence, motion } from "framer-motion"; 3 | import Link from "next/link"; 4 | import { useState } from "react"; 5 | import { 6 | CardTitle, 7 | CardHeader, 8 | CardContent, 9 | Card, 10 | CardIcon, 11 | } from "@/components/ui/card"; 12 | import { Button } from "./button"; 13 | import TrpcIcon from "../../../public/trpc-icon.svg"; 14 | import ExpoIcon from "../../../public/expo-icon.svg"; 15 | import TypescriptIcon from "../../../public/typescript-icon.svg"; 16 | import ExpoRouterIcon from "../../../public/expo-router-icon.svg"; 17 | import PrismaIcon from "../../../public/prisma-icon.svg"; 18 | 19 | const ICONS = { 20 | "expo-router-icon": , 21 | "expo-icon": , 22 | "typescript-icon": , 23 | "trpc-icon": , 24 | "prisma-icon": , 25 | }; 26 | 27 | export const HoverEffect = ({ 28 | items, 29 | className, 30 | }: { 31 | items: { 32 | title: string; 33 | description: string; 34 | link?: string; 35 | links?: Array<{ title: string; link: string }>; 36 | svg?: string; 37 | }[]; 38 | className?: string; 39 | }) => { 40 | const [hoveredIndex, setHoveredIndex] = useState(null); 41 | 42 | return ( 43 |
49 | {items.map((item, idx) => { 50 | return item.links ? ( 51 |
setHoveredIndex(idx)} 55 | onMouseLeave={() => setHoveredIndex(null)} 56 | > 57 | 58 | {hoveredIndex === idx && ( 59 | 72 | )} 73 | 74 | <_Card> 75 | 76 | {item.title} 77 | 78 | 79 | {item.description} 80 |
81 | {item.links?.map((linkItem, linkIdx) => ( 82 | 83 | →  84 | 91 | 92 | ))} 93 |
94 | 95 |
96 | ) : ( 97 | (!item.link ? e.preventDefault() : "")} 99 | href={item?.link ?? ""} 100 | target="_blank" 101 | key={idx} 102 | className="relative group block p-2 h-full w-full" 103 | onMouseEnter={() => setHoveredIndex(idx)} 104 | onMouseLeave={() => setHoveredIndex(null)} 105 | > 106 | 107 | {hoveredIndex === idx && ( 108 | 121 | )} 122 | 123 | <_Card> 124 | {/* {item.svg && item.svg()} */} 125 | 126 | {/* */} 127 | {item.svg && ICONS[item?.svg]} 128 | 129 | 130 | {item.title} 131 | 132 | {item.description} 133 | 134 | 135 | ); 136 | })} 137 |
138 | ); 139 | }; 140 | 141 | export const _Card = ({ 142 | className, 143 | children, 144 | }: { 145 | className?: string; 146 | children: React.ReactNode; 147 | }) => { 148 | return ( 149 |
155 |
156 |
{children}
157 |
158 |
159 | ); 160 | }; 161 | export const _CardTitle = ({ 162 | className, 163 | children, 164 | }: { 165 | className?: string; 166 | children: React.ReactNode; 167 | }) => { 168 | return ( 169 |

170 | {children} 171 |

172 | ); 173 | }; 174 | export const _CardDescription = ({ 175 | className, 176 | children, 177 | }: { 178 | className?: string; 179 | children: React.ReactNode; 180 | }) => { 181 | return ( 182 |

188 | {children} 189 |

190 | ); 191 | }; 192 | -------------------------------------------------------------------------------- /web/src/components/ui/my-footer.tsx: -------------------------------------------------------------------------------- 1 | import { twc } from "react-twc"; 2 | 3 | export const MyFooter = twc.div`container px-4 md:px-6 w-full rounded-lg border bg-slate-400 text-white shadow-sm`; 4 | -------------------------------------------------------------------------------- /web/src/components/ui/star-us.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * v0 by Vercel. 3 | * @see https://v0.dev/t/ObKvHXx87gt 4 | * Documentation: https://v0.dev/docs#integrating-generated-code-into-your-nextjs-app 5 | */ 6 | import { Button } from "@/components/ui/button"; 7 | import { useEffect, useState } from "react"; 8 | 9 | export default function StarUs() { 10 | const [starCount, setStarCount] = useState(null); 11 | 12 | useEffect(() => { 13 | async function fetchStarCount() { 14 | try { 15 | const response = await fetch( 16 | "https://api.github.com/repos/bidah/uni-stack" 17 | ); 18 | const data = await response.json(); 19 | setStarCount(data.stargazers_count); 20 | } catch (error) { 21 | console.error("Error fetching star count:", error); 22 | } 23 | } 24 | 25 | fetchStarCount(); 26 | }, []); 27 | 28 | return ( 29 | 35 | 36 | Star on GitHub 37 | 49 | star 50 | 51 | 52 | 53 | {starCount !== null ? starCount.toLocaleString() : ""} 54 | 55 | 56 | ); 57 | } 58 | 59 | function GithubIcon(props) { 60 | return ( 61 | 73 | github icon 74 | 75 | 76 | 77 | ); 78 | } 79 | 80 | function XIcon(props) { 81 | return ( 82 | 94 | x icon 95 | 96 | 97 | 98 | ); 99 | } 100 | -------------------------------------------------------------------------------- /web/src/components/ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | const TooltipProvider = TooltipPrimitive.Provider; 7 | 8 | const Tooltip = TooltipPrimitive.Root; 9 | 10 | const TooltipTrigger = TooltipPrimitive.Trigger; 11 | 12 | const TooltipContent = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef 15 | >(({ className, sideOffset = 4, ...props }, ref) => ( 16 | 25 | )); 26 | TooltipContent.displayName = TooltipPrimitive.Content.displayName; 27 | 28 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; 29 | -------------------------------------------------------------------------------- /web/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /web/src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "@/styles/globals.css"; 2 | import type { AppProps } from "next/app"; 3 | import { Analytics } from "@vercel/analytics/react"; 4 | 5 | export default function App({ Component, pageProps }: AppProps) { 6 | return ( 7 | <> 8 | 9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /web/src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from "next/document"; 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | UNI STACK 8 | 9 | 13 | 14 | 15 | 16 | 20 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /web/src/pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from "next"; 3 | 4 | type Data = { 5 | name: string; 6 | }; 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse, 11 | ) { 12 | res.status(200).json({ name: "John Doe" }); 13 | } 14 | -------------------------------------------------------------------------------- /web/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import { Inter } from "next/font/google"; 3 | import { Homepage } from "@/components/homepage"; 4 | 5 | const inter = Inter({ subsets: ["latin"] }); 6 | 7 | export default function Home() { 8 | return ( 9 |
10 | ; 11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /web/src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-start-rgb: 214, 219, 220; 8 | --background-end-rgb: 255, 255, 255; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --foreground-rgb: 255, 255, 255; 14 | --background-start-rgb: 0, 0, 0; 15 | --background-end-rgb: 0, 0, 0; 16 | } 17 | } 18 | 19 | body { 20 | color: rgb(var(--foreground-rgb)); 21 | background: linear-gradient( 22 | to bottom, 23 | transparent, 24 | rgb(var(--background-end-rgb)) 25 | ) 26 | rgb(var(--background-start-rgb)); 27 | } 28 | 29 | @layer utilities { 30 | .text-balance { 31 | text-wrap: balance; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /web/src/utils/cn.ts: -------------------------------------------------------------------------------- 1 | import { ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /web/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | content: [ 5 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | theme: { 10 | extend: { 11 | backgroundImage: { 12 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", 13 | "gradient-conic": 14 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", 15 | }, 16 | animation: { 17 | grid: "grid 15s linear infinite", 18 | "spin-around": "spin-around calc(var(--speed) * 2) infinite linear", 19 | slide: "slide var(--speed) ease-in-out infinite alternate", 20 | }, 21 | keyframes: { 22 | grid: { 23 | "0%": { transform: "translateY(-50%)" }, 24 | "100%": { transform: "translateY(0)" }, 25 | }, 26 | "spin-around": { 27 | "0%": { 28 | transform: "translateZ(0) rotate(0)", 29 | }, 30 | "15%, 35%": { 31 | transform: "translateZ(0) rotate(90deg)", 32 | }, 33 | "65%, 85%": { 34 | transform: "translateZ(0) rotate(270deg)", 35 | }, 36 | "100%": { 37 | transform: "translateZ(0) rotate(360deg)", 38 | }, 39 | }, 40 | slide: { 41 | to: { 42 | transform: "translate(calc(100cqw - 100%), 0)", 43 | }, 44 | }, 45 | }, 46 | }, 47 | }, 48 | plugins: [require("tailwindcss-animate")], 49 | }; 50 | export default config; 51 | -------------------------------------------------------------------------------- /web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "dom", 5 | "dom.iterable", 6 | "esnext" 7 | ], 8 | "allowJs": true, 9 | "skipLibCheck": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "bundler", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve", 17 | "incremental": true, 18 | "paths": { 19 | "@/*": [ 20 | "./src/*" 21 | ] 22 | }, 23 | "strict": false 24 | }, 25 | "include": [ 26 | "next-env.d.ts", 27 | "**/*.ts", 28 | "**/*.tsx" 29 | ], 30 | "exclude": [ 31 | "node_modules" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /with-gluestack/.env.sample: -------------------------------------------------------------------------------- 1 | # Environment variables declared in this file are automatically made available to Prisma. 2 | # See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema 3 | 4 | # Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB. 5 | # See the documentation for all the connection string options: https://pris.ly/d/connection-strings 6 | 7 | DATABASE_URL="file:./dev.db" -------------------------------------------------------------------------------- /with-gluestack/.gitignore: -------------------------------------------------------------------------------- 1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files 2 | 3 | # dependencies 4 | node_modules/ 5 | 6 | # Expo 7 | .expo/ 8 | dist/ 9 | web-build/ 10 | 11 | # Native 12 | *.orig.* 13 | *.jks 14 | *.p8 15 | *.p12 16 | *.key 17 | *.mobileprovision 18 | 19 | # Metro 20 | .metro-health-check* 21 | 22 | # debug 23 | npm-debug.* 24 | yarn-debug.* 25 | yarn-error.* 26 | 27 | # macOS 28 | .DS_Store 29 | *.pem 30 | 31 | # local env files 32 | .env*.local 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | 37 | # @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb 38 | # The following patterns were generated by expo-cli 39 | 40 | expo-env.d.ts 41 | # @end expo-cli -------------------------------------------------------------------------------- /with-gluestack/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "uni-stack-with-gluestack", 4 | "slug": "uni-stack-with-gluestack", 5 | "scheme": "com.uni.gluestack", 6 | "version": "0.0.1", 7 | "orientation": "portrait", 8 | "userInterfaceStyle": "automatic", 9 | "assetBundlePatterns": ["**/*"], 10 | "web": { 11 | "output": "server", 12 | "bundler": "metro" 13 | }, 14 | "plugins": [ 15 | [ 16 | "expo-router", 17 | { 18 | "origin": "https://n" 19 | } 20 | ] 21 | ], 22 | "experiments": { 23 | "typedRoutes": true 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /with-gluestack/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'] 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /with-gluestack/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "expo-with-gluestack-ui", 3 | "main": "expo-router/entry", 4 | "version": "1.0.0", 5 | "scripts": { 6 | "start": "expo start", 7 | "android": "expo start --android", 8 | "ios": "expo start --ios", 9 | "web": "expo start --web", 10 | "prisma:studio": "npx prisma studio --schema src/prisma/schema.prisma", 11 | "prisma:generate": "npx prisma generate --schema src/prisma/schema.prisma", 12 | "prisma:push": "npx prisma db push --schema src/prisma/schema.prisma", 13 | "prisma:pull": "npx prisma db pull --schema src/prisma/schema.prisma", 14 | "prisma:introspect": "npx prisma introspect --schema src/prisma/schema.prisma" 15 | }, 16 | "jest": { 17 | "preset": "jest-expo" 18 | }, 19 | "dependencies": { 20 | "@expo/vector-icons": "^14.0.0", 21 | "@gluestack-style/react": "^1.0.46", 22 | "@gluestack-ui/config": "^1.1.2", 23 | "@gluestack-ui/themed": "^1.1.4", 24 | "@prisma/client": "^5.9.1", 25 | "@react-navigation/native": "^6.0.2", 26 | "@tanstack/react-query": "^5.20.1", 27 | "@trpc/client": "^10.45.1", 28 | "@trpc/react-query": "^10.45.1", 29 | "@trpc/server": "^10.45.1", 30 | "expo": "~50.0.6", 31 | "expo-font": "~11.10.2", 32 | "expo-linking": "~6.2.2", 33 | "expo-router": "~3.4.7", 34 | "expo-splash-screen": "~0.26.4", 35 | "expo-status-bar": "~1.11.1", 36 | "expo-system-ui": "~2.9.3", 37 | "expo-web-browser": "~12.8.2", 38 | "react": "18.2.0", 39 | "react-dom": "18.2.0", 40 | "react-native": "0.73.4", 41 | "react-native-safe-area-context": "4.8.2", 42 | "react-native-screens": "~3.29.0", 43 | "react-native-svg": "13.4.0", 44 | "react-native-web": "~0.19.6", 45 | "zod": "^3.22.4", 46 | "superjson": "^2.2.1" 47 | }, 48 | "devDependencies": { 49 | "@babel/core": "^7.20.0", 50 | "@types/react": "~18.2.45", 51 | "jest": "^29.2.1", 52 | "jest-expo": "~50.0.2", 53 | "react-test-renderer": "18.2.0", 54 | "typescript": "^5.1.3", 55 | "prisma": "^5.9.1" 56 | }, 57 | "private": true 58 | } 59 | -------------------------------------------------------------------------------- /with-gluestack/providers.tsx: -------------------------------------------------------------------------------- 1 | import { GluestackUIProvider, Text, Box } from "@gluestack-ui/themed"; 2 | import { config } from "@gluestack-ui/config"; // Optional if you want to use default theme 3 | import React, { ReactNode } from "react"; 4 | 5 | interface ProvidersProps { 6 | children: ReactNode; 7 | } 8 | 9 | export default function Providers({ children }: ProvidersProps) { 10 | return {children}; 11 | } 12 | -------------------------------------------------------------------------------- /with-gluestack/src/app/+html.tsx: -------------------------------------------------------------------------------- 1 | import { ScrollViewStyleReset } from 'expo-router/html'; 2 | 3 | // This file is web-only and used to configure the root HTML for every 4 | // web page during static rendering. 5 | // The contents of this function only run in Node.js environments and 6 | // do not have access to the DOM or browser APIs. 7 | export default function Root({ children }: { children: React.ReactNode }) { 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 | 15 | {/* 16 | Disable body scrolling on web. This makes ScrollView components work closer to how they do on native. 17 | However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line. 18 | */} 19 | 20 | 21 | {/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */} 22 |