diff --git a/backend/src/visited/templates/visit_template.ts b/backend/src/visited/templates/visit_template.ts index 49b4fb79e199e446b22ca353d804f20a62c667da..c16cab2f5c96612626254f39b89eef3239540dd4 100644 --- a/backend/src/visited/templates/visit_template.ts +++ b/backend/src/visited/templates/visit_template.ts @@ -104,16 +104,16 @@ export const visit_template = (places: string[]) => `

Jerez de García Salinas, Zacatecas

- ${ - places.map( + ${places + .map( (place) => `
-
` - ).join('\n') - } + `, + ) + .join('\n')}
`
-`; \ No newline at end of file +`; diff --git a/backend/src/visited/visited.controller.ts b/backend/src/visited/visited.controller.ts index a2f1f386cc98d4cd8ed181906463d54b1a2c1f78..1198df3d3bbaa95dd2d02a9736a6e2f577a30832 100644 --- a/backend/src/visited/visited.controller.ts +++ b/backend/src/visited/visited.controller.ts @@ -24,8 +24,8 @@ export class VisitedController { } @Get('/getImage/:routeId') - async getVisitedPlacesImage() { - return await this.visitedService.getVisitedPlacesImage(); + async getVisitedPlacesImage(@Param('routeId') routeId: string) { + return await this.visitedService.getVisitedPlacesImage(+routeId); } @UseGuards(AuthUserGuard) diff --git a/backend/src/visited/visited.module.ts b/backend/src/visited/visited.module.ts index 69bd37103adc479bb28b09c6b6862f496931f87c..66ca532f4c09506d47f1f69c83e72879d2ed2d9d 100644 --- a/backend/src/visited/visited.module.ts +++ b/backend/src/visited/visited.module.ts @@ -1,4 +1,4 @@ -import { Module } from '@nestjs/common'; +import { forwardRef, Module } from '@nestjs/common'; import { VisitedService } from './visited.service'; import { VisitedController } from './visited.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; @@ -19,6 +19,8 @@ import { UserResetCode } from 'src/auth/user/entities/user-reset-code.entity'; import { UserConfirmCode } from 'src/auth/user/entities/user-confirm-code.entity'; import { EmailService } from 'src/email/email.service'; import { TravelPlace } from 'src/travel-place/entities/travel-place.entity'; +import { RouteModule } from 'src/route/route.module'; +import { RouteService } from 'src/route/route.service'; @Module({ controllers: [VisitedController], @@ -33,6 +35,7 @@ import { TravelPlace } from 'src/travel-place/entities/travel-place.entity'; EmailService, ], imports: [ + RouteModule, TypeOrmModule.forFeature([ User, Place, diff --git a/backend/src/visited/visited.service.ts b/backend/src/visited/visited.service.ts index b9051ee8b5fe56b60e362c5fd1f3767a27528d9c..202954b9534132fb9de7b47817743fb3ff57d2e0 100644 --- a/backend/src/visited/visited.service.ts +++ b/backend/src/visited/visited.service.ts @@ -11,10 +11,12 @@ import { LANGUAGES } from 'src/shared/enum/languages.enum'; import { VisitedPlacesImageCreator } from './utils/visited_places_image_creator'; import { ServerConstants } from 'src/constants/server.contants'; import { TravelPlace } from 'src/travel-place/entities/travel-place.entity'; +import { RouteService } from 'src/route/route.service'; @Injectable() export class VisitedService { constructor( + private readonly routeService: RouteService, @InjectRepository(Visited) private visitedRepository: Repository, private readonly userService: UserService, private readonly placeService: PlaceService, @@ -56,12 +58,14 @@ export class VisitedService { return visited; } - async getVisitedPlacesImage() { + async getVisitedPlacesImage(routeId: number) { // obtener imágenes de los lugares visitados en una ruta try { - const visitedPlaces = (await this.placeService.findAllByTown(1, LANGUAGES.EN)) - .map((place) => place.imageName) + const visitedPlaces = (await this.routeService.getRouteInfoById(routeId)).travelPlace + .filter((travelPlace) => travelPlace.done) + .map((travelPlace) => travelPlace.place.imageName) .slice(0, 5); + console.info('visitedPlaces', visitedPlaces); const visitedPlacesImageCreator = new VisitedPlacesImageCreator(); return await visitedPlacesImageCreator.generateImage(visitedPlaces); } catch (error) { diff --git a/mobile/app/_layout.tsx b/mobile/app/_layout.tsx index 5d190e5d771da581115df1eab37272f4fffc87d0..34580862ce1cf15d73c30d59cc26997b9016a299 100644 --- a/mobile/app/_layout.tsx +++ b/mobile/app/_layout.tsx @@ -14,6 +14,7 @@ import { SetUpContextProvider, useSetUp, } from "../src/common/contexts/set_up_context"; +import { useEffect } from "react"; export default function Root() { return ( @@ -35,9 +36,15 @@ export default function Root() { const MainLayout = () => { const { isLoading } = useAuth(); + const { isLoading: profileLoading } = useSetUp(); const { t } = useTranslation(); - if (isLoading) { + useEffect(() => { + console.log("Auth Loading: ", isLoading); + console.log("Profile Loading: ", profileLoading); + }, [isLoading, profileLoading]); + + if (isLoading || profileLoading) { return ; } @@ -157,6 +164,21 @@ const MainLayout = () => { headerShown: false, }} /> + ); }; diff --git a/mobile/app/state/[stateId]/town/[townId]/activity/_layout.tsx b/mobile/app/state/[stateId]/town/[townId]/activity/_layout.tsx index cdf22644d62088408159c3e7b873e11b472e4e9e..6e5b50f543798480b7fe6d35c04e9ea2b1bc5b79 100644 --- a/mobile/app/state/[stateId]/town/[townId]/activity/_layout.tsx +++ b/mobile/app/state/[stateId]/town/[townId]/activity/_layout.tsx @@ -13,7 +13,6 @@ export default function ActivitySelectionScreen() { name="[activityId]/travel" options={{ headerShown: false, - }} /> diff --git a/mobile/app/travel_history/_layout.tsx b/mobile/app/travel_history/_layout.tsx index a72ebe32982fe7830ee29b4416f1e56eef811ce5..c99b0016a62c8e3fe290782d38eab4070c7c6955 100644 --- a/mobile/app/travel_history/_layout.tsx +++ b/mobile/app/travel_history/_layout.tsx @@ -17,17 +17,32 @@ export default function Layout() { headerTintColor: LIGHT_THEME.color.white, }} > - - }}/> + - - + + + + ); } diff --git a/mobile/src/activity/screens/activity_point.tsx b/mobile/src/activity/screens/activity_point.tsx index 415f5fc0fc3c248b198e2c24bc8bfb56a1d3a49a..ff99b59a4677b1c49011700ff788771dea9f169d 100644 --- a/mobile/src/activity/screens/activity_point.tsx +++ b/mobile/src/activity/screens/activity_point.tsx @@ -12,6 +12,8 @@ import { StarRatingForm } from "../../common/components/rating_page/star_rating_ import { FullPageRating } from "../../common/components/rating_page/full_page_rating"; import { useRankActivity } from "../hooks/useRankActivity"; import { useGetActivityPoint } from "../hooks/useGetActivityPoint"; +import { FloatingBackButton } from "../../common/components/floating_back_button"; +import { Ionicons } from "@expo/vector-icons"; const touristGuide = require("../../../assets/guide.gif"); @@ -48,13 +50,21 @@ export const ActivityPointScreen = memo( backAction ); - return () => backHandler.remove(); + return () => { + onUnmount(); + backHandler.remove(); + }; }, []); const doNextActivity = () => { router.replace("/scan"); }; + const handleBack = () => { + onUnmount(); + router.back(); + }; + if (requestStatus === ApiRequestStatus.LOADING) { return ; } @@ -65,6 +75,31 @@ export const ActivityPointScreen = memo( return ( + + + + + + Activity Point + + + + End Activity + + + {data.name} @@ -131,7 +166,7 @@ export const ActivityPointScreen = memo( const styles = StyleSheet.create({ container: { flex: 1, - gap: 15, + gap: 5, justifyContent: "space-between", }, placeContainer: { @@ -185,4 +220,22 @@ const styles = StyleSheet.create({ fontWeight: "bold", fontSize: 16, }, + back_button: { + height: 40, + width: 40, + justifyContent: "center", + alignItems: "center", + borderRadius: 25, + backgroundColor: "white", + borderWidth: 2, + elevation: 5, + }, + headerEndActivityButton: { + backgroundColor: LIGHT_THEME.color.primary, + padding: 10, + borderRadius: 25, + alignItems: "center", + justifyContent: "center", + height: 50, + }, }); diff --git a/mobile/src/auth/contexts/auth_context.tsx b/mobile/src/auth/contexts/auth_context.tsx index 636841f85e90a646ed6a00c8ee8cb738d98cd370..25d8ddc5c6199c59bf019eab29a04cf4296e57a4 100644 --- a/mobile/src/auth/contexts/auth_context.tsx +++ b/mobile/src/auth/contexts/auth_context.tsx @@ -8,6 +8,7 @@ import { import { UserInfoEntity } from "../domain/entities/user_info_entity"; import * as SecureStore from "expo-secure-store"; import axios from "axios"; +import { UserPreferences } from "../../common/domain/entities/user_preferences"; type AuthContextType = { user: UserInfoEntity | null; @@ -33,24 +34,49 @@ export const AuthContextProvider = ({ children }: AuthContextProviderProps) => { const [isLoading, setIsLoading] = useState(true); const checkSession = async () => { - const isVerified = await SecureStore.getItemAsync("isVerified"); - if (isVerified === "true") { - setIsVerified(true); - } const user = await SecureStore.getItemAsync("user"); const token = await SecureStore.getItemAsync("token"); console.log(user); console.log(token); if (user && token) { - setUser(JSON.parse(user)); + const recoveredUser = JSON.parse(user) as UserInfoEntity; + setUser(recoveredUser); + const ssKey = recoveredUser.email.split("@")[0]; + const userPreferences = await SecureStore.getItemAsync(ssKey); + if (userPreferences) { + const parsedUserPreferences = JSON.parse( + userPreferences + ) as UserPreferences; + console.log("User Preferences from verify: 2", userPreferences); + setIsVerified(parsedUserPreferences.isVerifiedEmail); + } else { + console.log("User Preferences from verify: 4", userPreferences); + const emptyUserPreferences = { + isFirstTime: true, + isVerifiedEmail: false, + }; + await SecureStore.setItemAsync( + ssKey, + JSON.stringify(emptyUserPreferences) + ); + setIsVerified(false); + } axios.defaults.headers.common["Authorization"] = `Bearer ${token}`; } setIsLoading(false); }; const verify = () => { + if (!user) return; + const userPreferences = SecureStore.getItemAsync(user.email); + console.log("User Preferences from verify 1: ", userPreferences); + const sskey = user.email.split("@")[0]; + console.log(sskey); + SecureStore.setItemAsync( + sskey, + JSON.stringify({ ...userPreferences, isVerifiedEmail: true }) + ); setIsVerified(true); - SecureStore.setItemAsync("isVerified", "true"); }; const login = async (userInfo: UserInfoEntity, token: string) => { diff --git a/mobile/src/common/contexts/set_up_context.tsx b/mobile/src/common/contexts/set_up_context.tsx index 575de77ccb31912e6a7c0c22b3f44598307ce724..ac8e1519be141755e4eac53e437117839fb5de99 100644 --- a/mobile/src/common/contexts/set_up_context.tsx +++ b/mobile/src/common/contexts/set_up_context.tsx @@ -6,8 +6,11 @@ import { useState, } from "react"; import * as SecureStore from "expo-secure-store"; +import { useAuth } from "../../auth/contexts/auth_context"; +import { UserPreferences } from "../domain/entities/user_preferences"; type SetUpContextType = { + isLoading: boolean; isFirstTime: boolean; setFirstTime: () => Promise; }; @@ -17,24 +20,70 @@ type SetUpContextProviderProps = PropsWithChildren<{}>; const SetUpContext = createContext({ isFirstTime: true, setFirstTime: async () => {}, + isLoading: true, }); export const SetUpContextProvider = ({ children, }: SetUpContextProviderProps) => { - const [isFirstTime, setIsFirstTime] = useState( - SecureStore.getItem("isFirstTime") === "false" ? false : true - ); + const [isLoading, setIsLoading] = useState(true); + const { user, isLoading: isLoadingUser } = useAuth(); + const [isFirstTime, setIsFirstTime] = useState(true); + const setFirstTime = async () => { - await SecureStore.setItemAsync("isFirstTime", "false"); - setIsFirstTime(false); + if (!user) { + setIsLoading(false); + return; + } + const sskey = user.email.split("@")[0]; + const userPreferences = await SecureStore.getItemAsync(sskey); + if (userPreferences) { + const parsedUserPreferences = JSON.parse( + userPreferences + ) as UserPreferences; + parsedUserPreferences.isFirstTime = false; + await SecureStore.setItemAsync( + sskey, + JSON.stringify(parsedUserPreferences) + ); + setIsFirstTime(false); + } }; const value = { isFirstTime, setFirstTime, + isLoading, }; + useEffect(() => { + const checkFirstTime = async () => { + if (isLoadingUser) return; + if (!user) { + setIsLoading(false); + return; + } + const sskey = user.email.split("@")[0]; + const value = await SecureStore.getItemAsync(sskey); + if (value) { + const userPreferences = JSON.parse(value) as UserPreferences; + setIsFirstTime(userPreferences.isFirstTime); + } else { + const emptyUserPreferences = { + isFirstTime: true, + isVerifiedEmail: false, + }; + await SecureStore.setItemAsync( + sskey, + JSON.stringify(emptyUserPreferences) + ); + } + setIsLoading(false); + }; + + checkFirstTime(); + }, [user, isLoadingUser]); + return ( {children} ); diff --git a/mobile/src/common/domain/entities/user_preferences.ts b/mobile/src/common/domain/entities/user_preferences.ts new file mode 100644 index 0000000000000000000000000000000000000000..13fdc3031f08b64d8b39c1905cbf20642f9388c1 --- /dev/null +++ b/mobile/src/common/domain/entities/user_preferences.ts @@ -0,0 +1,4 @@ +export interface UserPreferences { + isFirstTime: boolean; + isVerifiedEmail: boolean; +} diff --git a/mobile/src/route/components/route_activity_tile.tsx b/mobile/src/route/components/route_activity_tile.tsx index fa7d67e0d1b7c5053dc7991e4fb155a187c1c8c4..6928a784be64218a9e5fac86612d4ed0e804e5d7 100644 --- a/mobile/src/route/components/route_activity_tile.tsx +++ b/mobile/src/route/components/route_activity_tile.tsx @@ -3,6 +3,7 @@ import { Entypo } from "@expo/vector-icons"; import { LIGHT_THEME } from "../../common/const/theme"; import { FlatList } from "react-native-gesture-handler"; import { ActivityRouteEntity } from "../../activity/domain/entities/activity_info_entity"; +import { formatToDDMMYYYY } from "../../common/utils/time"; interface RouteActivityTileProps { activity: ActivityRouteEntity; @@ -42,8 +43,8 @@ export const RouteActivityTile = ({ {activity.name} - {activity.startTime.toISOString()} - {activity.location} + {formatToDDMMYYYY(activity.startTime)} + {activity.location} { + console.log(item); return ( {item.description} - - {item.rating.toFixed(1)} - - + {item.done ? ( + <> + + {item.rating.toFixed(1)} + + + + ) : ( + + Not Finished + + )} diff --git a/mobile/src/travel/domain/entities/travel_details.ts b/mobile/src/travel/domain/entities/travel_details.ts index 6ab94783716cef9edb154911b8f67a8007791c40..6c164904b98ed6bc1012a89d15d8328cc20ce11f 100644 --- a/mobile/src/travel/domain/entities/travel_details.ts +++ b/mobile/src/travel/domain/entities/travel_details.ts @@ -2,12 +2,15 @@ import { PlaceInfoEntity } from "../../../place/domain/entities/place_info_entit import { Route } from "./travel_history"; export interface TravelDetails { - travel: Route; - activityList: TravelHistoryActivity[]; + travel: Route; + isAbleToCreateAnImage: boolean; + activityList: TravelHistoryActivity[]; } export interface TravelHistoryActivity extends PlaceInfoEntity { - startDate: Date; - endDate: Date; - rating: number; -} \ No newline at end of file + startDate: Date; + endDate: Date; + rating: number; + categories?: string[]; + done: boolean; +} diff --git a/mobile/src/travel/infrastructure/datasources/prod/travel_datasource_prod.ts b/mobile/src/travel/infrastructure/datasources/prod/travel_datasource_prod.ts index b10e249cb56de77bca2ad3e17f9ea048f13dd66b..cddd4a5fa454f1230da521451be321fd4aa9e0de 100644 --- a/mobile/src/travel/infrastructure/datasources/prod/travel_datasource_prod.ts +++ b/mobile/src/travel/infrastructure/datasources/prod/travel_datasource_prod.ts @@ -41,7 +41,7 @@ export class TravelDatasourceProd implements TravelDataSource { } async getTravelDetails(id: number): Promise { const { data, status } = await axios.get( - `${API_URL}/route/${id}/es` + `${API_URL}/route/info/${id}` ); if (status !== 200) { throw new Error("Error fetching travel details"); @@ -50,18 +50,22 @@ export class TravelDatasourceProd implements TravelDataSource { return { travel: { id: data.idRoute, - imageUri: "http://192.168.0.14:3005/" + data.town.imageName, + imageUri: `${API_URL}/towns/` + data.town.imageName, destination: data.town.name, startDate: new Date(data.startDate), endDate: new Date(data.endDate), }, + isAbleToCreateAnImage: + data.travelPlace.filter((place) => place.done).length > 0, activityList: data.travelPlace.map((place) => ({ id: place.place.idPlace, name: place.place.name, - rating: place.place.idPlace < 5 ? 5 : 0, + rating: place.place.visited?.rating || 0, + done: place.done, startDate: new Date(place.startDate), endDate: new Date(place.endDate), - image: place.place.imageName, + imageUri: place.place.imageName, + categories: place.place.categories?.map((category) => category.name), })), }; } diff --git a/mobile/src/travel/screens/travel_details_page.tsx b/mobile/src/travel/screens/travel_details_page.tsx index ce3714744d8a877a416e6aab1e23b05426390885..f4edfb5183e31fbda9b28dc12a505055ed19ad20 100644 --- a/mobile/src/travel/screens/travel_details_page.tsx +++ b/mobile/src/travel/screens/travel_details_page.tsx @@ -1,9 +1,19 @@ -import { View, Text, StyleSheet, Image, Dimensions } from "react-native"; +import { + View, + Text, + StyleSheet, + Image, + Dimensions, + TouchableOpacity, +} from "react-native"; import { useGetTravelDetails } from "../../travel/hooks/useGetTravelDetails"; import { ApiRequestStatus } from "../../common/const/api_request_states"; import { FullPageLoader } from "../../common/components/full_page_loader"; import { TravelActivityList } from "../../travel/components/activity_list"; import { formatToDDMMYYYY } from "../../common/utils/time"; +import { AntDesign } from "@expo/vector-icons"; +import { LIGHT_THEME } from "../../common/const/theme"; +import { router } from "expo-router"; interface TravelDetailsPageProps { id: number; @@ -12,6 +22,10 @@ interface TravelDetailsPageProps { export const TravelDetailsPage = ({ id }: TravelDetailsPageProps) => { const { data, requestStatus } = useGetTravelDetails({ id }); + const onSharePress = () => { + router.push(`/travel_history/download/${id}`); + }; + if (requestStatus === ApiRequestStatus.LOADING) { return ; } @@ -42,6 +56,11 @@ export const TravelDetailsPage = ({ id }: TravelDetailsPageProps) => { + {data.isAbleToCreateAnImage && ( + + + + )} ); }; @@ -74,4 +93,16 @@ const styles = StyleSheet.create({ fontSize: 30, fontWeight: "bold", }, + shareButton: { + position: "absolute", + bottom: 20, + right: 20, + height: 50, + width: 50, + borderRadius: 25, + backgroundColor: LIGHT_THEME.color.primary, + justifyContent: "center", + alignItems: "center", + elevation: 5, + }, }); diff --git a/mobile/src/travel/screens/travel_history_page.tsx b/mobile/src/travel/screens/travel_history_page.tsx index 7162ef377226e81ea4d74560aa490a62dd11ba3c..ab93cb03c681c935f7e486af63ebdbd11b35e2ff 100644 --- a/mobile/src/travel/screens/travel_history_page.tsx +++ b/mobile/src/travel/screens/travel_history_page.tsx @@ -1,4 +1,11 @@ -import { View, Text, FlatList, StyleSheet, SectionList } from "react-native"; +import { + View, + Text, + FlatList, + StyleSheet, + SectionList, + Button, +} from "react-native"; import { TravelHistorySection, useGetTravelHistory, @@ -22,7 +29,7 @@ export const TravelHistoryPage = () => { } }); - const onTresholdReached = () => { + const refreshClick = () => { refresh(); }; @@ -52,6 +59,17 @@ export const TravelHistoryPage = () => { { + return ( + +