├── .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 |
26 | {/* Add any additional elements that you want globally available on web... */}
27 |
28 | {children}
29 |
30 | );
31 | }
32 |
33 | const responsiveBackground = `
34 | body {
35 | background-color: #fff;
36 | }
37 | @media (prefers-color-scheme: dark) {
38 | body {
39 | background-color: #000;
40 | }
41 | }`;
42 |
--------------------------------------------------------------------------------
/src/components/Themed.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Learn more about Light and Dark modes:
3 | * https://docs.expo.io/guides/color-schemes/
4 | */
5 |
6 | import { Text as DefaultText, View as DefaultView } from "react-native";
7 |
8 | import Colors from "@/lib/constants/Colors";
9 | import { useColorScheme } from "./useColorScheme";
10 |
11 | type ThemeProps = {
12 | lightColor?: string;
13 | darkColor?: string;
14 | };
15 |
16 | export type TextProps = ThemeProps & DefaultText["props"];
17 | export type ViewProps = ThemeProps & DefaultView["props"];
18 |
19 | export function useThemeColor(
20 | props: { light?: string; dark?: string },
21 | colorName: keyof typeof Colors.light & keyof typeof Colors.dark,
22 | ) {
23 | const theme = useColorScheme() ?? "light";
24 | const colorFromProps = props[theme];
25 |
26 | if (colorFromProps) {
27 | return colorFromProps;
28 | } else {
29 | return Colors[theme][colorName];
30 | }
31 | }
32 |
33 | export function Text(props: TextProps) {
34 | const { style, lightColor, darkColor, className, ...otherProps } = props;
35 | const color = useThemeColor({ light: lightColor, dark: darkColor }, "text");
36 |
37 | return (
38 |
43 | );
44 | }
45 |
46 | export function View(props: ViewProps) {
47 | const { style, lightColor, darkColor, className, ...otherProps } = props;
48 | const backgroundColor = useThemeColor(
49 | { light: lightColor, dark: darkColor },
50 | "background",
51 | );
52 |
53 | return (
54 |
59 | );
60 | }
61 |
--------------------------------------------------------------------------------
/src/components/PressableWithScale.tsx:
--------------------------------------------------------------------------------
1 | import { selectionAsync } from "expo-haptics";
2 | import React from 'react';
3 | import { Pressable, PressableProps, StyleProp, ViewStyle } from 'react-native';
4 | import Animated, {
5 | cancelAnimation,
6 | runOnJS,
7 | useAnimatedStyle,
8 | useReducedMotion,
9 | useSharedValue,
10 | withTiming,
11 | } from 'react-native-reanimated';
12 |
13 | const DEFAULT_TARGET_SCALE = 0.98;
14 |
15 | const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
16 |
17 | export function PressableWithScale({
18 | targetScale = DEFAULT_TARGET_SCALE,
19 | children,
20 | style,
21 | onPressIn,
22 | onPressOut,
23 | ...rest
24 | }: {
25 | targetScale?: number;
26 | style?: StyleProp;
27 | } & Exclude) {
28 | const reducedMotion = useReducedMotion();
29 |
30 | const scale = useSharedValue(1);
31 |
32 | const animatedStyle = useAnimatedStyle(() => ({
33 | transform: [{ scale: scale.value }],
34 | }));
35 |
36 | return (
37 | {
40 | 'worklet';
41 | if (onPressIn) {
42 | runOnJS(onPressIn)(e);
43 | }
44 | runOnJS(selectionAsync)();
45 | cancelAnimation(scale);
46 | scale.value = withTiming(targetScale, { duration: 100 });
47 | }}
48 | onPressOut={(e) => {
49 | 'worklet';
50 | if (onPressOut) {
51 | runOnJS(onPressOut)(e);
52 | }
53 | cancelAnimation(scale);
54 | scale.value = withTiming(1, { duration: 100 });
55 | }}
56 | style={[!reducedMotion && animatedStyle, style]}
57 | {...rest}
58 | >
59 | {children}
60 |
61 | );
62 | }
63 |
--------------------------------------------------------------------------------
/src/app/_layout.tsx:
--------------------------------------------------------------------------------
1 | import "expo-dev-client";
2 | import "../../global.css";
3 | import FontAwesome from "@expo/vector-icons/FontAwesome";
4 | import {
5 | DarkTheme,
6 | DefaultTheme,
7 | ThemeProvider,
8 | } from "@react-navigation/native";
9 | import { useFonts } from "expo-font";
10 | import { Stack } from "expo-router";
11 | import * as SplashScreen from "expo-splash-screen";
12 | import { useEffect } from "react";
13 | import "react-native-reanimated";
14 |
15 | import { useColorScheme } from "@/components/useColorScheme";
16 | import { MigrationProvider } from "@/lib/db/MigrationProvider";
17 |
18 | export {
19 | // Catch any errors thrown by the Layout component.
20 | ErrorBoundary,
21 | } from "expo-router";
22 |
23 | export const unstable_settings = {
24 | // Ensure that reloading on `/modal` keeps a back button present.
25 | initialRouteName: "(tabs)",
26 | };
27 |
28 | // Prevent the splash screen from auto-hiding before asset loading is complete.
29 | SplashScreen.preventAutoHideAsync();
30 |
31 | export default function RootLayout() {
32 | const [loaded, error] = useFonts({
33 | SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"),
34 | ...FontAwesome.font,
35 | });
36 |
37 | // Expo Router uses Error Boundaries to catch errors in the navigation tree.
38 | useEffect(() => {
39 | if (error) throw error;
40 | }, [error]);
41 |
42 | useEffect(() => {
43 | if (loaded) {
44 | SplashScreen.hideAsync();
45 | }
46 | }, [loaded]);
47 |
48 | if (!loaded) {
49 | return null;
50 | }
51 |
52 | return ;
53 | }
54 |
55 | function RootLayoutNav() {
56 | const colorScheme = useColorScheme();
57 |
58 | return (
59 |
60 |
61 |
62 |
63 |
64 |
73 |
74 |
75 |
76 | );
77 | }
78 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mobile-template-1",
3 | "main": "expo-router/entry",
4 | "version": "1.0.0",
5 | "scripts": {
6 | "start": "expo start",
7 | "android": "expo run:android",
8 | "ios": "expo run:ios",
9 | "web": "expo start --web",
10 | "test": "jest --watchAll",
11 | "lint": "eslint .",
12 | "db:generate": "drizzle-kit generate",
13 | "db:migrate": "drizzle-kit migrate",
14 | "db:view": "drizzle-kit studio"
15 | },
16 | "jest": {
17 | "preset": "jest-expo"
18 | },
19 | "dependencies": {
20 | "@expo/vector-icons": "^14.0.2",
21 | "@react-navigation/native": "^7.0.14",
22 | "babel-plugin-inline-import": "^3.0.0",
23 | "drizzle-orm": "^0.40.0",
24 | "expo": "~52.0.37",
25 | "expo-asset": "~11.0.4",
26 | "expo-auth-session": "~6.0.3",
27 | "expo-av": "~15.0.2",
28 | "expo-camera": "~16.0.17",
29 | "expo-crypto": "~14.0.2",
30 | "expo-dev-client": "~5.0.12",
31 | "expo-device": "~7.0.2",
32 | "expo-file-system": "~18.0.11",
33 | "expo-font": "~13.0.4",
34 | "expo-haptics": "~14.0.1",
35 | "expo-image": "~2.0.6",
36 | "expo-image-picker": "~16.0.6",
37 | "expo-linking": "~7.0.5",
38 | "expo-location": "~18.0.7",
39 | "expo-media-library": "~17.0.6",
40 | "expo-network": "~7.0.5",
41 | "expo-notifications": "~0.29.13",
42 | "expo-random": "^14.0.1",
43 | "expo-router": "~4.0.17",
44 | "expo-secure-store": "~14.0.1",
45 | "expo-sensors": "~14.0.2",
46 | "expo-sharing": "~13.0.1",
47 | "expo-splash-screen": "~0.29.22",
48 | "expo-sqlite": "^15.1.2",
49 | "expo-status-bar": "~2.0.1",
50 | "expo-system-ui": "~4.0.8",
51 | "expo-updates": "~0.27.1",
52 | "expo-web-browser": "~14.0.2",
53 | "nativewind": "^4.1.23",
54 | "react": "18.3.1",
55 | "react-dom": "18.3.1",
56 | "react-native": "0.76.7",
57 | "react-native-reanimated": "3.16.2",
58 | "react-native-safe-area-context": "4.12.0",
59 | "react-native-screens": "~4.4.0",
60 | "react-native-web": "~0.19.13",
61 | "tailwindcss": "^3.4.17"
62 | },
63 | "devDependencies": {
64 | "@babel/core": "^7.25.2",
65 | "@types/react": "~18.3.12",
66 | "drizzle-kit": "^0.30.5",
67 | "eslint": "^8.57.0",
68 | "eslint-config-expo": "~8.0.1",
69 | "eslint-config-prettier": "^10.0.2",
70 | "eslint-plugin-prettier": "^5.2.3",
71 | "jest": "^29.2.1",
72 | "jest-expo": "~52.0.4",
73 | "prettier": "^3.5.3",
74 | "react-test-renderer": "18.3.1",
75 | "typescript": "~5.3.3"
76 | },
77 | "private": true
78 | }
79 |
--------------------------------------------------------------------------------
/src/app/(tabs)/_layout.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import FontAwesome from "@expo/vector-icons/FontAwesome";
3 | import { Link, Tabs } from "expo-router";
4 | import { Pressable } from "react-native";
5 |
6 | import Colors from "@/lib/constants/Colors";
7 | import { useColorScheme } from "@/components/useColorScheme";
8 | import { useClientOnlyValue } from "@/components/useClientOnlyValue";
9 |
10 | // You can explore the built-in icon families and icons on the web at https://icons.expo.fyi/
11 | function TabBarIcon(props: {
12 | name: React.ComponentProps["name"];
13 | color: string;
14 | }) {
15 | return ;
16 | }
17 |
18 | export default function TabLayout() {
19 | const colorScheme = useColorScheme();
20 |
21 | return (
22 |
30 | ,
35 | headerRight: () => (
36 |
37 |
38 | {({ pressed }) => (
39 |
45 | )}
46 |
47 |
48 | ),
49 | }}
50 | />
51 | ,
56 | headerRight: () => (
57 |
58 |
59 | {({ pressed }) => (
60 |
66 | )}
67 |
68 |
69 | ),
70 | }}
71 | />
72 |
73 | );
74 | }
75 |
--------------------------------------------------------------------------------