diff --git a/attachments/02-basics/00-starting-project/App.js b/attachments/02-basics/00-starting-project/App.js
new file mode 100644
index 00000000..eeadebd5
--- /dev/null
+++ b/attachments/02-basics/00-starting-project/App.js
@@ -0,0 +1,80 @@
+import { useState } from 'react';
+import { Button, FlatList, StyleSheet, View } from 'react-native';
+import GoalItem from './components/GoalItem';
+import GoalInput from './components/GoalInput';
+import { StatusBar } from 'expo-status-bar';
+
+export default function App() {
+ const [courseGoals, setCourseGoals] = useState([]);
+ const [modalIsVisible, setModalIsVisible] = useState(false);
+
+ function startAddGoalHandler() {
+ setModalIsVisible(true);
+ }
+
+ function endAddGoalHandler() {
+ setModalIsVisible(false);
+ }
+
+ function addGoalHandler(enteredGoalText) {
+ console.log(enteredGoalText);
+ setCourseGoals((prev) => [
+ ...prev,
+ { text: enteredGoalText, id: Math.random().toString() }
+ ]);
+ endAddGoalHandler();
+ }
+
+ function deleteGoalHandler(id) {
+ setCourseGoals((prev) => {
+ return prev.filter((goal) => goal.id !== id);
+ });
+ }
+
+ return (
+ <>
+
+
+
+
+
+ (
+
+ )}
+ keyExtractor={(item, index) => {
+ return item.id;
+ }}
+ alwaysBounceVertical={false}
+ >
+
+
+ >
+ );
+}
+
+// Besides auto-completion, the StyleSheet object aslo provides validation.
+// If you would use invalid style properties or values, you would get an error/warning.
+const styles = StyleSheet.create({
+ appContainer: {
+ flex: 1,
+ paddingTop: 50,
+ paddingHorizontal: 16
+ },
+ goalsContainer: {
+ flex: 5
+ }
+});
diff --git a/attachments/02-basics/00-starting-project/app.json b/attachments/02-basics/00-starting-project/app.json
new file mode 100644
index 00000000..420cf590
--- /dev/null
+++ b/attachments/02-basics/00-starting-project/app.json
@@ -0,0 +1,31 @@
+{
+ "expo": {
+ "name": "RNCourse",
+ "slug": "RNCourse",
+ "version": "1.0.0",
+ "orientation": "portrait",
+ "icon": "./assets/icon.png",
+ "backgroundColor": "#1e085a",
+ "splash": {
+ "image": "./assets/splash.png",
+ "resizeMode": "contain",
+ "backgroundColor": "#ffffff"
+ },
+ "updates": {
+ "fallbackToCacheTimeout": 0
+ },
+ "assetBundlePatterns": ["**/*"],
+ "ios": {
+ "supportsTablet": true
+ },
+ "android": {
+ "adaptiveIcon": {
+ "foregroundImage": "./assets/adaptive-icon.png",
+ "backgroundColor": "#FFFFFF"
+ }
+ },
+ "web": {
+ "favicon": "./assets/favicon.png"
+ }
+ }
+}
diff --git a/attachments/02-basics/00-starting-project/assets/adaptive-icon.png b/attachments/02-basics/00-starting-project/assets/adaptive-icon.png
new file mode 100644
index 00000000..03d6f6b6
Binary files /dev/null and b/attachments/02-basics/00-starting-project/assets/adaptive-icon.png differ
diff --git a/attachments/02-basics/00-starting-project/assets/favicon.png b/attachments/02-basics/00-starting-project/assets/favicon.png
new file mode 100644
index 00000000..e75f697b
Binary files /dev/null and b/attachments/02-basics/00-starting-project/assets/favicon.png differ
diff --git a/attachments/02-basics/00-starting-project/assets/icon.png b/attachments/02-basics/00-starting-project/assets/icon.png
new file mode 100644
index 00000000..a0b1526f
Binary files /dev/null and b/attachments/02-basics/00-starting-project/assets/icon.png differ
diff --git a/attachments/02-basics/00-starting-project/assets/images/goal.png b/attachments/02-basics/00-starting-project/assets/images/goal.png
new file mode 100644
index 00000000..04c24ca0
Binary files /dev/null and b/attachments/02-basics/00-starting-project/assets/images/goal.png differ
diff --git a/attachments/02-basics/00-starting-project/assets/splash.png b/attachments/02-basics/00-starting-project/assets/splash.png
new file mode 100644
index 00000000..0e89705a
Binary files /dev/null and b/attachments/02-basics/00-starting-project/assets/splash.png differ
diff --git a/attachments/02-basics/00-starting-project/babel.config.js b/attachments/02-basics/00-starting-project/babel.config.js
new file mode 100644
index 00000000..2900afe9
--- /dev/null
+++ b/attachments/02-basics/00-starting-project/babel.config.js
@@ -0,0 +1,6 @@
+module.exports = function(api) {
+ api.cache(true);
+ return {
+ presets: ['babel-preset-expo'],
+ };
+};
diff --git a/attachments/02-basics/00-starting-project/components/GoalInput.js b/attachments/02-basics/00-starting-project/components/GoalInput.js
new file mode 100644
index 00000000..dc929aa0
--- /dev/null
+++ b/attachments/02-basics/00-starting-project/components/GoalInput.js
@@ -0,0 +1,79 @@
+import { useState } from 'react';
+import {
+ View,
+ TextInput,
+ Button,
+ StyleSheet,
+ Modal,
+ Image
+} from 'react-native';
+
+export default function GoalInput(props) {
+ const [enteredGoalText, setEnteredGoalText] = useState('');
+
+ function goalInputHandler(enteredText) {
+ setEnteredGoalText(enteredText);
+ }
+
+ function addGoalHandler() {
+ props.onAddGoal(enteredGoalText);
+ setEnteredGoalText('');
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ inputContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 16,
+ backgroundColor: '#311b6b'
+ },
+ image: {
+ width: 100,
+ height: 100,
+ margin: 20
+ },
+ textInput: {
+ borderWidth: 1,
+ borderColor: '#e4d0ff',
+ backgroundColor: '#e4d0ff',
+ color: '#120438',
+ borderRadius: 6,
+ width: '100%',
+ padding: 16
+ },
+ buttonContainer: {
+ marginTop: 8,
+ flexDirection: 'row'
+ },
+ button: {
+ width: 100,
+ marginHorizontal: 8
+ }
+});
diff --git a/attachments/02-basics/00-starting-project/components/GoalItem.js b/attachments/02-basics/00-starting-project/components/GoalItem.js
new file mode 100644
index 00000000..fce4ae0b
--- /dev/null
+++ b/attachments/02-basics/00-starting-project/components/GoalItem.js
@@ -0,0 +1,29 @@
+import { Text, StyleSheet, Pressable } from 'react-native';
+
+function GoalItem(props) {
+ return (
+ // bind is a standard JS function which basically allows you pre-configure a function for future execution
+ pressed && styles.pressedItem}
+ >
+ {props.text};
+
+ );
+}
+
+export default GoalItem;
+
+const styles = StyleSheet.create({
+ goalItem: {
+ margin: 8,
+ padding: 8,
+ borderRadius: 6,
+ backgroundColor: '#5e0acc',
+ color: 'white'
+ },
+ pressedItem: {
+ opacity: 0.5
+ }
+});
diff --git a/attachments/02-basics/00-starting-project/package.json b/attachments/02-basics/00-starting-project/package.json
new file mode 100644
index 00000000..5e41c518
--- /dev/null
+++ b/attachments/02-basics/00-starting-project/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "rncourse",
+ "version": "1.0.0",
+ "main": "node_modules/expo/AppEntry.js",
+ "scripts": {
+ "start": "expo start",
+ "android": "expo start --android",
+ "ios": "expo start --ios",
+ "web": "expo start --web",
+ "eject": "expo eject"
+ },
+ "dependencies": {
+ "expo": "^52.0.0",
+ "expo-status-bar": "~2.0.1",
+ "react": "18.3.1",
+ "react-dom": "18.3.1",
+ "react-native": "0.76.7",
+ "react-native-web": "~0.19.13"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.25.2"
+ },
+ "private": true
+}
diff --git a/attachments/03-debugging/00-starting-project/App.js b/attachments/03-debugging/00-starting-project/App.js
new file mode 100644
index 00000000..76513864
--- /dev/null
+++ b/attachments/03-debugging/00-starting-project/App.js
@@ -0,0 +1,80 @@
+import { useState } from 'react';
+import { StyleSheet, View, FlatList, Button } from 'react-native';
+import { StatusBar } from 'expo-status-bar';
+
+import GoalItem from './components/GoalItem';
+import GoalInput from './components/GoalInput';
+
+export default function App() {
+ const [modalIsVisible, setModalIsVisible] = useState(false);
+ const [courseGoals, setCourseGoals] = useState([]);
+
+ function startAddGoalHandler() {
+ setModalIsVisible(true);
+ }
+
+ function endAddGoalHandler() {
+ setModalIsVisible(false);
+ }
+
+ function addGoalHandler(enteredGoalText) {
+ setCourseGoals((currentCourseGoals) => [
+ ...currentCourseGoals,
+ { text: enteredGoalText, id: Math.random().toString() },
+ ]);
+ endAddGoalHandler();
+ }
+
+ function deleteGoalHandler(id) {
+ setCourseGoals((currentCourseGoals) => {
+ return currentCourseGoals.filter((goal) => goal.id !== id);
+ });
+ }
+
+ return (
+ <>
+
+
+
+
+
+ {
+ return (
+
+ );
+ }}
+ keyExtractor={(item, index) => {
+ return item.id;
+ }}
+ alwaysBounceVertical={false}
+ />
+
+
+ >
+ );
+}
+
+const styles = StyleSheet.create({
+ appContainer: {
+ flex: 1,
+ paddingTop: 50,
+ paddingHorizontal: 16,
+ },
+ goalsContainer: {
+ flex: 5,
+ },
+});
diff --git a/attachments/03-debugging/00-starting-project/app.json b/attachments/03-debugging/00-starting-project/app.json
new file mode 100644
index 00000000..13de605b
--- /dev/null
+++ b/attachments/03-debugging/00-starting-project/app.json
@@ -0,0 +1,33 @@
+{
+ "expo": {
+ "name": "RNCourse",
+ "slug": "RNCourse",
+ "version": "1.0.0",
+ "orientation": "portrait",
+ "icon": "./assets/icon.png",
+ "backgroundColor": "#1e085a",
+ "splash": {
+ "image": "./assets/splash.png",
+ "resizeMode": "contain",
+ "backgroundColor": "#ffffff"
+ },
+ "updates": {
+ "fallbackToCacheTimeout": 0
+ },
+ "assetBundlePatterns": [
+ "**/*"
+ ],
+ "ios": {
+ "supportsTablet": true
+ },
+ "android": {
+ "adaptiveIcon": {
+ "foregroundImage": "./assets/adaptive-icon.png",
+ "backgroundColor": "#FFFFFF"
+ }
+ },
+ "web": {
+ "favicon": "./assets/favicon.png"
+ }
+ }
+}
diff --git a/attachments/03-debugging/00-starting-project/assets/adaptive-icon.png b/attachments/03-debugging/00-starting-project/assets/adaptive-icon.png
new file mode 100644
index 00000000..03d6f6b6
Binary files /dev/null and b/attachments/03-debugging/00-starting-project/assets/adaptive-icon.png differ
diff --git a/attachments/03-debugging/00-starting-project/assets/favicon.png b/attachments/03-debugging/00-starting-project/assets/favicon.png
new file mode 100644
index 00000000..e75f697b
Binary files /dev/null and b/attachments/03-debugging/00-starting-project/assets/favicon.png differ
diff --git a/attachments/03-debugging/00-starting-project/assets/icon.png b/attachments/03-debugging/00-starting-project/assets/icon.png
new file mode 100644
index 00000000..a0b1526f
Binary files /dev/null and b/attachments/03-debugging/00-starting-project/assets/icon.png differ
diff --git a/attachments/03-debugging/00-starting-project/assets/images/goal.png b/attachments/03-debugging/00-starting-project/assets/images/goal.png
new file mode 100644
index 00000000..04c24ca0
Binary files /dev/null and b/attachments/03-debugging/00-starting-project/assets/images/goal.png differ
diff --git a/attachments/03-debugging/00-starting-project/assets/splash.png b/attachments/03-debugging/00-starting-project/assets/splash.png
new file mode 100644
index 00000000..0e89705a
Binary files /dev/null and b/attachments/03-debugging/00-starting-project/assets/splash.png differ
diff --git a/attachments/03-debugging/00-starting-project/babel.config.js b/attachments/03-debugging/00-starting-project/babel.config.js
new file mode 100644
index 00000000..e1babf6b
--- /dev/null
+++ b/attachments/03-debugging/00-starting-project/babel.config.js
@@ -0,0 +1,6 @@
+module.exports = function(api) {
+ api.cache(true);
+ return {
+ presets: ['babel-preset-expo'],
+ };
+};
diff --git a/attachments/03-debugging/00-starting-project/components/GoalInput.js b/attachments/03-debugging/00-starting-project/components/GoalInput.js
new file mode 100644
index 00000000..3d456174
--- /dev/null
+++ b/attachments/03-debugging/00-starting-project/components/GoalInput.js
@@ -0,0 +1,81 @@
+import { useState } from 'react';
+import {
+ View,
+ TextInput,
+ Button,
+ StyleSheet,
+ Modal,
+ Image,
+} from 'react-native';
+
+function GoalInput(props) {
+ const [enteredGoalText, setEnteredGoalText] = useState('');
+
+ function goalInputHandler(enteredText) {
+ setEnteredGoalText(enteredText);
+ }
+
+ function addGoalHandler() {
+ props.onAddGoal(enteredGoalText);
+ setEnteredGoalText('');
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default GoalInput;
+
+const styles = StyleSheet.create({
+ inputContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 16,
+ backgroundColor: '#311b6b',
+ },
+ image: {
+ width: 100,
+ height: 100,
+ margin: 20,
+ },
+ textInput: {
+ borderWidth: 1,
+ borderColor: '#e4d0ff',
+ backgroundColor: '#e4d0ff',
+ color: '#120438',
+ borderRadius: 6,
+ width: '100%',
+ padding: 16,
+ },
+ buttonContainer: {
+ marginTop: 16,
+ flexDirection: 'row',
+ },
+ button: {
+ width: 100,
+ marginHorizontal: 8,
+ },
+});
diff --git a/attachments/03-debugging/00-starting-project/components/GoalItem.js b/attachments/03-debugging/00-starting-project/components/GoalItem.js
new file mode 100644
index 00000000..f02ccfa5
--- /dev/null
+++ b/attachments/03-debugging/00-starting-project/components/GoalItem.js
@@ -0,0 +1,32 @@
+import { StyleSheet, View, Text, Pressable } from 'react-native';
+
+function GoalItem(props) {
+ return (
+
+ pressed && styles.pressedItem}
+ >
+ {props.text}
+
+
+ );
+}
+
+export default GoalItem;
+
+const styles = StyleSheet.create({
+ goalItem: {
+ margin: 8,
+ borderRadius: 6,
+ backgroundColor: '#5e0acc',
+ },
+ pressedItem: {
+ opacity: 0.5,
+ },
+ goalText: {
+ color: 'white',
+ padding: 8,
+ },
+});
diff --git a/attachments/03-debugging/00-starting-project/package.json b/attachments/03-debugging/00-starting-project/package.json
new file mode 100644
index 00000000..5e41c518
--- /dev/null
+++ b/attachments/03-debugging/00-starting-project/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "rncourse",
+ "version": "1.0.0",
+ "main": "node_modules/expo/AppEntry.js",
+ "scripts": {
+ "start": "expo start",
+ "android": "expo start --android",
+ "ios": "expo start --ios",
+ "web": "expo start --web",
+ "eject": "expo eject"
+ },
+ "dependencies": {
+ "expo": "^52.0.0",
+ "expo-status-bar": "~2.0.1",
+ "react": "18.3.1",
+ "react-dom": "18.3.1",
+ "react-native": "0.76.7",
+ "react-native-web": "~0.19.13"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.25.2"
+ },
+ "private": true
+}
diff --git a/attachments/04-deep-dive-real-app/00-starting-project/App.js b/attachments/04-deep-dive-real-app/00-starting-project/App.js
new file mode 100644
index 00000000..fa73d77b
--- /dev/null
+++ b/attachments/04-deep-dive-real-app/00-starting-project/App.js
@@ -0,0 +1,84 @@
+import { StyleSheet, ImageBackground, SafeAreaView } from 'react-native';
+import StartGameScreen from './screens/StartGameScreen';
+import { LinearGradient } from 'expo-linear-gradient';
+import { useState } from 'react';
+import GameScreen from './screens/GameScreen';
+import Colors from './constants/colors';
+import GameOverScreen from './screens/GameOverScreen';
+
+import { useFonts } from 'expo-font';
+import AppLoading from 'expo-app-loading';
+
+export default function App() {
+ const [userNumber, setUserNumber] = useState();
+ const [gameIsOver, setGameIsOver] = useState(true);
+ const [guessRounds, setGuessRounds] = useState(0);
+
+ const [fontsLoaded] = useFonts({
+ 'open-sans': require('./assets/fonts/OpenSans-Regular.ttf'),
+ 'open-sans-bold': require('./assets/fonts/OpenSans-Bold.ttf')
+ });
+
+ if (!fontsLoaded) {
+ return ;
+ }
+
+ function pickedNumberHandler(pickedNumber) {
+ setUserNumber(pickedNumber);
+ setGameIsOver(false);
+ }
+
+ function gameOverHandler(numberOfRounds) {
+ setGameIsOver(true);
+ setGuessRounds(numberOfRounds);
+ }
+
+ function startNewGameHandler() {
+ setUserNumber(null);
+ // setGameIsOver(true);
+ setGuessRounds(0);
+ }
+
+ let screen = ;
+
+ if (userNumber) {
+ screen = (
+
+ );
+ }
+
+ if (gameIsOver && userNumber) {
+ screen = (
+
+ );
+ }
+
+ return (
+
+
+ {screen}
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ rootScreen: {
+ flex: 1
+ },
+ backgroundImage: {
+ opacity: 0.15
+ }
+});
diff --git a/attachments/04-deep-dive-real-app/00-starting-project/app.json b/attachments/04-deep-dive-real-app/00-starting-project/app.json
new file mode 100644
index 00000000..c81cea9a
--- /dev/null
+++ b/attachments/04-deep-dive-real-app/00-starting-project/app.json
@@ -0,0 +1,35 @@
+{
+ "expo": {
+ "name": "RNCourse",
+ "slug": "RNCourse",
+ "version": "1.0.0",
+ "orientation": "portrait",
+ "icon": "./assets/icon.png",
+ "splash": {
+ "image": "./assets/splash.png",
+ "resizeMode": "contain",
+ "backgroundColor": "#ffffff"
+ },
+ "updates": {
+ "fallbackToCacheTimeout": 0
+ },
+ "assetBundlePatterns": [
+ "**/*"
+ ],
+ "ios": {
+ "supportsTablet": true
+ },
+ "android": {
+ "adaptiveIcon": {
+ "foregroundImage": "./assets/adaptive-icon.png",
+ "backgroundColor": "#FFFFFF"
+ }
+ },
+ "web": {
+ "favicon": "./assets/favicon.png"
+ },
+ "plugins": [
+ "expo-font"
+ ]
+ }
+}
diff --git a/attachments/04-deep-dive-real-app/00-starting-project/assets/adaptive-icon.png b/attachments/04-deep-dive-real-app/00-starting-project/assets/adaptive-icon.png
new file mode 100644
index 00000000..03d6f6b6
Binary files /dev/null and b/attachments/04-deep-dive-real-app/00-starting-project/assets/adaptive-icon.png differ
diff --git a/attachments/04-deep-dive-real-app/00-starting-project/assets/favicon.png b/attachments/04-deep-dive-real-app/00-starting-project/assets/favicon.png
new file mode 100644
index 00000000..e75f697b
Binary files /dev/null and b/attachments/04-deep-dive-real-app/00-starting-project/assets/favicon.png differ
diff --git a/attachments/04-deep-dive-real-app/00-starting-project/assets/fonts/OpenSans-Bold.ttf b/attachments/04-deep-dive-real-app/00-starting-project/assets/fonts/OpenSans-Bold.ttf
new file mode 100644
index 00000000..96fabd86
Binary files /dev/null and b/attachments/04-deep-dive-real-app/00-starting-project/assets/fonts/OpenSans-Bold.ttf differ
diff --git a/attachments/04-deep-dive-real-app/00-starting-project/assets/fonts/OpenSans-Regular.ttf b/attachments/04-deep-dive-real-app/00-starting-project/assets/fonts/OpenSans-Regular.ttf
new file mode 100644
index 00000000..2d4da3a6
Binary files /dev/null and b/attachments/04-deep-dive-real-app/00-starting-project/assets/fonts/OpenSans-Regular.ttf differ
diff --git a/attachments/04-deep-dive-real-app/00-starting-project/assets/icon.png b/attachments/04-deep-dive-real-app/00-starting-project/assets/icon.png
new file mode 100644
index 00000000..a0b1526f
Binary files /dev/null and b/attachments/04-deep-dive-real-app/00-starting-project/assets/icon.png differ
diff --git a/attachments/04-deep-dive-real-app/00-starting-project/assets/images/background.png b/attachments/04-deep-dive-real-app/00-starting-project/assets/images/background.png
new file mode 100644
index 00000000..300c5477
Binary files /dev/null and b/attachments/04-deep-dive-real-app/00-starting-project/assets/images/background.png differ
diff --git a/attachments/04-deep-dive-real-app/00-starting-project/assets/images/success.png b/attachments/04-deep-dive-real-app/00-starting-project/assets/images/success.png
new file mode 100644
index 00000000..aae773a0
Binary files /dev/null and b/attachments/04-deep-dive-real-app/00-starting-project/assets/images/success.png differ
diff --git a/attachments/04-deep-dive-real-app/00-starting-project/assets/splash.png b/attachments/04-deep-dive-real-app/00-starting-project/assets/splash.png
new file mode 100644
index 00000000..0e89705a
Binary files /dev/null and b/attachments/04-deep-dive-real-app/00-starting-project/assets/splash.png differ
diff --git a/attachments/04-deep-dive-real-app/00-starting-project/babel.config.js b/attachments/04-deep-dive-real-app/00-starting-project/babel.config.js
new file mode 100644
index 00000000..2900afe9
--- /dev/null
+++ b/attachments/04-deep-dive-real-app/00-starting-project/babel.config.js
@@ -0,0 +1,6 @@
+module.exports = function(api) {
+ api.cache(true);
+ return {
+ presets: ['babel-preset-expo'],
+ };
+};
diff --git a/attachments/04-deep-dive-real-app/00-starting-project/components/game/GuessLogItem.js b/attachments/04-deep-dive-real-app/00-starting-project/components/game/GuessLogItem.js
new file mode 100644
index 00000000..fac59cf4
--- /dev/null
+++ b/attachments/04-deep-dive-real-app/00-starting-project/components/game/GuessLogItem.js
@@ -0,0 +1,35 @@
+import { StyleSheet, View, Text } from 'react-native';
+import Colors from '../../constants/colors';
+
+function GuessLogItem({ roundNumber, guess }) {
+ return (
+
+ #{roundNumber}
+ Opponent's Guess: {guess}
+
+ );
+}
+
+export default GuessLogItem;
+
+const styles = StyleSheet.create({
+ listItem: {
+ borderColor: Colors.primary800,
+ borderWidth: 1,
+ borderRadius: 40,
+ padding: 12,
+ marginVertical: 8,
+ backgroundColor: Colors.accent500,
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ width: '100%',
+ elevation: 4,
+ shadowColor: 'black',
+ shadowOffset: { width: 0, height: 0 },
+ shadowOpacity: 0.25,
+ shadowRadius: 3
+ },
+ itemText: {
+ fontFamily: 'open-sans'
+ }
+});
diff --git a/attachments/04-deep-dive-real-app/00-starting-project/components/game/NumberContainer.js b/attachments/04-deep-dive-real-app/00-starting-project/components/game/NumberContainer.js
new file mode 100644
index 00000000..3e8cc333
--- /dev/null
+++ b/attachments/04-deep-dive-real-app/00-starting-project/components/game/NumberContainer.js
@@ -0,0 +1,30 @@
+import { StyleSheet, Text, View } from 'react-native';
+import Colors from '../../constants/colors';
+
+function NumberContainer({ children }) {
+ return (
+
+ {children}
+
+ );
+}
+
+export default NumberContainer;
+
+const styles = StyleSheet.create({
+ container: {
+ borderWidth: 4,
+ borderColor: Colors.accent500,
+ padding: 24,
+ margin: 8,
+ borderRadius: 8,
+ alignItems: 'center',
+ justifyContent: 'center'
+ },
+ numberText: {
+ fontFamily: 'open-sans-bold',
+ color: Colors.accent500,
+ fontSize: 36,
+ fontWeight: 'bold'
+ }
+});
diff --git a/attachments/04-deep-dive-real-app/00-starting-project/components/ui/Card.js b/attachments/04-deep-dive-real-app/00-starting-project/components/ui/Card.js
new file mode 100644
index 00000000..6c3db984
--- /dev/null
+++ b/attachments/04-deep-dive-real-app/00-starting-project/components/ui/Card.js
@@ -0,0 +1,24 @@
+import { StyleSheet, View } from 'react-native';
+import Colors from '../../constants/colors';
+
+function Card({ children }) {
+ return {children};
+}
+
+export default Card;
+
+const styles = StyleSheet.create({
+ card: {
+ alignItems: 'center',
+ marginTop: 36,
+ marginHorizontal: 24,
+ padding: 16,
+ backgroundColor: Colors.primary800,
+ borderRadius: 8,
+ elevation: 4, // create shadow for Android device, the higher the number the more shadow!
+ shadowColor: 'black', // shadow properties are for creating a shadow on iOS!
+ shadowOffset: { width: 0, height: 2 },
+ shadowRadius: 6,
+ shadowOpacity: 0.25
+ }
+});
diff --git a/attachments/04-deep-dive-real-app/00-starting-project/components/ui/InstructionsText.js b/attachments/04-deep-dive-real-app/00-starting-project/components/ui/InstructionsText.js
new file mode 100644
index 00000000..eb987b63
--- /dev/null
+++ b/attachments/04-deep-dive-real-app/00-starting-project/components/ui/InstructionsText.js
@@ -0,0 +1,16 @@
+import { StyleSheet, Text } from 'react-native';
+import Colors from '../../constants/colors';
+
+function InstructionsText({ children, style }) {
+ return {children};
+}
+
+export default InstructionsText;
+
+const styles = StyleSheet.create({
+ instructionText: {
+ fontFamily: "open-sans",
+ color: Colors.accent500,
+ fontSize: 24
+ }
+});
diff --git a/attachments/04-deep-dive-real-app/00-starting-project/components/ui/PrimaryButton.js b/attachments/04-deep-dive-real-app/00-starting-project/components/ui/PrimaryButton.js
new file mode 100644
index 00000000..7ea05df5
--- /dev/null
+++ b/attachments/04-deep-dive-real-app/00-starting-project/components/ui/PrimaryButton.js
@@ -0,0 +1,43 @@
+import { Pressable, StyleSheet, Text, View } from 'react-native';
+import Colors from '../../constants/colors';
+
+function PrimaryButton({ children, onPress }) {
+ return (
+
+
+ pressed
+ ? [styles.buttonInnerContainer, styles.pressed]
+ : styles.buttonInnerContainer
+ }
+ onPress={onPress}
+ android_ripple={{ color: Colors.primary600 }}
+ >
+ {children}
+
+
+ );
+}
+
+export default PrimaryButton;
+
+const styles = StyleSheet.create({
+ buttonOuterContainer: {
+ borderRadius: 28,
+ margin: 4,
+ overflow: 'hidden'
+ },
+ buttonInnerContainer: {
+ backgroundColor: Colors.primary500,
+ paddingVertical: 8,
+ paddingHorizontal: 16,
+ elevation: 2
+ },
+ buttonText: {
+ color: 'white',
+ textAlign: 'center'
+ },
+ pressed: {
+ opacity: 0.75
+ }
+});
diff --git a/attachments/04-deep-dive-real-app/00-starting-project/components/ui/Title.js b/attachments/04-deep-dive-real-app/00-starting-project/components/ui/Title.js
new file mode 100644
index 00000000..e700d6f4
--- /dev/null
+++ b/attachments/04-deep-dive-real-app/00-starting-project/components/ui/Title.js
@@ -0,0 +1,20 @@
+import { StyleSheet, Text } from 'react-native';
+
+function Title({ children }) {
+ return {children};
+}
+
+export default Title;
+
+const styles = StyleSheet.create({
+ title: {
+ fontFamily: 'open-sans-bold',
+ fontSize: 24,
+ // fontWeight: 'bold',
+ color: 'white',
+ textAlign: 'center',
+ borderWidth: 2,
+ borderColor: 'white',
+ padding: 12
+ }
+});
diff --git a/attachments/04-deep-dive-real-app/00-starting-project/constants/colors.js b/attachments/04-deep-dive-real-app/00-starting-project/constants/colors.js
new file mode 100644
index 00000000..b1740836
--- /dev/null
+++ b/attachments/04-deep-dive-real-app/00-starting-project/constants/colors.js
@@ -0,0 +1,9 @@
+const Colors = {
+ primary500: '#72063c',
+ primary600: '#640233',
+ primary700: '#4e0329',
+ primary800: '#3b021f',
+ accent500: '#ddb52f'
+};
+
+export default Colors;
diff --git a/attachments/04-deep-dive-real-app/00-starting-project/package.json b/attachments/04-deep-dive-real-app/00-starting-project/package.json
new file mode 100644
index 00000000..137ec56a
--- /dev/null
+++ b/attachments/04-deep-dive-real-app/00-starting-project/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "rncourse",
+ "version": "1.0.0",
+ "main": "node_modules/expo/AppEntry.js",
+ "scripts": {
+ "start": "expo start",
+ "android": "expo start --android",
+ "ios": "expo start --ios",
+ "web": "expo start --web",
+ "eject": "expo eject"
+ },
+ "dependencies": {
+ "expo": "^52.0.0",
+ "expo-app-loading": "^2.1.1",
+ "expo-font": "~13.0.4",
+ "expo-linear-gradient": "~14.0.2",
+ "expo-status-bar": "~2.0.1",
+ "react": "18.3.1",
+ "react-dom": "18.3.1",
+ "react-native": "0.76.7",
+ "react-native-web": "~0.19.13"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.25.2"
+ },
+ "private": true
+}
diff --git a/attachments/04-deep-dive-real-app/00-starting-project/screens/GameOverScreen.js b/attachments/04-deep-dive-real-app/00-starting-project/screens/GameOverScreen.js
new file mode 100644
index 00000000..9d043b4b
--- /dev/null
+++ b/attachments/04-deep-dive-real-app/00-starting-project/screens/GameOverScreen.js
@@ -0,0 +1,58 @@
+import { Image, StyleSheet, Text, View } from 'react-native';
+import Title from '../components/ui/Title';
+import Colors from '../constants/colors';
+import PrimaryButton from '../components/ui/PrimaryButton';
+
+function GameOverScreen({ roundsNumber, userNumber, onStartNewGame }) {
+ return (
+
+ GAME OVER!
+
+
+
+
+ Your phone needed {roundsNumber}{' '}
+ rounds to guess the number{' '}
+ {userNumber}.
+
+ Start New Game
+
+ );
+}
+
+export default GameOverScreen;
+
+const styles = StyleSheet.create({
+ rootContainer: {
+ flex: 1,
+ padding: 24,
+ justifyContent: 'center',
+ alignItems: 'center'
+ },
+ imageContainer: {
+ width: 300,
+ height: 300,
+ borderRadius: 150,
+ borderWidth: 3,
+ borderColor: Colors.primary800,
+ overflow: 'hidden',
+ margin: 36
+ },
+ image: {
+ width: '100%',
+ height: '100%'
+ },
+ summaryText: {
+ fontFamily: 'open-sans',
+ fontSize: 24,
+ textAlign: 'center',
+ marginBottom: 24
+ },
+ higlight: {
+ fontFamily: 'open-sans-bold',
+ color: Colors.primary500
+ }
+});
diff --git a/attachments/04-deep-dive-real-app/00-starting-project/screens/GameScreen.js b/attachments/04-deep-dive-real-app/00-starting-project/screens/GameScreen.js
new file mode 100644
index 00000000..396b8df9
--- /dev/null
+++ b/attachments/04-deep-dive-real-app/00-starting-project/screens/GameScreen.js
@@ -0,0 +1,132 @@
+import { Alert, StyleSheet, View, FlatList } from 'react-native';
+import Title from '../components/ui/Title';
+import { useEffect, useState } from 'react';
+import NumberContainer from '../components/game/NumberContainer';
+import PrimaryButton from '../components/ui/PrimaryButton';
+import Card from '../components/ui/Card';
+import InstructionsText from '../components/ui/InstructionsText';
+import Ionicons from '@expo/vector-icons/Ionicons';
+import GuessLogItem from '../components/game/GuessLogItem';
+
+function generateRandomBetween(min, max, exclude) {
+ const rndNum = Math.floor(Math.random() * (max - min)) + min;
+
+ if (rndNum === exclude) {
+ return generateRandomBetween(min, max, exclude);
+ } else {
+ return rndNum;
+ }
+}
+
+let minBoundary = 1;
+let maxBoundary = 100;
+
+function GameScreen({ userNumber, onGameOver }) {
+ const initialGuess = generateRandomBetween(1, 100, userNumber);
+ const [currentGuess, setCurrentGuess] = useState(initialGuess);
+ const [guessRounds, setGuessRounds] = useState([initialGuess]);
+
+ useEffect(() => {
+ if (userNumber === currentGuess) {
+ onGameOver(guessRounds.length);
+ }
+ }, [currentGuess, userNumber, onGameOver]);
+
+ useEffect(() => {
+ minBoundary = 1;
+ maxBoundary = 100;
+ }, []);
+
+ function nextGuessHandler(direction) {
+ // direction => "lower", "greater"
+
+ if (
+ (direction === 'lower' && currentGuess < userNumber) ||
+ (direction === 'greater' && currentGuess > userNumber)
+ ) {
+ Alert.alert("Don't lie!", 'You know that this is wrong...', [
+ { text: 'Sorry', style: 'cancel' }
+ ]);
+
+ return;
+ }
+
+ if (direction === 'lower') {
+ maxBoundary = currentGuess;
+ } else {
+ minBoundary = currentGuess + 1;
+ }
+ const newRandomNumber = generateRandomBetween(
+ minBoundary,
+ maxBoundary,
+ currentGuess
+ );
+ setCurrentGuess(newRandomNumber);
+ setGuessRounds((prev) => [newRandomNumber, ...prev]);
+ }
+
+ const guessRoundsListLength = guessRounds.length;
+
+ return (
+
+ Opponent's Guess
+ {currentGuess}
+
+
+ Higher or lower?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* {guessRounds.map((guessRound) => (
+ {guessRound}
+ ))} */}
+
+ (
+
+ )}
+ keyExtractor={(item, index) => item}
+ />
+
+
+ );
+}
+
+export default GameScreen;
+
+const styles = StyleSheet.create({
+ screen: {
+ flex: 1,
+ padding: 24
+ },
+ instructionText: {
+ marginBottom: 12
+ },
+ buttonsContainer: {
+ flexDirection: 'row'
+ },
+ buttonContainer: {
+ flex: 1
+ },
+ listContainer: {
+ flex: 1,
+ padding: 16
+ }
+});
diff --git a/attachments/04-deep-dive-real-app/00-starting-project/screens/StartGameScreen.js b/attachments/04-deep-dive-real-app/00-starting-project/screens/StartGameScreen.js
new file mode 100644
index 00000000..f42753d1
--- /dev/null
+++ b/attachments/04-deep-dive-real-app/00-starting-project/screens/StartGameScreen.js
@@ -0,0 +1,92 @@
+import { Alert, StyleSheet, TextInput, View } from 'react-native';
+import PrimaryButton from '../components/ui/PrimaryButton';
+import { useState } from 'react';
+import Colors from '../constants/colors';
+import Title from '../components/ui/Title';
+import Card from '../components/ui/Card';
+import InstructionsText from '../components/ui/InstructionsText';
+
+function StartGameScreen({ onPickNumber }) {
+ const [enteredNumber, setEnteredNumber] = useState('');
+
+ function numberInputHandler(enteredText) {
+ setEnteredNumber(enteredText);
+ }
+
+ function resetInputHandler() {
+ setEnteredNumber('');
+ }
+
+ function confirmInputHandler() {
+ const chosenNumber = parseInt(enteredNumber);
+
+ if (isNaN(chosenNumber) || chosenNumber <= 0 || chosenNumber > 99) {
+ // Show Alert
+
+ Alert.alert(
+ 'Invalid number',
+ 'Number has to be a number between 1 and 99',
+ // in the alert buttons are constructed as objects with a text and Style, we can have multiple that's why there is an array
+ [{ text: 'Okay', style: 'destructive', onPress: resetInputHandler }]
+ );
+ return;
+ }
+
+ onPickNumber(chosenNumber);
+ }
+
+ return (
+
+ Guess My Number
+
+ Enter a Number
+
+
+
+
+ Reset
+
+
+ Confirm
+
+
+
+
+ );
+}
+
+export default StartGameScreen;
+
+const styles = StyleSheet.create({
+ rootContainer: {
+ flex: 1,
+ marginTop: 100,
+ alignItems: 'center'
+ },
+
+ numberInput: {
+ height: 50,
+ width: 50,
+ fontSize: 32,
+ borderBottomColor: Colors.accent500,
+ borderBottomWidth: 2,
+ color: Colors.accent500,
+ marginVertical: 8,
+ fontWeight: 'bold',
+ textAlign: 'center'
+ },
+ buttonsContainer: {
+ flexDirection: 'row'
+ },
+ buttonContainer: {
+ flex: 1
+ }
+});
diff --git a/attachments/05-adaptive-uis/00-starting-project/App.js b/attachments/05-adaptive-uis/00-starting-project/App.js
new file mode 100644
index 00000000..deefde42
--- /dev/null
+++ b/attachments/05-adaptive-uis/00-starting-project/App.js
@@ -0,0 +1,87 @@
+import { useState } from 'react';
+import { StyleSheet, ImageBackground, SafeAreaView } from 'react-native';
+import { LinearGradient } from 'expo-linear-gradient';
+import { useFonts } from 'expo-font';
+import AppLoading from 'expo-app-loading';
+import { StatusBar } from 'expo-status-bar';
+
+import StartGameScreen from './screens/StartGameScreen';
+import GameScreen from './screens/GameScreen';
+import GameOverScreen from './screens/GameOverScreen';
+import Colors from './constants/colors';
+
+export default function App() {
+ const [userNumber, setUserNumber] = useState();
+ const [gameIsOver, setGameIsOver] = useState(true);
+ const [guessRounds, setGuessRounds] = useState(0);
+
+ const [fontsLoaded] = useFonts({
+ 'open-sans': require('./assets/fonts/OpenSans-Regular.ttf'),
+ 'open-sans-bold': require('./assets/fonts/OpenSans-Bold.ttf')
+ });
+
+ if (!fontsLoaded) {
+ return ;
+ }
+
+ function pickedNumberHandler(pickedNumber) {
+ setUserNumber(pickedNumber);
+ setGameIsOver(false);
+ }
+
+ function gameOverHandler(numberOfRounds) {
+ setGameIsOver(true);
+ setGuessRounds(numberOfRounds);
+ }
+
+ function startNewGameHandler() {
+ setUserNumber(null);
+ setGuessRounds(0);
+ }
+
+ let screen = ;
+
+ if (userNumber) {
+ screen = (
+
+ );
+ }
+
+ if (gameIsOver && userNumber) {
+ screen = (
+
+ );
+ }
+
+ return (
+ <>
+
+
+
+ {screen}
+
+
+ >
+ );
+}
+
+const styles = StyleSheet.create({
+ rootScreen: {
+ flex: 1
+ },
+ backgroundImage: {
+ opacity: 0.15
+ }
+});
diff --git a/attachments/05-adaptive-uis/00-starting-project/app.json b/attachments/05-adaptive-uis/00-starting-project/app.json
new file mode 100644
index 00000000..df63705f
--- /dev/null
+++ b/attachments/05-adaptive-uis/00-starting-project/app.json
@@ -0,0 +1,32 @@
+{
+ "expo": {
+ "name": "RNCourse",
+ "slug": "RNCourse",
+ "version": "1.0.0",
+ "orientation": "default",
+ "icon": "./assets/icon.png",
+ "splash": {
+ "image": "./assets/splash.png",
+ "resizeMode": "contain",
+ "backgroundColor": "#ffffff"
+ },
+ "updates": {
+ "fallbackToCacheTimeout": 0
+ },
+ "assetBundlePatterns": [
+ "**/*"
+ ],
+ "ios": {
+ "supportsTablet": true
+ },
+ "android": {
+ "adaptiveIcon": {
+ "foregroundImage": "./assets/adaptive-icon.png",
+ "backgroundColor": "#FFFFFF"
+ }
+ },
+ "web": {
+ "favicon": "./assets/favicon.png"
+ }
+ }
+}
diff --git a/attachments/05-adaptive-uis/00-starting-project/assets/adaptive-icon.png b/attachments/05-adaptive-uis/00-starting-project/assets/adaptive-icon.png
new file mode 100644
index 00000000..03d6f6b6
Binary files /dev/null and b/attachments/05-adaptive-uis/00-starting-project/assets/adaptive-icon.png differ
diff --git a/attachments/05-adaptive-uis/00-starting-project/assets/favicon.png b/attachments/05-adaptive-uis/00-starting-project/assets/favicon.png
new file mode 100644
index 00000000..e75f697b
Binary files /dev/null and b/attachments/05-adaptive-uis/00-starting-project/assets/favicon.png differ
diff --git a/attachments/05-adaptive-uis/00-starting-project/assets/fonts/OpenSans-Bold.ttf b/attachments/05-adaptive-uis/00-starting-project/assets/fonts/OpenSans-Bold.ttf
new file mode 100644
index 00000000..96fabd86
Binary files /dev/null and b/attachments/05-adaptive-uis/00-starting-project/assets/fonts/OpenSans-Bold.ttf differ
diff --git a/attachments/05-adaptive-uis/00-starting-project/assets/fonts/OpenSans-Regular.ttf b/attachments/05-adaptive-uis/00-starting-project/assets/fonts/OpenSans-Regular.ttf
new file mode 100644
index 00000000..2d4da3a6
Binary files /dev/null and b/attachments/05-adaptive-uis/00-starting-project/assets/fonts/OpenSans-Regular.ttf differ
diff --git a/attachments/05-adaptive-uis/00-starting-project/assets/icon.png b/attachments/05-adaptive-uis/00-starting-project/assets/icon.png
new file mode 100644
index 00000000..a0b1526f
Binary files /dev/null and b/attachments/05-adaptive-uis/00-starting-project/assets/icon.png differ
diff --git a/attachments/05-adaptive-uis/00-starting-project/assets/images/background.png b/attachments/05-adaptive-uis/00-starting-project/assets/images/background.png
new file mode 100644
index 00000000..300c5477
Binary files /dev/null and b/attachments/05-adaptive-uis/00-starting-project/assets/images/background.png differ
diff --git a/attachments/05-adaptive-uis/00-starting-project/assets/images/success.png b/attachments/05-adaptive-uis/00-starting-project/assets/images/success.png
new file mode 100644
index 00000000..aae773a0
Binary files /dev/null and b/attachments/05-adaptive-uis/00-starting-project/assets/images/success.png differ
diff --git a/attachments/05-adaptive-uis/00-starting-project/assets/splash.png b/attachments/05-adaptive-uis/00-starting-project/assets/splash.png
new file mode 100644
index 00000000..0e89705a
Binary files /dev/null and b/attachments/05-adaptive-uis/00-starting-project/assets/splash.png differ
diff --git a/attachments/05-adaptive-uis/00-starting-project/babel.config.js b/attachments/05-adaptive-uis/00-starting-project/babel.config.js
new file mode 100644
index 00000000..2900afe9
--- /dev/null
+++ b/attachments/05-adaptive-uis/00-starting-project/babel.config.js
@@ -0,0 +1,6 @@
+module.exports = function(api) {
+ api.cache(true);
+ return {
+ presets: ['babel-preset-expo'],
+ };
+};
diff --git a/attachments/05-adaptive-uis/00-starting-project/components/game/GuessLogItem.js b/attachments/05-adaptive-uis/00-starting-project/components/game/GuessLogItem.js
new file mode 100644
index 00000000..943e656c
--- /dev/null
+++ b/attachments/05-adaptive-uis/00-starting-project/components/game/GuessLogItem.js
@@ -0,0 +1,36 @@
+import { View, Text, StyleSheet } from 'react-native';
+
+import Colors from '../../constants/colors';
+
+function GuessLogItem({ roundNumber, guess }) {
+ return (
+
+ #{roundNumber}
+ Opponent's Guess: {guess}
+
+ );
+}
+
+export default GuessLogItem;
+
+const styles = StyleSheet.create({
+ listItem: {
+ borderColor: Colors.primary800,
+ borderWidth: 1,
+ borderRadius: 40,
+ padding: 12,
+ marginVertical: 8,
+ backgroundColor: Colors.accent500,
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ width: '100%',
+ elevation: 4,
+ shadowColor: 'black',
+ shadowOffset: { width: 0, height: 0 },
+ shadowOpacity: 0.25,
+ shadowRadius: 3,
+ },
+ itemText: {
+ fontFamily: 'open-sans'
+ }
+});
diff --git a/attachments/05-adaptive-uis/00-starting-project/components/game/NumberContainer.js b/attachments/05-adaptive-uis/00-starting-project/components/game/NumberContainer.js
new file mode 100644
index 00000000..a3e5ec12
--- /dev/null
+++ b/attachments/05-adaptive-uis/00-starting-project/components/game/NumberContainer.js
@@ -0,0 +1,33 @@
+import { View, Text, StyleSheet, Dimensions } from 'react-native';
+
+import Colors from '../../constants/colors';
+
+function NumberContainer({ children }) {
+ return (
+
+ {children}
+
+ );
+}
+
+export default NumberContainer;
+
+const deviceWidth = Dimensions.get('window').width;
+
+const styles = StyleSheet.create({
+ container: {
+ borderWidth: 4,
+ borderColor: Colors.accent500,
+ padding: deviceWidth < 380 ? 12 : 24,
+ margin: deviceWidth < 380 ? 12 : 24,
+ borderRadius: 8,
+ alignItems: 'center',
+ justifyContent: 'center'
+ },
+ numberText: {
+ color: Colors.accent500,
+ fontSize: 36,
+ // fontWeight: 'bold',
+ fontFamily: 'open-sans-bold'
+ }
+});
diff --git a/attachments/05-adaptive-uis/00-starting-project/components/ui/Card.js b/attachments/05-adaptive-uis/00-starting-project/components/ui/Card.js
new file mode 100644
index 00000000..cd4c23f0
--- /dev/null
+++ b/attachments/05-adaptive-uis/00-starting-project/components/ui/Card.js
@@ -0,0 +1,28 @@
+import { View, StyleSheet, Dimensions } from 'react-native';
+
+import Colors from '../../constants/colors';
+
+function Card({ children }) {
+ return {children};
+}
+
+export default Card;
+
+const deviceWidth = Dimensions.get('window').width;
+
+const styles = StyleSheet.create({
+ card: {
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginTop: deviceWidth < 380 ? 18 : 36,
+ marginHorizontal: 24,
+ padding: 16,
+ backgroundColor: Colors.primary800,
+ borderRadius: 8,
+ elevation: 4,
+ shadowColor: 'black',
+ shadowOffset: { width: 0, height: 2 },
+ shadowRadius: 6,
+ shadowOpacity: 0.25
+ }
+});
diff --git a/attachments/05-adaptive-uis/00-starting-project/components/ui/InstructionText.js b/attachments/05-adaptive-uis/00-starting-project/components/ui/InstructionText.js
new file mode 100644
index 00000000..8b69fe31
--- /dev/null
+++ b/attachments/05-adaptive-uis/00-starting-project/components/ui/InstructionText.js
@@ -0,0 +1,17 @@
+import { Text, StyleSheet } from 'react-native';
+
+import Colors from '../../constants/colors';
+
+function InstructionText({ children, style }) {
+ return {children};
+}
+
+export default InstructionText;
+
+const styles = StyleSheet.create({
+ instructionText: {
+ fontFamily: 'open-sans',
+ color: Colors.accent500,
+ fontSize: 24,
+ },
+});
diff --git a/attachments/05-adaptive-uis/00-starting-project/components/ui/PrimaryButton.js b/attachments/05-adaptive-uis/00-starting-project/components/ui/PrimaryButton.js
new file mode 100644
index 00000000..bd963545
--- /dev/null
+++ b/attachments/05-adaptive-uis/00-starting-project/components/ui/PrimaryButton.js
@@ -0,0 +1,44 @@
+import { View, Text, Pressable, StyleSheet } from 'react-native';
+
+import Colors from '../../constants/colors';
+
+function PrimaryButton({ children, onPress }) {
+ return (
+
+
+ pressed
+ ? [styles.buttonInnerContainer, styles.pressed]
+ : styles.buttonInnerContainer
+ }
+ onPress={onPress}
+ android_ripple={{ color: Colors.primary600 }}
+ >
+ {children}
+
+
+ );
+}
+
+export default PrimaryButton;
+
+const styles = StyleSheet.create({
+ buttonOuterContainer: {
+ borderRadius: 28,
+ margin: 4,
+ overflow: 'hidden',
+ },
+ buttonInnerContainer: {
+ backgroundColor: Colors.primary500,
+ paddingVertical: 8,
+ paddingHorizontal: 16,
+ elevation: 2,
+ },
+ buttonText: {
+ color: 'white',
+ textAlign: 'center',
+ },
+ pressed: {
+ opacity: 0.75,
+ },
+});
diff --git a/attachments/05-adaptive-uis/00-starting-project/components/ui/Title.android.js b/attachments/05-adaptive-uis/00-starting-project/components/ui/Title.android.js
new file mode 100644
index 00000000..0bcc2c68
--- /dev/null
+++ b/attachments/05-adaptive-uis/00-starting-project/components/ui/Title.android.js
@@ -0,0 +1,26 @@
+import { Text, StyleSheet, Platform } from 'react-native';
+
+// Because this file is Title.android.js it will only be used for Android!
+// We need to make sure that were we use it, the import doens't say "android" or "ios" because it will do that automatically based on the platform
+
+function Title({ children }) {
+ return {children};
+}
+
+export default Title;
+
+const styles = StyleSheet.create({
+ title: {
+ fontFamily: 'open-sans-bold',
+ fontSize: 24,
+ // fontWeight: 'bold',
+ color: 'white',
+ textAlign: 'center',
+ // borderWidth: Platform.OS === "android" ? 2 : 0,
+ // borderWidth: Platform.select({ios: 0, android: 2}),
+ borderWidth: 2,
+ borderColor: 'white',
+ padding: 12,
+ maxWidth: '80%'
+ }
+});
diff --git a/attachments/05-adaptive-uis/00-starting-project/components/ui/Title.ios.js b/attachments/05-adaptive-uis/00-starting-project/components/ui/Title.ios.js
new file mode 100644
index 00000000..b34a9713
--- /dev/null
+++ b/attachments/05-adaptive-uis/00-starting-project/components/ui/Title.ios.js
@@ -0,0 +1,25 @@
+import { Text, StyleSheet, Platform } from 'react-native';
+
+// Because this file is Title.android.js it will only be used for Android!
+// We need to make sure that were we use it, the import doens't say "android" or "ios" because it will do that automatically based on the platform
+
+function Title({ children }) {
+ return {children};
+}
+
+export default Title;
+
+const styles = StyleSheet.create({
+ title: {
+ fontFamily: 'open-sans-bold',
+ fontSize: 24,
+ // fontWeight: 'bold',
+ color: 'white',
+ textAlign: 'center',
+ // borderWidth: Platform.OS === "android" ? 2 : 0,
+ // borderWidth: Platform.select({ios: 0, android: 2}),
+ borderColor: 'white',
+ padding: 12,
+ maxWidth: '80%'
+ }
+});
diff --git a/attachments/05-adaptive-uis/00-starting-project/components/ui/Title.js b/attachments/05-adaptive-uis/00-starting-project/components/ui/Title.js
new file mode 100644
index 00000000..d0b76ffb
--- /dev/null
+++ b/attachments/05-adaptive-uis/00-starting-project/components/ui/Title.js
@@ -0,0 +1,24 @@
+// import { Text, StyleSheet, Platform } from 'react-native';
+
+// THIS FILE SHOWS HOW TO HANDLE PLATFORM SPECIFIC CODE FOR IOS AND ANDROID. THE OTHER FILES THAT HAVE TITLE..JS ARE ANOTHER WAY OF DOING THIS IN SEPARAT FILES!
+
+// function Title({ children }) {
+// return {children};
+// }
+
+// export default Title;
+
+// const styles = StyleSheet.create({
+// title: {
+// fontFamily: 'open-sans-bold',
+// fontSize: 24,
+// // fontWeight: 'bold',
+// color: 'white',
+// textAlign: 'center',
+// // borderWidth: Platform.OS === "android" ? 2 : 0,
+// borderWidth: Platform.select({ios: 0, android: 2}),
+// borderColor: 'white',
+// padding: 12,
+// maxWidth: "80%",
+// },
+// });
diff --git a/attachments/05-adaptive-uis/00-starting-project/constants/colors.android.js b/attachments/05-adaptive-uis/00-starting-project/constants/colors.android.js
new file mode 100644
index 00000000..d181ccea
--- /dev/null
+++ b/attachments/05-adaptive-uis/00-starting-project/constants/colors.android.js
@@ -0,0 +1,9 @@
+const Colors = {
+ primary500: '#72063c',
+ primary600: '#640233',
+ primary700: '#4e0329',
+ primary800: '#3b021f',
+ accent500: '#ddb52f'
+};
+
+export default Colors;
\ No newline at end of file
diff --git a/attachments/05-adaptive-uis/00-starting-project/constants/colors.ios.js b/attachments/05-adaptive-uis/00-starting-project/constants/colors.ios.js
new file mode 100644
index 00000000..d181ccea
--- /dev/null
+++ b/attachments/05-adaptive-uis/00-starting-project/constants/colors.ios.js
@@ -0,0 +1,9 @@
+const Colors = {
+ primary500: '#72063c',
+ primary600: '#640233',
+ primary700: '#4e0329',
+ primary800: '#3b021f',
+ accent500: '#ddb52f'
+};
+
+export default Colors;
\ No newline at end of file
diff --git a/attachments/05-adaptive-uis/00-starting-project/package.json b/attachments/05-adaptive-uis/00-starting-project/package.json
new file mode 100644
index 00000000..e3993482
--- /dev/null
+++ b/attachments/05-adaptive-uis/00-starting-project/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "rncourse",
+ "version": "1.0.0",
+ "main": "node_modules/expo/AppEntry.js",
+ "scripts": {
+ "start": "expo start",
+ "android": "expo start --android",
+ "ios": "expo start --ios",
+ "web": "expo start --web",
+ "eject": "expo eject"
+ },
+ "dependencies": {
+ "expo": "^52.0.0",
+ "expo-status-bar": "~2.0.1",
+ "react": "18.3.1",
+ "react-dom": "18.3.1",
+ "react-native": "0.76.7",
+ "react-native-web": "~0.19.13",
+ "expo-linear-gradient": "~14.0.2",
+ "expo-font": "~13.0.4",
+ "expo-app-loading": "~1.3.0"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.25.2"
+ },
+ "private": true
+}
diff --git a/attachments/05-adaptive-uis/00-starting-project/screens/GameOverScreen.js b/attachments/05-adaptive-uis/00-starting-project/screens/GameOverScreen.js
new file mode 100644
index 00000000..f5411067
--- /dev/null
+++ b/attachments/05-adaptive-uis/00-starting-project/screens/GameOverScreen.js
@@ -0,0 +1,92 @@
+import {
+ View,
+ Image,
+ Text,
+ StyleSheet,
+ Dimensions,
+ ScrollView,
+ useWindowDimensions
+} from 'react-native';
+
+import Title from '../components/ui/Title';
+import PrimaryButton from '../components/ui/PrimaryButton';
+import Colors from '../constants/colors';
+
+function GameOverScreen({ roundsNumber, userNumber, onStartNewGame }) {
+ const { width, height } = useWindowDimensions();
+
+ let imageSize = 300;
+
+ if (width < 380) {
+ imageSize = 150;
+ }
+
+ if (height < 380) {
+ imageSize = 80;
+ }
+
+ const imageStyle = {
+ width: imageSize,
+ height: imageSize,
+ borderRadius: imageSize / 2
+ };
+
+ return (
+
+
+ GAME OVER!
+
+
+
+
+ Your phone needed {roundsNumber}{' '}
+ rounds to guess the number{' '}
+ {userNumber}.
+
+ Start New Game
+
+
+ );
+}
+
+export default GameOverScreen;
+
+// const deviceWidth = Dimensions.get('window').width;
+
+const styles = StyleSheet.create({
+ screen: {
+ flex: 1
+ },
+ rootContainer: {
+ flex: 1,
+ padding: 24,
+ justifyContent: 'center',
+ alignItems: 'center'
+ },
+ imageContainer: {
+ // width: deviceWidth < 380 ? 150 : 300,
+ // height: deviceWidth < 380 ? 150 : 300,
+ // borderRadius: deviceWidth < 380 ? 75 : 300,
+ borderWidth: 3,
+ borderColor: Colors.primary800,
+ overflow: 'hidden',
+ margin: 36
+ },
+ image: {
+ width: '100%',
+ height: '100%'
+ },
+ summaryText: {
+ fontFamily: 'open-sans',
+ fontSize: 24,
+ textAlign: 'center',
+ marginBottom: 24
+ },
+ highlight: {
+ fontFamily: 'open-sans-bold',
+ color: Colors.primary500
+ }
+});
diff --git a/attachments/05-adaptive-uis/00-starting-project/screens/GameScreen.js b/attachments/05-adaptive-uis/00-starting-project/screens/GameScreen.js
new file mode 100644
index 00000000..3582c96a
--- /dev/null
+++ b/attachments/05-adaptive-uis/00-starting-project/screens/GameScreen.js
@@ -0,0 +1,172 @@
+import { useState, useEffect } from 'react';
+import {
+ View,
+ StyleSheet,
+ Alert,
+ FlatList,
+ useWindowDimensions
+} from 'react-native';
+import { Ionicons } from '@expo/vector-icons';
+
+import NumberContainer from '../components/game/NumberContainer';
+import Card from '../components/ui/Card';
+import InstructionText from '../components/ui/InstructionText';
+import PrimaryButton from '../components/ui/PrimaryButton';
+import Title from '../components/ui/Title';
+import GuessLogItem from '../components/game/GuessLogItem';
+
+function generateRandomBetween(min, max, exclude) {
+ const rndNum = Math.floor(Math.random() * (max - min)) + min;
+
+ if (rndNum === exclude) {
+ return generateRandomBetween(min, max, exclude);
+ } else {
+ return rndNum;
+ }
+}
+
+let minBoundary = 1;
+let maxBoundary = 100;
+
+function GameScreen({ userNumber, onGameOver }) {
+ const initialGuess = generateRandomBetween(1, 100, userNumber);
+ const [currentGuess, setCurrentGuess] = useState(initialGuess);
+ const [guessRounds, setGuessRounds] = useState([initialGuess]);
+ const { width, height } = useWindowDimensions();
+
+ useEffect(() => {
+ if (currentGuess === userNumber) {
+ onGameOver(guessRounds.length);
+ }
+ }, [currentGuess, userNumber, onGameOver]);
+
+ useEffect(() => {
+ minBoundary = 1;
+ maxBoundary = 100;
+ }, []);
+
+ function nextGuessHandler(direction) {
+ // direction => 'lower', 'greater'
+ if (
+ (direction === 'lower' && currentGuess < userNumber) ||
+ (direction === 'greater' && currentGuess > userNumber)
+ ) {
+ Alert.alert("Don't lie!", 'You know that this is wrong...', [
+ { text: 'Sorry!', style: 'cancel' }
+ ]);
+ return;
+ }
+
+ if (direction === 'lower') {
+ maxBoundary = currentGuess;
+ } else {
+ minBoundary = currentGuess + 1;
+ }
+
+ const newRndNumber = generateRandomBetween(
+ minBoundary,
+ maxBoundary,
+ currentGuess
+ );
+ setCurrentGuess(newRndNumber);
+ setGuessRounds((prevGuessRounds) => [newRndNumber, ...prevGuessRounds]);
+ }
+
+ const guessRoundsListLength = guessRounds.length;
+
+ let content = (
+ <>
+ {currentGuess}
+
+
+ Higher or lower?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+
+ if (width > 500) {
+ content = (
+ <>
+
+ Higher or lower?
+
+
+
+
+
+
+
+
+ {currentGuess}
+
+
+
+
+
+
+ >
+ );
+ }
+
+ return (
+
+ Opponent's Guess
+
+ {content}
+
+
+ {/* {guessRounds.map(guessRound => {guessRound})} */}
+ (
+
+ )}
+ keyExtractor={(item) => item}
+ />
+
+
+ );
+}
+
+export default GameScreen;
+
+const styles = StyleSheet.create({
+ screen: {
+ flex: 1,
+ padding: 24,
+ alignItems: 'center'
+ },
+ instructionText: {
+ marginBottom: 12
+ },
+ buttonsContainer: {
+ flexDirection: 'row'
+ },
+ buttonContainer: {
+ flex: 1
+ },
+ buttonsContainerWide: {
+ flexDirection: 'row',
+ alignItems: 'center'
+ },
+ listContainer: {
+ flex: 1,
+ padding: 16
+ }
+});
diff --git a/attachments/05-adaptive-uis/00-starting-project/screens/StartGameScreen.js b/attachments/05-adaptive-uis/00-starting-project/screens/StartGameScreen.js
new file mode 100644
index 00000000..bb65cf80
--- /dev/null
+++ b/attachments/05-adaptive-uis/00-starting-project/screens/StartGameScreen.js
@@ -0,0 +1,114 @@
+import { useState } from 'react';
+import {
+ TextInput,
+ View,
+ StyleSheet,
+ Alert,
+ ScrollView,
+ Dimensions,
+ useWindowDimensions,
+ KeyboardAvoidingView
+} from 'react-native';
+
+import PrimaryButton from '../components/ui/PrimaryButton';
+import Title from '../components/ui/Title';
+import Colors from '../constants/colors';
+import Card from '../components/ui/Card';
+import InstructionText from '../components/ui/InstructionText';
+
+function StartGameScreen({ onPickNumber }) {
+ const [enteredNumber, setEnteredNumber] = useState('');
+
+ // for dynamic (eg screen rotation) dimension we can use the useWindowDimensions hook!
+ const { width, height } = useWindowDimensions();
+
+ function numberInputHandler(enteredText) {
+ setEnteredNumber(enteredText);
+ }
+
+ function resetInputHandler() {
+ setEnteredNumber('');
+ }
+
+ function confirmInputHandler() {
+ const chosenNumber = parseInt(enteredNumber);
+
+ if (isNaN(chosenNumber) || chosenNumber <= 0 || chosenNumber > 99) {
+ Alert.alert(
+ 'Invalid number!',
+ 'Number has to be a number between 1 and 99.',
+ [{ text: 'Okay', style: 'destructive', onPress: resetInputHandler }]
+ );
+ return;
+ }
+
+ onPickNumber(chosenNumber);
+ }
+
+ const marginTopDistance = height < 380 ? 30 : 100;
+
+ return (
+
+
+
+ Guess My Number
+
+ Enter a Number
+
+
+
+ Reset
+
+
+
+ Confirm
+
+
+
+
+
+
+
+ );
+}
+
+export default StartGameScreen;
+
+// this code is only executed once, when this component code, this entire file code is parsed and executed for the first time
+const deviceHeight = Dimensions.get('window').height;
+
+const styles = StyleSheet.create({
+ screen: {
+ flex: 1
+ },
+ rootContainer: {
+ flex: 1,
+ // marginTop: deviceHeight < 380 ? 30 : 100,
+ alignItems: 'center'
+ },
+ numberInput: {
+ height: 50,
+ width: 50,
+ fontSize: 32,
+ borderBottomColor: Colors.accent500,
+ borderBottomWidth: 2,
+ color: Colors.accent500,
+ marginVertical: 8,
+ fontWeight: 'bold',
+ textAlign: 'center'
+ },
+ buttonsContainer: {
+ flexDirection: 'row'
+ },
+ buttonContainer: {
+ flex: 1
+ }
+});
diff --git a/attachments/06-navigation/00-starting-project/App.js b/attachments/06-navigation/00-starting-project/App.js
new file mode 100644
index 00000000..002ef539
--- /dev/null
+++ b/attachments/06-navigation/00-starting-project/App.js
@@ -0,0 +1,114 @@
+import { StatusBar } from 'expo-status-bar';
+import { StyleSheet, Text } from 'react-native';
+import CategoriesScreen from './screens/CategoriesScreen';
+
+import { NavigationContainer } from '@react-navigation/native';
+import { createNativeStackNavigator } from '@react-navigation/native-stack';
+import { createDrawerNavigator } from '@react-navigation/drawer';
+import MealsOverviewScreen from './screens/MealsOverviewScreen';
+import MealDetailScreen from './screens/MealDetailScreen';
+import FavoritesScreen from './screens/FavoritesScreen';
+import { Ionicons } from '@expo/vector-icons';
+
+const Stack = createNativeStackNavigator();
+const Drawer = createDrawerNavigator();
+
+function DrawerNavigator() {
+ return (
+
+ (
+
+ )
+ }}
+ />
+ (
+
+ )
+ }}
+ />
+
+ );
+}
+
+export default function App() {
+ return (
+ <>
+
+
+
+
+ {
+ // const catId = route.params.categoryId;
+ // return {
+ // title: catId
+ // };
+ // }}
+ />
+ {
+ // return ;
+ // }
+ title: 'About the Meal'
+ }}
+ />
+
+
+ >
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: '#fff',
+ alignItems: 'center',
+ justifyContent: 'center'
+ }
+});
diff --git a/attachments/06-navigation/00-starting-project/app.json b/attachments/06-navigation/00-starting-project/app.json
new file mode 100644
index 00000000..4a30af8f
--- /dev/null
+++ b/attachments/06-navigation/00-starting-project/app.json
@@ -0,0 +1,31 @@
+{
+ "expo": {
+ "name": "RNCourse",
+ "slug": "RNCourse",
+ "version": "1.0.0",
+ "orientation": "portrait",
+ "icon": "./assets/icon.png",
+ "backgroundColor": "#24180f",
+ "splash": {
+ "image": "./assets/splash.png",
+ "resizeMode": "contain",
+ "backgroundColor": "#ffffff"
+ },
+ "updates": {
+ "fallbackToCacheTimeout": 0
+ },
+ "assetBundlePatterns": ["**/*"],
+ "ios": {
+ "supportsTablet": true
+ },
+ "android": {
+ "adaptiveIcon": {
+ "foregroundImage": "./assets/adaptive-icon.png",
+ "backgroundColor": "#FFFFFF"
+ }
+ },
+ "web": {
+ "favicon": "./assets/favicon.png"
+ }
+ }
+}
diff --git a/attachments/06-navigation/00-starting-project/assets/adaptive-icon.png b/attachments/06-navigation/00-starting-project/assets/adaptive-icon.png
new file mode 100644
index 00000000..03d6f6b6
Binary files /dev/null and b/attachments/06-navigation/00-starting-project/assets/adaptive-icon.png differ
diff --git a/attachments/06-navigation/00-starting-project/assets/favicon.png b/attachments/06-navigation/00-starting-project/assets/favicon.png
new file mode 100644
index 00000000..e75f697b
Binary files /dev/null and b/attachments/06-navigation/00-starting-project/assets/favicon.png differ
diff --git a/attachments/06-navigation/00-starting-project/assets/icon.png b/attachments/06-navigation/00-starting-project/assets/icon.png
new file mode 100644
index 00000000..a0b1526f
Binary files /dev/null and b/attachments/06-navigation/00-starting-project/assets/icon.png differ
diff --git a/attachments/06-navigation/00-starting-project/assets/splash.png b/attachments/06-navigation/00-starting-project/assets/splash.png
new file mode 100644
index 00000000..0e89705a
Binary files /dev/null and b/attachments/06-navigation/00-starting-project/assets/splash.png differ
diff --git a/attachments/06-navigation/00-starting-project/babel.config.js b/attachments/06-navigation/00-starting-project/babel.config.js
new file mode 100644
index 00000000..2900afe9
--- /dev/null
+++ b/attachments/06-navigation/00-starting-project/babel.config.js
@@ -0,0 +1,6 @@
+module.exports = function(api) {
+ api.cache(true);
+ return {
+ presets: ['babel-preset-expo'],
+ };
+};
diff --git a/attachments/06-navigation/00-starting-project/components/CategoryGridTile.js b/attachments/06-navigation/00-starting-project/components/CategoryGridTile.js
new file mode 100644
index 00000000..7d41da6d
--- /dev/null
+++ b/attachments/06-navigation/00-starting-project/components/CategoryGridTile.js
@@ -0,0 +1,57 @@
+import { useNavigation } from '@react-navigation/native';
+import { View, Pressable, Text, StyleSheet, Platform } from 'react-native';
+
+function CategoryGridTile({ title, color, onPress }) {
+ return (
+
+ [
+ styles.button,
+ pressed ? styles.buttonPressed : null
+ ]}
+ onPress={onPress}
+ >
+
+ {title}
+
+
+
+ );
+}
+
+export default CategoryGridTile;
+
+const styles = StyleSheet.create({
+ gridItem: {
+ flex: 1,
+ margin: 16,
+ height: 150,
+ borderRadius: 8,
+ elevation: 4,
+ // for iOS for the shadow to have any effect we should add a background color!
+ backgroundColor: 'white',
+ shadowColor: 'black',
+ shadowOpacity: 0.25,
+ shadowOffset: { width: 0, height: 2 },
+ shadowRadius: 8,
+ overflow: Platform.OS === 'android' ? 'hidden' : 'visible'
+ },
+ button: {
+ flex: 1
+ },
+ buttonPressed: {
+ opacity: 0.5
+ },
+ innerContainer: {
+ flex: 1,
+ padding: 16,
+ borderRadius: 8,
+ justifyContent: 'center',
+ alignItems: 'center'
+ },
+ title: {
+ fontWeight: 'bold',
+ fontSize: 18
+ }
+});
diff --git a/attachments/06-navigation/00-starting-project/components/IconButton.js b/attachments/06-navigation/00-starting-project/components/IconButton.js
new file mode 100644
index 00000000..f701da12
--- /dev/null
+++ b/attachments/06-navigation/00-starting-project/components/IconButton.js
@@ -0,0 +1,22 @@
+import { Pressable } from 'react-native';
+import { Ionicons } from '@expo/vector-icons';
+import { StyleSheet } from 'react-native';
+
+function IconButton({ icon, color, onPress }) {
+ return (
+ pressed && styles.pressed}
+ >
+
+
+ );
+}
+
+export default IconButton;
+
+const styles = StyleSheet.create({
+ pressed: {
+ opacity: 0.7
+ }
+});
diff --git a/attachments/06-navigation/00-starting-project/components/MealDetail/List.js b/attachments/06-navigation/00-starting-project/components/MealDetail/List.js
new file mode 100644
index 00000000..4bcba8a3
--- /dev/null
+++ b/attachments/06-navigation/00-starting-project/components/MealDetail/List.js
@@ -0,0 +1,29 @@
+import React from 'react';
+import { View } from 'react-native';
+import { StyleSheet } from 'react-native';
+import { Text } from 'react-native';
+
+function List({ data }) {
+ return data.map((dataPoint) => (
+
+ {dataPoint}
+
+ ));
+}
+
+export default List;
+
+const styles = StyleSheet.create({
+ listItem: {
+ borderRadius: 6,
+ paddingHorizontal: 8,
+ paddingVertical: 4,
+ marginVertical: 4,
+ marginHorizontal: 12,
+ backgroundColor: '#e2b497'
+ },
+ itemText: {
+ color: '#351401',
+ textAlign: 'center'
+ }
+});
diff --git a/attachments/06-navigation/00-starting-project/components/MealDetail/Subtitle.js b/attachments/06-navigation/00-starting-project/components/MealDetail/Subtitle.js
new file mode 100644
index 00000000..4cdd87cb
--- /dev/null
+++ b/attachments/06-navigation/00-starting-project/components/MealDetail/Subtitle.js
@@ -0,0 +1,30 @@
+import React from 'react';
+import { Text } from 'react-native';
+import { StyleSheet } from 'react-native';
+import { View } from 'react-native';
+
+function Subtitle({ children }) {
+ return (
+
+ {children}
+
+ );
+}
+
+export default Subtitle;
+
+const styles = StyleSheet.create({
+ subTitle: {
+ color: '#e2b497',
+ fontSize: 18,
+ fontWeight: 'bold',
+ textAlign: 'center'
+ },
+ subTitleContainer: {
+ padding: 6,
+ marginHorizontal: 12,
+ marginVertical: 4,
+ borderBottomColor: '#e2b497',
+ borderBottomWidth: 2
+ }
+});
diff --git a/attachments/06-navigation/00-starting-project/components/MealDetails.js b/attachments/06-navigation/00-starting-project/components/MealDetails.js
new file mode 100644
index 00000000..48869a8d
--- /dev/null
+++ b/attachments/06-navigation/00-starting-project/components/MealDetails.js
@@ -0,0 +1,22 @@
+import { View, Text, StyleSheet } from 'react-native';
+
+function MealDetails({ duration, complexity, affordability, style, textStyle }) {
+ return (
+
+ {duration}
+ {complexity.toUpperCase()}
+ {affordability.toUpperCase()}
+
+ );
+}
+
+export default MealDetails;
+
+const styles = StyleSheet.create({
+ details: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ padding: 8
+ }
+});
diff --git a/attachments/06-navigation/00-starting-project/components/MealItem.js b/attachments/06-navigation/00-starting-project/components/MealItem.js
new file mode 100644
index 00000000..1f31c728
--- /dev/null
+++ b/attachments/06-navigation/00-starting-project/components/MealItem.js
@@ -0,0 +1,90 @@
+import { useNavigation } from '@react-navigation/native';
+import {
+ Text,
+ View,
+ Pressable,
+ Image,
+ StyleSheet,
+ Platform
+} from 'react-native';
+import MealDetails from './MealDetails';
+
+function MealItem({
+ id,
+ title,
+ imageUrl,
+ complexity,
+ duration,
+ affordability
+}) {
+ const navigation = useNavigation();
+
+ function selectMealItemHandler() {
+ navigation.navigate('MealDetail', { mealId: id });
+ }
+
+ return (
+
+ (pressed ? styles.buttonPressed : null)}
+ onPress={selectMealItemHandler}
+ >
+
+
+
+ {title}
+
+
+
+
+
+ );
+}
+
+export default MealItem;
+
+const styles = StyleSheet.create({
+ mealItem: {
+ margin: 16,
+ borderRadius: 8,
+ overflow: Platform.OS === 'android' ? 'hidden' : 'visible',
+ backgroundColor: 'white',
+ elevation: 4,
+ shadowColor: 'black',
+ shadowOpacity: 0.25,
+ shadowOffset: { width: 0, height: 2 },
+ shadowRadius: 8
+ },
+ buttonPressed: {
+ opacity: 0.5
+ },
+ innerContainer: {
+ borderRadius: 8,
+ overflow: 'hidden'
+ },
+ image: {
+ width: '100%',
+ height: 200
+ },
+ title: {
+ fontWeight: 'bold',
+ textAlign: 'center',
+ fontSize: 18,
+ margin: 8
+ },
+ details: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ padding: 8
+ },
+ detailItem: {
+ marginHorizontal: 4,
+ fontSize: 12
+ }
+});
diff --git a/attachments/06-navigation/00-starting-project/data/dummy-data.js b/attachments/06-navigation/00-starting-project/data/dummy-data.js
new file mode 100644
index 00000000..fa2ff3f5
--- /dev/null
+++ b/attachments/06-navigation/00-starting-project/data/dummy-data.js
@@ -0,0 +1,337 @@
+import Category from '../models/category';
+import Meal from '../models/meal';
+
+export const CATEGORIES = [
+ new Category('c1', 'Italian', '#f5428d'),
+ new Category('c2', 'Quick & Easy', '#f54242'),
+ new Category('c3', 'Hamburgers', '#f5a442'),
+ new Category('c4', 'German', '#f5d142'),
+ new Category('c5', 'Light & Lovely', '#368dff'),
+ new Category('c6', 'Exotic', '#41d95d'),
+ new Category('c7', 'Breakfast', '#9eecff'),
+ new Category('c8', 'Asian', '#b9ffb0'),
+ new Category('c9', 'French', '#ffc7ff'),
+ new Category('c10', 'Summer', '#47fced')
+];
+
+export const MEALS = [
+ new Meal(
+ 'm1',
+ ['c1', 'c2'],
+ 'Spaghetti with Tomato Sauce',
+ 'affordable',
+ 'simple',
+ 'https://upload.wikimedia.org/wikipedia/commons/thumb/2/20/Spaghetti_Bolognese_mit_Parmesan_oder_Grana_Padano.jpg/800px-Spaghetti_Bolognese_mit_Parmesan_oder_Grana_Padano.jpg',
+ 20,
+ [
+ '4 Tomatoes',
+ '1 Tablespoon of Olive Oil',
+ '1 Onion',
+ '250g Spaghetti',
+ 'Spices',
+ 'Cheese (optional)'
+ ],
+ [
+ 'Cut the tomatoes and the onion into small pieces.',
+ 'Boil some water - add salt to it once it boils.',
+ 'Put the spaghetti into the boiling water - they should be done in about 10 to 12 minutes.',
+ 'In the meantime, heaten up some olive oil and add the cut onion.',
+ 'After 2 minutes, add the tomato pieces, salt, pepper and your other spices.',
+ 'The sauce will be done once the spaghetti are.',
+ 'Feel free to add some cheese on top of the finished dish.'
+ ],
+ false,
+ true,
+ true,
+ true
+ ),
+
+ new Meal(
+ 'm2',
+ ['c2'],
+ 'Toast Hawaii',
+ 'affordable',
+ 'simple',
+ 'https://cdn.pixabay.com/photo/2018/07/11/21/51/toast-3532016_1280.jpg',
+ 10,
+ [
+ '1 Slice White Bread',
+ '1 Slice Ham',
+ '1 Slice Pineapple',
+ '1-2 Slices of Cheese',
+ 'Butter'
+ ],
+ [
+ 'Butter one side of the white bread',
+ 'Layer ham, the pineapple and cheese on the white bread',
+ 'Bake the toast for round about 10 minutes in the oven at 200°C'
+ ],
+ false,
+ false,
+ false,
+ false
+ ),
+
+ new Meal(
+ 'm3',
+ ['c3'],
+ 'Classic Hamburger',
+ 'pricey',
+ 'simple',
+ 'https://cdn.pixabay.com/photo/2014/10/23/18/05/burger-500054_1280.jpg',
+ 45,
+ [
+ '300g Cattle Hack',
+ '1 Tomato',
+ '1 Cucumber',
+ '1 Onion',
+ 'Ketchup',
+ '2 Burger Buns'
+ ],
+ [
+ 'Form 2 patties',
+ 'Fry the patties for c. 4 minutes on each side',
+ 'Quickly fry the buns for c. 1 minute on each side',
+ 'Bruch buns with ketchup',
+ 'Serve burger with tomato, cucumber and onion'
+ ],
+ false,
+ false,
+ false,
+ true
+ ),
+
+ new Meal(
+ 'm4',
+ ['c4'],
+ 'Wiener Schnitzel',
+ 'luxurious',
+ 'challenging',
+ 'https://cdn.pixabay.com/photo/2018/03/31/19/29/schnitzel-3279045_1280.jpg',
+ 60,
+ [
+ '8 Veal Cutlets',
+ '4 Eggs',
+ '200g Bread Crumbs',
+ '100g Flour',
+ '300ml Butter',
+ '100g Vegetable Oil',
+ 'Salt',
+ 'Lemon Slices'
+ ],
+ [
+ 'Tenderize the veal to about 2–4mm, and salt on both sides.',
+ 'On a flat plate, stir the eggs briefly with a fork.',
+ 'Lightly coat the cutlets in flour then dip into the egg, and finally, coat in breadcrumbs.',
+ 'Heat the butter and oil in a large pan (allow the fat to get very hot) and fry the schnitzels until golden brown on both sides.',
+ 'Make sure to toss the pan regularly so that the schnitzels are surrounded by oil and the crumbing becomes ‘fluffy’.',
+ 'Remove, and drain on kitchen paper. Fry the parsley in the remaining oil and drain.',
+ 'Place the schnitzels on awarmed plate and serve garnishedwith parsley and slices of lemon.'
+ ],
+ false,
+ false,
+ false,
+ false
+ ),
+
+ new Meal(
+ 'm5',
+ ['c2', 'c5', 'c10'],
+ 'Salad with Smoked Salmon',
+ 'luxurious',
+ 'simple',
+ 'https://cdn.pixabay.com/photo/2016/10/25/13/29/smoked-salmon-salad-1768890_1280.jpg',
+ 15,
+ [
+ 'Arugula',
+ "Lamb's Lettuce",
+ 'Parsley',
+ 'Fennel',
+ '200g Smoked Salmon',
+ 'Mustard',
+ 'Balsamic Vinegar',
+ 'Olive Oil',
+ 'Salt and Pepper'
+ ],
+ [
+ 'Wash and cut salad and herbs',
+ 'Dice the salmon',
+ 'Process mustard, vinegar and olive oil into a dessing',
+ 'Prepare the salad',
+ 'Add salmon cubes and dressing'
+ ],
+ true,
+ false,
+ true,
+ true
+ ),
+
+ new Meal(
+ 'm6',
+ ['c6', 'c10'],
+ 'Delicious Orange Mousse',
+ 'affordable',
+ 'hard',
+ 'https://cdn.pixabay.com/photo/2017/05/01/05/18/pastry-2274750_1280.jpg',
+ 240,
+ [
+ '4 Sheets of Gelatine',
+ '150ml Orange Juice',
+ '80g Sugar',
+ '300g Yoghurt',
+ '200g Cream',
+ 'Orange Peel'
+ ],
+ [
+ 'Dissolve gelatine in pot',
+ 'Add orange juice and sugar',
+ 'Take pot off the stove',
+ 'Add 2 tablespoons of yoghurt',
+ 'Stir gelatin under remaining yoghurt',
+ 'Cool everything down in the refrigerator',
+ 'Whip the cream and lift it under die orange mass',
+ 'Cool down again for at least 4 hours',
+ 'Serve with orange peel'
+ ],
+ true,
+ false,
+ true,
+ false
+ ),
+
+ new Meal(
+ 'm7',
+ ['c7'],
+ 'Pancakes',
+ 'affordable',
+ 'simple',
+ 'https://cdn.pixabay.com/photo/2018/07/10/21/23/pancake-3529653_1280.jpg',
+ 20,
+ [
+ '1 1/2 Cups all-purpose Flour',
+ '3 1/2 Teaspoons Baking Powder',
+ '1 Teaspoon Salt',
+ '1 Tablespoon White Sugar',
+ '1 1/4 cups Milk',
+ '1 Egg',
+ '3 Tablespoons Butter, melted'
+ ],
+ [
+ 'In a large bowl, sift together the flour, baking powder, salt and sugar.',
+ 'Make a well in the center and pour in the milk, egg and melted butter; mix until smooth.',
+ 'Heat a lightly oiled griddle or frying pan over medium high heat.',
+ 'Pour or scoop the batter onto the griddle, using approximately 1/4 cup for each pancake. Brown on both sides and serve hot.'
+ ],
+ true,
+ false,
+ true,
+ false
+ ),
+
+ new Meal(
+ 'm8',
+ ['c8'],
+ 'Creamy Indian Chicken Curry',
+ 'pricey',
+ 'challenging',
+ 'https://cdn.pixabay.com/photo/2018/06/18/16/05/indian-food-3482749_1280.jpg',
+ 35,
+ [
+ '4 Chicken Breasts',
+ '1 Onion',
+ '2 Cloves of Garlic',
+ '1 Piece of Ginger',
+ '4 Tablespoons Almonds',
+ '1 Teaspoon Cayenne Pepper',
+ '500ml Coconut Milk'
+ ],
+ [
+ 'Slice and fry the chicken breast',
+ 'Process onion, garlic and ginger into paste and sauté everything',
+ 'Add spices and stir fry',
+ 'Add chicken breast + 250ml of water and cook everything for 10 minutes',
+ 'Add coconut milk',
+ 'Serve with rice'
+ ],
+ true,
+ false,
+ false,
+ true
+ ),
+
+ new Meal(
+ 'm9',
+ ['c9'],
+ 'Chocolate Souffle',
+ 'affordable',
+ 'hard',
+ 'https://cdn.pixabay.com/photo/2014/08/07/21/07/souffle-412785_1280.jpg',
+ 45,
+ [
+ '1 Teaspoon melted Butter',
+ '2 Tablespoons white Sugar',
+ '2 Ounces 70% dark Chocolate, broken into pieces',
+ '1 Tablespoon Butter',
+ '1 Tablespoon all-purpose Flour',
+ '4 1/3 tablespoons cold Milk',
+ '1 Pinch Salt',
+ '1 Pinch Cayenne Pepper',
+ '1 Large Egg Yolk',
+ '2 Large Egg Whites',
+ '1 Pinch Cream of Tartar',
+ '1 Tablespoon white Sugar'
+ ],
+ [
+ 'Preheat oven to 190°C. Line a rimmed baking sheet with parchment paper.',
+ 'Brush bottom and sides of 2 ramekins lightly with 1 teaspoon melted butter; cover bottom and sides right up to the rim.',
+ 'Add 1 tablespoon white sugar to ramekins. Rotate ramekins until sugar coats all surfaces.',
+ 'Place chocolate pieces in a metal mixing bowl.',
+ 'Place bowl over a pan of about 3 cups hot water over low heat.',
+ 'Melt 1 tablespoon butter in a skillet over medium heat. Sprinkle in flour. Whisk until flour is incorporated into butter and mixture thickens.',
+ 'Whisk in cold milk until mixture becomes smooth and thickens. Transfer mixture to bowl with melted chocolate.',
+ 'Add salt and cayenne pepper. Mix together thoroughly. Add egg yolk and mix to combine.',
+ 'Leave bowl above the hot (not simmering) water to keep chocolate warm while you whip the egg whites.',
+ 'Place 2 egg whites in a mixing bowl; add cream of tartar. Whisk until mixture begins to thicken and a drizzle from the whisk stays on the surface about 1 second before disappearing into the mix.',
+ 'Add 1/3 of sugar and whisk in. Whisk in a bit more sugar about 15 seconds.',
+ 'whisk in the rest of the sugar. Continue whisking until mixture is about as thick as shaving cream and holds soft peaks, 3 to 5 minutes.',
+ 'Transfer a little less than half of egg whites to chocolate.',
+ 'Mix until egg whites are thoroughly incorporated into the chocolate.',
+ 'Add the rest of the egg whites; gently fold into the chocolate with a spatula, lifting from the bottom and folding over.',
+ 'Stop mixing after the egg white disappears. Divide mixture between 2 prepared ramekins. Place ramekins on prepared baking sheet.',
+ 'Bake in preheated oven until scuffles are puffed and have risen above the top of the rims, 12 to 15 minutes.'
+ ],
+ true,
+ false,
+ true,
+ false
+ ),
+ new Meal(
+ 'm10',
+ ['c2', 'c5', 'c10'],
+ 'Asparagus Salad with Cherry Tomatoes',
+ 'luxurious',
+ 'simple',
+ 'https://cdn.pixabay.com/photo/2018/04/09/18/26/asparagus-3304997_1280.jpg',
+ 30,
+ [
+ 'White and Green Asparagus',
+ '30g Pine Nuts',
+ '300g Cherry Tomatoes',
+ 'Salad',
+ 'Salt, Pepper and Olive Oil'
+ ],
+ [
+ 'Wash, peel and cut the asparagus',
+ 'Cook in salted water',
+ 'Salt and pepper the asparagus',
+ 'Roast the pine nuts',
+ 'Halve the tomatoes',
+ 'Mix with asparagus, salad and dressing',
+ 'Serve with Baguette'
+ ],
+ true,
+ true,
+ true,
+ true
+ )
+];
diff --git a/attachments/06-navigation/00-starting-project/models/category.js b/attachments/06-navigation/00-starting-project/models/category.js
new file mode 100755
index 00000000..17607202
--- /dev/null
+++ b/attachments/06-navigation/00-starting-project/models/category.js
@@ -0,0 +1,9 @@
+class Category {
+ constructor(id, title, color) {
+ this.id = id;
+ this.title = title;
+ this.color = color;
+ }
+}
+
+export default Category;
diff --git a/attachments/06-navigation/00-starting-project/models/meal.js b/attachments/06-navigation/00-starting-project/models/meal.js
new file mode 100755
index 00000000..48cae8dc
--- /dev/null
+++ b/attachments/06-navigation/00-starting-project/models/meal.js
@@ -0,0 +1,33 @@
+class Meal {
+ constructor(
+ id,
+ categoryIds,
+ title,
+ affordability,
+ complexity,
+ imageUrl,
+ duration,
+ ingredients,
+ steps,
+ isGlutenFree,
+ isVegan,
+ isVegetarian,
+ isLactoseFree
+ ) {
+ this.id = id;
+ this.categoryIds = categoryIds;
+ this.title = title;
+ this.imageUrl = imageUrl;
+ this.ingredients = ingredients;
+ this.steps = steps;
+ this.duration = duration;
+ this.complexity = complexity;
+ this.affordability = affordability;
+ this.isGlutenFree = isGlutenFree;
+ this.isVegan = isVegan;
+ this.isVegetarian = isVegetarian;
+ this.isLactoseFree = isLactoseFree;
+ }
+}
+
+export default Meal;
diff --git a/attachments/06-navigation/00-starting-project/package.json b/attachments/06-navigation/00-starting-project/package.json
new file mode 100644
index 00000000..97dd6a78
--- /dev/null
+++ b/attachments/06-navigation/00-starting-project/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "rncourse",
+ "version": "1.0.0",
+ "main": "node_modules/expo/AppEntry.js",
+ "scripts": {
+ "start": "expo start",
+ "android": "expo start --android",
+ "ios": "expo start --ios",
+ "web": "expo start --web",
+ "eject": "expo eject"
+ },
+ "dependencies": {
+ "@react-navigation/drawer": "^6.7.2",
+ "@react-navigation/native": "^6.1.18",
+ "@react-navigation/native-stack": "^6.11.0",
+ "expo": "^52.0.0",
+ "expo-status-bar": "~2.0.1",
+ "react": "18.3.1",
+ "react-dom": "18.3.1",
+ "react-native": "0.76.7",
+ "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.4.0",
+ "react-native-web": "~0.19.13"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.25.2"
+ },
+ "private": true
+}
diff --git a/attachments/06-navigation/00-starting-project/screens/CategoriesScreen.js b/attachments/06-navigation/00-starting-project/screens/CategoriesScreen.js
new file mode 100644
index 00000000..c8e11e61
--- /dev/null
+++ b/attachments/06-navigation/00-starting-project/screens/CategoriesScreen.js
@@ -0,0 +1,29 @@
+import { FlatList } from 'react-native';
+import { CATEGORIES } from '../data/dummy-data';
+import CategoryGridTile from '../components/CategoryGridTile';
+
+function CategoriesScreen({ navigation }) {
+ function renderCategoryItem(itemData) {
+ function pressHandler() {
+ navigation.navigate('MealsOverview', { categoryId: itemData.item.id });
+ }
+ return (
+
+ );
+ }
+
+ return (
+ item.id}
+ renderItem={renderCategoryItem}
+ numColumns={2}
+ >
+ );
+}
+
+export default CategoriesScreen;
diff --git a/attachments/06-navigation/00-starting-project/screens/FavoritesScreen.js b/attachments/06-navigation/00-starting-project/screens/FavoritesScreen.js
new file mode 100644
index 00000000..ba8fd7ed
--- /dev/null
+++ b/attachments/06-navigation/00-starting-project/screens/FavoritesScreen.js
@@ -0,0 +1,8 @@
+import { Text } from 'react-native';
+import { View } from 'react-native';
+
+const FavoritesScreen = () => {
+ return The Favorites screen!
+};
+
+export default FavoritesScreen;
diff --git a/attachments/06-navigation/00-starting-project/screens/MealDetailScreen.js b/attachments/06-navigation/00-starting-project/screens/MealDetailScreen.js
new file mode 100644
index 00000000..308c722e
--- /dev/null
+++ b/attachments/06-navigation/00-starting-project/screens/MealDetailScreen.js
@@ -0,0 +1,83 @@
+import {
+ Button,
+ Text,
+ View,
+ Image,
+ ScrollView,
+ StyleSheet
+} from 'react-native';
+import { MEALS } from '../data/dummy-data';
+import MealDetails from '../components/MealDetails';
+import Subtitle from '../components/MealDetail/Subtitle';
+import List from '../components/MealDetail/List';
+import { useLayoutEffect } from 'react';
+import IconButton from '../components/IconButton';
+
+function MealDetailScreen({ route, navigation }) {
+ const mealId = route.params.mealId;
+
+ const selectedMeal = MEALS.find((meal) => meal.id === mealId);
+
+ function headerButtonPressHandler() {
+ console.log('Pressed!');
+ }
+
+ useLayoutEffect(() => {
+ navigation.setOptions({
+ headerRight: () => {
+ return ;
+ }
+ });
+ }, [navigation, headerButtonPressHandler]);
+
+ return (
+
+
+ {selectedMeal.title}
+
+
+
+
+ Ingredients
+
+
+ Steps
+
+
+
+
+ );
+}
+
+export default MealDetailScreen;
+
+const styles = StyleSheet.create({
+ rootContainer: {
+ marginBottom: 32
+ },
+ image: {
+ width: '100%',
+ height: 350
+ },
+ title: {
+ fontWeight: 'bold',
+ fontSize: 24,
+ margin: 8,
+ textAlign: 'center',
+ color: 'white'
+ },
+ detailText: {
+ color: 'white'
+ },
+ listOuterContainer: {
+ alignItems: 'center'
+ },
+ listContainer: {
+ width: '80%'
+ }
+});
diff --git a/attachments/06-navigation/00-starting-project/screens/MealsOverviewScreen.js b/attachments/06-navigation/00-starting-project/screens/MealsOverviewScreen.js
new file mode 100644
index 00000000..6e035b88
--- /dev/null
+++ b/attachments/06-navigation/00-starting-project/screens/MealsOverviewScreen.js
@@ -0,0 +1,57 @@
+import { View, StyleSheet, FlatList } from 'react-native';
+import { MEALS, CATEGORIES } from '../data/dummy-data';
+
+import MealItem from '../components/MealItem';
+import { useLayoutEffect } from 'react';
+// We kunnen ook een hook gebruiken ipv de via de properties
+// import { useRoute } from '@react-navigation/native';
+
+// MealsOverviewScreen is geregistreerd als een scherm en krijgt daarom automatisch een navigation en route property
+function MealsOverviewScreen({ route, navigation }) {
+ const catId = route.params.categoryId;
+
+ const displayedMeals = MEALS.filter((mealItem) => {
+ return mealItem.categoryIds.indexOf(catId) >= 0;
+ });
+
+ useLayoutEffect(() => {
+ const categoryTitle = CATEGORIES.find(
+ (category) => category.id === catId
+ ).title;
+ // To set the options from within this screen we should use the useEffect hook!
+ navigation.setOptions({ title: categoryTitle });
+ }, [catId, navigation]);
+
+ function renderMealItem(itemData) {
+ const item = itemData.item;
+
+ const mealItemProps = {
+ id: item.id,
+ title: item.title,
+ imageUrl: itemData.item.imageUrl,
+ affordability: item.affordability,
+ complexity: item.complexity,
+ duration: item.duration
+ };
+ return ;
+ }
+
+ return (
+
+ item.id}
+ renderItem={renderMealItem}
+ />
+
+ );
+}
+
+export default MealsOverviewScreen;
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ padding: 16
+ }
+});
diff --git a/attachments/06-navigation/11-other-navigators-starting-code/App.js b/attachments/06-navigation/11-other-navigators-starting-code/App.js
new file mode 100644
index 00000000..ea6961bf
--- /dev/null
+++ b/attachments/06-navigation/11-other-navigators-starting-code/App.js
@@ -0,0 +1,77 @@
+// import { createDrawerNavigator } from '@react-navigation/drawer';
+import { NavigationContainer } from '@react-navigation/native';
+import WelcomeScreen from './screens/WelcomeScreen';
+import UserScreen from './screens/UserScreen';
+import { Ionicons } from '@expo/vector-icons';
+import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
+
+// const Drawer = createDrawerNavigator();
+
+const BottomTab = createBottomTabNavigator();
+
+export default function App() {
+ return (
+ // NAVIGATION USING DRAWER
+ //
+ // {/* Via the initialRouteName="User" param we can set the screen that should be shown initially instead of it using the order from top to bottom as we define it eg below */}
+ //
+ // (
+ //
+ // )
+ // }}
+ // />
+ // (
+ //
+ // )
+ // }}
+ // />
+ //
+ //
+
+
+
+ (
+
+ )
+ }}
+ />
+ (
+
+ )
+ }}
+ />
+
+
+ );
+}
diff --git a/attachments/06-navigation/11-other-navigators-starting-code/app.json b/attachments/06-navigation/11-other-navigators-starting-code/app.json
new file mode 100644
index 00000000..9a1223e7
--- /dev/null
+++ b/attachments/06-navigation/11-other-navigators-starting-code/app.json
@@ -0,0 +1,32 @@
+{
+ "expo": {
+ "name": "RNCourse",
+ "slug": "RNCourse",
+ "version": "1.0.0",
+ "orientation": "portrait",
+ "icon": "./assets/icon.png",
+ "splash": {
+ "image": "./assets/splash.png",
+ "resizeMode": "contain",
+ "backgroundColor": "#ffffff"
+ },
+ "updates": {
+ "fallbackToCacheTimeout": 0
+ },
+ "assetBundlePatterns": [
+ "**/*"
+ ],
+ "ios": {
+ "supportsTablet": true
+ },
+ "android": {
+ "adaptiveIcon": {
+ "foregroundImage": "./assets/adaptive-icon.png",
+ "backgroundColor": "#FFFFFF"
+ }
+ },
+ "web": {
+ "favicon": "./assets/favicon.png"
+ }
+ }
+}
diff --git a/attachments/06-navigation/11-other-navigators-starting-code/assets/adaptive-icon.png b/attachments/06-navigation/11-other-navigators-starting-code/assets/adaptive-icon.png
new file mode 100644
index 00000000..03d6f6b6
Binary files /dev/null and b/attachments/06-navigation/11-other-navigators-starting-code/assets/adaptive-icon.png differ
diff --git a/attachments/06-navigation/11-other-navigators-starting-code/assets/favicon.png b/attachments/06-navigation/11-other-navigators-starting-code/assets/favicon.png
new file mode 100644
index 00000000..e75f697b
Binary files /dev/null and b/attachments/06-navigation/11-other-navigators-starting-code/assets/favicon.png differ
diff --git a/attachments/06-navigation/11-other-navigators-starting-code/assets/icon.png b/attachments/06-navigation/11-other-navigators-starting-code/assets/icon.png
new file mode 100644
index 00000000..a0b1526f
Binary files /dev/null and b/attachments/06-navigation/11-other-navigators-starting-code/assets/icon.png differ
diff --git a/attachments/06-navigation/11-other-navigators-starting-code/assets/splash.png b/attachments/06-navigation/11-other-navigators-starting-code/assets/splash.png
new file mode 100644
index 00000000..0e89705a
Binary files /dev/null and b/attachments/06-navigation/11-other-navigators-starting-code/assets/splash.png differ
diff --git a/attachments/06-navigation/11-other-navigators-starting-code/babel.config.js b/attachments/06-navigation/11-other-navigators-starting-code/babel.config.js
new file mode 100644
index 00000000..2900afe9
--- /dev/null
+++ b/attachments/06-navigation/11-other-navigators-starting-code/babel.config.js
@@ -0,0 +1,6 @@
+module.exports = function(api) {
+ api.cache(true);
+ return {
+ presets: ['babel-preset-expo'],
+ };
+};
diff --git a/attachments/06-navigation/11-other-navigators-starting-code/package.json b/attachments/06-navigation/11-other-navigators-starting-code/package.json
new file mode 100644
index 00000000..307a1eee
--- /dev/null
+++ b/attachments/06-navigation/11-other-navigators-starting-code/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "rncourse",
+ "version": "1.0.0",
+ "main": "node_modules/expo/AppEntry.js",
+ "scripts": {
+ "start": "expo start",
+ "android": "expo start --android",
+ "ios": "expo start --ios",
+ "web": "expo start --web",
+ "eject": "expo eject"
+ },
+ "dependencies": {
+ "@react-navigation/bottom-tabs": "^6.6.1",
+ "@react-navigation/drawer": "^6.7.2",
+ "@react-navigation/native": "^6.0.8",
+ "@react-navigation/native-stack": "^6.5.0",
+ "expo": "^52.0.0",
+ "expo-status-bar": "~2.0.1",
+ "react": "18.3.1",
+ "react-dom": "18.3.1",
+ "react-native": "0.76.7",
+ "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.4.0",
+ "react-native-web": "~0.19.13"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.25.2"
+ },
+ "private": true
+}
diff --git a/attachments/06-navigation/11-other-navigators-starting-code/screens/UserScreen.js b/attachments/06-navigation/11-other-navigators-starting-code/screens/UserScreen.js
new file mode 100644
index 00000000..bce9bf3b
--- /dev/null
+++ b/attachments/06-navigation/11-other-navigators-starting-code/screens/UserScreen.js
@@ -0,0 +1,31 @@
+import { View, Text, Button, StyleSheet } from 'react-native';
+
+function UserScreen({ route, navigation }) {
+ function openDrawerHandler() {
+ // opens the drawer programmatically, this is an extra method when using the Drawer instead of the stack
+ navigation.toggleDrawer();
+ }
+
+ return (
+
+
+ This is the "User" screen!
+
+
+
+ );
+}
+
+export default UserScreen;
+
+const styles = StyleSheet.create({
+ rootContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center'
+ },
+ highlight: {
+ fontWeight: 'bold',
+ color: '#eb1064'
+ }
+});
diff --git a/attachments/06-navigation/11-other-navigators-starting-code/screens/WelcomeScreen.js b/attachments/06-navigation/11-other-navigators-starting-code/screens/WelcomeScreen.js
new file mode 100644
index 00000000..1ad6e932
--- /dev/null
+++ b/attachments/06-navigation/11-other-navigators-starting-code/screens/WelcomeScreen.js
@@ -0,0 +1,25 @@
+import { View, Text, StyleSheet } from 'react-native';
+
+function WelcomeScreen() {
+ return (
+
+
+ This is the "Welcome" screen!
+
+
+ );
+}
+
+export default WelcomeScreen;
+
+const styles = StyleSheet.create({
+ rootContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ highlight: {
+ fontWeight: 'bold',
+ color: '#eb1064',
+ },
+});
diff --git a/attachments/07-redux-context/00-starting-project/App.js b/attachments/07-redux-context/00-starting-project/App.js
new file mode 100644
index 00000000..7e6855e7
--- /dev/null
+++ b/attachments/07-redux-context/00-starting-project/App.js
@@ -0,0 +1,97 @@
+import { StatusBar } from 'expo-status-bar';
+import { StyleSheet, Button } from 'react-native';
+import { NavigationContainer } from '@react-navigation/native';
+import { createNativeStackNavigator } from '@react-navigation/native-stack';
+import { createDrawerNavigator } from '@react-navigation/drawer';
+import { Ionicons } from '@expo/vector-icons';
+
+import CategoriesScreen from './screens/CategoriesScreen';
+import MealsOverviewScreen from './screens/MealsOverviewScreen';
+import MealDetailScreen from './screens/MealDetailScreen';
+import FavoritesScreen from './screens/FavoritesScreen';
+// import FavoritesContextProvider from './store/context/favorites-context';
+import { Provider } from 'react-redux';
+import { store } from './store/redux/store';
+
+const Stack = createNativeStackNavigator();
+const Drawer = createDrawerNavigator();
+
+function DrawerNavigator() {
+ return (
+
+ (
+
+ )
+ }}
+ />
+ (
+
+ )
+ }}
+ />
+
+ );
+}
+
+export default function App() {
+ return (
+ <>
+
+ {/* */}
+
+
+
+
+
+
+
+
+
+ {/* */}
+ >
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {}
+});
diff --git a/attachments/07-redux-context/00-starting-project/app.json b/attachments/07-redux-context/00-starting-project/app.json
new file mode 100644
index 00000000..6d7b5ecb
--- /dev/null
+++ b/attachments/07-redux-context/00-starting-project/app.json
@@ -0,0 +1,33 @@
+{
+ "expo": {
+ "name": "RNCourse",
+ "slug": "RNCourse",
+ "version": "1.0.0",
+ "orientation": "portrait",
+ "icon": "./assets/icon.png",
+ "backgroundColor": "#24180f",
+ "splash": {
+ "image": "./assets/splash.png",
+ "resizeMode": "contain",
+ "backgroundColor": "#ffffff"
+ },
+ "updates": {
+ "fallbackToCacheTimeout": 0
+ },
+ "assetBundlePatterns": [
+ "**/*"
+ ],
+ "ios": {
+ "supportsTablet": true
+ },
+ "android": {
+ "adaptiveIcon": {
+ "foregroundImage": "./assets/adaptive-icon.png",
+ "backgroundColor": "#FFFFFF"
+ }
+ },
+ "web": {
+ "favicon": "./assets/favicon.png"
+ }
+ }
+}
diff --git a/attachments/07-redux-context/00-starting-project/assets/adaptive-icon.png b/attachments/07-redux-context/00-starting-project/assets/adaptive-icon.png
new file mode 100644
index 00000000..03d6f6b6
Binary files /dev/null and b/attachments/07-redux-context/00-starting-project/assets/adaptive-icon.png differ
diff --git a/attachments/07-redux-context/00-starting-project/assets/favicon.png b/attachments/07-redux-context/00-starting-project/assets/favicon.png
new file mode 100644
index 00000000..e75f697b
Binary files /dev/null and b/attachments/07-redux-context/00-starting-project/assets/favicon.png differ
diff --git a/attachments/07-redux-context/00-starting-project/assets/icon.png b/attachments/07-redux-context/00-starting-project/assets/icon.png
new file mode 100644
index 00000000..a0b1526f
Binary files /dev/null and b/attachments/07-redux-context/00-starting-project/assets/icon.png differ
diff --git a/attachments/07-redux-context/00-starting-project/assets/splash.png b/attachments/07-redux-context/00-starting-project/assets/splash.png
new file mode 100644
index 00000000..0e89705a
Binary files /dev/null and b/attachments/07-redux-context/00-starting-project/assets/splash.png differ
diff --git a/attachments/07-redux-context/00-starting-project/babel.config.js b/attachments/07-redux-context/00-starting-project/babel.config.js
new file mode 100644
index 00000000..2900afe9
--- /dev/null
+++ b/attachments/07-redux-context/00-starting-project/babel.config.js
@@ -0,0 +1,6 @@
+module.exports = function(api) {
+ api.cache(true);
+ return {
+ presets: ['babel-preset-expo'],
+ };
+};
diff --git a/attachments/07-redux-context/00-starting-project/components/CategoryGridTile.js b/attachments/07-redux-context/00-starting-project/components/CategoryGridTile.js
new file mode 100644
index 00000000..8ffd864d
--- /dev/null
+++ b/attachments/07-redux-context/00-starting-project/components/CategoryGridTile.js
@@ -0,0 +1,55 @@
+import { Pressable, View, Text, StyleSheet, Platform } from 'react-native';
+
+function CategoryGridTile({ title, color, onPress }) {
+ return (
+
+ [
+ styles.button,
+ pressed ? styles.buttonPressed : null,
+ ]}
+ onPress={onPress}
+ >
+
+ {title}
+
+
+
+ );
+}
+
+export default CategoryGridTile;
+
+const styles = StyleSheet.create({
+ gridItem: {
+ flex: 1,
+ margin: 16,
+ height: 150,
+ borderRadius: 8,
+ elevation: 4,
+ backgroundColor: 'white',
+ shadowColor: 'black',
+ shadowOpacity: 0.25,
+ shadowOffset: { width: 0, height: 2 },
+ shadowRadius: 8,
+ overflow: Platform.OS === 'android' ? 'hidden' : 'visible',
+ },
+ button: {
+ flex: 1,
+ },
+ buttonPressed: {
+ opacity: 0.5,
+ },
+ innerContainer: {
+ flex: 1,
+ padding: 16,
+ borderRadius: 8,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ title: {
+ fontWeight: 'bold',
+ fontSize: 18,
+ },
+});
diff --git a/attachments/07-redux-context/00-starting-project/components/IconButton.js b/attachments/07-redux-context/00-starting-project/components/IconButton.js
new file mode 100644
index 00000000..9b830e43
--- /dev/null
+++ b/attachments/07-redux-context/00-starting-project/components/IconButton.js
@@ -0,0 +1,21 @@
+import { Pressable, StyleSheet } from 'react-native';
+import { Ionicons } from '@expo/vector-icons';
+
+function IconButton({ icon, color, onPress }) {
+ return (
+ pressed && styles.pressed}
+ >
+
+
+ );
+}
+
+export default IconButton;
+
+const styles = StyleSheet.create({
+ pressed: {
+ opacity: 0.7,
+ },
+});
diff --git a/attachments/07-redux-context/00-starting-project/components/MealDetail/List.js b/attachments/07-redux-context/00-starting-project/components/MealDetail/List.js
new file mode 100644
index 00000000..af21d6c8
--- /dev/null
+++ b/attachments/07-redux-context/00-starting-project/components/MealDetail/List.js
@@ -0,0 +1,26 @@
+import { View, Text, StyleSheet } from 'react-native';
+
+function List({ data }) {
+ return data.map((dataPoint) => (
+
+ {dataPoint}
+
+ ));
+}
+
+export default List;
+
+const styles = StyleSheet.create({
+ listItem: {
+ borderRadius: 6,
+ paddingHorizontal: 8,
+ paddingVertical: 4,
+ marginVertical: 4,
+ marginHorizontal: 12,
+ backgroundColor: '#e2b497',
+ },
+ itemText: {
+ color: '#351401',
+ textAlign: 'center',
+ },
+});
diff --git a/attachments/07-redux-context/00-starting-project/components/MealDetail/Subtitle.js b/attachments/07-redux-context/00-starting-project/components/MealDetail/Subtitle.js
new file mode 100644
index 00000000..c69f53d1
--- /dev/null
+++ b/attachments/07-redux-context/00-starting-project/components/MealDetail/Subtitle.js
@@ -0,0 +1,27 @@
+import { View, Text, StyleSheet } from 'react-native';
+
+function Subtitle({children}) {
+ return (
+
+ {children}
+
+ );
+}
+
+export default Subtitle;
+
+const styles = StyleSheet.create({
+ subtitle: {
+ color: '#e2b497',
+ fontSize: 18,
+ fontWeight: 'bold',
+ textAlign: 'center',
+ },
+ subtitleContainer: {
+ padding: 6,
+ marginHorizontal: 12,
+ marginVertical: 4,
+ borderBottomColor: '#e2b497',
+ borderBottomWidth: 2,
+ },
+});
diff --git a/attachments/07-redux-context/00-starting-project/components/MealDetails.js b/attachments/07-redux-context/00-starting-project/components/MealDetails.js
new file mode 100644
index 00000000..2acdfab0
--- /dev/null
+++ b/attachments/07-redux-context/00-starting-project/components/MealDetails.js
@@ -0,0 +1,36 @@
+import { View, Text, StyleSheet } from 'react-native';
+
+function MealDetails({
+ duration,
+ complexity,
+ affordability,
+ style,
+ textStyle,
+}) {
+ return (
+
+ {duration}m
+
+ {complexity.toUpperCase()}
+
+
+ {affordability.toUpperCase()}
+
+
+ );
+}
+
+export default MealDetails;
+
+const styles = StyleSheet.create({
+ details: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ padding: 8,
+ },
+ detailItem: {
+ marginHorizontal: 4,
+ fontSize: 12,
+ },
+});
diff --git a/attachments/07-redux-context/00-starting-project/components/MealItem.js b/attachments/07-redux-context/00-starting-project/components/MealItem.js
new file mode 100644
index 00000000..36c137ea
--- /dev/null
+++ b/attachments/07-redux-context/00-starting-project/components/MealItem.js
@@ -0,0 +1,83 @@
+import {
+ View,
+ Pressable,
+ Text,
+ Image,
+ StyleSheet,
+ Platform,
+} from 'react-native';
+import { useNavigation } from '@react-navigation/native';
+
+import MealDetails from './MealDetails';
+
+function MealItem({
+ id,
+ title,
+ imageUrl,
+ duration,
+ complexity,
+ affordability,
+}) {
+ const navigation = useNavigation();
+
+ function selectMealItemHandler() {
+ navigation.navigate('MealDetail', {
+ mealId: id,
+ });
+ }
+
+ return (
+
+ (pressed ? styles.buttonPressed : null)}
+ onPress={selectMealItemHandler}
+ >
+
+
+
+ {title}
+
+
+
+
+
+ );
+}
+
+export default MealItem;
+
+const styles = StyleSheet.create({
+ mealItem: {
+ margin: 16,
+ borderRadius: 8,
+ overflow: Platform.OS === 'android' ? 'hidden' : 'visible',
+ backgroundColor: 'white',
+ elevation: 4,
+ shadowColor: 'black',
+ shadowOpacity: 0.25,
+ shadowOffset: { width: 0, height: 2 },
+ shadowRadius: 8,
+ },
+ buttonPressed: {
+ opacity: 0.5,
+ },
+ innerContainer: {
+ borderRadius: 8,
+ overflow: 'hidden',
+ },
+ image: {
+ width: '100%',
+ height: 200,
+ },
+ title: {
+ fontWeight: 'bold',
+ textAlign: 'center',
+ fontSize: 18,
+ margin: 8,
+ },
+});
diff --git a/attachments/07-redux-context/00-starting-project/components/MealstList/MealsList.js b/attachments/07-redux-context/00-starting-project/components/MealstList/MealsList.js
new file mode 100644
index 00000000..f1ab4456
--- /dev/null
+++ b/attachments/07-redux-context/00-starting-project/components/MealstList/MealsList.js
@@ -0,0 +1,36 @@
+import { FlatList, View, StyleSheet } from 'react-native';
+import MealItem from '../MealItem';
+
+function MealsList({ items }) {
+ function renderMealItem(itemData) {
+ const item = itemData.item;
+
+ const mealItemProps = {
+ id: item.id,
+ title: item.title,
+ imageUrl: item.imageUrl,
+ affordability: item.affordability,
+ complexity: item.complexity,
+ duration: item.duration
+ };
+ return ;
+ }
+ return (
+
+ item.id}
+ renderItem={renderMealItem}
+ />
+
+ );
+}
+
+export default MealsList;
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ padding: 16
+ }
+});
diff --git a/attachments/07-redux-context/00-starting-project/data/dummy-data.js b/attachments/07-redux-context/00-starting-project/data/dummy-data.js
new file mode 100644
index 00000000..fa2ff3f5
--- /dev/null
+++ b/attachments/07-redux-context/00-starting-project/data/dummy-data.js
@@ -0,0 +1,337 @@
+import Category from '../models/category';
+import Meal from '../models/meal';
+
+export const CATEGORIES = [
+ new Category('c1', 'Italian', '#f5428d'),
+ new Category('c2', 'Quick & Easy', '#f54242'),
+ new Category('c3', 'Hamburgers', '#f5a442'),
+ new Category('c4', 'German', '#f5d142'),
+ new Category('c5', 'Light & Lovely', '#368dff'),
+ new Category('c6', 'Exotic', '#41d95d'),
+ new Category('c7', 'Breakfast', '#9eecff'),
+ new Category('c8', 'Asian', '#b9ffb0'),
+ new Category('c9', 'French', '#ffc7ff'),
+ new Category('c10', 'Summer', '#47fced')
+];
+
+export const MEALS = [
+ new Meal(
+ 'm1',
+ ['c1', 'c2'],
+ 'Spaghetti with Tomato Sauce',
+ 'affordable',
+ 'simple',
+ 'https://upload.wikimedia.org/wikipedia/commons/thumb/2/20/Spaghetti_Bolognese_mit_Parmesan_oder_Grana_Padano.jpg/800px-Spaghetti_Bolognese_mit_Parmesan_oder_Grana_Padano.jpg',
+ 20,
+ [
+ '4 Tomatoes',
+ '1 Tablespoon of Olive Oil',
+ '1 Onion',
+ '250g Spaghetti',
+ 'Spices',
+ 'Cheese (optional)'
+ ],
+ [
+ 'Cut the tomatoes and the onion into small pieces.',
+ 'Boil some water - add salt to it once it boils.',
+ 'Put the spaghetti into the boiling water - they should be done in about 10 to 12 minutes.',
+ 'In the meantime, heaten up some olive oil and add the cut onion.',
+ 'After 2 minutes, add the tomato pieces, salt, pepper and your other spices.',
+ 'The sauce will be done once the spaghetti are.',
+ 'Feel free to add some cheese on top of the finished dish.'
+ ],
+ false,
+ true,
+ true,
+ true
+ ),
+
+ new Meal(
+ 'm2',
+ ['c2'],
+ 'Toast Hawaii',
+ 'affordable',
+ 'simple',
+ 'https://cdn.pixabay.com/photo/2018/07/11/21/51/toast-3532016_1280.jpg',
+ 10,
+ [
+ '1 Slice White Bread',
+ '1 Slice Ham',
+ '1 Slice Pineapple',
+ '1-2 Slices of Cheese',
+ 'Butter'
+ ],
+ [
+ 'Butter one side of the white bread',
+ 'Layer ham, the pineapple and cheese on the white bread',
+ 'Bake the toast for round about 10 minutes in the oven at 200°C'
+ ],
+ false,
+ false,
+ false,
+ false
+ ),
+
+ new Meal(
+ 'm3',
+ ['c3'],
+ 'Classic Hamburger',
+ 'pricey',
+ 'simple',
+ 'https://cdn.pixabay.com/photo/2014/10/23/18/05/burger-500054_1280.jpg',
+ 45,
+ [
+ '300g Cattle Hack',
+ '1 Tomato',
+ '1 Cucumber',
+ '1 Onion',
+ 'Ketchup',
+ '2 Burger Buns'
+ ],
+ [
+ 'Form 2 patties',
+ 'Fry the patties for c. 4 minutes on each side',
+ 'Quickly fry the buns for c. 1 minute on each side',
+ 'Bruch buns with ketchup',
+ 'Serve burger with tomato, cucumber and onion'
+ ],
+ false,
+ false,
+ false,
+ true
+ ),
+
+ new Meal(
+ 'm4',
+ ['c4'],
+ 'Wiener Schnitzel',
+ 'luxurious',
+ 'challenging',
+ 'https://cdn.pixabay.com/photo/2018/03/31/19/29/schnitzel-3279045_1280.jpg',
+ 60,
+ [
+ '8 Veal Cutlets',
+ '4 Eggs',
+ '200g Bread Crumbs',
+ '100g Flour',
+ '300ml Butter',
+ '100g Vegetable Oil',
+ 'Salt',
+ 'Lemon Slices'
+ ],
+ [
+ 'Tenderize the veal to about 2–4mm, and salt on both sides.',
+ 'On a flat plate, stir the eggs briefly with a fork.',
+ 'Lightly coat the cutlets in flour then dip into the egg, and finally, coat in breadcrumbs.',
+ 'Heat the butter and oil in a large pan (allow the fat to get very hot) and fry the schnitzels until golden brown on both sides.',
+ 'Make sure to toss the pan regularly so that the schnitzels are surrounded by oil and the crumbing becomes ‘fluffy’.',
+ 'Remove, and drain on kitchen paper. Fry the parsley in the remaining oil and drain.',
+ 'Place the schnitzels on awarmed plate and serve garnishedwith parsley and slices of lemon.'
+ ],
+ false,
+ false,
+ false,
+ false
+ ),
+
+ new Meal(
+ 'm5',
+ ['c2', 'c5', 'c10'],
+ 'Salad with Smoked Salmon',
+ 'luxurious',
+ 'simple',
+ 'https://cdn.pixabay.com/photo/2016/10/25/13/29/smoked-salmon-salad-1768890_1280.jpg',
+ 15,
+ [
+ 'Arugula',
+ "Lamb's Lettuce",
+ 'Parsley',
+ 'Fennel',
+ '200g Smoked Salmon',
+ 'Mustard',
+ 'Balsamic Vinegar',
+ 'Olive Oil',
+ 'Salt and Pepper'
+ ],
+ [
+ 'Wash and cut salad and herbs',
+ 'Dice the salmon',
+ 'Process mustard, vinegar and olive oil into a dessing',
+ 'Prepare the salad',
+ 'Add salmon cubes and dressing'
+ ],
+ true,
+ false,
+ true,
+ true
+ ),
+
+ new Meal(
+ 'm6',
+ ['c6', 'c10'],
+ 'Delicious Orange Mousse',
+ 'affordable',
+ 'hard',
+ 'https://cdn.pixabay.com/photo/2017/05/01/05/18/pastry-2274750_1280.jpg',
+ 240,
+ [
+ '4 Sheets of Gelatine',
+ '150ml Orange Juice',
+ '80g Sugar',
+ '300g Yoghurt',
+ '200g Cream',
+ 'Orange Peel'
+ ],
+ [
+ 'Dissolve gelatine in pot',
+ 'Add orange juice and sugar',
+ 'Take pot off the stove',
+ 'Add 2 tablespoons of yoghurt',
+ 'Stir gelatin under remaining yoghurt',
+ 'Cool everything down in the refrigerator',
+ 'Whip the cream and lift it under die orange mass',
+ 'Cool down again for at least 4 hours',
+ 'Serve with orange peel'
+ ],
+ true,
+ false,
+ true,
+ false
+ ),
+
+ new Meal(
+ 'm7',
+ ['c7'],
+ 'Pancakes',
+ 'affordable',
+ 'simple',
+ 'https://cdn.pixabay.com/photo/2018/07/10/21/23/pancake-3529653_1280.jpg',
+ 20,
+ [
+ '1 1/2 Cups all-purpose Flour',
+ '3 1/2 Teaspoons Baking Powder',
+ '1 Teaspoon Salt',
+ '1 Tablespoon White Sugar',
+ '1 1/4 cups Milk',
+ '1 Egg',
+ '3 Tablespoons Butter, melted'
+ ],
+ [
+ 'In a large bowl, sift together the flour, baking powder, salt and sugar.',
+ 'Make a well in the center and pour in the milk, egg and melted butter; mix until smooth.',
+ 'Heat a lightly oiled griddle or frying pan over medium high heat.',
+ 'Pour or scoop the batter onto the griddle, using approximately 1/4 cup for each pancake. Brown on both sides and serve hot.'
+ ],
+ true,
+ false,
+ true,
+ false
+ ),
+
+ new Meal(
+ 'm8',
+ ['c8'],
+ 'Creamy Indian Chicken Curry',
+ 'pricey',
+ 'challenging',
+ 'https://cdn.pixabay.com/photo/2018/06/18/16/05/indian-food-3482749_1280.jpg',
+ 35,
+ [
+ '4 Chicken Breasts',
+ '1 Onion',
+ '2 Cloves of Garlic',
+ '1 Piece of Ginger',
+ '4 Tablespoons Almonds',
+ '1 Teaspoon Cayenne Pepper',
+ '500ml Coconut Milk'
+ ],
+ [
+ 'Slice and fry the chicken breast',
+ 'Process onion, garlic and ginger into paste and sauté everything',
+ 'Add spices and stir fry',
+ 'Add chicken breast + 250ml of water and cook everything for 10 minutes',
+ 'Add coconut milk',
+ 'Serve with rice'
+ ],
+ true,
+ false,
+ false,
+ true
+ ),
+
+ new Meal(
+ 'm9',
+ ['c9'],
+ 'Chocolate Souffle',
+ 'affordable',
+ 'hard',
+ 'https://cdn.pixabay.com/photo/2014/08/07/21/07/souffle-412785_1280.jpg',
+ 45,
+ [
+ '1 Teaspoon melted Butter',
+ '2 Tablespoons white Sugar',
+ '2 Ounces 70% dark Chocolate, broken into pieces',
+ '1 Tablespoon Butter',
+ '1 Tablespoon all-purpose Flour',
+ '4 1/3 tablespoons cold Milk',
+ '1 Pinch Salt',
+ '1 Pinch Cayenne Pepper',
+ '1 Large Egg Yolk',
+ '2 Large Egg Whites',
+ '1 Pinch Cream of Tartar',
+ '1 Tablespoon white Sugar'
+ ],
+ [
+ 'Preheat oven to 190°C. Line a rimmed baking sheet with parchment paper.',
+ 'Brush bottom and sides of 2 ramekins lightly with 1 teaspoon melted butter; cover bottom and sides right up to the rim.',
+ 'Add 1 tablespoon white sugar to ramekins. Rotate ramekins until sugar coats all surfaces.',
+ 'Place chocolate pieces in a metal mixing bowl.',
+ 'Place bowl over a pan of about 3 cups hot water over low heat.',
+ 'Melt 1 tablespoon butter in a skillet over medium heat. Sprinkle in flour. Whisk until flour is incorporated into butter and mixture thickens.',
+ 'Whisk in cold milk until mixture becomes smooth and thickens. Transfer mixture to bowl with melted chocolate.',
+ 'Add salt and cayenne pepper. Mix together thoroughly. Add egg yolk and mix to combine.',
+ 'Leave bowl above the hot (not simmering) water to keep chocolate warm while you whip the egg whites.',
+ 'Place 2 egg whites in a mixing bowl; add cream of tartar. Whisk until mixture begins to thicken and a drizzle from the whisk stays on the surface about 1 second before disappearing into the mix.',
+ 'Add 1/3 of sugar and whisk in. Whisk in a bit more sugar about 15 seconds.',
+ 'whisk in the rest of the sugar. Continue whisking until mixture is about as thick as shaving cream and holds soft peaks, 3 to 5 minutes.',
+ 'Transfer a little less than half of egg whites to chocolate.',
+ 'Mix until egg whites are thoroughly incorporated into the chocolate.',
+ 'Add the rest of the egg whites; gently fold into the chocolate with a spatula, lifting from the bottom and folding over.',
+ 'Stop mixing after the egg white disappears. Divide mixture between 2 prepared ramekins. Place ramekins on prepared baking sheet.',
+ 'Bake in preheated oven until scuffles are puffed and have risen above the top of the rims, 12 to 15 minutes.'
+ ],
+ true,
+ false,
+ true,
+ false
+ ),
+ new Meal(
+ 'm10',
+ ['c2', 'c5', 'c10'],
+ 'Asparagus Salad with Cherry Tomatoes',
+ 'luxurious',
+ 'simple',
+ 'https://cdn.pixabay.com/photo/2018/04/09/18/26/asparagus-3304997_1280.jpg',
+ 30,
+ [
+ 'White and Green Asparagus',
+ '30g Pine Nuts',
+ '300g Cherry Tomatoes',
+ 'Salad',
+ 'Salt, Pepper and Olive Oil'
+ ],
+ [
+ 'Wash, peel and cut the asparagus',
+ 'Cook in salted water',
+ 'Salt and pepper the asparagus',
+ 'Roast the pine nuts',
+ 'Halve the tomatoes',
+ 'Mix with asparagus, salad and dressing',
+ 'Serve with Baguette'
+ ],
+ true,
+ true,
+ true,
+ true
+ )
+];
diff --git a/attachments/07-redux-context/00-starting-project/models/category.js b/attachments/07-redux-context/00-starting-project/models/category.js
new file mode 100644
index 00000000..17607202
--- /dev/null
+++ b/attachments/07-redux-context/00-starting-project/models/category.js
@@ -0,0 +1,9 @@
+class Category {
+ constructor(id, title, color) {
+ this.id = id;
+ this.title = title;
+ this.color = color;
+ }
+}
+
+export default Category;
diff --git a/attachments/07-redux-context/00-starting-project/models/meal.js b/attachments/07-redux-context/00-starting-project/models/meal.js
new file mode 100644
index 00000000..48cae8dc
--- /dev/null
+++ b/attachments/07-redux-context/00-starting-project/models/meal.js
@@ -0,0 +1,33 @@
+class Meal {
+ constructor(
+ id,
+ categoryIds,
+ title,
+ affordability,
+ complexity,
+ imageUrl,
+ duration,
+ ingredients,
+ steps,
+ isGlutenFree,
+ isVegan,
+ isVegetarian,
+ isLactoseFree
+ ) {
+ this.id = id;
+ this.categoryIds = categoryIds;
+ this.title = title;
+ this.imageUrl = imageUrl;
+ this.ingredients = ingredients;
+ this.steps = steps;
+ this.duration = duration;
+ this.complexity = complexity;
+ this.affordability = affordability;
+ this.isGlutenFree = isGlutenFree;
+ this.isVegan = isVegan;
+ this.isVegetarian = isVegetarian;
+ this.isLactoseFree = isLactoseFree;
+ }
+}
+
+export default Meal;
diff --git a/attachments/07-redux-context/00-starting-project/package.json b/attachments/07-redux-context/00-starting-project/package.json
new file mode 100644
index 00000000..a58a9132
--- /dev/null
+++ b/attachments/07-redux-context/00-starting-project/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "rncourse",
+ "version": "1.0.0",
+ "main": "node_modules/expo/AppEntry.js",
+ "scripts": {
+ "start": "expo start",
+ "android": "expo start --android",
+ "ios": "expo start --ios",
+ "web": "expo start --web",
+ "eject": "expo eject"
+ },
+ "dependencies": {
+ "@react-navigation/drawer": "^6.3.1",
+ "@react-navigation/native": "^6.0.8",
+ "@react-navigation/native-stack": "^6.5.0",
+ "@reduxjs/toolkit": "^2.11.1",
+ "expo": "^52.0.0",
+ "expo-status-bar": "~2.0.1",
+ "react": "18.3.1",
+ "react-dom": "18.3.1",
+ "react-native": "0.76.7",
+ "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.4.0",
+ "react-native-web": "~0.19.13",
+ "react-redux": "^9.2.0"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.25.2"
+ },
+ "private": true
+}
diff --git a/attachments/07-redux-context/00-starting-project/screens/CategoriesScreen.js b/attachments/07-redux-context/00-starting-project/screens/CategoriesScreen.js
new file mode 100644
index 00000000..1dde077c
--- /dev/null
+++ b/attachments/07-redux-context/00-starting-project/screens/CategoriesScreen.js
@@ -0,0 +1,33 @@
+import { FlatList } from 'react-native';
+import CategoryGridTile from '../components/CategoryGridTile';
+
+import { CATEGORIES } from '../data/dummy-data';
+
+function CategoriesScreen({ navigation }) {
+ function renderCategoryItem(itemData) {
+ function pressHandler() {
+ navigation.navigate('MealsOverview', {
+ categoryId: itemData.item.id,
+ });
+ }
+
+ return (
+
+ );
+ }
+
+ return (
+ item.id}
+ renderItem={renderCategoryItem}
+ numColumns={2}
+ />
+ );
+}
+
+export default CategoriesScreen;
diff --git a/attachments/07-redux-context/00-starting-project/screens/FavoritesScreen.js b/attachments/07-redux-context/00-starting-project/screens/FavoritesScreen.js
new file mode 100644
index 00000000..b873a7f0
--- /dev/null
+++ b/attachments/07-redux-context/00-starting-project/screens/FavoritesScreen.js
@@ -0,0 +1,41 @@
+import { useSelector } from 'react-redux';
+import MealsList from '../components/MealstList/MealsList';
+import { MEALS } from '../data/dummy-data';
+import { View, Text, StyleSheet } from 'react-native';
+// import { FavoritesContext } from '../store/context/favorites-context';
+// import { useContext } from 'react';
+
+function FavoritesScreen() {
+ // const { ids } = useContext(FavoritesContext);
+ // const favoriteMeals = MEALS.filter((meal) => ids.includes(meal.id));
+
+ const favoriteMealIds = useSelector((state) => state.favoriteMeals.ids);
+ const favoriteMeals = MEALS.filter((meal) =>
+ favoriteMealIds.includes(meal.id)
+ );
+
+ if (favoriteMeals.length === 0) {
+ return (
+
+ You have no favorite meals yet.
+
+ );
+ }
+
+ return ;
+}
+
+export default FavoritesScreen;
+
+const styles = StyleSheet.create({
+ rootContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center'
+ },
+ text: {
+ fontSize: 18,
+ fontWeight: 'bold',
+ color: 'white'
+ }
+});
diff --git a/attachments/07-redux-context/00-starting-project/screens/MealDetailScreen.js b/attachments/07-redux-context/00-starting-project/screens/MealDetailScreen.js
new file mode 100644
index 00000000..e3a24b5e
--- /dev/null
+++ b/attachments/07-redux-context/00-starting-project/screens/MealDetailScreen.js
@@ -0,0 +1,100 @@
+import { useContext, useLayoutEffect } from 'react';
+import { View, Text, Image, StyleSheet, ScrollView } from 'react-native';
+
+import IconButton from '../components/IconButton';
+import List from '../components/MealDetail/List';
+import Subtitle from '../components/MealDetail/Subtitle';
+import MealDetails from '../components/MealDetails';
+import { MEALS } from '../data/dummy-data';
+// import { FavoritesContext } from '../store/context/favorites-context';
+import { useDispatch, useSelector } from 'react-redux';
+import { addFavorite, removeFavorite } from '../store/redux/favorites';
+
+function MealDetailScreen({ route, navigation }) {
+ // const { ids, addFavorite, removeFavorite } = useContext(FavoritesContext);
+
+ const mealId = route.params.mealId;
+
+ const selectedMeal = MEALS.find((meal) => meal.id === mealId);
+
+ // const mealIsFavorite = ids.includes(mealId);
+ const favoriteMealIds = useSelector((state) => state.ids);
+ const mealIsFavorite = favoriteMealIds.includes(mealId);
+
+ const dispatch = useDispatch();
+
+ function changeFavoriteStatusHandler() {
+ if (mealIsFavorite) {
+ // removeFavorite(mealId);
+
+ dispatch(removeFavorite({ id: mealId }));
+ } else {
+ // addFavorite(mealId);
+
+ dispatch(addFavorite({ id: mealId }));
+ }
+ }
+
+ useLayoutEffect(() => {
+ navigation.setOptions({
+ headerRight: () => {
+ return (
+
+ );
+ }
+ });
+ }, [navigation, changeFavoriteStatusHandler]);
+
+ return (
+
+
+ {selectedMeal.title}
+
+
+
+ Ingredients
+
+ Steps
+
+
+
+
+ );
+}
+
+export default MealDetailScreen;
+
+const styles = StyleSheet.create({
+ rootContainer: {
+ marginBottom: 32
+ },
+ image: {
+ width: '100%',
+ height: 350
+ },
+ title: {
+ fontWeight: 'bold',
+ fontSize: 24,
+ margin: 8,
+ textAlign: 'center',
+ color: 'white'
+ },
+ detailText: {
+ color: 'white'
+ },
+ listOuterContainer: {
+ alignItems: 'center'
+ },
+ listContainer: {
+ width: '80%'
+ }
+});
diff --git a/attachments/07-redux-context/00-starting-project/screens/MealsOverviewScreen.js b/attachments/07-redux-context/00-starting-project/screens/MealsOverviewScreen.js
new file mode 100644
index 00000000..d0060919
--- /dev/null
+++ b/attachments/07-redux-context/00-starting-project/screens/MealsOverviewScreen.js
@@ -0,0 +1,28 @@
+import { useLayoutEffect } from 'react';
+import { View, FlatList, StyleSheet } from 'react-native';
+
+import MealItem from '../components/MealItem';
+import { MEALS, CATEGORIES } from '../data/dummy-data';
+import MealsList from '../components/MealstList/MealsList';
+
+function MealsOverviewScreen({ route, navigation }) {
+ const catId = route.params.categoryId;
+
+ const displayedMeals = MEALS.filter((mealItem) => {
+ return mealItem.categoryIds.indexOf(catId) >= 0;
+ });
+
+ useLayoutEffect(() => {
+ const categoryTitle = CATEGORIES.find(
+ (category) => category.id === catId
+ ).title;
+
+ navigation.setOptions({
+ title: categoryTitle
+ });
+ }, [catId, navigation]);
+
+ return ;
+}
+
+export default MealsOverviewScreen;
diff --git a/attachments/07-redux-context/00-starting-project/store/context/favorites-context.js b/attachments/07-redux-context/00-starting-project/store/context/favorites-context.js
new file mode 100644
index 00000000..b2844d3b
--- /dev/null
+++ b/attachments/07-redux-context/00-starting-project/store/context/favorites-context.js
@@ -0,0 +1,35 @@
+import { createContext, useState } from 'react';
+
+export const FavoritesContext = createContext({
+ ids: [],
+ addFavorite: (id) => {},
+ removeFavorite: (id) => {}
+});
+
+export default function FavoritesContextProvider({ children }) {
+ const [favoriteMealIds, setFavoriteMealIds] = useState([]);
+
+ function addFavorite(id) {
+ setFavoriteMealIds((currentFavIds) => {
+ return [...currentFavIds, id];
+ });
+ }
+
+ function removeFavorite(id) {
+ setFavoriteMealIds((currentFavIds) => {
+ return currentFavIds.filter((mealId) => mealId !== id);
+ });
+ }
+
+ const value = {
+ ids: favoriteMealIds,
+ addFavorite: addFavorite,
+ removeFavorite: removeFavorite
+ };
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/attachments/07-redux-context/00-starting-project/store/redux/favorites.js b/attachments/07-redux-context/00-starting-project/store/redux/favorites.js
new file mode 100644
index 00000000..12c967f7
--- /dev/null
+++ b/attachments/07-redux-context/00-starting-project/store/redux/favorites.js
@@ -0,0 +1,21 @@
+import { createSlice } from '@reduxjs/toolkit';
+
+const favoriteSlice = createSlice({
+ name: 'favorites',
+ initialState: {
+ ids: []
+ },
+ reducers: {
+ addFavorite: (state, action) => {
+ return state.ids.push(action.payload);
+ },
+ removeFavorite: (state) => {
+ // return state.ids.filter((id) => id !== action.payload)
+ return state.ids.splice(state.ids.indexOf(action.payload.id), 1);
+ }
+ }
+});
+
+export const addFavorite = favoriteSlice.actions.addFavorite;
+export const removeFavorite = favoriteSlice.actions.removeFavorite;
+export default favoriteSlice.reducer;
diff --git a/attachments/07-redux-context/00-starting-project/store/redux/store.js b/attachments/07-redux-context/00-starting-project/store/redux/store.js
new file mode 100644
index 00000000..84784559
--- /dev/null
+++ b/attachments/07-redux-context/00-starting-project/store/redux/store.js
@@ -0,0 +1,9 @@
+import { configureStore } from '@reduxjs/toolkit';
+
+import favoritesReducer from './favorites';
+
+export const store = configureStore({
+ reducer: {
+ favoriteMeals: favoritesReducer
+ }
+});
diff --git a/attachments/08-practice-app/00-starting-project/App.js b/attachments/08-practice-app/00-starting-project/App.js
new file mode 100644
index 00000000..469994fd
--- /dev/null
+++ b/attachments/08-practice-app/00-starting-project/App.js
@@ -0,0 +1,102 @@
+import { StatusBar } from 'expo-status-bar';
+import { createNativeStackNavigator } from '@react-navigation/native-stack';
+import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
+import { Ionicons } from '@expo/vector-icons';
+import { NavigationContainer } from '@react-navigation/native';
+
+import AllExpenses from './screens/AllExpenses';
+import RecentExpenses from './screens/RecentExpenses';
+import ManageExpense from './screens/ManageExpense';
+import { GlobalStyles } from './constants/styles';
+import IconButton from './components/UI/IconButton';
+import ExpensesContextProvider from './store/expenses-context';
+
+const Stack = createNativeStackNavigator();
+const BottomTab = createBottomTabNavigator();
+
+function ExpensesOverview() {
+ return (
+ ({
+ headerTintColor: 'white',
+ headerStyle: {
+ backgroundColor: GlobalStyles.colors.primary500
+ },
+ tabBarActiveTintColor: GlobalStyles.colors.accent500,
+ tabBarStyle: {
+ backgroundColor: GlobalStyles.colors.primary500
+ },
+ headerRight: ({ tintColor }) => {
+ return (
+ {
+ navigation.navigate('MangeExpense');
+ }}
+ />
+ );
+ }
+ })}
+ >
+ (
+
+ )
+ }}
+ />
+ (
+
+ )
+ }}
+ />
+
+ );
+}
+
+export default function App() {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/attachments/08-practice-app/00-starting-project/app.json b/attachments/08-practice-app/00-starting-project/app.json
new file mode 100644
index 00000000..1a9f941a
--- /dev/null
+++ b/attachments/08-practice-app/00-starting-project/app.json
@@ -0,0 +1,30 @@
+{
+ "expo": {
+ "name": "RNCourse",
+ "slug": "RNCourse",
+ "version": "1.0.0",
+ "orientation": "portrait",
+ "icon": "./assets/icon.png",
+ "splash": {
+ "image": "./assets/splash.png",
+ "resizeMode": "contain",
+ "backgroundColor": "#ffffff"
+ },
+ "updates": {
+ "fallbackToCacheTimeout": 0
+ },
+ "assetBundlePatterns": ["**/*"],
+ "ios": {
+ "supportsTablet": true
+ },
+ "android": {
+ "adaptiveIcon": {
+ "foregroundImage": "./assets/adaptive-icon.png",
+ "backgroundColor": "#FFFFFF"
+ }
+ },
+ "web": {
+ "favicon": "./assets/favicon.png"
+ }
+ }
+}
diff --git a/attachments/08-practice-app/00-starting-project/assets/adaptive-icon.png b/attachments/08-practice-app/00-starting-project/assets/adaptive-icon.png
new file mode 100644
index 00000000..03d6f6b6
Binary files /dev/null and b/attachments/08-practice-app/00-starting-project/assets/adaptive-icon.png differ
diff --git a/attachments/08-practice-app/00-starting-project/assets/favicon.png b/attachments/08-practice-app/00-starting-project/assets/favicon.png
new file mode 100644
index 00000000..e75f697b
Binary files /dev/null and b/attachments/08-practice-app/00-starting-project/assets/favicon.png differ
diff --git a/attachments/08-practice-app/00-starting-project/assets/icon.png b/attachments/08-practice-app/00-starting-project/assets/icon.png
new file mode 100644
index 00000000..a0b1526f
Binary files /dev/null and b/attachments/08-practice-app/00-starting-project/assets/icon.png differ
diff --git a/attachments/08-practice-app/00-starting-project/assets/splash.png b/attachments/08-practice-app/00-starting-project/assets/splash.png
new file mode 100644
index 00000000..0e89705a
Binary files /dev/null and b/attachments/08-practice-app/00-starting-project/assets/splash.png differ
diff --git a/attachments/08-practice-app/00-starting-project/babel.config.js b/attachments/08-practice-app/00-starting-project/babel.config.js
new file mode 100644
index 00000000..e1babf6b
--- /dev/null
+++ b/attachments/08-practice-app/00-starting-project/babel.config.js
@@ -0,0 +1,6 @@
+module.exports = function(api) {
+ api.cache(true);
+ return {
+ presets: ['babel-preset-expo'],
+ };
+};
diff --git a/attachments/08-practice-app/00-starting-project/components/Expenses/ExpenseItem.js b/attachments/08-practice-app/00-starting-project/components/Expenses/ExpenseItem.js
new file mode 100644
index 00000000..17f73862
--- /dev/null
+++ b/attachments/08-practice-app/00-starting-project/components/Expenses/ExpenseItem.js
@@ -0,0 +1,75 @@
+import { Pressable } from 'react-native';
+import { View, Text, StyleSheet } from 'react-native';
+import { GlobalStyles } from '../../constants/styles';
+import { getFormattedDate } from '../../util/date';
+
+import { useNavigation } from '@react-navigation/native';
+
+function ExpenseItem({ id, description, date, amount }) {
+ const navigation = useNavigation();
+
+ function expenseHandler() {
+ navigation.navigate('MangeExpense', { expenseId: id });
+ }
+
+ return (
+ pressed && styles.pressed}
+ >
+
+
+
+ {description}
+
+ {getFormattedDate(date)}
+
+
+ {amount.toFixed(2)}
+
+
+
+ );
+}
+
+export default ExpenseItem;
+
+const styles = StyleSheet.create({
+ pressed: {
+ opacity: 0.75
+ },
+ expenseItem: {
+ padding: 12,
+ marginVertical: 8,
+ backgroundColor: GlobalStyles.colors.primary500,
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ borderRadius: 6,
+ elevation: 3,
+ shadowColor: GlobalStyles.colors.gray500,
+ shadowRadius: 4,
+ shadowOffset: { width: 1, height: 1 },
+ shadowOpacity: 0.4
+ },
+ textBase: {
+ color: GlobalStyles.colors.primary50
+ },
+ description: {
+ fontSize: 16,
+ marginBottom: 4,
+ fontWeight: 'bold'
+ },
+ amountContainer: {
+ paddingHorizontal: 12,
+ paddingVertical: 4,
+ backgroundColor: 'white',
+ justifyContent: 'center',
+ alignItems: 'center',
+ borderRadius: 4,
+ minWidth: 80
+ },
+ amount: {
+ color: GlobalStyles.colors.primary500,
+ fontWeight: 'bold'
+ }
+});
diff --git a/attachments/08-practice-app/00-starting-project/components/Expenses/ExpensesList.js b/attachments/08-practice-app/00-starting-project/components/Expenses/ExpensesList.js
new file mode 100644
index 00000000..ec5da271
--- /dev/null
+++ b/attachments/08-practice-app/00-starting-project/components/Expenses/ExpensesList.js
@@ -0,0 +1,20 @@
+import { View, FlatList } from 'react-native';
+import ExpenseItem from './ExpenseItem';
+
+function renderExpenseItem(itemData) {
+ return ;
+}
+
+function ExpensesList({ expenses }) {
+ return (
+
+ item.id}
+ />
+
+ );
+}
+
+export default ExpensesList;
diff --git a/attachments/08-practice-app/00-starting-project/components/Expenses/ExpensesOutput.js b/attachments/08-practice-app/00-starting-project/components/Expenses/ExpensesOutput.js
new file mode 100644
index 00000000..c2ddcd1d
--- /dev/null
+++ b/attachments/08-practice-app/00-starting-project/components/Expenses/ExpensesOutput.js
@@ -0,0 +1,41 @@
+import { View, Text, StyleSheet } from 'react-native';
+import ExpensesSummary from './ExpensesSummary';
+import ExpensesList from './ExpensesList';
+import { GlobalStyles } from '../../constants/styles';
+
+function ExpensesOutput({ expenses, expensesPeriod, amount, fallbackText }) {
+ let content = {fallbackText};
+
+ if (expenses.length > 0) {
+ content = ;
+ }
+
+ return (
+
+
+
+ {content}
+
+ );
+}
+
+export default ExpensesOutput;
+
+const styles = StyleSheet.create({
+ rootContainer: {
+ flex: 1,
+ paddingHorizontal: 24,
+ paddingTop: 24,
+ backgroundColor: GlobalStyles.colors.primary700
+ },
+ infoText: {
+ color: 'white',
+ fontSize: 16,
+ textAlign: 'center',
+ marginTop: 32
+ }
+});
diff --git a/attachments/08-practice-app/00-starting-project/components/Expenses/ExpensesSummary.js b/attachments/08-practice-app/00-starting-project/components/Expenses/ExpensesSummary.js
new file mode 100644
index 00000000..0b11a07e
--- /dev/null
+++ b/attachments/08-practice-app/00-starting-project/components/Expenses/ExpensesSummary.js
@@ -0,0 +1,37 @@
+import { Text, View, StyleSheet } from 'react-native';
+import { GlobalStyles } from '../../constants/styles';
+
+function ExpensesSummary({ periodName, expenses }) {
+ const expensesSum = expenses.reduce((sum, expense) => {
+ return sum + expense.amount;
+ }, 0);
+
+ return (
+
+ {periodName}
+ ${expensesSum.toFixed(2)}
+
+ );
+}
+
+export default ExpensesSummary;
+
+const styles = StyleSheet.create({
+ rootContainer: {
+ padding: 8,
+ backgroundColor: GlobalStyles.colors.primary50,
+ borderRadius: 6,
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center'
+ },
+ sum: {
+ fontWeight: 'bold',
+ fontSize: 16,
+ color: GlobalStyles.colors.primary500
+ },
+ period: {
+ fontSize: 12,
+ color: GlobalStyles.colors.primary400
+ }
+});
diff --git a/attachments/08-practice-app/00-starting-project/components/UI/Button.js b/attachments/08-practice-app/00-starting-project/components/UI/Button.js
new file mode 100644
index 00000000..7330322e
--- /dev/null
+++ b/attachments/08-practice-app/00-starting-project/components/UI/Button.js
@@ -0,0 +1,47 @@
+import { Text } from 'react-native';
+import { StyleSheet } from 'react-native';
+import { Pressable } from 'react-native';
+import { View } from 'react-native';
+import { GlobalStyles } from '../../constants/styles';
+
+function Button({ children, onPress, mode, style }) {
+ return (
+
+ pressed && styles.pressed}
+ >
+
+
+ {children}
+
+
+
+
+ );
+}
+
+export default Button;
+
+const styles = StyleSheet.create({
+ button: {
+ borderRadius: 4,
+ padding: 8,
+ backgroundColor: GlobalStyles.colors.primary500
+ },
+ flat: {
+ backgroundColor: 'transparent'
+ },
+ buttonText: {
+ color: 'white',
+ textAlign: 'center'
+ },
+ flatText: {
+ color: GlobalStyles.colors.primary200
+ },
+ pressed: {
+ opacity: 0.75,
+ backgroundColor: GlobalStyles.colors.primary100,
+ borderRadius: 4
+ }
+});
diff --git a/attachments/08-practice-app/00-starting-project/components/UI/IconButton.js b/attachments/08-practice-app/00-starting-project/components/UI/IconButton.js
new file mode 100644
index 00000000..c454713f
--- /dev/null
+++ b/attachments/08-practice-app/00-starting-project/components/UI/IconButton.js
@@ -0,0 +1,30 @@
+import { View, Pressable } from 'react-native';
+import { Ionicons } from '@expo/vector-icons';
+import { StyleSheet } from 'react-native';
+
+function IconButton({ name, size, color, onPress }) {
+ return (
+ pressed && styles.pressed}
+ >
+
+
+
+
+ );
+}
+
+export default IconButton;
+
+const styles = StyleSheet.create({
+ buttonContainer: {
+ borderRadius: 24,
+ padding: 6,
+ marginHorizontal: 8,
+ marginVertical: 2
+ },
+ pressed: {
+ opacity: 0.75
+ }
+});
diff --git a/attachments/08-practice-app/00-starting-project/constants/styles.js b/attachments/08-practice-app/00-starting-project/constants/styles.js
new file mode 100644
index 00000000..1d24ee1e
--- /dev/null
+++ b/attachments/08-practice-app/00-starting-project/constants/styles.js
@@ -0,0 +1,16 @@
+export const GlobalStyles = {
+ colors: {
+ primary50: '#e4d9fd',
+ primary100: '#c6affc',
+ primary200: '#a281f0',
+ primary400: '#5721d4',
+ primary500: '#3e04c3',
+ primary700: '#2d0689',
+ primary800: '#200364',
+ accent500: '#f7bc0c',
+ error50: '#fcc4e4',
+ error500: '#9b095c',
+ gray500: '#39324a',
+ gray700: '#221c30'
+ }
+};
diff --git a/attachments/08-practice-app/00-starting-project/package.json b/attachments/08-practice-app/00-starting-project/package.json
new file mode 100644
index 00000000..6e3abbcb
--- /dev/null
+++ b/attachments/08-practice-app/00-starting-project/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "rncourse",
+ "version": "1.0.0",
+ "main": "node_modules/expo/AppEntry.js",
+ "scripts": {
+ "start": "expo start",
+ "android": "expo start --android",
+ "ios": "expo start --ios",
+ "web": "expo start --web",
+ "eject": "expo eject"
+ },
+ "dependencies": {
+ "@react-native-vector-icons/ionicons": "^12.3.0",
+ "@react-navigation/bottom-tabs": "^6.6.1",
+ "@react-navigation/native": "^6.1.18",
+ "@react-navigation/native-stack": "^6.11.0",
+ "expo": "^52.0.0",
+ "expo-status-bar": "~2.0.1",
+ "react": "18.3.1",
+ "react-dom": "18.3.1",
+ "react-native": "0.76.7",
+ "react-native-safe-area-context": "4.12.0",
+ "react-native-screens": "~4.4.0",
+ "react-native-web": "~0.19.13"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.25.2"
+ },
+ "private": true
+}
diff --git a/attachments/08-practice-app/00-starting-project/screens/AllExpenses.js b/attachments/08-practice-app/00-starting-project/screens/AllExpenses.js
new file mode 100644
index 00000000..5806e926
--- /dev/null
+++ b/attachments/08-practice-app/00-starting-project/screens/AllExpenses.js
@@ -0,0 +1,17 @@
+import { useContext } from 'react';
+import ExpensesOutput from '../components/Expenses/ExpensesOutput';
+import { ExpensesContext } from '../store/expenses-context';
+
+function AllExpenses() {
+ const expensesContext = useContext(ExpensesContext);
+
+ return (
+
+ );
+}
+
+export default AllExpenses;
diff --git a/attachments/08-practice-app/00-starting-project/screens/ManageExpense.js b/attachments/08-practice-app/00-starting-project/screens/ManageExpense.js
new file mode 100644
index 00000000..2ec500f6
--- /dev/null
+++ b/attachments/08-practice-app/00-starting-project/screens/ManageExpense.js
@@ -0,0 +1,97 @@
+import { useContext, useLayoutEffect } from 'react';
+import { View, StyleSheet } from 'react-native';
+import { GlobalStyles } from '../constants/styles';
+import IconButton from '../components/UI/IconButton';
+import Button from '../components/UI/Button';
+import { ExpensesContext } from '../store/expenses-context';
+
+function ManageExpense({ route, navigation }) {
+ const expensesCtx = useContext(ExpensesContext);
+
+ const editedExpensedId = route.params?.expenseId;
+
+ const isEditing = !!editedExpensedId;
+
+ useLayoutEffect(() => {
+ navigation.setOptions({
+ title: isEditing ? 'Edit Expense' : 'Add Expense'
+ });
+ }, [navigation, isEditing]);
+
+ function deleteExpenseHandler() {
+ expensesCtx.deleteExpense(editedExpensedId);
+ navigation.goBack();
+ }
+
+ function cancelHandler() {
+ navigation.goBack();
+ }
+
+ function confirmHandler() {
+ if (isEditing) {
+ expensesCtx.updateExpense(editedExpensedId, {
+ description: 'Test',
+ amount: 0.99,
+ date: new Date('2025-12-01')
+ });
+ } else {
+ expensesCtx.addExpense({
+ description: 'Test',
+ amount: 0.99,
+ date: new Date('2025-12-10')
+ });
+ }
+
+ navigation.goBack();
+ }
+
+ return (
+
+
+
+
+
+
+ {isEditing && (
+
+
+
+ )}
+
+ );
+}
+
+export default ManageExpense;
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ padding: 24,
+ backgroundColor: GlobalStyles.colors.primary800
+ },
+ buttonsContainer: {
+ flexDirection: 'row',
+ justifyContent: 'center',
+ alignItems: 'center'
+ },
+ button: {
+ minWidth: 120,
+ marginHorizontal: 8
+ },
+ deleteContainer: {
+ marginTop: 16,
+ paddingTop: 8,
+ borderTopWidth: 2,
+ borderTopColor: GlobalStyles.colors.primary200,
+ alignItems: 'center'
+ }
+});
diff --git a/attachments/08-practice-app/00-starting-project/screens/RecentExpenses.js b/attachments/08-practice-app/00-starting-project/screens/RecentExpenses.js
new file mode 100644
index 00000000..135f5386
--- /dev/null
+++ b/attachments/08-practice-app/00-starting-project/screens/RecentExpenses.js
@@ -0,0 +1,26 @@
+import { useContext } from 'react';
+import ExpensesOutput from '../components/Expenses/ExpensesOutput';
+import { ExpensesContext } from '../store/expenses-context';
+
+import { getDateMinusDays } from '../util/date';
+
+function RecentExpenses() {
+ const expensesCtx = useContext(ExpensesContext);
+
+ const recentExpenses = expensesCtx.expenses.filter((expense) => {
+ const today = new Date();
+ const date7DaysAgo = getDateMinusDays(today, 7);
+
+ return expense.date > date7DaysAgo;
+ });
+
+ return (
+
+ );
+}
+
+export default RecentExpenses;
diff --git a/attachments/08-practice-app/00-starting-project/store/expenses-context.js b/attachments/08-practice-app/00-starting-project/store/expenses-context.js
new file mode 100644
index 00000000..2c720a4a
--- /dev/null
+++ b/attachments/08-practice-app/00-starting-project/store/expenses-context.js
@@ -0,0 +1,97 @@
+import { createContext, useReducer } from 'react';
+
+export const DUMMY_EXPENSES = [
+ {
+ id: 'e1',
+ description: 'A pair of shoes',
+ date: new Date('2021-12-19'),
+ amount: 59.99
+ },
+ {
+ id: 'e2',
+ description: 'A pair of trousers',
+ date: new Date('2021-12-19'),
+ amount: 18.59
+ },
+ {
+ id: 'e3',
+ description: 'Some bananas',
+ date: new Date('2021-12-01'),
+ amount: 5.99
+ },
+ {
+ id: 'e4',
+ description: 'A book',
+ date: new Date('2022-02-19'),
+ amount: 14.99
+ },
+ {
+ id: 'e5',
+ description: 'Another book',
+ date: new Date('2022-02-18'),
+ amount: 18.59
+ }
+];
+
+export const ExpensesContext = createContext({
+ expenses: [],
+ addExpense: ({ description, amount, date }) => {},
+ deleteExpense: (id) => {},
+ updateExpense: (id, { description, amount, date }) => {}
+});
+
+function expensesReducer(state, action) {
+ switch (action.type) {
+ case 'ADD':
+ const id = new Date().toString() + Math.random().toExponential.toString();
+
+ return [{ ...action.payload }, ...state];
+ case 'UPDATE':
+ const updatableExpenseIndex = state.findIndex(
+ (expense) => expense.id === action.payload.id
+ );
+
+ const updatableExpense = state[updatableExpenseIndex];
+ const updatedItem = { ...updatableExpense, ...action.payload.data };
+ const updatedExpenses = [...state];
+ updatedExpenses[updatableExpenseIndex] = updatedItem;
+
+ return updatedExpenses;
+ case 'DELETE':
+ return state.expenses.filter(
+ (expense) => expense.id !== action.payload.id
+ );
+
+ default:
+ return state;
+ }
+}
+
+export default function ExpensesContextProvider({ children }) {
+ const [expensesState, dispatch] = useReducer(expensesReducer, DUMMY_EXPENSES);
+
+ function addExpense(expenseData) {
+ dispatch({ type: 'ADD', payload: expenseData });
+ }
+
+ function deleteExpense(id) {
+ dispatch({ type: 'DELETE', payload: id });
+ }
+
+ function updateExpense(id, expenseData) {
+ dispatch({ type: 'UPDATE', payload: { id: id, data: expenseData } });
+ }
+
+ const value = {
+ expenses: expensesState,
+ addExpense: addExpense,
+ deleteExpense: deleteExpense,
+ updateExpense: updateExpense
+ };
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/attachments/08-practice-app/00-starting-project/util/date.js b/attachments/08-practice-app/00-starting-project/util/date.js
new file mode 100644
index 00000000..c666432e
--- /dev/null
+++ b/attachments/08-practice-app/00-starting-project/util/date.js
@@ -0,0 +1,7 @@
+export function getFormattedDate(date) {
+ return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`;
+}
+
+export function getDateMinusDays(date, days) {
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate() - days);
+}
diff --git a/attachments/09-user-input/00-starting-project/App.js b/attachments/09-user-input/00-starting-project/App.js
new file mode 100644
index 00000000..728db090
--- /dev/null
+++ b/attachments/09-user-input/00-starting-project/App.js
@@ -0,0 +1,92 @@
+import { StatusBar } from 'expo-status-bar';
+import { NavigationContainer } from '@react-navigation/native';
+import { createNativeStackNavigator } from '@react-navigation/native-stack';
+import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
+import { Ionicons } from '@expo/vector-icons';
+
+import ManageExpense from './screens/ManageExpense';
+import RecentExpenses from './screens/RecentExpenses';
+import AllExpenses from './screens/AllExpenses';
+import { GlobalStyles } from './constants/styles';
+import IconButton from './components/UI/IconButton';
+import ExpensesContextProvider from './store/expenses-context';
+
+const Stack = createNativeStackNavigator();
+const BottomTabs = createBottomTabNavigator();
+
+function ExpensesOverview() {
+ return (
+ ({
+ headerStyle: { backgroundColor: GlobalStyles.colors.primary500 },
+ headerTintColor: 'white',
+ tabBarStyle: { backgroundColor: GlobalStyles.colors.primary500 },
+ tabBarActiveTintColor: GlobalStyles.colors.accent500,
+ headerRight: ({ tintColor }) => (
+ {
+ navigation.navigate('ManageExpense');
+ }}
+ />
+ ),
+ })}
+ >
+ (
+
+ ),
+ }}
+ />
+ (
+
+ ),
+ }}
+ />
+
+ );
+}
+
+export default function App() {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/attachments/09-user-input/00-starting-project/app.json b/attachments/09-user-input/00-starting-project/app.json
new file mode 100644
index 00000000..de352e82
--- /dev/null
+++ b/attachments/09-user-input/00-starting-project/app.json
@@ -0,0 +1,32 @@
+{
+ "expo": {
+ "name": "RNCourse",
+ "slug": "RNCourse",
+ "version": "1.0.0",
+ "orientation": "portrait",
+ "icon": "./assets/icon.png",
+ "splash": {
+ "image": "./assets/splash.png",
+ "resizeMode": "contain",
+ "backgroundColor": "#ffffff"
+ },
+ "updates": {
+ "fallbackToCacheTimeout": 0
+ },
+ "assetBundlePatterns": [
+ "**/*"
+ ],
+ "ios": {
+ "supportsTablet": true
+ },
+ "android": {
+ "adaptiveIcon": {
+ "foregroundImage": "./assets/adaptive-icon.png",
+ "backgroundColor": "#FFFFFF"
+ }
+ },
+ "web": {
+ "favicon": "./assets/favicon.png"
+ }
+ }
+}
diff --git a/attachments/09-user-input/00-starting-project/assets/adaptive-icon.png b/attachments/09-user-input/00-starting-project/assets/adaptive-icon.png
new file mode 100644
index 00000000..03d6f6b6
Binary files /dev/null and b/attachments/09-user-input/00-starting-project/assets/adaptive-icon.png differ
diff --git a/attachments/09-user-input/00-starting-project/assets/favicon.png b/attachments/09-user-input/00-starting-project/assets/favicon.png
new file mode 100644
index 00000000..e75f697b
Binary files /dev/null and b/attachments/09-user-input/00-starting-project/assets/favicon.png differ
diff --git a/attachments/09-user-input/00-starting-project/assets/icon.png b/attachments/09-user-input/00-starting-project/assets/icon.png
new file mode 100644
index 00000000..a0b1526f
Binary files /dev/null and b/attachments/09-user-input/00-starting-project/assets/icon.png differ
diff --git a/attachments/09-user-input/00-starting-project/assets/splash.png b/attachments/09-user-input/00-starting-project/assets/splash.png
new file mode 100644
index 00000000..0e89705a
Binary files /dev/null and b/attachments/09-user-input/00-starting-project/assets/splash.png differ
diff --git a/attachments/09-user-input/00-starting-project/babel.config.js b/attachments/09-user-input/00-starting-project/babel.config.js
new file mode 100644
index 00000000..e1babf6b
--- /dev/null
+++ b/attachments/09-user-input/00-starting-project/babel.config.js
@@ -0,0 +1,6 @@
+module.exports = function(api) {
+ api.cache(true);
+ return {
+ presets: ['babel-preset-expo'],
+ };
+};
diff --git a/attachments/09-user-input/00-starting-project/components/ExpensesOutput/ExpenseItem.js b/attachments/09-user-input/00-starting-project/components/ExpensesOutput/ExpenseItem.js
new file mode 100644
index 00000000..4a32d1bc
--- /dev/null
+++ b/attachments/09-user-input/00-starting-project/components/ExpensesOutput/ExpenseItem.js
@@ -0,0 +1,76 @@
+import { Pressable, StyleSheet, Text, View } from 'react-native';
+import { useNavigation } from '@react-navigation/native';
+
+import { GlobalStyles } from '../../constants/styles';
+import { getFormattedDate } from '../../util/date';
+
+function ExpenseItem({ id, description, amount, date }) {
+ const navigation = useNavigation();
+
+ function expensePressHandler() {
+ navigation.navigate('ManageExpense', {
+ expenseId: id
+ });
+ }
+
+ return (
+ pressed && styles.pressed}
+ >
+
+
+
+ {description}
+
+ {getFormattedDate(date)}
+
+
+ {amount.toFixed(2)}
+
+
+
+ );
+}
+
+export default ExpenseItem;
+
+const styles = StyleSheet.create({
+ pressed: {
+ opacity: 0.75,
+ },
+ expenseItem: {
+ padding: 12,
+ marginVertical: 8,
+ backgroundColor: GlobalStyles.colors.primary500,
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ borderRadius: 6,
+ elevation: 3,
+ shadowColor: GlobalStyles.colors.gray500,
+ shadowRadius: 4,
+ shadowOffset: { width: 1, height: 1 },
+ shadowOpacity: 0.4,
+ },
+ textBase: {
+ color: GlobalStyles.colors.primary50,
+ },
+ description: {
+ fontSize: 16,
+ marginBottom: 4,
+ fontWeight: 'bold',
+ },
+ amountContainer: {
+ paddingHorizontal: 12,
+ paddingVertical: 4,
+ backgroundColor: 'white',
+ justifyContent: 'center',
+ alignItems: 'center',
+ borderRadius: 4,
+ minWidth: 80,
+ },
+ amount: {
+ color: GlobalStyles.colors.primary500,
+ fontWeight: 'bold',
+ },
+});
diff --git a/attachments/09-user-input/00-starting-project/components/ExpensesOutput/ExpensesList.js b/attachments/09-user-input/00-starting-project/components/ExpensesOutput/ExpensesList.js
new file mode 100644
index 00000000..80519f75
--- /dev/null
+++ b/attachments/09-user-input/00-starting-project/components/ExpensesOutput/ExpensesList.js
@@ -0,0 +1,19 @@
+import { FlatList } from 'react-native';
+
+import ExpenseItem from './ExpenseItem';
+
+function renderExpenseItem(itemData) {
+ return ;
+}
+
+function ExpensesList({ expenses }) {
+ return (
+ item.id}
+ />
+ );
+}
+
+export default ExpensesList;
diff --git a/attachments/09-user-input/00-starting-project/components/ExpensesOutput/ExpensesOutput.js b/attachments/09-user-input/00-starting-project/components/ExpensesOutput/ExpensesOutput.js
new file mode 100644
index 00000000..675d12f2
--- /dev/null
+++ b/attachments/09-user-input/00-starting-project/components/ExpensesOutput/ExpensesOutput.js
@@ -0,0 +1,38 @@
+import { StyleSheet, Text, View } from 'react-native';
+
+import { GlobalStyles } from '../../constants/styles';
+import ExpensesList from './ExpensesList';
+import ExpensesSummary from './ExpensesSummary';
+
+function ExpensesOutput({ expenses, expensesPeriod, fallbackText }) {
+ let content = {fallbackText};
+
+ if (expenses.length > 0) {
+ content = ;
+ }
+
+ return (
+
+
+ {content}
+
+ );
+}
+
+export default ExpensesOutput;
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ paddingHorizontal: 24,
+ paddingTop: 24,
+ paddingBottom: 0,
+ backgroundColor: GlobalStyles.colors.primary700,
+ },
+ infoText: {
+ color: 'white',
+ fontSize: 16,
+ textAlign: 'center',
+ marginTop: 32,
+ },
+});
diff --git a/attachments/09-user-input/00-starting-project/components/ExpensesOutput/ExpensesSummary.js b/attachments/09-user-input/00-starting-project/components/ExpensesOutput/ExpensesSummary.js
new file mode 100644
index 00000000..33fdbfa0
--- /dev/null
+++ b/attachments/09-user-input/00-starting-project/components/ExpensesOutput/ExpensesSummary.js
@@ -0,0 +1,38 @@
+import { View, Text, StyleSheet } from 'react-native';
+
+import { GlobalStyles } from '../../constants/styles';
+
+function ExpensesSummary({ expenses, periodName }) {
+ const expensesSum = expenses.reduce((sum, expense) => {
+ return sum + expense.amount;
+ }, 0);
+
+ return (
+
+ {periodName}
+ ${expensesSum.toFixed(2)}
+
+ );
+}
+
+export default ExpensesSummary;
+
+const styles = StyleSheet.create({
+ container: {
+ padding: 8,
+ backgroundColor: GlobalStyles.colors.primary50,
+ borderRadius: 6,
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ },
+ period: {
+ fontSize: 12,
+ color: GlobalStyles.colors.primary400,
+ },
+ sum: {
+ fontSize: 16,
+ fontWeight: 'bold',
+ color: GlobalStyles.colors.primary500,
+ },
+});
diff --git a/attachments/09-user-input/00-starting-project/components/ManageExpense/ExpenseForm.js b/attachments/09-user-input/00-starting-project/components/ManageExpense/ExpenseForm.js
new file mode 100644
index 00000000..527f6624
--- /dev/null
+++ b/attachments/09-user-input/00-starting-project/components/ManageExpense/ExpenseForm.js
@@ -0,0 +1,162 @@
+import { View, StyleSheet, Text, Alert } from 'react-native';
+import Input from './Input';
+import { isValidElement, useState } from 'react';
+import Button from '../UI/Button';
+import { getFormattedDate } from '../../util/date';
+import { GlobalStyles } from '../../constants/styles';
+
+function ExpenseForm({ submitButtonLabel, onCancel, onSubmit, defaultValues }) {
+ const [inputs, setInputs] = useState({
+ amount: {
+ value: defaultValues ? defaultValues.amount.toString() : '',
+ isValid: true
+ },
+ date: {
+ value: defaultValues ? getFormattedDate(defaultValues.date) : '',
+ isValid: true
+ },
+ description: {
+ value: defaultValues ? defaultValues.description : '',
+ isValid: true
+ }
+ });
+
+ function inputChangedHandler(inputIdentifier, enteredValue) {
+ setInputs((currentInputValues) => {
+ return {
+ ...currentInputValues,
+ [inputIdentifier]: { value: enteredValue, isValid: true }
+ };
+ });
+ }
+
+ function submitHandler() {
+ const expenseData = {
+ amount: +inputs.amount.value,
+ date: new Date(inputs.date.value),
+ description: inputs.description.value
+ };
+
+ const amountIsValid = !isNaN(expenseData.amount) && expenseData.amount > 0;
+ const dateIsValid = expenseData.date.toString() !== 'Invalid Date';
+ const descriptionIsValid = expenseData.description.trim().length > 0;
+
+ if (!amountIsValid || !dateIsValid || !descriptionIsValid) {
+ // Dit is een manier om feedback te geven aan de user maar er zijn nog andere manieren
+ // Alert.alert('Invalid input', 'Please check you input values');
+
+ setInputs((currentInputs) => {
+ return {
+ amount: {
+ value: currentInputs.amount.value,
+ isValid: amountIsValid
+ },
+ date: { value: currentInputs.date.value, isValid: dateIsValid },
+ description: {
+ value: currentInputs.description.value,
+ isValid: descriptionIsValid
+ }
+ };
+ });
+ return;
+ }
+
+ onSubmit(expenseData);
+ }
+
+ const formIsInvalid =
+ !inputs.amount.isValid ||
+ !inputs.date.isValid ||
+ !inputs.description.isValid;
+
+ return (
+
+ Your Expense
+
+
+
+
+
+
+ {formIsInvalid && (
+
+ Invalid input values - please check your entered data!{' '}
+
+ )}
+
+
+
+
+
+
+ );
+}
+
+export default ExpenseForm;
+
+const styles = StyleSheet.create({
+ form: {
+ marginTop: 40
+ },
+ title: {
+ marginVertical: 24,
+ textAlign: 'center',
+ fontSize: 24,
+ fontWeight: 'bold',
+ color: 'white'
+ },
+ inputsRow: {
+ flexDirection: 'row',
+ justifyContent: 'space-between'
+ },
+ rowInput: {
+ flex: 1
+ },
+ errorText: {
+ textAlign: 'center',
+ color: GlobalStyles.colors.error500,
+ margin: 8
+ },
+ buttons: {
+ flexDirection: 'row',
+ justifyContent: 'center',
+ alignItems: 'center'
+ },
+ button: {
+ minWidth: 120,
+ marginHorizontal: 8
+ }
+});
diff --git a/attachments/09-user-input/00-starting-project/components/ManageExpense/Input.js b/attachments/09-user-input/00-starting-project/components/ManageExpense/Input.js
new file mode 100644
index 00000000..3c1fec6b
--- /dev/null
+++ b/attachments/09-user-input/00-starting-project/components/ManageExpense/Input.js
@@ -0,0 +1,54 @@
+import { View, Text, StyleSheet, TextInput } from 'react-native';
+import { GlobalStyles } from '../../constants/styles';
+
+function Input({ label, invalid, style, textInputConfig }) {
+ const inputStyles = [styles.input];
+
+ if (textInputConfig && textInputConfig.multiline) {
+ inputStyles.push(styles.inputMultiline);
+ }
+
+ if (invalid) {
+ inputStyles.push(styles.invalidInput);
+ }
+
+ return (
+
+
+ {label}
+
+
+
+ );
+}
+
+export default Input;
+
+const styles = StyleSheet.create({
+ inputContainer: {
+ marginHorizontal: 4,
+ marginVertical: 8
+ },
+ label: {
+ fontSize: 12,
+ color: GlobalStyles.colors.primary100,
+ marginBottom: 4
+ },
+ input: {
+ backgroundColor: GlobalStyles.colors.primary100,
+ padding: 6,
+ borderRadius: 6,
+ fontSize: 18,
+ color: GlobalStyles.colors.primary700
+ },
+ inputMultiline: {
+ minHeight: 100,
+ textAlignVertical: 'top'
+ },
+ invalidLabel: {
+ color: GlobalStyles.colors.error500
+ },
+ invalidInput: {
+ backgroundColor: GlobalStyles.colors.error50
+ }
+});
diff --git a/attachments/09-user-input/00-starting-project/components/UI/Button.js b/attachments/09-user-input/00-starting-project/components/UI/Button.js
new file mode 100644
index 00000000..16e27972
--- /dev/null
+++ b/attachments/09-user-input/00-starting-project/components/UI/Button.js
@@ -0,0 +1,44 @@
+import { Pressable, StyleSheet, Text, View } from 'react-native';
+import { GlobalStyles } from '../../constants/styles';
+
+function Button({ children, onPress, mode, style }) {
+ return (
+
+ pressed && styles.pressed}
+ >
+
+
+ {children}
+
+
+
+
+ );
+}
+
+export default Button;
+
+const styles = StyleSheet.create({
+ button: {
+ borderRadius: 4,
+ padding: 8,
+ backgroundColor: GlobalStyles.colors.primary500,
+ },
+ flat: {
+ backgroundColor: 'transparent',
+ },
+ buttonText: {
+ color: 'white',
+ textAlign: 'center',
+ },
+ flatText: {
+ color: GlobalStyles.colors.primary200,
+ },
+ pressed: {
+ opacity: 0.75,
+ backgroundColor: GlobalStyles.colors.primary100,
+ borderRadius: 4,
+ },
+});
diff --git a/attachments/09-user-input/00-starting-project/components/UI/IconButton.js b/attachments/09-user-input/00-starting-project/components/UI/IconButton.js
new file mode 100644
index 00000000..e0dd633e
--- /dev/null
+++ b/attachments/09-user-input/00-starting-project/components/UI/IconButton.js
@@ -0,0 +1,29 @@
+import { Pressable, StyleSheet, View } from 'react-native';
+import { Ionicons } from '@expo/vector-icons';
+
+function IconButton({ icon, size, color, onPress }) {
+ return (
+ pressed && styles.pressed}
+ >
+
+
+
+
+ );
+}
+
+export default IconButton;
+
+const styles = StyleSheet.create({
+ buttonContainer: {
+ borderRadius: 24,
+ padding: 6,
+ marginHorizontal: 8,
+ marginVertical: 2
+ },
+ pressed: {
+ opacity: 0.75,
+ },
+});
diff --git a/attachments/09-user-input/00-starting-project/constants/styles.js b/attachments/09-user-input/00-starting-project/constants/styles.js
new file mode 100644
index 00000000..323f3002
--- /dev/null
+++ b/attachments/09-user-input/00-starting-project/constants/styles.js
@@ -0,0 +1,16 @@
+export const GlobalStyles = {
+ colors: {
+ primary50: '#e4d9fd',
+ primary100: '#c6affc',
+ primary200: '#a281f0',
+ primary400: '#5721d4',
+ primary500: '#3e04c3',
+ primary700: '#2d0689',
+ primary800: '#200364',
+ accent500: '#f7bc0c',
+ error50: '#fcc4e4',
+ error500: '#9b095c',
+ gray500: '#39324a',
+ gray700: '#221c30',
+ },
+};
diff --git a/attachments/09-user-input/00-starting-project/package.json b/attachments/09-user-input/00-starting-project/package.json
new file mode 100644
index 00000000..813998ec
--- /dev/null
+++ b/attachments/09-user-input/00-starting-project/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "rncourse",
+ "version": "1.0.0",
+ "main": "node_modules/expo/AppEntry.js",
+ "scripts": {
+ "start": "expo start",
+ "android": "expo start --android",
+ "ios": "expo start --ios",
+ "web": "expo start --web",
+ "eject": "expo eject"
+ },
+ "dependencies": {
+ "@react-navigation/bottom-tabs": "^6.2.0",
+ "@react-navigation/native": "^6.0.8",
+ "@react-navigation/native-stack": "^6.5.0",
+ "expo": "^52.0.0",
+ "expo-status-bar": "~2.0.1",
+ "react": "18.3.1",
+ "react-dom": "18.3.1",
+ "react-native": "0.76.7",
+ "react-native-safe-area-context": "4.12.0",
+ "react-native-screens": "~4.4.0",
+ "react-native-web": "~0.19.13"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.25.2"
+ },
+ "private": true
+}
diff --git a/attachments/09-user-input/00-starting-project/screens/AllExpenses.js b/attachments/09-user-input/00-starting-project/screens/AllExpenses.js
new file mode 100644
index 00000000..7cb8f019
--- /dev/null
+++ b/attachments/09-user-input/00-starting-project/screens/AllExpenses.js
@@ -0,0 +1,18 @@
+import { useContext } from 'react';
+
+import ExpensesOutput from '../components/ExpensesOutput/ExpensesOutput';
+import { ExpensesContext } from '../store/expenses-context';
+
+function AllExpenses() {
+ const expensesCtx = useContext(ExpensesContext);
+
+ return (
+
+ );
+}
+
+export default AllExpenses;
diff --git a/attachments/09-user-input/00-starting-project/screens/ManageExpense.js b/attachments/09-user-input/00-starting-project/screens/ManageExpense.js
new file mode 100644
index 00000000..0d19189b
--- /dev/null
+++ b/attachments/09-user-input/00-starting-project/screens/ManageExpense.js
@@ -0,0 +1,81 @@
+import { useContext, useLayoutEffect } from 'react';
+import { StyleSheet, View, TextInput } from 'react-native';
+
+import IconButton from '../components/UI/IconButton';
+import { GlobalStyles } from '../constants/styles';
+import { ExpensesContext } from '../store/expenses-context';
+import ExpenseForm from '../components/ManageExpense/ExpenseForm';
+
+function ManageExpense({ route, navigation }) {
+ const expensesCtx = useContext(ExpensesContext);
+
+ const editedExpenseId = route.params?.expenseId;
+ const isEditing = !!editedExpenseId;
+
+ const selectedExpense = expensesCtx.expenses.find(
+ (expense) => expense.id === editedExpenseId
+ );
+
+ useLayoutEffect(() => {
+ navigation.setOptions({
+ title: isEditing ? 'Edit Expense' : 'Add Expense'
+ });
+ }, [navigation, isEditing]);
+
+ function deleteExpenseHandler() {
+ expensesCtx.deleteExpense(editedExpenseId);
+ navigation.goBack();
+ }
+
+ function cancelHandler() {
+ navigation.goBack();
+ }
+
+ function confirmHandler(expenseData) {
+ if (isEditing) {
+ expensesCtx.updateExpense(editedExpenseId, expenseData);
+ } else {
+ expensesCtx.addExpense(expenseData);
+ }
+ navigation.goBack();
+ }
+
+ return (
+
+
+
+ {isEditing && (
+
+
+
+ )}
+
+ );
+}
+
+export default ManageExpense;
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ padding: 24,
+ backgroundColor: GlobalStyles.colors.primary800
+ },
+ deleteContainer: {
+ marginTop: 16,
+ paddingTop: 8,
+ borderTopWidth: 2,
+ borderTopColor: GlobalStyles.colors.primary200,
+ alignItems: 'center'
+ }
+});
diff --git a/attachments/09-user-input/00-starting-project/screens/RecentExpenses.js b/attachments/09-user-input/00-starting-project/screens/RecentExpenses.js
new file mode 100644
index 00000000..05d7935e
--- /dev/null
+++ b/attachments/09-user-input/00-starting-project/screens/RecentExpenses.js
@@ -0,0 +1,26 @@
+import { useContext } from 'react';
+
+import ExpensesOutput from '../components/ExpensesOutput/ExpensesOutput';
+import { ExpensesContext } from '../store/expenses-context';
+import { getDateMinusDays } from '../util/date';
+
+function RecentExpenses() {
+ const expensesCtx = useContext(ExpensesContext);
+
+ const recentExpenses = expensesCtx.expenses.filter((expense) => {
+ const today = new Date();
+ const date7DaysAgo = getDateMinusDays(today, 7);
+
+ return expense.date >= date7DaysAgo && expense.date <= today;
+ });
+
+ return (
+
+ );
+}
+
+export default RecentExpenses;
diff --git a/attachments/09-user-input/00-starting-project/store/expenses-context.js b/attachments/09-user-input/00-starting-project/store/expenses-context.js
new file mode 100644
index 00000000..61100e3d
--- /dev/null
+++ b/attachments/09-user-input/00-starting-project/store/expenses-context.js
@@ -0,0 +1,117 @@
+import { createContext, useReducer } from 'react';
+
+const DUMMY_EXPENSES = [
+ {
+ id: 'e1',
+ description: 'A pair of shoes',
+ amount: 59.99,
+ date: new Date('2021-12-19'),
+ },
+ {
+ id: 'e2',
+ description: 'A pair of trousers',
+ amount: 89.29,
+ date: new Date('2022-01-05'),
+ },
+ {
+ id: 'e3',
+ description: 'Some bananas',
+ amount: 5.99,
+ date: new Date('2021-12-01'),
+ },
+ {
+ id: 'e4',
+ description: 'A book',
+ amount: 14.99,
+ date: new Date('2022-02-19'),
+ },
+ {
+ id: 'e5',
+ description: 'Another book',
+ amount: 18.59,
+ date: new Date('2022-02-18'),
+ },
+ {
+ id: 'e6',
+ description: 'A pair of trousers',
+ amount: 89.29,
+ date: new Date('2022-01-05'),
+ },
+ {
+ id: 'e7',
+ description: 'Some bananas',
+ amount: 5.99,
+ date: new Date('2021-12-01'),
+ },
+ {
+ id: 'e8',
+ description: 'A book',
+ amount: 14.99,
+ date: new Date('2022-02-19'),
+ },
+ {
+ id: 'e9',
+ description: 'Another book',
+ amount: 18.59,
+ date: new Date('2022-02-18'),
+ },
+];
+
+export const ExpensesContext = createContext({
+ expenses: [],
+ addExpense: ({ description, amount, date }) => {},
+ deleteExpense: (id) => {},
+ updateExpense: (id, { description, amount, date }) => {},
+});
+
+function expensesReducer(state, action) {
+ switch (action.type) {
+ case 'ADD':
+ const id = new Date().toString() + Math.random().toString();
+ return [{ ...action.payload, id: id }, ...state];
+ case 'UPDATE':
+ const updatableExpenseIndex = state.findIndex(
+ (expense) => expense.id === action.payload.id
+ );
+ const updatableExpense = state[updatableExpenseIndex];
+ const updatedItem = { ...updatableExpense, ...action.payload.data };
+ const updatedExpenses = [...state];
+ updatedExpenses[updatableExpenseIndex] = updatedItem;
+ return updatedExpenses;
+ case 'DELETE':
+ return state.filter((expense) => expense.id !== action.payload);
+ default:
+ return state;
+ }
+}
+
+function ExpensesContextProvider({ children }) {
+ const [expensesState, dispatch] = useReducer(expensesReducer, DUMMY_EXPENSES);
+
+ function addExpense(expenseData) {
+ dispatch({ type: 'ADD', payload: expenseData });
+ }
+
+ function deleteExpense(id) {
+ dispatch({ type: 'DELETE', payload: id });
+ }
+
+ function updateExpense(id, expenseData) {
+ dispatch({ type: 'UPDATE', payload: { id: id, data: expenseData } });
+ }
+
+ const value = {
+ expenses: expensesState,
+ addExpense: addExpense,
+ deleteExpense: deleteExpense,
+ updateExpense: updateExpense,
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+export default ExpensesContextProvider;
diff --git a/attachments/09-user-input/00-starting-project/util/date.js b/attachments/09-user-input/00-starting-project/util/date.js
new file mode 100644
index 00000000..cd4de2aa
--- /dev/null
+++ b/attachments/09-user-input/00-starting-project/util/date.js
@@ -0,0 +1,8 @@
+export function getFormattedDate(date) {
+ // return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`;
+ return date.toISOString().slice(0, 10);
+}
+
+export function getDateMinusDays(date, days) {
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate() - days);
+}
diff --git a/attachments/10-http/00-starting-project/App.js b/attachments/10-http/00-starting-project/App.js
new file mode 100644
index 00000000..728db090
--- /dev/null
+++ b/attachments/10-http/00-starting-project/App.js
@@ -0,0 +1,92 @@
+import { StatusBar } from 'expo-status-bar';
+import { NavigationContainer } from '@react-navigation/native';
+import { createNativeStackNavigator } from '@react-navigation/native-stack';
+import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
+import { Ionicons } from '@expo/vector-icons';
+
+import ManageExpense from './screens/ManageExpense';
+import RecentExpenses from './screens/RecentExpenses';
+import AllExpenses from './screens/AllExpenses';
+import { GlobalStyles } from './constants/styles';
+import IconButton from './components/UI/IconButton';
+import ExpensesContextProvider from './store/expenses-context';
+
+const Stack = createNativeStackNavigator();
+const BottomTabs = createBottomTabNavigator();
+
+function ExpensesOverview() {
+ return (
+ ({
+ headerStyle: { backgroundColor: GlobalStyles.colors.primary500 },
+ headerTintColor: 'white',
+ tabBarStyle: { backgroundColor: GlobalStyles.colors.primary500 },
+ tabBarActiveTintColor: GlobalStyles.colors.accent500,
+ headerRight: ({ tintColor }) => (
+ {
+ navigation.navigate('ManageExpense');
+ }}
+ />
+ ),
+ })}
+ >
+ (
+
+ ),
+ }}
+ />
+ (
+
+ ),
+ }}
+ />
+
+ );
+}
+
+export default function App() {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/attachments/10-http/00-starting-project/app.json b/attachments/10-http/00-starting-project/app.json
new file mode 100644
index 00000000..de352e82
--- /dev/null
+++ b/attachments/10-http/00-starting-project/app.json
@@ -0,0 +1,32 @@
+{
+ "expo": {
+ "name": "RNCourse",
+ "slug": "RNCourse",
+ "version": "1.0.0",
+ "orientation": "portrait",
+ "icon": "./assets/icon.png",
+ "splash": {
+ "image": "./assets/splash.png",
+ "resizeMode": "contain",
+ "backgroundColor": "#ffffff"
+ },
+ "updates": {
+ "fallbackToCacheTimeout": 0
+ },
+ "assetBundlePatterns": [
+ "**/*"
+ ],
+ "ios": {
+ "supportsTablet": true
+ },
+ "android": {
+ "adaptiveIcon": {
+ "foregroundImage": "./assets/adaptive-icon.png",
+ "backgroundColor": "#FFFFFF"
+ }
+ },
+ "web": {
+ "favicon": "./assets/favicon.png"
+ }
+ }
+}
diff --git a/attachments/10-http/00-starting-project/assets/adaptive-icon.png b/attachments/10-http/00-starting-project/assets/adaptive-icon.png
new file mode 100644
index 00000000..03d6f6b6
Binary files /dev/null and b/attachments/10-http/00-starting-project/assets/adaptive-icon.png differ
diff --git a/attachments/10-http/00-starting-project/assets/favicon.png b/attachments/10-http/00-starting-project/assets/favicon.png
new file mode 100644
index 00000000..e75f697b
Binary files /dev/null and b/attachments/10-http/00-starting-project/assets/favicon.png differ
diff --git a/attachments/10-http/00-starting-project/assets/icon.png b/attachments/10-http/00-starting-project/assets/icon.png
new file mode 100644
index 00000000..a0b1526f
Binary files /dev/null and b/attachments/10-http/00-starting-project/assets/icon.png differ
diff --git a/attachments/10-http/00-starting-project/assets/splash.png b/attachments/10-http/00-starting-project/assets/splash.png
new file mode 100644
index 00000000..0e89705a
Binary files /dev/null and b/attachments/10-http/00-starting-project/assets/splash.png differ
diff --git a/attachments/10-http/00-starting-project/babel.config.js b/attachments/10-http/00-starting-project/babel.config.js
new file mode 100644
index 00000000..e1babf6b
--- /dev/null
+++ b/attachments/10-http/00-starting-project/babel.config.js
@@ -0,0 +1,6 @@
+module.exports = function(api) {
+ api.cache(true);
+ return {
+ presets: ['babel-preset-expo'],
+ };
+};
diff --git a/attachments/10-http/00-starting-project/components/ExpensesOutput/ExpenseItem.js b/attachments/10-http/00-starting-project/components/ExpensesOutput/ExpenseItem.js
new file mode 100644
index 00000000..4a32d1bc
--- /dev/null
+++ b/attachments/10-http/00-starting-project/components/ExpensesOutput/ExpenseItem.js
@@ -0,0 +1,76 @@
+import { Pressable, StyleSheet, Text, View } from 'react-native';
+import { useNavigation } from '@react-navigation/native';
+
+import { GlobalStyles } from '../../constants/styles';
+import { getFormattedDate } from '../../util/date';
+
+function ExpenseItem({ id, description, amount, date }) {
+ const navigation = useNavigation();
+
+ function expensePressHandler() {
+ navigation.navigate('ManageExpense', {
+ expenseId: id
+ });
+ }
+
+ return (
+ pressed && styles.pressed}
+ >
+
+
+
+ {description}
+
+ {getFormattedDate(date)}
+
+
+ {amount.toFixed(2)}
+
+
+
+ );
+}
+
+export default ExpenseItem;
+
+const styles = StyleSheet.create({
+ pressed: {
+ opacity: 0.75,
+ },
+ expenseItem: {
+ padding: 12,
+ marginVertical: 8,
+ backgroundColor: GlobalStyles.colors.primary500,
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ borderRadius: 6,
+ elevation: 3,
+ shadowColor: GlobalStyles.colors.gray500,
+ shadowRadius: 4,
+ shadowOffset: { width: 1, height: 1 },
+ shadowOpacity: 0.4,
+ },
+ textBase: {
+ color: GlobalStyles.colors.primary50,
+ },
+ description: {
+ fontSize: 16,
+ marginBottom: 4,
+ fontWeight: 'bold',
+ },
+ amountContainer: {
+ paddingHorizontal: 12,
+ paddingVertical: 4,
+ backgroundColor: 'white',
+ justifyContent: 'center',
+ alignItems: 'center',
+ borderRadius: 4,
+ minWidth: 80,
+ },
+ amount: {
+ color: GlobalStyles.colors.primary500,
+ fontWeight: 'bold',
+ },
+});
diff --git a/attachments/10-http/00-starting-project/components/ExpensesOutput/ExpensesList.js b/attachments/10-http/00-starting-project/components/ExpensesOutput/ExpensesList.js
new file mode 100644
index 00000000..80519f75
--- /dev/null
+++ b/attachments/10-http/00-starting-project/components/ExpensesOutput/ExpensesList.js
@@ -0,0 +1,19 @@
+import { FlatList } from 'react-native';
+
+import ExpenseItem from './ExpenseItem';
+
+function renderExpenseItem(itemData) {
+ return ;
+}
+
+function ExpensesList({ expenses }) {
+ return (
+ item.id}
+ />
+ );
+}
+
+export default ExpensesList;
diff --git a/attachments/10-http/00-starting-project/components/ExpensesOutput/ExpensesOutput.js b/attachments/10-http/00-starting-project/components/ExpensesOutput/ExpensesOutput.js
new file mode 100644
index 00000000..675d12f2
--- /dev/null
+++ b/attachments/10-http/00-starting-project/components/ExpensesOutput/ExpensesOutput.js
@@ -0,0 +1,38 @@
+import { StyleSheet, Text, View } from 'react-native';
+
+import { GlobalStyles } from '../../constants/styles';
+import ExpensesList from './ExpensesList';
+import ExpensesSummary from './ExpensesSummary';
+
+function ExpensesOutput({ expenses, expensesPeriod, fallbackText }) {
+ let content = {fallbackText};
+
+ if (expenses.length > 0) {
+ content = ;
+ }
+
+ return (
+
+
+ {content}
+
+ );
+}
+
+export default ExpensesOutput;
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ paddingHorizontal: 24,
+ paddingTop: 24,
+ paddingBottom: 0,
+ backgroundColor: GlobalStyles.colors.primary700,
+ },
+ infoText: {
+ color: 'white',
+ fontSize: 16,
+ textAlign: 'center',
+ marginTop: 32,
+ },
+});
diff --git a/attachments/10-http/00-starting-project/components/ExpensesOutput/ExpensesSummary.js b/attachments/10-http/00-starting-project/components/ExpensesOutput/ExpensesSummary.js
new file mode 100644
index 00000000..33fdbfa0
--- /dev/null
+++ b/attachments/10-http/00-starting-project/components/ExpensesOutput/ExpensesSummary.js
@@ -0,0 +1,38 @@
+import { View, Text, StyleSheet } from 'react-native';
+
+import { GlobalStyles } from '../../constants/styles';
+
+function ExpensesSummary({ expenses, periodName }) {
+ const expensesSum = expenses.reduce((sum, expense) => {
+ return sum + expense.amount;
+ }, 0);
+
+ return (
+
+ {periodName}
+ ${expensesSum.toFixed(2)}
+
+ );
+}
+
+export default ExpensesSummary;
+
+const styles = StyleSheet.create({
+ container: {
+ padding: 8,
+ backgroundColor: GlobalStyles.colors.primary50,
+ borderRadius: 6,
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ },
+ period: {
+ fontSize: 12,
+ color: GlobalStyles.colors.primary400,
+ },
+ sum: {
+ fontSize: 16,
+ fontWeight: 'bold',
+ color: GlobalStyles.colors.primary500,
+ },
+});
diff --git a/attachments/10-http/00-starting-project/components/ManageExpense/ExpenseForm.js b/attachments/10-http/00-starting-project/components/ManageExpense/ExpenseForm.js
new file mode 100644
index 00000000..a1a0a361
--- /dev/null
+++ b/attachments/10-http/00-starting-project/components/ManageExpense/ExpenseForm.js
@@ -0,0 +1,156 @@
+import { useState } from 'react';
+import { StyleSheet, Text, View } from 'react-native';
+
+import Input from './Input';
+import Button from '../UI/Button';
+import { getFormattedDate } from '../../util/date';
+import { GlobalStyles } from '../../constants/styles';
+
+function ExpenseForm({ submitButtonLabel, onCancel, onSubmit, defaultValues }) {
+ const [inputs, setInputs] = useState({
+ amount: {
+ value: defaultValues ? defaultValues.amount.toString() : '',
+ isValid: true,
+ },
+ date: {
+ value: defaultValues ? getFormattedDate(defaultValues.date) : '',
+ isValid: true,
+ },
+ description: {
+ value: defaultValues ? defaultValues.description : '',
+ isValid: true,
+ },
+ });
+
+ function inputChangedHandler(inputIdentifier, enteredValue) {
+ setInputs((curInputs) => {
+ return {
+ ...curInputs,
+ [inputIdentifier]: { value: enteredValue, isValid: true },
+ };
+ });
+ }
+
+ function submitHandler() {
+ const expenseData = {
+ amount: +inputs.amount.value,
+ date: new Date(inputs.date.value),
+ description: inputs.description.value,
+ };
+
+ const amountIsValid = !isNaN(expenseData.amount) && expenseData.amount > 0;
+ const dateIsValid = expenseData.date.toString() !== 'Invalid Date';
+ const descriptionIsValid = expenseData.description.trim().length > 0;
+
+ if (!amountIsValid || !dateIsValid || !descriptionIsValid) {
+ // Alert.alert('Invalid input', 'Please check your input values');
+ setInputs((curInputs) => {
+ return {
+ amount: { value: curInputs.amount.value, isValid: amountIsValid },
+ date: { value: curInputs.date.value, isValid: dateIsValid },
+ description: {
+ value: curInputs.description.value,
+ isValid: descriptionIsValid,
+ },
+ };
+ });
+ return;
+ }
+
+ onSubmit(expenseData);
+ }
+
+ const formIsInvalid =
+ !inputs.amount.isValid ||
+ !inputs.date.isValid ||
+ !inputs.description.isValid;
+
+ return (
+
+ Your Expense
+
+
+
+
+
+ {formIsInvalid && (
+
+ Invalid input values - please check your entered data!
+
+ )}
+
+
+
+
+
+ );
+}
+
+export default ExpenseForm;
+
+const styles = StyleSheet.create({
+ form: {
+ marginTop: 40,
+ },
+ title: {
+ fontSize: 24,
+ fontWeight: 'bold',
+ color: 'white',
+ marginVertical: 24,
+ textAlign: 'center',
+ },
+ inputsRow: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ },
+ rowInput: {
+ flex: 1,
+ },
+ errorText: {
+ textAlign: 'center',
+ color: GlobalStyles.colors.error500,
+ margin: 8,
+ },
+ buttons: {
+ flexDirection: 'row',
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ button: {
+ minWidth: 120,
+ marginHorizontal: 8,
+ },
+});
diff --git a/attachments/10-http/00-starting-project/components/ManageExpense/Input.js b/attachments/10-http/00-starting-project/components/ManageExpense/Input.js
new file mode 100644
index 00000000..5093a0bd
--- /dev/null
+++ b/attachments/10-http/00-starting-project/components/ManageExpense/Input.js
@@ -0,0 +1,54 @@
+import { StyleSheet, Text, TextInput, View } from 'react-native';
+
+import { GlobalStyles } from '../../constants/styles';
+
+function Input({ label, invalid, style, textInputConfig }) {
+
+ const inputStyles = [styles.input];
+
+ if (textInputConfig && textInputConfig.multiline) {
+ inputStyles.push(styles.inputMultiline)
+ }
+
+ if (invalid) {
+ inputStyles.push(styles.invalidInput);
+ }
+
+ return (
+
+ {label}
+
+
+ );
+}
+
+export default Input;
+
+const styles = StyleSheet.create({
+ inputContainer: {
+ marginHorizontal: 4,
+ marginVertical: 8
+ },
+ label: {
+ fontSize: 12,
+ color: GlobalStyles.colors.primary100,
+ marginBottom: 4,
+ },
+ input: {
+ backgroundColor: GlobalStyles.colors.primary100,
+ color: GlobalStyles.colors.primary700,
+ padding: 6,
+ borderRadius: 6,
+ fontSize: 18,
+ },
+ inputMultiline: {
+ minHeight: 100,
+ textAlignVertical: 'top'
+ },
+ invalidLabel: {
+ color: GlobalStyles.colors.error500
+ },
+ invalidInput: {
+ backgroundColor: GlobalStyles.colors.error50
+ }
+});
diff --git a/attachments/10-http/00-starting-project/components/UI/Button.js b/attachments/10-http/00-starting-project/components/UI/Button.js
new file mode 100644
index 00000000..16e27972
--- /dev/null
+++ b/attachments/10-http/00-starting-project/components/UI/Button.js
@@ -0,0 +1,44 @@
+import { Pressable, StyleSheet, Text, View } from 'react-native';
+import { GlobalStyles } from '../../constants/styles';
+
+function Button({ children, onPress, mode, style }) {
+ return (
+
+ pressed && styles.pressed}
+ >
+
+
+ {children}
+
+
+
+
+ );
+}
+
+export default Button;
+
+const styles = StyleSheet.create({
+ button: {
+ borderRadius: 4,
+ padding: 8,
+ backgroundColor: GlobalStyles.colors.primary500,
+ },
+ flat: {
+ backgroundColor: 'transparent',
+ },
+ buttonText: {
+ color: 'white',
+ textAlign: 'center',
+ },
+ flatText: {
+ color: GlobalStyles.colors.primary200,
+ },
+ pressed: {
+ opacity: 0.75,
+ backgroundColor: GlobalStyles.colors.primary100,
+ borderRadius: 4,
+ },
+});
diff --git a/attachments/10-http/00-starting-project/components/UI/ErrorOverlay.js b/attachments/10-http/00-starting-project/components/UI/ErrorOverlay.js
new file mode 100644
index 00000000..ca76b020
--- /dev/null
+++ b/attachments/10-http/00-starting-project/components/UI/ErrorOverlay.js
@@ -0,0 +1,31 @@
+import { View, Text, StyleSheet } from 'react-native';
+import { GlobalStyles } from '../../constants/styles';
+
+function ErrorOverlay({ message }) {
+ return (
+
+ An error occurred!
+ {message}
+
+ );
+}
+
+export default ErrorOverlay;
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 24,
+ backgroundColor: GlobalStyles.colors.primary700
+ },
+ text: {
+ textAlign: 'center',
+ marginBottom: 8
+ },
+ title: {
+ fontSize: 20,
+ fontWeight: 'bold'
+ }
+});
diff --git a/attachments/10-http/00-starting-project/components/UI/IconButton.js b/attachments/10-http/00-starting-project/components/UI/IconButton.js
new file mode 100644
index 00000000..e0dd633e
--- /dev/null
+++ b/attachments/10-http/00-starting-project/components/UI/IconButton.js
@@ -0,0 +1,29 @@
+import { Pressable, StyleSheet, View } from 'react-native';
+import { Ionicons } from '@expo/vector-icons';
+
+function IconButton({ icon, size, color, onPress }) {
+ return (
+ pressed && styles.pressed}
+ >
+
+
+
+
+ );
+}
+
+export default IconButton;
+
+const styles = StyleSheet.create({
+ buttonContainer: {
+ borderRadius: 24,
+ padding: 6,
+ marginHorizontal: 8,
+ marginVertical: 2
+ },
+ pressed: {
+ opacity: 0.75,
+ },
+});
diff --git a/attachments/10-http/00-starting-project/components/UI/LoadingOverlay.js b/attachments/10-http/00-starting-project/components/UI/LoadingOverlay.js
new file mode 100644
index 00000000..2135d7af
--- /dev/null
+++ b/attachments/10-http/00-starting-project/components/UI/LoadingOverlay.js
@@ -0,0 +1,23 @@
+import React from 'react';
+import { View, ActivityIndicator, StyleSheet } from 'react-native';
+import { GlobalStyles } from '../../constants/styles';
+
+function LoadingOverlay() {
+ return (
+
+
+
+ );
+}
+
+export default LoadingOverlay;
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 24,
+ backgroundColor: GlobalStyles.colors.primary700
+ }
+});
diff --git a/attachments/10-http/00-starting-project/constants/styles.js b/attachments/10-http/00-starting-project/constants/styles.js
new file mode 100644
index 00000000..323f3002
--- /dev/null
+++ b/attachments/10-http/00-starting-project/constants/styles.js
@@ -0,0 +1,16 @@
+export const GlobalStyles = {
+ colors: {
+ primary50: '#e4d9fd',
+ primary100: '#c6affc',
+ primary200: '#a281f0',
+ primary400: '#5721d4',
+ primary500: '#3e04c3',
+ primary700: '#2d0689',
+ primary800: '#200364',
+ accent500: '#f7bc0c',
+ error50: '#fcc4e4',
+ error500: '#9b095c',
+ gray500: '#39324a',
+ gray700: '#221c30',
+ },
+};
diff --git a/attachments/10-http/00-starting-project/package.json b/attachments/10-http/00-starting-project/package.json
new file mode 100644
index 00000000..58970c6b
--- /dev/null
+++ b/attachments/10-http/00-starting-project/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "rncourse",
+ "version": "1.0.0",
+ "main": "node_modules/expo/AppEntry.js",
+ "scripts": {
+ "start": "expo start",
+ "android": "expo start --android",
+ "ios": "expo start --ios",
+ "web": "expo start --web",
+ "eject": "expo eject"
+ },
+ "dependencies": {
+ "@react-navigation/bottom-tabs": "^6.2.0",
+ "@react-navigation/native": "^6.0.8",
+ "@react-navigation/native-stack": "^6.5.0",
+ "axios": "^1.13.2",
+ "expo": "^52.0.0",
+ "expo-status-bar": "~2.0.1",
+ "react": "18.3.1",
+ "react-dom": "18.3.1",
+ "react-native": "0.76.7",
+ "react-native-safe-area-context": "4.12.0",
+ "react-native-screens": "~4.4.0",
+ "react-native-web": "~0.19.13"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.25.2"
+ },
+ "private": true
+}
diff --git a/attachments/10-http/00-starting-project/screens/AllExpenses.js b/attachments/10-http/00-starting-project/screens/AllExpenses.js
new file mode 100644
index 00000000..7cb8f019
--- /dev/null
+++ b/attachments/10-http/00-starting-project/screens/AllExpenses.js
@@ -0,0 +1,18 @@
+import { useContext } from 'react';
+
+import ExpensesOutput from '../components/ExpensesOutput/ExpensesOutput';
+import { ExpensesContext } from '../store/expenses-context';
+
+function AllExpenses() {
+ const expensesCtx = useContext(ExpensesContext);
+
+ return (
+
+ );
+}
+
+export default AllExpenses;
diff --git a/attachments/10-http/00-starting-project/screens/ManageExpense.js b/attachments/10-http/00-starting-project/screens/ManageExpense.js
new file mode 100644
index 00000000..9d6ea805
--- /dev/null
+++ b/attachments/10-http/00-starting-project/screens/ManageExpense.js
@@ -0,0 +1,113 @@
+import { useContext, useLayoutEffect, useState } from 'react';
+import { StyleSheet, TextInput, View } from 'react-native';
+
+import ExpenseForm from '../components/ManageExpense/ExpenseForm';
+import Button from '../components/UI/Button';
+import IconButton from '../components/UI/IconButton';
+import { GlobalStyles } from '../constants/styles';
+import { ExpensesContext } from '../store/expenses-context';
+import { deleteExpense, storeExpense, updateExpense } from '../util/http';
+import LoadingOverlay from '../components/UI/LoadingOverlay';
+import ErrorOverlay from '../components/UI/ErrorOverlay';
+
+function ManageExpense({ route, navigation }) {
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [error, setError] = useState();
+ const expensesCtx = useContext(ExpensesContext);
+
+ const editedExpenseId = route.params?.expenseId;
+ const isEditing = !!editedExpenseId;
+
+ const selectedExpense = expensesCtx.expenses.find(
+ (expense) => expense.id === editedExpenseId
+ );
+
+ useLayoutEffect(() => {
+ navigation.setOptions({
+ title: isEditing ? 'Edit Expense' : 'Add Expense'
+ });
+ }, [navigation, isEditing]);
+
+ async function deleteExpenseHandler() {
+ setIsSubmitting(true);
+
+ try {
+ await deleteExpense(editedExpenseId);
+ expensesCtx.deleteExpense(editedExpenseId);
+
+ navigation.goBack();
+ } catch (error) {
+ setError('Could not delete expense - please try again later');
+ setIsSubmitting(false);
+ }
+ }
+
+ function cancelHandler() {
+ navigation.goBack();
+ }
+
+ async function confirmHandler(expenseData) {
+ setIsSubmitting(true);
+
+ try {
+ if (isEditing) {
+ expensesCtx.updateExpense(editedExpenseId, expenseData);
+
+ await updateExpense(expenseId, expenseData);
+ } else {
+ const id = await storeExpense(expenseData);
+ expensesCtx.addExpense({ ...expenseData, id: id });
+ }
+ navigation.goBack();
+ } catch (error) {
+ setError('Could not save data - please try again later!');
+ // setIsSubmitting(false);
+ }
+ }
+
+ if (error && !isSubmitting) {
+ return ;
+ }
+
+ if (isSubmitting) {
+ return ;
+ }
+
+ return (
+
+
+ {isEditing && (
+
+
+
+ )}
+
+ );
+}
+
+export default ManageExpense;
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ padding: 24,
+ backgroundColor: GlobalStyles.colors.primary800
+ },
+ deleteContainer: {
+ marginTop: 16,
+ paddingTop: 8,
+ borderTopWidth: 2,
+ borderTopColor: GlobalStyles.colors.primary200,
+ alignItems: 'center'
+ }
+});
diff --git a/attachments/10-http/00-starting-project/screens/RecentExpenses.js b/attachments/10-http/00-starting-project/screens/RecentExpenses.js
new file mode 100644
index 00000000..5b315acd
--- /dev/null
+++ b/attachments/10-http/00-starting-project/screens/RecentExpenses.js
@@ -0,0 +1,55 @@
+import { useContext, useEffect, useState } from 'react';
+
+import ExpensesOutput from '../components/ExpensesOutput/ExpensesOutput';
+import { ExpensesContext } from '../store/expenses-context';
+import { getDateMinusDays } from '../util/date';
+import { fetchExpenses } from '../util/http';
+import LoadingOverlay from '../components/UI/LoadingOverlay';
+import ErrorOverlay from '../components/UI/ErrorOverlay';
+
+function RecentExpenses() {
+ const [isFetching, setIsFetching] = useState(true); // true because when this component is loading for the first time we are fetching data
+ const [error, setError] = useState();
+ const expensesCtx = useContext(ExpensesContext);
+
+ useEffect(() => {
+ async function getExpenses() {
+ setIsFetching(true);
+ try {
+ const expenses = await fetchExpenses();
+ expensesCtx.setExpenses(expenses);
+ } catch (error) {
+ setError('Could not fetch expenses!');
+ }
+
+ setIsFetching(false);
+ }
+
+ getExpenses();
+ }, []);
+
+ if (error && !isFetching) {
+ return ;
+ }
+
+ if (isFetching) {
+ return ;
+ }
+
+ const recentExpenses = expensesCtx.expenses.filter((expense) => {
+ const today = new Date();
+ const date7DaysAgo = getDateMinusDays(today, 7);
+
+ return expense.date >= date7DaysAgo && expense.date <= today;
+ });
+
+ return (
+
+ );
+}
+
+export default RecentExpenses;
diff --git a/attachments/10-http/00-starting-project/store/expenses-context.js b/attachments/10-http/00-starting-project/store/expenses-context.js
new file mode 100644
index 00000000..7948e6cd
--- /dev/null
+++ b/attachments/10-http/00-starting-project/store/expenses-context.js
@@ -0,0 +1,68 @@
+import { createContext, useReducer } from 'react';
+
+export const ExpensesContext = createContext({
+ expenses: [],
+ addExpense: ({ description, amount, date }) => {},
+ setExpenses: (expenses) => {},
+ deleteExpense: (id) => {},
+ updateExpense: (id, { description, amount, date }) => {}
+});
+
+function expensesReducer(state, action) {
+ switch (action.type) {
+ case 'ADD':
+ return [{ ...action.payload }, ...state];
+ case 'SET':
+ const inverted = action.payload.reverse();
+ return inverted;
+ case 'UPDATE':
+ const updatableExpenseIndex = state.findIndex(
+ (expense) => expense.id === action.payload.id
+ );
+ const updatableExpense = state[updatableExpenseIndex];
+ const updatedItem = { ...updatableExpense, ...action.payload.data };
+ const updatedExpenses = [...state];
+ updatedExpenses[updatableExpenseIndex] = updatedItem;
+ return updatedExpenses;
+ case 'DELETE':
+ return state.filter((expense) => expense.id !== action.payload);
+ default:
+ return state;
+ }
+}
+
+function ExpensesContextProvider({ children }) {
+ const [expensesState, dispatch] = useReducer(expensesReducer, []);
+
+ function addExpense(expenseData) {
+ dispatch({ type: 'ADD', payload: expenseData });
+ }
+
+ function setExpenses(expenses) {
+ dispatch({ type: 'SET', payload: expenses });
+ }
+
+ function deleteExpense(id) {
+ dispatch({ type: 'DELETE', payload: id });
+ }
+
+ function updateExpense(id, expenseData) {
+ dispatch({ type: 'UPDATE', payload: { id: id, data: expenseData } });
+ }
+
+ const value = {
+ expenses: expensesState,
+ addExpense: addExpense,
+ setExpenses: setExpenses,
+ deleteExpense: deleteExpense,
+ updateExpense: updateExpense
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+export default ExpensesContextProvider;
diff --git a/attachments/10-http/00-starting-project/util/date.js b/attachments/10-http/00-starting-project/util/date.js
new file mode 100644
index 00000000..4abc38c5
--- /dev/null
+++ b/attachments/10-http/00-starting-project/util/date.js
@@ -0,0 +1,7 @@
+export function getFormattedDate(date) {
+ return date.toISOString().slice(0, 10);
+}
+
+export function getDateMinusDays(date, days) {
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate() - days);
+}
diff --git a/attachments/10-http/00-starting-project/util/http.js b/attachments/10-http/00-starting-project/util/http.js
new file mode 100644
index 00000000..c0741250
--- /dev/null
+++ b/attachments/10-http/00-starting-project/util/http.js
@@ -0,0 +1,40 @@
+import axios from 'axios';
+
+const BACKEND_URL =
+ 'https://react-native-course-e6af0-default-rtdb.europe-west1.firebasedatabase.app';
+
+export async function storeExpense(expenseData) {
+ const response = await axios.post(
+ BACKEND_URL + '/expenses.json',
+ expenseData
+ );
+
+ // this is the id for the firebase object, id is generated by firebase when we add an expense
+ const id = response.data.name;
+ return id;
+}
+
+export async function fetchExpenses() {
+ const response = await axios.get(BACKEND_URL + '/expenses.json');
+ const expenses = [];
+
+ for (const key in response.data) {
+ const expenseObj = {
+ id: key,
+ amount: response.data[key].amount,
+ date: new Date(response.data[key].date),
+ description: response.data[key].description
+ };
+ expenses.push(expenseObj);
+ }
+
+ return expenses;
+}
+
+export function updateExpense(id, expenseData) {
+ return axios.put(BACKEND_URL + `/expenses/${id}.json`, expenseData);
+}
+
+export function deleteExpense(id) {
+ return axios.delete(BACKEND_URL + `/expenses/${id}.json`, expenseData);
+}
diff --git a/attachments/11-auth/00-starting-project/App.js b/attachments/11-auth/00-starting-project/App.js
new file mode 100644
index 00000000..c44b5ff0
--- /dev/null
+++ b/attachments/11-auth/00-starting-project/App.js
@@ -0,0 +1,115 @@
+import { NavigationContainer } from '@react-navigation/native';
+import { createNativeStackNavigator } from '@react-navigation/native-stack';
+import { StatusBar } from 'expo-status-bar';
+
+import LoginScreen from './screens/LoginScreen';
+import SignupScreen from './screens/SignupScreen';
+import WelcomeScreen from './screens/WelcomeScreen';
+import { Colors } from './constants/styles';
+import AuthContextProvider, { AuthContext } from './store/auth-context';
+import { useContext, useEffect, useState } from 'react';
+
+import IconButton from './components/ui/IconButton';
+import AsyncStorage from '@react-native-async-storage/async-storage';
+import AppLoading from 'expo-app-loading';
+
+const Stack = createNativeStackNavigator();
+
+function AuthStack() {
+ return (
+
+
+
+
+ );
+}
+// We can add route protection by simply putting this screen into its own screen setup and only rendering this navigator if a certain condition is met
+function AuthenticatedStack() {
+ const authContext = useContext(AuthContext);
+ return (
+
+ (
+
+ )
+ }}
+ />
+
+ );
+}
+
+function Navigation() {
+ const authContext = useContext(AuthContext);
+
+ let content = ;
+
+ if (authContext.isAuthenticated) {
+ content = ;
+ }
+
+ return (
+
+ {/* */}
+ {content}
+
+ );
+}
+
+// We added this use component so we can update our AuthContext when we have fetched the token, we then return the component and use the component in the App function instead of the
+// The adventage off this approach is that we can now use the AppLoading component to prolong the loading screen until we're done fetching this token, to do this we need to install the package expo-app-loading (expo install expo-app-loading)
+function Root() {
+ const [isTryingLogin, setIsTryingLogin] = useState(true);
+ const authContext = useContext(AuthContext);
+
+ useEffect(() => {
+ async function fetchToken() {
+ const storedToken = await AsyncStorage.getItem('token');
+
+ if (storedToken) {
+ authContext.authenticate(storedToken);
+ }
+
+ setIsTryingLogin(false);
+ }
+
+ fetchToken();
+ }, []);
+
+ if (isTryingLogin) {
+ // this (AppLoading) will make sure that the splashscreen is prolonged
+ return ;
+ }
+
+ return ;
+}
+
+export default function App() {
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
diff --git a/attachments/11-auth/00-starting-project/app.json b/attachments/11-auth/00-starting-project/app.json
new file mode 100644
index 00000000..de352e82
--- /dev/null
+++ b/attachments/11-auth/00-starting-project/app.json
@@ -0,0 +1,32 @@
+{
+ "expo": {
+ "name": "RNCourse",
+ "slug": "RNCourse",
+ "version": "1.0.0",
+ "orientation": "portrait",
+ "icon": "./assets/icon.png",
+ "splash": {
+ "image": "./assets/splash.png",
+ "resizeMode": "contain",
+ "backgroundColor": "#ffffff"
+ },
+ "updates": {
+ "fallbackToCacheTimeout": 0
+ },
+ "assetBundlePatterns": [
+ "**/*"
+ ],
+ "ios": {
+ "supportsTablet": true
+ },
+ "android": {
+ "adaptiveIcon": {
+ "foregroundImage": "./assets/adaptive-icon.png",
+ "backgroundColor": "#FFFFFF"
+ }
+ },
+ "web": {
+ "favicon": "./assets/favicon.png"
+ }
+ }
+}
diff --git a/attachments/11-auth/00-starting-project/assets/adaptive-icon.png b/attachments/11-auth/00-starting-project/assets/adaptive-icon.png
new file mode 100644
index 00000000..03d6f6b6
Binary files /dev/null and b/attachments/11-auth/00-starting-project/assets/adaptive-icon.png differ
diff --git a/attachments/11-auth/00-starting-project/assets/favicon.png b/attachments/11-auth/00-starting-project/assets/favicon.png
new file mode 100644
index 00000000..e75f697b
Binary files /dev/null and b/attachments/11-auth/00-starting-project/assets/favicon.png differ
diff --git a/attachments/11-auth/00-starting-project/assets/icon.png b/attachments/11-auth/00-starting-project/assets/icon.png
new file mode 100644
index 00000000..a0b1526f
Binary files /dev/null and b/attachments/11-auth/00-starting-project/assets/icon.png differ
diff --git a/attachments/11-auth/00-starting-project/assets/splash.png b/attachments/11-auth/00-starting-project/assets/splash.png
new file mode 100644
index 00000000..0e89705a
Binary files /dev/null and b/attachments/11-auth/00-starting-project/assets/splash.png differ
diff --git a/attachments/11-auth/00-starting-project/babel.config.js b/attachments/11-auth/00-starting-project/babel.config.js
new file mode 100644
index 00000000..e1babf6b
--- /dev/null
+++ b/attachments/11-auth/00-starting-project/babel.config.js
@@ -0,0 +1,6 @@
+module.exports = function(api) {
+ api.cache(true);
+ return {
+ presets: ['babel-preset-expo'],
+ };
+};
diff --git a/attachments/11-auth/00-starting-project/components/Auth/AuthContent.js b/attachments/11-auth/00-starting-project/components/Auth/AuthContent.js
new file mode 100644
index 00000000..ab5801ef
--- /dev/null
+++ b/attachments/11-auth/00-starting-project/components/Auth/AuthContent.js
@@ -0,0 +1,91 @@
+import { useState } from 'react';
+import { Alert, StyleSheet, View } from 'react-native';
+
+import FlatButton from '../ui/FlatButton';
+import AuthForm from './AuthForm';
+import { Colors } from '../../constants/styles';
+
+import { useNavigation } from '@react-navigation/native';
+
+function AuthContent({ isLogin, onAuthenticate }) {
+ const navigation = useNavigation();
+
+ const [credentialsInvalid, setCredentialsInvalid] = useState({
+ email: false,
+ password: false,
+ confirmEmail: false,
+ confirmPassword: false
+ });
+
+ function switchAuthModeHandler() {
+ if (isLogin) {
+ navigation.navigate('Signup');
+ // We kunnen ook de replace method gebruiken ipv navigate, dit zorgt ervoor dat er geen back button is! Het voegt geen scherm toe aan de stack of screens maar vervangt het huidige scherm met het nieuwe scherm
+ } else {
+ navigation.navigate('Login');
+ }
+ }
+
+ function submitHandler(credentials) {
+ let { email, confirmEmail, password, confirmPassword } = credentials;
+
+ email = email.trim();
+ password = password.trim();
+
+ const emailIsValid = email.includes('@');
+ const passwordIsValid = password.length > 6;
+ const emailsAreEqual = email === confirmEmail;
+ const passwordsAreEqual = password === confirmPassword;
+
+ if (
+ !emailIsValid ||
+ !passwordIsValid ||
+ (!isLogin && (!emailsAreEqual || !passwordsAreEqual))
+ ) {
+ Alert.alert('Invalid input', 'Please check your entered credentials.');
+ setCredentialsInvalid({
+ email: !emailIsValid,
+ confirmEmail: !emailIsValid || !emailsAreEqual,
+ password: !passwordIsValid,
+ confirmPassword: !passwordIsValid || !passwordsAreEqual
+ });
+ return;
+ }
+ onAuthenticate({ email, password });
+ }
+
+ return (
+
+
+
+
+ {isLogin ? 'Create a new user' : 'Log in instead'}
+
+
+
+ );
+}
+
+export default AuthContent;
+
+const styles = StyleSheet.create({
+ authContent: {
+ marginTop: 64,
+ marginHorizontal: 32,
+ padding: 16,
+ borderRadius: 8,
+ backgroundColor: Colors.primary800,
+ elevation: 2,
+ shadowColor: 'black',
+ shadowOffset: { width: 1, height: 1 },
+ shadowOpacity: 0.35,
+ shadowRadius: 4
+ },
+ buttons: {
+ marginTop: 8
+ }
+});
diff --git a/attachments/11-auth/00-starting-project/components/Auth/AuthForm.js b/attachments/11-auth/00-starting-project/components/Auth/AuthForm.js
new file mode 100644
index 00000000..02979775
--- /dev/null
+++ b/attachments/11-auth/00-starting-project/components/Auth/AuthForm.js
@@ -0,0 +1,100 @@
+import { useState } from 'react';
+import { StyleSheet, View } from 'react-native';
+
+import Button from '../ui/Button';
+import Input from './Input';
+
+function AuthForm({ isLogin, onSubmit, credentialsInvalid }) {
+ const [enteredEmail, setEnteredEmail] = useState('');
+ const [enteredConfirmEmail, setEnteredConfirmEmail] = useState('');
+ const [enteredPassword, setEnteredPassword] = useState('');
+ const [enteredConfirmPassword, setEnteredConfirmPassword] = useState('');
+
+ const {
+ email: emailIsInvalid,
+ confirmEmail: emailsDontMatch,
+ password: passwordIsInvalid,
+ confirmPassword: passwordsDontMatch,
+ } = credentialsInvalid;
+
+ function updateInputValueHandler(inputType, enteredValue) {
+ switch (inputType) {
+ case 'email':
+ setEnteredEmail(enteredValue);
+ break;
+ case 'confirmEmail':
+ setEnteredConfirmEmail(enteredValue);
+ break;
+ case 'password':
+ setEnteredPassword(enteredValue);
+ break;
+ case 'confirmPassword':
+ setEnteredConfirmPassword(enteredValue);
+ break;
+ }
+ }
+
+ function submitHandler() {
+ onSubmit({
+ email: enteredEmail,
+ confirmEmail: enteredConfirmEmail,
+ password: enteredPassword,
+ confirmPassword: enteredConfirmPassword,
+ });
+ }
+
+ return (
+
+
+
+ {!isLogin && (
+
+ )}
+
+ {!isLogin && (
+
+ )}
+
+
+
+
+
+ );
+}
+
+export default AuthForm;
+
+const styles = StyleSheet.create({
+ buttons: {
+ marginTop: 12,
+ },
+});
diff --git a/attachments/11-auth/00-starting-project/components/Auth/Input.js b/attachments/11-auth/00-starting-project/components/Auth/Input.js
new file mode 100644
index 00000000..1de9878e
--- /dev/null
+++ b/attachments/11-auth/00-starting-project/components/Auth/Input.js
@@ -0,0 +1,53 @@
+import { View, Text, TextInput, StyleSheet } from 'react-native';
+
+import { Colors } from '../../constants/styles';
+
+function Input({
+ label,
+ keyboardType,
+ secure,
+ onUpdateValue,
+ value,
+ isInvalid
+}) {
+ return (
+
+
+ {label}
+
+
+
+ );
+}
+
+export default Input;
+
+const styles = StyleSheet.create({
+ inputContainer: {
+ marginVertical: 8
+ },
+ label: {
+ color: 'white',
+ marginBottom: 4
+ },
+ labelInvalid: {
+ color: Colors.error500
+ },
+ input: {
+ paddingVertical: 8,
+ paddingHorizontal: 6,
+ backgroundColor: Colors.primary100,
+ borderRadius: 4,
+ fontSize: 16
+ },
+ inputInvalid: {
+ backgroundColor: Colors.error100
+ }
+});
diff --git a/attachments/11-auth/00-starting-project/components/ui/Button.js b/attachments/11-auth/00-starting-project/components/ui/Button.js
new file mode 100644
index 00000000..7dc391dd
--- /dev/null
+++ b/attachments/11-auth/00-starting-project/components/ui/Button.js
@@ -0,0 +1,41 @@
+import { Pressable, StyleSheet, Text, View } from 'react-native';
+
+import { Colors } from '../../constants/styles';
+
+function Button({ children, onPress }) {
+ return (
+ [styles.button, pressed && styles.pressed]}
+ onPress={onPress}
+ >
+
+ {children}
+
+
+ );
+}
+
+export default Button;
+
+const styles = StyleSheet.create({
+ button: {
+ borderRadius: 6,
+ paddingVertical: 6,
+ paddingHorizontal: 12,
+ backgroundColor: Colors.primary500,
+ elevation: 2,
+ shadowColor: 'black',
+ shadowOffset: { width: 1, height: 1 },
+ shadowOpacity: 0.25,
+ shadowRadius: 4,
+ },
+ pressed: {
+ opacity: 0.7,
+ },
+ buttonText: {
+ textAlign: 'center',
+ color: 'white',
+ fontSize: 16,
+ fontWeight: 'bold'
+ },
+});
diff --git a/attachments/11-auth/00-starting-project/components/ui/FlatButton.js b/attachments/11-auth/00-starting-project/components/ui/FlatButton.js
new file mode 100644
index 00000000..4aad4287
--- /dev/null
+++ b/attachments/11-auth/00-starting-project/components/ui/FlatButton.js
@@ -0,0 +1,32 @@
+import { Pressable, StyleSheet, Text, View } from 'react-native';
+
+import { Colors } from '../../constants/styles';
+
+function FlatButton({ children, onPress }) {
+ return (
+ [styles.button, pressed && styles.pressed]}
+ onPress={onPress}
+ >
+
+ {children}
+
+
+ );
+}
+
+export default FlatButton;
+
+const styles = StyleSheet.create({
+ button: {
+ paddingVertical: 6,
+ paddingHorizontal: 12,
+ },
+ pressed: {
+ opacity: 0.7,
+ },
+ buttonText: {
+ textAlign: 'center',
+ color: Colors.primary100,
+ },
+});
diff --git a/attachments/11-auth/00-starting-project/components/ui/IconButton.js b/attachments/11-auth/00-starting-project/components/ui/IconButton.js
new file mode 100644
index 00000000..3cdf769b
--- /dev/null
+++ b/attachments/11-auth/00-starting-project/components/ui/IconButton.js
@@ -0,0 +1,25 @@
+import { Pressable, StyleSheet } from 'react-native';
+import { Ionicons } from '@expo/vector-icons';
+
+function IconButton({ icon, color, size, onPress }) {
+ return (
+ [styles.button, pressed && styles.pressed]}
+ onPress={onPress}
+ >
+
+
+ );
+}
+
+export default IconButton;
+
+const styles = StyleSheet.create({
+ button: {
+ margin: 8,
+ borderRadius: 20,
+ },
+ pressed: {
+ opacity: 0.7,
+ },
+});
diff --git a/attachments/11-auth/00-starting-project/components/ui/LoadingOverlay.js b/attachments/11-auth/00-starting-project/components/ui/LoadingOverlay.js
new file mode 100644
index 00000000..2d8a87ae
--- /dev/null
+++ b/attachments/11-auth/00-starting-project/components/ui/LoadingOverlay.js
@@ -0,0 +1,25 @@
+import { ActivityIndicator, StyleSheet, Text, View } from 'react-native';
+
+function LoadingOverlay({ message }) {
+ return (
+
+ {message}
+
+
+ );
+}
+
+export default LoadingOverlay;
+
+const styles = StyleSheet.create({
+ rootContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 32,
+ },
+ message: {
+ fontSize: 16,
+ marginBottom: 12,
+ },
+});
diff --git a/attachments/11-auth/00-starting-project/constants/styles.js b/attachments/11-auth/00-starting-project/constants/styles.js
new file mode 100644
index 00000000..cca02d18
--- /dev/null
+++ b/attachments/11-auth/00-starting-project/constants/styles.js
@@ -0,0 +1,7 @@
+export const Colors = {
+ primary100: '#f9beda',
+ primary500: '#c30b64',
+ primary800: '#610440',
+ error100: '#fcdcbf',
+ error500: '#f37c13',
+}
\ No newline at end of file
diff --git a/attachments/11-auth/00-starting-project/package.json b/attachments/11-auth/00-starting-project/package.json
new file mode 100644
index 00000000..3c1a8b19
--- /dev/null
+++ b/attachments/11-auth/00-starting-project/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "rncourse",
+ "version": "1.0.0",
+ "main": "node_modules/expo/AppEntry.js",
+ "scripts": {
+ "start": "expo start",
+ "android": "expo start --android",
+ "ios": "expo start --ios",
+ "web": "expo start --web",
+ "eject": "expo eject"
+ },
+ "dependencies": {
+ "@react-native-async-storage/async-storage": "^2.2.0",
+ "@react-navigation/native": "^6.0.8",
+ "@react-navigation/native-stack": "^6.5.0",
+ "axios": "^1.13.2",
+ "expo": "^52.0.0",
+ "expo-app-loading": "^2.1.1",
+ "expo-status-bar": "~2.0.1",
+ "react": "18.3.1",
+ "react-dom": "18.3.1",
+ "react-native": "0.76.7",
+ "react-native-safe-area-context": "4.12.0",
+ "react-native-screens": "~4.4.0",
+ "react-native-web": "~0.19.13"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.25.2"
+ },
+ "private": true
+}
diff --git a/attachments/11-auth/00-starting-project/screens/LoginScreen.js b/attachments/11-auth/00-starting-project/screens/LoginScreen.js
new file mode 100644
index 00000000..ce7de1ad
--- /dev/null
+++ b/attachments/11-auth/00-starting-project/screens/LoginScreen.js
@@ -0,0 +1,35 @@
+import { useContext, useState } from 'react';
+import AuthContent from '../components/Auth/AuthContent';
+import LoadingOverlay from '../components/ui/LoadingOverlay';
+import { login } from '../util/Auth';
+import { Alert } from 'react-native';
+import { AuthContext } from '../store/auth-context';
+
+// import { login } from '../util/auth';
+
+function LoginScreen() {
+ const [isAuthenticating, setIsAuthenticating] = useState(false);
+ const authContext = useContext(AuthContext);
+
+ async function loginHandler({ email, passwword }) {
+ setIsAuthenticating(true);
+ try {
+ const token = await login(email, passwword);
+ authContext.authenticate(token);
+ } catch {
+ Alert.alert(
+ 'Authentication failed!',
+ 'Could not log you in. Please check your credentials or try again later!'
+ );
+ setIsAuthenticating(false);
+ }
+ }
+
+ if (isAuthenticating) {
+ return ;
+ }
+
+ return ;
+}
+
+export default LoginScreen;
diff --git a/attachments/11-auth/00-starting-project/screens/SignupScreen.js b/attachments/11-auth/00-starting-project/screens/SignupScreen.js
new file mode 100644
index 00000000..bdd96045
--- /dev/null
+++ b/attachments/11-auth/00-starting-project/screens/SignupScreen.js
@@ -0,0 +1,33 @@
+import { useContext, useState } from 'react';
+import { Alert } from 'react-native';
+import { createUser } from '../util/Auth';
+import LoadingOverlay from '../components/ui/LoadingOverlay';
+import AuthContent from '../components/Auth/AuthContent';
+import { AuthContext } from '../store/auth-context';
+
+function SignupScreen({ email, password }) {
+ const [isAuthenticating, setIsAuthenticating] = useState(false);
+ const authContext = useContext(AuthContext);
+
+ async function signupHandler() {
+ setIsAuthenticating(true);
+ try {
+ const token = await createUser(email, password);
+ authContext.authenticate(token);
+ } catch (error) {
+ Alert.alert(
+ 'Authentication failed!',
+ 'Could not create user, please your input and try again later.'
+ );
+ setIsAuthenticating(false);
+ }
+ }
+
+ if (isAuthenticating) {
+ return ;
+ }
+
+ return ;
+}
+
+export default SignupScreen;
diff --git a/attachments/11-auth/00-starting-project/screens/WelcomeScreen.js b/attachments/11-auth/00-starting-project/screens/WelcomeScreen.js
new file mode 100644
index 00000000..2c5ab201
--- /dev/null
+++ b/attachments/11-auth/00-starting-project/screens/WelcomeScreen.js
@@ -0,0 +1,46 @@
+import { StyleSheet, Text, View } from 'react-native';
+import axios from 'axios';
+import { useContext, useState } from 'react';
+import { AuthContext } from '../store/auth-context';
+
+function WelcomeScreen() {
+ const [fetchedMessage, setFetchedMessage] = useState('');
+
+ const authContext = useContext(AuthContext);
+ const token = authContext.token;
+
+ useEffect(() => {
+ axios
+ .get(
+ `https://react-native-course-e6af0-default-rtdb.europe-west1.firebasedatabase.app/message.json?auth=${token}`
+ )
+ .then((response) => {
+ console.log(response.data);
+ setFetchedMessage(response.data);
+ });
+ }, [token]);
+
+ return (
+
+ Welcome!
+ You authenticated successfully!
+ {fetchedMessage}
+
+ );
+}
+
+export default WelcomeScreen;
+
+const styles = StyleSheet.create({
+ rootContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 32
+ },
+ title: {
+ fontSize: 20,
+ fontWeight: 'bold',
+ marginBottom: 8
+ }
+});
diff --git a/attachments/11-auth/00-starting-project/store/auth-context.js b/attachments/11-auth/00-starting-project/store/auth-context.js
new file mode 100644
index 00000000..bfcb988f
--- /dev/null
+++ b/attachments/11-auth/00-starting-project/store/auth-context.js
@@ -0,0 +1,33 @@
+import { createContext, useEffect, useState } from 'react';
+import AsyncStorage from '@react-native-async-storage/async-storage';
+
+export const AuthContext = createContext({
+ token: '',
+ isAuthenticated: false,
+ authenticate: (token) => {},
+ logout: () => {}
+});
+
+export default function AuthContextProvider({ children }) {
+ const [authToken, setAuthToken] = useState();
+
+ function authenticate(token) {
+ setAuthToken(token);
+ // the value (in this case token) should always be a string, if it's a number it should be converted to a string first, an object could be converted to JSON (which is a string).
+ // Using AsyncStorage we can use the token when the app loads up to see if we have a stored token
+ AsyncStorage.setItem('token', token);
+ }
+
+ function logout() {
+ setAuthToken(null);
+ AsyncStorage.removeItem('token');
+ }
+
+ const value = {
+ token: authToken,
+ isAuthenticated: !!authToken,
+ authenticate: authenticate,
+ logout: logout
+ };
+ return {children};
+}
diff --git a/attachments/11-auth/00-starting-project/util/Auth.js b/attachments/11-auth/00-starting-project/util/Auth.js
new file mode 100644
index 00000000..ead9b811
--- /dev/null
+++ b/attachments/11-auth/00-starting-project/util/Auth.js
@@ -0,0 +1,27 @@
+import axios from 'axios';
+
+const API_KEY = '';
+
+async function authenticate(mode, email, password) {
+ const url = `https://identitytoolkit.googleapis.com/v1/accounts:${mode}?key=${API_KEY}`;
+
+ const response = await axios.post(url, {
+ email: email,
+ password: password,
+ returnSecureToken: true
+ });
+
+ console.log(response.data);
+
+ const token = response.data.idToken;
+
+ return token;
+}
+
+export async function createUser(email, password) {
+ return authenticate('signUp', email, password);
+}
+
+export async function login(email, password) {
+ return authenticate('signInWithPassword', email, password);
+}
diff --git a/attachments/12-native-features/00-starting-project/App.js b/attachments/12-native-features/00-starting-project/App.js
new file mode 100644
index 00000000..8c317101
--- /dev/null
+++ b/attachments/12-native-features/00-starting-project/App.js
@@ -0,0 +1,90 @@
+import { NavigationContainer } from '@react-navigation/native';
+import { createNativeStackNavigator } from '@react-navigation/native-stack';
+import { StatusBar } from 'expo-status-bar';
+import { StyleSheet } from 'react-native';
+import AllPlaces from './screens/AllPlaces';
+import AddPlace from './screens/AddPlace';
+import IconButton from './components/UI/IconButton';
+import { Colors } from './constants/colors';
+import Map from './screens/Map';
+import { useEffect, useState } from 'react';
+import { init } from './util/database';
+import AppLoading from 'expo-app-loading';
+import PlaceDetails from './screens/PlaceDetails';
+
+const Stack = createNativeStackNavigator();
+
+export default function App() {
+ const [dbInitialized, setDbInitialized] = useState(false);
+ useEffect(() => {
+ async function initDatabase() {
+ try {
+ await init();
+ console.log('Databse initialized');
+
+ setDbInitialized(true);
+ } catch (error) {
+ console.log('Failed to initialize db with error: ', error);
+ }
+ }
+
+ initDatabase();
+ }, []);
+
+ if (!dbInitialized) {
+ return ;
+ }
+
+ return (
+ <>
+
+
+
+ ({
+ title: 'Your Favorite Places',
+ headerRight: ({ tintColor }) => (
+ navigation.navigate('AddPlace')}
+ />
+ )
+ })}
+ />
+
+
+
+
+
+ >
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: '#fff',
+ alignItems: 'center',
+ justifyContent: 'center'
+ }
+});
diff --git a/attachments/12-native-features/00-starting-project/Models/place.js b/attachments/12-native-features/00-starting-project/Models/place.js
new file mode 100644
index 00000000..a6fe6933
--- /dev/null
+++ b/attachments/12-native-features/00-starting-project/Models/place.js
@@ -0,0 +1,9 @@
+export class Place {
+ constructor(title, imageUri, location, id) {
+ this.title = title;
+ this.imageUri = imageUri;
+ this.address = location.address;
+ this.location = { lat: location.lat, lng: location.lng }; // { lat: 0.141241, lng: 127.121 }
+ this.id = id; // new Date().toString() + Math.random().toString(); // normally this would come from a BE but as we don't have one we create an id ourselves
+ }
+}
diff --git a/attachments/12-native-features/00-starting-project/app.json b/attachments/12-native-features/00-starting-project/app.json
new file mode 100644
index 00000000..5b5aea46
--- /dev/null
+++ b/attachments/12-native-features/00-starting-project/app.json
@@ -0,0 +1,41 @@
+{
+ "expo": {
+ "name": "RNCourse",
+ "slug": "RNCourse",
+ "version": "1.0.0",
+ "orientation": "portrait",
+ "icon": "./assets/icon.png",
+ "splash": {
+ "image": "./assets/splash.png",
+ "resizeMode": "contain",
+ "backgroundColor": "#ffffff"
+ },
+ "updates": {
+ "fallbackToCacheTimeout": 0
+ },
+ "assetBundlePatterns": [
+ "**/*"
+ ],
+ "ios": {
+ "supportsTablet": true
+ },
+ "android": {
+ "adaptiveIcon": {
+ "foregroundImage": "./assets/adaptive-icon.png",
+ "backgroundColor": "#FFFFFF"
+ }
+ },
+ "web": {
+ "favicon": "./assets/favicon.png"
+ },
+ "plugins": [
+ [
+ "expo-image-picker",
+ {
+ "cameraPermission": "The app needs access to your camera in order to take photos of your favorite places."
+ }
+ ],
+ "expo-sqlite"
+ ]
+ }
+}
diff --git a/attachments/12-native-features/00-starting-project/assets/adaptive-icon.png b/attachments/12-native-features/00-starting-project/assets/adaptive-icon.png
new file mode 100644
index 00000000..03d6f6b6
Binary files /dev/null and b/attachments/12-native-features/00-starting-project/assets/adaptive-icon.png differ
diff --git a/attachments/12-native-features/00-starting-project/assets/favicon.png b/attachments/12-native-features/00-starting-project/assets/favicon.png
new file mode 100644
index 00000000..e75f697b
Binary files /dev/null and b/attachments/12-native-features/00-starting-project/assets/favicon.png differ
diff --git a/attachments/12-native-features/00-starting-project/assets/icon.png b/attachments/12-native-features/00-starting-project/assets/icon.png
new file mode 100644
index 00000000..a0b1526f
Binary files /dev/null and b/attachments/12-native-features/00-starting-project/assets/icon.png differ
diff --git a/attachments/12-native-features/00-starting-project/assets/splash.png b/attachments/12-native-features/00-starting-project/assets/splash.png
new file mode 100644
index 00000000..0e89705a
Binary files /dev/null and b/attachments/12-native-features/00-starting-project/assets/splash.png differ
diff --git a/attachments/12-native-features/00-starting-project/babel.config.js b/attachments/12-native-features/00-starting-project/babel.config.js
new file mode 100644
index 00000000..e1babf6b
--- /dev/null
+++ b/attachments/12-native-features/00-starting-project/babel.config.js
@@ -0,0 +1,6 @@
+module.exports = function(api) {
+ api.cache(true);
+ return {
+ presets: ['babel-preset-expo'],
+ };
+};
diff --git a/attachments/12-native-features/00-starting-project/components/Places/ImagePicker.js b/attachments/12-native-features/00-starting-project/components/Places/ImagePicker.js
new file mode 100644
index 00000000..7931b17b
--- /dev/null
+++ b/attachments/12-native-features/00-starting-project/components/Places/ImagePicker.js
@@ -0,0 +1,85 @@
+import { Alert, Image, StyleSheet, Text, View } from 'react-native';
+import {
+ launchCameraAsync,
+ useCameraPermissions,
+ PermissionStatus
+} from 'expo-image-picker';
+import { useState } from 'react';
+import { Colors } from '../../constants/colors';
+import OutlineButton from '../UI/OutlineButton';
+
+function ImagePicker({ onTakeImage }) {
+ const [pickedImage, setPickedImage] = useState();
+ const [cameraPermissionInformation, requestPermission] =
+ useCameraPermissions();
+
+ async function verifyPermissions() {
+ if (cameraPermissionInformation.status === PermissionStatus.UNDETERMINED) {
+ // request permisison, user will give the permission or decline it
+ const permissionResponse = await requestPermission();
+
+ return permissionResponse.granted; // true or false
+ }
+
+ if (cameraPermissionInformation.status === PermissionStatus.DENIED) {
+ Alert.alert(
+ 'Insufficient Permissions',
+ 'You need to grant camera permisions to use this app.'
+ );
+ return false;
+ }
+
+ return true;
+ }
+
+ async function takeImageHandler() {
+ const hasPermission = await verifyPermissions();
+
+ if (!hasPermission) {
+ return;
+ }
+
+ const image = await launchCameraAsync({
+ allowsEditing: true,
+ aspect: [16, 9],
+ quality: 0.5
+ });
+
+ setPickedImage(image.uri);
+ onTakeImage(image.uri);
+ }
+
+ let imagePreview = No image taken yet.;
+
+ if (pickedImage) {
+ imagePreview = ;
+ }
+
+ return (
+
+ {imagePreview}
+
+
+ Take Image
+
+
+ );
+}
+
+export default ImagePicker;
+
+const styles = StyleSheet.create({
+ imagePreview: {
+ width: '100%',
+ height: 200,
+ marginVertical: 8,
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: Colors.primary100,
+ borderRadius: 4
+ },
+ image: {
+ width: '100%',
+ height: '100%'
+ }
+});
diff --git a/attachments/12-native-features/00-starting-project/components/Places/LocationPicker.js b/attachments/12-native-features/00-starting-project/components/Places/LocationPicker.js
new file mode 100644
index 00000000..f5e6682d
--- /dev/null
+++ b/attachments/12-native-features/00-starting-project/components/Places/LocationPicker.js
@@ -0,0 +1,142 @@
+import { Alert, Image, StyleSheet, Text, View } from 'react-native';
+import OutlineButton from '../UI/OutlineButton';
+import { Colors } from '../../constants/colors';
+import {
+ getCurrentPositionAsync,
+ useForegroundPermissions,
+ PermissionStatus
+} from 'expo-location';
+import { useEffect, useState } from 'react';
+import { getAddress, getMapPreview } from '../../util/location';
+import {
+ useNavigation,
+ useRoute,
+ useIsFocused
+} from '@react-navigation/native';
+
+function LocationPicker({ onPickLocation }) {
+ const [pickedLocation, setPickedLocation] = useState();
+ const isFocused = useIsFocused();
+
+ const navigation = useNavigation();
+ const route = useRoute();
+
+ const [locationPermissionInformation, requestPermission] =
+ useForegroundPermissions();
+
+ useEffect(() => {
+ if (isFocused && route.params) {
+ const mapPickedLocation = route.params && {
+ lat: route.params.pickedLat,
+ lng: route.params.pickedLng
+ };
+ setPickedLocation(mapPickedLocation);
+ }
+ }, [route, isFocused]);
+
+ useEffect(() => {
+ async function handleLocation() {
+ if (pickedLocation) {
+ const address = await getAddress(
+ pickedLocation.lat,
+ pickedLocation.lng
+ );
+ onPickLocation({ ...pickedLocation, address: address });
+ }
+ }
+
+ handleLocation();
+ }, [pickedLocation, onPickLocation]);
+
+ async function verifyPermissions() {
+ if (
+ locationPermissionInformation.status === PermissionStatus.UNDETERMINED
+ ) {
+ // request permisison, user will give the permission or decline it
+ const permissionResponse = await requestPermission();
+
+ return permissionResponse.granted; // true or false
+ }
+
+ if (locationPermissionInformation.status === PermissionStatus.DENIED) {
+ Alert.alert(
+ 'Insufficient Permissions',
+ 'You need to grant camera permisions to use this app.'
+ );
+ return false;
+ }
+
+ return true;
+ }
+
+ async function getLocationHandler() {
+ const hasPermission = await verifyPermissions();
+
+ if (!hasPermission) {
+ return;
+ }
+
+ const location = await getCurrentPositionAsync();
+
+ setPickedLocation({
+ lat: location.coords.latitude,
+ lng: location.coords.longitude
+ });
+ }
+
+ function pickOnMapHandler() {
+ navigation.navigate('Map');
+ }
+
+ let locationPreview = No location picked yet.;
+
+ if (pickedLocation) {
+ locationPreview = (
+
+ );
+ }
+
+ return (
+
+ {locationPreview}
+
+
+ Locate User
+
+
+ Pick on Map
+
+
+
+ );
+}
+
+export default LocationPicker;
+
+const styles = StyleSheet.create({
+ mapPreview: {
+ width: '100%',
+ height: 200,
+ marginVertical: 8,
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: Colors.primary100,
+ borderRadius: 4,
+ overflow: 'hidden'
+ },
+ actions: {
+ flexDirection: 'row',
+ justifyContent: 'space-around',
+ alignItems: 'center'
+ },
+ image: {
+ width: '100%',
+ height: '100%',
+ borderRadius: 4
+ }
+});
diff --git a/attachments/12-native-features/00-starting-project/components/Places/PlaceForm.js b/attachments/12-native-features/00-starting-project/components/Places/PlaceForm.js
new file mode 100644
index 00000000..25fac860
--- /dev/null
+++ b/attachments/12-native-features/00-starting-project/components/Places/PlaceForm.js
@@ -0,0 +1,75 @@
+import { useCallback, useState } from 'react';
+import { ScrollView, Text, TextInput, View, StyleSheet } from 'react-native';
+import { Colors } from '../../constants/colors';
+import ImagePicker from './ImagePicker';
+import LocationPicker from './LocationPicker';
+import Button from '../UI/Button';
+import { Place } from '../../Models/place';
+
+function PlaceForm({ onCreatePlace }) {
+ const [enteredTitle, setEnteredTitle] = useState('');
+ const [pickedLocation, setPickedLocation] = useState();
+ const [selectImage, setSelectedImage] = useState();
+
+ function changeTitleHandler(enteredText) {
+ console.log('changeTitleHanlder, enteredText: ', enteredText);
+
+ setEnteredTitle(enteredText);
+ }
+
+ function imageTakenHandler(imageUri) {
+ setSelectedImage(imageUri);
+ }
+
+ const pickLocationHandler = useCallback((location) => {
+ setPickedLocation(location);
+ }, []);
+
+ function savePlaceHandler() {
+ const placeData = new Place(enteredTitle, selectImage, pickedLocation);
+
+ onCreatePlace(placeData);
+ }
+
+ return (
+
+
+ TITLE
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default PlaceForm;
+
+const styles = StyleSheet.create({
+ form: {
+ flex: 1,
+ padding: 24
+ },
+ label: {
+ fontWeight: 'bold',
+ marginBottom: 4,
+ color: Colors.primary500
+ },
+ input: {
+ marginVertical: 8,
+ paddingHorizontal: 4,
+ paddingVertical: 8,
+ fontSize: 16,
+ borderBottomColor: Colors.primary700,
+ borderBottomWidth: 2,
+ backgroundColor: Colors.primary100
+ }
+});
diff --git a/attachments/12-native-features/00-starting-project/components/Places/PlaceItem.js b/attachments/12-native-features/00-starting-project/components/Places/PlaceItem.js
new file mode 100644
index 00000000..28333a84
--- /dev/null
+++ b/attachments/12-native-features/00-starting-project/components/Places/PlaceItem.js
@@ -0,0 +1,56 @@
+import { Image, Pressable, StyleSheet, View } from 'react-native';
+import { Colors } from '../../constants/colors';
+
+function PlaceItem({ place, onSelect }) {
+ return (
+ [styles.item, pressed && styles.pressed]}
+ onPress={onSelect.bind(this, place.id)}
+ >
+
+
+ {place.title}
+ place.address
+
+
+ );
+}
+
+export default PlaceItem;
+
+const styles = StyleSheet.create({
+ item: {
+ flexDirection: 'row',
+ alignItems: 'flex-start',
+ borderRadius: 6,
+ marginVertical: 12,
+ backgroundColor: Colors.primary500,
+ elevation: 2,
+ shadowColor: 'black',
+ shadowOpacity: 0.15,
+ shadowOffset: { width: 1, height: 1 },
+ shadowRadius: 2
+ },
+ pressed: {
+ opacity: 0.9
+ },
+ image: {
+ flex: 1,
+ borderBottomLeftRadius: 4,
+ borderTopLeftRadius: 4,
+ height: 100
+ },
+ info: {
+ flex: 2, // make this twice as big as the image (2/3 of the availble space)
+ padding: 12
+ },
+ title: {
+ fontWeight: 'bold',
+ fontSize: 18,
+ color: Colors.gray700
+ },
+ address: {
+ fontSize: 12,
+ color: Colors.gray700
+ }
+});
diff --git a/attachments/12-native-features/00-starting-project/components/Places/PlacesList.js b/attachments/12-native-features/00-starting-project/components/Places/PlacesList.js
new file mode 100644
index 00000000..b07f76a8
--- /dev/null
+++ b/attachments/12-native-features/00-starting-project/components/Places/PlacesList.js
@@ -0,0 +1,51 @@
+import { FlatList, StyleSheet, Text, View } from 'react-native';
+import PlaceItem from './PlaceItem';
+import { Colors } from '../../constants/colors';
+import { useNavigation } from '@react-navigation/native';
+
+function PlacesList({ places }) {
+ const navigation = useNavigation();
+
+ function selectPlaceHandler(id) {
+ navigation.navigate('PlaceDetails', {
+ placeId: id
+ });
+ }
+
+ if (!places || places.length === 0) {
+ return (
+
+
+ No places added yet - start adding some!
+
+
+ );
+ }
+ return (
+ item.id}
+ renderItem={({ item }) => (
+
+ )}
+ >
+ );
+}
+
+export default PlacesList;
+
+const styles = StyleSheet.create({
+ list: {
+ margin: 24
+ },
+ fallbackContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center'
+ },
+ fallbackText: {
+ fontSize: 16,
+ color: Colors.primary200
+ }
+});
diff --git a/attachments/12-native-features/00-starting-project/components/UI/Button.js b/attachments/12-native-features/00-starting-project/components/UI/Button.js
new file mode 100644
index 00000000..32e8928c
--- /dev/null
+++ b/attachments/12-native-features/00-starting-project/components/UI/Button.js
@@ -0,0 +1,37 @@
+import { Pressable, StyleSheet, Text } from 'react-native';
+import { Colors } from '../../constants/colors';
+
+function Button({ onPress, children }) {
+ return (
+ [styles.button, pressed && styles.pressed]}
+ onPress={onPress}
+ >
+ {children}
+
+ );
+}
+
+export default Button;
+const styles = StyleSheet.create({
+ button: {
+ paddingHorizontal: 12,
+ paddingVertical: 8,
+ margin: 4,
+ backgroundColor: Colors.primary800,
+ elevation: 2,
+ shadowColor: 'black',
+ shadowOpacity: 0.15,
+ shadowOffset: { width: 1, height: 1 },
+ shadowRadius: 2,
+ borderRadius: 4
+ },
+ pressed: {
+ opacity: 0.7
+ },
+ text: {
+ textAlign: 'center',
+ fontSize: 16,
+ color: Colors.primary50
+ }
+});
diff --git a/attachments/12-native-features/00-starting-project/components/UI/IconButton.js b/attachments/12-native-features/00-starting-project/components/UI/IconButton.js
new file mode 100644
index 00000000..20591371
--- /dev/null
+++ b/attachments/12-native-features/00-starting-project/components/UI/IconButton.js
@@ -0,0 +1,26 @@
+import { Pressable, StyleSheet } from 'react-native';
+import { Ionicons } from '@expo/vector-icons';
+
+function IconButton({ icon, size, color, onPress }) {
+ return (
+ [styles.button, pressed && styles.pressed]}
+ >
+
+
+ );
+}
+
+export default IconButton;
+
+const styles = StyleSheet.create({
+ button: {
+ padding: 8,
+ justifyContent: 'center',
+ alignItems: 'center'
+ },
+ pressed: {
+ opacity: 0.7
+ }
+});
diff --git a/attachments/12-native-features/00-starting-project/components/UI/OutlineButton.js b/attachments/12-native-features/00-starting-project/components/UI/OutlineButton.js
new file mode 100644
index 00000000..c08cc020
--- /dev/null
+++ b/attachments/12-native-features/00-starting-project/components/UI/OutlineButton.js
@@ -0,0 +1,45 @@
+import React from 'react';
+import { Pressable, StyleSheet, Text } from 'react-native';
+import { Ionicons } from '@expo/vector-icons';
+import { Colors } from '../../constants/colors';
+
+function OutlineButton({ onPress, icon, children }) {
+ return (
+ [styles.button, pressed && styles.pressed]}
+ onPress={onPress}
+ >
+
+ {children}
+
+ );
+}
+
+export default OutlineButton;
+
+const styles = StyleSheet.create({
+ button: {
+ paddingHorizontal: 12,
+ paddingVertical: 6,
+ margin: 4,
+ flexDirection: 'row',
+ justifyContent: 'center',
+ alignItems: 'center',
+ borderWidth: 1,
+ borderColor: Colors.primary500
+ },
+ pressed: {
+ opacity: 0.7
+ },
+ icon: {
+ marginRight: 6
+ },
+ text: {
+ color: Colors.primary500
+ }
+});
diff --git a/attachments/12-native-features/00-starting-project/constants/colors.js b/attachments/12-native-features/00-starting-project/constants/colors.js
new file mode 100644
index 00000000..4680347c
--- /dev/null
+++ b/attachments/12-native-features/00-starting-project/constants/colors.js
@@ -0,0 +1,11 @@
+export const Colors = {
+ primary50: '#cfeffd',
+ primary100: '#a0defb',
+ primary200: '#77cff8',
+ primary400: '#44bdf5',
+ primary500: '#1aacf0',
+ primary700: '#0570c9',
+ primary800: '#003b88',
+ accent500: '#e6b30b',
+ gray700: '#221c30'
+};
diff --git a/attachments/12-native-features/00-starting-project/package.json b/attachments/12-native-features/00-starting-project/package.json
new file mode 100644
index 00000000..58f5ce3d
--- /dev/null
+++ b/attachments/12-native-features/00-starting-project/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "rncourse",
+ "version": "1.0.0",
+ "main": "node_modules/expo/AppEntry.js",
+ "scripts": {
+ "start": "expo start",
+ "android": "expo start --android",
+ "ios": "expo start --ios",
+ "web": "expo start --web",
+ "eject": "expo eject"
+ },
+ "dependencies": {
+ "@react-navigation/native": "^7.1.25",
+ "@react-navigation/native-stack": "^7.8.6",
+ "expo": "^52.0.0",
+ "expo-app-loading": "^2.1.1",
+ "expo-image-picker": "~16.0.6",
+ "expo-location": "~18.0.10",
+ "expo-sqlite": "~15.1.4",
+ "expo-status-bar": "~2.0.1",
+ "react": "18.3.1",
+ "react-dom": "18.3.1",
+ "react-native": "0.76.7",
+ "react-native-maps": "1.18.0",
+ "react-native-safe-area-context": "4.12.0",
+ "react-native-screens": "~4.4.0",
+ "react-native-web": "~0.19.13"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.25.2"
+ },
+ "private": true
+}
diff --git a/attachments/12-native-features/00-starting-project/screens/AddPlace.js b/attachments/12-native-features/00-starting-project/screens/AddPlace.js
new file mode 100644
index 00000000..fe0efceb
--- /dev/null
+++ b/attachments/12-native-features/00-starting-project/screens/AddPlace.js
@@ -0,0 +1,17 @@
+import PlaceForm from '../components/Places/PlaceForm';
+import { insertPlace } from '../util/database';
+
+function AddPlace({ navigation }) {
+ async function createPlaceHandler(place) {
+ try {
+ await insertPlace(place);
+ navigation.navigate('AllPlaces');
+ } catch (error) {
+ console.log('AddPlace, failed to insert place into databse');
+ }
+ }
+
+ return ;
+}
+
+export default AddPlace;
diff --git a/attachments/12-native-features/00-starting-project/screens/AllPlaces.js b/attachments/12-native-features/00-starting-project/screens/AllPlaces.js
new file mode 100644
index 00000000..660f2e44
--- /dev/null
+++ b/attachments/12-native-features/00-starting-project/screens/AllPlaces.js
@@ -0,0 +1,26 @@
+import { useEffect, useState } from 'react';
+import PlacesList from '../components/Places/PlacesList';
+import { useIsFocused } from '@react-navigation/native';
+import { fetchPlaces } from '../util/database';
+
+function AllPlaces({ route }) {
+ const [loadedPlaces, setLoadedPlaces] = useState([]);
+ const isFocused = useIsFocused();
+
+ useEffect(() => {
+ async function loadPlaces() {
+ const places = await fetchPlaces();
+ setLoadedPlaces(places);
+ }
+
+ if (isFocused) {
+ // setLoadedPlaces((currPlaces) => [...currPlaces, route.params.place]);
+
+ loadPlaces();
+ }
+ }, [isFocused]);
+
+ return ;
+}
+
+export default AllPlaces;
diff --git a/attachments/12-native-features/00-starting-project/screens/Map.js b/attachments/12-native-features/00-starting-project/screens/Map.js
new file mode 100644
index 00000000..f4862ff9
--- /dev/null
+++ b/attachments/12-native-features/00-starting-project/screens/Map.js
@@ -0,0 +1,90 @@
+import { useCallback, useLayoutEffect, useState } from 'react';
+import { Alert, StyleSheet } from 'react-native';
+import MapView, { Marker } from 'react-native-maps';
+import IconButton from '../components/UI/IconButton';
+
+function Map({ navigation, route }) {
+ const initialLocation = route.params && {
+ lat: route.params.initialLat,
+ lng: route.params.initialLng
+ };
+
+ const [selectedLocation, setSelectedLocation] = useState(initialLocation);
+
+ const region = {
+ latitude: initialLocation ? initialLocation.lat : 37.78, // center of the map
+ longitude: initialLocation ? initialLocation.lng : -122.43,
+ latitudeDelta: 0.0922, // how much content besides the center will be visible
+ longitudeDelta: 0.0421
+ };
+
+ function selectLocationHandler(event) {
+ if (initialLocation) {
+ return;
+ }
+
+ const lat = event.nativeEvent.coordinate.latitude;
+ const lng = event.nativeEvent.coordinate.longitude;
+
+ setSelectedLocation({ lat: lat, lng: lng });
+ }
+
+ const savePickedLocationHandler = useCallback(() => {
+ if (!selectedLocation) {
+ Alert.alert(
+ 'No location picked!',
+ 'You have to pick a location (by tapping on the map) first!'
+ );
+ return;
+ }
+
+ // with the object added it will be passed to the AddPlace screen
+ navigation.navigate('AddPlace', {
+ pickedLat: selectedLocation.lat,
+ pickedLng: selectedLocation.lng
+ });
+ }, [navigation, selectedLocation]);
+
+ useLayoutEffect(() => {
+ if (initialLocation) {
+ return;
+ }
+
+ navigation.setOptions({
+ headerRight: ({ tintColor }) => (
+
+ )
+ });
+ }, [navigation, savePickedLocationHandler, initialLocation]);
+
+ return (
+
+ {selectedLocation && (
+
+ )}
+
+ );
+}
+
+export default Map;
+
+const styles = StyleSheet.create({
+ map: {
+ flex: 1
+ }
+});
diff --git a/attachments/12-native-features/00-starting-project/screens/PlaceDetails.js b/attachments/12-native-features/00-starting-project/screens/PlaceDetails.js
new file mode 100644
index 00000000..649dc1b8
--- /dev/null
+++ b/attachments/12-native-features/00-starting-project/screens/PlaceDetails.js
@@ -0,0 +1,82 @@
+import { Image, ScrollView, StyleSheet, Text, View } from 'react-native';
+import OutlineButton from '../components/UI/OutlineButton';
+import { Colors } from '../constants/colors';
+import { useEffect, useState } from 'react';
+import { fetchPlaceDetails } from '../util/database';
+
+function PlaceDetails({ route, navigation }) {
+ const [fetchedPlace, setFetchedPlace] = useState();
+ const selectedPlaceId = route.params.placeId;
+
+ useEffect(() => {
+ // use selectedPlaceId to fetch data for a single place
+
+ async function loadPlaceData() {
+ const place = fetchPlaceDetails(selectedPlaceId);
+ setFetchedPlace(place);
+
+ navigation.setOptions({
+ title: place.title
+ });
+ }
+
+ loadPlaceData();
+ }, [selectedPlaceId]);
+
+ if (!fetchedPlace) {
+ return (
+
+ Loading place data...
+
+ );
+ }
+
+ function showOnMapHandler() {
+ navigation.navigate('Map', {
+ initialLat: fetchedPlace.location.lat,
+ initialLng: fetchedPlace.location.lng
+ });
+ }
+
+ return (
+
+
+
+
+ {fetchedPlace.address}
+
+
+
+ View on Map
+
+
+ );
+}
+
+export default PlaceDetails;
+
+const styles = StyleSheet.create({
+ fallback: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center'
+ },
+ image: {
+ height: '35%',
+ minHeight: 300,
+ width: '100%'
+ },
+ locationContainer: {
+ justifyContent: 'center',
+ alignItems: 'center'
+ },
+ addressContainer: {
+ padding: 20
+ },
+ address: {
+ color: Colors.primary500,
+ textAlign: 'center',
+ fontWeight: 'bold',
+ fontSize: 16
+ }
+});
diff --git a/attachments/12-native-features/00-starting-project/util/database.js b/attachments/12-native-features/00-starting-project/util/database.js
new file mode 100644
index 00000000..ff936ea4
--- /dev/null
+++ b/attachments/12-native-features/00-starting-project/util/database.js
@@ -0,0 +1,195 @@
+import * as SQLite from 'expo-sqlite';
+import { Place } from '../Models/place';
+
+const database = SQLite.openDatabaseSync('places.db');
+
+export async function init() {
+ try {
+ database.withTransactionSync(() => {
+ database.execAsync(`
+ CREATE TABLE IF NOT EXISTS places (
+ id INTEGER PRIMARY KEY NOT NULL,
+ title TEXT NOT NULL,
+ imageUri TEXT NOT NULL,
+ address TEXT NOT NULL,
+ lat REAL NOT NULL,
+ lng REAL NOT NULL
+ );
+ `);
+ });
+ } catch (error) {
+ console.log('Database init failed:', error);
+ throw error;
+ }
+
+ // OLD CODE FROM COURSE, EXPO SQLite HAS SINCE CHANGED!
+
+ // const promise = new Promise((resolve, reject) => {
+ // database.transaction((tx) => {
+ // tx.executeSql(
+ // `CREATE TABLE IF NOT EXISTS places (
+ // id INTEGER PRIMARY KEY NOT NULL,
+ // title TEXT NOT NULL,
+ // imageUri TEXT NOT NULL,
+ // address TEXT NOT NULL,
+ // lat REAL NOT NULL,
+ // lng REAL NOT NULL
+ // )`,
+ // [],
+ // // Callback if everything succeeded
+ // () => {
+ // resolve();
+ // },
+ // // Callback if we have an error, _ is the transaction that failed but we are not interested in that in this case
+ // (_, error) => {
+ // reject(error);
+ // }
+ // );
+ // });
+ // });
+
+ // return promise;
+}
+
+export async function insertPlace(place) {
+ try {
+ const result = await database.runAsync(
+ `INSERT INTO places (title, imageUri, address, lat, lng) VALUES (?, ?, ?, ?,?)`,
+ [
+ place.title,
+ place.imageUri,
+ place.address,
+ place.location.lat,
+ place.location.lng
+ ]
+ );
+ return result;
+ } catch (error) {
+ console.log('Insert place failed: ', error);
+ throw error;
+ }
+
+ // CODE FROM THE COURSE, SQLite HAS CHANGED SINCE!
+ // const promise = new Promise((resolve, reject) => {
+ // database.transaction((tx) => {
+ // tx.executeSql(
+ // `INSERT INTO places (title, imageUri, address, lat, lng) VALUES (?, ?, ?, ?,?)`,
+ // [
+ // place.title,
+ // place.imageUri,
+ // place.address,
+ // place.location.lat,
+ // place.location.lng
+ // ],
+ // // success case, first argument is the transaction but we are not interested in that for this case
+ // (_, result) => {
+ // console.log(result);
+ // resolve(result);
+ // },
+ // // error case
+ // (_, error) => {
+ // reject(error);
+ // }
+ // );
+ // });
+ // });
+
+ // return promise;
+}
+
+export async function fetchPlaces() {
+ try {
+ const result = await database.getAllAsync('SELECT * FROM places');
+
+ // return result;
+
+ // TODO: - Check if this is correct, at the momemt of following and implementing the course I couldn't test it
+ const places = [];
+ for (const dp of result.rows._array) {
+ places.push(
+ new Place(
+ dp.title,
+ dp.imageUri,
+ {
+ address: dp.address,
+ lat: dp.lat,
+ lng: dp.lng
+ },
+ dp.id
+ )
+ );
+ }
+
+ return places;
+ } catch (error) {
+ console.log('Fetch places failed:', error);
+ throw error;
+ }
+
+ // CODE FROM THE COURSE, SQLite has changed since then
+ // const promise = new Promise((resolve, reject) => {
+ // database.transaction((tx) => {
+ // tx.executeSql(
+ // 'SELECT * FROM places',
+ // // the second argument is the array of data should be injected into this command, but here we don't have any placeholders so we use an empty array!
+ // [],
+ // // success function
+ // (_, result) => {
+ // console.log(result);
+
+ // resolve(result.rows._array);
+
+ // resolve(result);
+ // },
+ // // error function
+ // (_, error) => {
+ // reject(error);
+ // }
+ // );
+ // });
+ // });
+
+ // return promise;
+}
+
+export async function fetchPlaceDetails(id) {
+ try {
+ const result = await database.getFirstAsync(
+ 'SELECT * FROM places WHERE id = ?',
+ [id]
+ );
+
+ // return place;
+ const dbPlace = result.rows._array[0];
+ const place = new Place(
+ dbPlace.title,
+ dbPlace.imageUri,
+ { lat: dbPlace.lat, lng: dbPlace.lng, address: dbPlace.address },
+ dbPlace.id
+ );
+ return place;
+ } catch (error) {
+ console.log('Fetch place details failed:', error);
+ throw error;
+ }
+
+ // CODE FROM THE COURSE, SQLite HAS CHANGED SINCE THEN!
+ // const promise = new Promise((resolve, reject) => {
+ // database.transaction((tx) => {
+ // tx.executeSql(
+ // 'SELECT * FROM places WHERE id = ?',
+ // [id],
+ // // success
+ // (_, result) => {
+ // resolve(result);
+ // },
+ // // error
+ // (_, error) => {
+ // reject(error);
+ // }
+ // );
+ // });
+ // });
+
+ // return promise;
+}
diff --git a/attachments/12-native-features/00-starting-project/util/location.js b/attachments/12-native-features/00-starting-project/util/location.js
new file mode 100644
index 00000000..83afc113
--- /dev/null
+++ b/attachments/12-native-features/00-starting-project/util/location.js
@@ -0,0 +1,22 @@
+const GOOGLE_API_KEY = '';
+
+export function getMapPreview(lat, lng) {
+ const imagePreviewUrl = `https://maps.googleapis.com/maps/api/staticmap?center=${lat},${lng}&zoom=14&size=400x200&maptype=roadmap
+&markers=color:red%7Clabel:S%7C${lat},${lng}
+&key=${GOOGLE_API_KEY}`; // &signature=YOUR_SIGNATURE
+
+ return imagePreviewUrl;
+}
+
+export async function getAddress(lat, lng) {
+ const url = `https://maps.googleapis.com/maps/api/geocode/json?latlng=${lat},${lng}&key=${GOOGLE_API_KEY}`;
+ const response = await fetch(url);
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch adress!');
+ }
+
+ const data = await response.json();
+ const address = data.results[0].formatted_address;
+ return address;
+}