├── .prettierrc ├── nativewind-env.d.ts ├── global.css ├── src ├── components │ ├── useColorScheme.ts │ ├── useClientOnlyValue.ts │ ├── useClientOnlyValue.web.ts │ ├── useColorScheme.web.ts │ ├── EditScreenInfo.tsx │ ├── Themed.tsx │ └── PressableWithScale.tsx ├── assets │ ├── images │ │ ├── icon.png │ │ ├── favicon.png │ │ ├── splash-icon.png │ │ └── adaptive-icon.png │ └── fonts │ │ └── SpaceMono-Regular.ttf ├── lib │ ├── db │ │ ├── db.ts │ │ ├── MigrationProvider.tsx │ │ └── schema.ts │ └── constants │ │ └── Colors.ts └── app │ ├── (tabs) │ ├── two.tsx │ ├── index.tsx │ └── _layout.tsx │ ├── +not-found.tsx │ ├── modal.tsx │ ├── sheet.tsx │ ├── +html.tsx │ └── _layout.tsx ├── drizzle.config.ts ├── drizzle ├── 0000_blushing_pestilence.sql ├── meta │ ├── _journal.json │ └── 0000_snapshot.json └── migrations.js ├── babel.config.js ├── tailwind.config.js ├── metro.config.js ├── tsconfig.json ├── .cursor └── rules │ ├── running-the-project.mdc │ ├── project.mdc │ └── first-time-setup.mdc ├── .eslintrc.js ├── .gitignore ├── README.md ├── app.json └── package.json /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2 4 | } 5 | -------------------------------------------------------------------------------- /nativewind-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /global.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /src/components/useColorScheme.ts: -------------------------------------------------------------------------------- 1 | export { useColorScheme } from "react-native"; 2 | -------------------------------------------------------------------------------- /src/assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vibecodeapp/mobile-template-1/HEAD/src/assets/images/icon.png -------------------------------------------------------------------------------- /src/assets/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vibecodeapp/mobile-template-1/HEAD/src/assets/images/favicon.png -------------------------------------------------------------------------------- /src/assets/images/splash-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vibecodeapp/mobile-template-1/HEAD/src/assets/images/splash-icon.png -------------------------------------------------------------------------------- /src/assets/images/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vibecodeapp/mobile-template-1/HEAD/src/assets/images/adaptive-icon.png -------------------------------------------------------------------------------- /src/assets/fonts/SpaceMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vibecodeapp/mobile-template-1/HEAD/src/assets/fonts/SpaceMono-Regular.ttf -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "drizzle-kit"; 2 | 3 | export default { 4 | schema: "./src/lib/db/schema.ts", 5 | out: "./drizzle", 6 | dialect: "sqlite", 7 | driver: "expo", 8 | } satisfies Config; 9 | -------------------------------------------------------------------------------- /src/lib/db/db.ts: -------------------------------------------------------------------------------- 1 | import { drizzle } from "drizzle-orm/expo-sqlite"; 2 | import { openDatabaseSync } from "expo-sqlite"; 3 | 4 | const expo = openDatabaseSync("db.db"); 5 | const db = drizzle(expo); 6 | 7 | export default db; 8 | -------------------------------------------------------------------------------- /src/components/useClientOnlyValue.ts: -------------------------------------------------------------------------------- 1 | // This function is web-only as native doesn't currently support server (or build-time) rendering. 2 | export function useClientOnlyValue(server: S, client: C): S | C { 3 | return client; 4 | } 5 | -------------------------------------------------------------------------------- /drizzle/0000_blushing_pestilence.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `users` ( 2 | `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, 3 | `name` text NOT NULL, 4 | `created_at` integer DEFAULT CURRENT_TIMESTAMP NOT NULL, 5 | `updated_at` integer DEFAULT CURRENT_TIMESTAMP NOT NULL 6 | ); 7 | -------------------------------------------------------------------------------- /drizzle/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "7", 3 | "dialect": "sqlite", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "6", 8 | "when": 1741040383234, 9 | "tag": "0000_blushing_pestilence", 10 | "breakpoints": true 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /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 | plugins: [["inline-import", { extensions: [".sql"] }]], 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /drizzle/migrations.js: -------------------------------------------------------------------------------- 1 | // This file is required for Expo/React Native SQLite migrations - https://orm.drizzle.team/quick-sqlite/expo 2 | 3 | import journal from "./meta/_journal.json"; 4 | import m0000 from "./0000_blushing_pestilence.sql"; 5 | 6 | export default { 7 | journal, 8 | migrations: { 9 | m0000, 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | // NOTE: Update this to include the paths to all of your component files. 4 | content: ["./src/**/*.{js,jsx,ts,tsx}"], 5 | presets: [require("nativewind/preset")], 6 | theme: { 7 | extend: {}, 8 | }, 9 | plugins: [], 10 | }; 11 | -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | // Learn more https://docs.expo.io/guides/customizing-metro 2 | const { getDefaultConfig } = require("expo/metro-config"); 3 | const { withNativeWind } = require("nativewind/metro"); 4 | 5 | const config = getDefaultConfig(__dirname); 6 | config.resolver.sourceExts.push("sql"); 7 | 8 | module.exports = withNativeWind(config, { input: "./global.css" }); 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true, 5 | "paths": { 6 | "@/*": ["./src/*"] 7 | }, 8 | "baseUrl": "." 9 | }, 10 | "include": [ 11 | "**/*.ts", 12 | "**/*.tsx", 13 | ".expo/types/**/*.ts", 14 | "expo-env.d.ts", 15 | "nativewind-env.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.cursor/rules/running-the-project.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Rules for running the project and installing dependencies 3 | globs: package.json, app.json 4 | alwaysApply: true 5 | --- 6 | - To run the project, run `bun start` 7 | - If any native dependencies change, that is, there are any changes in in [app.json](mdc:app.json) or [package.json](mdc:package.json), run `bun ios` to create a new native build -------------------------------------------------------------------------------- /src/components/useClientOnlyValue.web.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | // `useEffect` is not invoked during server rendering, meaning 4 | // we can use this to determine if we're on the server or not. 5 | export function useClientOnlyValue(server: S, client: C): S | C { 6 | const [value, setValue] = React.useState(server); 7 | React.useEffect(() => { 8 | setValue(client); 9 | }, [client]); 10 | 11 | return value; 12 | } 13 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // https://docs.expo.dev/guides/using-eslint/ 2 | module.exports = { 3 | extends: ["expo", "prettier"], 4 | plugins: ["prettier"], 5 | rules: { 6 | "prettier/prettier": "error", 7 | }, 8 | ignorePatterns: ["/dist/*"], 9 | settings: { 10 | "import/resolver": { 11 | typescript: { 12 | alwaysTryTypes: true, 13 | project: path.resolve(__dirname, "./tsconfig.json"), 14 | }, 15 | }, 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /src/lib/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 | -------------------------------------------------------------------------------- /src/components/useColorScheme.web.ts: -------------------------------------------------------------------------------- 1 | // NOTE: The default React Native styling doesn't support server rendering. 2 | // Server rendered styles should not change between the first render of the HTML 3 | // and the first render on the client. Typically, web developers will use CSS media queries 4 | // to render different styles on the client and server, these aren't directly supported in React Native 5 | // but can be achieved using a styling library like Nativewind. 6 | export function useColorScheme() { 7 | return "light"; 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/db/MigrationProvider.tsx: -------------------------------------------------------------------------------- 1 | import migrations from "../../../drizzle/migrations"; 2 | import db from "./db"; 3 | import { useMigrations } from "drizzle-orm/expo-sqlite/migrator"; 4 | import { Text } from "@/components/Themed"; 5 | 6 | export const MigrationProvider = ({ 7 | children, 8 | }: { 9 | children: React.ReactNode; 10 | }) => { 11 | const { success, error } = useMigrations(db, migrations); 12 | 13 | if (success) { 14 | return children; 15 | } 16 | 17 | if (error) { 18 | return Error: {error.message}; 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /.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 | expo-env.d.ts 11 | 12 | # Native 13 | *.orig.* 14 | *.jks 15 | *.p8 16 | *.p12 17 | *.key 18 | *.mobileprovision 19 | 20 | # Metro 21 | .metro-health-check* 22 | 23 | # debug 24 | npm-debug.* 25 | yarn-debug.* 26 | yarn-error.* 27 | 28 | # macOS 29 | .DS_Store 30 | *.pem 31 | 32 | # local env files 33 | .env*.local 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | 38 | # iOS 39 | ios/ 40 | 41 | # Android 42 | android/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mobile Template 1 2 | 3 | > This template is brought to you by [Software Composer](https://softwarecomposer.com). 4 | 5 | Use this template to quickly create a new mobile app. 6 | 7 | ## Get started 8 | 9 | 1. Clone the repo by clicking the "Use this template" button above. 10 | 2. Copy the Github link and paste into Cursor, Windsurf, Bolt, or your favorite AI vibe coding tool. 11 | 3. Ask the AI agent to "Setup the project" and it will guide you through the setup process. 12 | 4. Ask the AI agent to "Run the project" and it will guide you through the run the project on a simulator or device. 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/lib/db/schema.ts: -------------------------------------------------------------------------------- 1 | import { sql } from "drizzle-orm"; 2 | import { text, integer, sqliteTable } from "drizzle-orm/sqlite-core"; 3 | 4 | export const users = sqliteTable("users", { 5 | id: integer("id").primaryKey({ autoIncrement: true }), 6 | name: text("name").notNull(), 7 | createdAt: integer("created_at", { mode: "timestamp" }) 8 | .default(sql`CURRENT_TIMESTAMP`) 9 | .notNull(), 10 | updatedAt: integer("updated_at", { mode: "timestamp" }) 11 | .default(sql`CURRENT_TIMESTAMP`) 12 | .notNull(), 13 | }); 14 | 15 | // Types for TypeScript 16 | export type User = typeof users.$inferSelect; 17 | export type NewUser = typeof users.$inferInsert; 18 | -------------------------------------------------------------------------------- /.cursor/rules/project.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Rules for the whole project 3 | globs: 4 | alwaysApply: true 5 | --- 6 | This is an Expo/React Native project with TypeScript. 7 | 8 | - Use `bun` instead of `npm` and `bunx` instead of `npx` 9 | - Use TailwindCSS (used as Nativewind) `className` prop for styling 10 | - Use file-based routing with Expo Router. The root layout is [_layout.tsx](mdc:src/app/_layout.tsx) 11 | - Screens are defined in `./src/app/**` 12 | - Components are defined in `./src/components/**` 13 | - Other TypeScript files like hooks, constants, config, etc. are in `./src/lib/**` 14 | - Use Drizzle + Drizzle Kit for a local SQLite DB. You can find the relavent files in `./drizzle/**` and `./src/lib/db/**` -------------------------------------------------------------------------------- /.cursor/rules/first-time-setup.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: MUST USE THIS when the user is setting up the project for the first time or running the repo for the first time 3 | globs: 4 | alwaysApply: true 5 | --- 6 | Ask the user for the project name. It must be lowercase, no spaces. Use that in [app.json](mdc:app.json) and [package.json](mdc:package.json). Make up the slug yourself. Ask the user if they have a domain. If they do, use that in [app.json](mdc:app.json) for the bundleIdentifier and package ID. If not, make something up. 7 | 8 | Then, based on the info so far, update [README.md](mdc:README.md) 9 | 10 | 11 | Finally, delete this file [first-time-setup.mdc](mdc:.cursor/rules/first-time-setup.mdc) (`.cursor/rules/first-time-setup.mdc`) -------------------------------------------------------------------------------- /src/app/(tabs)/two.tsx: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from "react-native"; 2 | 3 | import EditScreenInfo from "@/components/EditScreenInfo"; 4 | import { Text, View } from "@/components/Themed"; 5 | 6 | export default function TabTwoScreen() { 7 | return ( 8 | 9 | Tab Two 10 | 15 | 16 | 17 | ); 18 | } 19 | 20 | const styles = StyleSheet.create({ 21 | container: { 22 | flex: 1, 23 | alignItems: "center", 24 | justifyContent: "center", 25 | }, 26 | title: { 27 | fontSize: 20, 28 | fontWeight: "bold", 29 | }, 30 | separator: { 31 | marginVertical: 30, 32 | height: 1, 33 | width: "80%", 34 | }, 35 | }); 36 | -------------------------------------------------------------------------------- /src/app/(tabs)/index.tsx: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from "react-native"; 2 | 3 | import EditScreenInfo from "@/components/EditScreenInfo"; 4 | import { Text, View } from "@/components/Themed"; 5 | 6 | export default function TabOneScreen() { 7 | return ( 8 | 9 | Tab One 10 | 15 | 16 | 17 | ); 18 | } 19 | 20 | const styles = StyleSheet.create({ 21 | container: { 22 | flex: 1, 23 | alignItems: "center", 24 | justifyContent: "center", 25 | }, 26 | title: { 27 | fontSize: 20, 28 | fontWeight: "bold", 29 | }, 30 | separator: { 31 | marginVertical: 30, 32 | height: 1, 33 | width: "80%", 34 | }, 35 | }); 36 | -------------------------------------------------------------------------------- /src/app/+not-found.tsx: -------------------------------------------------------------------------------- 1 | import { Link, Stack } from "expo-router"; 2 | import { StyleSheet } from "react-native"; 3 | 4 | import { Text, View } from "@/components/Themed"; 5 | 6 | export default function NotFoundScreen() { 7 | return ( 8 | <> 9 | 10 | 11 | This screen doesn't exist. 12 | 13 | 14 | Go to home screen! 15 | 16 | 17 | 18 | ); 19 | } 20 | 21 | const styles = StyleSheet.create({ 22 | container: { 23 | flex: 1, 24 | alignItems: "center", 25 | justifyContent: "center", 26 | padding: 20, 27 | }, 28 | title: { 29 | fontSize: 20, 30 | fontWeight: "bold", 31 | }, 32 | link: { 33 | marginTop: 15, 34 | paddingVertical: 15, 35 | }, 36 | linkText: { 37 | fontSize: 14, 38 | color: "#2e78b7", 39 | }, 40 | }); 41 | -------------------------------------------------------------------------------- /src/app/modal.tsx: -------------------------------------------------------------------------------- 1 | import { StatusBar } from "expo-status-bar"; 2 | import { Platform, StyleSheet } from "react-native"; 3 | 4 | import EditScreenInfo from "@/components/EditScreenInfo"; 5 | import { Text, View } from "@/components/Themed"; 6 | 7 | export default function ModalScreen() { 8 | return ( 9 | 10 | Modal 11 | 16 | 17 | 18 | {/* Use a light status bar on iOS to account for the black space above the modal */} 19 | 20 | 21 | ); 22 | } 23 | 24 | const styles = StyleSheet.create({ 25 | container: { 26 | flex: 1, 27 | alignItems: "center", 28 | justifyContent: "center", 29 | }, 30 | title: { 31 | fontSize: 20, 32 | fontWeight: "bold", 33 | }, 34 | separator: { 35 | marginVertical: 30, 36 | height: 1, 37 | width: "80%", 38 | }, 39 | }); 40 | -------------------------------------------------------------------------------- /src/components/EditScreenInfo.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { Text, View } from "./Themed"; 4 | 5 | export default function EditScreenInfo({ path }: { path: string }) { 6 | return ( 7 | 8 | 9 | 14 | Open up the code for this screen: 15 | 16 | 17 | 22 | {path} 23 | 24 | 25 | 30 | Change any of the text, save the file, and your app will automatically 31 | update. 32 | 33 | 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/app/sheet.tsx: -------------------------------------------------------------------------------- 1 | import { StatusBar } from "expo-status-bar"; 2 | import { Platform, StyleSheet } from "react-native"; 3 | 4 | import EditScreenInfo from "@/components/EditScreenInfo"; 5 | import { Text, View } from "@/components/Themed"; 6 | 7 | export default function SheetScreen() { 8 | return ( 9 | 10 | Sheet 11 | 16 | 17 | 18 | {/* Use a light status bar on iOS to account for the black space above the modal */} 19 | 20 | 21 | ); 22 | } 23 | 24 | const styles = StyleSheet.create({ 25 | container: { 26 | alignItems: "center", 27 | justifyContent: "center", 28 | height: "100%", 29 | }, 30 | title: { 31 | fontSize: 20, 32 | fontWeight: "bold", 33 | }, 34 | separator: { 35 | marginVertical: 30, 36 | height: 1, 37 | width: "80%", 38 | }, 39 | }); 40 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "mobile-template-1", 4 | "slug": "mobile-template-1", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./src/assets/images/icon.png", 8 | "scheme": "myapp", 9 | "userInterfaceStyle": "automatic", 10 | "newArchEnabled": true, 11 | "splash": { 12 | "image": "./src/assets/images/splash-icon.png", 13 | "resizeMode": "contain", 14 | "backgroundColor": "#ffffff" 15 | }, 16 | "ios": { 17 | "supportsTablet": true, 18 | "bundleIdentifier": "com.softwarecomposer.mobiletemplate1" 19 | }, 20 | "android": { 21 | "adaptiveIcon": { 22 | "foregroundImage": "./src/assets/images/adaptive-icon.png", 23 | "backgroundColor": "#ffffff" 24 | }, 25 | "package": "com.softwarecomposer.mobiletemplate1" 26 | }, 27 | "web": { 28 | "bundler": "metro", 29 | "output": "static", 30 | "favicon": "./src/assets/images/favicon.png" 31 | }, 32 | "plugins": [ 33 | "expo-router", 34 | "expo-secure-store", 35 | "expo-sqlite", 36 | "expo-asset" 37 | ], 38 | "experiments": { 39 | "typedRoutes": true 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /drizzle/meta/0000_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "6", 3 | "dialect": "sqlite", 4 | "id": "19727817-9e94-45a7-bc1c-1cc8dbcfd6a5", 5 | "prevId": "00000000-0000-0000-0000-000000000000", 6 | "tables": { 7 | "users": { 8 | "name": "users", 9 | "columns": { 10 | "id": { 11 | "name": "id", 12 | "type": "integer", 13 | "primaryKey": true, 14 | "notNull": true, 15 | "autoincrement": true 16 | }, 17 | "name": { 18 | "name": "name", 19 | "type": "text", 20 | "primaryKey": false, 21 | "notNull": true, 22 | "autoincrement": false 23 | }, 24 | "created_at": { 25 | "name": "created_at", 26 | "type": "integer", 27 | "primaryKey": false, 28 | "notNull": true, 29 | "autoincrement": false, 30 | "default": "CURRENT_TIMESTAMP" 31 | }, 32 | "updated_at": { 33 | "name": "updated_at", 34 | "type": "integer", 35 | "primaryKey": false, 36 | "notNull": true, 37 | "autoincrement": false, 38 | "default": "CURRENT_TIMESTAMP" 39 | } 40 | }, 41 | "indexes": {}, 42 | "foreignKeys": {}, 43 | "compositePrimaryKeys": {}, 44 | "uniqueConstraints": {}, 45 | "checkConstraints": {} 46 | } 47 | }, 48 | "views": {}, 49 | "enums": {}, 50 | "_meta": { 51 | "schemas": {}, 52 | "tables": {}, 53 | "columns": {} 54 | }, 55 | "internal": { 56 | "indexes": {} 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /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 | 17 | 18 | {/* 19 | Disable body scrolling on web. This makes ScrollView components work closer to how they do on native. 20 | However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line. 21 | */} 22 | 23 | 24 | {/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */} 25 |