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