From 4cfe39208ea7101153918286157e33ca1e2e1d26 Mon Sep 17 00:00:00 2001 From: zoscra Date: Tue, 22 Jul 2025 16:06:46 +0000 Subject: [PATCH] Prueba --- migrations/versions/0763d677d453_.py | 35 --- migrations/versions/413e5fe65505_.py | 52 ++++ package-lock.json | 41 ++- package.json | 29 +- src/api/models.py | 44 ++- src/api/routes.py | 123 +++++++- src/app.py | 6 + .../components/GoogleMapWithCustomControl.jsx | 15 + src/front/components/Navbar.jsx | 121 +++++++- src/front/components/RegistroForm.jsx | 268 ++++++++++++++++++ src/front/main.jsx | 6 +- src/front/pages/Registro.jsx | 16 ++ src/front/routes.jsx | 2 + src/front/store.js | 45 +-- 14 files changed, 709 insertions(+), 94 deletions(-) delete mode 100644 migrations/versions/0763d677d453_.py create mode 100644 migrations/versions/413e5fe65505_.py create mode 100644 src/front/components/GoogleMapWithCustomControl.jsx create mode 100644 src/front/components/RegistroForm.jsx create mode 100644 src/front/pages/Registro.jsx diff --git a/migrations/versions/0763d677d453_.py b/migrations/versions/0763d677d453_.py deleted file mode 100644 index 88964176f1..0000000000 --- a/migrations/versions/0763d677d453_.py +++ /dev/null @@ -1,35 +0,0 @@ -"""empty message - -Revision ID: 0763d677d453 -Revises: -Create Date: 2025-02-25 14:47:16.337069 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '0763d677d453' -down_revision = None -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('user', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('email', sa.String(length=120), nullable=False), - sa.Column('password', sa.String(), nullable=False), - sa.Column('is_active', sa.Boolean(), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('email') - ) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('user') - # ### end Alembic commands ### diff --git a/migrations/versions/413e5fe65505_.py b/migrations/versions/413e5fe65505_.py new file mode 100644 index 0000000000..56d804298c --- /dev/null +++ b/migrations/versions/413e5fe65505_.py @@ -0,0 +1,52 @@ +"""empty message + +Revision ID: 413e5fe65505 +Revises: +Create Date: 2025-07-21 12:24:39.717542 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '413e5fe65505' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('user', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('email', sa.String(length=120), nullable=False), + sa.Column('password', sa.String(), nullable=False), + sa.Column('vehicle', sa.Boolean(), nullable=False), + sa.Column('coordenates', sa.String(length=120), nullable=False), + sa.Column('name', sa.String(length=200), nullable=False), + sa.Column('vehicle_consume_km', sa.Float(precision=50), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('email') + ) + op.create_table('oferta', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('id_comprador', sa.Integer(), nullable=True), + sa.Column('id_vendedor', sa.Integer(), nullable=False), + sa.Column('esta_realizada', sa.Boolean(), nullable=False), + sa.Column('descripcion', sa.String(length=600), nullable=False), + sa.Column('titulo', sa.String(length=200), nullable=False), + sa.Column('coordenates_vendedor', sa.String(length=120), nullable=False), + sa.Column('coordenates_comprador', sa.String(length=120), nullable=True), + sa.ForeignKeyConstraint(['id_comprador'], ['user.id'], ), + sa.ForeignKeyConstraint(['id_vendedor'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('oferta') + op.drop_table('user') + # ### end Alembic commands ### diff --git a/package-lock.json b/package-lock.json index 8d43d98ab7..748d34db99 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.1", "license": "ISC", "dependencies": { + "@vis.gl/react-google-maps": "^1.5.4", "prop-types": "^15.8.1", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -997,6 +998,12 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/google.maps": { + "version": "3.58.1", + "resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.58.1.tgz", + "integrity": "sha512-X9QTSvGJ0nCfMzYOnaVs/k6/4L+7F5uCS+4iUmkLEls6J9S/Phv+m/i3mDeyc49ZBgwab3EFO1HEoBY7k98EGQ==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "16.11.12", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.12.tgz", @@ -1040,6 +1047,20 @@ "dev": true, "license": "ISC" }, + "node_modules/@vis.gl/react-google-maps": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@vis.gl/react-google-maps/-/react-google-maps-1.5.4.tgz", + "integrity": "sha512-pD3e2wDtOfd439mamkacRgrM6I2B/lue61QCR0pGQT8MVaG9pz9/LajHbsjZW2lms8Ao8mf2PQJeiGC2FxI0Fw==", + "license": "MIT", + "dependencies": { + "@types/google.maps": "^3.54.10", + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "react": ">=16.8.0 || ^19.0 || ^19.0.0-rc", + "react-dom": ">=16.8.0 || ^19.0 || ^19.0.0-rc" + } + }, "node_modules/@vitejs/plugin-react": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz", @@ -2215,8 +2236,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", @@ -5044,6 +5064,11 @@ "@babel/types": "^7.20.7" } }, + "@types/google.maps": { + "version": "3.58.1", + "resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.58.1.tgz", + "integrity": "sha512-X9QTSvGJ0nCfMzYOnaVs/k6/4L+7F5uCS+4iUmkLEls6J9S/Phv+m/i3mDeyc49ZBgwab3EFO1HEoBY7k98EGQ==" + }, "@types/node": { "version": "16.11.12", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.12.tgz", @@ -5081,6 +5106,15 @@ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "dev": true }, + "@vis.gl/react-google-maps": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@vis.gl/react-google-maps/-/react-google-maps-1.5.4.tgz", + "integrity": "sha512-pD3e2wDtOfd439mamkacRgrM6I2B/lue61QCR0pGQT8MVaG9pz9/LajHbsjZW2lms8Ao8mf2PQJeiGC2FxI0Fw==", + "requires": { + "@types/google.maps": "^3.54.10", + "fast-deep-equal": "^3.1.3" + } + }, "@vitejs/plugin-react": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz", @@ -5883,8 +5917,7 @@ "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "fast-json-stable-stringify": { "version": "2.1.0", diff --git a/package.json b/package.json index 0caab10749..067d473770 100755 --- a/package.json +++ b/package.json @@ -8,10 +8,10 @@ "main": "index.js", "scripts": { "dev": "vite", - "start": "vite", - "build": "vite build", - "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", - "preview": "vite preview" + "start": "vite", + "build": "vite build", + "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" }, "author": { "name": "Alejandro Sanchez", @@ -30,13 +30,13 @@ "license": "ISC", "devDependencies": { "@types/react": "^18.2.18", - "@types/react-dom": "^18.2.7", - "@vitejs/plugin-react": "^4.0.4", - "eslint": "^8.46.0", - "eslint-plugin-react": "^7.33.1", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.3", - "vite": "^4.4.8" + "@types/react-dom": "^18.2.7", + "@vitejs/plugin-react": "^4.0.4", + "eslint": "^8.46.0", + "eslint-plugin-react": "^7.33.1", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.3", + "vite": "^4.4.8" }, "babel": { "presets": [ @@ -54,9 +54,10 @@ ] }, "dependencies": { + "@vis.gl/react-google-maps": "^1.5.4", "prop-types": "^15.8.1", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-router-dom": "^6.18.0" + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.18.0" } } diff --git a/src/api/models.py b/src/api/models.py index da515f6a1a..96dc2c09f8 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -1,19 +1,59 @@ from flask_sqlalchemy import SQLAlchemy -from sqlalchemy import String, Boolean +from sqlalchemy import String, Boolean, Float,Integer,ForeignKey from sqlalchemy.orm import Mapped, mapped_column db = SQLAlchemy() class User(db.Model): + __tablename__ = "user" + id: Mapped[int] = mapped_column(primary_key=True) email: Mapped[str] = mapped_column(String(120), unique=True, nullable=False) password: Mapped[str] = mapped_column(nullable=False) - is_active: Mapped[bool] = mapped_column(Boolean(), nullable=False) + vehicle: Mapped[bool] = mapped_column(Boolean(), nullable=False) + coordenates: Mapped[str] = mapped_column(String(120),nullable=False) + name: Mapped[str]= mapped_column(String(200),nullable=False) + vehicle_consume_km: Mapped[float] = mapped_column(Float(50),nullable= True) + def serialize(self): return { "id": self.id, "email": self.email, + "name":self.name, + "coordenates":self.coordenates, + "vehicle":self.vehicle, + "vehicle_consume_km":self.vehicle_consume_km + + # do not serialize the password, its a security breach + } + +class Oferta(db.Model): + __tablename__="oferta" + + id: Mapped[int] = mapped_column(primary_key=True) + id_comprador: Mapped[int] = mapped_column(Integer(),ForeignKey("user.id"), nullable=True) + id_vendedor: Mapped[int] = mapped_column(Integer(),ForeignKey("user.id"),nullable=False) + esta_realizada: Mapped[bool] = mapped_column(Boolean(), nullable=False) + descripcion: Mapped[str] = mapped_column(String(600),nullable=False) + titulo: Mapped[str]= mapped_column(String(200),nullable=False) + coordenates_vendedor: Mapped[str] = mapped_column(String(120),nullable=False) + coordenates_comprador: Mapped[str] = mapped_column(String(120),nullable=True) + + + + + def serialize(self): + return { + "id": self.id, + "id_comprador": self.id_comprador, + "id_vendedor":self.id_vendedor, + "esta_realizada":self.esta_realizada, + "descripcion":self.descripcion, + "titulo":self.titulo, + "coordenates_vendedor":self.coordenates_vendedor, + "coordenates_comprador":self.coordenates_comprador + # do not serialize the password, its a security breach } \ No newline at end of file diff --git a/src/api/routes.py b/src/api/routes.py index 029589a3a1..62f143f224 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -1,10 +1,14 @@ """ This module takes care of starting the API Server, Loading the DB and Adding the endpoints """ + from flask import Flask, request, jsonify, url_for, Blueprint -from api.models import db, User +from api.models import db, User, Oferta from api.utils import generate_sitemap, APIException from flask_cors import CORS +import bcrypt +from flask_jwt_extended import create_access_token +from flask_jwt_extended import jwt_required, get_jwt_identity api = Blueprint('api', __name__) @@ -12,7 +16,7 @@ CORS(api) -@api.route('/hello', methods=['POST', 'GET']) +@api.route('/', methods=['POST', 'GET']) def handle_hello(): response_body = { @@ -20,3 +24,118 @@ def handle_hello(): } return jsonify(response_body), 200 + +# Post para registrar un usuario +@api.route('/user/register', methods=['POST']) +def user_register(): + + body = request.get_json() + new_pass=bcrypt.hashpw(body["password"].encode(), bcrypt.gensalt()) + + + new_user = User() + new_user.name = body["name"] + new_user.email = body["email"] + new_user.password = new_pass.decode() + new_user.vehicle = body["vehicle"] + new_user.vehicle_consume_km = body["vehicle_consume_km"] + new_user.coordenates = body["coordenates"] + + db.session.add(new_user) + db.session.commit() + + return jsonify("new_user"), 200 + +# Post para logear un usuario +@api.route("/user/login", methods=["POST"]) +def user_login(): + body = request.get_json() + user = User.query.filter_by(email=body["email"]).first() + user_pass = User.query.filter_by(password=body["password"]).first() + + if user is None: + return jsonify("Cuenta no existe"),404 + + if bcrypt.checkpw(body["password"].encode(),user.password.encode()): + user_serialize = user.serialize() + token = create_access_token(identity = str(user_serialize["id"])) + return jsonify({"token":token}),200 + + + return jsonify("Usuario logueado"),200 + +# GET pedir informacion sobre un usuario +@api.route("/user", methods=["GET"]) +@jwt_required() +def get_user(): + current_user = get_jwt_identity() + user = User.query.get(current_user) + if user is None: + return jsonify("Usuario no valido"),400 + return jsonify({"user":user.serialize()}) + + +# GET pedir informacion sobre todas las ofertas disponibles de todos los usuarios +@api.route("/user/ofertas", methods=["GET"]) +@jwt_required() +def get_ofertas(): + current_user = get_jwt_identity() + user = User.query.get(current_user) + ofertas = Oferta.query.all() + iterar_ofertas = [oferta.serialize() for oferta in ofertas] + if user is None: + return jsonify("Usuario no valido"),400 + if ofertas is None: + return jsonify("No hay ofertas disponibles") + return jsonify(iterar_ofertas) + +# GET pedir informacion sobre una oferta + +@api.route("/user/oferta/info/", methods=["GET"]) +@jwt_required() +def get_oferta(oferta_id): + current_user = get_jwt_identity() + user = User.query.get(current_user) + if user is None: + return jsonify("Usuario no valido"),400 + + oferta = Oferta.query.get(oferta_id) + + if oferta is None: + return jsonify("No existe esa oferta"),400 + oferta_serializada = oferta.serialize() + print(oferta_serializada) + print(oferta) + print(oferta_id) + + return jsonify(oferta_serializada) + + +# POST crear una nueva oferta +@api.route("/user/ofertas", methods=["POST"]) +@jwt_required() +def post_ofertas(): + current_user = get_jwt_identity() + user = User.query.get(current_user) + + body = request.get_json() + nueva_oferta = Oferta() + nueva_oferta.id_comprador = None + nueva_oferta.coordenates_comprador = None + nueva_oferta.id_vendedor = user.id + nueva_oferta.esta_realizada = body["esta_realizada"] + nueva_oferta.descripcion = body["descripcion"] + nueva_oferta.titulo = body["titulo"] + nueva_oferta.coordenates_vendedor = user.coordenates + + db.session.add(nueva_oferta) + db.session.commit() + + + if user is None: + return jsonify("Usuario no valido"),400 + return jsonify(nueva_oferta.serialize()),200 + + + + diff --git a/src/app.py b/src/app.py index 1b3340c0fa..f470e9e3e5 100644 --- a/src/app.py +++ b/src/app.py @@ -10,6 +10,7 @@ from api.routes import api from api.admin import setup_admin from api.commands import setup_commands +from flask_jwt_extended import JWTManager # from models import Person @@ -28,9 +29,14 @@ app.config['SQLALCHEMY_DATABASE_URI'] = "sqlite:////tmp/test.db" app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + +app.config["JWT_SECRET_KEY"] = os.getenv("TOKEN_KEY") + MIGRATE = Migrate(app, db, compare_type=True) db.init_app(app) +jwt = JWTManager(app) + # add the admin setup_admin(app) diff --git a/src/front/components/GoogleMapWithCustomControl.jsx b/src/front/components/GoogleMapWithCustomControl.jsx new file mode 100644 index 0000000000..eaa5790072 --- /dev/null +++ b/src/front/components/GoogleMapWithCustomControl.jsx @@ -0,0 +1,15 @@ +import React, { useEffect, useRef, useState } from 'react'; +import useGlobalReducer from "../hooks/useGlobalReducer"; +import {APIProvider, useMap ,Map} from '@vis.gl/react-google-maps'; + +const MADRID_LOCATION = { lat: 40.4168, lng: -3.7038 }; + + + +export const GoogleMapWithCustomControl = () => { + + + + +}; + diff --git a/src/front/components/Navbar.jsx b/src/front/components/Navbar.jsx index 30d43a2636..000b142d58 100644 --- a/src/front/components/Navbar.jsx +++ b/src/front/components/Navbar.jsx @@ -1,19 +1,110 @@ import { Link } from "react-router-dom"; +// src/components/NavbarAgricola.js +import React from 'react'; + +// --- Estilos CSS en línea para la Navbar --- +const navbarContainerStyles = { + backgroundColor: '#8bc34a', // Verde oliva claro, evocando campos + padding: '15px 30px', + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + boxShadow: '0 4px 10px rgba(0, 70, 0, 0.2)', // Sombra sutil con tono verde + fontFamily: 'Arial, sans-serif', + borderRadius: '8px', // Bordes redondeados para un toque orgánico + margin: '20px auto', + maxWidth: '1200px', + boxSizing: 'border-box', +}; + +const logoStyles = { + display: 'flex', + alignItems: 'center', + textDecoration: 'none', // Elimina el subrayado del enlace + color: 'white', + fontWeight: 'bold', + fontSize: '1.8em', + textShadow: '1px 1px 3px rgba(0,0,0,0.2)', + letterSpacing: '0.5px', +}; + +const logoSvgStyles = { + marginRight: '10px', + width: '35px', + height: '35px', + fill: '#ffeb3b', // Amarillo cereal +}; + +const navLinksStyles = { + display: 'flex', + gap: '25px', +}; + +const linkStyles = { + color: 'white', + textDecoration: 'none', + fontSize: '1.1em', + padding: '8px 15px', + borderRadius: '5px', + transition: 'background-color 0.3s ease, transform 0.2s ease', + fontWeight: 'bold', + letterSpacing: '0.2px', +}; + +const linkHoverStyles = { + backgroundColor: '#689f38', // Verde más oscuro al pasar el ratón + transform: 'translateY(-2px)', +}; + +// --- Componente NavbarAgricola --- export const Navbar = () => { + return ( + + ); +}; +export default Navbar; - return ( - - ); -}; \ No newline at end of file +//onClick={(e) => + // setCoordinates({ + // latitude: e.detail.latLng?.lat || 0, + // longitude: e.detail.latLng?.lng || 0, + // }) + // } \ No newline at end of file diff --git a/src/front/components/RegistroForm.jsx b/src/front/components/RegistroForm.jsx new file mode 100644 index 0000000000..0571542581 --- /dev/null +++ b/src/front/components/RegistroForm.jsx @@ -0,0 +1,268 @@ +import React, { useEffect, useState } from 'react'; +import {GoogleMapWithCustomControl} from "../components/GoogleMapWithCustomControl"; +import useGlobalReducer from "../hooks/useGlobalReducer" +import {APIProvider, useMap} from '@vis.gl/react-google-maps'; + +// --- Estilos CSS en línea para el formulario agrícola --- +const formStyles = { + maxWidth: '600px', + margin: '50px auto', + padding: '30px', + border: '1px solid #7cb342', // Verde un poco más oscuro + borderRadius: '12px', + boxShadow: '0 8px 20px rgba(0, 100, 0, 0.15)', // Sombra verde para profundidad + backgroundColor: '#f0f4c3', // Un verde muy claro, casi crema + fontFamily: 'Arial, sans-serif', + color: '#333', + backgroundImage: 'url("data:image/svg+xml,%3Csvg width=\'100%25\' height=\'100%25\' viewBox=\'0 0 100 100\' xmlns=\'http://www.w3.org/2000/svg\'%3E%3Cpath fill=\'%23dcedc8\' d=\'M75.1%2C47.8C81.1%2C58.8%2C78.4%2C76.8%2C68.3%2C83.2C58.2%2C89.5%2C40.6%2C84.2%2C30%2C76.3C19.3%2C68.4%2C15.5%2C57.9%2C12.1%2C47C8.7%2C36.1%2C5.7%2C24.7%2C11.5%2C15.6C17.3%2C6.5%2C31.8%2C0.6%2C44.6%2C3.1C57.4%2C5.6%2C68.5%2C16.4%2C75.2%2C29.6C81.9%2C42.8%2C80.8%2C36.7%2C75.1%2C47.8Z\' transform=\'translate(0 0)\' stroke-width=\'0\' style=\'transition: all 0.3s ease 0s;\'%3E%3C/path%3E%3C/svg%3E")', // Pequeño patrón orgánico + backgroundSize: 'cover', + backgroundRepeat: 'no-repeat', + backgroundPosition: 'center', +}; + +const inputGroupStyles = { + marginBottom: '20px', +}; + +const labelStyles = { + display: 'block', + marginBottom: '8px', + fontWeight: 'bold', + color: '#558b2f', // Verde oscuro para las etiquetas + fontSize: '1.1em', +}; + +const inputStyles = { + width: 'calc(100% - 22px)', // Ancho completo menos padding y borde + padding: '12px', + border: '2px solid #aed581', // Borde verde claro + borderRadius: '6px', + fontSize: '1em', + color: '#333', + backgroundColor: 'white', + boxSizing: 'border-box', + transition: 'border-color 0.3s ease, box-shadow 0.3s ease', +}; + +const checkboxGroupStyles = { + display: 'flex', + alignItems: 'center', + marginBottom: '20px', + gap: '10px', +}; + +const checkboxInputStyles = { + transform: 'scale(1.2)', +}; + +const buttonStyles = { + width: '100%', + padding: '15px 25px', + backgroundColor: '#689f38', // Verde medio para el botón + color: 'white', + border: 'none', + borderRadius: '8px', + fontSize: '1.1em', + fontWeight: 'bold', + cursor: 'pointer', + transition: 'background-color 0.3s ease, transform 0.2s ease', + boxShadow: '0 4px 8px rgba(0, 128, 0, 0.2)', // Sombra sutil para el botón +}; + +const buttonHoverStyles = { + backgroundColor: '#558b2f', // Verde más oscuro al pasar el ratón + transform: 'translateY(-2px)', +}; +// --- Fin de Estilos CSS en línea --- + + +export const RegistroForm = () => { + const { store, dispatch } = useGlobalReducer() + // Estados para cada campo del formulario + const [nombreApellidos, setNombreApellidos] = useState(''); + const [email, setEmail] = useState(''); + const [contrasena, setContrasena] = useState(''); + const [direccion, setDireccion] = useState(''); + const [tieneVehiculo, setTieneVehiculo] = useState(false); + const [consumoVehiculoKm, setConsumoVehiculoKm] = useState(''); + const [coordenadas, setCoordenadas] = useState(''); // Para que el usuario ponga sus coordenadas (ej: "lat,lon") + + const API_KEY = import.meta.env.GOOGLE_MAPS_API_KEY + // Función que se ejecuta al enviar el formulario + const handleSubmit = async (e) => { // <-- Asegúrate de que esta función sea 'async' + e.preventDefault(); // Previene la recarga de la página + + // Define el objeto 'nuevoUser' aquí, al inicio de la función handleSubmit + + + const nuevoUser = { + "name": nombreApellidos, // Mapea nombreApellidos a 'name' para el backend + "email": email, + "password": contrasena, + "vehicle": tieneVehiculo, + "vehicle_consume_km": tieneVehiculo ? parseFloat(consumoVehiculoKm) : null, // Convertir a número si es necesario + "coordenates": coordenadas // Las coordenadas del mapa o introducidas manualmente + }; + + console.log('Datos del formulario de registro (nuevoUser):', nuevoUser); + + try { + // Realiza la petición POST al backend + const response = await fetch("https://friendly-journey-r49vwgpppjqhx74g-3001.app.github.dev/api/user/register", { + method: "POST", + body: JSON.stringify(nuevoUser), // Usa 'nuevoUser' aquí + headers: { "Content-Type": "application/json" } + }); + + if (!response.ok) { + // Manejar errores de respuesta HTTP (ej. 400, 500) + const errorData = await response.json(); + throw new Error(errorData.message || `Error en el registro: ${response.status}`); + } + + const result = await response.json(); + console.log('Respuesta del backend (registro exitoso):', result); + // Aquí podrías redirigir al usuario, mostrar un mensaje de éxito, etc. + alert('¡Registro exitoso! Revisa la consola para más detalles.'); // Reemplazado alert por un mensaje más informativo + + // Opcional: Limpiar el formulario después del envío exitoso + setNombreApellidos(''); + setEmail(''); + setContrasena(''); + setDireccion(''); + setTieneVehiculo(false); + setConsumoVehiculoKm(''); + setCoordenadas(''); + + } catch (error) { + console.error('Error al registrar:', error); + // Aquí puedes mostrar un mensaje de error al usuario en la UI + alert(`Error al registrar: ${error.message}`); // Usando alert temporalmente para el error + } + }; // <-- Cierre correcto del handleSubmit + useEffect(() => { + setCoordenadas(store.lastSelectedCoordinates) + + }, [store.lastSelectedCoordinates]); + return ( +
+

+ Registro de Usuario Agrícola +

+
+ {/* Nombre y Apellidos */} +
+ + setNombreApellidos(e.target.value)} + required + style={inputStyles} + placeholder="Ej: Juan Pérez" + /> +
+ + {/* Email */} +
+ + setEmail(e.target.value)} + required + style={inputStyles} + placeholder="ejemplo@dominio.com" + /> +
+ + {/* Contraseña */} +
+ + setContrasena(e.target.value)} + required + style={inputStyles} + placeholder="Mínimo 8 caracteres" + /> +
+ + {/* Dirección */} +
+ + setDireccion(e.target.value)} + required + style={inputStyles} + placeholder="Ej: Calle del Campo 123, Pueblo, Provincia" + /> +
+ + {/* Checkbox: ¿Tiene vehículo de transporte? */} +
+ setTieneVehiculo(e.target.checked)} + style={checkboxInputStyles} + /> + +
+ + {/* Consumo del vehículo (condicional) */} + {tieneVehiculo && ( +
+ + setConsumoVehiculoKm(e.target.value)} + required={tieneVehiculo} // Es requerido solo si el checkbox está marcado + style={inputStyles} + placeholder="Ej: 8.5" + step="0.1" // Permite números decimales + /> +
+ )} + + + {/* Coordenadas del punto de compraventa y el MAPA */} +
+ + setCoordenadas(e.target.value)} + required + style={inputStyles} + placeholder="Arrastra el marcador en el mapa o introduce aquí" + /> +
+
+ + +
+ {/* Botón de Registro */} + +
+
+ ); +}; diff --git a/src/front/main.jsx b/src/front/main.jsx index a5a3c781dc..5d2040c14f 100644 --- a/src/front/main.jsx +++ b/src/front/main.jsx @@ -5,14 +5,10 @@ import { RouterProvider } from "react-router-dom"; // Import RouterProvider to import { router } from "./routes"; // Import the router configuration import { StoreProvider } from './hooks/useGlobalReducer'; // Import the StoreProvider for global state management import { BackendURL } from './components/BackendURL'; +import {APIProvider, useMap} from '@vis.gl/react-google-maps'; const Main = () => { - if(! import.meta.env.VITE_BACKEND_URL || import.meta.env.VITE_BACKEND_URL == "") return ( - - - - ); return ( {/* Provide global state to all components */} diff --git a/src/front/pages/Registro.jsx b/src/front/pages/Registro.jsx new file mode 100644 index 0000000000..264a699064 --- /dev/null +++ b/src/front/pages/Registro.jsx @@ -0,0 +1,16 @@ +import { Outlet } from "react-router-dom/dist" +import ScrollToTop from "../components/ScrollToTop" +import { Navbar } from "../components/Navbar" +import { Footer } from "../components/Footer" +import { RegistroForm } from "../components/RegistroForm" +import {APIProvider, useMap} from '@vis.gl/react-google-maps'; + +// Base component that maintains the navbar and footer throughout the page and the scroll to top functionality. +export const Registro = () => { + return ( + + + + + ) +} \ No newline at end of file diff --git a/src/front/routes.jsx b/src/front/routes.jsx index 0557df6141..53b11db60b 100644 --- a/src/front/routes.jsx +++ b/src/front/routes.jsx @@ -9,6 +9,7 @@ import { Layout } from "./pages/Layout"; import { Home } from "./pages/Home"; import { Single } from "./pages/Single"; import { Demo } from "./pages/Demo"; +import { Registro } from "./pages/Registro"; export const router = createBrowserRouter( createRoutesFromElements( @@ -25,6 +26,7 @@ export const router = createBrowserRouter( } /> } /> {/* Dynamic route for single items */} } /> + } /> ) ); \ No newline at end of file diff --git a/src/front/store.js b/src/front/store.js index 3062cd222d..2fa7bbcc7a 100644 --- a/src/front/store.js +++ b/src/front/store.js @@ -1,17 +1,14 @@ export const initialStore=()=>{ return{ message: null, - todos: [ - { - id: 1, - title: "Make the bed", - background: null, - }, - { - id: 2, - title: "Do my homework", - background: null, - } + ofertas: [ + + ], + usuarios: [ + + ], + lastSelectedCoordinates:[ + ] } } @@ -23,16 +20,30 @@ export default function storeReducer(store, action = {}) { ...store, message: action.payload }; - - case 'add_task': - const { id, color } = action.payload + case 'add_oferta': + const nuevaOferta = action.payload + + return{ + ...store, + ofertas: [...store.ofertas, nuevaOferta] + }; - return { - ...store, - todos: store.todos.map((todo) => (todo.id === id ? { ...todo, background: color } : todo)) + case 'add_usuario': + const nuevoUsuario = action.payload + + return{ + ...store, + usuarios: [...store.usuarios, nuevoUsuario] + } + case 'add_coordenates': + const nuevaCordenate = action.payload + + return{ + lastSelectedCoordinates:nuevaCordenate }; default: throw Error('Unknown action.'); + } }