├── .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 |
--------------------------------------------------------------------------------
/web/public/expo-router-icon.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/web/public/expo-router.svg:
--------------------------------------------------------------------------------
1 |
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 |
--------------------------------------------------------------------------------
/web/public/trpc-icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/web/public/typescript-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/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 |
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 |
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 |
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 |
199 |
200 |
201 | */}
202 |
203 | {/* hello there */}
204 |
217 |
218 | );
219 | }
220 |
221 | function TickIcon(props) {
222 | return (
223 |
237 | );
238 | }
239 |
240 | function CopyIcon(props) {
241 | return (
242 |
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 |
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 |
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 |
52 |
53 | {starCount !== null ? starCount.toLocaleString() : ""}
54 |
55 |
56 | );
57 | }
58 |
59 | function GithubIcon(props) {
60 | return (
61 |
77 | );
78 | }
79 |
80 | function XIcon(props) {
81 | return (
82 |
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 |
23 | {/* Add any additional elements that you want globally available on web... */}
24 |
25 | {children}
26 |
27 | );
28 | }
29 |
30 | const responsiveBackground = `
31 | body {
32 | background-color: #fff;
33 | }
34 | @media (prefers-color-scheme: dark) {
35 | body {
36 | background-color: #000;
37 | }
38 | }`;
39 |
--------------------------------------------------------------------------------
/with-gluestack/src/app/_layout.tsx:
--------------------------------------------------------------------------------
1 | import { Slot } from "expo-router";
2 | import Providers from "@/providers";
3 |
4 | export default function Layout() {
5 | return (
6 |
7 |
8 |
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/with-gluestack/src/app/api/trpc/[trpc]+api.ts:
--------------------------------------------------------------------------------
1 | import { ExpoRequest, ExpoResponse } from "expo-router/server";
2 | import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
3 | import { appRouter } from "@/trpc-server/routers/_app";
4 |
5 | export async function GET(req: ExpoRequest) {
6 | return fetchRequestHandler({
7 | endpoint: "/api/trpc",
8 | req: req as unknown as Request,
9 | router: appRouter,
10 | createContext: () => ({}),
11 | });
12 | }
13 |
14 | export async function POST(req: ExpoRequest) {
15 | return fetchRequestHandler({
16 | endpoint: "/api/trpc",
17 | req: req as unknown as Request,
18 | router: appRouter,
19 | createContext: () => ({}),
20 | });
21 | }
22 |
--------------------------------------------------------------------------------
/with-gluestack/src/app/index.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Text } from "@gluestack-ui/themed";
2 | import { trpc } from "@/utils/trpc";
3 |
4 | const Home = () => {
5 | const firstPost = trpc.firstPost.useQuery();
6 | return (
7 | //
13 | // Open up App.js to start working on your app!
14 | //
15 |
16 |
17 |
18 | {firstPost.data?.title}
19 |
20 |
21 |
22 | {firstPost.data?.content}
23 |
24 |
25 |
26 | );
27 | };
28 |
29 | export default Home;
30 |
--------------------------------------------------------------------------------
/with-gluestack/src/prisma/dev.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bidah/uni-stack/96d62ca4c6d598a7887ab6d97cf50d3a5cd4bae1/with-gluestack/src/prisma/dev.db
--------------------------------------------------------------------------------
/with-gluestack/src/prisma/migrations/20240201150705_init/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "User" (
3 | "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
4 | "email" TEXT NOT NULL,
5 | "name" TEXT
6 | );
7 |
8 | -- CreateTable
9 | CREATE TABLE "Post" (
10 | "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
11 | "title" TEXT NOT NULL,
12 | "content" TEXT,
13 | "published" BOOLEAN NOT NULL DEFAULT false,
14 | "authorId" INTEGER NOT NULL,
15 | CONSTRAINT "Post_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
16 | );
17 |
18 | -- CreateIndex
19 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
20 |
--------------------------------------------------------------------------------
/with-gluestack/src/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (i.e. Git)
3 | provider = "sqlite"
--------------------------------------------------------------------------------
/with-gluestack/src/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | generator client {
2 | provider = "prisma-client-js"
3 | }
4 |
5 | datasource db {
6 | provider = "sqlite"
7 | url = env("DATABASE_URL")
8 | }
9 |
10 | model User {
11 | id Int @id @default(autoincrement())
12 | email String @unique
13 | name String?
14 | posts Post[]
15 | }
16 |
17 | model Post {
18 | id Int @id @default(autoincrement())
19 | title String
20 | content String?
21 | published Boolean @default(false)
22 | authorId Int
23 | author User @relation(fields: [authorId], references: [id])
24 | }
25 |
--------------------------------------------------------------------------------
/with-gluestack/src/providers.tsx:
--------------------------------------------------------------------------------
1 | import { trpc } from "@/utils/trpc";
2 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
3 | import { httpBatchLink } from "@trpc/client";
4 | import React, { useState } from "react";
5 | import { GluestackUIProvider } from "@gluestack-ui/themed";
6 | import { config } from "@gluestack-ui/config"; // Optional if you want to use default theme
7 | import superjson from "superjson";
8 |
9 | function Providers({ children }: { children: React.ReactNode }) {
10 | const [queryClient] = useState(() => new QueryClient());
11 | const [trpcClient] = useState(() =>
12 | trpc.createClient({
13 | links: [
14 | httpBatchLink({
15 | url: "http://localhost:8081/api/trpc",
16 | }),
17 | ],
18 | transformer: superjson,
19 | })
20 | );
21 |
22 | return (
23 |
24 |
25 |
26 | {children}
27 |
28 |
29 |
30 | );
31 | }
32 |
33 | export default Providers;
34 |
--------------------------------------------------------------------------------
/with-gluestack/src/trpc-server/db.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client";
2 |
3 | export const db = new PrismaClient({
4 | log: ["query", "error", "warn"],
5 | });
6 |
--------------------------------------------------------------------------------
/with-gluestack/src/trpc-server/routers/_app.ts:
--------------------------------------------------------------------------------
1 | import { procedure, router } from "../trpc";
2 | import { db } from "@/trpc-server/db";
3 |
4 | export const appRouter = router({
5 | health: procedure.query(({ ctx }) => {
6 | return "hello";
7 | }),
8 | firstPost: procedure.query(async ({ ctx }) => {
9 | return db.post.findFirst();
10 | }),
11 | });
12 |
13 | export type AppRouter = typeof appRouter;
14 |
--------------------------------------------------------------------------------
/with-gluestack/src/trpc-server/trpc.ts:
--------------------------------------------------------------------------------
1 | import { initTRPC } from "@trpc/server";
2 | import { db } from "@/trpc-server/db";
3 | import SuperJSON from "superjson";
4 |
5 | const t = initTRPC.create({
6 | transformer: SuperJSON,
7 | });
8 |
9 | // Base router and procedure helpers
10 | export const router = t.router;
11 | export const procedure = t.procedure;
12 |
--------------------------------------------------------------------------------
/with-gluestack/src/utils/trpc.ts:
--------------------------------------------------------------------------------
1 | import { createTRPCReact } from "@trpc/react-query";
2 | import { AppRouter } from "@/trpc-server/routers/_app";
3 |
4 | export const trpc = createTRPCReact();
5 |
--------------------------------------------------------------------------------
/with-gluestack/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "expo/tsconfig.base",
3 | "compilerOptions": {
4 | "baseUrl": ".",
5 | "paths": {
6 | "@/*": ["./src/*"]
7 | }
8 | },
9 | "include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/with-nativewind/.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-nativewind/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .expo/
3 | dist/
4 | npm-debug.*
5 | *.jks
6 | *.p8
7 | *.p12
8 | *.key
9 | *.mobileprovision
10 | *.orig.*
11 | web-build/
12 |
13 | # macOS
14 | .DS_Store
15 | .idea
16 |
--------------------------------------------------------------------------------
/with-nativewind/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "uni-stack-with-nativewind",
4 | "slug": "uni-stack-with-nativewind",
5 | "scheme": "com.uni.nativewind",
6 | "userInterfaceStyle": "automatic",
7 | "orientation": "portrait",
8 | "version": "0.0.1",
9 | "web": {
10 | "output": "server",
11 | "bundler": "metro"
12 | },
13 | "plugins": [
14 | [
15 | "expo-router",
16 | {
17 | "origin": "https://n"
18 | }
19 | ]
20 | ],
21 | "experiments": {
22 | "typedRoutes": true
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/with-nativewind/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function (api) {
2 | api.cache(true);
3 | return {
4 | presets: [
5 | ["babel-preset-expo", { jsxImportSource: "nativewind" }],
6 | "nativewind/babel",
7 | ],
8 | };
9 | };
10 |
--------------------------------------------------------------------------------
/with-nativewind/bun.lockb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bidah/uni-stack/96d62ca4c6d598a7887ab6d97cf50d3a5cd4bae1/with-nativewind/bun.lockb
--------------------------------------------------------------------------------
/with-nativewind/global.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/with-nativewind/metro.config.js:
--------------------------------------------------------------------------------
1 | const { getDefaultConfig } = require("expo/metro-config");
2 | const { withNativeWind } = require("nativewind/metro");
3 |
4 | const config = getDefaultConfig(__dirname);
5 |
6 | module.exports = withNativeWind(config, { input: "./src/global.css" });
7 |
--------------------------------------------------------------------------------
/with-nativewind/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "with-nativewind",
3 | "version": "1.0.0",
4 | "main": "expo-router/entry",
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 | "dependencies": {
17 | "@prisma/client": "^5.9.1",
18 | "@tanstack/react-query": "^5.18.0",
19 | "@trpc/client": "^10.45.0",
20 | "@trpc/react-query": "^10.45.0",
21 | "@trpc/server": "^10.45.0",
22 | "expo": "^50.0.1",
23 | "expo-constants": "~15.4.2",
24 | "expo-linking": "~6.2.1",
25 | "expo-router": "~3.4.1",
26 | "expo-splash-screen": "~0.26.1",
27 | "expo-status-bar": "~1.11.1",
28 | "nativewind": "^4.0.1",
29 | "react": "18.2.0",
30 | "react-dom": "18.2.0",
31 | "react-native": "0.73.2",
32 | "react-native-reanimated": "~3.6.0",
33 | "react-native-safe-area-context": "4.8.2",
34 | "react-native-screens": "~3.29.0",
35 | "react-native-web": "~0.19.6",
36 | "tailwindcss": "^3.4.0",
37 | "zod": "^3.22.4",
38 | "superjson": "^2.2.1"
39 | },
40 | "devDependencies": {
41 | "@babel/core": "^7.20.0",
42 | "@types/react": "~18.2.45",
43 | "prisma": "^5.9.1",
44 | "typescript": "^5.3.0"
45 | },
46 | "private": true
47 | }
48 |
--------------------------------------------------------------------------------
/with-nativewind/src/app/_layout.tsx:
--------------------------------------------------------------------------------
1 | import "../global.css";
2 | import { Slot } from "expo-router";
3 | import Providers from "@/providers";
4 |
5 | export default function Layout() {
6 | return (
7 |
8 |
9 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/with-nativewind/src/app/api/trpc/[trpc]+api.ts:
--------------------------------------------------------------------------------
1 | import { ExpoRequest, ExpoResponse } from "expo-router/server";
2 | import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
3 | import { appRouter } from "@/trpc-server/routers/_app";
4 |
5 | export async function GET(req: ExpoRequest) {
6 | return fetchRequestHandler({
7 | endpoint: "/api/trpc",
8 | req: req as unknown as Request,
9 | router: appRouter,
10 | createContext: () => ({}),
11 | });
12 | }
13 |
14 | export async function POST(req: ExpoRequest) {
15 | return fetchRequestHandler({
16 | endpoint: "/api/trpc",
17 | req: req as unknown as Request,
18 | router: appRouter,
19 | createContext: () => ({}),
20 | });
21 | }
22 |
--------------------------------------------------------------------------------
/with-nativewind/src/app/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Text, View } from "react-native";
3 | import { trpc } from "@/utils/trpc";
4 |
5 | const Home = () => {
6 | const firstPost = trpc.firstPost.useQuery();
7 |
8 | return (
9 |
10 | {firstPost.data?.title}
11 |
12 | {firstPost.data?.content}
13 |
14 |
15 | );
16 | };
17 |
18 | export default Home;
19 |
--------------------------------------------------------------------------------
/with-nativewind/src/global.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
--------------------------------------------------------------------------------
/with-nativewind/src/prisma/dev.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bidah/uni-stack/96d62ca4c6d598a7887ab6d97cf50d3a5cd4bae1/with-nativewind/src/prisma/dev.db
--------------------------------------------------------------------------------
/with-nativewind/src/prisma/migrations/20240201150705_init/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "User" (
3 | "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
4 | "email" TEXT NOT NULL,
5 | "name" TEXT
6 | );
7 |
8 | -- CreateTable
9 | CREATE TABLE "Post" (
10 | "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
11 | "title" TEXT NOT NULL,
12 | "content" TEXT,
13 | "published" BOOLEAN NOT NULL DEFAULT false,
14 | "authorId" INTEGER NOT NULL,
15 | CONSTRAINT "Post_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
16 | );
17 |
18 | -- CreateIndex
19 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
20 |
--------------------------------------------------------------------------------
/with-nativewind/src/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (i.e. Git)
3 | provider = "sqlite"
--------------------------------------------------------------------------------
/with-nativewind/src/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | generator client {
2 | provider = "prisma-client-js"
3 | }
4 |
5 | datasource db {
6 | provider = "sqlite"
7 | url = env("DATABASE_URL")
8 | }
9 |
10 | model User {
11 | id Int @id @default(autoincrement())
12 | email String @unique
13 | name String?
14 | posts Post[]
15 | }
16 |
17 | model Post {
18 | id Int @id @default(autoincrement())
19 | title String
20 | content String?
21 | published Boolean @default(false)
22 | authorId Int
23 | author User @relation(fields: [authorId], references: [id])
24 | }
25 |
--------------------------------------------------------------------------------
/with-nativewind/src/providers.tsx:
--------------------------------------------------------------------------------
1 | import { trpc } from "@/utils/trpc";
2 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
3 | import { httpBatchLink } from "@trpc/client";
4 | import React, { useState } from "react";
5 | import superjson from "superjson";
6 |
7 | function Providers({ children }: { children: React.ReactNode }) {
8 | const [queryClient] = useState(() => new QueryClient());
9 | const [trpcClient] = useState(() =>
10 | trpc.createClient({
11 | links: [
12 | httpBatchLink({
13 | url: "http://localhost:8081/api/trpc",
14 | }),
15 | ],
16 | transformer: superjson,
17 | })
18 | );
19 |
20 | return (
21 |
22 | {children}
23 |
24 | );
25 | }
26 |
27 | export default Providers;
28 |
--------------------------------------------------------------------------------
/with-nativewind/src/trpc-server/db.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client";
2 |
3 | export const db = new PrismaClient({
4 | log: ["query", "error", "warn"],
5 | });
6 |
--------------------------------------------------------------------------------
/with-nativewind/src/trpc-server/routers/_app.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 | import { procedure, router } from "../trpc";
3 | import * as trpcNext from "@trpc/server/adapters/next";
4 | import { db } from "@/trpc-server/db";
5 |
6 | export const appRouter = router({
7 | health: procedure.query(({ ctx }) => {
8 | return "hello";
9 | }),
10 | firstPost: procedure.query(async ({ ctx }) => {
11 | return db.post.findFirst();
12 | }),
13 | });
14 |
15 | export type AppRouter = typeof appRouter;
16 | export { trpcNext };
17 |
--------------------------------------------------------------------------------
/with-nativewind/src/trpc-server/trpc.ts:
--------------------------------------------------------------------------------
1 | import { initTRPC } from "@trpc/server";
2 | import { db } from "@/trpc-server/db";
3 | import SuperJSON from "superjson";
4 |
5 | const t = initTRPC.create({
6 | transformer: SuperJSON,
7 | });
8 |
9 | // Base router and procedure helpers
10 | export const router = t.router;
11 | export const procedure = t.procedure;
12 |
--------------------------------------------------------------------------------
/with-nativewind/src/utils/trpc.ts:
--------------------------------------------------------------------------------
1 | import { createTRPCReact } from "@trpc/react-query";
2 | import { AppRouter } from "@/trpc-server/routers/_app";
3 |
4 | export const trpc = createTRPCReact();
5 |
--------------------------------------------------------------------------------
/with-nativewind/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: ["./src/**/*.{js,jsx,ts,tsx}"],
4 | presets: [require("nativewind/preset")],
5 | theme: {
6 | extend: {},
7 | },
8 | future: {
9 | hoverOnlyWhenSupported: true,
10 | },
11 | plugins: [],
12 | };
13 |
--------------------------------------------------------------------------------
/with-nativewind/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "expo/tsconfig.base",
3 | "compilerOptions": {
4 | "baseUrl": ".",
5 | "paths": {
6 | "@/*": ["./src/*"]
7 | }
8 | },
9 | "include": ["global.d.ts", "**/*.ts", "**/*.tsx"]
10 | }
11 |
--------------------------------------------------------------------------------
/with-tamagui-lab/.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-tamagui-lab/.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 | # keep types in repo to track them
9 | # **/types/**/*.d.ts
10 | # but map can be ignored and just published on npm
11 | **/types/**/*.d.ts.map
12 |
13 | .yarn/*
14 | !.yarn/patches
15 | !.yarn/plugins
16 | !.yarn/releases
17 | !.yarn/sdks
18 | !.yarn/versions
19 |
20 | .turbo
21 |
22 | .ultra.cache.json
23 | tmp
24 | *.tsbuildinfo
25 | *.tmp.js
26 | yarn-error.log
27 | dist
28 | tsconfig.tsbuildinfo
29 | node_modules
30 | **/_
31 | **/tests/spec/out
32 | .DS_Store
33 |
34 | .next
35 |
36 | .vercel
37 |
38 | apps/site/out
39 | apps/kitchen-sink/ios
40 |
41 | .tamagui
42 |
43 | .idea
44 |
45 | .env
46 | # local env files
47 | .env.local
48 | .env.development.local
49 | .env.test.localp
50 | .env.production.local
51 |
52 | apps/kitchen-sink/ios
53 | apps/kitchen-sink/android
54 |
55 | # ignore until videos finalized
56 | apps/site/public/talk/
57 |
58 | apps/studio/types/**
59 |
60 | .expo
61 |
--------------------------------------------------------------------------------
/with-tamagui-lab/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "uni-stack-with-tamagui",
4 | "slug": "uni-stack-with-tamagui",
5 | "version": "0.0.1",
6 | "orientation": "portrait",
7 | "scheme": "com.uni.with-tamagui",
8 | "userInterfaceStyle": "automatic",
9 | "assetBundlePatterns": [
10 | "**/*"
11 | ],
12 | "ios": {
13 | "supportsTablet": true
14 | },
15 | "web": {
16 | "bundler": "metro",
17 | "output": "static"
18 | },
19 | "plugins": [
20 | "expo-router",
21 | "expo-font"
22 | ],
23 | "experiments": {
24 | "typedRoutes": true
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/with-tamagui-lab/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = (api) => {
2 | api.cache(true);
3 | return {
4 | presets: ["babel-preset-expo"],
5 | };
6 | };
7 |
--------------------------------------------------------------------------------
/with-tamagui-lab/metro.config.js:
--------------------------------------------------------------------------------
1 | // Learn more https://docs.expo.io/guides/customizing-metro
2 | const { getDefaultConfig } = require('expo/metro-config')
3 |
4 | /** @type {import('expo/metro-config').MetroConfig} */
5 | let config = getDefaultConfig(__dirname, {
6 | // [Web-only]: Enables CSS support in Metro.
7 | isCSSEnabled: true,
8 | })
9 |
10 | // 2. Enable Tamagui
11 | const { withTamagui } = require('@tamagui/metro-plugin')
12 | config = withTamagui(config, {
13 | components: ['tamagui'],
14 | config: './tamagui.config.ts',
15 | outputCSS: './tamagui-web.css',
16 | })
17 |
18 | // REMOVE THIS (just for tamagui internal devs):
19 | if (process.env.IS_TAMAGUI_DEV) {
20 | console.info(`Welcome tamagui dev, you need to yarn link ~/tamagui --all first!`)
21 | // just have to run first:
22 | // yarn link ~/tamagui --all
23 | const fs = require('fs')
24 | const path = require('path')
25 | const projectRoot = __dirname
26 | const monorepoRoot = path.resolve(projectRoot, '../..')
27 | config.watchFolders = [monorepoRoot]
28 | config.resolver.nodeModulesPaths = [
29 | path.resolve(projectRoot, 'node_modules'),
30 | path.resolve(monorepoRoot, 'node_modules'),
31 | ]
32 | // have to manually de-deupe
33 | try {
34 | fs.rmSync(path.join(projectRoot, 'node_modules', 'react'), {
35 | recursive: true,
36 | force: true,
37 | })
38 | } catch {}
39 | try {
40 | fs.rmSync(path.join(projectRoot, 'node_modules', 'react-dom'), {
41 | recursive: true,
42 | force: true,
43 | })
44 | } catch {}
45 | }
46 |
47 | module.exports = config
48 |
--------------------------------------------------------------------------------
/with-tamagui-lab/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nate-fix-tamagui-expo-router",
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/metro-runtime": "~3.2.1",
21 | "@prisma/client": "^5.9.1",
22 | "@react-navigation/native": "^6.0.2",
23 | "@tamagui/config": "^1.90.2",
24 | "@tamagui/metro-plugin": "^1.90.2",
25 | "@tanstack/react-query": "^5.20.5",
26 | "@trpc/client": "^10.45.1",
27 | "@trpc/react-query": "^10.45.1",
28 | "@trpc/server": "^10.45.1",
29 | "babel-preset-expo": "~11.0.0",
30 | "expo": "51.0.27",
31 | "expo-font": "~12.0.9",
32 | "expo-linking": "~6.3.1",
33 | "expo-router": "~3.5.21",
34 | "expo-splash-screen": "~0.27.5",
35 | "expo-status-bar": "~1.12.1",
36 | "expo-system-ui": "~3.0.7",
37 | "expo-web-browser": "~13.0.3",
38 | "react": "18.2.0",
39 | "react-dom": "18.2.0",
40 | "react-helmet-async": "~2.0.4",
41 | "react-native": "0.74.5",
42 | "react-native-safe-area-context": "4.10.5",
43 | "react-native-screens": "3.31.1",
44 | "react-native-web": "^0.19.9",
45 | "tamagui": "1.108.3",
46 | "typescript": "~5.3.3",
47 | "zod": "^3.22.4"
48 | },
49 | "devDependencies": {
50 | "@babel/core": "^7.23.3",
51 | "@types/react": "~18.2.14",
52 | "prisma": "^5.9.1"
53 | },
54 | "private": true
55 | }
56 |
--------------------------------------------------------------------------------
/with-tamagui-lab/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 | This viewport disables scaling which makes the mobile website act more like a native app.
16 | However this does reduce built-in accessibility. If you want to enable scaling, use this instead:
17 |
18 | */}
19 |
23 | {/*
24 | Disable body scrolling on web. This makes ScrollView components work closer to how they do on native.
25 | However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line.
26 | */}
27 |
28 |
29 | {/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */}
30 |
31 | {/* Add any additional elements that you want globally available on web... */}
32 |
33 | {children}
34 |
35 | )
36 | }
37 |
38 | const responsiveBackground = `
39 | body {
40 | background-color: #fff;
41 | }
42 | @media (prefers-color-scheme: dark) {
43 | body {
44 | background-color: #000;
45 | }
46 | }`
47 |
--------------------------------------------------------------------------------
/with-tamagui-lab/src/app/_layout.tsx:
--------------------------------------------------------------------------------
1 | import { Slot } from "expo-router";
2 | import Providers from "@/providers";
3 |
4 | export default function Layout() {
5 | return (
6 |
7 |
8 |
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/with-tamagui-lab/src/app/api/trpc/[trpc]+api.ts:
--------------------------------------------------------------------------------
1 | import { ExpoRequest, ExpoResponse } from "expo-router/server";
2 | import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
3 | import { appRouter } from "@/trpc-server/routers/_app";
4 |
5 | export async function GET(req: ExpoRequest) {
6 | return fetchRequestHandler({
7 | endpoint: "/api/trpc",
8 | req: req as unknown as Request,
9 | router: appRouter,
10 | createContext: () => ({}),
11 | });
12 | }
13 |
14 | export async function POST(req: ExpoRequest) {
15 | return fetchRequestHandler({
16 | endpoint: "/api/trpc",
17 | req: req as unknown as Request,
18 | router: appRouter,
19 | createContext: () => ({}),
20 | });
21 | }
22 |
--------------------------------------------------------------------------------
/with-tamagui-lab/src/app/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Text, View } from "tamagui";
3 | import { trpc } from "@/utils/trpc";
4 |
5 | const Home = () => {
6 | const firstPost = trpc.firstPost.useQuery();
7 |
8 | return (
9 |
15 |
16 | {firstPost.data?.title}
17 |
18 |
19 |
20 | {firstPost.data?.content}
21 |
22 |
23 |
24 | );
25 | };
26 |
27 | export default Home;
28 |
--------------------------------------------------------------------------------
/with-tamagui-lab/src/prisma/dev.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bidah/uni-stack/96d62ca4c6d598a7887ab6d97cf50d3a5cd4bae1/with-tamagui-lab/src/prisma/dev.db
--------------------------------------------------------------------------------
/with-tamagui-lab/src/prisma/migrations/20240201150705_init/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "User" (
3 | "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
4 | "email" TEXT NOT NULL,
5 | "name" TEXT
6 | );
7 |
8 | -- CreateTable
9 | CREATE TABLE "Post" (
10 | "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
11 | "title" TEXT NOT NULL,
12 | "content" TEXT,
13 | "published" BOOLEAN NOT NULL DEFAULT false,
14 | "authorId" INTEGER NOT NULL,
15 | CONSTRAINT "Post_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
16 | );
17 |
18 | -- CreateIndex
19 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
20 |
--------------------------------------------------------------------------------
/with-tamagui-lab/src/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (i.e. Git)
3 | provider = "sqlite"
--------------------------------------------------------------------------------
/with-tamagui-lab/src/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | generator client {
2 | provider = "prisma-client-js"
3 | }
4 |
5 | datasource db {
6 | provider = "sqlite"
7 | url = env("DATABASE_URL")
8 | }
9 |
10 | model User {
11 | id Int @id @default(autoincrement())
12 | email String @unique
13 | name String?
14 | posts Post[]
15 | }
16 |
17 | model Post {
18 | id Int @id @default(autoincrement())
19 | title String
20 | content String?
21 | published Boolean @default(false)
22 | authorId Int
23 | author User @relation(fields: [authorId], references: [id])
24 | }
25 |
--------------------------------------------------------------------------------
/with-tamagui-lab/src/providers.tsx:
--------------------------------------------------------------------------------
1 | import { trpc } from "@/utils/trpc";
2 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
3 | import { httpBatchLink } from "@trpc/client";
4 | import React, { useState } from "react";
5 | import { Button, ScrollView, Text, TextInput, View } from "react-native";
6 | import { TamaguiProvider } from "tamagui";
7 | import config from "tamagui.config";
8 |
9 | function Providers({ children }: { children: React.ReactNode }) {
10 | const [queryClient] = useState(() => new QueryClient());
11 | const [trpcClient] = useState(() =>
12 | trpc.createClient({
13 | links: [
14 | httpBatchLink({
15 | url: "http://localhost:8081/api/trpc",
16 | }),
17 | ],
18 | })
19 | );
20 |
21 | return (
22 |
23 |
24 |
25 | {children}
26 |
27 |
28 |
29 | );
30 | }
31 |
32 | export default Providers;
33 |
--------------------------------------------------------------------------------
/with-tamagui-lab/src/trpc-server/db.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client";
2 |
3 | export const db = new PrismaClient({
4 | log: ["query", "error", "warn"],
5 | });
6 |
--------------------------------------------------------------------------------
/with-tamagui-lab/src/trpc-server/routers/_app.ts:
--------------------------------------------------------------------------------
1 | import { procedure, router } from "../trpc";
2 | import { db } from "@/trpc-server/db";
3 |
4 | export const appRouter = router({
5 | health: procedure.query(({ ctx }) => {
6 | return "hello";
7 | }),
8 | firstPost: procedure.query(async ({ ctx }) => {
9 | return db.post.findFirst();
10 | }),
11 | });
12 |
13 | export type AppRouter = typeof appRouter;
14 |
--------------------------------------------------------------------------------
/with-tamagui-lab/src/trpc-server/trpc.ts:
--------------------------------------------------------------------------------
1 | import { initTRPC } from "@trpc/server";
2 | import { db } from "@/trpc-server/db";
3 |
4 | const t = initTRPC.create();
5 |
6 | // Base router and procedure helpers
7 | export const router = t.router;
8 | export const procedure = t.procedure;
9 |
--------------------------------------------------------------------------------
/with-tamagui-lab/src/utils/trpc.ts:
--------------------------------------------------------------------------------
1 | import { createTRPCReact } from "@trpc/react-query";
2 | import { AppRouter } from "@/trpc-server/routers/_app";
3 |
4 | export const trpc = createTRPCReact();
5 |
--------------------------------------------------------------------------------
/with-tamagui-lab/tamagui.config.ts:
--------------------------------------------------------------------------------
1 | import { config as configBase } from '@tamagui/config'
2 | import { createTamagui } from 'tamagui'
3 |
4 | export const config = createTamagui(configBase)
5 |
6 | export default config
7 |
8 | export type Conf = typeof config
9 |
10 | declare module 'tamagui' {
11 | interface TamaguiCustomConfig extends Conf {}
12 | }
13 |
--------------------------------------------------------------------------------
/with-tamagui-lab/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "expo/tsconfig.base",
3 | "compilerOptions": {
4 | "baseUrl": ".",
5 | "paths": {
6 | "@/*": [
7 | "./src/*"
8 | ]
9 | }
10 | },
11 | "include": [
12 | "global.d.ts",
13 | "**/*.ts",
14 | "**/*.tsx",
15 | ".expo/types/**/*.ts",
16 | "expo-env.d.ts"
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/with-tamagui/.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-tamagui/.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 | # keep types in repo to track them
9 | # **/types/**/*.d.ts
10 | # but map can be ignored and just published on npm
11 | **/types/**/*.d.ts.map
12 |
13 | .yarn/*
14 | !.yarn/patches
15 | !.yarn/plugins
16 | !.yarn/releases
17 | !.yarn/sdks
18 | !.yarn/versions
19 |
20 | .turbo
21 |
22 | .ultra.cache.json
23 | tmp
24 | *.tsbuildinfo
25 | *.tmp.js
26 | yarn-error.log
27 | dist
28 | tsconfig.tsbuildinfo
29 | node_modules
30 | **/_
31 | **/tests/spec/out
32 | .DS_Store
33 |
34 | .next
35 |
36 | .vercel
37 |
38 | apps/site/out
39 | apps/kitchen-sink/ios
40 |
41 | .tamagui
42 |
43 | .idea
44 |
45 | .env
46 | # local env files
47 | .env.local
48 | .env.development.local
49 | .env.test.localp
50 | .env.production.local
51 |
52 | apps/kitchen-sink/ios
53 | apps/kitchen-sink/android
54 |
55 | # ignore until videos finalized
56 | apps/site/public/talk/
57 |
58 | apps/studio/types/**
59 |
60 | .expo
61 |
--------------------------------------------------------------------------------
/with-tamagui/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "uni-stack-with-tamagui",
4 | "slug": "uni-stack-with-tamagui",
5 | "version": "0.0.1",
6 | "orientation": "portrait",
7 | "scheme": "com.uni.with-tamagui",
8 | "userInterfaceStyle": "automatic",
9 | "assetBundlePatterns": ["**/*"],
10 | "ios": {
11 | "supportsTablet": true
12 | },
13 | "web": {
14 | "bundler": "metro",
15 | "output": "static"
16 | },
17 | "plugins": ["expo-router"],
18 | "experiments": {
19 | "typedRoutes": true
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/with-tamagui/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = (api) => {
2 | api.cache(true)
3 | return {
4 | presets: ['babel-preset-expo'],
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/with-tamagui/biome.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://biomejs.dev/schemas/1.5.1/schema.json",
3 | "organizeImports": {
4 | "enabled": false
5 | },
6 | "linter": {
7 | "enabled": true,
8 | "rules": {
9 | "correctness": {
10 | "useExhaustiveDependencies": "off",
11 | "noInnerDeclarations": "off",
12 | "noUnnecessaryContinue": "off",
13 | "noConstructorReturn": "off"
14 | },
15 | "suspicious": {
16 | "noImplicitAnyLet": "off",
17 | "noConfusingVoidType": "off",
18 | "noEmptyInterface": "off",
19 | "noExplicitAny": "off",
20 | "noArrayIndexKey": "off",
21 | "noDoubleEquals": "off",
22 | "noConsoleLog": "error",
23 | "noAssignInExpressions": "off",
24 | "noRedeclare": "off"
25 | },
26 | "style": {
27 | "noParameterAssign": "off",
28 | "noNonNullAssertion": "off",
29 | "noArguments": "off",
30 | "noUnusedTemplateLiteral": "off",
31 | "useDefaultParameterLast": "off",
32 | "useConst": "off",
33 | "useEnumInitializers": "off",
34 | "useTemplate": "off",
35 | "useSelfClosingElements": "off"
36 | },
37 | "security": {
38 | "noDangerouslySetInnerHtml": "off",
39 | "noDangerouslySetInnerHtmlWithChildren": "off"
40 | },
41 | "performance": {
42 | "noDelete": "off",
43 | "noAccumulatingSpread": "off"
44 | },
45 | "complexity": {
46 | "noForEach": "off",
47 | "noBannedTypes": "off",
48 | "useLiteralKeys": "off",
49 | "useSimplifiedLogicExpression": "off",
50 | "useOptionalChain": "off"
51 | },
52 | "a11y": {
53 | "noSvgWithoutTitle": "off",
54 | "useMediaCaption": "off",
55 | "noHeaderScope": "off",
56 | "useAltText": "off",
57 | "useButtonType": "off"
58 | }
59 | }
60 | },
61 | "formatter": {
62 | "enabled": true,
63 | "formatWithErrors": false,
64 | "indentStyle": "space",
65 | "indentWidth": 2,
66 | "lineWidth": 90,
67 | "ignore": ["**/*/generated-new.ts", "**/*/generated-v2.ts"]
68 | },
69 | "javascript": {
70 | "formatter": {
71 | "trailingComma": "es5",
72 | "jsxQuoteStyle": "double",
73 | "semicolons": "asNeeded",
74 | "quoteStyle": "single"
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/with-tamagui/metro.config.js:
--------------------------------------------------------------------------------
1 | // Learn more https://docs.expo.io/guides/customizing-metro
2 | const { getDefaultConfig } = require('expo/metro-config')
3 |
4 | /** @type {import('expo/metro-config').MetroConfig} */
5 | let config = getDefaultConfig(__dirname, {
6 | // [Web-only]: Enables CSS support in Metro.
7 | isCSSEnabled: false,
8 | })
9 |
10 | // 2. Enable Tamagui
11 | const { withTamagui } = require('@tamagui/metro-plugin')
12 | module.exports = withTamagui(config, {
13 | components: ['tamagui'],
14 | config: './tamagui.config.ts',
15 | outputCSS: './tamagui-web.css',
16 | })
17 |
18 | // REMOVE THIS (just for tamagui internal devs to work in monorepo):
19 | // if (process.env.IS_TAMAGUI_DEV && __dirname.includes('tamagui')) {
20 | // const fs = require('fs')
21 | // const path = require('path')
22 | // const projectRoot = __dirname
23 | // const monorepoRoot = path.resolve(projectRoot, '../..')
24 | // config.watchFolders = [monorepoRoot]
25 | // config.resolver.nodeModulesPaths = [
26 | // path.resolve(projectRoot, 'node_modules'),
27 | // path.resolve(monorepoRoot, 'node_modules'),
28 | // ]
29 | // // have to manually de-deupe
30 | // try {
31 | // fs.rmSync(path.join(projectRoot, 'node_modules', '@tamagui'), {
32 | // recursive: true,
33 | // force: true,
34 | // })
35 | // } catch {}
36 | // try {
37 | // fs.rmSync(path.join(projectRoot, 'node_modules', 'tamagui'), {
38 | // recursive: true,
39 | // force: true,
40 | // })
41 | // } catch {}
42 | // try {
43 | // fs.rmSync(path.join(projectRoot, 'node_modules', 'react'), {
44 | // recursive: true,
45 | // force: true,
46 | // })
47 | // } catch {}
48 | // try {
49 | // fs.rmSync(path.join(projectRoot, 'node_modules', 'react-dom'), {
50 | // recursive: true,
51 | // force: true,
52 | // })
53 | // } catch {}
54 | // }
55 |
--------------------------------------------------------------------------------
/with-tamagui/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "myapp",
3 | "main": "expo-router/entry",
4 | "version": "1.0.0",
5 | "scripts": {
6 | "upgrade:tamagui": "yarn up '*tamagui*'@latest '@tamagui/*'@latest",
7 | "start": "expo start",
8 | "android": "expo start --android",
9 | "ios": "expo start --ios",
10 | "web": "expo start --web",
11 | "prisma:studio": "npx prisma studio --schema src/prisma/schema.prisma",
12 | "prisma:generate": "npx prisma generate --schema src/prisma/schema.prisma",
13 | "prisma:push": "npx prisma db push --schema src/prisma/schema.prisma",
14 | "prisma:pull": "npx prisma db pull --schema src/prisma/schema.prisma",
15 | "prisma:introspect": "npx prisma introspect --schema src/prisma/schema.prisma"
16 | },
17 | "jest": {
18 | "preset": "jest-expo"
19 | },
20 | "dependencies": {
21 | "@expo/metro-runtime": "3.1.1",
22 | "@prisma/client": "^5.9.1",
23 | "@react-navigation/native": "^6.0.2",
24 | "@tamagui/config": "^1.90.2",
25 | "@tamagui/metro-plugin": "^1.90.2",
26 | "@tanstack/react-query": "^5.20.5",
27 | "@trpc/client": "^10.45.1",
28 | "@trpc/react-query": "^10.45.1",
29 | "@trpc/server": "^10.45.1",
30 | "babel-preset-expo": "^10.0.1",
31 | "expo": "50.0.2",
32 | "expo-font": "~11.10.2",
33 | "expo-linking": "~6.2.1",
34 | "expo-router": "~3.4.4",
35 | "expo-splash-screen": "~0.26.3",
36 | "expo-status-bar": "~1.11.1",
37 | "expo-system-ui": "~2.9.3",
38 | "expo-web-browser": "~12.8.1",
39 | "react": "^18.2.0",
40 | "react-dom": "^18.2.0",
41 | "react-helmet-async": "~2.0.4",
42 | "react-native": "0.73.2",
43 | "react-native-safe-area-context": "4.8.2",
44 | "react-native-screens": "~3.29.0",
45 | "react-native-web": "^0.19.9",
46 | "superjson": "^2.2.1",
47 | "tamagui": "^1.90.2",
48 | "typescript": "^5.3.0",
49 | "zod": "^3.22.4"
50 | },
51 | "devDependencies": {
52 | "@babel/core": "^7.23.3",
53 | "@types/react": "~18.2.14",
54 | "prisma": "^5.9.1"
55 | },
56 | "private": true
57 | }
58 |
--------------------------------------------------------------------------------
/with-tamagui/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 | This viewport disables scaling which makes the mobile website act more like a native app.
16 | However this does reduce built-in accessibility. If you want to enable scaling, use this instead:
17 |
18 | */}
19 |
23 | {/*
24 | Disable body scrolling on web. This makes ScrollView components work closer to how they do on native.
25 | However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line.
26 | */}
27 |
28 |
29 | {/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */}
30 |
31 | {/* Add any additional elements that you want globally available on web... */}
32 |
33 | {children}
34 |
35 | )
36 | }
37 |
38 | const responsiveBackground = `
39 | body {
40 | background-color: #fff;
41 | }
42 | @media (prefers-color-scheme: dark) {
43 | body {
44 | background-color: #000;
45 | }
46 | }`
47 |
--------------------------------------------------------------------------------
/with-tamagui/src/app/_layout.tsx:
--------------------------------------------------------------------------------
1 | import { Slot } from "expo-router";
2 | import Providers from "@/providers";
3 |
4 | export default function Layout() {
5 | return (
6 |
7 |
8 |
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/with-tamagui/src/app/api/trpc/[trpc]+api.ts:
--------------------------------------------------------------------------------
1 | import { ExpoRequest, ExpoResponse } from "expo-router/server";
2 | import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
3 | import { appRouter } from "@/trpc-server/routers/_app";
4 |
5 | export async function GET(req: ExpoRequest) {
6 | return fetchRequestHandler({
7 | endpoint: "/api/trpc",
8 | req: req as unknown as Request,
9 | router: appRouter,
10 | createContext: () => ({}),
11 | });
12 | }
13 |
14 | export async function POST(req: ExpoRequest) {
15 | return fetchRequestHandler({
16 | endpoint: "/api/trpc",
17 | req: req as unknown as Request,
18 | router: appRouter,
19 | createContext: () => ({}),
20 | });
21 | }
22 |
--------------------------------------------------------------------------------
/with-tamagui/src/app/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Text, View } from "tamagui";
3 | import { trpc } from "@/utils/trpc";
4 |
5 | const Home = () => {
6 | const firstPost = trpc.firstPost.useQuery();
7 |
8 | return (
9 |
15 |
16 | {firstPost.data?.title}
17 |
18 |
19 |
20 | {firstPost.data?.content}
21 |
22 |
23 |
24 | );
25 | };
26 |
27 | export default Home;
28 |
--------------------------------------------------------------------------------
/with-tamagui/src/assets/fonts/SpaceMono-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bidah/uni-stack/96d62ca4c6d598a7887ab6d97cf50d3a5cd4bae1/with-tamagui/src/assets/fonts/SpaceMono-Regular.ttf
--------------------------------------------------------------------------------
/with-tamagui/src/assets/images/adaptive-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bidah/uni-stack/96d62ca4c6d598a7887ab6d97cf50d3a5cd4bae1/with-tamagui/src/assets/images/adaptive-icon.png
--------------------------------------------------------------------------------
/with-tamagui/src/assets/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bidah/uni-stack/96d62ca4c6d598a7887ab6d97cf50d3a5cd4bae1/with-tamagui/src/assets/images/favicon.png
--------------------------------------------------------------------------------
/with-tamagui/src/assets/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bidah/uni-stack/96d62ca4c6d598a7887ab6d97cf50d3a5cd4bae1/with-tamagui/src/assets/images/icon.png
--------------------------------------------------------------------------------
/with-tamagui/src/assets/images/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bidah/uni-stack/96d62ca4c6d598a7887ab6d97cf50d3a5cd4bae1/with-tamagui/src/assets/images/splash.png
--------------------------------------------------------------------------------
/with-tamagui/src/constants/Colors.ts:
--------------------------------------------------------------------------------
1 | const tintColorLight = '#2f95dc';
2 | const tintColorDark = '#fff';
3 |
4 | export default {
5 | light: {
6 | text: '#000',
7 | background: '#fff',
8 | tint: tintColorLight,
9 | tabIconDefault: '#ccc',
10 | tabIconSelected: tintColorLight,
11 | },
12 | dark: {
13 | text: '#fff',
14 | background: '#000',
15 | tint: tintColorDark,
16 | tabIconDefault: '#ccc',
17 | tabIconSelected: tintColorDark,
18 | },
19 | };
20 |
--------------------------------------------------------------------------------
/with-tamagui/src/prisma/dev.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bidah/uni-stack/96d62ca4c6d598a7887ab6d97cf50d3a5cd4bae1/with-tamagui/src/prisma/dev.db
--------------------------------------------------------------------------------
/with-tamagui/src/prisma/migrations/20240201150705_init/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "User" (
3 | "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
4 | "email" TEXT NOT NULL,
5 | "name" TEXT
6 | );
7 |
8 | -- CreateTable
9 | CREATE TABLE "Post" (
10 | "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
11 | "title" TEXT NOT NULL,
12 | "content" TEXT,
13 | "published" BOOLEAN NOT NULL DEFAULT false,
14 | "authorId" INTEGER NOT NULL,
15 | CONSTRAINT "Post_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
16 | );
17 |
18 | -- CreateIndex
19 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
20 |
--------------------------------------------------------------------------------
/with-tamagui/src/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (i.e. Git)
3 | provider = "sqlite"
--------------------------------------------------------------------------------
/with-tamagui/src/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | generator client {
2 | provider = "prisma-client-js"
3 | }
4 |
5 | datasource db {
6 | provider = "sqlite"
7 | url = env("DATABASE_URL")
8 | }
9 |
10 | model User {
11 | id Int @id @default(autoincrement())
12 | email String @unique
13 | name String?
14 | posts Post[]
15 | }
16 |
17 | model Post {
18 | id Int @id @default(autoincrement())
19 | title String
20 | content String?
21 | published Boolean @default(false)
22 | authorId Int
23 | author User @relation(fields: [authorId], references: [id])
24 | }
25 |
--------------------------------------------------------------------------------
/with-tamagui/src/providers.tsx:
--------------------------------------------------------------------------------
1 | import { trpc } from "@/utils/trpc";
2 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
3 | import { httpBatchLink } from "@trpc/client";
4 | import React, { useState } from "react";
5 | import { TamaguiProvider } from "tamagui";
6 | import config from "tamagui.config";
7 | import superjson from "superjson";
8 |
9 | function Providers({ children }: { children: React.ReactNode }) {
10 | const [queryClient] = useState(() => new QueryClient());
11 | const [trpcClient] = useState(() =>
12 | trpc.createClient({
13 | links: [
14 | httpBatchLink({
15 | url: "http://localhost:8081/api/trpc",
16 | }),
17 | ],
18 | transformer: superjson,
19 | })
20 | );
21 |
22 | return (
23 |
24 |
25 |
26 | {children}
27 |
28 |
29 |
30 | );
31 | }
32 |
33 | export default Providers;
34 |
--------------------------------------------------------------------------------
/with-tamagui/src/trpc-server/db.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client";
2 |
3 | export const db = new PrismaClient({
4 | log: ["query", "error", "warn"],
5 | });
6 |
--------------------------------------------------------------------------------
/with-tamagui/src/trpc-server/routers/_app.ts:
--------------------------------------------------------------------------------
1 | import { procedure, router } from "../trpc";
2 | import { db } from "@/trpc-server/db";
3 |
4 | export const appRouter = router({
5 | health: procedure.query(({ ctx }) => {
6 | return "hello";
7 | }),
8 | firstPost: procedure.query(async ({ ctx }) => {
9 | return db.post.findFirst();
10 | }),
11 | });
12 |
13 | export type AppRouter = typeof appRouter;
14 |
--------------------------------------------------------------------------------
/with-tamagui/src/trpc-server/trpc.ts:
--------------------------------------------------------------------------------
1 | import { initTRPC } from "@trpc/server";
2 | import { db } from "@/trpc-server/db";
3 | import SuperJSON from "superjson";
4 |
5 | const t = initTRPC.create({
6 | transformer: SuperJSON,
7 | });
8 |
9 | // Base router and procedure helpers
10 | export const router = t.router;
11 | export const procedure = t.procedure;
12 |
--------------------------------------------------------------------------------
/with-tamagui/src/utils/trpc.ts:
--------------------------------------------------------------------------------
1 | import { createTRPCReact } from "@trpc/react-query";
2 | import { AppRouter } from "@/trpc-server/routers/_app";
3 |
4 | export const trpc = createTRPCReact();
5 |
--------------------------------------------------------------------------------
/with-tamagui/tamagui.config.ts:
--------------------------------------------------------------------------------
1 | import { config as configBase } from '@tamagui/config'
2 | import { createTamagui } from 'tamagui'
3 |
4 | export const config = createTamagui(configBase)
5 |
6 | export default config
7 |
8 | export type Conf = typeof config
9 |
10 | declare module 'tamagui' {
11 | interface TamaguiCustomConfig extends Conf {}
12 | }
13 |
--------------------------------------------------------------------------------
/with-tamagui/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "expo/tsconfig.base",
3 | "compilerOptions": {
4 | "baseUrl": ".",
5 | "paths": {
6 | "@/*": [
7 | "./src/*"
8 | ]
9 | }
10 | },
11 | "include": [
12 | "global.d.ts",
13 | "**/*.ts",
14 | "**/*.tsx",
15 | ".expo/types/**/*.ts",
16 | "expo-env.d.ts"
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------