Move to monorepo for React Native!
							
								
								
									
										44
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -4,44 +4,38 @@
 | 
			
		||||
/host/convex/docker/data
 | 
			
		||||
 | 
			
		||||
# dependencies
 | 
			
		||||
/node_modules
 | 
			
		||||
/.pnp
 | 
			
		||||
.pnp.*
 | 
			
		||||
.yarn/*
 | 
			
		||||
!.yarn/patches
 | 
			
		||||
!.yarn/plugins
 | 
			
		||||
!.yarn/releases
 | 
			
		||||
!.yarn/versions
 | 
			
		||||
node_modules
 | 
			
		||||
.pnp
 | 
			
		||||
.pnp.js
 | 
			
		||||
 | 
			
		||||
# testing
 | 
			
		||||
/coverage
 | 
			
		||||
coverage
 | 
			
		||||
 | 
			
		||||
# next.js
 | 
			
		||||
/.next/
 | 
			
		||||
/out/
 | 
			
		||||
.next/
 | 
			
		||||
.swc/
 | 
			
		||||
out/
 | 
			
		||||
build
 | 
			
		||||
 | 
			
		||||
# production
 | 
			
		||||
/build
 | 
			
		||||
# expo
 | 
			
		||||
.expo
 | 
			
		||||
 | 
			
		||||
# misc
 | 
			
		||||
.DS_Store
 | 
			
		||||
*.pem
 | 
			
		||||
dist
 | 
			
		||||
 | 
			
		||||
# debug
 | 
			
		||||
npm-debug.log*
 | 
			
		||||
yarn-debug.log*
 | 
			
		||||
yarn-error.log*
 | 
			
		||||
.pnpm-debug.log*
 | 
			
		||||
 | 
			
		||||
# env files (can opt-in for committing if needed)
 | 
			
		||||
.env*
 | 
			
		||||
# local env files
 | 
			
		||||
.env.local
 | 
			
		||||
.env.development.local
 | 
			
		||||
.env.test.local
 | 
			
		||||
.env.production.local
 | 
			
		||||
.env
 | 
			
		||||
 | 
			
		||||
# vercel
 | 
			
		||||
.vercel
 | 
			
		||||
 | 
			
		||||
# typescript
 | 
			
		||||
*.tsbuildinfo
 | 
			
		||||
next-env.d.ts
 | 
			
		||||
 | 
			
		||||
# Ignored for the template, you probably want to remove it:
 | 
			
		||||
package-lock.json
 | 
			
		||||
# turbo
 | 
			
		||||
.turbo
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@
 | 
			
		||||
  "singleQuote": true,
 | 
			
		||||
  "jsxSingleQuote": true,
 | 
			
		||||
  "trailingComma": "all",
 | 
			
		||||
	"tabWidth": 2
 | 
			
		||||
	"tabWidth": 2,
 | 
			
		||||
  "arrowParens": "always"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										12
									
								
								README.md
									
									
									
									
									
								
							
							
						
						@@ -1,8 +1,8 @@
 | 
			
		||||
<h1 align="center">
 | 
			
		||||
    <br>
 | 
			
		||||
    <img
 | 
			
		||||
        src="https://git.gbrown.org/gib/techtracker/raw/branch/main/public/favicon.png"
 | 
			
		||||
        alt="Next Template"
 | 
			
		||||
        src="https://git.gbrown.org/gib/techtracker/raw/branch/main/apps/next/public/favicon.png"
 | 
			
		||||
        alt="Tech Tracker"
 | 
			
		||||
        width="100"
 | 
			
		||||
    >
 | 
			
		||||
    <br>
 | 
			
		||||
@@ -38,13 +38,17 @@ You will also need docker installed on whatever host you plan to run the Supabas
 | 
			
		||||
Copy the example environment variable files and paste them in the same directory named `.env`.
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
cp ./env.example ./.env
 | 
			
		||||
cp ./app/next/env.example ./app/next/.env
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
cp ./host/convex/docker/env.example ./host/convex/docker/.env
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
cp ./host/next/docker/env.example ./host/next/docker/.env
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Start self hosted convex
 | 
			
		||||
 | 
			
		||||
The basic gist is to run the commands below after you have filled out the environment variables you plan to use, but you should ultimately follow the [guide they provide](https://github.com/get-convex/convex-backend/tree/main/self-hosted)
 | 
			
		||||
@@ -70,7 +74,7 @@ You can also run
 | 
			
		||||
bun dev:slow
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
to start your development environment with webpack
 | 
			
		||||
to start your development environment with webpack (This is for the next app.)
 | 
			
		||||
 | 
			
		||||
### Start your Production Environment.
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										43
									
								
								apps/expo/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,43 @@
 | 
			
		||||
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
 | 
			
		||||
 | 
			
		||||
# dependencies
 | 
			
		||||
node_modules/
 | 
			
		||||
 | 
			
		||||
# Expo
 | 
			
		||||
.expo/
 | 
			
		||||
dist/
 | 
			
		||||
web-build/
 | 
			
		||||
expo-env.d.ts
 | 
			
		||||
 | 
			
		||||
# Native
 | 
			
		||||
.kotlin/
 | 
			
		||||
*.orig.*
 | 
			
		||||
*.jks
 | 
			
		||||
*.p8
 | 
			
		||||
*.p12
 | 
			
		||||
*.key
 | 
			
		||||
*.mobileprovision
 | 
			
		||||
 | 
			
		||||
# Metro
 | 
			
		||||
.metro-health-check*
 | 
			
		||||
 | 
			
		||||
# debug
 | 
			
		||||
npm-debug.*
 | 
			
		||||
yarn-debug.*
 | 
			
		||||
yarn-error.*
 | 
			
		||||
 | 
			
		||||
# macOS
 | 
			
		||||
.DS_Store
 | 
			
		||||
*.pem
 | 
			
		||||
 | 
			
		||||
# local env files
 | 
			
		||||
.env*.local
 | 
			
		||||
 | 
			
		||||
# typescript
 | 
			
		||||
*.tsbuildinfo
 | 
			
		||||
 | 
			
		||||
app-example
 | 
			
		||||
 | 
			
		||||
# generated native folders
 | 
			
		||||
/ios
 | 
			
		||||
/android
 | 
			
		||||
							
								
								
									
										48
									
								
								apps/expo/app.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,48 @@
 | 
			
		||||
{
 | 
			
		||||
  "expo": {
 | 
			
		||||
    "name": "techtracker-expo",
 | 
			
		||||
    "slug": "techtracker-expo",
 | 
			
		||||
    "version": "1.0.0",
 | 
			
		||||
    "orientation": "portrait",
 | 
			
		||||
    "icon": "./assets/images/icon.png",
 | 
			
		||||
    "scheme": "techtrackerexpo",
 | 
			
		||||
    "userInterfaceStyle": "automatic",
 | 
			
		||||
    "newArchEnabled": true,
 | 
			
		||||
    "ios": {
 | 
			
		||||
      "supportsTablet": true
 | 
			
		||||
    },
 | 
			
		||||
    "android": {
 | 
			
		||||
      "adaptiveIcon": {
 | 
			
		||||
        "backgroundColor": "#E6F4FE",
 | 
			
		||||
        "foregroundImage": "./assets/images/android-icon-foreground.png",
 | 
			
		||||
        "backgroundImage": "./assets/images/android-icon-background.png",
 | 
			
		||||
        "monochromeImage": "./assets/images/android-icon-monochrome.png"
 | 
			
		||||
      },
 | 
			
		||||
      "edgeToEdgeEnabled": true,
 | 
			
		||||
      "predictiveBackGestureEnabled": false
 | 
			
		||||
    },
 | 
			
		||||
    "web": {
 | 
			
		||||
      "output": "static",
 | 
			
		||||
      "favicon": "./assets/images/favicon.png"
 | 
			
		||||
    },
 | 
			
		||||
    "plugins": [
 | 
			
		||||
      "expo-router",
 | 
			
		||||
      [
 | 
			
		||||
        "expo-splash-screen",
 | 
			
		||||
        {
 | 
			
		||||
          "image": "./assets/images/splash-icon.png",
 | 
			
		||||
          "imageWidth": 200,
 | 
			
		||||
          "resizeMode": "contain",
 | 
			
		||||
          "backgroundColor": "#ffffff",
 | 
			
		||||
          "dark": {
 | 
			
		||||
            "backgroundColor": "#000000"
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
    ],
 | 
			
		||||
    "experiments": {
 | 
			
		||||
      "typedRoutes": true,
 | 
			
		||||
      "reactCompiler": true
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								apps/expo/assets/images/android-icon-background.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 17 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								apps/expo/assets/images/android-icon-foreground.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 77 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								apps/expo/assets/images/android-icon-monochrome.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 4.0 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								apps/expo/assets/images/favicon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.1 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								apps/expo/assets/images/icon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 384 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								apps/expo/assets/images/partial-react-logo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 5.0 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								apps/expo/assets/images/react-logo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 6.2 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								apps/expo/assets/images/react-logo@2x.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 14 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								apps/expo/assets/images/react-logo@3x.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 21 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								apps/expo/assets/images/splash-icon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 17 KiB  | 
							
								
								
									
										10
									
								
								apps/expo/eslint.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,10 @@
 | 
			
		||||
// https://docs.expo.dev/guides/using-eslint/
 | 
			
		||||
const { defineConfig } = require('eslint/config');
 | 
			
		||||
const expoConfig = require('eslint-config-expo/flat');
 | 
			
		||||
 | 
			
		||||
module.exports = defineConfig([
 | 
			
		||||
  expoConfig,
 | 
			
		||||
  {
 | 
			
		||||
    ignores: ['dist/*'],
 | 
			
		||||
  },
 | 
			
		||||
]);
 | 
			
		||||
							
								
								
									
										45
									
								
								apps/expo/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,45 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "techtracker-expo",
 | 
			
		||||
  "main": "expo-router/entry",
 | 
			
		||||
  "version": "1.0.0",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "start": "expo start",
 | 
			
		||||
    "dev": "expo start",
 | 
			
		||||
    "android": "expo start --android",
 | 
			
		||||
    "ios": "expo start --ios",
 | 
			
		||||
    "web": "expo start --web",
 | 
			
		||||
    "lint": "expo lint"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@expo/vector-icons": "^15.0.2",
 | 
			
		||||
    "@react-navigation/bottom-tabs": "^7.4.0",
 | 
			
		||||
    "@react-navigation/elements": "^2.6.3",
 | 
			
		||||
    "@react-navigation/native": "^7.1.8",
 | 
			
		||||
    "expo": "~54.0.4",
 | 
			
		||||
    "expo-constants": "~18.0.8",
 | 
			
		||||
    "expo-font": "~14.0.8",
 | 
			
		||||
    "expo-haptics": "~15.0.7",
 | 
			
		||||
    "expo-image": "~3.0.8",
 | 
			
		||||
    "expo-linking": "~8.0.8",
 | 
			
		||||
    "expo-router": "~6.0.2",
 | 
			
		||||
    "expo-splash-screen": "~31.0.9",
 | 
			
		||||
    "expo-status-bar": "~3.0.8",
 | 
			
		||||
    "expo-symbols": "~1.0.7",
 | 
			
		||||
    "expo-system-ui": "~6.0.7",
 | 
			
		||||
    "expo-web-browser": "~15.0.7",
 | 
			
		||||
    "react": "19.1.0",
 | 
			
		||||
    "react-dom": "19.1.0",
 | 
			
		||||
    "react-native": "0.81.4",
 | 
			
		||||
    "react-native-gesture-handler": "~2.28.0",
 | 
			
		||||
    "react-native-worklets": "0.5.1",
 | 
			
		||||
    "react-native-reanimated": "~4.1.0",
 | 
			
		||||
    "react-native-safe-area-context": "~5.6.0",
 | 
			
		||||
    "react-native-screens": "~4.16.0",
 | 
			
		||||
    "react-native-web": "~0.21.0"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@types/react": "~19.1.0",
 | 
			
		||||
    "eslint-config-expo": "~10.0.0"
 | 
			
		||||
  },
 | 
			
		||||
  "private": true
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										35
									
								
								apps/expo/src/app/(tabs)/_layout.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,35 @@
 | 
			
		||||
import { Tabs } from 'expo-router';
 | 
			
		||||
import React from 'react';
 | 
			
		||||
 | 
			
		||||
import { HapticTab } from '@/components/haptic-tab';
 | 
			
		||||
import { IconSymbol } from '@/components/ui/icon-symbol';
 | 
			
		||||
import { Colors } from '@/constants/theme';
 | 
			
		||||
import { useColorScheme } from '@/hooks/use-color-scheme';
 | 
			
		||||
 | 
			
		||||
export default function TabLayout() {
 | 
			
		||||
  const colorScheme = useColorScheme();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Tabs
 | 
			
		||||
      screenOptions={{
 | 
			
		||||
        tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint,
 | 
			
		||||
        headerShown: false,
 | 
			
		||||
        tabBarButton: HapticTab,
 | 
			
		||||
      }}>
 | 
			
		||||
      <Tabs.Screen
 | 
			
		||||
        name="index"
 | 
			
		||||
        options={{
 | 
			
		||||
          title: 'Home',
 | 
			
		||||
          tabBarIcon: ({ color }) => <IconSymbol size={28} name="house.fill" color={color} />,
 | 
			
		||||
        }}
 | 
			
		||||
      />
 | 
			
		||||
      <Tabs.Screen
 | 
			
		||||
        name="explore"
 | 
			
		||||
        options={{
 | 
			
		||||
          title: 'Explore',
 | 
			
		||||
          tabBarIcon: ({ color }) => <IconSymbol size={28} name="paperplane.fill" color={color} />,
 | 
			
		||||
        }}
 | 
			
		||||
      />
 | 
			
		||||
    </Tabs>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										112
									
								
								apps/expo/src/app/(tabs)/explore.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,112 @@
 | 
			
		||||
import { Image } from 'expo-image';
 | 
			
		||||
import { Platform, StyleSheet } from 'react-native';
 | 
			
		||||
 | 
			
		||||
import { Collapsible } from '@/components/ui/collapsible';
 | 
			
		||||
import { ExternalLink } from '@/components/external-link';
 | 
			
		||||
import ParallaxScrollView from '@/components/parallax-scroll-view';
 | 
			
		||||
import { ThemedText } from '@/components/themed-text';
 | 
			
		||||
import { ThemedView } from '@/components/themed-view';
 | 
			
		||||
import { IconSymbol } from '@/components/ui/icon-symbol';
 | 
			
		||||
import { Fonts } from '@/constants/theme';
 | 
			
		||||
 | 
			
		||||
export default function TabTwoScreen() {
 | 
			
		||||
  return (
 | 
			
		||||
    <ParallaxScrollView
 | 
			
		||||
      headerBackgroundColor={{ light: '#D0D0D0', dark: '#353636' }}
 | 
			
		||||
      headerImage={
 | 
			
		||||
        <IconSymbol
 | 
			
		||||
          size={310}
 | 
			
		||||
          color="#808080"
 | 
			
		||||
          name="chevron.left.forwardslash.chevron.right"
 | 
			
		||||
          style={styles.headerImage}
 | 
			
		||||
        />
 | 
			
		||||
      }>
 | 
			
		||||
      <ThemedView style={styles.titleContainer}>
 | 
			
		||||
        <ThemedText
 | 
			
		||||
          type="title"
 | 
			
		||||
          style={{
 | 
			
		||||
            fontFamily: Fonts.rounded,
 | 
			
		||||
          }}>
 | 
			
		||||
          Explore
 | 
			
		||||
        </ThemedText>
 | 
			
		||||
      </ThemedView>
 | 
			
		||||
      <ThemedText>This app includes example code to help you get started.</ThemedText>
 | 
			
		||||
      <Collapsible title="File-based routing">
 | 
			
		||||
        <ThemedText>
 | 
			
		||||
          This app has two screens:{' '}
 | 
			
		||||
          <ThemedText type="defaultSemiBold">app/(tabs)/index.tsx</ThemedText> and{' '}
 | 
			
		||||
          <ThemedText type="defaultSemiBold">app/(tabs)/explore.tsx</ThemedText>
 | 
			
		||||
        </ThemedText>
 | 
			
		||||
        <ThemedText>
 | 
			
		||||
          The layout file in <ThemedText type="defaultSemiBold">app/(tabs)/_layout.tsx</ThemedText>{' '}
 | 
			
		||||
          sets up the tab navigator.
 | 
			
		||||
        </ThemedText>
 | 
			
		||||
        <ExternalLink href="https://docs.expo.dev/router/introduction">
 | 
			
		||||
          <ThemedText type="link">Learn more</ThemedText>
 | 
			
		||||
        </ExternalLink>
 | 
			
		||||
      </Collapsible>
 | 
			
		||||
      <Collapsible title="Android, iOS, and web support">
 | 
			
		||||
        <ThemedText>
 | 
			
		||||
          You can open this project on Android, iOS, and the web. To open the web version, press{' '}
 | 
			
		||||
          <ThemedText type="defaultSemiBold">w</ThemedText> in the terminal running this project.
 | 
			
		||||
        </ThemedText>
 | 
			
		||||
      </Collapsible>
 | 
			
		||||
      <Collapsible title="Images">
 | 
			
		||||
        <ThemedText>
 | 
			
		||||
          For static images, you can use the <ThemedText type="defaultSemiBold">@2x</ThemedText> and{' '}
 | 
			
		||||
          <ThemedText type="defaultSemiBold">@3x</ThemedText> suffixes to provide files for
 | 
			
		||||
          different screen densities
 | 
			
		||||
        </ThemedText>
 | 
			
		||||
        <Image
 | 
			
		||||
          source={require('@/assets/images/react-logo.png')}
 | 
			
		||||
          style={{ width: 100, height: 100, alignSelf: 'center' }}
 | 
			
		||||
        />
 | 
			
		||||
        <ExternalLink href="https://reactnative.dev/docs/images">
 | 
			
		||||
          <ThemedText type="link">Learn more</ThemedText>
 | 
			
		||||
        </ExternalLink>
 | 
			
		||||
      </Collapsible>
 | 
			
		||||
      <Collapsible title="Light and dark mode components">
 | 
			
		||||
        <ThemedText>
 | 
			
		||||
          This template has light and dark mode support. The{' '}
 | 
			
		||||
          <ThemedText type="defaultSemiBold">useColorScheme()</ThemedText> hook lets you inspect
 | 
			
		||||
          what the user's current color scheme is, and so you can adjust UI colors accordingly.
 | 
			
		||||
        </ThemedText>
 | 
			
		||||
        <ExternalLink href="https://docs.expo.dev/develop/user-interface/color-themes/">
 | 
			
		||||
          <ThemedText type="link">Learn more</ThemedText>
 | 
			
		||||
        </ExternalLink>
 | 
			
		||||
      </Collapsible>
 | 
			
		||||
      <Collapsible title="Animations">
 | 
			
		||||
        <ThemedText>
 | 
			
		||||
          This template includes an example of an animated component. The{' '}
 | 
			
		||||
          <ThemedText type="defaultSemiBold">components/HelloWave.tsx</ThemedText> component uses
 | 
			
		||||
          the powerful{' '}
 | 
			
		||||
          <ThemedText type="defaultSemiBold" style={{ fontFamily: Fonts.mono }}>
 | 
			
		||||
            react-native-reanimated
 | 
			
		||||
          </ThemedText>{' '}
 | 
			
		||||
          library to create a waving hand animation.
 | 
			
		||||
        </ThemedText>
 | 
			
		||||
        {Platform.select({
 | 
			
		||||
          ios: (
 | 
			
		||||
            <ThemedText>
 | 
			
		||||
              The <ThemedText type="defaultSemiBold">components/ParallaxScrollView.tsx</ThemedText>{' '}
 | 
			
		||||
              component provides a parallax effect for the header image.
 | 
			
		||||
            </ThemedText>
 | 
			
		||||
          ),
 | 
			
		||||
        })}
 | 
			
		||||
      </Collapsible>
 | 
			
		||||
    </ParallaxScrollView>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const styles = StyleSheet.create({
 | 
			
		||||
  headerImage: {
 | 
			
		||||
    color: '#808080',
 | 
			
		||||
    bottom: -90,
 | 
			
		||||
    left: -35,
 | 
			
		||||
    position: 'absolute',
 | 
			
		||||
  },
 | 
			
		||||
  titleContainer: {
 | 
			
		||||
    flexDirection: 'row',
 | 
			
		||||
    gap: 8,
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										98
									
								
								apps/expo/src/app/(tabs)/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,98 @@
 | 
			
		||||
import { Image } from 'expo-image';
 | 
			
		||||
import { Platform, StyleSheet } from 'react-native';
 | 
			
		||||
 | 
			
		||||
import { HelloWave } from '@/components/hello-wave';
 | 
			
		||||
import ParallaxScrollView from '@/components/parallax-scroll-view';
 | 
			
		||||
import { ThemedText } from '@/components/themed-text';
 | 
			
		||||
import { ThemedView } from '@/components/themed-view';
 | 
			
		||||
import { Link } from 'expo-router';
 | 
			
		||||
 | 
			
		||||
export default function HomeScreen() {
 | 
			
		||||
  return (
 | 
			
		||||
    <ParallaxScrollView
 | 
			
		||||
      headerBackgroundColor={{ light: '#A1CEDC', dark: '#1D3D47' }}
 | 
			
		||||
      headerImage={
 | 
			
		||||
        <Image
 | 
			
		||||
          source={require('@/assets/images/partial-react-logo.png')}
 | 
			
		||||
          style={styles.reactLogo}
 | 
			
		||||
        />
 | 
			
		||||
      }>
 | 
			
		||||
      <ThemedView style={styles.titleContainer}>
 | 
			
		||||
        <ThemedText type="title">Welcome!</ThemedText>
 | 
			
		||||
        <HelloWave />
 | 
			
		||||
      </ThemedView>
 | 
			
		||||
      <ThemedView style={styles.stepContainer}>
 | 
			
		||||
        <ThemedText type="subtitle">Step 1: Try it</ThemedText>
 | 
			
		||||
        <ThemedText>
 | 
			
		||||
          Edit <ThemedText type="defaultSemiBold">app/(tabs)/index.tsx</ThemedText> to see changes.
 | 
			
		||||
          Press{' '}
 | 
			
		||||
          <ThemedText type="defaultSemiBold">
 | 
			
		||||
            {Platform.select({
 | 
			
		||||
              ios: 'cmd + d',
 | 
			
		||||
              android: 'cmd + m',
 | 
			
		||||
              web: 'F12',
 | 
			
		||||
            })}
 | 
			
		||||
          </ThemedText>{' '}
 | 
			
		||||
          to open developer tools.
 | 
			
		||||
        </ThemedText>
 | 
			
		||||
      </ThemedView>
 | 
			
		||||
      <ThemedView style={styles.stepContainer}>
 | 
			
		||||
        <Link href="/modal">
 | 
			
		||||
          <Link.Trigger>
 | 
			
		||||
            <ThemedText type="subtitle">Step 2: Explore</ThemedText>
 | 
			
		||||
          </Link.Trigger>
 | 
			
		||||
          <Link.Preview />
 | 
			
		||||
          <Link.Menu>
 | 
			
		||||
            <Link.MenuAction title="Action" icon="cube" onPress={() => alert('Action pressed')} />
 | 
			
		||||
            <Link.MenuAction
 | 
			
		||||
              title="Share"
 | 
			
		||||
              icon="square.and.arrow.up"
 | 
			
		||||
              onPress={() => alert('Share pressed')}
 | 
			
		||||
            />
 | 
			
		||||
            <Link.Menu title="More" icon="ellipsis">
 | 
			
		||||
              <Link.MenuAction
 | 
			
		||||
                title="Delete"
 | 
			
		||||
                icon="trash"
 | 
			
		||||
                destructive
 | 
			
		||||
                onPress={() => alert('Delete pressed')}
 | 
			
		||||
              />
 | 
			
		||||
            </Link.Menu>
 | 
			
		||||
          </Link.Menu>
 | 
			
		||||
        </Link>
 | 
			
		||||
 | 
			
		||||
        <ThemedText>
 | 
			
		||||
          {`Tap the Explore tab to learn more about what's included in this starter app.`}
 | 
			
		||||
        </ThemedText>
 | 
			
		||||
      </ThemedView>
 | 
			
		||||
      <ThemedView style={styles.stepContainer}>
 | 
			
		||||
        <ThemedText type="subtitle">Step 3: Get a fresh start</ThemedText>
 | 
			
		||||
        <ThemedText>
 | 
			
		||||
          {`When you're ready, run `}
 | 
			
		||||
          <ThemedText type="defaultSemiBold">npm run reset-project</ThemedText> to get a fresh{' '}
 | 
			
		||||
          <ThemedText type="defaultSemiBold">app</ThemedText> directory. This will move the current{' '}
 | 
			
		||||
          <ThemedText type="defaultSemiBold">app</ThemedText> to{' '}
 | 
			
		||||
          <ThemedText type="defaultSemiBold">app-example</ThemedText>.
 | 
			
		||||
        </ThemedText>
 | 
			
		||||
      </ThemedView>
 | 
			
		||||
    </ParallaxScrollView>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const styles = StyleSheet.create({
 | 
			
		||||
  titleContainer: {
 | 
			
		||||
    flexDirection: 'row',
 | 
			
		||||
    alignItems: 'center',
 | 
			
		||||
    gap: 8,
 | 
			
		||||
  },
 | 
			
		||||
  stepContainer: {
 | 
			
		||||
    gap: 8,
 | 
			
		||||
    marginBottom: 8,
 | 
			
		||||
  },
 | 
			
		||||
  reactLogo: {
 | 
			
		||||
    height: 178,
 | 
			
		||||
    width: 290,
 | 
			
		||||
    bottom: 0,
 | 
			
		||||
    left: 0,
 | 
			
		||||
    position: 'absolute',
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										24
									
								
								apps/expo/src/app/_layout.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,24 @@
 | 
			
		||||
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
 | 
			
		||||
import { Stack } from 'expo-router';
 | 
			
		||||
import { StatusBar } from 'expo-status-bar';
 | 
			
		||||
import 'react-native-reanimated';
 | 
			
		||||
 | 
			
		||||
import { useColorScheme } from '@/hooks/use-color-scheme';
 | 
			
		||||
 | 
			
		||||
export const unstable_settings = {
 | 
			
		||||
  anchor: '(tabs)',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default function RootLayout() {
 | 
			
		||||
  const colorScheme = useColorScheme();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
 | 
			
		||||
      <Stack>
 | 
			
		||||
        <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
 | 
			
		||||
        <Stack.Screen name="modal" options={{ presentation: 'modal', title: 'Modal' }} />
 | 
			
		||||
      </Stack>
 | 
			
		||||
      <StatusBar style="auto" />
 | 
			
		||||
    </ThemeProvider>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										29
									
								
								apps/expo/src/app/modal.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,29 @@
 | 
			
		||||
import { Link } from 'expo-router';
 | 
			
		||||
import { StyleSheet } from 'react-native';
 | 
			
		||||
 | 
			
		||||
import { ThemedText } from '@/components/themed-text';
 | 
			
		||||
import { ThemedView } from '@/components/themed-view';
 | 
			
		||||
 | 
			
		||||
export default function ModalScreen() {
 | 
			
		||||
  return (
 | 
			
		||||
    <ThemedView style={styles.container}>
 | 
			
		||||
      <ThemedText type="title">This is a modal</ThemedText>
 | 
			
		||||
      <Link href="/" dismissTo style={styles.link}>
 | 
			
		||||
        <ThemedText type="link">Go to home screen</ThemedText>
 | 
			
		||||
      </Link>
 | 
			
		||||
    </ThemedView>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const styles = StyleSheet.create({
 | 
			
		||||
  container: {
 | 
			
		||||
    flex: 1,
 | 
			
		||||
    alignItems: 'center',
 | 
			
		||||
    justifyContent: 'center',
 | 
			
		||||
    padding: 20,
 | 
			
		||||
  },
 | 
			
		||||
  link: {
 | 
			
		||||
    marginTop: 15,
 | 
			
		||||
    paddingVertical: 15,
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										25
									
								
								apps/expo/src/components/external-link.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,25 @@
 | 
			
		||||
import { Href, Link } from 'expo-router';
 | 
			
		||||
import { openBrowserAsync, WebBrowserPresentationStyle } from 'expo-web-browser';
 | 
			
		||||
import { type ComponentProps } from 'react';
 | 
			
		||||
 | 
			
		||||
type Props = Omit<ComponentProps<typeof Link>, 'href'> & { href: Href & string };
 | 
			
		||||
 | 
			
		||||
export function ExternalLink({ href, ...rest }: Props) {
 | 
			
		||||
  return (
 | 
			
		||||
    <Link
 | 
			
		||||
      target="_blank"
 | 
			
		||||
      {...rest}
 | 
			
		||||
      href={href}
 | 
			
		||||
      onPress={async (event) => {
 | 
			
		||||
        if (process.env.EXPO_OS !== 'web') {
 | 
			
		||||
          // Prevent the default behavior of linking to the default browser on native.
 | 
			
		||||
          event.preventDefault();
 | 
			
		||||
          // Open the link in an in-app browser.
 | 
			
		||||
          await openBrowserAsync(href, {
 | 
			
		||||
            presentationStyle: WebBrowserPresentationStyle.AUTOMATIC,
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
      }}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										18
									
								
								apps/expo/src/components/haptic-tab.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,18 @@
 | 
			
		||||
import { BottomTabBarButtonProps } from '@react-navigation/bottom-tabs';
 | 
			
		||||
import { PlatformPressable } from '@react-navigation/elements';
 | 
			
		||||
import * as Haptics from 'expo-haptics';
 | 
			
		||||
 | 
			
		||||
export function HapticTab(props: BottomTabBarButtonProps) {
 | 
			
		||||
  return (
 | 
			
		||||
    <PlatformPressable
 | 
			
		||||
      {...props}
 | 
			
		||||
      onPressIn={(ev) => {
 | 
			
		||||
        if (process.env.EXPO_OS === 'ios') {
 | 
			
		||||
          // Add a soft haptic feedback when pressing down on the tabs.
 | 
			
		||||
          Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
 | 
			
		||||
        }
 | 
			
		||||
        props.onPressIn?.(ev);
 | 
			
		||||
      }}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										19
									
								
								apps/expo/src/components/hello-wave.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,19 @@
 | 
			
		||||
import Animated from 'react-native-reanimated';
 | 
			
		||||
 | 
			
		||||
export function HelloWave() {
 | 
			
		||||
  return (
 | 
			
		||||
    <Animated.Text
 | 
			
		||||
      style={{
 | 
			
		||||
        fontSize: 28,
 | 
			
		||||
        lineHeight: 32,
 | 
			
		||||
        marginTop: -6,
 | 
			
		||||
        animationName: {
 | 
			
		||||
          '50%': { transform: [{ rotate: '25deg' }] },
 | 
			
		||||
        },
 | 
			
		||||
        animationIterationCount: 4,
 | 
			
		||||
        animationDuration: '300ms',
 | 
			
		||||
      }}>
 | 
			
		||||
      👋
 | 
			
		||||
    </Animated.Text>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										79
									
								
								apps/expo/src/components/parallax-scroll-view.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,79 @@
 | 
			
		||||
import type { PropsWithChildren, ReactElement } from 'react';
 | 
			
		||||
import { StyleSheet } from 'react-native';
 | 
			
		||||
import Animated, {
 | 
			
		||||
  interpolate,
 | 
			
		||||
  useAnimatedRef,
 | 
			
		||||
  useAnimatedStyle,
 | 
			
		||||
  useScrollOffset,
 | 
			
		||||
} from 'react-native-reanimated';
 | 
			
		||||
 | 
			
		||||
import { ThemedView } from '@/components/themed-view';
 | 
			
		||||
import { useColorScheme } from '@/hooks/use-color-scheme';
 | 
			
		||||
import { useThemeColor } from '@/hooks/use-theme-color';
 | 
			
		||||
 | 
			
		||||
const HEADER_HEIGHT = 250;
 | 
			
		||||
 | 
			
		||||
type Props = PropsWithChildren<{
 | 
			
		||||
  headerImage: ReactElement;
 | 
			
		||||
  headerBackgroundColor: { dark: string; light: string };
 | 
			
		||||
}>;
 | 
			
		||||
 | 
			
		||||
export default function ParallaxScrollView({
 | 
			
		||||
  children,
 | 
			
		||||
  headerImage,
 | 
			
		||||
  headerBackgroundColor,
 | 
			
		||||
}: Props) {
 | 
			
		||||
  const backgroundColor = useThemeColor({}, 'background');
 | 
			
		||||
  const colorScheme = useColorScheme() ?? 'light';
 | 
			
		||||
  const scrollRef = useAnimatedRef<Animated.ScrollView>();
 | 
			
		||||
  const scrollOffset = useScrollOffset(scrollRef);
 | 
			
		||||
  const headerAnimatedStyle = useAnimatedStyle(() => {
 | 
			
		||||
    return {
 | 
			
		||||
      transform: [
 | 
			
		||||
        {
 | 
			
		||||
          translateY: interpolate(
 | 
			
		||||
            scrollOffset.value,
 | 
			
		||||
            [-HEADER_HEIGHT, 0, HEADER_HEIGHT],
 | 
			
		||||
            [-HEADER_HEIGHT / 2, 0, HEADER_HEIGHT * 0.75]
 | 
			
		||||
          ),
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          scale: interpolate(scrollOffset.value, [-HEADER_HEIGHT, 0, HEADER_HEIGHT], [2, 1, 1]),
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    };
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Animated.ScrollView
 | 
			
		||||
      ref={scrollRef}
 | 
			
		||||
      style={{ backgroundColor, flex: 1 }}
 | 
			
		||||
      scrollEventThrottle={16}>
 | 
			
		||||
      <Animated.View
 | 
			
		||||
        style={[
 | 
			
		||||
          styles.header,
 | 
			
		||||
          { backgroundColor: headerBackgroundColor[colorScheme] },
 | 
			
		||||
          headerAnimatedStyle,
 | 
			
		||||
        ]}>
 | 
			
		||||
        {headerImage}
 | 
			
		||||
      </Animated.View>
 | 
			
		||||
      <ThemedView style={styles.content}>{children}</ThemedView>
 | 
			
		||||
    </Animated.ScrollView>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const styles = StyleSheet.create({
 | 
			
		||||
  container: {
 | 
			
		||||
    flex: 1,
 | 
			
		||||
  },
 | 
			
		||||
  header: {
 | 
			
		||||
    height: HEADER_HEIGHT,
 | 
			
		||||
    overflow: 'hidden',
 | 
			
		||||
  },
 | 
			
		||||
  content: {
 | 
			
		||||
    flex: 1,
 | 
			
		||||
    padding: 32,
 | 
			
		||||
    gap: 16,
 | 
			
		||||
    overflow: 'hidden',
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										60
									
								
								apps/expo/src/components/themed-text.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,60 @@
 | 
			
		||||
import { StyleSheet, Text, type TextProps } from 'react-native';
 | 
			
		||||
 | 
			
		||||
import { useThemeColor } from '@/hooks/use-theme-color';
 | 
			
		||||
 | 
			
		||||
export type ThemedTextProps = TextProps & {
 | 
			
		||||
  lightColor?: string;
 | 
			
		||||
  darkColor?: string;
 | 
			
		||||
  type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link';
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function ThemedText({
 | 
			
		||||
  style,
 | 
			
		||||
  lightColor,
 | 
			
		||||
  darkColor,
 | 
			
		||||
  type = 'default',
 | 
			
		||||
  ...rest
 | 
			
		||||
}: ThemedTextProps) {
 | 
			
		||||
  const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text');
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Text
 | 
			
		||||
      style={[
 | 
			
		||||
        { color },
 | 
			
		||||
        type === 'default' ? styles.default : undefined,
 | 
			
		||||
        type === 'title' ? styles.title : undefined,
 | 
			
		||||
        type === 'defaultSemiBold' ? styles.defaultSemiBold : undefined,
 | 
			
		||||
        type === 'subtitle' ? styles.subtitle : undefined,
 | 
			
		||||
        type === 'link' ? styles.link : undefined,
 | 
			
		||||
        style,
 | 
			
		||||
      ]}
 | 
			
		||||
      {...rest}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const styles = StyleSheet.create({
 | 
			
		||||
  default: {
 | 
			
		||||
    fontSize: 16,
 | 
			
		||||
    lineHeight: 24,
 | 
			
		||||
  },
 | 
			
		||||
  defaultSemiBold: {
 | 
			
		||||
    fontSize: 16,
 | 
			
		||||
    lineHeight: 24,
 | 
			
		||||
    fontWeight: '600',
 | 
			
		||||
  },
 | 
			
		||||
  title: {
 | 
			
		||||
    fontSize: 32,
 | 
			
		||||
    fontWeight: 'bold',
 | 
			
		||||
    lineHeight: 32,
 | 
			
		||||
  },
 | 
			
		||||
  subtitle: {
 | 
			
		||||
    fontSize: 20,
 | 
			
		||||
    fontWeight: 'bold',
 | 
			
		||||
  },
 | 
			
		||||
  link: {
 | 
			
		||||
    lineHeight: 30,
 | 
			
		||||
    fontSize: 16,
 | 
			
		||||
    color: '#0a7ea4',
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										14
									
								
								apps/expo/src/components/themed-view.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,14 @@
 | 
			
		||||
import { View, type ViewProps } from 'react-native';
 | 
			
		||||
 | 
			
		||||
import { useThemeColor } from '@/hooks/use-theme-color';
 | 
			
		||||
 | 
			
		||||
export type ThemedViewProps = ViewProps & {
 | 
			
		||||
  lightColor?: string;
 | 
			
		||||
  darkColor?: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function ThemedView({ style, lightColor, darkColor, ...otherProps }: ThemedViewProps) {
 | 
			
		||||
  const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background');
 | 
			
		||||
 | 
			
		||||
  return <View style={[{ backgroundColor }, style]} {...otherProps} />;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										45
									
								
								apps/expo/src/components/ui/collapsible.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,45 @@
 | 
			
		||||
import { PropsWithChildren, useState } from 'react';
 | 
			
		||||
import { StyleSheet, TouchableOpacity } from 'react-native';
 | 
			
		||||
 | 
			
		||||
import { ThemedText } from '@/components/themed-text';
 | 
			
		||||
import { ThemedView } from '@/components/themed-view';
 | 
			
		||||
import { IconSymbol } from '@/components/ui/icon-symbol';
 | 
			
		||||
import { Colors } from '@/constants/theme';
 | 
			
		||||
import { useColorScheme } from '@/hooks/use-color-scheme';
 | 
			
		||||
 | 
			
		||||
export function Collapsible({ children, title }: PropsWithChildren & { title: string }) {
 | 
			
		||||
  const [isOpen, setIsOpen] = useState(false);
 | 
			
		||||
  const theme = useColorScheme() ?? 'light';
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <ThemedView>
 | 
			
		||||
      <TouchableOpacity
 | 
			
		||||
        style={styles.heading}
 | 
			
		||||
        onPress={() => setIsOpen((value) => !value)}
 | 
			
		||||
        activeOpacity={0.8}>
 | 
			
		||||
        <IconSymbol
 | 
			
		||||
          name="chevron.right"
 | 
			
		||||
          size={18}
 | 
			
		||||
          weight="medium"
 | 
			
		||||
          color={theme === 'light' ? Colors.light.icon : Colors.dark.icon}
 | 
			
		||||
          style={{ transform: [{ rotate: isOpen ? '90deg' : '0deg' }] }}
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
        <ThemedText type="defaultSemiBold">{title}</ThemedText>
 | 
			
		||||
      </TouchableOpacity>
 | 
			
		||||
      {isOpen && <ThemedView style={styles.content}>{children}</ThemedView>}
 | 
			
		||||
    </ThemedView>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const styles = StyleSheet.create({
 | 
			
		||||
  heading: {
 | 
			
		||||
    flexDirection: 'row',
 | 
			
		||||
    alignItems: 'center',
 | 
			
		||||
    gap: 6,
 | 
			
		||||
  },
 | 
			
		||||
  content: {
 | 
			
		||||
    marginTop: 6,
 | 
			
		||||
    marginLeft: 24,
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										32
									
								
								apps/expo/src/components/ui/icon-symbol.ios.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,32 @@
 | 
			
		||||
import { SymbolView, SymbolViewProps, SymbolWeight } from 'expo-symbols';
 | 
			
		||||
import { StyleProp, ViewStyle } from 'react-native';
 | 
			
		||||
 | 
			
		||||
export function IconSymbol({
 | 
			
		||||
  name,
 | 
			
		||||
  size = 24,
 | 
			
		||||
  color,
 | 
			
		||||
  style,
 | 
			
		||||
  weight = 'regular',
 | 
			
		||||
}: {
 | 
			
		||||
  name: SymbolViewProps['name'];
 | 
			
		||||
  size?: number;
 | 
			
		||||
  color: string;
 | 
			
		||||
  style?: StyleProp<ViewStyle>;
 | 
			
		||||
  weight?: SymbolWeight;
 | 
			
		||||
}) {
 | 
			
		||||
  return (
 | 
			
		||||
    <SymbolView
 | 
			
		||||
      weight={weight}
 | 
			
		||||
      tintColor={color}
 | 
			
		||||
      resizeMode="scaleAspectFit"
 | 
			
		||||
      name={name}
 | 
			
		||||
      style={[
 | 
			
		||||
        {
 | 
			
		||||
          width: size,
 | 
			
		||||
          height: size,
 | 
			
		||||
        },
 | 
			
		||||
        style,
 | 
			
		||||
      ]}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										41
									
								
								apps/expo/src/components/ui/icon-symbol.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,41 @@
 | 
			
		||||
// Fallback for using MaterialIcons on Android and web.
 | 
			
		||||
 | 
			
		||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
 | 
			
		||||
import { SymbolWeight, SymbolViewProps } from 'expo-symbols';
 | 
			
		||||
import { ComponentProps } from 'react';
 | 
			
		||||
import { OpaqueColorValue, type StyleProp, type TextStyle } from 'react-native';
 | 
			
		||||
 | 
			
		||||
type IconMapping = Record<SymbolViewProps['name'], ComponentProps<typeof MaterialIcons>['name']>;
 | 
			
		||||
type IconSymbolName = keyof typeof MAPPING;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Add your SF Symbols to Material Icons mappings here.
 | 
			
		||||
 * - see Material Icons in the [Icons Directory](https://icons.expo.fyi).
 | 
			
		||||
 * - see SF Symbols in the [SF Symbols](https://developer.apple.com/sf-symbols/) app.
 | 
			
		||||
 */
 | 
			
		||||
const MAPPING = {
 | 
			
		||||
  'house.fill': 'home',
 | 
			
		||||
  'paperplane.fill': 'send',
 | 
			
		||||
  'chevron.left.forwardslash.chevron.right': 'code',
 | 
			
		||||
  'chevron.right': 'chevron-right',
 | 
			
		||||
} as IconMapping;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * An icon component that uses native SF Symbols on iOS, and Material Icons on Android and web.
 | 
			
		||||
 * This ensures a consistent look across platforms, and optimal resource usage.
 | 
			
		||||
 * Icon `name`s are based on SF Symbols and require manual mapping to Material Icons.
 | 
			
		||||
 */
 | 
			
		||||
export function IconSymbol({
 | 
			
		||||
  name,
 | 
			
		||||
  size = 24,
 | 
			
		||||
  color,
 | 
			
		||||
  style,
 | 
			
		||||
}: {
 | 
			
		||||
  name: IconSymbolName;
 | 
			
		||||
  size?: number;
 | 
			
		||||
  color: string | OpaqueColorValue;
 | 
			
		||||
  style?: StyleProp<TextStyle>;
 | 
			
		||||
  weight?: SymbolWeight;
 | 
			
		||||
}) {
 | 
			
		||||
  return <MaterialIcons color={color} size={size} name={MAPPING[name]} style={style} />;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										53
									
								
								apps/expo/src/constants/theme.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,53 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Below are the colors that are used in the app. The colors are defined in the light and dark mode.
 | 
			
		||||
 * There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { Platform } from 'react-native';
 | 
			
		||||
 | 
			
		||||
const tintColorLight = '#0a7ea4';
 | 
			
		||||
const tintColorDark = '#fff';
 | 
			
		||||
 | 
			
		||||
export const Colors = {
 | 
			
		||||
  light: {
 | 
			
		||||
    text: '#11181C',
 | 
			
		||||
    background: '#fff',
 | 
			
		||||
    tint: tintColorLight,
 | 
			
		||||
    icon: '#687076',
 | 
			
		||||
    tabIconDefault: '#687076',
 | 
			
		||||
    tabIconSelected: tintColorLight,
 | 
			
		||||
  },
 | 
			
		||||
  dark: {
 | 
			
		||||
    text: '#ECEDEE',
 | 
			
		||||
    background: '#151718',
 | 
			
		||||
    tint: tintColorDark,
 | 
			
		||||
    icon: '#9BA1A6',
 | 
			
		||||
    tabIconDefault: '#9BA1A6',
 | 
			
		||||
    tabIconSelected: tintColorDark,
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const Fonts = Platform.select({
 | 
			
		||||
  ios: {
 | 
			
		||||
    /** iOS `UIFontDescriptorSystemDesignDefault` */
 | 
			
		||||
    sans: 'system-ui',
 | 
			
		||||
    /** iOS `UIFontDescriptorSystemDesignSerif` */
 | 
			
		||||
    serif: 'ui-serif',
 | 
			
		||||
    /** iOS `UIFontDescriptorSystemDesignRounded` */
 | 
			
		||||
    rounded: 'ui-rounded',
 | 
			
		||||
    /** iOS `UIFontDescriptorSystemDesignMonospaced` */
 | 
			
		||||
    mono: 'ui-monospace',
 | 
			
		||||
  },
 | 
			
		||||
  default: {
 | 
			
		||||
    sans: 'normal',
 | 
			
		||||
    serif: 'serif',
 | 
			
		||||
    rounded: 'normal',
 | 
			
		||||
    mono: 'monospace',
 | 
			
		||||
  },
 | 
			
		||||
  web: {
 | 
			
		||||
    sans: "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif",
 | 
			
		||||
    serif: "Georgia, 'Times New Roman', serif",
 | 
			
		||||
    rounded: "'SF Pro Rounded', 'Hiragino Maru Gothic ProN', Meiryo, 'MS PGothic', sans-serif",
 | 
			
		||||
    mono: "SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										1
									
								
								apps/expo/src/hooks/use-color-scheme.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1 @@
 | 
			
		||||
export { useColorScheme } from 'react-native';
 | 
			
		||||
							
								
								
									
										21
									
								
								apps/expo/src/hooks/use-color-scheme.web.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,21 @@
 | 
			
		||||
import { useEffect, useState } from 'react';
 | 
			
		||||
import { useColorScheme as useRNColorScheme } from 'react-native';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * To support static rendering, this value needs to be re-calculated on the client side for web
 | 
			
		||||
 */
 | 
			
		||||
export function useColorScheme() {
 | 
			
		||||
  const [hasHydrated, setHasHydrated] = useState(false);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setHasHydrated(true);
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const colorScheme = useRNColorScheme();
 | 
			
		||||
 | 
			
		||||
  if (hasHydrated) {
 | 
			
		||||
    return colorScheme;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return 'light';
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										21
									
								
								apps/expo/src/hooks/use-theme-color.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,21 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Learn more about light and dark modes:
 | 
			
		||||
 * https://docs.expo.dev/guides/color-schemes/
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { Colors } from '@/constants/theme';
 | 
			
		||||
import { useColorScheme } from '@/hooks/use-color-scheme';
 | 
			
		||||
 | 
			
		||||
export function useThemeColor(
 | 
			
		||||
  props: { light?: string; dark?: string },
 | 
			
		||||
  colorName: keyof typeof Colors.light & keyof typeof Colors.dark
 | 
			
		||||
) {
 | 
			
		||||
  const theme = useColorScheme() ?? 'light';
 | 
			
		||||
  const colorFromProps = props[theme];
 | 
			
		||||
 | 
			
		||||
  if (colorFromProps) {
 | 
			
		||||
    return colorFromProps;
 | 
			
		||||
  } else {
 | 
			
		||||
    return Colors[theme][colorName];
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										16
									
								
								apps/expo/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,16 @@
 | 
			
		||||
{
 | 
			
		||||
  "extends": "../../tsconfig.base.json",
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    "strict": true,
 | 
			
		||||
    "baseUrl": ".",
 | 
			
		||||
    "paths": {
 | 
			
		||||
      "@/*": ["./src/*"]
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "include": [
 | 
			
		||||
    "**/*.ts",
 | 
			
		||||
    "**/*.tsx",
 | 
			
		||||
    ".expo/types/**/*.ts",
 | 
			
		||||
    "expo-env.d.ts"
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										47
									
								
								apps/next/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,47 @@
 | 
			
		||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
 | 
			
		||||
 | 
			
		||||
# Hosting
 | 
			
		||||
/host/convex/docker/data
 | 
			
		||||
 | 
			
		||||
# dependencies
 | 
			
		||||
/node_modules
 | 
			
		||||
/.pnp
 | 
			
		||||
.pnp.*
 | 
			
		||||
.yarn/*
 | 
			
		||||
!.yarn/patches
 | 
			
		||||
!.yarn/plugins
 | 
			
		||||
!.yarn/releases
 | 
			
		||||
!.yarn/versions
 | 
			
		||||
 | 
			
		||||
# testing
 | 
			
		||||
/coverage
 | 
			
		||||
 | 
			
		||||
# next.js
 | 
			
		||||
/.next/
 | 
			
		||||
/out/
 | 
			
		||||
 | 
			
		||||
# production
 | 
			
		||||
/build
 | 
			
		||||
 | 
			
		||||
# misc
 | 
			
		||||
.DS_Store
 | 
			
		||||
*.pem
 | 
			
		||||
 | 
			
		||||
# debug
 | 
			
		||||
npm-debug.log*
 | 
			
		||||
yarn-debug.log*
 | 
			
		||||
yarn-error.log*
 | 
			
		||||
.pnpm-debug.log*
 | 
			
		||||
 | 
			
		||||
# env files (can opt-in for committing if needed)
 | 
			
		||||
.env*
 | 
			
		||||
 | 
			
		||||
# vercel
 | 
			
		||||
.vercel
 | 
			
		||||
 | 
			
		||||
# typescript
 | 
			
		||||
*.tsbuildinfo
 | 
			
		||||
next-env.d.ts
 | 
			
		||||
 | 
			
		||||
# Ignored for the template, you probably want to remove it:
 | 
			
		||||
package-lock.json
 | 
			
		||||
							
								
								
									
										24
									
								
								apps/next/eslint.config.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,24 @@
 | 
			
		||||
import { FlatCompat } from '@eslint/eslintrc';
 | 
			
		||||
import tseslint from 'typescript-eslint';
 | 
			
		||||
import { baseConfig } from '../../eslint.config.base.js';
 | 
			
		||||
 | 
			
		||||
const compat = new FlatCompat({
 | 
			
		||||
  baseDirectory: import.meta.dirname,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default tseslint.config(
 | 
			
		||||
  { ignores: ['.next'] },
 | 
			
		||||
  ...compat.extends('next/core-web-vitals'),
 | 
			
		||||
  baseConfig,
 | 
			
		||||
  {
 | 
			
		||||
    linterOptions: {
 | 
			
		||||
      reportUnusedDisableDirectives: true,
 | 
			
		||||
    },
 | 
			
		||||
    languageOptions: {
 | 
			
		||||
      parserOptions: {
 | 
			
		||||
        projectService: true,
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										60
									
								
								apps/next/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,60 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "techtracker-next",
 | 
			
		||||
  "version": "0.2.0",
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "type": "module",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "dev": "next dev --turbo",
 | 
			
		||||
    "dev:slow": "next dev",
 | 
			
		||||
    "build": "next build",
 | 
			
		||||
    "start": "next start",
 | 
			
		||||
    "lint": "next lint",
 | 
			
		||||
    "lint:fix": "next lint --fix"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@convex-dev/auth": "^0.0.81",
 | 
			
		||||
    "@hookform/resolvers": "^5.2.1",
 | 
			
		||||
    "@radix-ui/react-avatar": "^1.1.10",
 | 
			
		||||
    "@radix-ui/react-dialog": "^1.1.15",
 | 
			
		||||
    "@radix-ui/react-dropdown-menu": "^2.1.16",
 | 
			
		||||
    "@radix-ui/react-label": "^2.1.7",
 | 
			
		||||
    "@radix-ui/react-progress": "^1.1.7",
 | 
			
		||||
    "@radix-ui/react-scroll-area": "^1.2.10",
 | 
			
		||||
    "@radix-ui/react-separator": "^1.1.7",
 | 
			
		||||
    "@radix-ui/react-slot": "^1.2.3",
 | 
			
		||||
    "@radix-ui/react-tabs": "^1.1.13",
 | 
			
		||||
    "@sentry/nextjs": "^10.11.0",
 | 
			
		||||
    "@t3-oss/env-nextjs": "^0.13.8",
 | 
			
		||||
    "class-variance-authority": "^0.7.1",
 | 
			
		||||
    "clsx": "^2.1.1",
 | 
			
		||||
    "convex": "^1.27.0",
 | 
			
		||||
    "eslint-plugin-prettier": "^5.5.4",
 | 
			
		||||
    "lucide-react": "^0.542.0",
 | 
			
		||||
    "next": "^15.5.3",
 | 
			
		||||
    "next-plausible": "^3.12.4",
 | 
			
		||||
    "next-themes": "^0.4.6",
 | 
			
		||||
    "radix-ui": "^1.4.3",
 | 
			
		||||
    "react": "^19.1.1",
 | 
			
		||||
    "react-dom": "^19.1.1",
 | 
			
		||||
    "react-hook-form": "^7.62.0",
 | 
			
		||||
    "react-image-crop": "^11.0.10",
 | 
			
		||||
    "require-in-the-middle": "^7.5.2",
 | 
			
		||||
    "sonner": "^2.0.7",
 | 
			
		||||
    "tailwind-merge": "^3.3.1",
 | 
			
		||||
    "typescript-eslint": "^8.43.0",
 | 
			
		||||
    "vaul": "^1.1.2",
 | 
			
		||||
    "zod": "^4.1.7"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@eslint/eslintrc": "^3.3.1",
 | 
			
		||||
    "@tailwindcss/postcss": "^4.1.13",
 | 
			
		||||
    "@types/node": "^20.19.13",
 | 
			
		||||
    "@types/react": "^19.1.12",
 | 
			
		||||
    "@types/react-dom": "^19.1.9",
 | 
			
		||||
    "dotenv": "^16.6.1",
 | 
			
		||||
    "eslint-config-next": "^15.5.3",
 | 
			
		||||
    "npm-run-all": "^4.1.5",
 | 
			
		||||
    "tailwindcss": "^4.1.13",
 | 
			
		||||
    "tw-animate-css": "^1.3.8"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB  | 
| 
		 Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB  | 
| 
		 Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB  | 
| 
		 Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB  | 
| 
		 Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB  | 
| 
		 Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB  | 
| 
		 Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB  | 
| 
		 Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB  | 
| 
		 Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB  | 
| 
		 Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB  | 
| 
		 Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB  | 
| 
		 Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB  | 
| 
		 Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB  |