diff --git a/backend/package-lock.json b/backend/package-lock.json index 240c5eb81b747230cda65170ddccbc2972840235..f487c14820109de20d308db79ff8cd51cc21472f 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -23,6 +23,7 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "dotenv": "^16.4.5", + "ejs": "^3.1.10", "mysql2": "^3.9.2", "network": "^0.7.0", "nodemailer": "^6.9.15", @@ -5321,7 +5322,6 @@ "version": "3.1.10", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", - "optional": true, "dependencies": { "jake": "^10.8.5" }, @@ -6120,7 +6120,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", - "optional": true, "dependencies": { "minimatch": "^5.0.1" } @@ -6129,7 +6128,6 @@ "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "optional": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -7575,7 +7573,6 @@ "version": "10.9.2", "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", - "optional": true, "dependencies": { "async": "^3.2.3", "chalk": "^4.0.2", @@ -7592,14 +7589,12 @@ "node_modules/jake/node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "optional": true + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" }, "node_modules/jake/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "optional": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -7609,7 +7604,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "optional": true, "dependencies": { "brace-expansion": "^1.1.7" }, diff --git a/backend/package.json b/backend/package.json index c3b63857df5f9f3d1435056dae62e6106aea6893..3c5b8991342b5193e029565921feb078bf43243a 100644 --- a/backend/package.json +++ b/backend/package.json @@ -34,6 +34,7 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "dotenv": "^16.4.5", + "ejs": "^3.1.10", "mysql2": "^3.9.2", "network": "^0.7.0", "nodemailer": "^6.9.15", diff --git a/backend/src/route/index.html b/backend/src/route/index.html deleted file mode 100644 index 35d7bb58d66fbaff3dcb70625e32208d72c53108..0000000000000000000000000000000000000000 --- a/backend/src/route/index.html +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - Document - - - -
-
-

- -

-
-
-
-
-
-
-
- -
-
- - \ No newline at end of file diff --git a/backend/src/visited/templates/index.html b/backend/src/visited/templates/index.html new file mode 100644 index 0000000000000000000000000000000000000000..d3e4cfb1acae41fdbedf9f1a9cc786cb9d95c756 --- /dev/null +++ b/backend/src/visited/templates/index.html @@ -0,0 +1,144 @@ + + + + + + Document + + + +
+
+

Jerez de García Salinas, Zacatecas

+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + diff --git a/backend/src/visited/templates/visit_template.ts b/backend/src/visited/templates/visit_template.ts new file mode 100644 index 0000000000000000000000000000000000000000..49b4fb79e199e446b22ca353d804f20a62c667da --- /dev/null +++ b/backend/src/visited/templates/visit_template.ts @@ -0,0 +1,126 @@ +export const visit_template = (places: string[]) => ` + + + + + + Document + + + +
+
+

Jerez de García Salinas, Zacatecas

+
+
+ ${ + places.map( + (place) => `
+ +
` + ).join('\n') + } +
+ +
+
+ + +`; \ No newline at end of file diff --git a/backend/src/visited/utils/visited_places_image_creator.ts b/backend/src/visited/utils/visited_places_image_creator.ts new file mode 100644 index 0000000000000000000000000000000000000000..7fd342e4509c4e2672a0d87d1345b93346915f20 --- /dev/null +++ b/backend/src/visited/utils/visited_places_image_creator.ts @@ -0,0 +1,40 @@ +import { render } from 'ejs'; +import { readFileSync, existsSync, mkdir } from 'fs'; +import puppeteer from 'puppeteer'; +import { randomUUID } from 'crypto'; +import { join } from 'path'; +import { visit_template } from '../templates/visit_template'; +import { ServerConstants } from 'src/constants/server.contants'; + +export class VisitedPlacesImageCreator { + public async generateImage(imagesURL: string[]) { + try { + const template = this.getTemplate(imagesURL); + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.setContent(template); + await page.setViewport({ width: 470, height: 320, deviceScaleFactor: 2 }); + const outDir = join(__dirname, "../../../static/visits"); + if (!existsSync(outDir)) { + mkdir(outDir, { recursive: true }, (err) => { + if (err) throw err; + }); + } + const imageName = `${randomUUID()}.png`; + const outPath = join(outDir, imageName); + await page.screenshot({ path: outPath }); + await browser.close(); + + return join(ServerConstants.HOST, "visits", imageName); + + } catch (error) { + console.log(error); + throw error; + } + } + + private getTemplate(images: string[]) { + const template = visit_template(images); + return render(template, { images }); + } +} \ No newline at end of file diff --git a/backend/src/visited/visited.controller.ts b/backend/src/visited/visited.controller.ts index 039d63dff9cdc676fdad6ecf6d9e2b5012030515..e5a6741ef228f39010c0b8e386d80adb576d7e02 100644 --- a/backend/src/visited/visited.controller.ts +++ b/backend/src/visited/visited.controller.ts @@ -23,6 +23,11 @@ export class VisitedController { } } + @Get('/getImage/:routeId') + async getVisitedPlacesImage(@Param('routeId') routeId: string) { + return await this.visitedService.getVisitedPlacesImage(); + } + @Get() findAll() { return this.visitedService.findAll(); diff --git a/backend/src/visited/visited.service.ts b/backend/src/visited/visited.service.ts index dbe0fa9ec38354224044106f28a8897aacd97af8..6b3f1d138c76d869b0f5e08f451c98580505adde 100644 --- a/backend/src/visited/visited.service.ts +++ b/backend/src/visited/visited.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { BadRequestException, HttpException, Injectable, InternalServerErrorException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Visited } from './entities/visited.entity'; import { Repository } from 'typeorm'; @@ -7,6 +7,9 @@ import { UserService } from 'src/user/user.service'; import { CreateVisitedDto } from './dto/create-visited.dto'; import { Place } from 'src/place/entities/place.entity'; import { PlaceService } from 'src/place/place.service'; +import { LANGUAGES } from 'src/shared/enum/languages.enum'; +import { VisitedPlacesImageCreator } from './utils/visited_places_image_creator'; +import { HttpErrorByCode } from '@nestjs/common/utils/http-error-by-code.util'; @Injectable() export class VisitedService { @@ -47,4 +50,15 @@ export class VisitedService { remove(id: number) { return `This action removes a #${id} visited`; } + + async getVisitedPlacesImage() { + // obtener imágenes de los lugares visitados en una ruta + try { + const visitedPlaces = (await this.placeService.findAllByTown(1, LANGUAGES.EN)).map((place) => place.imageName).slice(0, 5); + const visitedPlacesImageCreator = new VisitedPlacesImageCreator(); + return await visitedPlacesImageCreator.generateImage(visitedPlaces); + } catch (error) { + throw new InternalServerErrorException('Error generating image'); + } + } } diff --git a/backend/static/audios/default.mp3 b/backend/static/audios/default.mp3 deleted file mode 100644 index daf789f55d1e091af6dfb6982bf137ed623eefe2..0000000000000000000000000000000000000000 Binary files a/backend/static/audios/default.mp3 and /dev/null differ diff --git a/mobile/app.json b/mobile/app.json index 2329aa44d979dd3482badc525c712402a46098bd..be791f72bfad58b345f76f42a757f827d9fabd7d 100644 --- a/mobile/app.json +++ b/mobile/app.json @@ -49,6 +49,14 @@ "photosPermission": "The app accesses your photos to let you share them with your friends." } ], + [ + "expo-media-library", + { + "photosPermission": "Allow $(PRODUCT_NAME) to access your photos.", + "savePhotosPermission": "Allow $(PRODUCT_NAME) to save photos.", + "isAccessMediaLocationEnabled": true + } + ], "expo-router", "expo-secure-store", "expo-localization" diff --git a/mobile/app/routes/generate_route_form.tsx b/mobile/app/routes/generate_route_form.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f1a5d070d7b7ecc910df8947f63b769b565a7f0f --- /dev/null +++ b/mobile/app/routes/generate_route_form.tsx @@ -0,0 +1,10 @@ +import { useLocalSearchParams } from "expo-router"; +import { GenerateRoutePage } from "../../src/route/screens/generate_route_screen"; + +export default function GenerateRouteForm() { + const { townId } = useLocalSearchParams<{ townId: string }>(); + if (!townId) { + return null; + } + return ; +} diff --git a/mobile/app/travel_history/_layout.tsx b/mobile/app/travel_history/_layout.tsx index 50b65d532a26b264a8ec35fabb9aac1805256455..a72ebe32982fe7830ee29b4416f1e56eef811ce5 100644 --- a/mobile/app/travel_history/_layout.tsx +++ b/mobile/app/travel_history/_layout.tsx @@ -1,4 +1,4 @@ -import { Stack } from "expo-router"; +import { router, Stack } from "expo-router"; import { LIGHT_THEME } from "../../src/common/const/theme"; import { Button } from "react-native"; import { ShareButton } from "../../src/travel/components/share_button"; @@ -19,7 +19,14 @@ export default function Layout() { > {}}/> + headerRight: (props) => + }}/> + + + ); diff --git a/mobile/app/travel_history/download/[id].tsx b/mobile/app/travel_history/download/[id].tsx new file mode 100644 index 0000000000000000000000000000000000000000..2cbf6de14041a060169e988e332ce559e64d81ca --- /dev/null +++ b/mobile/app/travel_history/download/[id].tsx @@ -0,0 +1,19 @@ +import { useLocalSearchParams } from "expo-router"; +import { View, Text } from "react-native"; +import { DownloadTravel } from "../../../src/travel/screens/download_travel"; + +export default function Details() { + const { id } = useLocalSearchParams<{ id: string }>(); + + if (!id) { + return ( + + Invalid ID + + ); + } + + return ( + + ); +} \ No newline at end of file diff --git a/mobile/app/travel_history/error/save_travel_image_error.tsx b/mobile/app/travel_history/error/save_travel_image_error.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0ce233737b1514e8705dcb880367b1558bae1802 --- /dev/null +++ b/mobile/app/travel_history/error/save_travel_image_error.tsx @@ -0,0 +1,7 @@ +import { DownloadImageError } from "../../../src/travel/screens/download_image_error"; + +export default function DownloadImageErrorScreen() { + return ( + + ); +} \ No newline at end of file diff --git a/mobile/package-lock.json b/mobile/package-lock.json index 1cc4f104c2ea25ebc04fc431bb929b8ecc2a9b7a..7382d52821c21fbc45764483e981e012a938645e 100644 --- a/mobile/package-lock.json +++ b/mobile/package-lock.json @@ -20,10 +20,13 @@ "expo-camera": "~14.1.1", "expo-checkbox": "~2.7.0", "expo-constants": "~15.4.5", + "expo-crypto": "~12.8.1", + "expo-file-system": "~16.0.9", "expo-image-picker": "~14.7.1", "expo-linear-gradient": "~12.7.2", "expo-linking": "~6.2.2", "expo-localization": "~14.8.4", + "expo-media-library": "~15.9.2", "expo-router": "~3.4.8", "expo-screen-orientation": "~6.4.1", "expo-secure-store": "~12.8.1", @@ -8912,6 +8915,17 @@ "expo": "*" } }, + "node_modules/expo-crypto": { + "version": "12.8.1", + "resolved": "https://registry.npmjs.org/expo-crypto/-/expo-crypto-12.8.1.tgz", + "integrity": "sha512-EJEzmfBUSkGfALTlZRKUbh1RMKF7mWI12vkhO2w6bhGO4bjgGB8XzUHgLfrvSjphDFMx/lwaR6bAQDmXKO9UkQ==", + "dependencies": { + "base64-js": "^1.3.0" + }, + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-file-system": { "version": "16.0.9", "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-16.0.9.tgz", @@ -8986,6 +9000,14 @@ "expo": "*" } }, + "node_modules/expo-media-library": { + "version": "15.9.2", + "resolved": "https://registry.npmjs.org/expo-media-library/-/expo-media-library-15.9.2.tgz", + "integrity": "sha512-ExRcCxNO768aWPQr9axuBDQLcFnRTSiqvWZ1XvnopCfZEic04wJ/CPAE1hLqTp7AyYrd6jHpqxa/aNKBAAFVeA==", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-modules-autolinking": { "version": "1.10.3", "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-1.10.3.tgz", diff --git a/mobile/package.json b/mobile/package.json index 2101dbd7267762b7b73fdb515ff7e188c90612a2..b2ed53aafcbccd7aae0d599bd468a3dc50c94183 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -45,7 +45,10 @@ "react-native-reanimated": "~3.6.2", "react-native-safe-area-context": "4.8.2", "react-native-screens": "~3.29.0", - "react-native-svg": "14.1.0" + "react-native-svg": "14.1.0", + "expo-file-system": "~16.0.9", + "expo-media-library": "~15.9.2", + "expo-crypto": "~12.8.1" }, "devDependencies": { "@babel/core": "^7.20.0", diff --git a/mobile/src/activity/hooks/useRankActivity.ts b/mobile/src/activity/hooks/useRankActivity.ts index 1478086dc56694715cdf470a520db511a1502040..22166e35446f5c7b12df97451f1df4bac3c0ef17 100644 --- a/mobile/src/activity/hooks/useRankActivity.ts +++ b/mobile/src/activity/hooks/useRankActivity.ts @@ -1,6 +1,7 @@ import { useState } from "react"; import { useDataContext } from "../../common/contexts/data_context"; import { ApiRequestStatus } from "../../common/const/api_request_states"; +import { router } from "expo-router"; export const useRankActivity = (activityId: number) => { const { activityRepository } = useDataContext(); diff --git a/mobile/src/activity/infrastructure/datasources/prod/activity_datasource.ts b/mobile/src/activity/infrastructure/datasources/prod/activity_datasource.ts index 1dbbf26935889319e2b14edd4704977ec048f5e5..f6fedd3bd2cf6339100b992ce76e805372891b87 100644 --- a/mobile/src/activity/infrastructure/datasources/prod/activity_datasource.ts +++ b/mobile/src/activity/infrastructure/datasources/prod/activity_datasource.ts @@ -9,10 +9,16 @@ import { ActivityPlaceEntity } from "../../../domain/entities/activity_place_ent export class ActivityDatasourceProd implements ActivityDataSource { constructor(private lang: string = Languages.SPANISH) {} async rankActivity(activityId: number, rank: number): Promise { - throw new Error("Method not implemented."); + const {status} = await axios.post(`${API_URL}/visited`, { + idPlace: activityId, + rating: rank + }); + if (status !== 201) { + throw new Error("Error al calificar la actividad"); + } } async getPlaceActivity(activityId: number, townId: number, stateId: number, placeNumber: number): Promise { - const { data, status } = await axios.get(`${API_URL}/point/${placeNumber}lang?lang=${this.lang}`); + const { data, status } = await axios.get(`${API_URL}/point/${placeNumber}?lang=${this.lang}`); if (status !== 200) { throw new Error("Error al obtener la información del lugar"); } diff --git a/mobile/src/auth/components/code_form.tsx b/mobile/src/auth/components/code_form.tsx index cb62d54ff33e99165e666b695085b46d3de33098..6f6b1e78645f676c80ac4e88ee4f3771b1e38394 100644 --- a/mobile/src/auth/components/code_form.tsx +++ b/mobile/src/auth/components/code_form.tsx @@ -1,7 +1,7 @@ import { View, Text, StyleSheet } from "react-native"; import { MultipleDigitsCode } from "./multiple_digits_code"; import { Control, UseFormSetValue } from "react-hook-form"; -import { ResetPasswordFormValues } from "../pages/reset_password_page"; +import { ResetPasswordFormValues } from "../hooks/useResetPassword"; interface CodeFormProps { diff --git a/mobile/src/auth/errors/api_response_error.ts b/mobile/src/auth/errors/api_response_error.ts new file mode 100644 index 0000000000000000000000000000000000000000..2bd8e6cdc4653d9627670e56901cc1ef54b67a16 --- /dev/null +++ b/mobile/src/auth/errors/api_response_error.ts @@ -0,0 +1,7 @@ +export class ApiResponseError extends Error { + public readonly status: number; + constructor(status: number, message?: string) { + super(message); + this.status = status; + } +} \ No newline at end of file diff --git a/mobile/src/auth/hooks/useLoggin.ts b/mobile/src/auth/hooks/useLoggin.ts index 4c03e922d3968b14b3ff5a9642834032b1a7ba63..53fa44e2cfae87cf49149444a38f8d54826671e7 100644 --- a/mobile/src/auth/hooks/useLoggin.ts +++ b/mobile/src/auth/hooks/useLoggin.ts @@ -4,6 +4,7 @@ import { Navigator, Redirect, router } from "expo-router"; import { useDataContext } from "../../common/contexts/data_context"; import { useState } from "react"; import { ApiRequestStatus } from "../../common/const/api_request_states"; +import { ApiResponseError } from "../errors/api_response_error"; export type LoginFormValues = { email: string; @@ -15,6 +16,7 @@ export const useLoggin = () => { const { authRepository } = useDataContext(); const { login } = useAuth(); const [requestStatus, setRequestStatus] = useState(ApiRequestStatus.IDLE); + const [isErrorModalVisible, setIsErrorModalVisible] = useState(false); const setLoading = async () => { setRequestStatus(ApiRequestStatus.LOADING); @@ -28,19 +30,26 @@ export const useLoggin = () => { setRequestStatus(ApiRequestStatus.SUCCESS); router.replace('/(tabs)'); } catch (error: any) { - setRequestStatus(ApiRequestStatus.ERROR); - switch (error.response.status) { + const {status} = error as ApiResponseError; + console.log(error) + switch (status) { case 401: setError('email', { type: 'manual', message: 'Invalid email or password' }); setError('password', { type: 'manual', message: 'Invalid email or password' }); + setRequestStatus(ApiRequestStatus.ERROR); break; default: - console.log('Something went wrong'); + setIsErrorModalVisible(true); + setRequestStatus(ApiRequestStatus.ERROR); break; } } } + const closeErrorModal = () => { + setIsErrorModalVisible(false); + } + const invalidSubmit = (errors: any) => { console.log(errors); } @@ -49,5 +58,5 @@ export const useLoggin = () => { await handleSubmit(validSubmit, invalidSubmit)(); } - return { control, onSubmit, requestStatus }; + return { control, onSubmit, closeErrorModal, requestStatus, isErrorModalVisible }; } \ No newline at end of file diff --git a/mobile/src/auth/hooks/useSignUp.ts b/mobile/src/auth/hooks/useSignUp.ts index 12e90cdd60a25f15427c85d7c5d3e6c4d263c81f..af7570c7347b052f3af269a31465064a11033cde 100644 --- a/mobile/src/auth/hooks/useSignUp.ts +++ b/mobile/src/auth/hooks/useSignUp.ts @@ -4,6 +4,7 @@ import { useAuth } from "../contexts/auth_context"; import { router } from "expo-router"; import { ApiRequestStatus } from "../../common/const/api_request_states"; import { useCallback, useState } from "react"; +import { ApiResponseError } from "../errors/api_response_error"; export interface SignUpFormValues { name: string; @@ -19,6 +20,7 @@ export const useSignUp = () => { const { authRepository } = useDataContext(); const { login } = useAuth(); const [requestStatus, setRequestStatus] = useState(ApiRequestStatus.IDLE); + const [isErrorModalVisible, setIsErrorModalVisible] = useState(false); const setLoading = async () => { setRequestStatus(ApiRequestStatus.LOADING); @@ -44,18 +46,25 @@ export const useSignUp = () => { setRequestStatus(ApiRequestStatus.SUCCESS); router.replace("/(tabs)"); } catch (error: any) { - setRequestStatus(ApiRequestStatus.ERROR); - switch (error.response.status) { + const {status} = error as ApiResponseError; + console.log(status); + switch (status) { case 400: setError("email", { type: "manual", message: "Email already in use" }); + setRequestStatus(ApiRequestStatus.ERROR); break; default: - console.log("Something went wrong"); + setIsErrorModalVisible(true); + setRequestStatus(ApiRequestStatus.ERROR); break; } } } + const closeErrorModal = () => { + setIsErrorModalVisible(false); + } + const invalidSubmit = (errors: any) => { console.log(errors); } @@ -64,5 +73,5 @@ export const useSignUp = () => { await handleSubmit(validSubmit, invalidSubmit)(); }, [handleSubmit, validSubmit, invalidSubmit]); - return { control, onSubmit, requestStatus }; + return { control, onSubmit, requestStatus, isErrorModalVisible, closeErrorModal }; } \ No newline at end of file diff --git a/mobile/src/auth/infrastructure/prod/datasources/auth_datasource.ts b/mobile/src/auth/infrastructure/prod/datasources/auth_datasource.ts index a73bee3232b0eb64b146fbe580fef76401312f92..2b5526ecbc987f954fc472445490f7ca32b2101e 100644 --- a/mobile/src/auth/infrastructure/prod/datasources/auth_datasource.ts +++ b/mobile/src/auth/infrastructure/prod/datasources/auth_datasource.ts @@ -5,9 +5,9 @@ import { RegisterInfoEntity } from "../../../domain/entities/register_info"; import { userRegisterEntityToUserRegisterModel } from "../utils/user_util"; import { API_URL } from "../../../../common/const/api"; import { LoginUserModel } from "../models/login_user_model"; -import { UserInfoEntity } from "../../../domain/entities/user_info_entity"; import { RegisterResponseModel } from "../models/register_user_model"; import { ResetPasswordInfoEntity } from "../../../domain/entities/reset_password_entity"; +import { ApiResponseError } from "../../../errors/api_response_error"; export class AuthDatasourceProd implements AuthDataSource { async getResetCode (email: string): Promise { @@ -33,33 +33,36 @@ export class AuthDatasourceProd implements AuthDataSource { } } login: (email: string, password: string) => Promise = async (email, password) => { - const loginInfo: LoginUserModel = { - email, - password - }; - //TODO: Hacer el login info model y el user info model cuando se termine la documentación - const { data, status } = await axios.post(`${API_URL}/user/signin`, loginInfo); - if (status !== 201) { - throw new Error("Error logging in"); - } - const loginResponse: LoginInfoEntity = { - token: data.user.token, - user: { - id: data.user.userId, - email: data.user.email, - name: data.user.name, - lastName: data.user.lastName, + try{ + const loginInfo: LoginUserModel = { + email, + password + }; + //TODO: Hacer el login info model y el user info model cuando se termine la documentación + const { data, status } = await axios.post(`${API_URL}/user/signin`, loginInfo); + const loginResponse: LoginInfoEntity = { + token: data.user.token, + user: { + id: data.user.userId, + email: data.user.email, + name: data.user.name, + lastName: data.user.lastName, + } + } + return loginResponse; + } catch (error: any) { + if (axios.isAxiosError(error)) { + throw new ApiResponseError(error.response!.status, error.response?.data.message); } + throw new ApiResponseError(500, "Internal Server Error"); } - return loginResponse; }; register: (user: RegisterInfoEntity) => Promise = async (user) => { - const newUser = userRegisterEntityToUserRegisterModel(user); + try { + const newUser = userRegisterEntityToUserRegisterModel(user); const { data, status } = await axios.post(`${API_URL}/user/signup`, newUser); - if (status !== 201) { - throw new Error("Error registering user"); - } + const registeredUser: LoginInfoEntity = { token: data.user.token, user: { @@ -70,5 +73,11 @@ export class AuthDatasourceProd implements AuthDataSource { } } return registeredUser; + } catch (error: any) { + if (axios.isAxiosError(error)) { + throw new ApiResponseError(error.response!.status, error.response?.data.message); + } + throw new ApiResponseError(500, "Internal Server Error"); + } }; } \ No newline at end of file diff --git a/mobile/src/auth/pages/login_page.tsx b/mobile/src/auth/pages/login_page.tsx index b9eda1d16debbb57efd1b1b57de95051935e16ba..73309e4b1bd944f5d155b133470dd7914caaa565 100644 --- a/mobile/src/auth/pages/login_page.tsx +++ b/mobile/src/auth/pages/login_page.tsx @@ -1,4 +1,4 @@ -import { View, Image, StyleSheet, Button } from "react-native"; +import { View, Image, StyleSheet, Button, Text } from "react-native"; import { LoginForm } from "../components/login_form"; import { useLoggin } from "../hooks/useLoggin"; import { LIGHT_THEME } from "../../common/const/theme"; @@ -7,17 +7,18 @@ import { LanguageIcon } from "../../lang/components/language_icon"; import { ApiRequestStatus } from "../../common/const/api_request_states"; import { FullPageLoader } from "../../common/components/full_page_loader"; import { useTranslation } from "react-i18next"; +import { FullPageMessageModal } from "../../common/components/full_page_message_modal"; +import { ServerConnectionErrorModal } from "../../common/components/modals/server_connection_error"; const loginImage = require("../../../assets/login-image.jpg"); export const LoginPage = () => { - const { control, onSubmit, requestStatus } = useLoggin(); + const { control, onSubmit, closeErrorModal, requestStatus, isErrorModalVisible } = useLoggin(); const LANG = useTranslation(); if (requestStatus === ApiRequestStatus.LOADING) { return ; } - return ( @@ -30,6 +31,10 @@ export const LoginPage = () => { + ); }; diff --git a/mobile/src/auth/pages/sign_up_page.tsx b/mobile/src/auth/pages/sign_up_page.tsx index 3eaf0454b602a8e0f7aeaa1fdc843f5b5331a4b3..33e6a736d2a03860c88e8f0e14ea88fab6d68e3e 100644 --- a/mobile/src/auth/pages/sign_up_page.tsx +++ b/mobile/src/auth/pages/sign_up_page.tsx @@ -5,10 +5,11 @@ import { LIGHT_THEME } from "../../common/const/theme"; import { useSignUp } from "../hooks/useSignUp"; import { ApiRequestStatus } from "../../common/const/api_request_states"; import { FullPageLoader } from "../../common/components/full_page_loader"; +import { ServerConnectionErrorModal } from "../../common/components/modals/server_connection_error"; const loginImage = require("../../../assets/login-image.jpg"); export const SignUpPage = () => { - const { control, onSubmit, requestStatus } = useSignUp(); + const { control, onSubmit, requestStatus, closeErrorModal, isErrorModalVisible } = useSignUp(); if (requestStatus === ApiRequestStatus.LOADING) { return ; @@ -25,6 +26,10 @@ export const SignUpPage = () => { + ); }; diff --git a/mobile/src/common/components/form/date_text_input.tsx b/mobile/src/common/components/form/date_text_input.tsx index 9190c12c015d90913875e7b12d0026dc913e6e4d..54351bc2c8e435dca972536e2b60f510b4c198ed 100644 --- a/mobile/src/common/components/form/date_text_input.tsx +++ b/mobile/src/common/components/form/date_text_input.tsx @@ -9,15 +9,26 @@ interface DateInputProps { value: string; onBlur?: () => void; errors?: string; + mode?: "date" | "time"; } -export const DateTextInput = ({label, onChangeText, value, onBlur, errors}: DateInputProps) => { +export const DateTextInput = ({ + label, + onChangeText, + value, + onBlur, + errors, + mode = "date", +}: DateInputProps) => { const [isVisible, setIsVisible] = useState(false); useEffect(() => { console.log(isVisible); }, [isVisible]); return ( - setIsVisible(true)}> + setIsVisible(true)} + style={{ width: "100%" }} + > {}, }} + alwaysOpen={true} /> { - onChangeText(data.toDateString()); + onChangeText( + mode === "date" ? data.toDateString() : data.toTimeString() + ); setIsVisible(false); }} onCancel={() => { diff --git a/mobile/src/common/components/form/text_input.tsx b/mobile/src/common/components/form/text_input.tsx index b74ecee4fbef5faf105452e385227b1859177ca8..5dc6541b0812eda32864c28314864df0c864cbba 100644 --- a/mobile/src/common/components/form/text_input.tsx +++ b/mobile/src/common/components/form/text_input.tsx @@ -1,7 +1,15 @@ import React, { useEffect, useRef, useState } from "react"; -import { TextInput, Text, StyleSheet, View, Animated, TouchableOpacity, TextInputProps } from "react-native"; +import { + TextInput, + Text, + StyleSheet, + View, + Animated, + TouchableOpacity, + TextInputProps, +} from "react-native"; import { LIGHT_THEME } from "../../const/theme"; -import { Feather } from '@expo/vector-icons'; +import { Feather } from "@expo/vector-icons"; interface CustomTextInputProps { isPassword?: boolean; @@ -11,10 +19,22 @@ interface CustomTextInputProps { errors?: string; editable?: boolean; textInputProps?: TextInputProps; + alwaysOpen?: boolean; } -export const CustomTextInput = ({ textInputProps, label, value, errors, onBlur, editable, isPassword }: CustomTextInputProps) => { - const labelFocusAnimation = useRef(new Animated.Value(value.length > 0 ? 1 : 0)).current; +export const CustomTextInput = ({ + textInputProps, + label, + value, + errors, + onBlur, + editable, + isPassword, + alwaysOpen = false, +}: CustomTextInputProps) => { + const labelFocusAnimation = useRef( + new Animated.Value(value.length > 0 || alwaysOpen ? 1 : 0) + ).current; const inputRef = useRef(null); const [showPassword, setShowPassword] = useState(false); @@ -27,7 +47,7 @@ export const CustomTextInput = ({ textInputProps, label, value, errors, onBlur, }; const handleBlur = () => { - if (!value || value === "") { + if (!alwaysOpen && (!value || value === "")) { Animated.timing(labelFocusAnimation, { toValue: 0, duration: 200, @@ -39,7 +59,7 @@ export const CustomTextInput = ({ textInputProps, label, value, errors, onBlur, return ( - + - { - isPassword && ( - setShowPassword(!showPassword)} - /> - ) - } + {isPassword && ( + setShowPassword(!showPassword)} + /> + )} - { - errors ? {errors} : - } + {errors ? {errors} : } ); }; diff --git a/mobile/src/common/components/full_page_message_modal.tsx b/mobile/src/common/components/full_page_message_modal.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e581a36f043ae5910f29368bd9717c3c4a3cb041 --- /dev/null +++ b/mobile/src/common/components/full_page_message_modal.tsx @@ -0,0 +1,46 @@ +import { PropsWithChildren, ReactNode } from "react"; +import { StyleSheet, View } from "react-native"; +import { MaterialIcons } from '@expo/vector-icons'; + +type FullPageMessageModalProps = PropsWithChildren<{ + onDismiss: () => void; + isVisible: boolean; +}>; + +export const FullPageMessageModal = ({onDismiss, isVisible, children}: FullPageMessageModalProps) => { + + if (!isVisible) { + return null; + } + + return ( + + + + {children} + + + ); +} + +const styles = StyleSheet.create({ + mainContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'rgba(0,0,0,0.5)', + position: 'absolute', + width: '100%', + height: '100%', + zIndex: 4 + }, + modalContainer: { + padding: 20, + backgroundColor: 'white', + borderRadius: 10, + width: '80%', + aspectRatio: 1, + justifyContent: 'center', + alignItems: 'center' + } +}); \ No newline at end of file diff --git a/mobile/src/common/components/modals/server_connection_error.tsx b/mobile/src/common/components/modals/server_connection_error.tsx new file mode 100644 index 0000000000000000000000000000000000000000..4a5087e2859c130bc88e9496838d1f3c57655f1f --- /dev/null +++ b/mobile/src/common/components/modals/server_connection_error.tsx @@ -0,0 +1,21 @@ +import { FullPageMessageModal } from "../full_page_message_modal"; +import { Text } from "react-native"; + +interface ServerConnectionErrorModalProps { + isErrorModalVisible: boolean; + closeErrorModal: () => void; +} + +export const ServerConnectionErrorModal = ({ + isErrorModalVisible, + closeErrorModal, +}: ServerConnectionErrorModalProps) => { + return ( + closeErrorModal()} + > + Error de conexión, intente más tarde + + ); +}; diff --git a/mobile/src/common/components/rating_page/full_page_rating.tsx b/mobile/src/common/components/rating_page/full_page_rating.tsx index eab089b6f011591b7675a921b065ef6bd183f52b..515607f73b3bf38f713f96758793e7b9249f009b 100644 --- a/mobile/src/common/components/rating_page/full_page_rating.tsx +++ b/mobile/src/common/components/rating_page/full_page_rating.tsx @@ -40,11 +40,11 @@ export const FullPageRating = ({ onClose, onSubmitted }: FullPageRatingProps) => styles.ratingContainer, { width: opacityRef.interpolate({ - inputRange: [0, 0.7], // Valores de entrada, de 0 a 1 + inputRange: [0, 0.7], outputRange: ["0%", "70%"], }), opacity: opacityRef.interpolate({ - inputRange: [0.3, 0.7], // Valores de entrada, de 0 a 1 + inputRange: [0.3, 0.7], outputRange: [0, 1], }), }, diff --git a/mobile/src/common/const/api.ts b/mobile/src/common/const/api.ts index 24838169976313038bb4882ce32bdc88e509e832..c4d4fe8de65867d6440c49e432cba14106ae2b99 100644 --- a/mobile/src/common/const/api.ts +++ b/mobile/src/common/const/api.ts @@ -1 +1 @@ -export const API_URL = 'https://c749-200-92-164-74.ngrok-free.app'; \ No newline at end of file +export const API_URL = 'http://192.168.0.14:3005'; \ No newline at end of file diff --git a/mobile/src/common/contexts/data_context.tsx b/mobile/src/common/contexts/data_context.tsx index e82b1959cb5af23bcaa7b9f7fae2726d154835d4..213b6bf8316b2c8ba14728b39ed90c0d32aee6f4 100644 --- a/mobile/src/common/contexts/data_context.tsx +++ b/mobile/src/common/contexts/data_context.tsx @@ -2,7 +2,7 @@ import { createContext, PropsWithChildren, useContext } from "react"; import { StateRepositoryImpl } from "../../place/infrastructure/repositories/state_repository"; import { StateDataSourceDev } from "../../place/infrastructure/datasources/dev/state_datasource"; import { AuthRepository } from "../../auth/domain/repositories/auth_repository"; -import { AuthDataSourceDev } from '../../auth/infrastructure/dev/datasources/auth_datasource'; +import { AuthDataSourceDev } from "../../auth/infrastructure/dev/datasources/auth_datasource"; import { AuthRepositoryImpl } from "../../auth/infrastructure/prod/repositories/auth_repository"; import { RouteRepository } from "../../route/domain/repositories/route_repository"; import { RouteDataSourceDev } from "../../route/infrastructure/datasources/dev/route_datasource"; @@ -23,65 +23,67 @@ import { TravelRepository } from "../../travel/domain/repositories/travel_reposi import { TravelRepositoryImpl } from "../../travel/infrastructure/repositories/travel_repository"; import { AuthDatasourceProd } from "../../auth/infrastructure/prod/datasources/auth_datasource"; import { TravelDatasourceDev } from "../../travel/infrastructure/datasources/dev/travel_datasource"; +import { TravelDatasourceProd } from "../../travel/infrastructure/datasources/prod/travel_datasource_prod"; type DataContextType = { - statesRepository: StateRepository | null; - authRepository: AuthRepository | null; - activityRepository: ActivityRepository | null; - travelRepository: TravelRepository | null; - routeRepository: RouteRepository | null; - profileRepository: ProfileRepository | null; + statesRepository: StateRepository | null; + authRepository: AuthRepository | null; + activityRepository: ActivityRepository | null; + travelRepository: TravelRepository | null; + routeRepository: RouteRepository | null; + profileRepository: ProfileRepository | null; }; type DataContextProviderProps = PropsWithChildren<{}>; const DataContext = createContext({ - statesRepository: null, - authRepository: null, - activityRepository: null, - travelRepository: null, - routeRepository: null, - profileRepository: null + statesRepository: null, + authRepository: null, + activityRepository: null, + travelRepository: null, + routeRepository: null, + profileRepository: null, }); const getProductionContext = (language: string): DataContextType => { - return { - statesRepository: new StateRepositoryImpl(new StateDataSourceProd(language)), - authRepository: new AuthRepositoryImpl(new AuthDatasourceProd()), - activityRepository: new ActivityRepositoryDev(new ActivityDatasourceProd()), - travelRepository: new TravelRepositoryImpl(new TravelDatasourceDev()), - routeRepository: new RouteRepositoryImpl(new RouteDatasourceProd(language)), - profileRepository: new ProfileRepositoryImpl(new ProfileDataSourceProd(language)) - }; -} + return { + statesRepository: new StateRepositoryImpl( + new StateDataSourceProd(language) + ), + authRepository: new AuthRepositoryImpl(new AuthDatasourceProd()), + activityRepository: new ActivityRepositoryDev(new ActivityDatasourceProd()), + travelRepository: new TravelRepositoryImpl(new TravelDatasourceProd()), + routeRepository: new RouteRepositoryImpl(new RouteDatasourceProd(language)), + profileRepository: new ProfileRepositoryImpl( + new ProfileDataSourceProd(language) + ), + }; +}; const getDevelopmentContext = (): DataContextType => { - return { - statesRepository: new StateRepositoryImpl(new StateDataSourceDev()), - authRepository: new AuthRepositoryImpl(new AuthDataSourceDev()), - activityRepository: new ActivityRepositoryDev(new ActivityDatasourceDev()), - travelRepository: new TravelRepositoryImpl(new TravelDatasourceDev()), - routeRepository: new RouteRepositoryImpl(new RouteDataSourceDev()), - profileRepository: new ProfileRepositoryImpl(new ProfileDataSourceDev()) - }; -} + return { + statesRepository: new StateRepositoryImpl(new StateDataSourceDev()), + authRepository: new AuthRepositoryImpl(new AuthDataSourceDev()), + activityRepository: new ActivityRepositoryDev(new ActivityDatasourceDev()), + travelRepository: new TravelRepositoryImpl(new TravelDatasourceDev()), + routeRepository: new RouteRepositoryImpl(new RouteDataSourceDev()), + profileRepository: new ProfileRepositoryImpl(new ProfileDataSourceDev()), + }; +}; export const DataContextProvider = ({ children }: DataContextProviderProps) => { - const { i18n:{ language } } = useTranslation(); - - const value = getProductionContext(language); + const { + i18n: { language }, + } = useTranslation(); - return ( - - {children} - - ); + const value = getProductionContext(language); + return {children}; }; export const useDataContext = () => { - const context = useContext(DataContext); - if (!context) { - throw new Error("useDataContext must be used within a DataContextProvider"); - } - return context; -} \ No newline at end of file + const context = useContext(DataContext); + if (!context) { + throw new Error("useDataContext must be used within a DataContextProvider"); + } + return context; +}; diff --git a/mobile/src/common/hooks/useDownloadFile.ts b/mobile/src/common/hooks/useDownloadFile.ts new file mode 100644 index 0000000000000000000000000000000000000000..5509553aaa82d6ac6d151b2f09b5715bc2dd29f2 --- /dev/null +++ b/mobile/src/common/hooks/useDownloadFile.ts @@ -0,0 +1,27 @@ +import * as FileSystem from 'expo-file-system'; +import { useGalery } from './useGalery'; +import { useState } from 'react'; +import { ApiRequestStatus } from '../const/api_request_states'; +import { set } from 'react-hook-form'; +import * as ExpoCrypto from 'expo-crypto'; + +const TRAVEL_ALBUM = "Magic Towns Travel"; + +export const useDownloadFile = () => { + const { saveToGalery } = useGalery(); + const [requestStatus, setRequestStatus] = useState(ApiRequestStatus.IDLE); + const setLoading = async () => setRequestStatus(ApiRequestStatus.LOADING); + const handleDownload = async (url: string) => { + try { + await setLoading(); + const { uri } = await FileSystem.downloadAsync(url, FileSystem.documentDirectory + `${ExpoCrypto.randomUUID()}.jpg`); + await saveToGalery(uri, TRAVEL_ALBUM); + setRequestStatus(ApiRequestStatus.SUCCESS); + } catch (error) { + console.log("Error downloading file: " + error); + setRequestStatus(ApiRequestStatus.ERROR); + } + } + + return { handleDownload, saveFileStatus: requestStatus }; +} \ No newline at end of file diff --git a/mobile/src/common/hooks/useGalery.ts b/mobile/src/common/hooks/useGalery.ts new file mode 100644 index 0000000000000000000000000000000000000000..760d8d0658a3215c58bac35d98c34b7e30929adb --- /dev/null +++ b/mobile/src/common/hooks/useGalery.ts @@ -0,0 +1,27 @@ +import * as MediaLibrary from 'expo-media-library'; + +export const useGalery = () => { + const [permissionResponse, requestPermission] = MediaLibrary.usePermissions(); + + const checkPermission = async () => { + if (permissionResponse?.status !== 'granted') { + await requestPermission(); + } + } + + const saveToGalery = async (uri: string, album: string) => { + try { + await checkPermission(); + if (permissionResponse?.status !== 'granted') { + return; + } + const asset = await MediaLibrary.createAssetAsync(uri); + const result = await MediaLibrary.createAlbumAsync(album, asset, false); + console.log("Saved to galery: " + result); + } catch (error) { + console.log("Error saving to galery: " + error); + } + } + + return { saveToGalery }; +} \ No newline at end of file diff --git a/mobile/src/common/hooks/useGet.ts b/mobile/src/common/hooks/useGet.ts index 9657b2b8c576e0036d581973316b8363509662f9..c6fffae5bf5359be11a2904fc78f239764b0b8bb 100644 --- a/mobile/src/common/hooks/useGet.ts +++ b/mobile/src/common/hooks/useGet.ts @@ -11,17 +11,20 @@ export const useGet = (callback: () => Promise) => { const fetchData = async () => { try { await setLoading(); + console.log("Fetching data..."); const response = await callback(); + console.log("Data fetched: "+response); setData(response); + console.log("Data set: "+data); setRequestStatus(ApiRequestStatus.SUCCESS); + console.log("Request status set: "+requestStatus); } catch (error) { - console.error(error); + console.log("Error fetching data: "+error); setRequestStatus(ApiRequestStatus.ERROR); } }; - useEffect(() => { - + useEffect(() => { fetchData(); }, []); diff --git a/mobile/src/place/screens/town_activities_page.tsx b/mobile/src/place/screens/town_activities_page.tsx index fc44035a5fdbbfa7b5101616b1ee7f10b182fbe8..bc57b7413419bbbf66105f40371d1e2f75e8373d 100644 --- a/mobile/src/place/screens/town_activities_page.tsx +++ b/mobile/src/place/screens/town_activities_page.tsx @@ -21,7 +21,10 @@ interface TownActivitiesPageProps { stateId: number; } -export const TownActivitiesPage = ({ townId, stateId }: TownActivitiesPageProps) => { +export const TownActivitiesPage = ({ + townId, + stateId, +}: TownActivitiesPageProps) => { const { activities, requestStatus } = useGetActivities(townId); if (requestStatus === ApiRequestStatus.LOADING) { @@ -36,25 +39,30 @@ export const TownActivitiesPage = ({ townId, stateId }: TownActivitiesPageProps) return No activities found; } - const handleViewActivity = (activityId: number) =>{ + const handleViewActivity = (activityId: number) => { router.push(`/state/${stateId}/town/${townId}/activity/${activityId}/`); - } + }; const handleGenerateRoute = () => { - router.push(`/routes/generate_route?townId=${townId}`); - } + router.push(`/routes/generate_route_form?townId=${townId}`); + }; return ( } + renderItem={({ item }) => ( + + )} ItemSeparatorComponent={() => } ListFooterComponent={() => } keyExtractor={(item) => item.id.toString()} /> - + ); }; diff --git a/mobile/src/route/components/generate_route_form.tsx b/mobile/src/route/components/generate_route_form.tsx new file mode 100644 index 0000000000000000000000000000000000000000..fa1e7f2489102e9461ddb25f6d755a1cfa331c5f --- /dev/null +++ b/mobile/src/route/components/generate_route_form.tsx @@ -0,0 +1,67 @@ +import { Controller, Control } from "react-hook-form"; +import { View, StyleSheet } from "react-native"; +import { CustomTextInput } from "../../common/components/form/text_input"; +import { GenerateRouteFormValues } from "./../hooks/useGenerateRoute"; +import { DateTextInput } from "../../common/components/form/date_text_input"; + +interface GenerateRouteFormProps { + control: Control; +} + +export const GenerateRouteForm = ({ control }: GenerateRouteFormProps) => { + return ( + + ( + onChange(text)} + errors={errors.start?.message} + mode="time" + /> + )} + rules={{ + required: "Start time is required", + }} + /> + ( + + )} + rules={{ + required: "End time is required", + }} + /> + + ); +}; + +const styles = StyleSheet.create({ + mainContainer: { + width: "100%", + alignItems: "center", + padding: 20, + gap: 20, + }, +}); diff --git a/mobile/src/route/domain/datasource/route_datasource.ts b/mobile/src/route/domain/datasource/route_datasource.ts index e897c74018387eea8c85dbf736e2471b539cc73c..888fd8cc25a05ce495bba01a6284d6a38ed2dedc 100644 --- a/mobile/src/route/domain/datasource/route_datasource.ts +++ b/mobile/src/route/domain/datasource/route_datasource.ts @@ -1,7 +1,11 @@ import { ActivityRouteEntity } from "../../../activity/domain/entities/activity_info_entity"; - export interface RouteDataSource { - generateRoute: (townId: number) => Promise; - getRoute: (routeId: number) => Promise; -} \ No newline at end of file + generateRoute: ( + townId: number, + startTime: number, + endTime: number + ) => Promise; + getRoute: (routeId: number) => Promise; + saveRoute: (id: number) => Promise; +} diff --git a/mobile/src/route/domain/repositories/route_repository.ts b/mobile/src/route/domain/repositories/route_repository.ts index b655d59be369803b48f781b8548f6b0d5ca7bb73..8264334ebff3d92fe594050d9d0b611f71245a93 100644 --- a/mobile/src/route/domain/repositories/route_repository.ts +++ b/mobile/src/route/domain/repositories/route_repository.ts @@ -1,6 +1,11 @@ import { ActivityRouteEntity } from "../../../activity/domain/entities/activity_info_entity"; export interface RouteRepository { - generateRoute: (townId: number) => Promise; - getRoute: (routeId: number) => Promise; -} \ No newline at end of file + generateRoute: ( + townId: number, + startTime: number, + endTime: number + ) => Promise; + getRoute: (routeId: number) => Promise; + saveRoute: (id: number) => Promise; +} diff --git a/mobile/src/route/hooks/useGenerateRoute.ts b/mobile/src/route/hooks/useGenerateRoute.ts deleted file mode 100644 index a685b5127dec6f8d8e6d63a981bcca87eac9b967..0000000000000000000000000000000000000000 --- a/mobile/src/route/hooks/useGenerateRoute.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ActivityRouteEntity } from "../../activity/domain/entities/activity_info_entity"; -import { useDataContext } from "../../common/contexts/data_context"; -import { useGet } from "../../common/hooks/useGet"; - -export const useGenerateRoute = (townId: number) => { - const { routeRepository } = useDataContext(); - const callback = () => { - return routeRepository!.generateRoute(townId); - } - const { data: routeActivities, requestStatus } = useGet(callback); - - return { routeActivities, requestStatus }; -}; \ No newline at end of file diff --git a/mobile/src/route/hooks/useGenerateRoute.tsx b/mobile/src/route/hooks/useGenerateRoute.tsx new file mode 100644 index 0000000000000000000000000000000000000000..5a049a7d34c764c793e8007f48a1d1d99b58e520 --- /dev/null +++ b/mobile/src/route/hooks/useGenerateRoute.tsx @@ -0,0 +1,41 @@ +import { useForm } from "react-hook-form"; +import { ActivityRouteEntity } from "../../activity/domain/entities/activity_info_entity"; +import { useDataContext } from "../../common/contexts/data_context"; +import { useGet } from "../../common/hooks/useGet"; +import { GenerateRouteForm } from "./../components/generate_route_form"; + +export interface GenerateRouteFormValues { + start: string; + end: string; +} + +export const useGenerateRoute = (townId: number) => { + const { routeRepository } = useDataContext(); + const { control, handleSubmit } = useForm({ + defaultValues: { + start: "", + end: "", + }, + }); + + const onValidSubmit = async (data: GenerateRouteFormValues) => { + const { start, end } = data; + const [startHours] = start.split(":"); + const [endHours] = end.split(":"); + try { + console.log(startHours, endHours); + const route = await routeRepository!.generateRoute(townId, +start, +end); + console.log(route); + } catch (error) { + console.error(error); + } + }; + + const onSubmit = async () => { + console.log("submit"); + await handleSubmit(onValidSubmit, (errors) => { + console.log(errors); + })(); + }; + return { control, onSubmit }; +}; diff --git a/mobile/src/route/infrastructure/datasources/dev/route_datasource.ts b/mobile/src/route/infrastructure/datasources/dev/route_datasource.ts index 13d0bb052df9d1088b8c6f243d54f669fab715ab..eb80e2529abfd4f7ba437d6a90ae8057706c248f 100644 --- a/mobile/src/route/infrastructure/datasources/dev/route_datasource.ts +++ b/mobile/src/route/infrastructure/datasources/dev/route_datasource.ts @@ -1,63 +1,81 @@ -import { ActivityInfoEntity, ActivityRouteEntity } from "../../../../activity/domain/entities/activity_info_entity"; +import { + ActivityInfoEntity, + ActivityRouteEntity, +} from "../../../../activity/domain/entities/activity_info_entity"; import { RouteDataSource } from "../../../domain/datasource/route_datasource"; export class RouteDataSourceDev implements RouteDataSource { - async generateRoute(townId: number): Promise { - return new Promise((resolve) => { - const filteredActivities = activities.filter(activity => activity.townId === townId); - const startDate = new Date(); - const endDate = new Date(); - return resolve(filteredActivities.map(activity => ({ - ...activity, - coordinates: { - latitude: 37.78825, - longitude: -122.4324 - }, - startTime: startDate, - endTime: endDate, - done: false - }))); - }); - } - async getRoute(routeId: number): Promise { - return new Promise((resolve) => { - - const startDate = new Date(); - const endDate = new Date(); - return resolve(activities.map(activity => ({ - ...activity, - coordinates: { - latitude: 22.76843 + Math.random() * 0.1, - longitude: -102.58141 + Math.random() * 0.1 - }, - startTime: startDate, - endTime: endDate, - done: false - }))); - }); - } -}; - + async saveRoute(id: number): Promise { + return new Promise((resolve) => { + return resolve(); + }); + } + async generateRoute( + townId: number, + startTime: number, + endTime: number + ): Promise { + return new Promise((resolve) => { + const filteredActivities = activities.filter( + (activity) => activity.townId === townId + ); + const startDate = new Date(); + const endDate = new Date(); + return resolve( + filteredActivities.map((activity) => ({ + ...activity, + coordinates: { + latitude: 37.78825, + longitude: -122.4324, + }, + startTime: startDate, + endTime: endDate, + done: false, + })) + ); + }); + } + async getRoute(routeId: number): Promise { + return new Promise((resolve) => { + const startDate = new Date(); + const endDate = new Date(); + return resolve( + activities.map((activity) => ({ + ...activity, + coordinates: { + latitude: 22.76843 + Math.random() * 0.1, + longitude: -102.58141 + Math.random() * 0.1, + }, + startTime: startDate, + endTime: endDate, + done: false, + })) + ); + }); + } +} const activities: ActivityInfoEntity[] = [ - { - id: 1, - name: 'Santuario de Nuestra Señora de la Soledad', - description: 'Santuario de Nuestra Señora de la Soledad en Jerez', - townId: 1, - available: 'Todo el año', - location: 'Jerez, Zacatecas', - imageUri: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSAOt4JS0AzrMsxDp0caz26vuanMq692g17nbI6-_nycw&s', - tags: ['Religioso', 'Turismo'] - }, - { - id: 2, - name: 'Feria de la tostada', - description: 'Feria de la tostada en Jerez', - townId: 1, - available: 'Septiembre', - location: 'Jerez, Zacatecas', - imageUri: 'https://www.liderempresarial.com/wp-content/uploads/2022/08/WhatsApp-Image-2022-08-03-at-4.14.35-PM-856x570.jpeg', - tags: ['Feria', 'Tostada'] - } -] \ No newline at end of file + { + id: 1, + name: "Santuario de Nuestra Señora de la Soledad", + description: "Santuario de Nuestra Señora de la Soledad en Jerez", + townId: 1, + available: "Todo el año", + location: "Jerez, Zacatecas", + imageUri: + "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSAOt4JS0AzrMsxDp0caz26vuanMq692g17nbI6-_nycw&s", + tags: ["Religioso", "Turismo"], + }, + { + id: 2, + name: "Feria de la tostada", + description: "Feria de la tostada en Jerez", + townId: 1, + available: "Septiembre", + location: "Jerez, Zacatecas", + imageUri: + "https://www.liderempresarial.com/wp-content/uploads/2022/08/WhatsApp-Image-2022-08-03-at-4.14.35-PM-856x570.jpeg", + tags: ["Feria", "Tostada"], + }, +]; diff --git a/mobile/src/route/infrastructure/datasources/prod/route_datasource.ts b/mobile/src/route/infrastructure/datasources/prod/route_datasource.ts index 076e41a132f21feb904bce0588a68253c1b5d97c..8c9d231bffe03a51c90c7c2a8796bcfcb30cc0ea 100644 --- a/mobile/src/route/infrastructure/datasources/prod/route_datasource.ts +++ b/mobile/src/route/infrastructure/datasources/prod/route_datasource.ts @@ -6,22 +6,44 @@ import { RouteDataSource } from "../../../domain/datasource/route_datasource"; import { ActivityRouteModel } from "../../model/route_activity_model"; export class RouteDatasourceProd implements RouteDataSource { - private readonly language: string; - constructor(language: string) { - this.language = language; + private readonly language: string; + constructor(language: string) { + this.language = language; + } + async saveRoute(id: number): Promise { + const { status } = await axios.patch(`${API_URL}/route/${id}`, { + status: "accepted", + }); + if (status !== 200) { + throw new Error("Error saving route"); } - async generateRoute(townId: number): Promise{ - const {data, status} = await axios.get(`${API_URL}/route/recommend/${townId}?lang=${this.language}`); - if (status !== 200) { - throw new Error('Error fetching route'); - } - - return data.map(activityRouteModelToActivityRouteEntity); - }; - async getRoute(routeId: number): Promise { - throw new Error('Method not implemented.'); + } + async generateRoute( + townId: number, + startTime: number, + endTime: number + ): Promise { + const { data, status } = await axios.post( + `${API_URL}/route/${townId}?lang=${this.language}`, + { + start: startTime, + end: endTime, + } + ); + console.log(data); + if (status !== 201) { + throw new Error("Error fetching route"); } -} + return []; + } + async getRoute(routeId: number): Promise { + const { status, data } = await axios.get(`${API_URL}/route/${routeId}`); + if (status !== 200) { + throw new Error("Error fetching route"); + } + return []; + } +} -// /state/state_id/town/town_id/activity/activity_id/travel?id=id_point_of_interest \ No newline at end of file +// /state/state_id/town/town_id/activity/activity_id/travel?id=id_point_of_interest diff --git a/mobile/src/route/infrastructure/repositories/route_repository.ts b/mobile/src/route/infrastructure/repositories/route_repository.ts index dea92db56bafd4df232afb51d3a5bfd73edce1cc..a8cff848a3ac06369db0ace7b64d026438376d6b 100644 --- a/mobile/src/route/infrastructure/repositories/route_repository.ts +++ b/mobile/src/route/infrastructure/repositories/route_repository.ts @@ -2,11 +2,14 @@ import { RouteDataSource } from "../../domain/datasource/route_datasource"; import { RouteRepository } from "../../domain/repositories/route_repository"; export class RouteRepositoryImpl implements RouteRepository { - constructor(private routeDataSource: RouteDataSource) {} - async generateRoute(townId: number) { - return this.routeDataSource.generateRoute(townId); - } - async getRoute(routeId: number) { - return this.routeDataSource.getRoute(routeId); - } -} \ No newline at end of file + constructor(private routeDataSource: RouteDataSource) {} + async saveRoute(id: number): Promise { + return this.routeDataSource.saveRoute(id); + } + async generateRoute(townId: number, startTime: number, endTime: number) { + return this.routeDataSource.generateRoute(townId, startTime, endTime); + } + async getRoute(routeId: number) { + return this.routeDataSource.getRoute(routeId); + } +} diff --git a/mobile/src/route/screens/generate_route_screen.tsx b/mobile/src/route/screens/generate_route_screen.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9a9cf59e60cb5d3a6b52223fcedac3002660ddb6 --- /dev/null +++ b/mobile/src/route/screens/generate_route_screen.tsx @@ -0,0 +1,46 @@ +import { GenerateRouteForm } from "../components/generate_route_form"; +import { FullPageLoader } from "../../common/components/full_page_loader"; +import { ApiRequestStatus } from "../../common/const/api_request_states"; +import { useGenerateRoute } from "../hooks/useGenerateRoute"; +import { use } from "i18next"; +import { TouchableOpacity, View, Text, StyleSheet } from "react-native"; +import { LIGHT_THEME } from "../../common/const/theme"; + +interface GenerateRoutePageProps { + townId: number; +} + +export const GenerateRoutePage = ({ townId }: GenerateRoutePageProps) => { + const { control, onSubmit } = useGenerateRoute(townId); + + return ( + + + + Submit + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + gap: 30, + }, + button: { + justifyContent: "center", + alignItems: "center", + backgroundColor: LIGHT_THEME.color.primary, + borderRadius: 25, + bottom: 10, + width: 200, + height: 50, + elevation: 5, + alignSelf: "center", + }, + text: { + color: LIGHT_THEME.color.white, + fontWeight: "bold", + }, +}); diff --git a/mobile/src/travel/components/share_button.tsx b/mobile/src/travel/components/share_button.tsx index a926510cb20f3765f3a58ad01e15107039fba156..a9af7bb098b2e3cb03c6fa3f65e9b2f3c7bfb45c 100644 --- a/mobile/src/travel/components/share_button.tsx +++ b/mobile/src/travel/components/share_button.tsx @@ -1,13 +1,14 @@ import { TouchableOpacity } from "react-native-gesture-handler"; import { Feather } from '@expo/vector-icons'; +import { router, useLocalSearchParams } from "expo-router"; -interface ShareButtonProps { - onPress: () => void; -} - -export const ShareButton = ({ onPress } : ShareButtonProps) => { +export const ShareButton = () => { + const { id } = useLocalSearchParams<{ id: string }>(); + if (!id) { + return null; + } return ( - + router.push(`/travel_history/download/${id}`)}> ); diff --git a/mobile/src/travel/domain/datasources/travel_datasource.ts b/mobile/src/travel/domain/datasources/travel_datasource.ts index 4d46e6cda4bf7bc8cbf0cb39eb89046cd61dd4f7..1026ce82e2bf5aa7b82126757a5708003c4c7f2f 100644 --- a/mobile/src/travel/domain/datasources/travel_datasource.ts +++ b/mobile/src/travel/domain/datasources/travel_datasource.ts @@ -4,4 +4,5 @@ import { TravelHistory } from "../entities/travel_history"; export interface TravelDataSource { getTravelHistory(): Promise; getTravelDetails(id: number): Promise; + getVisitImage(id: number): Promise; } \ No newline at end of file diff --git a/mobile/src/travel/domain/repositories/travel_repository.ts b/mobile/src/travel/domain/repositories/travel_repository.ts index ea4a79268bedd3e9c7b4f50e0182e12f9ee5fc9e..e6df5972f8a7752e3e7c246af0731ddf7eb09b38 100644 --- a/mobile/src/travel/domain/repositories/travel_repository.ts +++ b/mobile/src/travel/domain/repositories/travel_repository.ts @@ -4,4 +4,5 @@ import { TravelHistory } from "../entities/travel_history"; export interface TravelRepository { getTravelHistory(): Promise; getTravelDetails(id: number): Promise; + getVisitImage(id: number): Promise; } \ No newline at end of file diff --git a/mobile/src/travel/hooks/useGetTravelImage.ts b/mobile/src/travel/hooks/useGetTravelImage.ts new file mode 100644 index 0000000000000000000000000000000000000000..54c3140b161629794404191d6cac7c3eaf0cd3a5 --- /dev/null +++ b/mobile/src/travel/hooks/useGetTravelImage.ts @@ -0,0 +1,39 @@ +import { useEffect, useState } from "react"; +import { useDataContext } from "../../common/contexts/data_context" +import { useGet } from "../../common/hooks/useGet"; +import { useDownloadFile } from "../../common/hooks/useDownloadFile"; +import { ApiRequestStatus } from "../../common/const/api_request_states"; +import { router } from "expo-router"; + +interface UseGetTravelImageProps { + id: number +} + +export const useGetTravelImage = ({ id }: UseGetTravelImageProps) => { + const { travelRepository } = useDataContext(); + const { handleDownload, saveFileStatus } = useDownloadFile(); + + const callback = async () => { + return await travelRepository!.getVisitImage(id); + } + const { requestStatus: getImageStatus, data } = useGet(callback); + const [requestStatus, setRequestStatus] = useState(ApiRequestStatus.IDLE); + + useEffect(() => { + const requests = [getImageStatus, saveFileStatus]; + console.log("Requests: "+requests); + if (requests.some(status => status === ApiRequestStatus.LOADING)) { + setRequestStatus(ApiRequestStatus.LOADING); + } else if (requests.some(status => status === ApiRequestStatus.ERROR)) { + console.log("Error saving image"); + router.push("travel_history/error/save_travel_image_error"); + } else if (requests.every(status => status === ApiRequestStatus.SUCCESS)) { + //TODO: Show success message + router.dismissAll(); + } else if (requests.some(status => status === ApiRequestStatus.IDLE || status === ApiRequestStatus.SUCCESS)) { + setRequestStatus(ApiRequestStatus.SUCCESS); + } + }, [getImageStatus, saveFileStatus]); + + return { requestStatus, data, handleDownload }; +} \ No newline at end of file diff --git a/mobile/src/travel/infrastructure/datasources/dev/travel_datasource.ts b/mobile/src/travel/infrastructure/datasources/dev/travel_datasource.ts index fefe9f4686a6fda01fa44c81d329929a4564b848..7cc8782102165e594fd6b58fc3c3dbe7b7cbe26e 100644 --- a/mobile/src/travel/infrastructure/datasources/dev/travel_datasource.ts +++ b/mobile/src/travel/infrastructure/datasources/dev/travel_datasource.ts @@ -3,6 +3,9 @@ import { TravelDetails } from "../../../domain/entities/travel_details"; import { TravelHistory } from "../../../domain/entities/travel_history"; export class TravelDatasourceDev implements TravelDataSource { + getVisitImage(id: number): Promise { + throw new Error("Method not implemented."); + } getTravelDetails(id: number): Promise { return Promise.resolve(travelDetails); } diff --git a/mobile/src/travel/infrastructure/datasources/prod/travel_datasource_prod.ts b/mobile/src/travel/infrastructure/datasources/prod/travel_datasource_prod.ts new file mode 100644 index 0000000000000000000000000000000000000000..4e206657c5415e2f3d45a54626abbd6d8cf55ba9 --- /dev/null +++ b/mobile/src/travel/infrastructure/datasources/prod/travel_datasource_prod.ts @@ -0,0 +1,26 @@ +import axios from "axios"; +import { TravelDataSource } from "../../../domain/datasources/travel_datasource"; +import { TravelDetails } from "../../../domain/entities/travel_details"; +import { TravelHistory } from "../../../domain/entities/travel_history"; +import { API_URL } from "../../../../common/const/api"; +import { TravelDatasourceDev } from "../dev/travel_datasource"; + +export class TravelDatasourceProd implements TravelDataSource { + async getTravelHistory(): Promise { + const dev = new TravelDatasourceDev(); + return await dev.getTravelHistory(); + } + async getTravelDetails(id: number): Promise { + const dev = new TravelDatasourceDev(); + return await dev.getTravelDetails(id); + } + async getVisitImage(id: number): Promise { + const { data, status } = await axios.get(`${API_URL}/visited/getImage/${id}`); + if (status !== 200) { + console.log("Error fetching image"); + throw new Error("Error generating ") + } + return data; + } + +} \ No newline at end of file diff --git a/mobile/src/travel/infrastructure/repositories/travel_repository.ts b/mobile/src/travel/infrastructure/repositories/travel_repository.ts index 4a6fd8a9ec18ce38872d88ccde083ab7f6717e98..345cdbccffa2182da95c13d670352b50d323a09d 100644 --- a/mobile/src/travel/infrastructure/repositories/travel_repository.ts +++ b/mobile/src/travel/infrastructure/repositories/travel_repository.ts @@ -9,11 +9,14 @@ export class TravelRepositoryImpl implements TravelRepository { constructor(travelDataSource: TravelDataSource) { this.travelDataSource = travelDataSource; } - getTravelDetails(id: number): Promise { + async getVisitImage(id: number): Promise { + return this.travelDataSource.getVisitImage(id); + } + async getTravelDetails(id: number): Promise { return this.travelDataSource.getTravelDetails(id); } - getTravelHistory(): Promise { + async getTravelHistory(): Promise { return this.travelDataSource.getTravelHistory(); } } \ No newline at end of file diff --git a/mobile/src/travel/screens/download_image_error.tsx b/mobile/src/travel/screens/download_image_error.tsx new file mode 100644 index 0000000000000000000000000000000000000000..1f26384c2d1531e00054ccbd5268d8107c22d6c2 --- /dev/null +++ b/mobile/src/travel/screens/download_image_error.tsx @@ -0,0 +1,20 @@ +import { View, Text, StyleSheet } from "react-native"; +import { FloatingEndActionButton } from "../../common/components/floating_end_action_button"; +import { router } from "expo-router"; + +export const DownloadImageError = () => { + return ( + + Error downloading image + router.dismissAll()}/> + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: "center", + alignItems: "center", + }, +}); \ No newline at end of file diff --git a/mobile/src/travel/screens/download_travel.tsx b/mobile/src/travel/screens/download_travel.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6997cfb247240f5baa75a19fee987f45326b7615 --- /dev/null +++ b/mobile/src/travel/screens/download_travel.tsx @@ -0,0 +1,31 @@ +import { View, Text, Image } from "react-native"; +import { useGetTravelImage } from "../hooks/useGetTravelImage"; +import { ApiRequestStatus } from "../../common/const/api_request_states"; +import { FullPageLoader } from "../../common/components/full_page_loader"; +import { useEffect } from "react"; +import { ScrollView } from "react-native-gesture-handler"; +import { FloatingEndActionButton } from "../../common/components/floating_end_action_button"; +import { useDownloadFile } from "../../common/hooks/useDownloadFile"; +import { router } from "expo-router"; + +interface DownloadTravelProps { + id: number; +} + +export const DownloadTravel = ({ id }: DownloadTravelProps) => { + const { data, requestStatus, handleDownload } = useGetTravelImage({id}); + + + if (requestStatus === ApiRequestStatus.LOADING) { + return ( + + ); + } + return ( + + Download the image + + handleDownload(data!)}/> + + ); +}; \ No newline at end of file