From 0474cf5cae01d7cc8201ceaec66a50b349f676e0 Mon Sep 17 00:00:00 2001 From: Andrea Cifuentes Date: Tue, 4 Nov 2025 15:27:27 -0500 Subject: [PATCH 01/12] Changes --- src/components/Slider.jsx | 61 ++++-- src/pages/Profile.jsx | 434 +++++++++++++++++++++++++++++++++----- src/pages/SignIn.jsx | 237 ++++++++++++++------- src/pages/SignUp.jsx | 242 +++++++++++++-------- tailwind.config.js | 24 +++ 5 files changed, 771 insertions(+), 227 deletions(-) diff --git a/src/components/Slider.jsx b/src/components/Slider.jsx index d2b4953..c1e3b7d 100644 --- a/src/components/Slider.jsx +++ b/src/components/Slider.jsx @@ -4,35 +4,55 @@ import React, { useState, useEffect, useCallback } from 'react'; export default function App() { const slideData = [ { - imgSrc: "https://www.fiestroevents.com/uploads/24/08/66b21e981eced0608241722949272.png", - title: "Elegant Wedding Receptions", - description: "Crafting unforgettable moments with stunning decor and seamless planning." + imgSrc: "https://images.unsplash.com/photo-1511795409834-ef04bbd61622?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mnx8YmlydGhkYXklMjBwYXJ0eXxlbnwwfHwwfHx8MA%3D%3D&w=1000&q=80", + title: "Cumpleaños", + description: "Celebra tu día especial con una fiesta inolvidable llena de alegría y diversión." }, { - imgSrc: "https://www.mrsfields.com/cdn/shop/articles/shutterstock_2408066691_1.jpg?v=1725556532", - title: "Joyful Birthday Parties", - description: "Celebrate another year with fun, laughter, and a party to remember." + imgSrc: "https://images.unsplash.com/photo-1519225421980-715cb0215aed?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mnx8d2VkZGluZ3xlbnwwfHwwfHx8MA%3D%3D&w=1000&q=80", + title: "Bodas", + description: "El día más importante de tu vida merece una celebración perfecta y mágica." }, { - imgSrc: "https://floweraura-blog-img.s3.ap-south-1.amazonaws.com/Celebrate+Anniversary+travel/cvr.jpg", - title: "Romantic Anniversaries", - description: "Mark your special milestone with a celebration of your love story." + imgSrc: "https://academia.unad.edu.co/images/2024/diplo2024.jpg", + title: "Grados", + description: "Celebra tus logros académicos con una ceremonia digna de tu esfuerzo y dedicación." }, { - imgSrc: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTzZg9yKSUy0oKJsxJZblexdfoMYhwUAtY2yA&s", - title: "Corporate Holiday Events", - description: "Boost team morale with a professionally organized and festive gathering." + imgSrc: "https://dulcesfiestas.com.co/wp-content/uploads/elementor/thumbs/2_Ideas-para-decorar-bautizos-y-primera-comunion-qpa0xxnlh99xaia43oqxwwzx48wliev1h99y57voew.png", + title: "Bautizos", + description: "Una celebración espiritual llena de amor para dar la bienvenida a la fe." }, { - imgSrc: "https://5.imimg.com/data5/SELLER/Default/2022/8/QF/JF/PJ/2713142/retirement-party-decoration-service.JPG", - title: "Honorable Retirement Parties", - description: "Celebrate a legacy of hard work and dedication with a fitting send-off." + imgSrc: "https://saketos.es/wp-content/uploads/2025/03/ninas-de-un-ano-3-1-jpg.webp", + title: "Fiesta Infantil", + description: "Diversión garantizada para los más pequeños con juegos, sorpresas y mucha alegría." }, { - imgSrc: "https://www.parents.com/thmb/JlKRSVpphFei1B17UE35pVnV5Fg=/1500x0/filters:no_upscale():max_bytes(150000):strip_icc()/shutterstock_762391612-41aca833e9184016833a754be5e7d5c3.jpg", - title: "Baby Shower Celebrations", - description: "Welcome the newest addition to the family with a beautiful and heartwarming event." + imgSrc: "https://m.media-amazon.com/images/I/71se54LSd8L._AC_UF1000,1000_QL80_.jpg", + title: "Baby Shower", + description: "Prepara la llegada del nuevo miembro de la familia con una celebración especial." }, + { + imgSrc: "https://www.mujerde10.com/wp-content/uploads/2025/07/estilo-de-vida-decoracion-para-xv-anos-tendencias--1024x678.jpg", + title: "Fiesta de 15 Años", + description: "Una noche mágica e inolvidable para celebrar esta importante transición." + }, + { + imgSrc: "https://cdn0.bodas.com.mx/usr/4/3/7/0/cfb_2x_104532.jpg", + title: "Despedida de Solter@", + description: "La última celebración de soltería llena de sorpresas y momentos divertidos." + }, + { + imgSrc: "https://images.unsplash.com/photo-1587825140708-dfaf72ae4b04?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mnx8Y29uZmVyZW5jZXxlbnwwfHwwfHx8MA%3D%3D&w=1000&q=80", + title: "Conferencias y Capacitaciones", + description: "Espacios profesionales para el aprendizaje, networking y desarrollo empresarial." + }, + { + imgSrc: "https://images.unsplash.com/photo-1542744173-8e7e53415bb0?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mnx8Y29ycG9yYXRlJTIwbWVldGluZ3xlbnwwfHwwfHx8MA%3D%3D&w=1000&q=80", + title: "Reunión Corporativa", + description: "Eventos empresariales que impulsan la productividad y el trabajo en equipo." + } ]; return ( @@ -42,7 +62,7 @@ export default function App() { ); } - +// Los componentes ChevronLeftIcon, ChevronRightIcon y EnhancedSlider se mantienen igual const ChevronLeftIcon = () => ( @@ -55,7 +75,7 @@ const ChevronRightIcon = () => ( ); - +// El componente EnhancedSlider se mantiene exactamente igual function EnhancedSlider({ slides }) { const [currentIndex, setCurrentIndex] = useState(0); @@ -139,4 +159,3 @@ function EnhancedSlider({ slides }) { ); } - diff --git a/src/pages/Profile.jsx b/src/pages/Profile.jsx index 6cbab53..c66e030 100644 --- a/src/pages/Profile.jsx +++ b/src/pages/Profile.jsx @@ -1,82 +1,416 @@ // src/pages/Profile.jsx - import { useEffect, useState } from "react"; -import { onAuthStateChanged } from "firebase/auth"; -import { auth } from "../firebase"; +import { onAuthStateChanged, signOut } from "firebase/auth"; +import { auth, db } from "../firebase"; +import { useNavigate } from "react-router-dom"; +import { toast } from "react-hot-toast"; +import { collection, query, where, getDocs, doc, getDoc } from "firebase/firestore"; export default function Profile() { const [user, setUser] = useState(null); + const [userData, setUserData] = useState(null); + const [reservations, setReservations] = useState([]); + const [payments, setPayments] = useState([]); + const [activeTab, setActiveTab] = useState("overview"); + const [isLoading, setIsLoading] = useState(true); + const navigate = useNavigate(); - // Fetch and set the authenticated user useEffect(() => { - const unsubscribe = onAuthStateChanged(auth, (currentUser) => { + const unsubscribe = onAuthStateChanged(auth, async (currentUser) => { setUser(currentUser); + if (currentUser) { + await fetchUserData(currentUser.uid); + await fetchUserReservations(currentUser.uid); + await fetchUserPayments(currentUser.uid); + } + setIsLoading(false); }); return () => unsubscribe(); }, []); + const fetchUserData = async (userId) => { + try { + const userDoc = await getDoc(doc(db, "users", userId)); + if (userDoc.exists()) { + setUserData(userDoc.data()); + } + } catch (error) { + console.error("Error fetching user data:", error); + } + }; + + const fetchUserReservations = async (userId) => { + try { + const q = query(collection(db, "reserves"), where("client.id", "==", userId)); + const querySnapshot = await getDocs(q); + const reservationsData = querySnapshot.docs.map(doc => ({ + id: doc.id, + ...doc.data() + })); + setReservations(reservationsData); + } catch (error) { + console.error("Error fetching reservations:", error); + } + }; + + const fetchUserPayments = async (userId) => { + try { + const userDoc = await getDoc(doc(db, "users", userId)); + if (userDoc.exists()) { + const userData = userDoc.data(); + setPayments(userData.payments || []); + } + } catch (error) { + console.error("Error fetching payments:", error); + } + }; + + const handleLogout = async () => { + try { + await signOut(auth); + toast.success("¡Sesión cerrada exitosamente!"); + navigate("/"); + } catch (error) { + toast.error("Error al cerrar sesión"); + } + }; + + const calculateTotalSpent = () => { + return payments.reduce((total, payment) => total + (payment.totalCost || 0), 0); + }; + + const getUpcomingEvents = () => { + const today = new Date(); + return reservations.filter(reservation => + new Date(reservation.dates?.[0]) >= today + ); + }; + + if (isLoading) { + return ( +
+
+
+

Cargando perfil...

+
+
+ ); + } + if (!user) { return ( -
- Loading profile... +
+
+
+ + + +
+

Usuario no encontrado

+

Inicia sesión para ver tu perfil

+ +
); } - return ( -
-
+ const upcomingEvents = getUpcomingEvents(); + return ( +
+
{/* Profile Header */} -
- User -
-

{user.displayName || "Your Name"}

-

Registered User

+
+
+
+
+ User +
+ +
+

+ {user.displayName || userData?.name || "Usuario"} +

+

{user.email}

+

{userData?.phone || "Teléfono no registrado"}

+
+ +
+
+

{reservations.length}

+

Reservas

+
+
+

{upcomingEvents.length}

+

Próximos

+
+
+

${calculateTotalSpent()}

+

Total Gastado

+
+
+
-
- {/* Divider */} -
- - {/* Email and Details */} -
-

- Email: {user.email} -

-

- User Type: Regular User - -

-

- Member Since: {" "} - {new Date(user.metadata?.creationTime).toLocaleDateString()} -

+ {/* Navigation Tabs */} +
+ +
+ {/* Tab Content */} +
+ {activeTab === "overview" && ( +
+

Resumen de Actividad

+ + {/* Upcoming Events */} +
+

Próximos Eventos

+ {upcomingEvents.length > 0 ? ( +
+ {upcomingEvents.slice(0, 3).map((reservation) => ( +
+
+
+

{reservation.event?.type}

+

+ {reservation.dates?.[0] && new Date(reservation.dates[0]).toLocaleDateString('es-ES')} +

+
+ + ${reservation.totalCost || 0} + +
+
+ ))} +
+ ) : ( +

No tienes eventos próximos

+ )} +
+ {/* Recent Payments */} +
+

Pagos Recientes

+ {payments.length > 0 ? ( +
+ {payments.slice(0, 3).map((payment, index) => ( +
+
+
+

Pago #{payment.id?.slice(0, 8)}

+

+ {payment.coveredServices?.length || 0} servicios +

+
+ + ${payment.totalCost || 0} + +
+
+ ))} +
+ ) : ( +

No hay historial de pagos

+ )} +
+
+ )} - {/* Action Buttons */} -
- - - + {/* Services Summary */} +
+

Servicios Contratados:

+
+ {reservation.services?.entertainment?.map((service, idx) => ( + + Entretenimiento + + ))} + {reservation.services?.decoration && ( + + Decoración + + )} + {reservation.services?.catering?.map((service, idx) => ( + + Catering + + ))} +
+
+
+ ))} +
+ ) : ( +
+

No tienes reservas aún

+ +
+ )} +
+ )} + + {activeTab === "payments" && ( +
+

Historial de Pagos

+ {payments.length > 0 ? ( +
+ {payments.map((payment, index) => ( +
+
+
+

Pago #{payment.id?.slice(0, 8) || `P${index + 1}`}

+

+ {payment.coveredServices?.join(", ") || "Servicios varios"} +

+
+ + ${payment.totalCost || 0} + +
+
+ Fecha del pago + Completado +
+
+ ))} +
+ ) : ( +

No hay historial de pagos

+ )} +
+ )} + + {activeTab === "settings" && ( +
+

Configuración

+
+
+

Información Personal

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+ +
+
+
+ )}
); -} +} \ No newline at end of file diff --git a/src/pages/SignIn.jsx b/src/pages/SignIn.jsx index 7228624..d120a06 100644 --- a/src/pages/SignIn.jsx +++ b/src/pages/SignIn.jsx @@ -1,18 +1,19 @@ import { useState } from "react"; import { FcGoogle } from "react-icons/fc"; -import { FaFacebook } from "react-icons/fa"; +import { FaFacebook, FaEye, FaEyeSlash } from "react-icons/fa"; import { signInWithEmailAndPassword, signInWithPopup, GoogleAuthProvider } from "firebase/auth"; import { auth } from "../firebase"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, Link } from "react-router-dom"; import { toast } from "react-hot-toast"; export default function SignIn() { const navigate = useNavigate(); - const [formData, setFormData] = useState({ email: "", password: "" }); + const [showPassword, setShowPassword] = useState(false); + const [isLoading, setIsLoading] = useState(false); const handleChange = (e) => { const { name, value } = e.target; @@ -25,105 +26,195 @@ export default function SignIn() { // Email/Password Sign-in const handleEmailSignIn = async (e) => { e.preventDefault(); + setIsLoading(true); try { await signInWithEmailAndPassword(auth, formData.email, formData.password); - toast.success("Signed in successfully!"); + toast.success("¡Bienvenido de vuelta!"); navigate("/"); } catch (error) { - toast.error("Invalid credentials"); + toast.error("Credenciales inválidas"); console.error(error.message); + } finally { + setIsLoading(false); } }; // Google Sign-in const handleGoogleSignIn = async () => { + setIsLoading(true); const provider = new GoogleAuthProvider(); try { await signInWithPopup(auth, provider); - toast.success("Signed in with Google"); + toast.success("¡Inicio de sesión exitoso con Google!"); navigate("/"); } catch (error) { - toast.error("Google sign-in failed"); + toast.error("Error al iniciar con Google"); console.error(error.message); + } finally { + setIsLoading(false); } }; return ( -
-
-

Sign In

+
+ {/* Background Decorations */} +
+
+
+
+
- {/* Email/Password Sign-in Form */} -
-
- - +
+ {/* Card Container */} +
+ {/* Header */} +
+
+ E +
+

+ Bienvenido +

+

Inicia sesión en tu cuenta

-
- - -
+ {/* Email/Password Form */} + +
+ +
+ +
+
- {/* Remember Me + Forgot */} -
- - Forgot Password? -
+
+ +
+ + +
+
- - + {/* Remember Me + Forgot Password */} +
+ + + ¿Olvidaste tu contraseña? + +
-
-
- OR -
-
+ {/* Submit Button */} + + + {/* Divider */} +
+
+ O continúa con +
+
- {/* OAuth Buttons */} -
- -
+ {/* OAuth Buttons */} +
+ + + +
-

- Don’t have an account?{" "} - - Sign up - -

+ {/* Sign Up Link */} +

+ ¿No tienes una cuenta?{" "} + + Regístrate aquí + +

+
+ + {/* Add this to your CSS or Tailwind config for animations */} +
); -} +} \ No newline at end of file diff --git a/src/pages/SignUp.jsx b/src/pages/SignUp.jsx index 390649a..a9aa189 100644 --- a/src/pages/SignUp.jsx +++ b/src/pages/SignUp.jsx @@ -1,6 +1,7 @@ import { useState } from "react"; import { Link, useNavigate } from "react-router-dom"; import { FcGoogle } from "react-icons/fc"; +import { FaEye, FaEyeSlash } from "react-icons/fa"; import { createUserWithEmailAndPassword, updateProfile, @@ -16,6 +17,8 @@ export default function SignUp() { email: "", password: "", }); + const [showPassword, setShowPassword] = useState(false); + const [isLoading, setIsLoading] = useState(false); const navigate = useNavigate(); @@ -31,6 +34,7 @@ export default function SignUp() { // Handle form submission (Email/Password sign-up) const handleSubmit = async (e) => { e.preventDefault(); + setIsLoading(true); try { const userCredential = await createUserWithEmailAndPassword( auth, @@ -40,115 +44,187 @@ export default function SignUp() { await updateProfile(userCredential.user, { displayName: formData.name, }); - toast.success("Account created successfully!"); - navigate("/"); // Redirect to home or profile + toast.success("¡Cuenta creada exitosamente! 🎉"); + navigate("/"); } catch (error) { toast.error(error.message); + } finally { + setIsLoading(false); } }; // Google sign-in logic const handleGoogleSignIn = async () => { + setIsLoading(true); const provider = new GoogleAuthProvider(); try { await signInWithPopup(auth, provider); - toast.success("Signed in with Google"); + toast.success("¡Registro con Google exitoso! 🎉"); navigate("/"); } catch (error) { toast.error(error.message); + } finally { + setIsLoading(false); } }; return ( -
-
-

- Create an Account -

+
+ {/* Background Decorations */} +
+
+
+
+
-
-
- - +
+ {/* Card Container */} +
+ {/* Header */} +
+
+ E +
+

+ Crear Cuenta +

+

Únete a nuestra comunidad

-
- - -
+ {/* Registration Form */} + + {/* Name Field */} +
+ +
+ +
+
-
- - -
+ {/* Email Field */} +
+ +
+ +
+
- - + {/* Password Field */} +
+ +
+ + +
+

+ Mínimo 6 caracteres +

+
-
-
- OR -
-
+ {/* Terms and Conditions */} +
+ + +
+ {/* Submit Button */} + + - {/* Google Sign In */} -
- -
+ {/* Divider */} +
+
+ O regístrate con +
+
- {/* Link to Login */} -

- Already have an account? - - - Log in - - -

+ {/* OAuth Buttons */} +
+ +
+ + {/* Login Link */} +

+ ¿Ya tienes una cuenta?{" "} + + Inicia sesión aquí + +

+
); -} +} \ No newline at end of file diff --git a/tailwind.config.js b/tailwind.config.js index 2040454..a4c8e73 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -11,6 +11,30 @@ export default { script: ['Dancing Script', 'cursive'], satisfy: ['Satisfy', 'cursive'], }, + // Agrega estas animaciones aquí + animation: { + blob: "blob 7s infinite", + }, + keyframes: { + blob: { + "0%": { + transform: "translate(0px, 0px) scale(1)", + }, + "33%": { + transform: "translate(30px, -50px) scale(1.1)", + }, + "66%": { + transform: "translate(-20px, 20px) scale(0.9)", + }, + "100%": { + transform: "translate(0px, 0px) scale(1)", + }, + }, + }, + animationDelay: { + 2000: "2000ms", + 4000: "4000ms", + } }, }, plugins: [], From 2448723d9e69759bbbb4c57ced76d502f8d28aa8 Mon Sep 17 00:00:00 2001 From: Andrea Cifuentes <108103954+AndreaCifuentess@users.noreply.github.com> Date: Tue, 4 Nov 2025 15:48:31 -0500 Subject: [PATCH 02/12] Update README.md --- README.md | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/README.md b/README.md index 0a3dbf5..0485c1c 100644 --- a/README.md +++ b/README.md @@ -56,35 +56,11 @@ npm install npm run dev ``` ---- - -## 📸 Screenshots - -### 🔹 Home page -![Home page](src/assets/Home_page_before_login.png) - -### 🔹 Home page Logged In -![Home page](src/assets/Home_page_after_login.png) - -### 🔹 Services -![services](src/assets/services_page.png) - --- - -## 🌐 Live Demo -👉 [https://neweventique.netlify.app](https://neweventique.netlify.app) - ---- - -## 🙌 Acknowledgements - -Built with ❤️ using React, Vite, and Tailwind CSS as part of a personal web development project. - ---- ## ⭐ Show Your Support If you like this project, give it a ⭐ on GitHub or share it! --- + From c80d6433166319f60f2957ba777faccace392c1c Mon Sep 17 00:00:00 2001 From: Andrea Cifuentes <108103954+AndreaCifuentess@users.noreply.github.com> Date: Tue, 4 Nov 2025 15:49:13 -0500 Subject: [PATCH 03/12] Update README.md --- README.md | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/README.md b/README.md index 0485c1c..5696ffd 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,6 @@ # 🎉 Eventique – Event Management Website -Welcome to **Eventique**, a stylish and modern event management platform built using React + Vite + Tailwind CSS + Firebase. -Live Demo: 👉 [https://neweventique.netlify.app](https://neweventique.netlify.app) - ---- - ## 🌟 Overview Eventique is a fast, elegant, and responsive event management frontend built with modern tools. @@ -57,10 +52,3 @@ npm run dev ``` - -## ⭐ Show Your Support - -If you like this project, give it a ⭐ on GitHub or share it! - ---- - From 1804d7cdbf5c361307e7efe1242ec82abcdbf95f Mon Sep 17 00:00:00 2001 From: Andrea Cifuentes Date: Wed, 12 Nov 2025 00:27:39 -0500 Subject: [PATCH 04/12] CORS configuration for frontend-backend communication --- src/api/auth.js | 10 +++ src/api/axios.js | 14 +++++ src/components/Slider.jsx | 2 +- src/pages/SignUp.jsx | 126 ++++++++++++++++++++++++-------------- 4 files changed, 105 insertions(+), 47 deletions(-) create mode 100644 src/api/auth.js create mode 100644 src/api/axios.js diff --git a/src/api/auth.js b/src/api/auth.js new file mode 100644 index 0000000..9e28fb7 --- /dev/null +++ b/src/api/auth.js @@ -0,0 +1,10 @@ +import api from './axios'; + +export const registerRequest = { + login: (email, password) => + api.post('/User/login', { email, password }), + + register: (userData) => + api.post('/User/register', userData), + +}; \ No newline at end of file diff --git a/src/api/axios.js b/src/api/axios.js new file mode 100644 index 0000000..63f2d39 --- /dev/null +++ b/src/api/axios.js @@ -0,0 +1,14 @@ +import axios from 'axios'; + + +const API_BASE_URL = 'http://localhost:8081'; + +const api = axios.create({ + baseURL: API_BASE_URL, + timeout: 10000, + headers: { + 'Content-Type': 'application/json', + } +}); + +export default api; \ No newline at end of file diff --git a/src/components/Slider.jsx b/src/components/Slider.jsx index c1e3b7d..3dd2c10 100644 --- a/src/components/Slider.jsx +++ b/src/components/Slider.jsx @@ -24,7 +24,7 @@ export default function App() { description: "Una celebración espiritual llena de amor para dar la bienvenida a la fe." }, { - imgSrc: "https://saketos.es/wp-content/uploads/2025/03/ninas-de-un-ano-3-1-jpg.webp", + imgSrc: "https://ladomingaeventos.com/wp-content/uploads/2013/11/fiestaninos2.jpg", title: "Fiesta Infantil", description: "Diversión garantizada para los más pequeños con juegos, sorpresas y mucha alegría." }, diff --git a/src/pages/SignUp.jsx b/src/pages/SignUp.jsx index a9aa189..8b8299f 100644 --- a/src/pages/SignUp.jsx +++ b/src/pages/SignUp.jsx @@ -2,20 +2,16 @@ import { useState } from "react"; import { Link, useNavigate } from "react-router-dom"; import { FcGoogle } from "react-icons/fc"; import { FaEye, FaEyeSlash } from "react-icons/fa"; -import { - createUserWithEmailAndPassword, - updateProfile, - signInWithPopup, - GoogleAuthProvider, -} from "firebase/auth"; -import { auth } from "../firebase"; +import { registerRequest } from "../api/auth"; // ← Importar tu servicio import { toast } from "react-hot-toast"; export default function SignUp() { const [formData, setFormData] = useState({ - name: "", + fullName: "", email: "", password: "", + phone: "", // ← Agregar teléfono + city: "" // ← Agregar ciudad }); const [showPassword, setShowPassword] = useState(false); const [isLoading, setIsLoading] = useState(false); @@ -31,41 +27,43 @@ export default function SignUp() { })); }; - // Handle form submission (Email/Password sign-up) - const handleSubmit = async (e) => { - e.preventDefault(); - setIsLoading(true); - try { - const userCredential = await createUserWithEmailAndPassword( - auth, - formData.email, - formData.password - ); - await updateProfile(userCredential.user, { - displayName: formData.name, - }); - toast.success("¡Cuenta creada exitosamente! 🎉"); - navigate("/"); - } catch (error) { - toast.error(error.message); - } finally { - setIsLoading(false); - } - }; + // Handle form submission (Spring Boot sign-up) +const handleSubmit = async (e) => { + e.preventDefault(); + console.log("🎯 handleSubmit EJECUTÁNDOSE - Formulario enviado"); // ← AGREGA ESTA LÍNEA + setIsLoading(true); + + try { + console.log("📝 Datos a enviar:", { // ← Y ESTA TAMBIÉN + fullName: formData.fullName, + email: formData.email, + phone: formData.phone, + city: formData.city + }); + + await registerRequest.register({ + fullName: formData.fullName, + email: formData.email, + password: formData.password, + phone: formData.phone, + city: formData.city, + type: "CLIENTE" + }); + + toast.success("¡Cuenta creada exitosamente! 🎉"); + navigate("/sign-in"); + } catch (error) { + console.error("Error en registro:", error); + toast.error(error.response?.data?.message || "Error al crear la cuenta"); + } finally { + setIsLoading(false); + } +}; - // Google sign-in logic + // Google sign-in - REMOVER o implementar con Spring Boot después const handleGoogleSignIn = async () => { - setIsLoading(true); - const provider = new GoogleAuthProvider(); - try { - await signInWithPopup(auth, provider); - toast.success("¡Registro con Google exitoso! 🎉"); - navigate("/"); - } catch (error) { - toast.error(error.message); - } finally { - setIsLoading(false); - } + toast.error("Registro con Google no disponible temporalmente"); + // Si quieres mantenerlo, necesitarás implementar OAuth2 en Spring Boot }; return ( @@ -101,8 +99,8 @@ export default function SignUp() {
+ {/* Phone Field */} +
+ +
+ +
+
+ + {/* City Field */} +
+ +
+ +
+
+ {/* Password Field */}

- Mínimo 6 caracteres + Mínimo 8 caracteres, una mayúscula, un número y un carácter especial

@@ -193,14 +227,14 @@ export default function SignUp() { - {/* Divider */} + {/* Divider y Google - Opcional remover */}
O regístrate con
- {/* OAuth Buttons */} + {/* OAuth Buttons - Opcional */}
+ {/* Snackbar Personalizado */} + {snackbar.open && ( +
+
+
+
+ {snackbar.type === 'success' ? ( + + ) : ( + + )} +
+
+

{snackbar.message}

+
+ +
+
+
+ )} +
{/* Card Container */}
@@ -187,7 +261,7 @@ const handleSubmit = async (e) => {

- Mínimo 8 caracteres, una mayúscula, un número y un carácter especial + Mínimo 8 caracteres, una mayúscula, un número y un carácter especial (@$!%*?&)

@@ -227,14 +301,14 @@ const handleSubmit = async (e) => { - {/* Divider y Google - Opcional remover */} + {/* Divider y Google */}
O regístrate con
- {/* OAuth Buttons - Opcional */} + {/* OAuth Buttons */}
- {/* Full screen overlay menu */} + {/* Menú móvil (full screen overlay) */}
    -
  • Home
  • -
  • Categories
  • -
  • Services
  • -
  • Contact
  • +
  • Home
  • +
  • Categories
  • +
  • Services
  • +
  • Contact
- {!user ? ( + {!isAuthenticated ? ( <> Log in @@ -97,19 +93,29 @@ export default function Navbar() { ) : ( <> My Cart - - Profile - + {user?.type === "ADMIN" ? ( + + Dashboard + + ) : ( + + {user?.fullName || "Profile"} + + )}
- {/* Desktop menu */} + {/* Menú desktop */}
  • Home
  • Categories
  • @@ -130,8 +136,9 @@ export default function Navbar() {
  • Contact
+ {/* Botones desktop */}
- {!user ? ( + {!isAuthenticated ? ( <> ) : ( <> + + 👋 Hola, {user?.fullName || user?.email || "Usuario"} + - My Cart - - - Profile + 🛒 Cart + {user?.type === "ADMIN" ? ( + + 🛠️ Dashboard + + ) : ( + + 👤 Profile + + )} +
+
+
+ ); +}; + +export default Snackbar; \ No newline at end of file diff --git a/src/context/AuthContext.jsx b/src/context/AuthContext.jsx index f9987dc..d42e58d 100644 --- a/src/context/AuthContext.jsx +++ b/src/context/AuthContext.jsx @@ -1,30 +1,90 @@ -import { createContext, useContext, useEffect, useState } from "react"; -import { auth } from "../firebase"; -import { onAuthStateChanged } from "firebase/auth"; +import { createContext, useState, useEffect } from "react"; +import { jwtDecode } from "jwt-decode"; -// Create context const AuthContext = createContext(); +export { AuthContext } -// Hook to use context -export function useAuth() { - return useContext(AuthContext); -} - -// Context Provider -export function AuthProvider({ children }) { +export const AuthProvider = ({ children }) => { const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); useEffect(() => { - const unsubscribe = onAuthStateChanged(auth, currentUser => { - setUser(currentUser); - }); - - return () => unsubscribe(); + const token = localStorage.getItem("token"); + console.log("🔄 AuthProvider - Cargando estado inicial"); + console.log("🔑 Token en localStorage:", token ? "SÍ" : "NO"); + if (token) { + try { + const decodedToken = jwtDecode(token); + console.log("🎫 Token decodificado:", decodedToken); + + // Verificar si el token no ha expirado + if (decodedToken.exp * 1000 < Date.now()) { + console.log("⏰ Token expirado"); + localStorage.removeItem("token"); + localStorage.removeItem("user"); + } else { + // Si el token es válido, establecer el usuario + const userData = localStorage.getItem("user"); + console.log("📦 User data en localStorage:", userData); + if (userData) { + const parsedUser = JSON.parse(userData); + console.log("👤 Usuario parseado:", parsedUser); + console.log("🎯 Tipo de usuario (desde localStorage):", parsedUser?.type); + // Verificamos que el usuario tenga un tipo, si no, podría ser un error + if (parsedUser && parsedUser.type) { + setUser(parsedUser); + } else { + console.warn("El usuario no tiene tipo definido, no se establecerá el estado de autenticación."); + } + } else { + console.log("No hay user en localStorage"); + } + } + } catch (error) { + console.error("❌ Error decodificando el token:", error); + localStorage.removeItem("token"); + localStorage.removeItem("user"); + } + } + setLoading(false); }, []); + const login = (token, userData) => { + console.log("🔥 AuthContext.login() llamado"); + console.log("🔑 Token recibido:", token ? "SÍ (longitud: " + token.length + ")" : "NO"); + console.log("👤 UserData recibido:", userData); + console.log("🎯 Tipo de usuario recibido:", userData?.type); + + localStorage.setItem("token", token); + localStorage.setItem("user", JSON.stringify(userData)); + setUser(userData); + + // Verificar que se guardó correctamente + const savedUser = JSON.parse(localStorage.getItem('user')); + console.log("💾 Usuario guardado en localStorage:", savedUser); + }; + + const logout = () => { + console.log("👋 AuthContext.logout() llamado"); + localStorage.removeItem("token"); + localStorage.removeItem("user"); + setUser(null); + }; + + const value = { + user, + login, + logout, + isAuthenticated: !!user, + loading + }; + + console.log("🔄 AuthProvider render - user:", user); + console.log("🔐 AuthProvider render - isAuthenticated:", !!user); + return ( - + {children} ); -} +}; \ No newline at end of file diff --git a/src/hooks/useAuth.jsx b/src/hooks/useAuth.jsx new file mode 100644 index 0000000..46ae026 --- /dev/null +++ b/src/hooks/useAuth.jsx @@ -0,0 +1,10 @@ +import { useContext } from "react"; +import { AuthContext } from "../context/AuthContext"; + +export const useAuth = () => { + const context = useContext(AuthContext); + if (!context) { + throw new Error("useAuth debe ser usado dentro de un AuthProvider"); + } + return context; +}; \ No newline at end of file diff --git a/src/hooks/useSnackbar.jsx b/src/hooks/useSnackbar.jsx new file mode 100644 index 0000000..17b8815 --- /dev/null +++ b/src/hooks/useSnackbar.jsx @@ -0,0 +1,76 @@ +import { useState, useCallback, useEffect } from 'react'; + +const useSnackbar = () => { + const [snackbar, setSnackbar] = useState({ + message: '', + type: 'success', + isVisible: false, + }); + + const showSnackbar = useCallback((message, type = 'success') => { + setSnackbar({ + message, + type, + isVisible: true, + }); + }, []); + + const hideSnackbar = useCallback(() => { + setSnackbar(prev => ({ ...prev, isVisible: false })); + }, []); + + // Efecto para auto-ocultar el snackbar + useEffect(() => { + if (snackbar.isVisible) { + const timer = setTimeout(() => { + hideSnackbar(); + }, 4000); // Duración de 4 segundos + + return () => clearTimeout(timer); + } + }, [snackbar.isVisible, hideSnackbar]); + + const SnackbarComponent = useCallback(({ position = 'top' }) => { + if (!snackbar.isVisible) return null; + + const positionClass = position === 'top' + ? 'top-4' + : position === 'bottom' + ? 'bottom-4' + : 'top-4'; + + const bgColor = snackbar.type === 'success' + ? 'bg-green-500' + : snackbar.type === 'error' + ? 'bg-red-500' + : 'bg-blue-500'; + + return ( +
+
+ + {snackbar.type === 'success' ? '✅' : '❌'} + + {snackbar.message} + +
+
+ ); + }, [snackbar, hideSnackbar]); + + return { + showSnackbar, + hideSnackbar, + SnackbarComponent, + snackbarMessage: snackbar.message, + snackbarType: snackbar.type, + isSnackbarVisible: snackbar.isVisible, + }; +}; + +export default useSnackbar; \ No newline at end of file diff --git a/src/main.jsx b/src/main.jsx index b9a1a6d..4c2a1c6 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,10 +1,16 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' +import { BrowserRouter } from 'react-router-dom' +import { AuthProvider } from './context/AuthContext' import './index.css' import App from './App.jsx' createRoot(document.getElementById('root')).render( - + + + + + , -) +) \ No newline at end of file diff --git a/src/pages/Profile.jsx b/src/pages/Profile.jsx index c66e030..d72523e 100644 --- a/src/pages/Profile.jsx +++ b/src/pages/Profile.jsx @@ -1,13 +1,12 @@ // src/pages/Profile.jsx import { useEffect, useState } from "react"; -import { onAuthStateChanged, signOut } from "firebase/auth"; -import { auth, db } from "../firebase"; import { useNavigate } from "react-router-dom"; import { toast } from "react-hot-toast"; -import { collection, query, where, getDocs, doc, getDoc } from "firebase/firestore"; +import { useAuth } from "../hooks/useAuth"; +import { getUserProfile, getUserReservations, getUserPayments } from "../api/user"; export default function Profile() { - const [user, setUser] = useState(null); + const { user, logout, isAuthenticated } = useAuth(); const [userData, setUserData] = useState(null); const [reservations, setReservations] = useState([]); const [payments, setPayments] = useState([]); @@ -16,59 +15,41 @@ export default function Profile() { const navigate = useNavigate(); useEffect(() => { - const unsubscribe = onAuthStateChanged(auth, async (currentUser) => { - setUser(currentUser); - if (currentUser) { - await fetchUserData(currentUser.uid); - await fetchUserReservations(currentUser.uid); - await fetchUserPayments(currentUser.uid); + const fetchUserData = async () => { + if (!user) { + setIsLoading(false); + return; } - setIsLoading(false); - }); - - return () => unsubscribe(); - }, []); - const fetchUserData = async (userId) => { - try { - const userDoc = await getDoc(doc(db, "users", userId)); - if (userDoc.exists()) { - setUserData(userDoc.data()); + try { + setIsLoading(true); + + // Obtener datos del perfil + const profileResponse = await getUserProfile(); + setUserData(profileResponse.data); + + // Obtener reservas del usuario + const reservationsResponse = await getUserReservations(); + setReservations(reservationsResponse.data || []); + + // Obtener pagos del usuario + const paymentsResponse = await getUserPayments(); + setPayments(paymentsResponse.data || []); + + } catch (error) { + console.error("Error fetching user data:", error); + toast.error("Error al cargar los datos del perfil"); + } finally { + setIsLoading(false); } - } catch (error) { - console.error("Error fetching user data:", error); - } - }; + }; - const fetchUserReservations = async (userId) => { - try { - const q = query(collection(db, "reserves"), where("client.id", "==", userId)); - const querySnapshot = await getDocs(q); - const reservationsData = querySnapshot.docs.map(doc => ({ - id: doc.id, - ...doc.data() - })); - setReservations(reservationsData); - } catch (error) { - console.error("Error fetching reservations:", error); - } - }; - - const fetchUserPayments = async (userId) => { - try { - const userDoc = await getDoc(doc(db, "users", userId)); - if (userDoc.exists()) { - const userData = userDoc.data(); - setPayments(userData.payments || []); - } - } catch (error) { - console.error("Error fetching payments:", error); - } - }; + fetchUserData(); + }, [user]); const handleLogout = async () => { try { - await signOut(auth); + logout(); toast.success("¡Sesión cerrada exitosamente!"); navigate("/"); } catch (error) { @@ -77,14 +58,15 @@ export default function Profile() { }; const calculateTotalSpent = () => { - return payments.reduce((total, payment) => total + (payment.totalCost || 0), 0); + return payments.reduce((total, payment) => total + (payment.total || payment.totalCost || 0), 0); }; const getUpcomingEvents = () => { const today = new Date(); - return reservations.filter(reservation => - new Date(reservation.dates?.[0]) >= today - ); + return reservations.filter(reservation => { + const eventDate = reservation.date || reservation.eventDate || reservation.dates?.[0]; + return eventDate && new Date(eventDate) >= today; + }); }; if (isLoading) { @@ -98,7 +80,7 @@ export default function Profile() { ); } - if (!user) { + if (!isAuthenticated || !user) { return (
@@ -107,7 +89,7 @@ export default function Profile() {
-

Usuario no encontrado

+

Acceso no autorizado

Inicia sesión para ver tu perfil

+
diff --git a/src/pages/SignIn.jsx b/src/pages/SignIn.jsx index d120a06..7e4c2cf 100644 --- a/src/pages/SignIn.jsx +++ b/src/pages/SignIn.jsx @@ -1,13 +1,14 @@ import { useState } from "react"; import { FcGoogle } from "react-icons/fc"; -import { FaFacebook, FaEye, FaEyeSlash } from "react-icons/fa"; -import { signInWithEmailAndPassword, signInWithPopup, GoogleAuthProvider } from "firebase/auth"; -import { auth } from "../firebase"; +import { FaEye, FaEyeSlash } from "react-icons/fa"; +import { loginRequest } from "../api/auth"; import { useNavigate, Link } from "react-router-dom"; import { toast } from "react-hot-toast"; +import { useAuth } from "../hooks/useAuth"; export default function SignIn() { const navigate = useNavigate(); + const { login } = useAuth(); const [formData, setFormData] = useState({ email: "", password: "" @@ -23,38 +24,50 @@ export default function SignIn() { })); }; - // Email/Password Sign-in - const handleEmailSignIn = async (e) => { - e.preventDefault(); - setIsLoading(true); - try { - await signInWithEmailAndPassword(auth, formData.email, formData.password); - toast.success("¡Bienvenido de vuelta!"); - navigate("/"); - } catch (error) { - toast.error("Credenciales inválidas"); - console.error(error.message); - } finally { - setIsLoading(false); - } - }; + +const handleEmailSignIn = async (e) => { + e.preventDefault(); + console.log(" Iniciando login..."); + + if (!formData.email || !formData.password) { + toast.error("Por favor completa todos los campos"); + setIsLoading(false); + return; + } - // Google Sign-in - const handleGoogleSignIn = async () => { - setIsLoading(true); - const provider = new GoogleAuthProvider(); - try { - await signInWithPopup(auth, provider); - toast.success("¡Inicio de sesión exitoso con Google!"); - navigate("/"); - } catch (error) { - toast.error("Error al iniciar con Google"); - console.error(error.message); - } finally { - setIsLoading(false); + try { + const response = await loginRequest(formData.email, formData.password); + console.log(" Respuesta completa del login:", response.data); + + if (response.data.token && response.data.user) { + + login(response.data.token, response.data.user); + + + localStorage.setItem("token", response.data.token); + localStorage.setItem("user", JSON.stringify(response.data.user)); + + console.log(" Redirigiendo según tipo:", response.data.user.type); + + // Redirigir según el tipo + if (response.data.user.type === "ADMIN") { + toast.success("¡Bienvenido Administrador!"); + navigate("/admin/dashboard"); + } else { + toast.success("¡Bienvenido de vuelta!"); + navigate("/"); + } + } else { + toast.error("Error: respuesta del servidor incompleta"); } - }; + } catch (error) { + console.log(" Errores"); + } finally { + setIsLoading(false); + } +}; + return (
{/* Background Decorations */} @@ -162,28 +175,7 @@ export default function SignIn() {
- {/* OAuth Buttons */} -
- - - -
- + {/* Sign Up Link */}

¿No tienes una cuenta?{" "} @@ -196,25 +188,6 @@ export default function SignIn() {

- - {/* Add this to your CSS or Tailwind config for animations */} -
); } \ No newline at end of file diff --git a/src/pages/SignUp.jsx b/src/pages/SignUp.jsx index 3adaf6d..8da9589 100644 --- a/src/pages/SignUp.jsx +++ b/src/pages/SignUp.jsx @@ -1,6 +1,5 @@ import { useState } from "react"; import { Link, useNavigate } from "react-router-dom"; -import { FcGoogle } from "react-icons/fc"; import { FaEye, FaEyeSlash } from "react-icons/fa"; import { registerRequest } from "../api/auth"; @@ -32,7 +31,6 @@ export default function SignUp() { })); }; - // Snackbar const showSnackbar = (message, type = "success") => { setSnackbar({ @@ -59,18 +57,18 @@ export default function SignUp() { const handleSubmit = async (e) => { e.preventDefault(); - console.log("🎯 Formulario enviándose"); + console.log(" Formulario enviándose"); setIsLoading(true); try { - console.log("📝 Datos a enviar:", { + console.log(" Datos a enviar:", { fullName: formData.fullName, email: formData.email, phone: formData.phone, city: formData.city }); - await registerRequest.register({ + await registerRequest({ fullName: formData.fullName, email: formData.email, password: formData.password, @@ -103,10 +101,6 @@ export default function SignUp() { } }; - // Google sign-in - const handleGoogleSignIn = async () => { - showSnackbar("Registro con Google no disponible temporalmente", "error"); - }; return (
@@ -301,25 +295,7 @@ export default function SignUp() { - {/* Divider y Google */} -
-
- O regístrate con -
-
- - {/* OAuth Buttons */} -
- -
+ {/* Login Link */}

diff --git a/src/pages/admin/Layout.jsx b/src/pages/admin/Layout.jsx new file mode 100644 index 0000000..6b9970e --- /dev/null +++ b/src/pages/admin/Layout.jsx @@ -0,0 +1,155 @@ +import { Outlet, NavLink, useNavigate } from 'react-router-dom'; +import { useContext } from 'react'; +import { AuthContext } from '../../context/AuthContext'; +export default function AdminLayout() { + const { user } = useContext(AuthContext); + const navigate = useNavigate(); + + const navItems = [ + { path: '/admin/dashboard', label: 'Dashboard', exact: true, icon: '📊' }, + { path: '/admin/establishments', label: 'Establecimientos', icon: '🏢' }, + { path: '/admin/events', label: 'Eventos', icon: '📅' }, + { path: '/admin/entertainment', label: 'Entretenimiento', icon: '🎭' }, + { path: '/admin/catering', label: 'Catering', icon: '🍽️' }, + { path: '/admin/services', label: 'Servicios', icon: '🔧' }, + { path: '/admin/users', label: 'Usuarios', icon: '👥' }, + ]; + + const quickCreateItems = [ + { path: '/admin/establishments/create', label: 'Establecimiento', icon: '🏢' }, + { path: '/admin/events/create', label: 'Evento', icon: '📅' }, + { path: '/admin/entertainment/create', label: 'Entretenimiento', icon: '🎭' }, + { path: '/admin/catering/create', label: 'Catering', icon: '🍽️' }, + { path: '/admin/services/create', label: 'Servicio', icon: '🔧' }, + ]; + + // Verificar si el usuario está autenticado + if (!user) { + // Redirigir al login si no está autenticado + navigate('/login'); + return null; + } + + return ( +

+ {/* Sidebar */} +
+ {/* Logo */} +
+
+
+ E +
+
+

Admin Panel

+

Bienvenido, {user?.fullName || user?.email?.split('@')[0] || 'Admin'}

+
+
+
+ + {/* Quick Create */} +
+
+ Crear Rápido +
+
+ {quickCreateItems.map((item) => ( + + `flex items-center gap-2 px-3 py-2 text-sm rounded-md transition-colors ${ + isActive + ? 'bg-purple-50 text-purple-600' + : 'text-gray-600 hover:bg-gray-100' + }` + } + > + {item.icon} + {item.label} + + ))} +
+
+ + {/* Main Navigation */} + + + {/* Bottom Actions */} +
+ +
+
+ + {/* Main Content */} +
+ {/* Top Bar */} +
+
+
+

+ Panel de Administración +

+

+ Gestiona todos los aspectos de tu plataforma +

+
+
+ +
+
+

+ {user?.fullName || user?.email?.split('@')[0] || 'Usuario'} +

+

{user?.email || ''}

+
+
+ {user?.fullName?.charAt(0)?.toUpperCase() || user?.email?.charAt(0)?.toUpperCase() || 'A'} +
+
+
+
+
+ + {/* Page Content */} +
+
+ +
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/pages/admin/dashboard.jsx b/src/pages/admin/dashboard.jsx new file mode 100644 index 0000000..3cc0b16 --- /dev/null +++ b/src/pages/admin/dashboard.jsx @@ -0,0 +1,215 @@ +import { useEffect, useState } from "react"; +import { useNavigate, Link } from "react-router-dom"; +import { useAuth } from "../../hooks/useAuth"; +import { toast } from "react-hot-toast"; + +export default function AdminDashboard() { + const { user, isAuthenticated } = useAuth(); + const navigate = useNavigate(); + const [loading, setLoading] = useState(true); + const [stats, setStats] = useState({ + establishments: 0, + events: 0, + entertainment: 0, + catering: 0, + services: 0, + reservations: 0, + users: 0, + revenue: 0 + }); + + useEffect(() => { + if (!isAuthenticated) { + navigate("/sign-in"); + return; + } + + if (user?.type !== "ADMIN") { + toast.error("No tienes permisos para acceder a esta página"); + navigate("/"); + return; + } + + loadDashboardData(); + }, [user, isAuthenticated, navigate]); + + const loadDashboardData = async () => { + // Simular carga de datos + setTimeout(() => { + setStats({ + establishments: 12, + events: 8, + entertainment: 15, + catering: 10, + services: 20, + reservations: 45, + users: 120, + revenue: 15200 + }); + setLoading(false); + }, 500); + }; + + const statCards = [ + { + title: "Establecimientos", + value: stats.establishments, + icon: "🏢", + color: "bg-blue-100 text-blue-800", + change: "+2 este mes", + link: "/admin/establishments" + }, + { + title: "Eventos", + value: stats.events, + icon: "📅", + color: "bg-purple-100 text-purple-800", + change: "+1 este mes", + link: "/admin/events" + }, + { + title: "Reservas", + value: stats.reservations, + icon: "📋", + color: "bg-green-100 text-green-800", + change: "+5 este mes", + link: "/admin/reservations" + }, + { + title: "Ingresos", + value: `$${stats.revenue.toLocaleString()}`, + icon: "💰", + color: "bg-orange-100 text-orange-800", + change: "+12% este mes", + link: "#" + }, + ]; + + const quickActions = [ + { + title: "Crear Establecimiento", + description: "Agregar un nuevo lugar para eventos", + link: "/admin/establishments/create", + icon: "🏢" + }, + { + title: "Crear Evento", + description: "Definir un nuevo tipo de evento", + link: "/admin/events/create", + icon: "🎉" + }, + { + title: "Agregar Servicio", + description: "Nuevo servicio de entretenimiento o catering", + link: "/admin/services/create", + icon: "🔧" + }, + { + title: "Ver Usuarios", + description: "Gestionar clientes y administradores", + link: "/admin/users", + icon: "👥" + }, + ]; + + if (loading) { + return ( +
+
+
+

Cargando estadísticas...

+
+
+ ); + } + + return ( +
+ {/* Welcome Section */} +
+

+ Bienvenido, { "Administrador"} 👋 +

+

+ Aquí tienes un resumen de tu plataforma +

+
+ + {/* Stats Grid */} +
+ {statCards.map((stat, index) => ( +
+
+
+ {stat.icon} +
+ + Ver → + +
+

{stat.value}

+

{stat.title}

+

{stat.change}

+
+ ))} +
+ + {/* Quick Actions */} +
+
+

Acciones Rápidas

+
+ {quickActions.map((action, index) => ( + +
+
+ {action.icon} +
+
+

{action.title}

+

{action.description}

+
+
+ → +
+
+ + ))} +
+
+ +
+

Resumen de Recursos

+
+ {[ + { label: "Establecimientos", value: stats.establishments, max: 20, color: "bg-blue-500" }, + { label: "Eventos", value: stats.events, max: 15, color: "bg-purple-500" }, + { label: "Entretenimiento", value: stats.entertainment, max: 25, color: "bg-pink-500" }, + { label: "Servicios", value: stats.services, max: 30, color: "bg-green-500" }, + ].map((item, index) => ( +
+
+ {item.label} + {item.value} de {item.max} +
+
+
+
+
+ ))} +
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/pages/admin/establishments/create.jsx b/src/pages/admin/establishments/create.jsx new file mode 100644 index 0000000..c09624f --- /dev/null +++ b/src/pages/admin/establishments/create.jsx @@ -0,0 +1,173 @@ +import { useState, useRef } from "react"; +import { useNavigate } from "react-router-dom"; +import { createEstablishment } from "../../../api/establishments"; +import useSnackbar from "../../../hooks/useSnackbar"; + +export default function CreateEstablishment() {s + const navigate = useNavigate(); + const isMounted = useRef(true); + const [creating, setCreating] = useState(false); + const [formData, setFormData] = useState({ + name: "", + address: "", + phone: "", + email: "", + description: "" + }); + + const { showSnackbar, SnackbarComponent } = useSnackbar(); + + useEffect(() => { + return () => { + isMounted.current = false; + }; + }, []); + + const handleChange = (e) => { + const { name, value } = e.target; + setFormData(prev => ({ ...prev, [name]: value })); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + if (creating) return; + + setCreating(true); + + try { + if (!formData.name.trim()) { + showSnackbar("El nombre es requerido", "error"); + setCreating(false); + return; + } + + await createEstablishment(formData); + showSnackbar("Establecimiento creado exitosamente", "success"); + + setTimeout(() => { + if (isMounted.current) { + navigate("/admin/establishments"); + } + }, 1500); + } catch (error) { + console.error("Error al crear establecimiento:", error); + showSnackbar(error.response?.data?.message || "Error al crear establecimiento", "error"); + setCreating(false); + } + }; + + return ( +
+ + + + +

Nuevo Establecimiento

+

Completa la información para crear un nuevo establecimiento

+ +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +