diff --git a/src/components/ExperienceDetail/InfoExperience.tsx b/src/components/ExperienceDetail/InfoExperience.tsx
index a0e67f7..e62dabe 100644
--- a/src/components/ExperienceDetail/InfoExperience.tsx
+++ b/src/components/ExperienceDetail/InfoExperience.tsx
@@ -44,7 +44,7 @@ function InfoExperience({description, capacity, location, createdAt, tags } : {d
export default InfoExperience;
-const InfoCard = ({ icon, first, second } : { icon: string, first: string, second: string | number }) => {
+export const InfoCard = ({ icon, first, second } : { icon: string, first: string, second: string | number }) => {
return (
diff --git a/src/components/ExperienceDetail/ReviewCard.tsx b/src/components/ExperienceDetail/ReviewCard.tsx
index c3a0e21..726c2e0 100644
--- a/src/components/ExperienceDetail/ReviewCard.tsx
+++ b/src/components/ExperienceDetail/ReviewCard.tsx
@@ -5,12 +5,15 @@ interface IReview {
userId: string
rating: number
comment: string
- createdAt: string
+ createdAt: string,
+ userAvatar: string,
+ userName: string,
}
function ReviewCard({review} : {review: IReview}) {
- const { rating, comment, createdAt } = review;
+ const { rating, comment, createdAt, userName, userAvatar } = review;
+ console.log(review)
const getRating = (rating: number) => {
switch (rating) {
@@ -31,9 +34,9 @@ function ReviewCard({review} : {review: IReview}) {
return (
-
+
-
Mariana Garcia Rodiguez
+
{userName ?? "Usuario"}
{getRating(rating)?.stars} {formatToShortDate(createdAt)}
diff --git a/src/components/ExperienceDetail/ReviewSection.tsx b/src/components/ExperienceDetail/ReviewSection.tsx
index 0f4bc20..126159e 100644
--- a/src/components/ExperienceDetail/ReviewSection.tsx
+++ b/src/components/ExperienceDetail/ReviewSection.tsx
@@ -13,6 +13,7 @@ function ReviewSection({ id }: { id: string }) {
const response = await fetch(`${BACK_URL}/reviews/experience/${id}`);
const data = await response.json();
setReviews(data);
+ console.log(reviews)
} catch (error) {
console.error("Error fetching reviews:", error);
}
diff --git a/src/components/ExperiencePanel/BookingInfo/BookingInfo.tsx b/src/components/ExperiencePanel/BookingInfo/BookingInfo.tsx
new file mode 100644
index 0000000..bae950e
--- /dev/null
+++ b/src/components/ExperiencePanel/BookingInfo/BookingInfo.tsx
@@ -0,0 +1,283 @@
+import { useState, useEffect } from "react";
+import { useParams } from "react-router-dom";
+import HeaderPanel from "../HeaderPanel";
+import imageSafari from "../../../assets/img/Safari.jpg";
+
+//@ts-ignore
+import Cookies from "js-cookie";
+import { getStatus } from "../../../utils/getDateFormat";
+import { useNavigate } from "react-router-dom";
+import { useContext } from "react";
+import { AuthContext } from "../../../contexts/auth.context";
+import reviewServices from "../../../services/review.services";
+
+function BookingInfo() {
+ const { id } = useParams();
+ const navigate = useNavigate();
+ const { user } = useContext(AuthContext);
+ const userId = user?._id;
+
+ const [booking, setBooking] = useState
(null);
+ const [experience, setExperience] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [userHasReview, setUserHasReview] = useState(false);
+ const [reviewId, setReviewId] = useState(null);
+ const [showConfirmationModal, setShowConfirmationModal] = useState(false); // Estado para controlar el modal de confirmación
+ const [successMessage, setSuccessMessage] = useState(null); // Mensaje de éxito
+
+ function formatDateAndTime(dateString: string) {
+ const date = new Date(dateString);
+ const day = String(date.getDate()).padStart(2, "0");
+ const month = String(date.getMonth() + 1).padStart(2, "0");
+ const year = date.getFullYear();
+ const formattedDate = `${day}/${month}/${year}`;
+ const hours = String(date.getHours()).padStart(2, "0");
+ const minutes = String(date.getMinutes()).padStart(2, "0");
+ const formattedTime = `${hours}:${minutes}`;
+ return { date: formattedDate, time: formattedTime };
+ }
+
+ const { date, time } = formatDateAndTime(booking?.bookingDate || "");
+ const { text } = getStatus(booking?.status || "");
+
+ useEffect(() => {
+ const fetchBookingData = async () => {
+ try {
+ setLoading(true);
+
+ const storedBooking = Cookies.get("booking");
+ if (storedBooking) {
+ const parsedBooking = JSON.parse(storedBooking);
+ setBooking(parsedBooking);
+
+ const experienceId = parsedBooking.experienceId;
+
+ const experienceResponse = await fetch(
+ `${import.meta.env.VITE_API_URL}/experiences/${experienceId}`
+ );
+ const experienceData = await experienceResponse.json();
+ setExperience(experienceData);
+
+ const reviewsResponse = await fetch(
+ `${import.meta.env.VITE_API_URL}/reviews/experience/${experienceId}`
+ );
+ const reviews = await reviewsResponse.json();
+
+ // Busca si el usuario ya tiene una reseña
+ const userReview = reviews.find(
+ (review: any) => review.userId === userId
+ );
+ if (userReview) {
+ setUserHasReview(true);
+ setReviewId(userReview.id); // Guarda el ID de la reseña
+ } else {
+ setUserHasReview(false);
+ }
+ } else {
+ console.error("No se encontró la reserva en las cookies.");
+ }
+ } catch (error) {
+ console.error("Error fetching booking or experience data:", error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ if (id) fetchBookingData();
+ }, [id, userId]);
+
+ const { provider } = booking || {};
+
+ const handleDeleteReview = async () => {
+ if (!reviewId) return;
+
+ try {
+ await reviewServices.delete(reviewId); // Usa el servicio actualizado
+
+ setSuccessMessage("Reseña eliminada con éxito."); // Muestra el mensaje de éxito
+ setUserHasReview(false);
+ setReviewId(null);
+ setShowConfirmationModal(false); // Cierra el modal
+
+ // Oculta el mensaje después de 3 segundos
+ setTimeout(() => {
+ setSuccessMessage(null);
+ }, 3000);
+ } catch (error) {
+ console.error("Error eliminando la reseña:", error);
+ alert("Hubo un problema al eliminar la reseña.");
+ }
+ };
+
+ const handleShowConfirmation = () => {
+ setShowConfirmationModal(true);
+ };
+
+ const handleCancelDelete = () => {
+ setShowConfirmationModal(false);
+ };
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (!booking || !experience) {
+ return No se encontraron datos.
;
+ }
+
+ return (
+
+
+
+
+
{experience.title}
+
+
+
+
+
+
+
+
+ }
+ />
+ navigate(`/experience/${experience.id}`)}
+ className="px-4 py-0.5"
+ >
+ Ver información
+
+ }
+ />
+ {booking.status === "CONFIRMED" && (
+
+ Eliminar Reseña
+
+ ) : (
+
+ )
+ }
+ />
+ )}
+
+
+
Datos del proveedor
+
+
+
+
+
+
+
+
+ {/* Modal de confirmación */}
+ {showConfirmationModal && (
+
+
+
+ ¿Estás seguro?
+
+
+ ¿Quieres eliminar esta reseña?
+
+
+
+
+
+
+
+ )}
+
+ {/* Mensaje de éxito */}
+ {successMessage && (
+
+ {successMessage}
+
+ )}
+
+ );
+}
+
+export default BookingInfo;
+
+const Info = ({ first, second }: { first: string; second: string | any }) => {
+ return (
+
+ );
+};
+
+const ButtonNavigate = ({
+ latitude,
+ longitude,
+}: {
+ latitude: string;
+ longitude: string;
+}) => {
+ const openLocation = () => {
+ const url = `https://www.google.com/maps?q=${latitude},${longitude}`;
+ window.open(url, "_blank");
+ };
+
+ return (
+
+ );
+};
diff --git a/src/components/ExperiencePanel/ExperiencePanel.tsx b/src/components/ExperiencePanel/ExperiencePanel.tsx
new file mode 100644
index 0000000..6b4d150
--- /dev/null
+++ b/src/components/ExperiencePanel/ExperiencePanel.tsx
@@ -0,0 +1,17 @@
+
+import { useContext } from "react"
+import { AuthContext } from "../../contexts/auth.context"
+import PanelTourist from "./TouristUser/PanelTourist";
+// import BookingInfo from "./BookingInfo/BookingInfo";
+import PanelProvider from "./ProviderUser/PanelProvider";
+
+function ExperiencePanel() {
+
+ const { user } = useContext(AuthContext);
+ console.log(user)
+
+
+ return user?.role !== 'TOURIST' ? :
+}
+
+export default ExperiencePanel;
\ No newline at end of file
diff --git a/src/components/ExperiencePanel/HeaderPanel.tsx b/src/components/ExperiencePanel/HeaderPanel.tsx
new file mode 100644
index 0000000..79b183d
--- /dev/null
+++ b/src/components/ExperiencePanel/HeaderPanel.tsx
@@ -0,0 +1,16 @@
+import iconLeft from '../../assets/icons/icon-arrow-left.svg'
+import { useNavigate } from 'react-router-dom';
+
+function HeaderPanel({title} : {title: string}) {
+ const navigate = useNavigate();
+
+ return (
+
+
navigate(-1)} src={iconLeft} alt="icon-left" className="w-8 pl-5 cursor-pointer" />
+
{title}
+
+
+ );
+}
+
+export default HeaderPanel;
diff --git a/src/components/ExperiencePanel/ManageCustomers/CustomerCard.tsx b/src/components/ExperiencePanel/ManageCustomers/CustomerCard.tsx
new file mode 100644
index 0000000..2fe5670
--- /dev/null
+++ b/src/components/ExperiencePanel/ManageCustomers/CustomerCard.tsx
@@ -0,0 +1,140 @@
+import { useState, useContext } from "react";
+import { AuthContext } from "../../../contexts/auth.context";
+import bookingServices from "../../../services/booking.services";
+
+type ConfirmationModalProps = {
+ isOpen: boolean;
+ onClose: () => void;
+ onConfirm: () => void;
+ message: string;
+};
+
+function ConfirmationModal({ isOpen, onClose, onConfirm, message } : ConfirmationModalProps) {
+ if (!isOpen) return null;
+
+ return (
+
+
+
{message}
+
+
+
+
+
+
+ );
+}
+
+function CustomerCard({ activeTab, dataBooking, onBookingUpdate }: any) {
+ const { user } = useContext(AuthContext);
+ const userId = user?._id;
+
+ const { tourist, participants, id: bookingId } = dataBooking;
+ const { avatar, name, email, phone } = tourist;
+ console.log(tourist)
+
+ const [isModalOpen, setModalOpen] = useState(false);
+ const [modalMessage, setModalMessage] = useState("");
+ const [modalAction, setModalAction] = useState<(() => void) | null>(null);
+ const [notification, setNotification] = useState<{ message: string; color: string } | null>(null);
+
+ const showNotification = (message: string, color: string) => {
+ setNotification({ message, color });
+ setTimeout(() => setNotification(null), 3000);
+ };
+
+ const handleConfirmBooking = async () => {
+ try {
+ await bookingServices.updateBooking(bookingId, { userId, status: "CONFIRMED" });
+ showNotification("Reserva confirmada con éxito.", "bg-green-600");
+ setTimeout(() => onBookingUpdate(), 1500);
+ } catch (error) {
+ console.error(error);
+ showNotification("Error al confirmar la reserva.", "bg-red-600");
+ } finally {
+ setModalOpen(false);
+ }
+ };
+
+ const handleCancelBooking = async () => {
+ try {
+ await bookingServices.updateBooking(bookingId, { userId, status: "CANCELLED" });
+ showNotification("Reserva cancelada con éxito.", "bg-primary");
+ onBookingUpdate(); // Llamamos a la función para actualizar los bookings en ManageCustomers
+ } catch (error) {
+ console.error(error);
+ showNotification("Error al cancelar la reserva.", "bg-red-600");
+ } finally {
+ setModalOpen(false);
+ }
+ };
+
+ const openConfirmModal = () => {
+ setModalMessage("¿Estás seguro de que deseas confirmar esta reserva?");
+ setModalAction(() => handleConfirmBooking);
+ setModalOpen(true);
+ };
+
+ const openCancelModal = () => {
+ setModalMessage("¿Estás seguro de que deseas cancelar esta reserva?");
+ setModalAction(() => handleCancelBooking);
+ setModalOpen(true);
+ };
+
+ return (
+
+
+
+
+
{name}
+
{email}
+
{phone}
+
+ {participants} {participants === 1 ? "Persona" : "Personas"}
+
+
+
+ {activeTab === "PENDING" && (
+
+
+
+
+ )}
+ setModalOpen(false)}
+ onConfirm={modalAction || (() => {})}
+ message={modalMessage}
+ />
+ {notification && (
+
+ {notification.message}
+
+ )}
+
+ );
+}
+
+export default CustomerCard;
diff --git a/src/components/ExperiencePanel/ManageCustomers/ManageCustomers.tsx b/src/components/ExperiencePanel/ManageCustomers/ManageCustomers.tsx
new file mode 100644
index 0000000..85ff5db
--- /dev/null
+++ b/src/components/ExperiencePanel/ManageCustomers/ManageCustomers.tsx
@@ -0,0 +1,182 @@
+import { useEffect, useState } from "react";
+import HeaderPanel from "../HeaderPanel";
+import CustomerCard from "./CustomerCard";
+import bookingService from "../../../services/booking.services";
+import { useParams } from "react-router-dom";
+
+function ManageCustomers() {
+ const [bookings, setBookings] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [uniqueDates, setUniqueDates] = useState([]);
+ const [uniqueTimes, setUniqueTimes] = useState([]);
+ const [activeTab, setActiveTab] = useState("PENDING");
+ const [selectedDate, setSelectedDate] = useState("Todos");
+ const [selectedTime, setSelectedTime] = useState("Todos");
+ const [filteredBookings, setFilteredBookings] = useState([]);
+ const { id } = useParams();
+
+ // Mover fetchBookings fuera del useEffect
+ const fetchBookings = async () => {
+ try {
+ setLoading(true);
+ const data = await bookingService.getBookingsFromExperience(id);
+ setBookings(data);
+
+ const dates: string[] = Array.from(
+ new Set(data.map((booking: any) => booking.bookingDate.split("T")[0]))
+ );
+
+ setUniqueDates(dates);
+
+ const times: string[] = Array.from(
+ new Set(
+ data.map((booking: any) =>
+ new Date(booking.bookingDate).toLocaleTimeString("en-GB", {
+ hour: "2-digit",
+ minute: "2-digit",
+ })
+ )
+ )
+ );
+
+ setUniqueTimes(times);
+
+ setFilteredBookings(data); // Inicialmente mostramos todo
+ } catch (error) {
+ console.error("Error fetching bookings:", error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ if (id) {
+ fetchBookings(); // Llamada al fetchBookings
+ }
+ }, [id]);
+
+ useEffect(() => {
+ // Filtrar los bookings según el estado activo, fecha y hora seleccionados
+ const filterResults = () => {
+ let filtered = bookings.filter((booking) => booking.status === activeTab);
+
+ if (selectedDate !== "Todos") {
+ filtered = filtered.filter((booking) =>
+ booking.bookingDate.startsWith(selectedDate)
+ );
+ }
+
+ if (selectedTime !== "Todos") {
+ filtered = filtered.filter((booking) => {
+ const bookingTime = new Date(booking.bookingDate).toLocaleTimeString(
+ "en-GB",
+ {
+ hour: "2-digit",
+ minute: "2-digit",
+ }
+ );
+ return bookingTime === selectedTime;
+ });
+ }
+
+ setFilteredBookings(filtered);
+ };
+
+ filterResults();
+ }, [activeTab, selectedDate, selectedTime, bookings]);
+
+ const handleBookingUpdate = () => {
+ if (id) {
+ fetchBookings(); // Vuelve a llamar a fetchBookings al actualizar
+ }
+ };
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ const tabTranslations: { [key: string]: string } = {
+ PENDING: "Pendientes",
+ CONFIRMED: "Confirmados",
+ CANCELLED: "Cancelados",
+ };
+
+ return (
+
+
+
+ {/* Filters */}
+
+
+
+
+
+ {/* Tabs */}
+
+ {["PENDING", "CONFIRMED", "CANCELLED"].map((tab) => (
+
+ ))}
+
+
+ {/* Bookings List */}
+
+ {filteredBookings.length > 0 ? (
+ filteredBookings.map((booking) => (
+
+ ))
+ ) : (
+
+ No hay reservas en {tabTranslations[activeTab]}.
+
+ )}
+
+
+ );
+}
+
+export default ManageCustomers;
diff --git a/src/components/ExperiencePanel/ProviderUser/ExperienceCardProvider.tsx b/src/components/ExperiencePanel/ProviderUser/ExperienceCardProvider.tsx
new file mode 100644
index 0000000..d694e94
--- /dev/null
+++ b/src/components/ExperiencePanel/ProviderUser/ExperienceCardProvider.tsx
@@ -0,0 +1,39 @@
+import imageSafari from "../../../assets/img/Safari.jpg";
+import { useNavigate } from "react-router-dom";
+
+function ExperienceCardProvider({ title, experienceId , price, status } : {title: string, experienceId: string, price: number, status:string } ) {
+ const navigate = useNavigate();
+
+ return (
+
+
+
+
+
+
+
+ {title}
+
+ {status ?
+ Estado: Activo
+
:
+ Estado: Inactivo
+
}
+
Precio: ${price}
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default ExperienceCardProvider;
diff --git a/src/components/ExperiencePanel/ProviderUser/PanelProvider.tsx b/src/components/ExperiencePanel/ProviderUser/PanelProvider.tsx
new file mode 100644
index 0000000..10c040d
--- /dev/null
+++ b/src/components/ExperiencePanel/ProviderUser/PanelProvider.tsx
@@ -0,0 +1,50 @@
+import { useEffect, useState, useContext } from "react";
+import HeaderPanel from "../HeaderPanel";
+import ExperienceCardProvider from "./ExperienceCardProvider";
+import { AuthContext } from "../../../contexts/auth.context";
+
+function PanelProvider() {
+ const { user } = useContext(AuthContext);
+ const userId = user?._id;
+ const [experiences, setExperiences] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ const fetchExperiences = async () => {
+ try {
+ const response = await fetch(`${import.meta.env.VITE_API_URL}/experiences/host/${userId}`);
+ const data = await response.json();
+ setExperiences(data);
+ } catch (error) {
+ console.error("Error fetching experiences:", error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ if (userId) {
+ fetchExperiences();
+ }
+ }, []);
+
+
+
+ return (
+
+
+ {loading ? (
+
+ ) : (
+
+ {experiences.map((experience) => (
+
+ ))}
+
+ )}
+
+ );
+}
+
+export default PanelProvider;
diff --git a/src/components/ExperiencePanel/TouristUser/ExperienceCard.tsx b/src/components/ExperiencePanel/TouristUser/ExperienceCard.tsx
new file mode 100644
index 0000000..17b66c6
--- /dev/null
+++ b/src/components/ExperiencePanel/TouristUser/ExperienceCard.tsx
@@ -0,0 +1,65 @@
+import imageSafari from "../../../assets/img/Safari.jpg";
+import { useNavigate } from "react-router-dom";
+import { Booking } from "../../../types/booking";
+import { getStatus } from "../../../utils/getDateFormat";
+import { formatToShortDate } from "../../../utils/getDateFormat";
+
+// @ts-ignore
+import Cookies from "js-cookie";
+
+interface ExperienceCardProps {
+ idBooking: string;
+ bookingInfo: Booking;
+ // userId: string;
+}
+
+function ExperienceCard({ idBooking, bookingInfo }: ExperienceCardProps) {
+ const navigate = useNavigate();
+
+ // experienceId posiblemente tenga que extraerlo
+ const { status, bookingDate, experienceTitle } = bookingInfo;
+ console.log(bookingInfo)
+
+ const dateFormatted = formatToShortDate(bookingDate);
+
+ const { text, color } = getStatus(status);
+
+
+ // Redirecciona a la informacion de la reserva para el usuario
+ const handleBooking = () => {
+ // Guardar el booking en la cookie
+ Cookies.set("booking", JSON.stringify(bookingInfo), { expires: 7 });
+ navigate(`/booking-info/${idBooking}`);
+ };
+
+ return (
+
+
+
+
+
+
+ {experienceTitle} {/* Nombre de la experiencia */}
+
+
+ Estado: {text}
+
+
Fecha: {dateFormatted}
+
+
+
+
+
+ );
+}
+
+export default ExperienceCard;
diff --git a/src/components/ExperiencePanel/TouristUser/PanelTourist.tsx b/src/components/ExperiencePanel/TouristUser/PanelTourist.tsx
new file mode 100644
index 0000000..400d3cc
--- /dev/null
+++ b/src/components/ExperiencePanel/TouristUser/PanelTourist.tsx
@@ -0,0 +1,56 @@
+import { useEffect, useState } from "react";
+import ExperienceCard from "./ExperienceCard";
+import HeaderPanel from "../HeaderPanel";
+// Importa el servicio de reservas (aunque ahora no se está usando en este ejemplo)
+import bookingServices from "../../../services/booking.services";
+import { AuthContext } from "../../../contexts/auth.context";
+import { useContext } from "react";
+import { Booking } from "../../../types/booking";
+
+function PanelTourist() {
+ const { user } = useContext(AuthContext);
+ const userId = user?._id;
+
+ const [bookings, setBookings] = useState([]);
+ const [loading, setLoading] = useState(true); // Estado para controlar la carga
+
+
+ useEffect(() => {
+ // TRAE LOS BOOKING DEL USUARIO
+ const fetchBookings = async () => {
+ try {
+ const response = await bookingServices.getBookingsFromUser(userId);
+ setBookings(response);
+ console.log(bookings)
+ } catch (error) {
+ console.error("Error fetching bookings:", error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchBookings();
+ }, [userId]);
+
+ return (
+
+
+
+ {/* Muestra el loader mientras está en carga */}
+ {loading ? (
+
+ ) : (
+ // RENDERIZA LOS BOOKINGS
+
+ {bookings?.map((booking) => (
+
+ ))}
+
+ )}
+
+ );
+}
+
+export default PanelTourist;
diff --git a/src/components/Filters/Filters.tsx b/src/components/Filters/Filters.tsx
index dd01d81..749389f 100644
--- a/src/components/Filters/Filters.tsx
+++ b/src/components/Filters/Filters.tsx
@@ -6,6 +6,12 @@ import CategorySelect from "./CategorySelect";
import LocationInput from "./LocationInput";
import { IExperience } from "../../types/experience";
import { MagnifyingGlassIcon } from "@heroicons/react/24/solid";
+import { formatCategory, formatToShortDate } from "../../utils/getDateFormat";
+import { InfoCard } from "../ExperienceDetail/InfoExperience";
+
+import imageSafari from "../../assets/img/Safari.jpg";
+import iconLocation from "../../assets/icons/icon-location.svg";
+import iconPrice from "../../assets/icons/icon-price.svg";
interface Filters {
title: string;
@@ -44,7 +50,8 @@ const Filters: React.FC = () => {
});
const handleInputChange = (key: string, value: string) => {
- const regex = /^[A-Za-z\s]*$/; // Only letters and spaces
+ const regex = /^[A-Za-záéíóúÁÉÍÓÚ\s]*$/;
+ // Only letters and spaces
if (value.length > 50 || !regex.test(value)) {
setInputErrors((prev) => ({
...prev,
@@ -65,27 +72,32 @@ const Filters: React.FC = () => {
}));
};
- const fetchExperiences = useCallback(async (queryParams: QueryParams) => {
- setLoading(true);
- setError(null);
+ const fetchExperiences = useCallback(
+ async (queryParams: QueryParams) => {
+ setLoading(true);
+ setError(null);
- try {
- const response = await axios.get(
- `${BACK_URL}/experiences/get-all?${queryString.stringify(queryParams)}`
- );
- console.log("API Response:", response.data); // Debugging API response
- if (Array.isArray(response.data)) {
- setExperiences(response.data);
- } else {
- setExperiences([]); // Fallback if response is not an array
+ try {
+ const response = await axios.get(
+ `${BACK_URL}/experiences/get-all?${queryString.stringify(
+ queryParams
+ )}`
+ );
+ console.log("API Response:", response.data); // Debugging API response
+ if (Array.isArray(response.data)) {
+ setExperiences(response.data);
+ } else {
+ setExperiences([]); // Fallback if response is not an array
+ }
+ } catch (err) {
+ setError("No existen experiencias.");
+ setExperiences([]); // Clear experiences on error
+ } finally {
+ setLoading(false);
}
- } catch (err) {
- setError("No existen experiencias.");
- setExperiences([]); // Clear experiences on error
- } finally {
- setLoading(false);
- }
- }, [BACK_URL]);
+ },
+ [BACK_URL]
+ );
const applyFilters = () => {
const { title, country, city, maxPrice, category } = filters;
@@ -164,7 +176,7 @@ const Filters: React.FC = () => {
-
+
{
-
- {inputErrors.title &&
{inputErrors.title}
}
+ {inputErrors.title && (
+
{inputErrors.title}
+ )}
{inputErrors.city &&
{inputErrors.city}
}
@@ -242,7 +255,8 @@ const Filters: React.FC = () => {
{error}
) : (
-
Experiencias
+
Experiencias
+
{Array.isArray(experiences) && experiences.length > 0 ? (
experiences.map((experience) => (
@@ -252,23 +266,34 @@ const Filters: React.FC = () => {
>
-
{experience.title}
-
- {experience.location[0] + ", " + experience.location[1]}
+
+
Categoria: {formatCategory(experience.tags[0])}
+
+
+ {experience.title}
+
+
+ {experience.description}
-
-
💰 ${experience.price}
-
- 📍 {experience.location[0]}, {experience.location[1]}
-
-
📍 {experience.tags[0]}
+
+
+
@@ -286,4 +311,4 @@ const Filters: React.FC = () => {
);
};
-export default Filters;
\ No newline at end of file
+export default Filters;
diff --git a/src/components/Reviews/Reviews.tsx b/src/components/Reviews/Reviews.tsx
index f3adf53..e0b557a 100644
--- a/src/components/Reviews/Reviews.tsx
+++ b/src/components/Reviews/Reviews.tsx
@@ -1,127 +1,131 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState, useContext } from "react";
+//@ts-ignore
+import Cookies from "js-cookie";
+import { useParams } from "react-router-dom";
+import imageSafari from "../../assets/img/Safari.jpg";
+import { AuthContext } from "../../contexts/auth.context";
+import HeaderPanel from "../ExperiencePanel/HeaderPanel";
+import reviewServices from "../../services/review.services";
+import { useNavigate } from "react-router-dom";
const Reviews: React.FC = () => {
- const [rating, setRating] = useState
('');
- const [review, setReview] = useState('');
+ const { id } = useParams();
+ const { user } = useContext(AuthContext);
+ const userId = user?._id;
+ const navigate = useNavigate();
+
+ const [rating, setRating] = useState();
+ const [review, setReview] = useState("");
const [loading, setLoading] = useState(false);
- const [success, setSuccess] = useState(null);
- const [activity, setActivity] = useState<{ title: string; image: string } | null>(null);
- const [fetchError, setFetchError] = useState(null);
-
-
- useEffect(() => {
- const fetchActivity = async () => {
- try {
- setFetchError(null);
- const response = await fetch('https://jsonplaceholder.typicode.com/photos/1');
- if (!response.ok) {
- throw new Error('Failed to fetch activity details');
- }
- const data = await response.json();
- setActivity({
- title: data.title || 'Default Activity Title',
- image: data.url || 'https://via.placeholder.com/300x150',
- });
- } catch (error: unknown) {
- if (error instanceof Error) {
- console.error('Error fetching activity:', error.message);
- setFetchError(error.message);
- } else {
- console.error('Unexpected error:', error);
- setFetchError('An unexpected error occurred while fetching activity details');
- }
- }
- };
+ const [errors, setErrors] = useState<{ rating?: string; review?: string }>({
+ rating: "",
+ review: "",
+ });
+ const [message, setMessage] = useState(null);
+
+ const CookieExperience = Cookies.get("experience");
+ const experience = JSON.parse(CookieExperience);
+
+ // Validación de campos
+ const isFormValid = () => {
+ let formIsValid = true;
+ const errors: { rating?: string; review?: string } = {};
+
+ if (!rating || rating < 1 || rating > 5) {
+ errors.rating = "La calificación debe ser entre 1 y 5.";
+ formIsValid = false;
+ }
+ if (!review || review.length < 10) {
+ errors.review = "La reseña debe tener al menos 10 caracteres.";
+ formIsValid = false;
+ }
- fetchActivity();
- }, []);
+ setErrors(errors);
+ return formIsValid;
+ };
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
- setLoading(true);
- setSuccess(null);
- try {
- const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- rating,
- review,
- activity: activity?.title,
- }),
- });
-
- if (!response.ok) {
- throw new Error('Failed to submit review');
- }
+ // Validar antes de proceder
+ if (!isFormValid()) {
+ console.log("Formulario inválido: Verifica los campos.");
+ return;
+ }
- const data = await response.json();
- console.log('Review submitted successfully:', data);
+ // Verificar que el usuario y el ID de la experiencia estén disponibles
+ if (!userId || !id) {
+ setMessage("Información de usuario o experiencia faltante.");
+ return;
+ }
- setSuccess(true);
- setRating('');
- setReview('');
- } catch (error: unknown) {
- if (error instanceof Error) {
- console.error('Error submitting review:', error.message);
- setSuccess(false);
+ setLoading(true);
+ setMessage(null); // Limpiar el mensaje anterior
+
+ try {
+ const data = {
+ experienceId: id,
+ userId,
+ rating: Number(rating),
+ comment: review,
+ };
+ console.log("Datos enviados: ", data);
+
+ const response = await reviewServices.create(data);
+ console.log(response);
+
+ if (response.status === 200) {
+ setRating(0);
+ setReview("");
+ setMessage("Reseña enviada con éxito!");
+ console.log("Reseña enviada con éxito");
+ } else if (response.message === "Review uploaded successfully.") {
+ setMessage("Reseña enviada con éxito!");
+ setTimeout(() => navigate(-1), 2500);
} else {
- console.error('Unexpected error:', error);
- setSuccess(false);
+ setMessage("Hubo un error al enviar la reseña.");
}
+ } catch (error) {
+ console.error("Error al enviar la reseña:", error);
+ setMessage("Error al enviar la reseña. Intenta nuevamente.");
} finally {
setLoading(false);
}
};
return (
-
-
-
-
-
Añadir reseña
+
+
+
-
- {fetchError ? (
-
Error: {fetchError}
- ) : !activity ? (
-
- Cargando datos de la actividad...
-
- ) : (
- <>
-
-
-
-
-
- {activity.title}
-
- >
- )}
+ <>
+
+
+
+
+ {experience?.title}
+
+ >
- {success === true && (
-
- Reseña enviada con éxito.
-
- )}
- {success === false && (
-
- Error al enviar la reseña. Intenta de nuevo.
-
- )}
+ {message && (
+
+ {message}
+
+ )}
+
);
};
diff --git a/src/components/UserProfile/DetailsCard.tsx b/src/components/UserProfile/DetailsCard.tsx
index b6e314a..3963001 100644
--- a/src/components/UserProfile/DetailsCard.tsx
+++ b/src/components/UserProfile/DetailsCard.tsx
@@ -9,11 +9,7 @@ const DetailsCard: React.FC
= ({ email, location, phone, role }) =>
const dataMap = { email, location, phone }
const handleBookingsClick = () => {
- if (role === 'TOURIST') {
- navigate('/mis-reservas')
- } else if (role === 'PROVIDER') {
- navigate('/reservas-de-clientes')
- }
+ navigate('/my-experiences')
}
return (
@@ -23,7 +19,7 @@ const DetailsCard: React.FC = ({ email, location, phone, role }) =>
const { field, icon, title } = deet
const data = dataMap[field as keyof typeof dataMap] || ''
- const adjustedTitle = field === 'bookings' && role === 'PROVIDER' ? 'Reservas de Clientes' : title
+ const adjustedTitle = field === 'bookings' && role.toLocaleUpperCase() === 'PROVIDER' ? 'Gestionar Reservas' : title
return (
diff --git a/src/consts/userProfileFields.ts b/src/consts/userProfileFields.ts
index efd5638..cab3c17 100644
--- a/src/consts/userProfileFields.ts
+++ b/src/consts/userProfileFields.ts
@@ -23,7 +23,7 @@ const detailsList = [
{
field: 'bookings',
icon: card,
- title: 'Reservas',
+ title: 'Mis experiencias',
},
]
diff --git a/src/routes/AppRoutes.tsx b/src/routes/AppRoutes.tsx
index 80b8bb1..71aa196 100644
--- a/src/routes/AppRoutes.tsx
+++ b/src/routes/AppRoutes.tsx
@@ -18,6 +18,10 @@ import ConfirmationView from '../components/ConfirmationView/ConfirmationView'
import { ProtectedPublicRoute } from './ProtectedPublicRoutes'
import { useLocation } from 'react-router-dom';
import { useEffect } from 'react'
+import ExperiencePanel from '../components/ExperiencePanel/ExperiencePanel'
+import BookingInfo from '../components/ExperiencePanel/BookingInfo/BookingInfo'
+import ManageCustomers from '../components/ExperiencePanel/ManageCustomers/ManageCustomers'
+import Reviews from '../components/Reviews/Reviews'
const AppRoutes = () => {
@@ -45,6 +49,11 @@ const AppRoutes = () => {
}>
} />
} />
+ } />
+ } />
+ } />
+ } />
+
{/* Rutas que necesitan contexto de reserva */}
@@ -61,6 +70,8 @@ const AppRoutes = () => {
+
+
404} />
@@ -69,3 +80,4 @@ const AppRoutes = () => {
export default AppRoutes
+
diff --git a/src/services/booking.services.ts b/src/services/booking.services.ts
index cef506b..76e3bf2 100644
--- a/src/services/booking.services.ts
+++ b/src/services/booking.services.ts
@@ -11,6 +11,20 @@ class BookingServices {
booking
)
}
+ async getBookingsFromUser(id: unknown) {
+ const responde = await this.api.get(`/user/${id}`);
+ return responde.data;
+ }
+
+ async getBookingsFromExperience(id: unknown) {
+ const responde = await this.api.get(`/experience/${id}`);
+ return responde.data;
+ }
+
+ async updateBooking(bookingId: unknown, data: unknown) {
+ await this.api.put(`/${bookingId}`, data);
+ }
+
}
const bookingServices = new BookingServices()
diff --git a/src/services/review.services.ts b/src/services/review.services.ts
new file mode 100644
index 0000000..794492e
--- /dev/null
+++ b/src/services/review.services.ts
@@ -0,0 +1,24 @@
+type CreateReviewResponse = {
+ status: number;
+ message: string;
+ };
+
+
+import createApiClient from "./apiClient"
+
+class ReviewServices {
+ private api = createApiClient(`${import.meta.env.VITE_API_URL}/reviews`)
+
+ async create(review: unknown): Promise {
+ const response = await this.api.post("/create", review);
+ return response.data;
+ }
+
+ async delete(review: unknown) {
+ await this.api.delete(`${review}`);
+ }
+}
+
+const reviewServices = new ReviewServices()
+
+export default reviewServices;
\ No newline at end of file
diff --git a/src/types/booking.ts b/src/types/booking.ts
new file mode 100644
index 0000000..55f6a57
--- /dev/null
+++ b/src/types/booking.ts
@@ -0,0 +1,13 @@
+export interface Booking {
+ bookingDate: string;
+ experienceTitle?: string;
+ createdAt: string;
+ experienceId: string;
+ id: string;
+ participants: number;
+ paymentStatus: string;
+ status: string;
+ totalPrice: number;
+ userId: string;
+ }
+
\ No newline at end of file
diff --git a/src/utils/getDateFormat.ts b/src/utils/getDateFormat.ts
index c21bbf2..a99002d 100644
--- a/src/utils/getDateFormat.ts
+++ b/src/utils/getDateFormat.ts
@@ -18,4 +18,18 @@ export function formatCategory(category: string | string[]) {
if (!categoryString) return '';
return categoryString.charAt(0).toUpperCase() + categoryString.slice(1);
- }
\ No newline at end of file
+}
+
+
+export const getStatus = (state: string) => {
+ switch (state) {
+ case "CONFIRMED":
+ return { text: "Confirmado", color: "text-green-600" };
+ case "CANCELLED":
+ return { text: "Cancelado", color: "text-red-600" };
+ case "PENDING":
+ return { text: "Pendiente", color: "text-gray-500" };
+ default:
+ return { text: "Desconocido", color: "text-gray-400" };
+ }
+ };
\ No newline at end of file