diff --git a/frontend/assets/colorConstants.tsx b/frontend/assets/colorConstants.tsx index 3fddb95..3a64572 100644 --- a/frontend/assets/colorConstants.tsx +++ b/frontend/assets/colorConstants.tsx @@ -1,18 +1,20 @@ const Colors = { - LIGHTFGREEN: '#E0EEC6', - DARKGREEN: '#243E36', + LIGHTFGREEN: '#d8edc2', + DARKGREEN: '#012b26', + DARKTRANS: '#07332f', DARKGREEN2: '#224A3E', DARKGREEN3: '#2E5C4E', - WHITE: '#fff', - BLACK: '000', + WHITE: '#ffffff', + BLACK: '#000000', DARKLIMEGREEN: '#4B8552', - GREY: '#ccc', + GREY: '#cccccc', FILLGREEN: '#7CA982', ERROR: 'red', TRANSGREEN: '#366959', BLACKTRANS: '#000000aa', BLUE: 'blue', GREYGREEN: '#B4C792', + TEAL: '#6bcfca' }; export default Colors; diff --git a/frontend/package.json b/frontend/package.json index 37c2cd2..d9282a8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -38,8 +38,8 @@ "react-native-circular-progress-indicator": "^4.4.2", "react-native-elements": "^3.4.3", "react-native-gesture-handler": "^2.13.3", - "react-native-paper": "^5.11.1", - "react-native-reanimated": "^3.5.4", + "react-native-paper": "^5.11.3", + "react-native-reanimated": "~3.3.0", "react-native-safe-area-context": "^4.6.3", "react-native-screens": "~3.22.0", "react-native-svg": "13.9.0", diff --git a/frontend/src/components/appNavigation.tsx b/frontend/src/components/appNavigation.tsx index f5724f4..9f2cef0 100644 --- a/frontend/src/components/appNavigation.tsx +++ b/frontend/src/components/appNavigation.tsx @@ -23,7 +23,7 @@ import FoodEntryEdit from '../screens/Food/foodEntryEdit'; import SettingsScreen from '../screens/settings'; import YourForms from '../screens/yourForms'; import CommunityHub from '../screens/communityHub'; - +import FootprintDecomp from '../screens/footpringDecomp'; const Stack = createNativeStackNavigator(); const Tab = createBottomTabNavigator(); @@ -34,11 +34,19 @@ const AppNavigation = (): JSX.Element => { - - - - + + + = ({ thisUser }) => { + const lvl = getUserLevel(thisUser); + // testing--const lvl = 7; + const lvlThresh = [250, 500, 1000, 2000, 4000, 8000, 16000, 32000, 64000]; + const maxExp = lvlThresh[lvl - 1]; + + const curr = thisUser.score; + // testing--const curr = 14039; + + const progress = (curr / maxExp) * 100; + + return ( + + + + + + + Level: {lvl} + + + {`${curr}/${maxExp}`} + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + width: 250, + marginVertical: 4, + }, + progressBar: { + flexDirection: 'row', + height: 5, + backgroundColor: Colors.WHITE, + borderRadius: 10, + overflow: 'hidden', + }, + currProgression:{ + backgroundColor: Colors.TEAL, + height: '100%', + + }, + levelContainer:{ + flex: 1, + justifyContent: 'flex-start', + paddingLeft: 5, + }, + texts:{ + marginTop: 5, + color: Colors.WHITE, + fontSize: 12, + }, + progContainer:{ + justifyContent: 'flex-end', + }, + textBox:{ + flexDirection: 'row' + } +}); + +export default ExpProgressBar; diff --git a/frontend/src/components/sampleData/sampleChllgInput.tsx b/frontend/src/components/sampleData/sampleChllgInput.tsx new file mode 100644 index 0000000..056267f --- /dev/null +++ b/frontend/src/components/sampleData/sampleChllgInput.tsx @@ -0,0 +1,21 @@ +import { type challenge } from "../types"; + +const sampleChallenges: challenge[] = [ + { + id: 1, + title: 'Meatless Mondays', + description: 'Try going vegitarian (or vegan for a bigger challenge) for an entire Monday', + }, + { + id: 2, + title: "One man's treasure another man's innovation", + description: 'Visit a thrift shop and turn 3 items into one sculpture', + }, + { + id: 3, + title: 'Good re-Soup', + description: 'Reuse and repurpose your leftovers by turning it into stock!', + }, + ]; + + export default sampleChallenges; \ No newline at end of file diff --git a/frontend/src/components/types.tsx b/frontend/src/components/types.tsx index 2a8294d..0a7221f 100644 --- a/frontend/src/components/types.tsx +++ b/frontend/src/components/types.tsx @@ -31,3 +31,20 @@ export interface profileWidgetBoxProps { photoURL: string; user: User; } + +export interface carbonWidgetProps { + carbonUser: User; +} + +export interface challenge { + id: number; + title: string; + description: string; +} +export interface challengesProps { + challenges: challenge[]; +} + +export interface ExpProgressBarProps { + thisUser: User, +} \ No newline at end of file diff --git a/frontend/src/screens/dashboard.tsx b/frontend/src/screens/dashboard.tsx index 8bbe62f..93ab8b9 100644 --- a/frontend/src/screens/dashboard.tsx +++ b/frontend/src/screens/dashboard.tsx @@ -1,12 +1,16 @@ import React, { useEffect, useState } from 'react'; -import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'; +import { ScrollView, View, StyleSheet, TouchableOpacity } from 'react-native'; import { useFonts } from 'expo-font'; import Colors from '../../assets/colorConstants'; + import ProfileWidgetBox from '../widgets/profileWidget'; -import WidgetBox from '../widgets/widgetBox'; +import CarbonWidgetBox from '../widgets/carbonWidgetBox'; +import ChallengesWidget from '../widgets/challengesWidgetBox'; + import type { RootStackParamList } from '../components/types'; import type { StackNavigationProp } from '@react-navigation/stack'; import { useNavigation } from '@react-navigation/native'; + import { type User } from '../models/User'; import { UsersAPI } from '../APIs/UsersAPI'; import { type TransportationEntry } from '../models/Transportation'; @@ -15,6 +19,7 @@ import { type FoodEntry } from '../models/Food'; import { type EnergyEntry } from '../models/Energy'; import { FoodAPI } from '../APIs/FoodAPI'; import { EnergyAPI } from '../APIs/EnergyAPI'; +import sampleChallenges from '../components/sampleData/sampleChllgInput'; export type StackNavigation = StackNavigationProp; export default function DashBoardScreen(): JSX.Element { @@ -22,7 +27,8 @@ export default function DashBoardScreen(): JSX.Element { const [transportationEntry, setTransportationEntry] = useState(); const [foodEntry, setFoodEntry] = useState(); const [energyEntry, setEnergyEntry] = useState(); - const [photoURL] = useState("https://upload.wikimedia.org/wikipedia/commons/thumb/2/2c/Default_pfp.svg/2048px-Default_pfp.svg.png"); + + const [photoURL] = useState("https://cdn.vox-cdn.com/thumbor/osQ-EchVP5I1xQlgtouC48YqzNc=/0x0:1750x941/1200x800/filters:focal(735x331:1015x611)/cdn.vox-cdn.com/uploads/chorus_image/image/53111667/Mewtwo_M01.0.0.png"); const navigation = useNavigation(); @@ -59,13 +65,7 @@ export default function DashBoardScreen(): JSX.Element { } return ( - - - - Dashboard - - - + + + { + navigation.navigate('FootprintDecomp'); + }} + > + + + + + + + + - - - - { - navigation.navigate('TransportationHistory'); - }} - > - - - - - - { - navigation.navigate('FoodHistory'); - }} - > - - - - - - - - { - navigation.navigate('EnergyHistory'); - }} - > - - - - - - + ); } const styles = StyleSheet.create({ container: { - alignItems: 'center', - backgroundColor: Colors.WHITE, - flex: 1, - justifyContent: 'center', - }, - header: { - fontSize: 24, - }, - headerBox: { - backgroundColor: Colors.WHITE, - }, - headerContainer: { - alignItems: 'center', + backgroundColor: Colors.LIGHTFGREEN, + flexGrow: 1, }, profileWidgetContainer: { - padding: 10, - flexDirection: 'column', - }, - widgetContainer: { - padding: 10, - flexDirection: 'row', + alignItems: 'center', + marginHorizontal: 10 }, widgetBoarder: { padding: 10, diff --git a/frontend/src/screens/footpringDecomp.tsx b/frontend/src/screens/footpringDecomp.tsx new file mode 100644 index 0000000..d1c889b --- /dev/null +++ b/frontend/src/screens/footpringDecomp.tsx @@ -0,0 +1,113 @@ +import React, { useEffect, useState } from 'react'; +import { ScrollView, View, StyleSheet, TouchableOpacity } from 'react-native'; +import { useFonts } from 'expo-font'; +import Colors from '../../assets/colorConstants'; +import WidgetBox from '../widgets/widgetBox'; +import type { RootStackParamList } from '../components/types'; +import type { StackNavigationProp } from '@react-navigation/stack'; +import { useNavigation } from '@react-navigation/native'; +import { type User } from '../models/User'; +import { UsersAPI } from '../APIs/UsersAPI'; +import { type TransportationEntry } from '../models/Transportation'; +import { TransportationAPI } from '../APIs/TransportationAPI'; +import { type FoodEntry } from '../models/Food'; +import { type EnergyEntry } from '../models/Energy'; +import { FoodAPI } from '../APIs/FoodAPI'; +import { EnergyAPI } from '../APIs/EnergyAPI'; +export type StackNavigation = StackNavigationProp; + +export default function FootprintDecomp(): JSX.Element { + const [user, setUser] = useState(undefined); + const [transportationEntry, setTransportationEntry] = useState(); + const [foodEntry, setFoodEntry] = useState(); + const [energyEntry, setEnergyEntry] = useState(); + + + const navigation = useNavigation(); + + const [loaded] = useFonts({ + Montserrat: require('../../assets/fonts/MontserratThinRegular.ttf'), + Josefin: require('../../assets/fonts/JosefinSansThinRegular.ttf'), + }); + + useEffect(() => { + void UsersAPI.GetLoggedInUser().then((res) => { + if (res != null) { + setUser(res); + } + }); + void TransportationAPI.getTransportationMetricForToday().then((res) => { + if (res != null) { + setTransportationEntry(res) + } + }); + void FoodAPI.getFoodMetricForToday().then((res) => { + if (res != null) { + setFoodEntry(res) + } + }); + void EnergyAPI.getEnergyMetricForToday().then((res) => { + if (res != null) { + setEnergyEntry(res) + } + }); + }, [loaded]); + + if (!loaded || user === undefined || transportationEntry === undefined || foodEntry === undefined || energyEntry === undefined) { + return <>; + } + + return ( + + + + { + navigation.navigate('TransportationHistory'); + }} + > + + + + + + { + navigation.navigate('FoodHistory'); + }} + > + + + + + + + + { + navigation.navigate('EnergyHistory'); + }} + > + + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + backgroundColor: Colors.LIGHTFGREEN, + flexGrow: 1, + }, + widgetContainer: { + flexDirection: 'row', + alignContent: 'center', + justifyContent: 'center' + }, + widgetBoarder: { + padding: 10, + }, +}); diff --git a/frontend/src/widgets/carbonWidgetBox.tsx b/frontend/src/widgets/carbonWidgetBox.tsx new file mode 100644 index 0000000..1fa42c0 --- /dev/null +++ b/frontend/src/widgets/carbonWidgetBox.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { View, Text,StyleSheet } from 'react-native'; +import Colors from '../../assets/colorConstants'; +import { type carbonWidgetProps } from '../components/types'; +import { useFonts } from 'expo-font'; + +const CarbonWidgetBox: React.FC = ({ carbonUser }) => { + const [loaded] = useFonts({ + Montserrat: require('../../assets/fonts/MontserratThinRegular.ttf'), + Josefin: require('../../assets/fonts/JosefinSansThinRegular.ttf'), + }); + + if (!loaded) { + return <>; + } + + return ( + + + Carbon Footprint + getUserFootprint + + + + ); +}; + +const styles = StyleSheet.create({ + boxContainer: { + backgroundColor: Colors.DARKGREEN, + borderRadius: 15, + height: 150, + width: 325, + flexDirection: 'column', + // ios shadow + shadowColor: Colors.BLACK, + shadowOffset: { width: 2, height: 4 }, + shadowOpacity: 0.6, + shadowRadius: 15, + // andriod shadow + elevation: 5, + alignItems: 'flex-start', + }, + footprint:{ + textAlign: 'center', + color: Colors.WHITE, + fontSize: 14, + paddingLeft: '30%', + paddingTop: '10%', + }, + header: { + color: Colors.WHITE, + textAlign: 'left', + fontSize: 18, + paddingTop: 25, + paddingLeft: 20, + fontWeight: '700', + }, + + +}); + +export default CarbonWidgetBox; diff --git a/frontend/src/widgets/challengesWidgetBox.tsx b/frontend/src/widgets/challengesWidgetBox.tsx new file mode 100644 index 0000000..96bfc24 --- /dev/null +++ b/frontend/src/widgets/challengesWidgetBox.tsx @@ -0,0 +1,128 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { View, Text, TouchableOpacity, StyleSheet, Animated } from 'react-native'; +import Colors from '../../assets/colorConstants'; +import { type challengesProps, type RootStackParamList } from '../components/types'; +import { useFonts } from 'expo-font'; +import type { StackNavigationProp } from '@react-navigation/stack'; +import { useNavigation } from '@react-navigation/native'; + +export type StackNavigation = StackNavigationProp; + +const ChallengesWidget: React.FC = ({ challenges }) => { + const [loaded] = useFonts({ + Montserrat: require('../../assets/fonts/MontserratThinRegular.ttf'), + Josefin: require('../../assets/fonts/JosefinSansThinRegular.ttf'), + }); + + const [expandedChallenge, setExpandedChallenge] = useState(null); + const containerHeight = useRef(new Animated.Value(0)).current; + const navigation = useNavigation(); + const handleChallengePress = (challengeId: number): void => { + setExpandedChallenge((prev) => (prev === challengeId ? null : challengeId)); + }; + + + useEffect(() => { + const expandedContentHeight = challenges.reduce((totalHeight, chal) => { + return totalHeight + (expandedChallenge === chal.id ? 50 : 0); + }, 0); + + const newHeight = Math.max(50, 150 + expandedContentHeight); + + Animated.timing(containerHeight, { + toValue: 130 + newHeight, + duration: 400, + useNativeDriver: false + }).start(); + }, [challenges, containerHeight, expandedChallenge]); + + if (!loaded) { + return <>; + } + + return ( + + Your Challenges + + {challenges.map((chal) => ( + + handleChallengePress(chal.id)}> + {chal.title} + {expandedChallenge === chal.id && ( + {chal.description} + )} + + + ))} + + navigation.navigate('CommunityHub')}> + View More + + + ); + }; + + const styles = StyleSheet.create({ + challengeBox: { + backgroundColor: Colors.DARKGREEN, + borderRadius: 15, + justifyContent: 'center', + width: 325, + flexDirection: 'column', + // ios shadow + shadowColor: Colors.BLACK, + shadowOffset: { width: 2, height: 4 }, + shadowOpacity: 0.6, + shadowRadius: 15, + // andriod shadow + elevation: 5, + margin: 10, + }, + challengeContainer: { + justifyContent: 'center', + marginTop: 30, + paddingVertical: 10, + marginHorizontal: 5, + borderRadius: 10 + }, + challengeListContainer: { + padding: 15, + backgroundColor: Colors.DARKTRANS, + borderRadius: 10, + marginHorizontal: 10, + marginVertical: 5, + }, + challengeTitle: { + fontSize: 16, + color: Colors.WHITE, + fontWeight: '600', + + }, + challengeDescription: { + marginTop: 5, + color: Colors.WHITE, + }, + header: { + color: Colors.WHITE, + textAlign: 'left', + fontSize: 18, + paddingLeft: 20, + fontWeight: '700', + paddingBottom: 10, + position: 'absolute', + top: 20, + left: 0, + right: 0, + zIndex: 1, + marginBottom: 10 + }, + viewMore:{ + color: Colors.WHITE, + textAlign: 'right', + marginHorizontal: 20, + marginBottom: 5, + + } + }); + +export default ChallengesWidget; diff --git a/frontend/src/widgets/profileWidget.tsx b/frontend/src/widgets/profileWidget.tsx index e23036f..2058b74 100644 --- a/frontend/src/widgets/profileWidget.tsx +++ b/frontend/src/widgets/profileWidget.tsx @@ -1,9 +1,9 @@ import React from 'react'; -import { View, Text, Image, StyleSheet } from 'react-native'; +import { View, Text, Image, StyleSheet, TouchableOpacity } from 'react-native'; import Colors from '../../assets/colorConstants'; import { type profileWidgetBoxProps } from '../components/types'; import { useFonts } from 'expo-font'; -import { getUserLevel } from '../models/User'; +import ExpProgressBar from '../components/expProgressBar'; const ProfileWidgetBox: React.FC = ({ photoURL, user }) => { const [loaded] = useFonts({ @@ -18,11 +18,25 @@ const ProfileWidgetBox: React.FC = ({ photoURL, user }) = return ( - + {user.full_name} - Email: {user.email} - Level: {getUserLevel(user)} + + + + + + Badges + + + Rank getUserRank + + + + + + + ); }; @@ -31,32 +45,53 @@ const styles = StyleSheet.create({ boxContainer: { alignItems: 'center', backgroundColor: Colors.DARKGREEN, - borderRadius: 10, + borderRadius: 15, justifyContent: 'center', - height: 300, - width: 300, + height: 350, + width: 325, flexDirection: 'column', + // ios shadow + shadowColor: Colors.BLACK, + shadowOffset: { width: 2, height: 4 }, + shadowOpacity: 0.6, + shadowRadius: 15, + // andriod shadow + elevation: 5, + }, + buttonsContainer:{ + flexDirection: 'row', + justifyContent: 'center', + }, + button:{ + backgroundColor: Colors.DARKTRANS, + marginHorizontal: 10, + padding: 15, + borderRadius: 10, + top: '5%' }, - level:{ + buttonText:{ color: Colors.WHITE, - fontSize: 16, - flex: 1, - }, + }, name: { - flex: 1, - fontSize: 16, + fontSize: 20, color: Colors.WHITE, fontWeight: '700', + }, + nameBox: { top: '20%', + flex: 2, + alignItems: 'center' }, profilePicture: { - width: 50, - height: 50, - borderRadius: 25, + width: 90, + height: 90, + borderRadius: 45, alignItems: 'flex-start', top: '15%', - }, + progressBar:{ + } + }); export default ProfileWidgetBox;