diff --git a/backend/src/email/email.service.ts b/backend/src/email/email.service.ts index 9a975c8551838597d623f8fc69d4f11cedbc47a6..621bed1f0fc16cf201dd702e3536114fe5896cbd 100644 --- a/backend/src/email/email.service.ts +++ b/backend/src/email/email.service.ts @@ -13,7 +13,7 @@ export class EmailService { const mailOptions = { to: email, subject: 'Reset your password', - text: `Your reset code is ${resetCode}`, + html: `

Your reset code is ${resetCode}

`, }; try { await this.mailerService.sendMail(mailOptions); diff --git a/mobile/app/_layout.tsx b/mobile/app/_layout.tsx index e31d84675e2a07aae26b574270f838b14b6f7018..17d423f8fdd6294b278c22b79c325146fb8971c4 100644 --- a/mobile/app/_layout.tsx +++ b/mobile/app/_layout.tsx @@ -102,6 +102,27 @@ const MainLayout = () => { statusBarColor: LIGHT_THEME.color.primary, }} /> + + ); }; diff --git a/mobile/app/auth/reset_password.tsx b/mobile/app/auth/reset_password.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9a95863d49f6858bcec8d6bec09b7ed163e84802 --- /dev/null +++ b/mobile/app/auth/reset_password.tsx @@ -0,0 +1,7 @@ +import { ResetPasswordPage } from "../../src/auth/pages/reset_password_page"; + +export default function ResetPassword() { + return ( + + ); +} \ No newline at end of file diff --git a/mobile/app/auth/reset_password_error.tsx b/mobile/app/auth/reset_password_error.tsx new file mode 100644 index 0000000000000000000000000000000000000000..3eda6ffe9b397ff60698f5cb784a6bf5a9ea7647 --- /dev/null +++ b/mobile/app/auth/reset_password_error.tsx @@ -0,0 +1,5 @@ +export default function ResetPasswordError() { + return ( + + ); +}; \ No newline at end of file diff --git a/mobile/app/state/[stateId]/town/[townId]/activity/[activityId]/index.tsx b/mobile/app/state/[stateId]/town/[townId]/activity/[activityId]/index.tsx index 40d153ab1766e47fb06125fe80d4fcb873b00fcc..aeee210c482d56d32f928773404875fa60cf7f6d 100644 --- a/mobile/app/state/[stateId]/town/[townId]/activity/[activityId]/index.tsx +++ b/mobile/app/state/[stateId]/town/[townId]/activity/[activityId]/index.tsx @@ -1,5 +1,5 @@ import { useLocalSearchParams } from "expo-router"; -import { ActivityDescriptionPage } from "../../../../../../../src/screens/activity_description/activity_description_page"; +import { ActivityDescriptionPage } from "../../../../../../../src/activity/screens/activity_description_page"; export default function ActivitySelectionScreen() { const { activityId, stateId, townId } = useLocalSearchParams<{activityId: string, stateId: string, townId: string}>(); diff --git a/mobile/app/state/[stateId]/town/[townId]/activity/[activityId]/travel.tsx b/mobile/app/state/[stateId]/town/[townId]/activity/[activityId]/travel.tsx index e75e46755d133a5e9777374443566c2cbf536b5f..3d1967cac93c574c340740e8231a7876de8952b0 100644 --- a/mobile/app/state/[stateId]/town/[townId]/activity/[activityId]/travel.tsx +++ b/mobile/app/state/[stateId]/town/[townId]/activity/[activityId]/travel.tsx @@ -1,6 +1,6 @@ import { useLocalSearchParams } from "expo-router"; import { View, Text } from "react-native"; -import { ActivityPointScreen } from "../../../../../../../src/screens/activity_point/activity_point"; +import { ActivityPointScreen } from "../../../../../../../src/activity/screens/activity_point"; export default function Travel() { diff --git a/mobile/src/components/activity_bottom_sheet/activity_bottom_sheet.tsx b/mobile/src/activity/components/activity_bottom_sheet.tsx similarity index 97% rename from mobile/src/components/activity_bottom_sheet/activity_bottom_sheet.tsx rename to mobile/src/activity/components/activity_bottom_sheet.tsx index ae97e81a2265e0dabfe3eab13f1ce7a1fe0172ef..4109f7e469867b178bbf64acfbde4c5c805fead9 100644 --- a/mobile/src/components/activity_bottom_sheet/activity_bottom_sheet.tsx +++ b/mobile/src/activity/components/activity_bottom_sheet.tsx @@ -11,9 +11,9 @@ import { View, TouchableOpacity, } from "react-native"; -import { ActivityInfoEntity } from "../../domain/entities/activity_info_entity"; import { LIGHT_THEME } from "../../common/constants/theme"; import { router } from "expo-router"; +import { ActivityInfoEntity } from "../domain/entities/activity_info_entity"; interface ActivityBottomSheetProps { startSnapPoint: number; diff --git a/mobile/src/components/activity_tile/activity_tile.tsx b/mobile/src/activity/components/activity_tile.tsx similarity index 95% rename from mobile/src/components/activity_tile/activity_tile.tsx rename to mobile/src/activity/components/activity_tile.tsx index 73471a4d04cc6fcf419c3eaad585c9b1454449cf..3676df248aca71154aeff4b4c2c0e6f9ff149858 100644 --- a/mobile/src/components/activity_tile/activity_tile.tsx +++ b/mobile/src/activity/components/activity_tile.tsx @@ -1,7 +1,7 @@ import { View, Text, StyleSheet, Image, TouchableOpacity } from "react-native"; -import { ActivityInfoEntity } from "../../domain/entities/activity_info_entity"; import { LIGHT_THEME } from "../../common/constants/theme"; import { ScrollView } from "react-native-gesture-handler"; +import { ActivityInfoEntity } from "../domain/entities/activity_info_entity"; interface ActivityTileProps { activity: ActivityInfoEntity; diff --git a/mobile/src/domain/datasources/activity_datasource.ts b/mobile/src/activity/domain/datasources/activity_datasource.ts similarity index 78% rename from mobile/src/domain/datasources/activity_datasource.ts rename to mobile/src/activity/domain/datasources/activity_datasource.ts index 9ae21e7a369defd0760c1a1d3187af5bfe8177fa..ea2fe13974d9bb60d588479f0d3b25247f0282f0 100644 --- a/mobile/src/domain/datasources/activity_datasource.ts +++ b/mobile/src/activity/domain/datasources/activity_datasource.ts @@ -2,4 +2,5 @@ import { ActivityPlaceEntity } from "../entities/activity_place_entity"; export interface ActivityDataSource { getPlaceActivity(activityId: number, townId: number, stateId: number, placeNumber: number): Promise; + rankActivity(activityId: number, rank: number): Promise; } \ No newline at end of file diff --git a/mobile/src/domain/entities/activity_info_entity.ts b/mobile/src/activity/domain/entities/activity_info_entity.ts similarity index 84% rename from mobile/src/domain/entities/activity_info_entity.ts rename to mobile/src/activity/domain/entities/activity_info_entity.ts index 2631049a002b8438d693693407e4c045f83cd49b..84457d39c4142df1a772312adf981c0c0c0be822 100644 --- a/mobile/src/domain/entities/activity_info_entity.ts +++ b/mobile/src/activity/domain/entities/activity_info_entity.ts @@ -1,4 +1,5 @@ -import { PlaceInfoEntity } from "./place_info_entity"; +import { PlaceInfoEntity } from "../../../domain/entities/place_info_entity"; + export interface ActivityInfoEntity extends PlaceInfoEntity { available: string; diff --git a/mobile/src/domain/entities/activity_place_entity.ts b/mobile/src/activity/domain/entities/activity_place_entity.ts similarity index 100% rename from mobile/src/domain/entities/activity_place_entity.ts rename to mobile/src/activity/domain/entities/activity_place_entity.ts diff --git a/mobile/src/domain/repositories/activity_repository.ts b/mobile/src/activity/domain/repositories/activity_repository.ts similarity index 78% rename from mobile/src/domain/repositories/activity_repository.ts rename to mobile/src/activity/domain/repositories/activity_repository.ts index 45706508e0122cf58755941178cdc9c7c05b0f1c..4aec1db99dab694805c74251432bb101e5aa885b 100644 --- a/mobile/src/domain/repositories/activity_repository.ts +++ b/mobile/src/activity/domain/repositories/activity_repository.ts @@ -2,4 +2,5 @@ import { ActivityPlaceEntity } from "../entities/activity_place_entity"; export interface ActivityRepository { getPlaceActivity(activityId: number, townId: number, stateId: number, placeNumber: number): Promise; + rankActivity(activityId: number, rank: number): Promise; } \ No newline at end of file diff --git a/mobile/src/activity/hooks/useRankActivity.ts b/mobile/src/activity/hooks/useRankActivity.ts new file mode 100644 index 0000000000000000000000000000000000000000..e8c3c9b72444296eac4fe928a1833b577998f36f --- /dev/null +++ b/mobile/src/activity/hooks/useRankActivity.ts @@ -0,0 +1,42 @@ +import { useState } from "react"; +import { useDataContext } from "../../common/contexts/data_context"; +import { ApiRequestStatus } from "../../common/constants/api_request_states"; + +export const useRankActivity = (activityId: number) => { + const { activityRepository } = useDataContext(); + const [requestStatus, setRequestStatus] = useState( + ApiRequestStatus.IDLE + ); + const [ratingModal, setRatingModal] = useState(false); + + const setLoading = async () => { + setRequestStatus(ApiRequestStatus.LOADING); + }; + + const rankActivity = async (rank: number) => { + try { + await setLoading(); + await activityRepository!.rankActivity(activityId, rank); + setRequestStatus(ApiRequestStatus.SUCCESS); + closeRatingModal(); + } catch (error) { + setRequestStatus(ApiRequestStatus.ERROR); + } + }; + + const closeRatingModal = () => { + setRatingModal(false); + }; + + const openRatingModal = () => { + setRatingModal(true); + }; + + return { + rankActivity, + requestStatus, + ratingModal, + closeRatingModal, + openRatingModal, + }; +}; diff --git a/mobile/src/screens/activity_description/activity_description_page.tsx b/mobile/src/activity/screens/activity_description_page.tsx similarity index 97% rename from mobile/src/screens/activity_description/activity_description_page.tsx rename to mobile/src/activity/screens/activity_description_page.tsx index 77a0a9ed19f0db0927e6ba53c4b2cb4dff5286a6..14046fc0d34d76e21e513ca25d561ce1656f385a 100644 --- a/mobile/src/screens/activity_description/activity_description_page.tsx +++ b/mobile/src/activity/screens/activity_description_page.tsx @@ -14,7 +14,7 @@ import { useGetActivityInfo } from "../../hooks/useGetActivityInfo"; import { router } from "expo-router"; import { Ionicons } from "@expo/vector-icons"; import { useCallback, useEffect, useRef, useState } from "react"; -import { ActivityBottomSheet } from "../../components/activity_bottom_sheet/activity_bottom_sheet"; +import { ActivityBottomSheet } from "../../activity/components/activity_bottom_sheet"; import * as ScreenOrientation from "expo-screen-orientation"; import { LIGHT_THEME } from "../../common/constants/theme"; import { useScreenOrientation } from "../../hooks/useScreenOrientation"; diff --git a/mobile/src/activity/screens/activity_point.tsx b/mobile/src/activity/screens/activity_point.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b8bd8eaafea2aaa05bf0291580302f8dd4075e7f --- /dev/null +++ b/mobile/src/activity/screens/activity_point.tsx @@ -0,0 +1,148 @@ +import { Image, Text, View, StyleSheet, BackHandler } from "react-native"; +import { FullPageLoader } from "../../common/components/full_page_loader"; +import { ApiRequestStatus } from "../../common/constants/api_request_states"; +import { useGetActivityPoint } from "../../hooks/useGetActivityPoint"; +import { ScrollView } from "react-native-gesture-handler"; +import { AudioPlayer } from "../../common/components/audio_player"; +import { TouchableOpacity } from "@gorhom/bottom-sheet"; +import { LIGHT_THEME } from "../../common/constants/theme"; +import { router } from "expo-router"; +import { useAudio } from "../../common/contexts/audio_context"; +import { memo, useEffect, useState } from "react"; +import { StarRatingForm } from "../../common/components/rating_page/star_rating_form"; +import { FullPageRating } from "../../common/components/rating_page/full_page_rating"; +import { useRankActivity } from "../hooks/useRankActivity"; + +interface ActivityPointScreenProps { + stateId: number; + townId: number; + activityId: number; + id: number; +} + +export const ActivityPointScreen = memo( + ({ stateId, townId, activityId, id }: ActivityPointScreenProps) => { + const { data, requestStatus } = useGetActivityPoint({ + activityId, + townId, + stateId, + placeNumber: id, + }); + + const { onUnmount } = useAudio(); + const { openRatingModal, closeRatingModal, ratingModal, rankActivity } = useRankActivity(activityId); + + useEffect(() => { + const backAction = () => { + onUnmount(); + router.back(); + return true; + }; + + const backHandler = BackHandler.addEventListener( + "hardwareBackPress", + backAction + ); + + return () => backHandler.remove(); + }, []); + + if (requestStatus === ApiRequestStatus.LOADING) { + return ; + } + + if (requestStatus === ApiRequestStatus.ERROR || !data) { + return null; + } + + return ( + + + {data.name} + + + + + {data.content.content} + + {data.directions && ( + <> + Directions + + {data.directions.content} + + + )} + {!data.directions && ( + + End Activity + + )} + + + + {!data.directions && ratingModal && } + + ); + } +); + +const styles = StyleSheet.create({ + container: { + flex: 1, + gap: 15, + justifyContent: "space-between", + }, + placeContainer: { + flex: 1, + gap: 20, + padding: 20, + alignItems: "center", + }, + title: { + fontSize: 24, + fontWeight: "400", + textAlign: "center", + }, + contentText: { + fontSize: 18, + lineHeight: 30, + textAlign: "justify", + }, + imageContainer: { + width: "100%", + height: 300, + backgroundColor: "lightgrey", + padding: 10, + borderRadius: 10, + overflow: "hidden", + borderWidth: 2, + }, + image: { + height: "70%", + width: "70%", + }, + endActivityButton: { + backgroundColor: LIGHT_THEME.color.primary, + padding: 10, + borderRadius: 25, + alignItems: "center", + justifyContent: "center", + width: 200, + height: 50, + }, + endActivityButtonText: { + color: LIGHT_THEME.color.white, + fontWeight: "bold", + fontSize: 16, + }, +}); diff --git a/mobile/src/auth/components/code_form.tsx b/mobile/src/auth/components/code_form.tsx new file mode 100644 index 0000000000000000000000000000000000000000..cb62d54ff33e99165e666b695085b46d3de33098 --- /dev/null +++ b/mobile/src/auth/components/code_form.tsx @@ -0,0 +1,31 @@ +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"; + + +interface CodeFormProps { + setValue: UseFormSetValue; +} + +export const CodeForm = ({ setValue }: CodeFormProps) => { + const onTextChange = (value: string) => { + console.log("CodeForm onTextChange", value); + setValue("code", value); + }; + + return ( + + Introduce el código de verificación + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + width: "100%", + gap: 20, + }, +}); \ No newline at end of file diff --git a/mobile/src/auth/components/login_form.tsx b/mobile/src/auth/components/login_form.tsx index 7715bdaf62e733e5622bd2d854a73781e0938745..67a551f2be66cbe0a218c969d9a02955908517ea 100644 --- a/mobile/src/auth/components/login_form.tsx +++ b/mobile/src/auth/components/login_form.tsx @@ -1,11 +1,11 @@ import { Control, Controller, FieldValues } from "react-hook-form"; -import { ScrollView, StyleSheet, Text, TouchableOpacity, View } from "react-native"; +import { Button, ScrollView, StyleSheet, Text, TouchableOpacity, View } from "react-native"; import { CustomTextInput } from "../../common/components/form/text_input"; import { LIGHT_THEME } from "../../common/constants/theme"; import { LoginFormValues } from "../hooks/useLoggin"; import { OrDivision } from "../../common/components/form/or_division"; import { AntDesign } from '@expo/vector-icons'; -import { Link } from "expo-router"; +import { Link, router } from "expo-router"; import { useTranslation } from "react-i18next"; import { LanguageIcon } from "../../lang/components/language_icon"; @@ -70,7 +70,7 @@ export const LoginForm = ({ control, onSubmit }: LoginFormProps) => { )} rules={{ required: "Password is required" }} /> - + Recuperar contraseña @@ -81,6 +81,8 @@ export const LoginForm = ({ control, onSubmit }: LoginFormProps) => { + + + { + if(event.key === 'Enter'){ + getPositionByAddress(); + } + }} + /> + +

{errors.address?.message}

+ + ) +} \ No newline at end of file diff --git a/web/src/components/loading_spinner/assets/css/styles.css b/web/src/components/loading_spinner/assets/css/styles.css index bf6048a208fb291cf37eb1112d9dfb53c49baa6a..f690fb0c214307196812a2f24429de4348e759eb 100644 --- a/web/src/components/loading_spinner/assets/css/styles.css +++ b/web/src/components/loading_spinner/assets/css/styles.css @@ -1,5 +1,6 @@ .spinner{ z-index: 999; + position: absolute; width: 150px; padding: 20px; aspect-ratio: 1; @@ -13,5 +14,10 @@ -webkit-mask-composite: source-out; mask-composite: subtract; animation: l3 1s infinite linear; + left: 0; + right: 0; + top: 0; + bottom: 0; + margin: auto; } @keyframes l3 {to{transform: rotate(1turn)}} \ No newline at end of file diff --git a/web/src/components/map/map.tsx b/web/src/components/map/map.tsx index 01fbedb4acc2ef054a98866c57eb32bcb5679c84..e88eecc085b19cdfc6924593249c1546210475d5 100644 --- a/web/src/components/map/map.tsx +++ b/web/src/components/map/map.tsx @@ -1,15 +1,14 @@ -import { useState, Dispatch, SetStateAction, useEffect } from "react"; -import { REACT_APP_GOOGLE_API_KEY } from "../../constants/api_keys"; +import { Dispatch, SetStateAction, useEffect } from "react"; import { UseFormClearErrors, UseFormSetValue } from "react-hook-form"; import { Place } from "../../infraestructure/entities/place"; -import { APIProvider, Map, Marker } from "@vis.gl/react-google-maps"; +import { Map, Marker, useMap } from "@vis.gl/react-google-maps"; interface props{ setValue: UseFormSetValue; setIsLoading: Dispatch>; clearErrors: UseFormClearErrors; - latitude?: number; - longitude?: number; + position: Position | null; + setPosition: Dispatch>; } export interface Position{ @@ -17,35 +16,39 @@ export interface Position{ longitude: number; } -export const MapComponent = ({setValue, setIsLoading, latitude, longitude, clearErrors}: props) => { - const [position, setPosition] = useState({latitude: 0.0, longitude: 0.0}); - const center = {lat: 23.687, lng: -102.74}; - useEffect(() => { - if(latitude && longitude){ - setPosition({latitude, longitude}); - } - }, [latitude]); +export const MapComponent = ({setValue, setIsLoading, position, setPosition, clearErrors}: props) => { + const defaultCenter = {lat: 23.687, lng: -102.74}; + const mapRef = useMap(); useEffect(() => { - setValue('latitude',position.latitude); - setValue('longitude',position.longitude); - clearErrors('latitude'); - clearErrors('longitude'); + if(position && position.latitude!==0 && position.longitude!==0){ + setValue('latitude',position.latitude); + setValue('longitude',position.longitude); + if(mapRef){ + mapRef.setCenter({lat: position.latitude, lng: position.longitude}); + mapRef.setZoom(16); + } + clearErrors('latitude'); + clearErrors('longitude'); + } },[position]); return ( - setIsLoading(false)}> -
- { +
+ { const lat = event.detail.latLng?.lat || 0.0; const lng = event.detail.latLng?.lng || 0.0; setPosition({latitude: lat, longitude: lng}); - }}> - - -
- + }} + + > + {position && } +
+
); } \ No newline at end of file diff --git a/web/src/data/datasources/prod/place_datasource.ts b/web/src/data/datasources/prod/place_datasource.ts index 7703c47fc7c2dcd1b1c72203f0e336cc7360f926..299676c58e61b8a8222115d7ee25e0d362b6803c 100644 --- a/web/src/data/datasources/prod/place_datasource.ts +++ b/web/src/data/datasources/prod/place_datasource.ts @@ -18,6 +18,7 @@ export class PlaceDatasourceProd implements PlaceDatasourceInf{ formToSend.append('longitude', String(form.longitude)); formToSend.append('openAt', String(form.openAt)); formToSend.append('closeAt', String(form.closeAt)); + formToSend.append('address', form.address); if(form.available === AvailableDays.CUSTOM){ formToSend.append('startDate', String(form.startDate)); @@ -77,6 +78,7 @@ export class PlaceDatasourceProd implements PlaceDatasourceInf{ formToSend.append('longitude', String(place.longitude)); formToSend.append('openAt', String(place.openAt)); formToSend.append('closeAt', String(place.closeAt)); + formToSend.append('address', place.address); if(place.available === AvailableDays.CUSTOM){ formToSend.append('startDate', String(place.startDate)); diff --git a/web/src/data/models/prod/PlaceModel.ts b/web/src/data/models/prod/PlaceModel.ts index 4adb0593327899751f16c08dc7089afbf2dab06a..5e4ea3e456073befd560a1646e5624033007bc20 100644 --- a/web/src/data/models/prod/PlaceModel.ts +++ b/web/src/data/models/prod/PlaceModel.ts @@ -15,6 +15,7 @@ export interface PlaceModel { closeAt: number; startDate?: Date; endDate?: Date; + address: string; } export const placeModelToEntity = (model: PlaceModel) =>{ @@ -47,7 +48,8 @@ export const placeModelToEntity = (model: PlaceModel) =>{ openAt: model.openAt, closeAt: model.closeAt, startDate: model.startDate, - endDate: model.endDate + endDate: model.endDate, + address: model.address } return place; } \ No newline at end of file diff --git a/web/src/hooks/usePlace.tsx b/web/src/hooks/usePlace.tsx index 69a8e095865c12430637b82fcbb1ccf192eb1ec6..550365e18c64ee08ad2821a4685d3af28139ffc5 100644 --- a/web/src/hooks/usePlace.tsx +++ b/web/src/hooks/usePlace.tsx @@ -22,14 +22,14 @@ const resolver: Resolver = async (data) => { } } - if(!data.openAt && data.openAt!=0){ + if(!data.openAt && data.openAt!==0){ errors.openAt = { type: "required", message: "La hora de apertura es requerida" }; } - if(!data.closeAt && data.closeAt!=0){ + if(!data.closeAt && data.closeAt!==0){ errors.closeAt = { type: "required", message: "La hora de cierre es requerida" @@ -64,6 +64,13 @@ const resolver: Resolver = async (data) => { } } + if(!data.address){ + errors.address = { + type: "required", + message: "Debe de ingresar la dirección al lugar" + } + } + for(var index = languaguesList.length-1; index>=0; index--){ if(!data.descriptions || !data.descriptions[index]){ errors.descriptions = { @@ -136,6 +143,7 @@ setIsWindowActive?: (visibility: boolean) => void) => { formState: {errors}, clearErrors, resetField, + getValues } = useForm({resolver}); const [errorMessage, setErrorMessage] = useState(""); const [languageDescriptionIndexSelected, setLanguageDescriptionIndexSelected] = useState(0); @@ -285,5 +293,6 @@ setIsWindowActive?: (visibility: boolean) => void) => { categoriesId, setCategoriesId, getPlaceById, + getValues }; } diff --git a/web/src/infraestructure/entities/place.ts b/web/src/infraestructure/entities/place.ts index 46fe46ba27aeb179f1ba940ce891acefcf353420..ca43cd0878319bf21a1843fda064e7ef1a12f835 100644 --- a/web/src/infraestructure/entities/place.ts +++ b/web/src/infraestructure/entities/place.ts @@ -12,6 +12,7 @@ export interface Place{ imagesList?: File[] | string[]; startDate?: Date; endDate?: Date; + address: string; } export enum AvailableDays { @@ -44,4 +45,5 @@ export const EmptyPlace : Place = { openAt: 0, closeAt: 0, available: AvailableDays.WEEKEND, + address: '' } \ No newline at end of file