MealPlanner is the ultimate solution for anyone seeking convenience, variety, and health-conscious meal choices. Say goodbye to the hassle of meal planning and the stress of wondering what to eat each day. With MealPlanner, you’ll have your personalized weekly food selection at your fingertips.
Gone are the days of struggling to come up with meal ideas or constantly repeating the same recipes. MealPlanner simplifies the process by offering a curated selection of delicious and nutritious meals tailored to your preferences and dietary needs.
This app was designed to assist you in arranging and evaluating your weekly diet effectively. Packed with features, it seamlessly connects to a “Firebase” database, allowing you to access it from any internet-connected computer using the correct URL.
Because the app is build in react native it is compiled by millions of files but the main one are inside the app folder
Files
_layout.tsx
calendar.tsx
forgotPassword.tsx
homepage.tsx
Signin.tsx
Signup.tsx
- Signup.tsx: Enables user registration using either email/password or Google sign-in. It includes input validation for email and password matching.
function signUpEmailFunction() { Keyboard.dismiss(); if (!email) { alert('Please enter your email address') } else if (!password) { alert('Please enter your password.') } else if (password !== passwordConfirm) { alert('The passwords do not match.') } else if (!emailRegex.test(email)) { alert('Please enter a valid email address.') } else { createUserWithEmailAndPassword(auth, email, password) .then((userCredential) => { const user = userCredential.user; setDoc(doc(db, "users", user.uid), { email: email, }); }) .catch((error) => { const errorCode = error.code; const errorMessage = error.message; alert(errorMessage) }); } } async function signUpGoogleFunction() { signInWithPopup(auth, provider) .then(async (result) => { const user = result.user; const userDocRef = doc(db, 'users', user.uid); const userDoc = await getDoc(userDocRef); if (userDoc.exists()) { } else { await setDoc(userDocRef, { email: user.email, }); } }).catch((error) => { const errorCode = error.code; const errorMessage = error.message; alert(errorMessage); }); }
- Signin.tsx: Facilitates user login with email/password or Google. Upon successful login, it navigates to the “homepage” screen.
function loginInGoogleFunction() { signInWithPopup(auth, provider) .then(async (result) => { const user = result.user; const userDocRef = doc(db, 'users', user.uid); const userDoc = await getDoc(userDocRef); if (userDoc.exists()) { } else { await setDoc(userDocRef, { email: user.email, }); } }).catch((error) => { const errorCode = error.code; const errorMessage = error.message; alert(errorMessage); }); } function loginEmailFunction() { Keyboard.dismiss(); signInWithEmailAndPassword(auth, email, password) .then((userCredential) => { }) .catch((error) => { const errorCode = error.code; const errorMessage = error.message; alert(errorMessage); }); }
- forgotPassword.tsx: Provides a password reset mechanism where users can request a password reset link via email.
function resetPassword() { if (!email) { alert('Please enter your email address') } else if (!emailRegex.test(email)) { alert('Please enter a valid email address.') } else { sendPasswordResetEmail(auth, email) .then(() => { alert("An email was sent to your inbox") }) .catch((error) => { const errorCode = error.code; const errorMessage = error.message; alert(errorMessage) }); } }
Lunch List Management (homepage.tsx):
- Allows users to add lunch items to a list using a text input field.
- Displays the list of lunch items with the ability to delete individual items.
- Persists the lunch list data in Firebase Firestore, ensuring data availability across user sessions.
import React, { useState, useEffect } from 'react'; import { StyleSheet, View, ScrollView, Text, TouchableOpacity, Image, TextInput, FlatList } from 'react-native'; import { Menu, PaperProvider, } from 'react-native-paper'; import * as ImagePicker from "expo-image-picker"; import { Link, router } from 'expo-router'; import { initializeAppCheck, ReCaptchaV3Provider } from "firebase/app-check"; import { getAuth, onAuthStateChanged, signOut } from "firebase/auth"; import { getFirestore, doc, updateDoc, getDoc } from "firebase/firestore"; import { getStorage, ref, getDownloadURL, uploadString, deleteObject } from "firebase/storage"; import { app, auth, db, storage } from './firebaseconfig'; export default function Homepage({ navigation }) { const [inputText, setInputText] = useState(''); const [uid, setUid] = useState(''); const [data, setData] = useState([]); const [visible, setVisible] = useState(false); const [image, setImage] = useState(null); let dataLength = data.length; const appCheck = initializeAppCheck(app, { provider: new ReCaptchaV3Provider('Input Your ReCAPTCHA key'), isTokenAutoRefreshEnabled: true }); useEffect(() => { async function fetchData(uid) { const userDocRef = doc(db, 'users', uid,); const userDoc = await getDoc(userDocRef); if (userDoc.exists()) { if (userDoc.data().data != null) { const mappedData = userDoc.data().data.map(item => ({ id: item.id, title: item.title })); setData(mappedData) } getDownloadURL(ref(storage, 'profileImages/' + uid + "/" + "profileImage")) .then((url) => { setImage(url) }) .catch((error) => { }); } } onAuthStateChanged(auth, (user) => { if (user) { const uid = user.uid; setUid(uid) fetchData(uid) } else { router.replace('/Signup'); } }); }, []); const pickImage = async () => { setVisible(false) let result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ImagePicker.MediaTypeOptions.All, allowsEditing: true, aspect: [4, 3], quality: 1, }); if (!result.canceled) { setImage(result.assets[0].uri); uploadString(ref(storage, 'profileImages/' + uid + "/" + "profileImage"), result.assets[0].uri, 'data_url') } }; function removeProfilePicture() { setVisible(false) setImage(null) deleteObject(ref(storage, 'profileImages/' + uid + "/" + "profileImage")) .then(() => { }) .catch((error) => { }); } function logoutFunction() { signOut(auth).then(() => { router.replace('/Signup'); setVisible(false) }).catch((error) => { alert(error) }) } function addItem() { if (inputText.trim() !== '') { const newItem = { id: (dataLength + 1).toString(), title: inputText.trim(), }; setData([...data, newItem]); setInputText(''); updateDoc(doc(db, "users", uid), { data: [...data, newItem] }); } }; function deleteItem(itemId) { setData(data.filter(item => item.id !== itemId)); dataLength = data.length; updateDoc(doc(db, "users", uid), { data: data.filter(item => item.id !== itemId) }); }; function renderItem({ item }) { return ( <TouchableOpacity style={styles.item} onPress={() => deleteItem(item.id)}> <Text style={{ fontSize: 24, }}>{item.title}</Text> </TouchableOpacity> ) }; return ( <View style={styles.container}> <PaperProvider> <View style={{ flexDirection: 'row', justifyContent: 'space-between', }}> <TouchableOpacity style={styles.buttonNext} onPress={() => ( router.replace('/calendar'))}><Text style={{ fontSize: 20, color: 'white' }}>Next</Text></TouchableOpacity> <View style={{ right: 10, top: 10, alignSelf: 'flex-end', borderRadius: 20, alignItems: 'center', justifyContent: 'center' }}> <Menu visible={visible} onDismiss={() => { setVisible(false) }} anchor={<TouchableOpacity onPress={() => { setVisible(true) }}> <Image source={image ? { uri: image } : require('../assets/images/defaultProfilePicture.png')} style={{ borderRadius: 50, width: 60, height: 60, }} /> </TouchableOpacity>}> <Menu.Item onPress={pickImage} title="Profile Picture" /> <Menu.Item onPress={removeProfilePicture} title="Remove Profile Picture" /> <Menu.Item style={{ borderTopWidth: 1, borderTopColor: 'black' }} onPress={logoutFunction} title="Logout" /> </Menu> </View> </View> <ScrollView contentContainerStyle={styles.scrollViewContent}> <TextInput style={styles.input} placeholder="Enter lunch" value={inputText} onChangeText={setInputText} /> <FlatList data={data} renderItem={renderItem} keyExtractor={item => item.id} style={{ width: "100%", }} /> </ScrollView> <TouchableOpacity style={styles.button} onPress={addItem} > <Image source={require('../assets/images/BlueRectangleWithWhiteCross.png')} style={{ width: '100%', height: '100%', }}></Image> </TouchableOpacity> </PaperProvider> </View> ); }; const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff0e5', }, scrollViewContent: { padding: 20, }, buttonNext: { backgroundColor: '#1166fd', height: 40, width: 100, justifyContent: 'center', left: 30, top: 20, borderRadius: 5, alignItems: 'center' }, button: { position: 'absolute', bottom: 20, right: 20, backgroundColor: '#fff0e5', borderRadius: 5, width: 70, height: 70, }, input: { height: 50, borderColor: 'gray', borderWidth: 2, marginBottom: 10, paddingHorizontal: 10, fontSize: 15, }, item: { backgroundColor: '#cceeff', padding: 20, marginVertical: 8, borderRadius: 20, borderWidth: 1, borderColor: "gray", }, });
Calendar View (calendar.tsx):
- Presents a calendar view, likely for scheduling lunches or associating lunch items with specific days.
- Fetches and displays lunch items from Firestore for each day of the week.
import React, { useState, useEffect } from 'react'; import { View, Text, StyleSheet, ScrollView, TouchableOpacity, Image } from 'react-native'; import { Menu, PaperProvider, } from 'react-native-paper'; import * as ImagePicker from "expo-image-picker"; import { Link, router } from 'expo-router'; import { initializeAppCheck, ReCaptchaV3Provider } from "firebase/app-check"; import { getAuth, onAuthStateChanged, signOut } from "firebase/auth"; import { getFirestore, doc, getDoc } from "firebase/firestore"; import { getStorage, ref, getDownloadURL, uploadString, deleteObject } from "firebase/storage"; import {app, auth, db, storage } from './firebaseconfig'; export default function calendar({ navigation }) { const [daysOfWeek, setDaysOfWeek] = useState(['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']) const [uid, setUid] = useState(''); const [shuffleTitles, setShuffleTitles] = useState([]); const [visible, setVisible] = useState(false); const [image, setImage] = useState(null); const appCheck = initializeAppCheck(app, { provider: new ReCaptchaV3Provider('Input Your ReCAPTCHA key'), isTokenAutoRefreshEnabled: true }); useEffect(() => { async function fetchData(uid) { const userDocRef = doc(db, 'users', uid,); const userDoc = await getDoc(userDocRef); if (userDoc.exists()) { if (userDoc.data().data != null) { const mappedData = userDoc.data().data.map(item => ({ id: item.id, title: item.title })); const shuffleTitles = userDoc.data().data.map(item => (item.title)); shuffle(shuffleTitles) setShuffleTitles(shuffleTitles) } getDownloadURL(ref(storage, 'profileImages/' + uid + "/" + "profileImage")) .then((url) => { setImage(url) }) .catch((error) => { }); } } onAuthStateChanged(auth, (user) => { if (user) { const uid = user.uid; setUid(uid) fetchData(uid) } else { router.replace('/Signup'); } }); }, []); function shuffle(array) { let currentIndex = array.length; while (currentIndex != 0) { let randomIndex = Math.floor(Math.random() * currentIndex); currentIndex--; [array[currentIndex], array[randomIndex]] = [ array[randomIndex], array[currentIndex]]; } } function logoutFunction() { signOut(auth).then(() => { router.replace('/Signup'); setVisible(false) }).catch((error) => { }) } const pickImage = async () => { setVisible(false) let result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ImagePicker.MediaTypeOptions.All, allowsEditing: true, aspect: [4, 3], quality: 1, }); if (!result.canceled) { setImage(result.assets[0].uri); uploadString(ref(storage, 'profileImages/' + uid + "/" + "profileImage"), result.assets[0].uri, 'data_url') } }; const getConditionalText = (index) => { if (shuffleTitles[index] == null) { return "N/A" } else { return shuffleTitles[index] } }; function removeProfilePicture() { setVisible(false) setImage(null) deleteObject(ref(storage, 'profileImages/' + uid + "/" + "profileImage")) .then(() => { }) .catch((error) => { }); } return ( <View style={styles.container}> <PaperProvider> <View style={{ flexDirection: 'row', justifyContent: 'space-between', }}> <TouchableOpacity style={styles.button} onPress={() => ( router.replace('/homepage'))}><Text style={{ fontSize: 20, color: 'white' }}>Back</Text></TouchableOpacity> <View style={{ right: 10, top: 10, alignSelf: 'flex-end', borderRadius: 20, alignItems: 'center', justifyContent: 'center' }}> <Menu visible={visible} onDismiss={() => { setVisible(false) }} anchor={<TouchableOpacity onPress={() => { setVisible(true) }}> <Image source={image ? { uri: image } : require('../assets/images/defaultProfilePicture.png')} style={{ borderRadius: 50, width: 60, height: 60, }} /> </TouchableOpacity>}> <Menu.Item onPress={pickImage} title="Profile Picture" /> <Menu.Item onPress={removeProfilePicture} title="Remove Profile Picture" /> <Menu.Item style={{ borderTopWidth: 1, borderTopColor: 'black' }} onPress={logoutFunction} title="Logout" /> </Menu> </View> </View> <ScrollView contentContainerStyle={styles.scrollViewContent}> {daysOfWeek.map((day, index) => ( <View key={index} style={styles.dayContainer}> <Text style={styles.dayTitle}>{day}</Text> <View style={styles.eventContainer}> <Text style={styles.eventText}>{getConditionalText(index)}</Text> <Text style={styles.eventText}>{ }</Text> </View> </View> ))} </ScrollView> </PaperProvider> </View > ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff0e5', }, button: { backgroundColor: '#1166fd', height: 40, width: 100, justifyContent: 'center', left: 30, top: 20, borderRadius: 5, alignItems: 'center' }, scrollViewContent: { paddingHorizontal: 20, paddingVertical: 5 }, dayContainer: { backgroundColor: '#fff', marginHorizontal: 20, marginVertical: 10, padding: 20, borderRadius: 10, elevation: 3, shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 5, shadowOffset: { width: 0, height: 2 }, }, dayTitle: { fontSize: 16.5, fontWeight: 'bold', marginBottom: 5, }, eventContainer: { borderTopWidth: 1, borderTopColor: '#eee', paddingTop: 10, }, eventText: { fontSize: 14, color: '#888', }, });
Navigation (_layout.tsx):
- Defines the app’s navigation structure using
react-navigation
. - Includes screens for Signup, Signin, Homepage, Calendar, and Forgot Password.
- Controls the transitions between screens based on user actions.
import * as React from 'react'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import 'react-native-gesture-handler'; const Stack = createNativeStackNavigator(); import homepage from "./homepage" import SignUpScreen from "./Signup" import forgotPasswordScreen from "./forgotPassword" import LoginIn from "./Signin" import calendar from "./calendar" export default function RootLayout() { return ( <> <Stack.Navigator initialRouteName="Signup"> <Stack.Screen name="calendar" component={calendar} options={{ title: 'Calendar', headerShown: false }} /> <Stack.Screen name="Signin" component={LoginIn} options={{ title: 'Sign in', headerShown: false }} /> <Stack.Screen name="Signup" component={SignUpScreen} options={{ title: 'Sign Up', headerShown: false }} /> <Stack.Screen name="forgotPassword" component={forgotPasswordScreen} options={{ title: 'Forgot Password', headerShown: false }} /> <Stack.Screen name="homepage" component={homepage} options={{ title: 'Homepage', headerShown: false }} /> </Stack.Navigator> </> ); }
I have hosted the app on a server so anyone can access it try it out
First of all the files of the program is 7. 2 HTML, 3 JavaScript and 1 CSS.
HTML
insertForm.html
weeklyDiet.html
JavaScript
codeInsertForm.js
codeWeeklyDiet.js
xlsx.full.min.js
CSS
style.css
The last part of code analyze is the CSS. The CSS has 1 file the style.css. The CSS is way so that we can improve the view of the html.
First there are the <div> “food” and “kind”. Basically they are 2 texts from weeklyDiet.html that are bold and with a bigger size of the letters.
#food { font-weight: bolder; font-size: 30px; } #type { font-weight: bolder; font-size: 30px; }
Next has the “foodElements” <div> that has inside all the element from the right column of codeInsertForm.js. It creates a box around it and gives them a letter size.
#foodElements{ font-size: 30px; border-style: solid; }
With the same logic works and the <div> “typeElements” but now the letter size and the box appears to the left column of codeInsertForm.js.
#typeElements{ font-size: 30px; border-style: solid; }
Next has the allElements <div> which has inside all the elements of codeInsertForm.js. The only usage is for align all the element.
#allElements { display: flex; float: center; }
The following one is the “day” text that is just being with a bigger size and the text bold.
#day { font-weight: bolder; font-size: 30px; }
The “body” is all the html with the elements that we can see. So the purpose of this is to align all the letters to the center and change the background color of HTML to “aqua”.
body { text-align: center; background-color: aqua; }
Also for creating a nice button I borrow a custom CSS button from the site uiverse.io. There are transactions and different color if it being hover.
/* From uiverse.io */ button { padding: 1.3em 3em; font-size: 12px; text-transform: uppercase; letter-spacing: 2.5px; font-weight: 500; color: #000; background-color: #fff; border: none; border-radius: 45px; box-shadow: 0px 8px 15px rgba(0, 0, 0, 0.1); transition: all 0.3s ease 0s; cursor: pointer; outline: none; margin-top: 50px; } button:hover { background-color: #2EE59D; box-shadow: 0px 15px 20px rgba(46, 229, 157, 0.4); color: #fff; transform: translateY(-7px); } button:active { transform: translateY(-1px); }
In addition there is and for the lang list modification like thicker border, color of the border, width, the size of the letters and the border radius (This is about the angles of the shape).
#lang{ border-radius: 5px; border-color: #000; border-width: 4px; width: 200px; font-size: 20px; }
Moreover there is and modifications to “myList” list like the width, the size of the letters, border-radius and border-width.
#myList { width: 250px; font-size: large; border-radius: 50px; border-width: 4px; border-color: #000; }
Finally the last part of the style.css is the table modifications. One of these is the border around every cell, the width,the size of the letters and the space around a text inside a cell.
table, td, th { border: 5px solid; } table { border-collapse: collapse; width: 100%; font-size: 25px; } th, td { padding: 15px; }
- Meal planner V2
-
Breaking Down the code
Because the app is build in react native it is compiled by millions of files but the main one are inside the app folder
Files
_layout.tsx
calendar.tsx
forgotPassword.tsx
homepage.tsx
Signin.tsx
Signup.tsx
- Signup.tsx: Enables user registration using either email/password or Google sign-in. It includes input validation for email and password matching.
function signUpEmailFunction() { Keyboard.dismiss(); if (!email) { alert('Please enter your email address') } else if (!password) { alert('Please enter your password.') } else if (password !== passwordConfirm) { alert('The passwords do not match.') } else if (!emailRegex.test(email)) { alert('Please enter a valid email address.') } else { createUserWithEmailAndPassword(auth, email, password) .then((userCredential) => { const user = userCredential.user; setDoc(doc(db, "users", user.uid), { email: email, }); }) .catch((error) => { const errorCode = error.code; const errorMessage = error.message; alert(errorMessage) }); } } async function signUpGoogleFunction() { signInWithPopup(auth, provider) .then(async (result) => { const user = result.user; const userDocRef = doc(db, 'users', user.uid); const userDoc = await getDoc(userDocRef); if (userDoc.exists()) { } else { await setDoc(userDocRef, { email: user.email, }); } }).catch((error) => { const errorCode = error.code; const errorMessage = error.message; alert(errorMessage); }); }
- Signin.tsx: Facilitates user login with email/password or Google. Upon successful login, it navigates to the “homepage” screen.
function loginInGoogleFunction() { signInWithPopup(auth, provider) .then(async (result) => { const user = result.user; const userDocRef = doc(db, 'users', user.uid); const userDoc = await getDoc(userDocRef); if (userDoc.exists()) { } else { await setDoc(userDocRef, { email: user.email, }); } }).catch((error) => { const errorCode = error.code; const errorMessage = error.message; alert(errorMessage); }); } function loginEmailFunction() { Keyboard.dismiss(); signInWithEmailAndPassword(auth, email, password) .then((userCredential) => { }) .catch((error) => { const errorCode = error.code; const errorMessage = error.message; alert(errorMessage); }); }
- forgotPassword.tsx: Provides a password reset mechanism where users can request a password reset link via email.
function resetPassword() { if (!email) { alert('Please enter your email address') } else if (!emailRegex.test(email)) { alert('Please enter a valid email address.') } else { sendPasswordResetEmail(auth, email) .then(() => { alert("An email was sent to your inbox") }) .catch((error) => { const errorCode = error.code; const errorMessage = error.message; alert(errorMessage) }); } }
Lunch List Management (homepage.tsx):
- Allows users to add lunch items to a list using a text input field.
- Displays the list of lunch items with the ability to delete individual items.
- Persists the lunch list data in Firebase Firestore, ensuring data availability across user sessions.
import React, { useState, useEffect } from 'react'; import { StyleSheet, View, ScrollView, Text, TouchableOpacity, Image, TextInput, FlatList } from 'react-native'; import { Menu, PaperProvider, } from 'react-native-paper'; import * as ImagePicker from "expo-image-picker"; import { Link, router } from 'expo-router'; import { initializeAppCheck, ReCaptchaV3Provider } from "firebase/app-check"; import { getAuth, onAuthStateChanged, signOut } from "firebase/auth"; import { getFirestore, doc, updateDoc, getDoc } from "firebase/firestore"; import { getStorage, ref, getDownloadURL, uploadString, deleteObject } from "firebase/storage"; import { app, auth, db, storage } from './firebaseconfig'; export default function Homepage({ navigation }) { const [inputText, setInputText] = useState(''); const [uid, setUid] = useState(''); const [data, setData] = useState([]); const [visible, setVisible] = useState(false); const [image, setImage] = useState(null); let dataLength = data.length; const appCheck = initializeAppCheck(app, { provider: new ReCaptchaV3Provider('Input Your ReCAPTCHA key'), isTokenAutoRefreshEnabled: true }); useEffect(() => { async function fetchData(uid) { const userDocRef = doc(db, 'users', uid,); const userDoc = await getDoc(userDocRef); if (userDoc.exists()) { if (userDoc.data().data != null) { const mappedData = userDoc.data().data.map(item => ({ id: item.id, title: item.title })); setData(mappedData) } getDownloadURL(ref(storage, 'profileImages/' + uid + "/" + "profileImage")) .then((url) => { setImage(url) }) .catch((error) => { }); } } onAuthStateChanged(auth, (user) => { if (user) { const uid = user.uid; setUid(uid) fetchData(uid) } else { router.replace('/Signup'); } }); }, []); const pickImage = async () => { setVisible(false) let result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ImagePicker.MediaTypeOptions.All, allowsEditing: true, aspect: [4, 3], quality: 1, }); if (!result.canceled) { setImage(result.assets[0].uri); uploadString(ref(storage, 'profileImages/' + uid + "/" + "profileImage"), result.assets[0].uri, 'data_url') } }; function removeProfilePicture() { setVisible(false) setImage(null) deleteObject(ref(storage, 'profileImages/' + uid + "/" + "profileImage")) .then(() => { }) .catch((error) => { }); } function logoutFunction() { signOut(auth).then(() => { router.replace('/Signup'); setVisible(false) }).catch((error) => { alert(error) }) } function addItem() { if (inputText.trim() !== '') { const newItem = { id: (dataLength + 1).toString(), title: inputText.trim(), }; setData([...data, newItem]); setInputText(''); updateDoc(doc(db, "users", uid), { data: [...data, newItem] }); } }; function deleteItem(itemId) { setData(data.filter(item => item.id !== itemId)); dataLength = data.length; updateDoc(doc(db, "users", uid), { data: data.filter(item => item.id !== itemId) }); }; function renderItem({ item }) { return ( <TouchableOpacity style={styles.item} onPress={() => deleteItem(item.id)}> <Text style={{ fontSize: 24, }}>{item.title}</Text> </TouchableOpacity> ) }; return ( <View style={styles.container}> <PaperProvider> <View style={{ flexDirection: 'row', justifyContent: 'space-between', }}> <TouchableOpacity style={styles.buttonNext} onPress={() => ( router.replace('/calendar'))}><Text style={{ fontSize: 20, color: 'white' }}>Next</Text></TouchableOpacity> <View style={{ right: 10, top: 10, alignSelf: 'flex-end', borderRadius: 20, alignItems: 'center', justifyContent: 'center' }}> <Menu visible={visible} onDismiss={() => { setVisible(false) }} anchor={<TouchableOpacity onPress={() => { setVisible(true) }}> <Image source={image ? { uri: image } : require('../assets/images/defaultProfilePicture.png')} style={{ borderRadius: 50, width: 60, height: 60, }} /> </TouchableOpacity>}> <Menu.Item onPress={pickImage} title="Profile Picture" /> <Menu.Item onPress={removeProfilePicture} title="Remove Profile Picture" /> <Menu.Item style={{ borderTopWidth: 1, borderTopColor: 'black' }} onPress={logoutFunction} title="Logout" /> </Menu> </View> </View> <ScrollView contentContainerStyle={styles.scrollViewContent}> <TextInput style={styles.input} placeholder="Enter lunch" value={inputText} onChangeText={setInputText} /> <FlatList data={data} renderItem={renderItem} keyExtractor={item => item.id} style={{ width: "100%", }} /> </ScrollView> <TouchableOpacity style={styles.button} onPress={addItem} > <Image source={require('../assets/images/BlueRectangleWithWhiteCross.png')} style={{ width: '100%', height: '100%', }}></Image> </TouchableOpacity> </PaperProvider> </View> ); }; const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff0e5', }, scrollViewContent: { padding: 20, }, buttonNext: { backgroundColor: '#1166fd', height: 40, width: 100, justifyContent: 'center', left: 30, top: 20, borderRadius: 5, alignItems: 'center' }, button: { position: 'absolute', bottom: 20, right: 20, backgroundColor: '#fff0e5', borderRadius: 5, width: 70, height: 70, }, input: { height: 50, borderColor: 'gray', borderWidth: 2, marginBottom: 10, paddingHorizontal: 10, fontSize: 15, }, item: { backgroundColor: '#cceeff', padding: 20, marginVertical: 8, borderRadius: 20, borderWidth: 1, borderColor: "gray", }, });
Calendar View (calendar.tsx):
- Presents a calendar view, likely for scheduling lunches or associating lunch items with specific days.
- Fetches and displays lunch items from Firestore for each day of the week.
import React, { useState, useEffect } from 'react'; import { View, Text, StyleSheet, ScrollView, TouchableOpacity, Image } from 'react-native'; import { Menu, PaperProvider, } from 'react-native-paper'; import * as ImagePicker from "expo-image-picker"; import { Link, router } from 'expo-router'; import { initializeAppCheck, ReCaptchaV3Provider } from "firebase/app-check"; import { getAuth, onAuthStateChanged, signOut } from "firebase/auth"; import { getFirestore, doc, getDoc } from "firebase/firestore"; import { getStorage, ref, getDownloadURL, uploadString, deleteObject } from "firebase/storage"; import {app, auth, db, storage } from './firebaseconfig'; export default function calendar({ navigation }) { const [daysOfWeek, setDaysOfWeek] = useState(['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']) const [uid, setUid] = useState(''); const [shuffleTitles, setShuffleTitles] = useState([]); const [visible, setVisible] = useState(false); const [image, setImage] = useState(null); const appCheck = initializeAppCheck(app, { provider: new ReCaptchaV3Provider('Input Your ReCAPTCHA key'), isTokenAutoRefreshEnabled: true }); useEffect(() => { async function fetchData(uid) { const userDocRef = doc(db, 'users', uid,); const userDoc = await getDoc(userDocRef); if (userDoc.exists()) { if (userDoc.data().data != null) { const mappedData = userDoc.data().data.map(item => ({ id: item.id, title: item.title })); const shuffleTitles = userDoc.data().data.map(item => (item.title)); shuffle(shuffleTitles) setShuffleTitles(shuffleTitles) } getDownloadURL(ref(storage, 'profileImages/' + uid + "/" + "profileImage")) .then((url) => { setImage(url) }) .catch((error) => { }); } } onAuthStateChanged(auth, (user) => { if (user) { const uid = user.uid; setUid(uid) fetchData(uid) } else { router.replace('/Signup'); } }); }, []); function shuffle(array) { let currentIndex = array.length; while (currentIndex != 0) { let randomIndex = Math.floor(Math.random() * currentIndex); currentIndex--; [array[currentIndex], array[randomIndex]] = [ array[randomIndex], array[currentIndex]]; } } function logoutFunction() { signOut(auth).then(() => { router.replace('/Signup'); setVisible(false) }).catch((error) => { }) } const pickImage = async () => { setVisible(false) let result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ImagePicker.MediaTypeOptions.All, allowsEditing: true, aspect: [4, 3], quality: 1, }); if (!result.canceled) { setImage(result.assets[0].uri); uploadString(ref(storage, 'profileImages/' + uid + "/" + "profileImage"), result.assets[0].uri, 'data_url') } }; const getConditionalText = (index) => { if (shuffleTitles[index] == null) { return "N/A" } else { return shuffleTitles[index] } }; function removeProfilePicture() { setVisible(false) setImage(null) deleteObject(ref(storage, 'profileImages/' + uid + "/" + "profileImage")) .then(() => { }) .catch((error) => { }); } return ( <View style={styles.container}> <PaperProvider> <View style={{ flexDirection: 'row', justifyContent: 'space-between', }}> <TouchableOpacity style={styles.button} onPress={() => ( router.replace('/homepage'))}><Text style={{ fontSize: 20, color: 'white' }}>Back</Text></TouchableOpacity> <View style={{ right: 10, top: 10, alignSelf: 'flex-end', borderRadius: 20, alignItems: 'center', justifyContent: 'center' }}> <Menu visible={visible} onDismiss={() => { setVisible(false) }} anchor={<TouchableOpacity onPress={() => { setVisible(true) }}> <Image source={image ? { uri: image } : require('../assets/images/defaultProfilePicture.png')} style={{ borderRadius: 50, width: 60, height: 60, }} /> </TouchableOpacity>}> <Menu.Item onPress={pickImage} title="Profile Picture" /> <Menu.Item onPress={removeProfilePicture} title="Remove Profile Picture" /> <Menu.Item style={{ borderTopWidth: 1, borderTopColor: 'black' }} onPress={logoutFunction} title="Logout" /> </Menu> </View> </View> <ScrollView contentContainerStyle={styles.scrollViewContent}> {daysOfWeek.map((day, index) => ( <View key={index} style={styles.dayContainer}> <Text style={styles.dayTitle}>{day}</Text> <View style={styles.eventContainer}> <Text style={styles.eventText}>{getConditionalText(index)}</Text> <Text style={styles.eventText}>{ }</Text> </View> </View> ))} </ScrollView> </PaperProvider> </View > ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff0e5', }, button: { backgroundColor: '#1166fd', height: 40, width: 100, justifyContent: 'center', left: 30, top: 20, borderRadius: 5, alignItems: 'center' }, scrollViewContent: { paddingHorizontal: 20, paddingVertical: 5 }, dayContainer: { backgroundColor: '#fff', marginHorizontal: 20, marginVertical: 10, padding: 20, borderRadius: 10, elevation: 3, shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 5, shadowOffset: { width: 0, height: 2 }, }, dayTitle: { fontSize: 16.5, fontWeight: 'bold', marginBottom: 5, }, eventContainer: { borderTopWidth: 1, borderTopColor: '#eee', paddingTop: 10, }, eventText: { fontSize: 14, color: '#888', }, });
Navigation (_layout.tsx):
- Defines the app’s navigation structure using
react-navigation
. - Includes screens for Signup, Signin, Homepage, Calendar, and Forgot Password.
- Controls the transitions between screens based on user actions.
import * as React from 'react'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import 'react-native-gesture-handler'; const Stack = createNativeStackNavigator(); import homepage from "./homepage" import SignUpScreen from "./Signup" import forgotPasswordScreen from "./forgotPassword" import LoginIn from "./Signin" import calendar from "./calendar" export default function RootLayout() { return ( <> <Stack.Navigator initialRouteName="Signup"> <Stack.Screen name="calendar" component={calendar} options={{ title: 'Calendar', headerShown: false }} /> <Stack.Screen name="Signin" component={LoginIn} options={{ title: 'Sign in', headerShown: false }} /> <Stack.Screen name="Signup" component={SignUpScreen} options={{ title: 'Sign Up', headerShown: false }} /> <Stack.Screen name="forgotPassword" component={forgotPasswordScreen} options={{ title: 'Forgot Password', headerShown: false }} /> <Stack.Screen name="homepage" component={homepage} options={{ title: 'Homepage', headerShown: false }} /> </Stack.Navigator> </> ); }
I have hosted the app on a server so anyone can access it try it out
- Meal planner V1
-
Breaking Down the code
First of all the files of the program is 7. 2 HTML, 3 JavaScript and 1 CSS.
HTML
insertForm.html
weeklyDiet.html
JavaScript
codeInsertForm.js
codeWeeklyDiet.js
xlsx.full.min.js
CSS
style.css
The last part of code analyze is the CSS. The CSS has 1 file the style.css. The CSS is way so that we can improve the view of the html.
First there are the <div> “food” and “kind”. Basically they are 2 texts from weeklyDiet.html that are bold and with a bigger size of the letters.
#food { font-weight: bolder; font-size: 30px; } #type { font-weight: bolder; font-size: 30px; }
Next has the “foodElements” <div> that has inside all the element from the right column of codeInsertForm.js. It creates a box around it and gives them a letter size.
#foodElements{ font-size: 30px; border-style: solid; }
With the same logic works and the <div> “typeElements” but now the letter size and the box appears to the left column of codeInsertForm.js.
#typeElements{ font-size: 30px; border-style: solid; }
Next has the allElements <div> which has inside all the elements of codeInsertForm.js. The only usage is for align all the element.
#allElements { display: flex; float: center; }
The following one is the “day” text that is just being with a bigger size and the text bold.
#day { font-weight: bolder; font-size: 30px; }
The “body” is all the html with the elements that we can see. So the purpose of this is to align all the letters to the center and change the background color of HTML to “aqua”.
body { text-align: center; background-color: aqua; }
Also for creating a nice button I borrow a custom CSS button from the site uiverse.io. There are transactions and different color if it being hover.
/* From uiverse.io */ button { padding: 1.3em 3em; font-size: 12px; text-transform: uppercase; letter-spacing: 2.5px; font-weight: 500; color: #000; background-color: #fff; border: none; border-radius: 45px; box-shadow: 0px 8px 15px rgba(0, 0, 0, 0.1); transition: all 0.3s ease 0s; cursor: pointer; outline: none; margin-top: 50px; } button:hover { background-color: #2EE59D; box-shadow: 0px 15px 20px rgba(46, 229, 157, 0.4); color: #fff; transform: translateY(-7px); } button:active { transform: translateY(-1px); }
In addition there is and for the lang list modification like thicker border, color of the border, width, the size of the letters and the border radius (This is about the angles of the shape).
#lang{ border-radius: 5px; border-color: #000; border-width: 4px; width: 200px; font-size: 20px; }
Moreover there is and modifications to “myList” list like the width, the size of the letters, border-radius and border-width.
#myList { width: 250px; font-size: large; border-radius: 50px; border-width: 4px; border-color: #000; }
Finally the last part of the style.css is the table modifications. One of these is the border around every cell, the width,the size of the letters and the space around a text inside a cell.
table, td, th { border: 5px solid; } table { border-collapse: collapse; width: 100%; font-size: 25px; } th, td { padding: 15px; }