diff --git a/package-lock.json b/package-lock.json index b64737bfa9..1add5b0728 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@headlessui/react": "^2.2.4", "@heroicons/react": "^2.2.0", + "@stripe/stripe-js": "^7.8.0", "@vis.gl/react-google-maps": "^1.5.5", "bcrypt": "^6.0.0", "lucide-react": "^0.525.0", @@ -1250,6 +1251,15 @@ "node": ">=14.0.0" } }, + "node_modules/@stripe/stripe-js": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-7.8.0.tgz", + "integrity": "sha512-DNXRfYUgkZlrniQORbA/wH8CdFRhiBSE0R56gYU0V5vvpJ9WZwvGrz9tBAZmfq2aTgw6SK7mNpmTizGzLWVezw==", + "license": "MIT", + "engines": { + "node": ">=12.16" + } + }, "node_modules/@swc/helpers": { "version": "0.5.17", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", @@ -8944,6 +8954,11 @@ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.22.0.tgz", "integrity": "sha512-MBOl8MeOzpK0HQQQshKB7pABXbmyHizdTpqnrIseTbsv0nAepwC2ENZa1aaBExNQcpLoXmWthhak8SABLzvGPw==" }, + "@stripe/stripe-js": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-7.8.0.tgz", + "integrity": "sha512-DNXRfYUgkZlrniQORbA/wH8CdFRhiBSE0R56gYU0V5vvpJ9WZwvGrz9tBAZmfq2aTgw6SK7mNpmTizGzLWVezw==" + }, "@swc/helpers": { "version": "0.5.17", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", diff --git a/package.json b/package.json index d02a41631c..44b15e03f8 100755 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "dependencies": { "@headlessui/react": "^2.2.4", "@heroicons/react": "^2.2.0", + "@stripe/stripe-js": "^7.8.0", "@vis.gl/react-google-maps": "^1.5.5", "bcrypt": "^6.0.0", "lucide-react": "^0.525.0", diff --git a/src/api/routes/games.py b/src/api/routes/games.py index 9c5d4507e3..8b620fb732 100644 --- a/src/api/routes/games.py +++ b/src/api/routes/games.py @@ -130,19 +130,4 @@ def get_games_by_platform(platform): games = [g.serialize() for g in q.all()] #devuelve todos los juegos encontrados return jsonify({"games": games}), 200 - # BUSCAR JUEGOS NAVBAR -#@api.route('/SearchGames/', methods=["GET"]) -#def get_games_by_search(): - #query = request.args.get("query") - - #if not query: - #return jsonify({"error": "Falta el parámetro 'query'"}), 400 - - #try: - #results = Games.query.filter(Games.name.ilike(f"%{query}%")).all() - #games_list = [{"id": game.id, "name": game.name} for game in results] - #return jsonify(games_list) - #except Exception as e: - #print("Error en la búsqueda:", e) - #return jsonify({"error": "Juego no encontrado"}), 500 - \ No newline at end of file + \ No newline at end of file diff --git a/src/api/routes/payment.py b/src/api/routes/payment.py index ee6e5d99ab..35789e4cf9 100644 --- a/src/api/routes/payment.py +++ b/src/api/routes/payment.py @@ -1,43 +1,108 @@ -# #! /usr/bin/env python3.6 - -# """ -# server.py -# Stripe Sample. -# Python 3.6 or newer required. -# """ import os -from flask import Flask, redirect, request +from flask import Flask, redirect, request, jsonify, Blueprint from api.models.Games import Games import stripe from api.database.db import db -# This is your test secret API key. -os.getenv('VITE_FRONT_URL') -stripe.api_key = os.getenv('SECRET_KEY_STRIPE') - -app = Flask(__name__, - static_url_path='', - static_folder='public') +from flask_cors import CORS +# Blueprint para las rutas de pago +payment_api = Blueprint("api/payment", __name__) +# Configuración de Stripe +stripe.api_key = os.getenv('SECRET_KEY_STRIPE') YOUR_DOMAIN = os.getenv('VITE_FRONT_URL') -@app.route("/checkout", methods=['POST']) +@payment_api.route("/create-checkout-session", methods=['POST']) def create_checkout_session(): try: + # Obtener los datos del carrito desde el frontend + data = request.get_json() + cart_items = data.get('cart_items', []) + + if not cart_items: + return jsonify({"error": "El carrito está vacío"}), 400 + + # Crear line_items para Stripe + line_items = [] + + for item in cart_items: + # Verificar que el juego existe en la base de datos + game = Games.query.get(item['id']) + if not game: + return jsonify({"error": f"Juego con ID {item['id']} no encontrado"}), 400 + + line_item = { + 'price_data': { + 'currency': 'eur', # Cambié a euros ya que veo precios en € + 'product_data': { + 'name': game.name, + 'description': f"Plataforma: {game.platform}", + 'images': [game.img] if game.img else [], + }, + 'unit_amount': int(game.price * 100), # Stripe usa centavos + }, + 'quantity': item.get('quantity', 1), + } + line_items.append(line_item) + + # Crear la sesión de checkout checkout_session = stripe.checkout.Session.create( - line_items=[ - #aqui va la logica del carrito de compra - ], + payment_method_types=['card'], + line_items=line_items, mode='payment', - success_url=YOUR_DOMAIN + '?success=true', - cancel_url=YOUR_DOMAIN + '?canceled=true', + success_url=YOUR_DOMAIN + 'success?session_id={CHECKOUT_SESSION_ID}', + cancel_url=YOUR_DOMAIN + 'cancel', + automatic_tax={'enabled': True}, # Para manejar IVA automáticamente + # Opcional: agregar metadata del usuario + metadata={ + 'user_id': data.get('user_id', ''), + 'total_items': len(cart_items) + } ) + return jsonify({ + 'checkout_url': checkout_session.url, + 'session_id': checkout_session.id + }), 200 + + except stripe.error.StripeError as e: + return jsonify({"error": f"Error de Stripe: {str(e)}"}), 400 except Exception as e: - return str(e) - - return redirect(checkout_session.url, code=303) + return jsonify({"error": f"Error interno: {str(e)}"}), 500 -if __name__ == '__main__': - app.run(port=4242) +@payment_api.route("/verify-session/", methods=['GET']) +def verify_session(session_id): + """Verificar el estado de una sesión de pago""" + try: + session = stripe.checkout.Session.retrieve(session_id) + return jsonify({ + 'payment_status': session.payment_status, + 'amount_total': session.amount_total, + 'currency': session.currency + }), 200 + except stripe.error.StripeError as e: + return jsonify({"error": f"Error de Stripe: {str(e)}"}), 400 +# Webhook para manejar eventos de Stripe (opcional pero recomendado) +@payment_api.route("/webhook", methods=['POST']) +def stripe_webhook(): + payload = request.get_data(as_text=True) + sig_header = request.headers.get('Stripe-Signature') + endpoint_secret = os.getenv('STRIPE_WEBHOOK_SECRET') + + try: + event = stripe.Webhook.construct_event( + payload, sig_header, endpoint_secret + ) + except ValueError: + return "Invalid payload", 400 + except stripe.error.SignatureVerificationError: + return "Invalid signature", 400 + + # Manejar el evento + if event['type'] == 'checkout.session.completed': + session = event['data']['object'] + # Aquí puedes guardar la compra en tu base de datos + print(f"Pago completado para sesión: {session['id']}") + + return jsonify(success=True), 200 \ No newline at end of file diff --git a/src/app.py b/src/app.py index 6e598954cd..c6d8eec3ae 100644 --- a/src/app.py +++ b/src/app.py @@ -12,6 +12,8 @@ import api.routes.user as api_user import api.routes.games as api_games +from api.routes.payment import payment_api as payment_api + from api.admin import setup_admin from api.commands import setup_commands from flask_cors import CORS @@ -64,6 +66,8 @@ # Add all endpoints form the API with a "api" prefix app.register_blueprint(api_user.api, url_prefix='/api/user') app.register_blueprint(api_games.api, url_prefix='/api/games') +app.register_blueprint(payment_api, url_prefix="/api/payment") + # Handle/serialize errors like a JSON object diff --git a/src/front/components/Navbar.jsx b/src/front/components/Navbar.jsx index eab6a395a7..659f7b74bf 100644 --- a/src/front/components/Navbar.jsx +++ b/src/front/components/Navbar.jsx @@ -143,7 +143,7 @@ export default function Navbar({ showDrowpdown, setShowDrowpdown }) { gamesFilter.map((g) => { return ( - {g.name} + {g.name},{g.price} ) }) diff --git a/src/front/components/maps/Maps.jsx b/src/front/components/maps/Maps.jsx index 8742d3ecfd..f3ef3941d6 100644 --- a/src/front/components/maps/Maps.jsx +++ b/src/front/components/maps/Maps.jsx @@ -9,12 +9,11 @@ function MapInner() { return ( - + /> ); } diff --git a/src/front/components/searchgames/SearchGames.jsx b/src/front/components/searchgames/SearchGames.jsx deleted file mode 100644 index dcd2d33a32..0000000000 --- a/src/front/components/searchgames/SearchGames.jsx +++ /dev/null @@ -1,34 +0,0 @@ -import { useLocation } from "react-router-dom"; -import { useEffect, useState } from "react"; - -const backendUrl = "https://refactored-couscous-x5p76ppwgq5v3xxr-3001.app.github.dev/"; // Cada uno tiene que poner su url de su backend hasta tener BD común - -export default function SearchResults() { - const location = useLocation(); - const query = new URLSearchParams(location.search).get("query"); - const [results, setResults] = useState([]); - - useEffect(() => { - if (query) { - fetch(`${backendUrl}api/games/search?query=${encodeURIComponent(query)}`) - .then(res => res.json()) - .then(data => setResults(data)) - .catch(err => console.error("Error al buscar:", err)); - } - }, [query]); - - return ( -
-

Resultados para: {query}

- {results.length > 0 ? ( -
    - {results.map(game => ( -
  • {game.name}
  • - ))} -
- ) : ( -

No se encontraron resultados

- )} -
- ); -} \ No newline at end of file diff --git a/src/front/components/stripe/StripeIntegration.jsx b/src/front/components/stripe/StripeIntegration.jsx new file mode 100644 index 0000000000..7f06f0498b --- /dev/null +++ b/src/front/components/stripe/StripeIntegration.jsx @@ -0,0 +1,100 @@ +import { loadStripe } from '@stripe/stripe-js'; + +// Cargar Stripe con tu clave pública +const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY); + +export const StripeService = { + // Crear sesión de checkout + async createCheckoutSession(cartItems, userId = null) { + const backendUrl = import.meta.env.VITE_BACKEND_URL; + + try { + const response = await fetch(`${backendUrl}/api/payment/create-checkout-session`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + cart_items: cartItems, + user_id: userId + }), + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || 'Error al crear la sesión de pago'); + } + + const data = await response.json(); + return data; + } catch (error) { + console.error('Error al crear sesión de checkout:', error); + throw error; + } + }, + + // Redirigir a Stripe Checkout + async redirectToCheckout(sessionData) { + const stripe = await stripePromise; + + if (!stripe) { + throw new Error('Stripe no se ha cargado correctamente'); + } + + // Opción 1: Usar la URL directa (más simple) + window.location.href = sessionData.checkout_url; + + // Opción 2: Usar el método de Stripe (alternativa) + // const result = await stripe.redirectToCheckout({ + // sessionId: sessionData.session_id, + // }); + + // if (result.error) { + // throw new Error(result.error.message); + // } + }, + + // Verificar estado de una sesión + async verifySession(sessionId) { + const backendUrl = import.meta.env.VITE_BACKEND_URL; + + try { + const response = await fetch(`${backendUrl}/api/payment/verify-session/${sessionId}`); + + if (!response.ok) { + throw new Error('Error al verificar la sesión'); + } + + return await response.json(); + } catch (error) { + console.error('Error al verificar sesión:', error); + throw error; + } + } +}; + +// Hook personalizado para manejar pagos con Stripe +export const useStripePayment = () => { + const handlePayment = async (cartItems, userId = null) => { + try { + // Validar que el carrito no esté vacío + if (!cartItems || cartItems.length === 0) { + throw new Error('El carrito está vacío'); + } + + // Crear sesión de checkout + const sessionData = await StripeService.createCheckoutSession(cartItems, userId); + + // Redirigir a Stripe Checkout + await StripeService.redirectToCheckout(sessionData); + + } catch (error) { + console.error('Error en el proceso de pago:', error); + // Aquí puedes mostrar un mensaje de error al usuario + alert(`Error en el pago: ${error.message}`); + throw error; + } + }; + + return { handlePayment }; +}; \ No newline at end of file diff --git a/src/front/pages/Success.jsx b/src/front/pages/Success.jsx new file mode 100644 index 0000000000..0a9954cb8c --- /dev/null +++ b/src/front/pages/Success.jsx @@ -0,0 +1,179 @@ +import React, { useEffect, useState } from 'react'; +import { useSearchParams, useNavigate, Link } from 'react-router-dom'; +import { StripeService } from '../components/stripe/StripeIntegration'; +import useGlobalReducer from '../hooks/useGlobalReducer'; + +export const Success = () => { + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + const { dispatch } = useGlobalReducer(); + const [verificationStatus, setVerificationStatus] = useState('loading'); + const [paymentDetails, setPaymentDetails] = useState(null); + + useEffect(() => { + const sessionId = searchParams.get('session_id'); + + if (!sessionId) { + setVerificationStatus('error'); + return; + } + + // Verificar la sesión con Stripe + const verifyPayment = async () => { + try { + const sessionData = await StripeService.verifySession(sessionId); + + if (sessionData.payment_status === 'paid') { + setVerificationStatus('success'); + setPaymentDetails(sessionData); + + // Limpiar el carrito tras pago exitoso + dispatch({ type: "clearCarro" }); + localStorage.removeItem("carro"); + + // Opcional: Guardar en historial + const nuevaCompra = { + fecha: new Date().toLocaleString(), + sessionId: sessionId, + total: sessionData.amount_total / 100, // Convertir de centavos + currency: sessionData.currency, + status: 'completed' + }; + + const historial = JSON.parse(localStorage.getItem("historialCompras")) || []; + historial.push(nuevaCompra); + localStorage.setItem("historialCompras", JSON.stringify(historial)); + + } else { + setVerificationStatus('pending'); + } + } catch (error) { + console.error('Error al verificar pago:', error); + setVerificationStatus('error'); + } + }; + + verifyPayment(); + }, [searchParams, dispatch]); + + const renderContent = () => { + switch (verificationStatus) { + case 'loading': + return ( +
+
+

+ Verificando tu pago... +

+

+ Por favor espera mientras confirmamos tu transacción. +

+
+ ); + + case 'success': + return ( +
+
+ + + +
+

+ ¡Pago Exitoso! +

+

+ Tu pago ha sido procesado correctamente. +

+ {paymentDetails && ( +
+

+ Monto: €{(paymentDetails.amount_total / 100).toFixed(2)} +

+

+ Estado: Pagado +

+
+ )} +
+ + Ver Historial de Compras + + + Volver al Inicio + +
+
+ ); + + case 'pending': + return ( +
+
+ + + +
+

+ Pago Pendiente +

+

+ Tu pago está siendo procesado. Te notificaremos cuando se complete. +

+ + Volver al Inicio + +
+ ); + + case 'error': + default: + return ( +
+
+ + + +
+

+ Error al Verificar Pago +

+

+ Hubo un problema al verificar tu pago. Por favor contacta a soporte. +

+
+ + Volver al Carrito + + + Volver al Inicio + +
+
+ ); + } + }; + + return ( +
+
+ {renderContent()} +
+
+ ); +}; \ No newline at end of file diff --git a/src/front/pages/aboutus/AboutUs.jsx b/src/front/pages/aboutus/AboutUs.jsx index ceceb222da..73116d3432 100644 --- a/src/front/pages/aboutus/AboutUs.jsx +++ b/src/front/pages/aboutus/AboutUs.jsx @@ -1,4 +1,3 @@ -// AboutUs.jsx import "./aboutus.css"; import SilviaImg from "./img/Silvia.png"; import AlbertoImg from "./img/Alberto.jpg"; @@ -8,8 +7,7 @@ import { Instagram, Facebook, Mail, Twitter } from "lucide-react"; export const AboutUs = () => { return ( -
- {/* HERO */} +
@@ -24,7 +22,6 @@ export const AboutUs = () => {
- {/* STORY BLURBS */}
@@ -65,9 +62,8 @@ export const AboutUs = () => {
- {/* TEAM */}
-

TEAM

+

Equipo

{/* Silvia */} @@ -80,10 +76,10 @@ export const AboutUs = () => {

Silvia

Front-End

@@ -98,10 +94,10 @@ export const AboutUs = () => {

Alberto

Back-End

@@ -116,10 +112,10 @@ export const AboutUs = () => {

Adrián Beneroso

Full-Stack

@@ -127,4 +123,4 @@ export const AboutUs = () => {
); -}; +}; \ No newline at end of file diff --git a/src/front/pages/aboutus/aboutus.css b/src/front/pages/aboutus/aboutus.css index 56ee96d926..54372549a5 100644 --- a/src/front/pages/aboutus/aboutus.css +++ b/src/front/pages/aboutus/aboutus.css @@ -3,9 +3,23 @@ --gap: 24px; --brand: #7c3aed; --brand-2: #2563eb; - --ink: #0b1020; + --ink: #0b1020; + --nav-h: 72px; +} + +.container{ + width:min(100% - 32px, var(--container)); + margin-inline:auto; +} + +.navbar, .site-nav { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 2147483647; + pointer-events: auto; } -.container{ width:min(100% - 32px, var(--container)); margin-inline:auto; } .about{ color:#fff; @@ -13,8 +27,18 @@ radial-gradient(900px 400px at 80% 10%, #1b2150 0%, transparent 60%), linear-gradient(180deg, #0b1020 0%, #0b0f1a 100%); overflow: visible; + + padding-top: var(--nav-h); + position: relative; + z-index: 0; } + +[id] { + scroll-margin-top: calc(var(--nav-h) + 8px); +} + + .about__hero{ position:relative; min-height: 60vh; @@ -25,12 +49,14 @@ } .about__hero-bg{ - position:absolute; inset:-20% -5% 0 -5%; + position:absolute; + inset:-20% -5% 0 -5%; background: radial-gradient(800px 200px at 20% 0%, rgba(124,58,237,.35), transparent 65%), radial-gradient(700px 220px at 80% 5%, rgba(37,99,235,.35), transparent 65%); filter: blur(30px) saturate(120%); z-index:-1; + pointer-events: none; animation: pulse 6s ease-in-out infinite alternate; } @keyframes pulse{ @@ -146,23 +172,29 @@ line-height: 1.6; } + .about__team{ - padding-block: clamp(48px, 6vw, 96px) clamp(32px, 5vw, 72px); + position: relative; + z-index: 0; } .team__title{ - text-align:center; - font-size: clamp(1.4rem, 3.5vw, 2.2rem); - font-weight: 900; + display: inline-block; + line-height: 1.2; + padding-block: 12px; margin: clamp(32px, 4vw, 56px) 0; + font-size: 50px; + font-weight: 900; letter-spacing: .5px; + background: linear-gradient(135deg, #93c5fd, #a78bfa); - -webkit-background-clip:text; - background-clip:text; - color:transparent; + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + color: transparent; + filter: drop-shadow(0 10px 30px rgba(147,197,253,.25)); } - .team__grid{ display:grid; gap: clamp(16px, 2.4vw, 28px); @@ -170,7 +202,6 @@ align-items: stretch; } - .team-card{ position: relative; background: rgba(15, 17, 35, .8); @@ -182,10 +213,9 @@ transform-style: preserve-3d; transition: transform .25s ease, box-shadow .25s ease; - display:flex; flex-direction:column; - min-height: 480px; + min-height: 480px; } .team-card:focus-visible{ outline: 2px solid #7c3aed; outline-offset: 3px; } .team-card:hover{ @@ -200,17 +230,23 @@ pointer-events:none; } -.team-card__img-wrap{ - aspect-ratio: 4 / 3; - border-radius: 12px; - overflow: hidden; - border: 1px solid rgba(255,255,255,.12); - background:#0b0f1a; - box-shadow: inset 0 0 0 1px rgba(255,255,255,.04); -} -.team-card__img{ - width:100%; height:100%; object-fit: cover; display:block; - transition: transform .4s ease, filter .4s ease; +.team-card__img-wrap { + width: 100%; + aspect-ratio: 1 / 1; + border-radius: 50%; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + background: #0b0f1a; + border: 2px solid rgba(255,255,255,.12); +} +.team-card__img { + width: 100%; + height: 100%; + object-fit: cover; + object-position: center; + border-radius: 50%; } .team-card:hover .team-card__img{ transform: scale(1.04); filter:saturate(1.05); } diff --git a/src/front/pages/cancelPayment/Cancel.jsx b/src/front/pages/cancelPayment/Cancel.jsx new file mode 100644 index 0000000000..efa4524863 --- /dev/null +++ b/src/front/pages/cancelPayment/Cancel.jsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + +export const Cancel = () => { + return ( +
+
+
+ {/* Icono de cancelación */} +
+ + + +
+ + {/* Título */} +

+ Pago Cancelado +

+ + {/* Descripción */} +

+ Has cancelado el proceso de pago. Tus productos siguen en el carrito por si quieres intentarlo de nuevo. +

+ + {/* Botones */} +
+ + Volver al Carrito + + + Seguir Comprando + +
+ + {/* Información adicional */} +
+

+ ¿Tuviste algún problema? +

+

+ Si experimentaste algún error durante el proceso de pago, no dudes en contactar a nuestro soporte. +

+ + Contactar Soporte → + +
+
+
+
+ ); +}; \ No newline at end of file diff --git a/src/front/pages/carro/Carro.css b/src/front/pages/carro/Carro.css index 7830c6adfa..11fb816353 100644 --- a/src/front/pages/carro/Carro.css +++ b/src/front/pages/carro/Carro.css @@ -13,7 +13,6 @@ padding-bottom: 70px; } - .carro-bg{ position: fixed; inset: 0; @@ -56,10 +55,11 @@ display: grid; grid-template-columns: 1fr 350px; gap: 2rem; + align-items: start; } .carro-box { - background: #1f2937; /* gris oscuro */ + background: #1f2937; border-radius: 10px; padding: 1.25rem; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); @@ -142,9 +142,23 @@ } .carro-summary { + align-self: start; background: #1f2937; border-radius: 10px; padding: 1.5rem; color: #f3f4f6; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); + + position: sticky; + top: 90px; +} + +@media (max-width: 900px) { + .carro-grid { + grid-template-columns: 1fr; + } + .carro-summary { + position: static; + top: auto; + } } diff --git a/src/front/pages/carro/Carro.jsx b/src/front/pages/carro/Carro.jsx index cc715c6d77..8a50768aed 100644 --- a/src/front/pages/carro/Carro.jsx +++ b/src/front/pages/carro/Carro.jsx @@ -1,16 +1,22 @@ import React, { useEffect, useState, useRef } from "react"; import useGlobalReducer from "../../hooks/useGlobalReducer"; +import { useStripePayment } from "../../components/stripe/StripeIntegration"; import mario from "../aboutus/img/mario.jpg"; import "./Carro.css"; - - export const Carro = () => { const { store, dispatch } = useGlobalReducer(); - const [showModal, setShowModal] = useState(false); + const { handlePayment } = useStripePayment(); + const [paymentLoading, setPaymentLoading] = useState(false); const [paymentSuccess, setPaymentSuccess] = useState(false); const didInit = useRef(false); + // Obtener usuario actual si está logueado + const getCurrentUser = () => { + const user = localStorage.getItem('user'); + return user ? JSON.parse(user) : null; + }; + const handleRemove = (id) => dispatch({ type: "removeFromCarro", payload: id }); const handleClear = () => dispatch({ type: "clearCarro" }); const handleQuantityChange = (id, quantity) => @@ -35,15 +41,49 @@ export const Carro = () => { } }, [store.carro]); - const handlePayment = () => { - setShowModal(false); - setPaymentSuccess(true); + // Función para procesar pago con Stripe + const handleStripePayment = async () => { + if (store.carro.length === 0) { + alert('El carrito está vacío'); + return; + } + + setPaymentLoading(true); + + try { + const currentUser = getCurrentUser(); + const userId = currentUser ? currentUser.id : null; + + // Preparar items del carrito para Stripe + const cartItems = store.carro.map(item => ({ + id: item.id, + name: item.name, + price: item.price, + quantity: item.quantity, + platform: item.platform + })); + + // Procesar pago con Stripe + await handlePayment(cartItems, userId); + + // Si llegamos aquí, el usuario fue redirigido a Stripe + // La limpieza del carrito se hará en la página de éxito + + } catch (error) { + console.error('Error al procesar pago:', error); + setPaymentLoading(false); + // El error ya se muestra en useStripePayment + } + }; + // Función de pago simulado (mantener como backup o para testing) + const handleSimulatedPayment = () => { const nuevaCompra = { fecha: new Date().toLocaleString(), total: getTotal(), productos: [...store.carro], }; + const historial = JSON.parse(localStorage.getItem("historialCompras")) || []; historial.push(nuevaCompra); localStorage.setItem("historialCompras", JSON.stringify(historial)); @@ -51,13 +91,14 @@ export const Carro = () => { dispatch({ type: "clearCarro" }); localStorage.removeItem("carro"); + setPaymentSuccess(true); setTimeout(() => setPaymentSuccess(false), 3000); }; const getTitle = (it) => it.title || it.name || "Juego"; const getImg = (it) => it.img || it.image || "https://via.placeholder.com/100"; -return ( + return (
{/* Fondo a pantalla completa */}

Carro de compras

- {store.carro.length === 0 ? ( -
-

Tu carro está vacío

-

Empieza a añadir juegos para verlos aquí.

-
- ) : ( -
-
- {store.carro.map((item) => ( -
-
-
- {getTitle(item)} -
-

{getTitle(item)}

-

- ${item.price} - {item.platform ? ` · ${item.platform}` : ""} -

-
- - handleQuantityChange(item.id, e.target.value) - } - className="carro-input" - /> - + {store.carro.length === 0 ? ( +
+

Tu carro está vacío

+

Empieza a añadir juegos para verlos aquí.

+
+ ) : ( +
+
+ {store.carro.map((item) => ( +
+
+
+ {getTitle(item)} +
+

{getTitle(item)}

+

+ €{item.price} + {item.platform ? ` · ${item.platform}` : ""} +

+
+ + handleQuantityChange(item.id, e.target.value) + } + className="carro-input" + /> + +
+ + €{(item.price * item.quantity).toFixed(2)} +
- - ${(item.price * item.quantity).toFixed(2)} - -
-
- ))} - - -
+ + ))} - -
- )} - - {showModal && ( -
-
-

¿Confirmar pago?

-

Total a pagar: ${getTotal().toFixed(2)}

-
- - + + +
+ )} + + {/* Mensaje de éxito */} + {paymentSuccess && ( +
+ ¡Pago realizado con éxito!
-
+ )}
- )} - {paymentSuccess && ( -
- ¡Pago realizado con éxito! -
- )} -
); -}; +}; \ No newline at end of file diff --git a/src/front/pages/detailsGames/DetailsGames.jsx b/src/front/pages/detailsGames/DetailsGames.jsx index fa8a5331b3..9699824373 100644 --- a/src/front/pages/detailsGames/DetailsGames.jsx +++ b/src/front/pages/detailsGames/DetailsGames.jsx @@ -57,7 +57,7 @@ export const DetailsGames = () => {
@@ -86,11 +86,7 @@ export const DetailsGames = () => { > Añadir al carrito - +
)} @@ -161,15 +157,7 @@ export const DetailsGames = () => { {/* Age Rating */} -
- {/* AQUI PONDREMOS LA EDAD COMO PARAMETRO DE JUEGO */} -
18
-
- {/* AQUI PONDREMOS LAS OPCIONES DEL PEGY */} -

Lenguaje fuerte, Violencia fuerte,

-

Compras dentro del juego, Los usuarios interactúan

-
-
+ diff --git a/src/front/pages/home/Home.jsx b/src/front/pages/home/Home.jsx index ad0ca9388c..c8365995c4 100644 --- a/src/front/pages/home/Home.jsx +++ b/src/front/pages/home/Home.jsx @@ -8,7 +8,7 @@ import { Games } from "../../components/games/Games"; export const Home = () => { const images = [ { - src: "https://www.xtrafondos.com/wallpapers/god-of-war-ragnarok-11256.jpg", + src: "https://cdn1.epicgames.com/offer/24b9b5e323bc40eea252a10cdd3b2f10/EGS_LeagueofLegends_RiotGames_S1_2560x1440-47eb328eac5ddd63ebd096ded7d0d5ab", }, { src: "https://4kwallpapers.com/images/wallpapers/the-super-mario-2880x1800-10955.jpg", diff --git a/src/front/pages/soporte/Soporte.jsx b/src/front/pages/soporte/Soporte.jsx index 5ef7e1456f..e2edb0570f 100644 --- a/src/front/pages/soporte/Soporte.jsx +++ b/src/front/pages/soporte/Soporte.jsx @@ -1,7 +1,12 @@ +import { useState } from "react"; import "./soporte.css"; + export const Soporte = () => { + const [enviado, setEnviado] = useState(false); + const handleSubmit = (e) => { e.preventDefault(); + setEnviado(true); }; return ( @@ -14,56 +19,66 @@ export const Soporte = () => { Envíanos tu consulta y te responderemos lo antes posible.

-
-
- - -
+ {!enviado ? ( + +
+ + +
-
- - -
+
+ + +
-
- -