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
+ }
+});