Début du projet Traintrape-moi
							
								
								
									
										38
									
								
								client/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,38 @@
 | 
			
		||||
# 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
 | 
			
		||||
*.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
 | 
			
		||||
							
								
								
									
										50
									
								
								client/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,50 @@
 | 
			
		||||
# Welcome to your Expo app 👋
 | 
			
		||||
 | 
			
		||||
This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app).
 | 
			
		||||
 | 
			
		||||
## Get started
 | 
			
		||||
 | 
			
		||||
1. Install dependencies
 | 
			
		||||
 | 
			
		||||
   ```bash
 | 
			
		||||
   npm install
 | 
			
		||||
   ```
 | 
			
		||||
 | 
			
		||||
2. Start the app
 | 
			
		||||
 | 
			
		||||
   ```bash
 | 
			
		||||
    npx expo start
 | 
			
		||||
   ```
 | 
			
		||||
 | 
			
		||||
In the output, you'll find options to open the app in a
 | 
			
		||||
 | 
			
		||||
- [development build](https://docs.expo.dev/develop/development-builds/introduction/)
 | 
			
		||||
- [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/)
 | 
			
		||||
- [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/)
 | 
			
		||||
- [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo
 | 
			
		||||
 | 
			
		||||
You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction).
 | 
			
		||||
 | 
			
		||||
## Get a fresh project
 | 
			
		||||
 | 
			
		||||
When you're ready, run:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
npm run reset-project
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing.
 | 
			
		||||
 | 
			
		||||
## Learn more
 | 
			
		||||
 | 
			
		||||
To learn more about developing your project with Expo, look at the following resources:
 | 
			
		||||
 | 
			
		||||
- [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides).
 | 
			
		||||
- [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web.
 | 
			
		||||
 | 
			
		||||
## Join the community
 | 
			
		||||
 | 
			
		||||
Join our community of developers creating universal apps.
 | 
			
		||||
 | 
			
		||||
- [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute.
 | 
			
		||||
- [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions.
 | 
			
		||||
							
								
								
									
										41
									
								
								client/app.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,41 @@
 | 
			
		||||
{
 | 
			
		||||
  "expo": {
 | 
			
		||||
    "name": "traintrape-moi-client",
 | 
			
		||||
    "slug": "traintrape-moi-client",
 | 
			
		||||
    "version": "1.0.0",
 | 
			
		||||
    "orientation": "portrait",
 | 
			
		||||
    "icon": "./assets/images/icon.png",
 | 
			
		||||
    "scheme": "myapp",
 | 
			
		||||
    "userInterfaceStyle": "automatic",
 | 
			
		||||
    "newArchEnabled": true,
 | 
			
		||||
    "ios": {
 | 
			
		||||
      "supportsTablet": true
 | 
			
		||||
    },
 | 
			
		||||
    "android": {
 | 
			
		||||
      "adaptiveIcon": {
 | 
			
		||||
        "foregroundImage": "./assets/images/adaptive-icon.png",
 | 
			
		||||
        "backgroundColor": "#ffffff"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "web": {
 | 
			
		||||
      "bundler": "metro",
 | 
			
		||||
      "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"
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
    ],
 | 
			
		||||
    "experiments": {
 | 
			
		||||
      "typedRoutes": true
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										10
									
								
								client/app/_layout.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,10 @@
 | 
			
		||||
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native'
 | 
			
		||||
import { Stack } from "expo-router"
 | 
			
		||||
import { useColorScheme } from '@/hooks/useColorScheme'
 | 
			
		||||
 | 
			
		||||
export default function RootLayout() {
 | 
			
		||||
  const colorScheme = useColorScheme()
 | 
			
		||||
  return <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
 | 
			
		||||
    <Stack />
 | 
			
		||||
  </ThemeProvider>
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										16
									
								
								client/app/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,16 @@
 | 
			
		||||
import { View } from "react-native"
 | 
			
		||||
import { ThemedText } from "@/components/ThemedText"
 | 
			
		||||
 | 
			
		||||
export default function Index() {
 | 
			
		||||
  return (
 | 
			
		||||
    <View
 | 
			
		||||
      style={{
 | 
			
		||||
        flex: 1,
 | 
			
		||||
        justifyContent: "center",
 | 
			
		||||
        alignItems: "center",
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <ThemedText>Bienvue sur « Traintrape-moi » !</ThemedText>
 | 
			
		||||
    </View>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								client/assets/fonts/SpaceMono-Regular.ttf
									
									
									
									
									
										Executable file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								client/assets/images/adaptive-icon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 17 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								client/assets/images/favicon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.4 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								client/assets/images/icon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 22 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								client/assets/images/partial-react-logo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 5.0 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								client/assets/images/react-logo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 6.2 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								client/assets/images/react-logo@2x.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 14 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								client/assets/images/react-logo@3x.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 21 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								client/assets/images/splash-icon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 17 KiB  | 
							
								
								
									
										45
									
								
								client/components/Collapsible.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,45 @@
 | 
			
		||||
import { PropsWithChildren, useState } from 'react';
 | 
			
		||||
import { StyleSheet, TouchableOpacity } from 'react-native';
 | 
			
		||||
 | 
			
		||||
import { ThemedText } from '@/components/ThemedText';
 | 
			
		||||
import { ThemedView } from '@/components/ThemedView';
 | 
			
		||||
import { IconSymbol } from '@/components/ui/IconSymbol';
 | 
			
		||||
import { Colors } from '@/constants/Colors';
 | 
			
		||||
import { useColorScheme } from '@/hooks/useColorScheme';
 | 
			
		||||
 | 
			
		||||
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,
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										24
									
								
								client/components/ExternalLink.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,24 @@
 | 
			
		||||
import { Link } from 'expo-router';
 | 
			
		||||
import { openBrowserAsync } from 'expo-web-browser';
 | 
			
		||||
import { type ComponentProps } from 'react';
 | 
			
		||||
import { Platform } from 'react-native';
 | 
			
		||||
 | 
			
		||||
type Props = Omit<ComponentProps<typeof Link>, 'href'> & { href: string };
 | 
			
		||||
 | 
			
		||||
export function ExternalLink({ href, ...rest }: Props) {
 | 
			
		||||
  return (
 | 
			
		||||
    <Link
 | 
			
		||||
      target="_blank"
 | 
			
		||||
      {...rest}
 | 
			
		||||
      href={href}
 | 
			
		||||
      onPress={async (event) => {
 | 
			
		||||
        if (Platform.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);
 | 
			
		||||
        }
 | 
			
		||||
      }}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										18
									
								
								client/components/HapticTab.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);
 | 
			
		||||
      }}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										37
									
								
								client/components/HelloWave.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,37 @@
 | 
			
		||||
import { StyleSheet } from 'react-native';
 | 
			
		||||
import Animated, {
 | 
			
		||||
  useSharedValue,
 | 
			
		||||
  useAnimatedStyle,
 | 
			
		||||
  withTiming,
 | 
			
		||||
  withRepeat,
 | 
			
		||||
  withSequence,
 | 
			
		||||
} from 'react-native-reanimated';
 | 
			
		||||
 | 
			
		||||
import { ThemedText } from '@/components/ThemedText';
 | 
			
		||||
 | 
			
		||||
export function HelloWave() {
 | 
			
		||||
  const rotationAnimation = useSharedValue(0);
 | 
			
		||||
 | 
			
		||||
  rotationAnimation.value = withRepeat(
 | 
			
		||||
    withSequence(withTiming(25, { duration: 150 }), withTiming(0, { duration: 150 })),
 | 
			
		||||
    4 // Run the animation 4 times
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const animatedStyle = useAnimatedStyle(() => ({
 | 
			
		||||
    transform: [{ rotate: `${rotationAnimation.value}deg` }],
 | 
			
		||||
  }));
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Animated.View style={animatedStyle}>
 | 
			
		||||
      <ThemedText style={styles.text}>👋</ThemedText>
 | 
			
		||||
    </Animated.View>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const styles = StyleSheet.create({
 | 
			
		||||
  text: {
 | 
			
		||||
    fontSize: 28,
 | 
			
		||||
    lineHeight: 32,
 | 
			
		||||
    marginTop: -6,
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										82
									
								
								client/components/ParallaxScrollView.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,82 @@
 | 
			
		||||
import type { PropsWithChildren, ReactElement } from 'react';
 | 
			
		||||
import { StyleSheet } from 'react-native';
 | 
			
		||||
import Animated, {
 | 
			
		||||
  interpolate,
 | 
			
		||||
  useAnimatedRef,
 | 
			
		||||
  useAnimatedStyle,
 | 
			
		||||
  useScrollViewOffset,
 | 
			
		||||
} from 'react-native-reanimated';
 | 
			
		||||
 | 
			
		||||
import { ThemedView } from '@/components/ThemedView';
 | 
			
		||||
import { useBottomTabOverflow } from '@/components/ui/TabBarBackground';
 | 
			
		||||
import { useColorScheme } from '@/hooks/useColorScheme';
 | 
			
		||||
 | 
			
		||||
const HEADER_HEIGHT = 250;
 | 
			
		||||
 | 
			
		||||
type Props = PropsWithChildren<{
 | 
			
		||||
  headerImage: ReactElement;
 | 
			
		||||
  headerBackgroundColor: { dark: string; light: string };
 | 
			
		||||
}>;
 | 
			
		||||
 | 
			
		||||
export default function ParallaxScrollView({
 | 
			
		||||
  children,
 | 
			
		||||
  headerImage,
 | 
			
		||||
  headerBackgroundColor,
 | 
			
		||||
}: Props) {
 | 
			
		||||
  const colorScheme = useColorScheme() ?? 'light';
 | 
			
		||||
  const scrollRef = useAnimatedRef<Animated.ScrollView>();
 | 
			
		||||
  const scrollOffset = useScrollViewOffset(scrollRef);
 | 
			
		||||
  const bottom = useBottomTabOverflow();
 | 
			
		||||
  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 (
 | 
			
		||||
    <ThemedView style={styles.container}>
 | 
			
		||||
      <Animated.ScrollView
 | 
			
		||||
        ref={scrollRef}
 | 
			
		||||
        scrollEventThrottle={16}
 | 
			
		||||
        scrollIndicatorInsets={{ bottom }}
 | 
			
		||||
        contentContainerStyle={{ paddingBottom: bottom }}>
 | 
			
		||||
        <Animated.View
 | 
			
		||||
          style={[
 | 
			
		||||
            styles.header,
 | 
			
		||||
            { backgroundColor: headerBackgroundColor[colorScheme] },
 | 
			
		||||
            headerAnimatedStyle,
 | 
			
		||||
          ]}>
 | 
			
		||||
          {headerImage}
 | 
			
		||||
        </Animated.View>
 | 
			
		||||
        <ThemedView style={styles.content}>{children}</ThemedView>
 | 
			
		||||
      </Animated.ScrollView>
 | 
			
		||||
    </ThemedView>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const styles = StyleSheet.create({
 | 
			
		||||
  container: {
 | 
			
		||||
    flex: 1,
 | 
			
		||||
  },
 | 
			
		||||
  header: {
 | 
			
		||||
    height: HEADER_HEIGHT,
 | 
			
		||||
    overflow: 'hidden',
 | 
			
		||||
  },
 | 
			
		||||
  content: {
 | 
			
		||||
    flex: 1,
 | 
			
		||||
    padding: 32,
 | 
			
		||||
    gap: 16,
 | 
			
		||||
    overflow: 'hidden',
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										60
									
								
								client/components/ThemedText.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,60 @@
 | 
			
		||||
import { Text, type TextProps, StyleSheet } from 'react-native';
 | 
			
		||||
 | 
			
		||||
import { useThemeColor } from '@/hooks/useThemeColor';
 | 
			
		||||
 | 
			
		||||
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
									
								
								client/components/ThemedView.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,14 @@
 | 
			
		||||
import { View, type ViewProps } from 'react-native';
 | 
			
		||||
 | 
			
		||||
import { useThemeColor } from '@/hooks/useThemeColor';
 | 
			
		||||
 | 
			
		||||
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} />;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										10
									
								
								client/components/__tests__/ThemedText-test.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,10 @@
 | 
			
		||||
import * as React from 'react';
 | 
			
		||||
import renderer from 'react-test-renderer';
 | 
			
		||||
 | 
			
		||||
import { ThemedText } from '../ThemedText';
 | 
			
		||||
 | 
			
		||||
it(`renders correctly`, () => {
 | 
			
		||||
  const tree = renderer.create(<ThemedText>Snapshot test!</ThemedText>).toJSON();
 | 
			
		||||
 | 
			
		||||
  expect(tree).toMatchSnapshot();
 | 
			
		||||
});
 | 
			
		||||
@@ -0,0 +1,24 @@
 | 
			
		||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
 | 
			
		||||
 | 
			
		||||
exports[`renders correctly 1`] = `
 | 
			
		||||
<Text
 | 
			
		||||
  style={
 | 
			
		||||
    [
 | 
			
		||||
      {
 | 
			
		||||
        "color": "#11181C",
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "fontSize": 16,
 | 
			
		||||
        "lineHeight": 24,
 | 
			
		||||
      },
 | 
			
		||||
      undefined,
 | 
			
		||||
      undefined,
 | 
			
		||||
      undefined,
 | 
			
		||||
      undefined,
 | 
			
		||||
      undefined,
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
>
 | 
			
		||||
  Snapshot test!
 | 
			
		||||
</Text>
 | 
			
		||||
`;
 | 
			
		||||
							
								
								
									
										32
									
								
								client/components/ui/IconSymbol.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,
 | 
			
		||||
      ]}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										43
									
								
								client/components/ui/IconSymbol.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,43 @@
 | 
			
		||||
// This file is a fallback for using MaterialIcons on Android and web.
 | 
			
		||||
 | 
			
		||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
 | 
			
		||||
import { SymbolWeight } from 'expo-symbols';
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { OpaqueColorValue, StyleProp, ViewStyle } from 'react-native';
 | 
			
		||||
 | 
			
		||||
// Add your SFSymbol to MaterialIcons mappings here.
 | 
			
		||||
const MAPPING = {
 | 
			
		||||
  // See MaterialIcons here: https://icons.expo.fyi
 | 
			
		||||
  // See SF Symbols in the SF Symbols app on Mac.
 | 
			
		||||
  'house.fill': 'home',
 | 
			
		||||
  'paperplane.fill': 'send',
 | 
			
		||||
  'chevron.left.forwardslash.chevron.right': 'code',
 | 
			
		||||
  'chevron.right': 'chevron-right',
 | 
			
		||||
} as Partial<
 | 
			
		||||
  Record<
 | 
			
		||||
    import('expo-symbols').SymbolViewProps['name'],
 | 
			
		||||
    React.ComponentProps<typeof MaterialIcons>['name']
 | 
			
		||||
  >
 | 
			
		||||
>;
 | 
			
		||||
 | 
			
		||||
export type IconSymbolName = keyof typeof MAPPING;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * An icon component that uses native SFSymbols on iOS, and MaterialIcons on Android and web. This ensures a consistent look across platforms, and optimal resource usage.
 | 
			
		||||
 *
 | 
			
		||||
 * Icon `name`s are based on SFSymbols and require manual mapping to MaterialIcons.
 | 
			
		||||
 */
 | 
			
		||||
export function IconSymbol({
 | 
			
		||||
  name,
 | 
			
		||||
  size = 24,
 | 
			
		||||
  color,
 | 
			
		||||
  style,
 | 
			
		||||
}: {
 | 
			
		||||
  name: IconSymbolName;
 | 
			
		||||
  size?: number;
 | 
			
		||||
  color: string | OpaqueColorValue;
 | 
			
		||||
  style?: StyleProp<ViewStyle>;
 | 
			
		||||
  weight?: SymbolWeight;
 | 
			
		||||
}) {
 | 
			
		||||
  return <MaterialIcons color={color} size={size} name={MAPPING[name]} style={style} />;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										22
									
								
								client/components/ui/TabBarBackground.ios.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,22 @@
 | 
			
		||||
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
 | 
			
		||||
import { BlurView } from 'expo-blur';
 | 
			
		||||
import { StyleSheet } from 'react-native';
 | 
			
		||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
 | 
			
		||||
 | 
			
		||||
export default function BlurTabBarBackground() {
 | 
			
		||||
  return (
 | 
			
		||||
    <BlurView
 | 
			
		||||
      // System chrome material automatically adapts to the system's theme
 | 
			
		||||
      // and matches the native tab bar appearance on iOS.
 | 
			
		||||
      tint="systemChromeMaterial"
 | 
			
		||||
      intensity={100}
 | 
			
		||||
      style={StyleSheet.absoluteFill}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function useBottomTabOverflow() {
 | 
			
		||||
  const tabHeight = useBottomTabBarHeight();
 | 
			
		||||
  const { bottom } = useSafeAreaInsets();
 | 
			
		||||
  return tabHeight - bottom;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										6
									
								
								client/components/ui/TabBarBackground.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,6 @@
 | 
			
		||||
// This is a shim for web and Android where the tab bar is generally opaque.
 | 
			
		||||
export default undefined;
 | 
			
		||||
 | 
			
		||||
export function useBottomTabOverflow() {
 | 
			
		||||
  return 0;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										26
									
								
								client/constants/Colors.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,26 @@
 | 
			
		||||
/**
 | 
			
		||||
 * 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.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
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,
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										1
									
								
								client/hooks/useColorScheme.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1 @@
 | 
			
		||||
export { useColorScheme } from 'react-native';
 | 
			
		||||
							
								
								
									
										21
									
								
								client/hooks/useColorScheme.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
									
								
								client/hooks/useThemeColor.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/Colors';
 | 
			
		||||
import { useColorScheme } from '@/hooks/useColorScheme';
 | 
			
		||||
 | 
			
		||||
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];
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										14987
									
								
								client/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										53
									
								
								client/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,53 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "traintrape-moi-client",
 | 
			
		||||
  "main": "expo-router/entry",
 | 
			
		||||
  "version": "1.0.0",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "start": "expo start",
 | 
			
		||||
    "android": "expo start --android",
 | 
			
		||||
    "ios": "expo start --ios",
 | 
			
		||||
    "web": "expo start --web",
 | 
			
		||||
    "test": "jest --watchAll",
 | 
			
		||||
    "lint": "expo lint"
 | 
			
		||||
  },
 | 
			
		||||
  "jest": {
 | 
			
		||||
    "preset": "jest-expo"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@expo/vector-icons": "^14.0.2",
 | 
			
		||||
    "@react-navigation/bottom-tabs": "^7.0.0",
 | 
			
		||||
    "@react-navigation/native": "^7.0.0",
 | 
			
		||||
    "expo": "~52.0.11",
 | 
			
		||||
    "expo-blur": "~14.0.1",
 | 
			
		||||
    "expo-constants": "~17.0.3",
 | 
			
		||||
    "expo-font": "~13.0.1",
 | 
			
		||||
    "expo-haptics": "~14.0.0",
 | 
			
		||||
    "expo-linking": "~7.0.3",
 | 
			
		||||
    "expo-router": "~4.0.9",
 | 
			
		||||
    "expo-splash-screen": "~0.29.13",
 | 
			
		||||
    "expo-status-bar": "~2.0.0",
 | 
			
		||||
    "expo-symbols": "~0.2.0",
 | 
			
		||||
    "expo-system-ui": "~4.0.4",
 | 
			
		||||
    "expo-web-browser": "~14.0.1",
 | 
			
		||||
    "react": "18.3.1",
 | 
			
		||||
    "react-dom": "18.3.1",
 | 
			
		||||
    "react-native": "0.76.3",
 | 
			
		||||
    "react-native-gesture-handler": "~2.20.2",
 | 
			
		||||
    "react-native-reanimated": "~3.16.1",
 | 
			
		||||
    "react-native-safe-area-context": "4.12.0",
 | 
			
		||||
    "react-native-screens": "~4.1.0",
 | 
			
		||||
    "react-native-web": "~0.19.13",
 | 
			
		||||
    "react-native-webview": "13.12.2"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@babel/core": "^7.25.2",
 | 
			
		||||
    "@types/jest": "^29.5.12",
 | 
			
		||||
    "@types/react": "~18.3.12",
 | 
			
		||||
    "@types/react-test-renderer": "^18.3.0",
 | 
			
		||||
    "jest": "^29.2.1",
 | 
			
		||||
    "jest-expo": "~52.0.2",
 | 
			
		||||
    "react-test-renderer": "18.3.1",
 | 
			
		||||
    "typescript": "^5.3.3"
 | 
			
		||||
  },
 | 
			
		||||
  "private": true
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										17
									
								
								client/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,17 @@
 | 
			
		||||
{
 | 
			
		||||
  "extends": "expo/tsconfig.base",
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    "strict": true,
 | 
			
		||||
    "paths": {
 | 
			
		||||
      "@/*": [
 | 
			
		||||
        "./*"
 | 
			
		||||
      ]
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "include": [
 | 
			
		||||
    "**/*.ts",
 | 
			
		||||
    "**/*.tsx",
 | 
			
		||||
    ".expo/types/**/*.ts",
 | 
			
		||||
    "expo-env.d.ts"
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||