From cbe24792e52856fa139fc20a024e77ab7be6176d Mon Sep 17 00:00:00 2001 From: zoscra Date: Thu, 17 Jul 2025 20:58:51 +0000 Subject: [PATCH 01/22] Models y rutas basicas --- migrations/versions/0763d677d453_.py | 35 -------- migrations/versions/244e2e3c2edb_.py | 57 +++++++++++++ migrations/versions/d4369418a589_.py | 34 ++++++++ src/api/models.py | 44 +++++++++- src/api/routes.py | 116 ++++++++++++++++++++++++++- src/app.py | 6 ++ 6 files changed, 253 insertions(+), 39 deletions(-) delete mode 100644 migrations/versions/0763d677d453_.py create mode 100644 migrations/versions/244e2e3c2edb_.py create mode 100644 migrations/versions/d4369418a589_.py 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/244e2e3c2edb_.py b/migrations/versions/244e2e3c2edb_.py new file mode 100644 index 0000000000..38a9da9ae8 --- /dev/null +++ b/migrations/versions/244e2e3c2edb_.py @@ -0,0 +1,57 @@ +"""empty message + +Revision ID: 244e2e3c2edb +Revises: +Create Date: 2025-07-17 19:17:05.336777 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '244e2e3c2edb' +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('coordenates'), + 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(['coordenates_comprador'], ['user.coordenates'], ), + sa.ForeignKeyConstraint(['coordenates_vendedor'], ['user.coordenates'], ), + sa.ForeignKeyConstraint(['id_comprador'], ['user.id'], ), + sa.ForeignKeyConstraint(['id_vendedor'], ['user.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('id_comprador'), + sa.UniqueConstraint('id_vendedor') + ) + # ### 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/migrations/versions/d4369418a589_.py b/migrations/versions/d4369418a589_.py new file mode 100644 index 0000000000..905e09d331 --- /dev/null +++ b/migrations/versions/d4369418a589_.py @@ -0,0 +1,34 @@ +"""empty message + +Revision ID: d4369418a589 +Revises: 244e2e3c2edb +Create Date: 2025-07-17 19:33:59.832371 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'd4369418a589' +down_revision = '244e2e3c2edb' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('oferta', schema=None) as batch_op: + batch_op.drop_constraint('oferta_id_comprador_key', type_='unique') + batch_op.drop_constraint('oferta_id_vendedor_key', type_='unique') + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('oferta', schema=None) as batch_op: + batch_op.create_unique_constraint('oferta_id_vendedor_key', ['id_vendedor']) + batch_op.create_unique_constraint('oferta_id_comprador_key', ['id_comprador']) + + # ### end Alembic commands ### diff --git a/src/api/models.py b/src/api/models.py index da515f6a1a..fc3eeadd73 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, unique=True) + 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),ForeignKey("user.coordenates"),nullable=False) + coordenates_comprador: Mapped[str] = mapped_column(String(120),ForeignKey("user.coordenates"),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..503bca19fd 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,111 @@ 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 NO FUNCIONA +@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 No FUNCIONA + +@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) + oferta = Oferta.query.get(oferta_id) + + if user is None: + return jsonify("Usuario no valido"),400 + + return jsonify(oferta.serialize()) + + +# 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) From 2dd4bfc9a4670c77ce54c813c42d412311677fa5 Mon Sep 17 00:00:00 2001 From: daviidgodino Date: Fri, 18 Jul 2025 00:12:10 +0000 Subject: [PATCH 02/22] login y register --- package-lock.json | 50 ++++++++--------- package.json | 28 +++++----- src/front/AppRouter.jsx | 15 +++++ src/front/pages/Login.jsx | 44 +++++++++++++++ src/front/pages/Register.jsx | 105 +++++++++++++++++++++++++++++++++++ 5 files changed, 203 insertions(+), 39 deletions(-) create mode 100644 src/front/AppRouter.jsx create mode 100644 src/front/pages/Login.jsx create mode 100644 src/front/pages/Register.jsx diff --git a/package-lock.json b/package-lock.json index 8d43d98ab7..3d02171c55 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "prop-types": "^15.8.1", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^6.18.0" + "react-router-dom": "^6.30.1" }, "devDependencies": { "@types/react": "^18.2.18", @@ -944,9 +944,9 @@ } }, "node_modules/@remix-run/router": { - "version": "1.22.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.22.0.tgz", - "integrity": "sha512-MBOl8MeOzpK0HQQQshKB7pABXbmyHizdTpqnrIseTbsv0nAepwC2ENZa1aaBExNQcpLoXmWthhak8SABLzvGPw==", + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", + "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", "license": "MIT", "engines": { "node": ">=14.0.0" @@ -3522,12 +3522,12 @@ } }, "node_modules/react-router": { - "version": "6.29.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.29.0.tgz", - "integrity": "sha512-DXZJoE0q+KyeVw75Ck6GkPxFak63C4fGqZGNijnWgzB/HzSP1ZfTlBj5COaGWwhrMQ/R8bXiq5Ooy4KG+ReyjQ==", + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz", + "integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.22.0" + "@remix-run/router": "1.23.0" }, "engines": { "node": ">=14.0.0" @@ -3537,13 +3537,13 @@ } }, "node_modules/react-router-dom": { - "version": "6.29.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.29.0.tgz", - "integrity": "sha512-pkEbJPATRJ2iotK+wUwHfy0xs2T59YPEN8BQxVCPeBZvK7kfPESRc/nyxzdcxR17hXgUPYx2whMwl+eo9cUdnQ==", + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz", + "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.22.0", - "react-router": "6.29.0" + "@remix-run/router": "1.23.0", + "react-router": "6.30.1" }, "engines": { "node": ">=14.0.0" @@ -4999,9 +4999,9 @@ } }, "@remix-run/router": { - "version": "1.22.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.22.0.tgz", - "integrity": "sha512-MBOl8MeOzpK0HQQQshKB7pABXbmyHizdTpqnrIseTbsv0nAepwC2ENZa1aaBExNQcpLoXmWthhak8SABLzvGPw==" + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", + "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==" }, "@types/babel__core": { "version": "7.20.5", @@ -6727,20 +6727,20 @@ "dev": true }, "react-router": { - "version": "6.29.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.29.0.tgz", - "integrity": "sha512-DXZJoE0q+KyeVw75Ck6GkPxFak63C4fGqZGNijnWgzB/HzSP1ZfTlBj5COaGWwhrMQ/R8bXiq5Ooy4KG+ReyjQ==", + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz", + "integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==", "requires": { - "@remix-run/router": "1.22.0" + "@remix-run/router": "1.23.0" } }, "react-router-dom": { - "version": "6.29.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.29.0.tgz", - "integrity": "sha512-pkEbJPATRJ2iotK+wUwHfy0xs2T59YPEN8BQxVCPeBZvK7kfPESRc/nyxzdcxR17hXgUPYx2whMwl+eo9cUdnQ==", + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz", + "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==", "requires": { - "@remix-run/router": "1.22.0", - "react-router": "6.29.0" + "@remix-run/router": "1.23.0", + "react-router": "6.30.1" } }, "reflect.getprototypeof": { diff --git a/package.json b/package.json index 0caab10749..280939da9f 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": [ @@ -55,8 +55,8 @@ }, "dependencies": { "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.30.1" } } diff --git a/src/front/AppRouter.jsx b/src/front/AppRouter.jsx new file mode 100644 index 0000000000..dfb386bd97 --- /dev/null +++ b/src/front/AppRouter.jsx @@ -0,0 +1,15 @@ +import React from "react"; +import { BrowserRouter, Routes, Route } from "react-router-dom"; +import { Login } from "./pages/Login"; +import { Register } from "./pages/Register"; + +export const AppRouter = () => { + return ( + + + } /> + } /> + + + ); +}; \ No newline at end of file diff --git a/src/front/pages/Login.jsx b/src/front/pages/Login.jsx new file mode 100644 index 0000000000..cbb1d8bcdb --- /dev/null +++ b/src/front/pages/Login.jsx @@ -0,0 +1,44 @@ +import React, { useState } from "react"; +import { Link } from "react-router-dom"; + +export const Login = () => { + const [form, setForm] = useState({ email: "", password: "" }); + + const handleChange = e => { + setForm({ ...form, [e.target.name]: e.target.value }); + }; + + const handleSubmit = e => { + e.preventDefault(); + console.log(form); // aquí meteré el fetch al navbar© + }; + + return ( + <> +
+ + + +
+ +

+ No estás registrado?{" "} + + + +

+ + ); +}; \ No newline at end of file diff --git a/src/front/pages/Register.jsx b/src/front/pages/Register.jsx new file mode 100644 index 0000000000..02594704d1 --- /dev/null +++ b/src/front/pages/Register.jsx @@ -0,0 +1,105 @@ +import React, { useState } from "react"; +import { useNavigate } from "react-router-dom"; + +export const Register = () => { + const [form, setForm] = useState({ + fullName: "", + email: "", + password: "", + coordinates: "", + hasTransport: false + }); + + const [message, setMessage] = useState(null); + const [error, setError] = useState(null); + const [sending, setSending] = useState(false); + const navigate = useNavigate(); + + const handleChange = e => { + const { name, value, type, checked } = e.target; + setForm({ ...form, [name]: type === "checkbox" ? checked : value }); + }; + + const handleSubmit = async e => { + e.preventDefault(); + setError(null); + setMessage(null); + + if (!form.fullName || !form.email || !form.password) { + setError("Por favor, completa todos los campos obligatorios."); + return; + } + + if (!form.email.includes("@")) { + setError("El email no es válido."); + return; + } + + setSending(true); + + const resp = await fetch("FALTA EL LINKKKK!!!!!!!!!", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(form) + }); + + const data = await resp.json(); + + if (resp.ok) { + setMessage("Usuario registrado correctamente."); + setTimeout(() => navigate("/login"), 2000); + } else { + setError(data.msg || "Error al registrar."); + } + + setSending(false); + }; + + return ( +
+ + + + + + + + + {message &&

{message}

} + {error &&

{error}

} +
+ ); +}; From acca8c903dd9405b06cf7b9d6edb928a1e5d3382 Mon Sep 17 00:00:00 2001 From: daviidgodino Date: Fri, 18 Jul 2025 08:20:43 +0000 Subject: [PATCH 03/22] LOGINRegister --- src/front/pages/Register.jsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/front/pages/Register.jsx b/src/front/pages/Register.jsx index 02594704d1..ffc0627361 100644 --- a/src/front/pages/Register.jsx +++ b/src/front/pages/Register.jsx @@ -94,10 +94,6 @@ export const Register = () => { /> - - {message &&

{message}

} {error &&

{error}

} From 02213d0c86ac95d5a36407bedd3797ee6a28b806 Mon Sep 17 00:00:00 2001 From: zoscra Date: Fri, 18 Jul 2025 09:00:17 +0000 Subject: [PATCH 04/22] Models y rutas basicas arregladas --- src/api/routes.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/api/routes.py b/src/api/routes.py index 503bca19fd..62f143f224 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -75,7 +75,7 @@ def get_user(): return jsonify({"user":user.serialize()}) -# GET pedir informacion sobre todas las ofertas disponibles de todos los usuarios NO FUNCIONA +# GET pedir informacion sobre todas las ofertas disponibles de todos los usuarios @api.route("/user/ofertas", methods=["GET"]) @jwt_required() def get_ofertas(): @@ -89,19 +89,26 @@ def get_ofertas(): return jsonify("No hay ofertas disponibles") return jsonify(iterar_ofertas) -# GET pedir informacion sobre una oferta No FUNCIONA +# 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) - oferta = Oferta.query.get(oferta_id) - 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.serialize()) + return jsonify(oferta_serializada) # POST crear una nueva oferta From 21cdf1b5db539b86fb0e0ae06efd81c33bede6e1 Mon Sep 17 00:00:00 2001 From: CarlosAguayo1 Date: Fri, 18 Jul 2025 10:40:11 +0000 Subject: [PATCH 05/22] pagina principal --- Pipfile | 1 + Pipfile.lock | 60 ++++++++- src/front/components/Navbar.jsx | 31 ++--- src/front/main.jsx | 1 + src/front/pages/Home.jsx | 219 +++++++++++++++++++++++++------- 5 files changed, 244 insertions(+), 68 deletions(-) diff --git a/Pipfile b/Pipfile index 44e04f14ff..921f9d2b89 100644 --- a/Pipfile +++ b/Pipfile @@ -20,6 +20,7 @@ typing-extensions = "*" flask-jwt-extended = "==4.6.0" wtforms = "==3.1.2" sqlalchemy = "*" +bcrypt = "*" [requires] python_version = "3.13" diff --git a/Pipfile.lock b/Pipfile.lock index b201c3decc..e56f1c489e 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "d2e672e650278aeeee2fe49bd76d76497d8b65a50f8b5dbb121d265cbc6ef4e5" + "sha256": "dc64a1f0dd7551608933a91632e5eb1245b4768aa8ddf4f8f02467a6f71e4744" }, "pipfile-spec": 6, "requires": { @@ -24,6 +24,64 @@ "markers": "python_version >= '3.8'", "version": "==1.14.1" }, + "bcrypt": { + "hashes": [ + "sha256:0042b2e342e9ae3d2ed22727c1262f76cc4f345683b5c1715f0250cf4277294f", + "sha256:0142b2cb84a009f8452c8c5a33ace5e3dfec4159e7735f5afe9a4d50a8ea722d", + "sha256:08bacc884fd302b611226c01014eca277d48f0a05187666bca23aac0dad6fe24", + "sha256:0d3efb1157edebfd9128e4e46e2ac1a64e0c1fe46fb023158a407c7892b0f8c3", + "sha256:0e30e5e67aed0187a1764911af023043b4542e70a7461ad20e837e94d23e1d6c", + "sha256:107d53b5c67e0bbc3f03ebf5b030e0403d24dda980f8e244795335ba7b4a027d", + "sha256:12fa6ce40cde3f0b899729dbd7d5e8811cb892d31b6f7d0334a1f37748b789fd", + "sha256:17a854d9a7a476a89dcef6c8bd119ad23e0f82557afbd2c442777a16408e614f", + "sha256:191354ebfe305e84f344c5964c7cd5f924a3bfc5d405c75ad07f232b6dffb49f", + "sha256:2ef6630e0ec01376f59a006dc72918b1bf436c3b571b80fa1968d775fa02fe7d", + "sha256:3004df1b323d10021fda07a813fd33e0fd57bef0e9a480bb143877f6cba996fe", + "sha256:335a420cfd63fc5bc27308e929bee231c15c85cc4c496610ffb17923abf7f231", + "sha256:33752b1ba962ee793fa2b6321404bf20011fe45b9afd2a842139de3011898fef", + "sha256:3a3fd2204178b6d2adcf09cb4f6426ffef54762577a7c9b54c159008cb288c18", + "sha256:3b8d62290ebefd49ee0b3ce7500f5dbdcf13b81402c05f6dafab9a1e1b27212f", + "sha256:3e36506d001e93bffe59754397572f21bb5dc7c83f54454c990c74a468cd589e", + "sha256:41261d64150858eeb5ff43c753c4b216991e0ae16614a308a15d909503617732", + "sha256:50e6e80a4bfd23a25f5c05b90167c19030cf9f87930f7cb2eacb99f45d1c3304", + "sha256:531457e5c839d8caea9b589a1bcfe3756b0547d7814e9ce3d437f17da75c32b0", + "sha256:55a935b8e9a1d2def0626c4269db3fcd26728cbff1e84f0341465c31c4ee56d8", + "sha256:57967b7a28d855313a963aaea51bf6df89f833db4320da458e5b3c5ab6d4c938", + "sha256:584027857bc2843772114717a7490a37f68da563b3620f78a849bcb54dc11e62", + "sha256:59e1aa0e2cd871b08ca146ed08445038f42ff75968c7ae50d2fdd7860ade2180", + "sha256:5bd3cca1f2aa5dbcf39e2aa13dd094ea181f48959e1071265de49cc2b82525af", + "sha256:5c1949bf259a388863ced887c7861da1df681cb2388645766c89fdfd9004c669", + "sha256:62f26585e8b219cdc909b6a0069efc5e4267e25d4a3770a364ac58024f62a761", + "sha256:67a561c4d9fb9465ec866177e7aebcad08fe23aaf6fbd692a6fab69088abfc51", + "sha256:6fb1fd3ab08c0cbc6826a2e0447610c6f09e983a281b919ed721ad32236b8b23", + "sha256:74a8d21a09f5e025a9a23e7c0fd2c7fe8e7503e4d356c0a2c1486ba010619f09", + "sha256:79e70b8342a33b52b55d93b3a59223a844962bef479f6a0ea318ebbcadf71505", + "sha256:7a4be4cbf241afee43f1c3969b9103a41b40bcb3a3f467ab19f891d9bc4642e4", + "sha256:7c03296b85cb87db865d91da79bf63d5609284fc0cab9472fdd8367bbd830753", + "sha256:842d08d75d9fe9fb94b18b071090220697f9f184d4547179b60734846461ed59", + "sha256:864f8f19adbe13b7de11ba15d85d4a428c7e2f344bac110f667676a0ff84924b", + "sha256:97eea7408db3a5bcce4a55d13245ab3fa566e23b4c67cd227062bb49e26c585d", + "sha256:a839320bf27d474e52ef8cb16449bb2ce0ba03ca9f44daba6d93fa1d8828e48a", + "sha256:afe327968aaf13fc143a56a3360cb27d4ad0345e34da12c7290f1b00b8fe9a8b", + "sha256:b4d4e57f0a63fd0b358eb765063ff661328f69a04494427265950c71b992a39a", + "sha256:b6354d3760fcd31994a14c89659dee887f1351a06e5dac3c1142307172a79f90", + "sha256:b693dbb82b3c27a1604a3dff5bfc5418a7e6a781bb795288141e5f80cf3a3492", + "sha256:bdc6a24e754a555d7316fa4774e64c6c3997d27ed2d1964d55920c7c227bc4ce", + "sha256:beeefe437218a65322fbd0069eb437e7c98137e08f22c4660ac2dc795c31f8bb", + "sha256:c5eeac541cefd0bb887a371ef73c62c3cd78535e4887b310626036a7c0a817bb", + "sha256:c950d682f0952bafcceaf709761da0a32a942272fad381081b51096ffa46cea1", + "sha256:d9af79d322e735b1fc33404b5765108ae0ff232d4b54666d46730f8ac1a43676", + "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b", + "sha256:e965a9c1e9a393b8005031ff52583cedc15b7884fce7deb8b0346388837d6cfe", + "sha256:f01e060f14b6b57bbb72fc5b4a83ac21c443c9a2ee708e04a10e9192f90a6281", + "sha256:f1e3ffa1365e8702dc48c8b360fef8d7afeca482809c5e45e653af82ccd088c1", + "sha256:f6746e6fec103fcd509b96bacdfdaa2fbde9a553245dbada284435173a6f1aef", + "sha256:f81b0ed2639568bf14749112298f9e4e2b28853dab50a8b357e31798686a036d" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==4.3.0" + }, "blinker": { "hashes": [ "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", diff --git a/src/front/components/Navbar.jsx b/src/front/components/Navbar.jsx index 30d43a2636..55fcda2a79 100644 --- a/src/front/components/Navbar.jsx +++ b/src/front/components/Navbar.jsx @@ -1,19 +1,14 @@ -import { Link } from "react-router-dom"; +import React from 'react'; +import { Link } from 'react-router-dom'; -export const Navbar = () => { - - return ( - - ); -}; \ No newline at end of file +export const Navbar = () => ( + +); \ No newline at end of file diff --git a/src/front/main.jsx b/src/front/main.jsx index a5a3c781dc..168d0e5f34 100644 --- a/src/front/main.jsx +++ b/src/front/main.jsx @@ -1,3 +1,4 @@ + import React from 'react' import ReactDOM from 'react-dom/client' import './index.css' // Global styles for your application diff --git a/src/front/pages/Home.jsx b/src/front/pages/Home.jsx index 341ed21768..b2f118d5d1 100644 --- a/src/front/pages/Home.jsx +++ b/src/front/pages/Home.jsx @@ -1,52 +1,173 @@ -import React, { useEffect } from "react" -import rigoImageUrl from "../assets/img/rigo-baby.jpg"; -import useGlobalReducer from "../hooks/useGlobalReducer.jsx"; +import React, { useEffect, useState } from "react"; +import { Link } from "react-router-dom"; +import useGlobalReducer from '../hooks/useGlobalReducer'; export const Home = () => { + const { store } = useGlobalReducer(); + const [offers, setOffers] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); - const { store, dispatch } = useGlobalReducer() - - const loadMessage = async () => { - try { - const backendUrl = import.meta.env.VITE_BACKEND_URL - - if (!backendUrl) throw new Error("VITE_BACKEND_URL is not defined in .env file") - - const response = await fetch(backendUrl + "/api/hello") - const data = await response.json() - - if (response.ok) dispatch({ type: "set_hello", payload: data.message }) - - return data - - } catch (error) { - if (error.message) throw new Error( - `Could not fetch the message from the backend. - Please check if the backend is running and the backend port is public.` - ); - } - - } - - useEffect(() => { - loadMessage() - }, []) - - return ( -
-

Hello Rigo!!

-

- Rigo Baby -

-
- {store.message ? ( - {store.message} - ) : ( - - Loading message from the backend (make sure your python 🐍 backend is running)... - - )} -
-
- ); -}; \ No newline at end of file + // Form state + const [form, setForm] = useState({ name: "", seller: "", price: "", unit: "", img: "" }); + const [submitting, setSubmitting] = useState(false); + const [submitError, setSubmitError] = useState(null); + + useEffect(() => { + const fetchOffers = async () => { + try { + const backendUrl = import.meta.env.VITE_BACKEND_URL; + if (!backendUrl) throw new Error("VITE_BACKEND_URL is not defined"); + + const res = await fetch(`${backendUrl}/api/offers`); + if (!res.ok) throw new Error(`Error: ${res.statusText}`); + const data = await res.json(); + setOffers(data); + } catch (err) { + setError(err.message); + } finally { + setLoading(false); + } + }; + fetchOffers(); + }, []); + + // Handle input change + const handleChange = e => { + const { name, value } = e.target; + setForm(prev => ({ ...prev, [name]: value })); + }; + + // Submit new offer + const handleSubmit = async e => { + e.preventDefault(); + setSubmitting(true); + setSubmitError(null); + try { + const backendUrl = import.meta.env.VITE_BACKEND_URL; + const res = await fetch(`${backendUrl}/api/offers`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(form) + }); + if (!res.ok) throw new Error(await res.text()); + const newOffer = await res.json(); + setOffers(prev => [newOffer, ...prev]); + setForm({ name: "", seller: "", price: "", unit: "", img: "" }); + } catch (err) { + setSubmitError(err.message); + } finally { + setSubmitting(false); + } + }; + + return ( +
+
+
+
+ {/* Conditionally show form if user is logged in */} + {store.user ? ( + <> +

Crear Nueva Oferta

+ {submitError &&
{submitError}
} +
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+ + ) : ( +
+ Inicia sesión para crear nuevas ofertas. +
+ )} + + {/* Listing */} +

Ofertas

+ {loading &&
Cargando...
} + {error &&
{error}
} + {!loading && !error && ( +
+
+ {offers.map(o => ( +
+ {o.name} +
+
{o.name}
+

Agricultor: {o.seller}

+

€{o.price} / {o.unit}

+
+
+ ))} +
+
+ )} +
+ Mostrar más +
+
+
+
+ Comprar + Vender +
+
+
+ ); +}; From d0539703bfebb7fd0c14addf76cad24acaebca67 Mon Sep 17 00:00:00 2001 From: daviidgodino Date: Sat, 19 Jul 2025 19:10:56 +0000 Subject: [PATCH 06/22] cambiado env --- src/front/pages/Login.jsx | 57 ++++++++++++++++++++++++++++ src/front/pages/Register.jsx | 73 ++++++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 src/front/pages/Login.jsx create mode 100644 src/front/pages/Register.jsx diff --git a/src/front/pages/Login.jsx b/src/front/pages/Login.jsx new file mode 100644 index 0000000000..0a5dfe7da7 --- /dev/null +++ b/src/front/pages/Login.jsx @@ -0,0 +1,57 @@ +import React, { useState } from "react"; +import { Link, useNavigate } from "react-router-dom"; + +export const Login = () => { + const [form, setForm] = useState({ email: "", password: "" }); + const [message, setMessage] = useState(null); + const [error, setError] = useState(null); + const [sending, setSending] = useState(false); + const navigate = useNavigate(); + + const handleChange = e => { + setForm({ ...form, [e.target.name]: e.target.value }); + }; + + const handleSubmit = async e => { + e.preventDefault(); + setMessage(null); + setError(null); + setSending(true); + + const resp = await fetch("FALTA LINK", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(form) + }); + + const data = await resp.json(); + + if (resp.ok) { + setMessage("Login exitoso."); + setTimeout(() => navigate("/"), 2000); // o la ruta a tu dashboard + } else { + setError(data.msg || "Error al iniciar sesión."); + } + + setSending(false); + }; + + return ( + <> +
+ + +
+ +

+ No estás registrado?{" "} + + + +

+ + {message &&

{message}

} + {error &&

{error}

} + + ); +}; diff --git a/src/front/pages/Register.jsx b/src/front/pages/Register.jsx new file mode 100644 index 0000000000..019b0da97a --- /dev/null +++ b/src/front/pages/Register.jsx @@ -0,0 +1,73 @@ +import React, { useState } from "react"; +import { useNavigate } from "react-router-dom"; + +export const Register = () => { + const [form, setForm] = useState({ + fullName: "", + email: "", + password: "", + coordinates: "", + hasTransport: false + }); + + const [message, setMessage] = useState(null); + const [error, setError] = useState(null); + const navigate = useNavigate(); + + const handleChange = e => { + const { name, value, type, checked } = e.target; + setForm({ ...form, [name]: type === "checkbox" ? checked : value }); + }; + + const handleSubmit = async e => { + e.preventDefault(); + setError(null); + setMessage(null); + + if (!form.fullName || !form.email || !form.password) { + setError("Por favor, completa todos los campos obligatorios."); + return; + } + + if (!form.email.includes("@")) { + setError("El email no es válido."); + return; + } + + setSending(true); + + const resp = await fetch("http://localhost:5000/api/register", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(form) + }); + + const data = await resp.json(); + + if (resp.ok) { + setMessage("Usuario registrado correctamente."); + setTimeout(() => navigate("/login"), 2000); + } else { + setError(data.msg || "Error al registrar."); + } + + setSending(false); + }; + + return ( +
+ + + + + + + + + {message &&

{message}

} + {error &&

{error}

} +
+ ); +}; \ No newline at end of file From 50065be37744797b10ea33c0e2c2123f0c169e21 Mon Sep 17 00:00:00 2001 From: daviidgodino Date: Sat, 19 Jul 2025 19:59:17 +0000 Subject: [PATCH 07/22] barrabusqueda --- src/front/pages/ComprarVender.jsx | 65 +++++++++++++++++++++++++++++++ src/front/routes.jsx | 28 ++++++------- 2 files changed, 77 insertions(+), 16 deletions(-) create mode 100644 src/front/pages/ComprarVender.jsx diff --git a/src/front/pages/ComprarVender.jsx b/src/front/pages/ComprarVender.jsx new file mode 100644 index 0000000000..6aa1bd4d93 --- /dev/null +++ b/src/front/pages/ComprarVender.jsx @@ -0,0 +1,65 @@ +import React, { useState } from "react"; + +export const CompraVenta = () => { + const [filters, setFilters] = useState({ + cereal: "", + precioMin: "", + precioMax: "", + ciudad: "" + }); + + const handleChange = (e) => { + setFilters({ ...filters, [e.target.name]: e.target.value }); + }; + + const handleSearch = () => { + console.log("Filtros aplicados:", filters); + }; + + return ( +
+

Búsqueda de cereales para comprar o vender

+ +
+ + + +
+ + +
+ + + + +
+
+ ); +}; diff --git a/src/front/routes.jsx b/src/front/routes.jsx index 0557df6141..cfb94bedb3 100644 --- a/src/front/routes.jsx +++ b/src/front/routes.jsx @@ -1,5 +1,4 @@ // Import necessary components and functions from react-router-dom. - import { createBrowserRouter, createRoutesFromElements, @@ -9,22 +8,19 @@ import { Layout } from "./pages/Layout"; import { Home } from "./pages/Home"; import { Single } from "./pages/Single"; import { Demo } from "./pages/Demo"; +import { Login } from "./pages/Login"; +import { Register } from "./pages/Register"; +import { ComprarVender } from "./pages/ComprarVender"; export const router = createBrowserRouter( createRoutesFromElements( - // CreateRoutesFromElements function allows you to build route elements declaratively. - // Create your routes here, if you want to keep the Navbar and Footer in all views, add your new routes inside the containing Route. - // Root, on the contrary, create a sister Route, if you have doubts, try it! - // Note: keep in mind that errorElement will be the default page when you don't get a route, customize that page to make your project more attractive. - // Note: The child paths of the Layout element replace the Outlet component with the elements contained in the "element" attribute of these child paths. - - // Root Route: All navigation will start from here. - } errorElement={

Not found!

} > - - {/* Nested Routes: Defines sub-routes within the BaseHome component. */} - } /> - } /> {/* Dynamic route for single items */} - } /> - + } errorElement={

Not found!

} > + } /> + } /> + } /> + } /> + } /> + } /> + ) -); \ No newline at end of file +); From 1273e473849c043f449611f323cb3b5afc7281ad Mon Sep 17 00:00:00 2001 From: zoscra Date: Tue, 22 Jul 2025 15:26:56 +0000 Subject: [PATCH 08/22] Version que carga --- src/front/pages/Login.jsx | 44 ---------------- src/front/pages/Register.jsx | 98 ------------------------------------ src/front/routes.jsx | 3 +- 3 files changed, 1 insertion(+), 144 deletions(-) diff --git a/src/front/pages/Login.jsx b/src/front/pages/Login.jsx index f58d0f596b..0a5dfe7da7 100644 --- a/src/front/pages/Login.jsx +++ b/src/front/pages/Login.jsx @@ -55,47 +55,3 @@ export const Login = () => { ); }; -======= -import { Link } from "react-router-dom"; - -export const Login = () => { - const [form, setForm] = useState({ email: "", password: "" }); - - const handleChange = e => { - setForm({ ...form, [e.target.name]: e.target.value }); - }; - - const handleSubmit = e => { - e.preventDefault(); - console.log(form); // aquí meteré el fetch al navbar© - }; - - return ( - <> -
- - - -
- -

- No estás registrado?{" "} - - - -

- - ); -}; \ No newline at end of file diff --git a/src/front/pages/Register.jsx b/src/front/pages/Register.jsx index 3677036d2f..f824ae5d1e 100644 --- a/src/front/pages/Register.jsx +++ b/src/front/pages/Register.jsx @@ -71,101 +71,3 @@ export const Register = () => { ); }; -======= - const [form, setForm] = useState({ - fullName: "", - email: "", - password: "", - coordinates: "", - hasTransport: false - }); - - const [message, setMessage] = useState(null); - const [error, setError] = useState(null); - const [sending, setSending] = useState(false); - const navigate = useNavigate(); - - const handleChange = e => { - const { name, value, type, checked } = e.target; - setForm({ ...form, [name]: type === "checkbox" ? checked : value }); - }; - - const handleSubmit = async e => { - e.preventDefault(); - setError(null); - setMessage(null); - - if (!form.fullName || !form.email || !form.password) { - setError("Por favor, completa todos los campos obligatorios."); - return; - } - - if (!form.email.includes("@")) { - setError("El email no es válido."); - return; - } - - setSending(true); - - const resp = await fetch("FALTA EL LINKKKK!!!!!!!!!", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(form) - }); - - const data = await resp.json(); - - if (resp.ok) { - setMessage("Usuario registrado correctamente."); - setTimeout(() => navigate("/login"), 2000); - } else { - setError(data.msg || "Error al registrar."); - } - - setSending(false); - }; - - return ( -
- - - - - - - {message &&

{message}

} - {error &&

{error}

} -
- ); -}; \ No newline at end of file diff --git a/src/front/routes.jsx b/src/front/routes.jsx index cfb94bedb3..829a3e087d 100644 --- a/src/front/routes.jsx +++ b/src/front/routes.jsx @@ -10,7 +10,7 @@ import { Single } from "./pages/Single"; import { Demo } from "./pages/Demo"; import { Login } from "./pages/Login"; import { Register } from "./pages/Register"; -import { ComprarVender } from "./pages/ComprarVender"; + export const router = createBrowserRouter( createRoutesFromElements( @@ -20,7 +20,6 @@ export const router = createBrowserRouter( } /> } /> } /> - } /> ) ); From 374f10963ea5ea2f7afe8c9c7291c70cf4840e76 Mon Sep 17 00:00:00 2001 From: zoscra Date: Wed, 23 Jul 2025 09:46:01 +0000 Subject: [PATCH 09/22] Register con Api,Navbar,Store Funcional --- package-lock.json | 41 ++- package.json | 1 + src/front/AppRouter.jsx | 2 +- .../components/GoogleMapWithCustomControl.jsx | 59 ++++ src/front/components/Navbar.jsx | 113 +++++++- src/front/pages/Register.jsx | 73 ----- src/front/pages/Registro.jsx | 272 ++++++++++++++++++ src/front/routes.jsx | 4 +- src/front/store.js | 44 +-- 9 files changed, 501 insertions(+), 108 deletions(-) create mode 100644 src/front/components/GoogleMapWithCustomControl.jsx delete mode 100644 src/front/pages/Register.jsx create mode 100644 src/front/pages/Registro.jsx diff --git a/package-lock.json b/package-lock.json index 3d02171c55..e0d5a6b9e1 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 280939da9f..f5879dc9d3 100755 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ ] }, "dependencies": { + "@vis.gl/react-google-maps": "^1.5.4", "prop-types": "^15.8.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/src/front/AppRouter.jsx b/src/front/AppRouter.jsx index dfb386bd97..b6048c67a2 100644 --- a/src/front/AppRouter.jsx +++ b/src/front/AppRouter.jsx @@ -1,7 +1,7 @@ import React from "react"; import { BrowserRouter, Routes, Route } from "react-router-dom"; import { Login } from "./pages/Login"; -import { Register } from "./pages/Register"; +import { Register } from "./pages/Registro"; export const AppRouter = () => { return ( diff --git a/src/front/components/GoogleMapWithCustomControl.jsx b/src/front/components/GoogleMapWithCustomControl.jsx new file mode 100644 index 0000000000..374e4a6a2e --- /dev/null +++ b/src/front/components/GoogleMapWithCustomControl.jsx @@ -0,0 +1,59 @@ +import React, { useEffect, useRef, useState } from 'react'; +import useGlobalReducer from "../hooks/useGlobalReducer"; +import { APIProvider, useMap, Map } from '@vis.gl/react-google-maps'; +import {AdvancedMarker} from '@vis.gl/react-google-maps'; + +const MADRID_LOCATION = { lat: 40.4168, lng: -3.7038 }; +const logoSvgStyles = { + marginRight: '10px', + width: '35px', + height: '35px', + fill: '#ffeb3b', // Amarillo cereal +}; + + +export const GoogleMapWithCustomControl = () => { + + + const {store,dispatch} = useGlobalReducer() + const [coordenadas, setCoordenadas] = useState({ + latitude: MADRID_LOCATION.lat, + longitude: MADRID_LOCATION.lng + }); + + + + + + return ( + + { + const newCoordenates={ + latitude: e.detail.latLng?.lat || 0, + longitude: e.detail.latLng?.lng || 0,} + setCoordenadas(newCoordenates) + dispatch({ + type : "add_coordenates", + payload : newCoordenates + }) + + }} > + + + + + + + + ) +}; + diff --git a/src/front/components/Navbar.jsx b/src/front/components/Navbar.jsx index 55fcda2a79..730dc900e0 100644 --- a/src/front/components/Navbar.jsx +++ b/src/front/components/Navbar.jsx @@ -1,14 +1,103 @@ +import { Link } from "react-router-dom"; + +// src/components/NavbarAgricola.js import React from 'react'; -import { Link } from 'react-router-dom'; - -export const Navbar = () => ( - -); \ No newline at end of file + + ); +}; +export default Navbar; \ No newline at end of file diff --git a/src/front/pages/Register.jsx b/src/front/pages/Register.jsx deleted file mode 100644 index f824ae5d1e..0000000000 --- a/src/front/pages/Register.jsx +++ /dev/null @@ -1,73 +0,0 @@ -import React, { useState } from "react"; -import { useNavigate } from "react-router-dom"; - -export const Register = () => { - const [form, setForm] = useState({ - fullName: "", - email: "", - password: "", - coordinates: "", - hasTransport: false - }); - - const [message, setMessage] = useState(null); - const [error, setError] = useState(null); - const navigate = useNavigate(); - - const handleChange = e => { - const { name, value, type, checked } = e.target; - setForm({ ...form, [name]: type === "checkbox" ? checked : value }); - }; - - const handleSubmit = async e => { - e.preventDefault(); - setError(null); - setMessage(null); - - if (!form.fullName || !form.email || !form.password) { - setError("Por favor, completa todos los campos obligatorios."); - return; - } - - if (!form.email.includes("@")) { - setError("El email no es válido."); - return; - } - - setSending(true); - - const resp = await fetch("http://localhost:5000/api/register", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(form) - }); - - const data = await resp.json(); - - if (resp.ok) { - setMessage("Usuario registrado correctamente."); - setTimeout(() => navigate("/login"), 2000); - } else { - setError(data.msg || "Error al registrar."); - } - - setSending(false); - }; - - return ( -
- - - - - - - - - {message &&

{message}

} - {error &&

{error}

} -
- ); -}; diff --git a/src/front/pages/Registro.jsx b/src/front/pages/Registro.jsx new file mode 100644 index 0000000000..60973633e3 --- /dev/null +++ b/src/front/pages/Registro.jsx @@ -0,0 +1,272 @@ +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 Registro = () => { + const { store, dispatch } = useGlobalReducer() + console.log(store) + // 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 + console.log(store) + + + 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 + }; + dispatch({ + type : "add_usuario", + payload : nuevoUser + }) + + console.log('Datos del formulario de registro (nuevoUser):', nuevoUser); + + try { + // Realiza la petición POST al backend + const response = await fetch("https://animated-pancake-x5pjxq9vv4gj2ppgx-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/routes.jsx b/src/front/routes.jsx index 829a3e087d..3d2d9a086a 100644 --- a/src/front/routes.jsx +++ b/src/front/routes.jsx @@ -9,7 +9,7 @@ import { Home } from "./pages/Home"; import { Single } from "./pages/Single"; import { Demo } from "./pages/Demo"; import { Login } from "./pages/Login"; -import { Register } from "./pages/Register"; +import { Registro } from "./pages/Registro"; export const router = createBrowserRouter( @@ -19,7 +19,7 @@ export const router = createBrowserRouter( } /> } /> } /> - } /> + } /> ) ); diff --git a/src/front/store.js b/src/front/store.js index 3062cd222d..302be08f3d 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,31 @@ 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 { + case 'add_usuario': + const nuevoUsuario = action.payload + + return{ + ...store, + usuarios: [...store.usuarios, nuevoUsuario] + } + case 'add_coordenates': + const nuevaCordenate = action.payload + + return{ ...store, - todos: store.todos.map((todo) => (todo.id === id ? { ...todo, background: color } : todo)) + lastSelectedCoordinates:[nuevaCordenate.latitude,nuevaCordenate.longitude] }; default: throw Error('Unknown action.'); + } } From c8edc421e2be79d9ca97fa3b6231ac5c0428cb52 Mon Sep 17 00:00:00 2001 From: zoscra Date: Wed, 23 Jul 2025 09:56:44 +0000 Subject: [PATCH 10/22] Login Funcional y bonito --- src/front/pages/Login.jsx | 132 +++++++++++++++++++++++++++++++++----- 1 file changed, 117 insertions(+), 15 deletions(-) diff --git a/src/front/pages/Login.jsx b/src/front/pages/Login.jsx index 0a5dfe7da7..543be78f85 100644 --- a/src/front/pages/Login.jsx +++ b/src/front/pages/Login.jsx @@ -18,7 +18,7 @@ export const Login = () => { setError(null); setSending(true); - const resp = await fetch("FALTA LINK", { + const resp = await fetch("https://animated-pancake-x5pjxq9vv4gj2ppgx-3001.app.github.dev/api/user/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(form) @@ -27,31 +27,133 @@ export const Login = () => { const data = await resp.json(); if (resp.ok) { - setMessage("Login exitoso."); - setTimeout(() => navigate("/"), 2000); // o la ruta a tu dashboard + setMessage("¡Ingreso al campo exitoso!"); + setTimeout(() => navigate("/"), 2000); // o la ruta a tu "granja" principal } else { - setError(data.msg || "Error al iniciar sesión."); + setError(data.msg || "¡Terreno resbaladizo! Error al iniciar sesión."); } setSending(false); }; return ( - <> -
- - +
+

Siembra tu Acceso al Cereal

+ + + + -

- No estás registrado?{" "} - - +

+ ¿No tienes tu parcela?{" "} + +

- {message &&

{message}

} - {error &&

{error}

} - + {message &&

{message}

} {/* Verde hoja */} + {error &&

{error}

} {/* Rojo tierra */} +
); }; + +const styles = { + container: { + fontFamily: "'Trebuchet MS', 'Lucida Sans Unicode', 'Lucida Grande', 'Lucida Sans', Arial, sans-serif", + backgroundColor: '#f8f8f0', // Crema suave como el cereal + minHeight: '100vh', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + padding: '20px', + boxSizing: 'border-box', + }, + title: { + color: '#a0522d', // Marrón tierra + marginBottom: '30px', + fontSize: '2.5em', + textAlign: 'center', + textShadow: '1px 1px 2px rgba(0,0,0,0.1)', + }, + form: { + backgroundColor: '#fffbe6', // Blanco hueso, como grano recién cosechado + padding: '40px', + borderRadius: '15px', + boxShadow: '0 8px 20px rgba(0, 0, 0, 0.1)', + display: 'flex', + flexDirection: 'column', + gap: '20px', + maxWidth: '400px', + width: '100%', + border: '1px solid #dcdcdc', // Borde suave + }, + input: { + padding: '12px 15px', + border: '1px solid #b8a68c', // Borde de color paja + borderRadius: '8px', + fontSize: '1em', + backgroundColor: '#ffffff', + color: '#4a4a4a', + transition: 'border-color 0.3s ease, box-shadow 0.3s ease', + }, + button: { + backgroundColor: '#ffeb3b', // Amarillo brillante, como el sol o el cereal + color: '#6b4e2f', // Marrón oscuro para el texto + padding: '14px 25px', + borderRadius: '8px', + border: 'none', + fontSize: '1.1em', + fontWeight: 'bold', + cursor: 'pointer', + transition: 'background-color 0.3s ease, transform 0.2s ease', + boxShadow: '0 4px 10px rgba(0,0,0,0.1)', + }, + registerText: { + marginTop: '25px', + color: '#555', // Gris neutro + fontSize: '1em', + }, + link: { + textDecoration: 'none', + }, + registerButton: { + backgroundColor: '#8bc34a', // Verde campo + color: 'white', + padding: '10px 20px', + borderRadius: '8px', + border: 'none', + fontSize: '1em', + fontWeight: 'bold', + cursor: 'pointer', + transition: 'background-color 0.3s ease, transform 0.2s ease', + marginLeft: '10px', + }, + message: { + marginTop: '20px', + padding: '12px 20px', + borderRadius: '8px', + fontWeight: 'bold', + backgroundColor: 'rgba(255, 255, 255, 0.9)', + boxShadow: '0 2px 8px rgba(0,0,0,0.08)', + }, +}; \ No newline at end of file From e0546f0b4c61af7b3c7b2919bbad0b956ca0439d Mon Sep 17 00:00:00 2001 From: zoscra Date: Thu, 24 Jul 2025 14:03:48 +0000 Subject: [PATCH 11/22] ESTABLE --- src/api/routes.py | 11 ++-- src/front/components/Navbar.jsx | 107 ++++++++++++++++++++++---------- src/front/pages/Home.jsx | 37 ++++++++--- src/front/pages/Login.jsx | 10 +++ src/front/store.js | 16 +++++ 5 files changed, 130 insertions(+), 51 deletions(-) diff --git a/src/api/routes.py b/src/api/routes.py index 62f143f224..74eb28271b 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -77,17 +77,14 @@ def get_user(): # 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) + return jsonify("No hay ofertas disponibles"),404 + return jsonify({"ofertas": iterar_ofertas}),200 # GET pedir informacion sobre una oferta diff --git a/src/front/components/Navbar.jsx b/src/front/components/Navbar.jsx index 730dc900e0..625e48579a 100644 --- a/src/front/components/Navbar.jsx +++ b/src/front/components/Navbar.jsx @@ -1,7 +1,7 @@ -import { Link } from "react-router-dom"; +import React, { useEffect, useState } from "react"; +import { Link,useNavigate } from "react-router-dom"; +import useGlobalReducer from '../hooks/useGlobalReducer'; -// src/components/NavbarAgricola.js -import React from 'react'; // --- Estilos CSS en línea para la Navbar --- const navbarContainerStyles = { @@ -56,48 +56,87 @@ const linkHoverStyles = { backgroundColor: '#689f38', // Verde más oscuro al pasar el ratón transform: 'translateY(-2px)', }; +const buttonStyles = { + ...linkStyles, // Inherit base link styles + border: 'none', + backgroundColor: 'transparent', // Make button background transparent by default + // Specific styles for hover/active states if needed for buttons +}; + +const buttonHoverStyles = { + backgroundColor: '#689f38', // Darker green on hover + transform: 'translateY(-2px)', +}; // --- Componente NavbarAgricola --- export const Navbar = () => { - return ( + const navigate = useNavigate(); + const { store, dispatch } = useGlobalReducer(); + + const handleLogout = () => { + + dispatch({ type: 'eliminar_usuario' }); // Dispatch the action + localStorage.removeItem('jwt_token'); + // 3. Redirect to login page + navigate('/login'); + console.log('User logged out.'); // For debugging + }; + + return ( ); }; + export default Navbar; \ No newline at end of file diff --git a/src/front/pages/Home.jsx b/src/front/pages/Home.jsx index b2f118d5d1..2912220679 100644 --- a/src/front/pages/Home.jsx +++ b/src/front/pages/Home.jsx @@ -3,11 +3,11 @@ import { Link } from "react-router-dom"; import useGlobalReducer from '../hooks/useGlobalReducer'; export const Home = () => { - const { store } = useGlobalReducer(); + const { store,dispatch } = useGlobalReducer(); const [offers, setOffers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - +console.log(offers) // Form state const [form, setForm] = useState({ name: "", seller: "", price: "", unit: "", img: "" }); const [submitting, setSubmitting] = useState(false); @@ -18,11 +18,13 @@ export const Home = () => { try { const backendUrl = import.meta.env.VITE_BACKEND_URL; if (!backendUrl) throw new Error("VITE_BACKEND_URL is not defined"); - - const res = await fetch(`${backendUrl}/api/offers`); - if (!res.ok) throw new Error(`Error: ${res.statusText}`); + + const res = await fetch(`https://animated-pancake-x5pjxq9vv4gj2ppgx-3001.app.github.dev/api/user/ofertas`); + console.log(res) + const data = await res.json(); - setOffers(data); + console.log(data) + setOffers(data.ofertas); } catch (err) { setError(err.message); } finally { @@ -44,14 +46,29 @@ export const Home = () => { setSubmitting(true); setSubmitError(null); try { - const backendUrl = import.meta.env.VITE_BACKEND_URL; - const res = await fetch(`${backendUrl}/api/offers`, { + const token = store.token + const res = await fetch(`https://animated-pancake-x5pjxq9vv4gj2ppgx-3001.app.github.dev/api/user/ofertas`, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { + 'Content-Type': 'application/json', + "Authorization": `Bearer ${token}` + }, body: JSON.stringify(form) + }); + const resUser = await fetch(`https://animated-pancake-x5pjxq9vv4gj2ppgx-3001.app.github.dev/api/user`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + "Authorization": `Bearer ${token}` + }, }); if (!res.ok) throw new Error(await res.text()); const newOffer = await res.json(); + const usuarioLogeado = await resUser.json(); + dispatch({ + type : "add_user", + payload : usuarioLogeado + }) setOffers(prev => [newOffer, ...prev]); setForm({ name: "", seller: "", price: "", unit: "", img: "" }); } catch (err) { @@ -67,7 +84,7 @@ export const Home = () => {
{/* Conditionally show form if user is logged in */} - {store.user ? ( + {store.usuarios ? ( <>

Crear Nueva Oferta

{submitError &&
{submitError}
} diff --git a/src/front/pages/Login.jsx b/src/front/pages/Login.jsx index 543be78f85..58a44cb71a 100644 --- a/src/front/pages/Login.jsx +++ b/src/front/pages/Login.jsx @@ -1,7 +1,9 @@ import React, { useState } from "react"; import { Link, useNavigate } from "react-router-dom"; +import useGlobalReducer from '../hooks/useGlobalReducer'; export const Login = () => { + const { store,dispatch } = useGlobalReducer(); const [form, setForm] = useState({ email: "", password: "" }); const [message, setMessage] = useState(null); const [error, setError] = useState(null); @@ -25,15 +27,23 @@ export const Login = () => { }); const data = await resp.json(); + if (resp.ok) { setMessage("¡Ingreso al campo exitoso!"); + setTimeout(() => navigate("/"), 2000); // o la ruta a tu "granja" principal + } else { setError(data.msg || "¡Terreno resbaladizo! Error al iniciar sesión."); } setSending(false); + + dispatch({ + type : "add_token", + payload : data.token + }) }; return ( diff --git a/src/front/store.js b/src/front/store.js index 302be08f3d..96518a4dd8 100644 --- a/src/front/store.js +++ b/src/front/store.js @@ -9,6 +9,9 @@ export const initialStore=()=>{ ], lastSelectedCoordinates:[ + ], + token:[ + ] } } @@ -43,6 +46,19 @@ export default function storeReducer(store, action = {}) { ...store, lastSelectedCoordinates:[nuevaCordenate.latitude,nuevaCordenate.longitude] }; + case 'add_token': + const newToken = action.payload + + return{ + ...store, + token:[newToken] + }; + case "eliminar_usuario": + return{ + ...store, + usuarios:null, + token:null + } default: throw Error('Unknown action.'); From 466fb529ca39ed518ce1933afb133affaaa48f41 Mon Sep 17 00:00:00 2001 From: zoscra Date: Fri, 25 Jul 2025 09:48:41 +0000 Subject: [PATCH 12/22] Enlazado,registro,login --- src/api/routes.py | 2 +- src/front/components/Navbar.jsx | 6 +++--- src/front/pages/Home.jsx | 2 +- src/front/pages/Login.jsx | 13 ++++++++----- src/front/pages/Registro.jsx | 2 +- src/front/store.js | 26 ++++++++++++++------------ 6 files changed, 28 insertions(+), 23 deletions(-) diff --git a/src/api/routes.py b/src/api/routes.py index 74eb28271b..37232e6e15 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -59,7 +59,7 @@ def user_login(): 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({"token":token},{"user":user_serialize}),200 return jsonify("Usuario logueado"),200 diff --git a/src/front/components/Navbar.jsx b/src/front/components/Navbar.jsx index 625e48579a..602c191c7f 100644 --- a/src/front/components/Navbar.jsx +++ b/src/front/components/Navbar.jsx @@ -78,7 +78,7 @@ export const Navbar = () => { dispatch({ type: 'eliminar_usuario' }); // Dispatch the action localStorage.removeItem('jwt_token'); // 3. Redirect to login page - navigate('/login'); + navigate('/'); console.log('User logged out.'); // For debugging }; @@ -92,10 +92,10 @@ export const Navbar = () => { AgroCereal - {store.usuarios ? ( + {store.user ? ( <> - Hola, {store.usuarios.username || 'Agricultor'}! + Hola, {store.user.username || 'Agricultor'}! + + Hola, {store.user.username || 'Agricultor'}! + + ) : ( @@ -133,8 +133,8 @@ export const Navbar = () => { > Ver todas las ofertas
- )} - + )} + ); }; diff --git a/src/front/pages/Layout.jsx b/src/front/pages/Layout.jsx index 9bfa31325c..2aead051d0 100644 --- a/src/front/pages/Layout.jsx +++ b/src/front/pages/Layout.jsx @@ -2,9 +2,11 @@ import { Outlet } from "react-router-dom/dist" import ScrollToTop from "../components/ScrollToTop" import { Navbar } from "../components/Navbar" import { Footer } from "../components/Footer" +import React, { useEffect, useState } from "react"; // Base component that maintains the navbar and footer throughout the page and the scroll to top functionality. export const Layout = () => { + return ( diff --git a/src/front/pages/Login.jsx b/src/front/pages/Login.jsx index d1eb87ec34..4bdbb5bdbc 100644 --- a/src/front/pages/Login.jsx +++ b/src/front/pages/Login.jsx @@ -27,7 +27,7 @@ export const Login = () => { }); const data = await resp.json(); - localStorage.setItem("jwt-token", data.token); + localStorage.setItem("jwt_token", data.token); dispatch({ type:"add_user", payload:data.user_serialize @@ -43,6 +43,7 @@ export const Login = () => { dispatch({ type:"eliminar_user", }) + localStorage.removeItem('jwt_token'); } setSending(false); From 185a55d7b8bc8521f486e72953e104cee4c8ee2e Mon Sep 17 00:00:00 2001 From: zoscra Date: Wed, 30 Jul 2025 11:09:41 +0000 Subject: [PATCH 14/22] Sistema de creacion de ofertas en home y visualizacion en home --- .../{244e2e3c2edb_.py => 0c381ddbb1d1_.py} | 13 +- migrations/versions/d4369418a589_.py | 34 - src/api/models.py | 7 +- src/api/routes.py | 3 + src/front/pages/Home.jsx | 1420 +++++++++++++++-- src/front/pages/Login.jsx | 3 +- 6 files changed, 1310 insertions(+), 170 deletions(-) rename migrations/versions/{244e2e3c2edb_.py => 0c381ddbb1d1_.py} (86%) delete mode 100644 migrations/versions/d4369418a589_.py diff --git a/migrations/versions/244e2e3c2edb_.py b/migrations/versions/0c381ddbb1d1_.py similarity index 86% rename from migrations/versions/244e2e3c2edb_.py rename to migrations/versions/0c381ddbb1d1_.py index 38a9da9ae8..190a1f3ef8 100644 --- a/migrations/versions/244e2e3c2edb_.py +++ b/migrations/versions/0c381ddbb1d1_.py @@ -1,8 +1,8 @@ """empty message -Revision ID: 244e2e3c2edb +Revision ID: 0c381ddbb1d1 Revises: -Create Date: 2025-07-17 19:17:05.336777 +Create Date: 2025-07-29 12:15:41.441989 """ from alembic import op @@ -10,7 +10,7 @@ # revision identifiers, used by Alembic. -revision = '244e2e3c2edb' +revision = '0c381ddbb1d1' down_revision = None branch_labels = None depends_on = None @@ -39,13 +39,14 @@ def upgrade(): 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.Column('precio_ud', sa.Integer(), nullable=True), + sa.Column('ud', sa.String(length=200), nullable=False), + sa.Column('img_cosecha', sa.String(), nullable=True), sa.ForeignKeyConstraint(['coordenates_comprador'], ['user.coordenates'], ), sa.ForeignKeyConstraint(['coordenates_vendedor'], ['user.coordenates'], ), sa.ForeignKeyConstraint(['id_comprador'], ['user.id'], ), sa.ForeignKeyConstraint(['id_vendedor'], ['user.id'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('id_comprador'), - sa.UniqueConstraint('id_vendedor') + sa.PrimaryKeyConstraint('id') ) # ### end Alembic commands ### diff --git a/migrations/versions/d4369418a589_.py b/migrations/versions/d4369418a589_.py deleted file mode 100644 index 905e09d331..0000000000 --- a/migrations/versions/d4369418a589_.py +++ /dev/null @@ -1,34 +0,0 @@ -"""empty message - -Revision ID: d4369418a589 -Revises: 244e2e3c2edb -Create Date: 2025-07-17 19:33:59.832371 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = 'd4369418a589' -down_revision = '244e2e3c2edb' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('oferta', schema=None) as batch_op: - batch_op.drop_constraint('oferta_id_comprador_key', type_='unique') - batch_op.drop_constraint('oferta_id_vendedor_key', type_='unique') - - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('oferta', schema=None) as batch_op: - batch_op.create_unique_constraint('oferta_id_vendedor_key', ['id_vendedor']) - batch_op.create_unique_constraint('oferta_id_comprador_key', ['id_comprador']) - - # ### end Alembic commands ### diff --git a/src/api/models.py b/src/api/models.py index fc3eeadd73..351b59809b 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -40,6 +40,9 @@ class Oferta(db.Model): titulo: Mapped[str]= mapped_column(String(200),nullable=False) coordenates_vendedor: Mapped[str] = mapped_column(String(120),ForeignKey("user.coordenates"),nullable=False) coordenates_comprador: Mapped[str] = mapped_column(String(120),ForeignKey("user.coordenates"),nullable=True) + precio_ud: Mapped[int] = mapped_column(Integer(),nullable=True) + ud:Mapped[str] = mapped_column(String(200),nullable=False) + img_cosecha:Mapped[str] = mapped_column(String(),nullable=True) @@ -53,7 +56,9 @@ def serialize(self): "descripcion":self.descripcion, "titulo":self.titulo, "coordenates_vendedor":self.coordenates_vendedor, - "coordenates_comprador":self.coordenates_comprador + "coordenates_comprador":self.coordenates_comprador, + "precio_ud":self.precio_ud, + "img_cosecha":self.img_cosecha # 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 37232e6e15..94ad679150 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -124,6 +124,9 @@ def post_ofertas(): nueva_oferta.descripcion = body["descripcion"] nueva_oferta.titulo = body["titulo"] nueva_oferta.coordenates_vendedor = user.coordenates + nueva_oferta.precio_ud = body["precio_ud"] + nueva_oferta.ud = body["ud"] + nueva_oferta.img_cosecha = body["img_cosecha"] db.session.add(nueva_oferta) db.session.commit() diff --git a/src/front/pages/Home.jsx b/src/front/pages/Home.jsx index e28a919d1a..e22e8012ec 100644 --- a/src/front/pages/Home.jsx +++ b/src/front/pages/Home.jsx @@ -1,15 +1,820 @@ import React, { useEffect, useState } from "react"; import { Link } from "react-router-dom"; import useGlobalReducer from '../hooks/useGlobalReducer'; +import { APIProvider, useMap, Map } from '@vis.gl/react-google-maps'; +import {AdvancedMarker} from '@vis.gl/react-google-maps'; + +const beautifulStyles = ` + @import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;500;600;700;800;900&family=Inter:wght@300;400;500;600;700&display=swap'); + + :root { + --campo-verde: #2d5016; + --trigo-dorado: #f4d03f; + --tierra-marron: #8b4513; + --cielo-azul: #87ceeb; + --sol-naranja: #ff8c42; + --oliva-verde: #6b8e23; + --lavanda: #e6e6fa; + --rosa-primavera: #ffb3ba; + --crema: #fef9e7; + } + + * { + box-sizing: border-box; + } + + .campo-container { + background: + linear-gradient(135deg, + #fef9e7 0%, + #f8f4e6 20%, + #e8f5e8 40%, + #e6f3ff 60%, + #fff8dc 80%, + #fef9e7 100% + ); + min-height: 100vh; + position: relative; + font-family: 'Inter', sans-serif; + overflow-x: hidden; + } + + .campo-container::before { + content: ''; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-image: + url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23f4d03f' fill-opacity='0.05'%3E%3Cpath d='M30 30c0-11.046-8.954-20-20-20s-20 8.954-20 20 8.954 20 20 20 20-8.954 20-20zm15 0c0-11.046-8.954-20-20-20s-20 8.954-20 20 8.954 20 20 20 20-8.954 20-20z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E"); + pointer-events: none; + z-index: 1; + } + + .campo-header { + background: + linear-gradient(135deg, rgba(45, 80, 22, 0.95) 0%, rgba(107, 142, 35, 0.9) 100%), + url("data:image/svg+xml,%3Csvg width='100' height='100' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%23f4d03f' fill-opacity='0.1'%3E%3Cpath d='M50 10c22.091 0 40 17.909 40 40S72.091 90 50 90 10 72.091 10 50 27.909 10 50 10zm0 5c19.33 0 35 15.67 35 35S69.33 85 50 85 15 69.33 15 50 30.67 15 50 15z'/%3E%3C/g%3E%3C/svg%3E"); + padding: 4rem 0; + text-align: center; + color: white; + position: relative; + z-index: 10; + margin-bottom: 3rem; + box-shadow: 0 8px 32px rgba(45, 80, 22, 0.3); + } + + .campo-header::after { + content: ''; + position: absolute; + bottom: -20px; + left: 50%; + transform: translateX(-50%); + width: 0; + height: 0; + border-left: 30px solid transparent; + border-right: 30px solid transparent; + border-top: 20px solid var(--oliva-verde); + } + + .titulo-principal { + font-family: 'Playfair Display', serif; + font-size: clamp(2.5rem, 6vw, 4.5rem); + font-weight: 800; + margin-bottom: 1rem; + text-shadow: 2px 2px 4px rgba(0,0,0,0.3); + position: relative; + } + + .titulo-principal::before { + content: '🌾'; + position: absolute; + left: -60px; + top: 50%; + transform: translateY(-50%); + font-size: 3rem; + animation: wheat-sway 3s ease-in-out infinite; + } + + .titulo-principal::after { + content: '🌾'; + position: absolute; + right: -60px; + top: 50%; + transform: translateY(-50%); + font-size: 3rem; + animation: wheat-sway 3s ease-in-out infinite reverse; + } + + @keyframes wheat-sway { + 0%, 100% { transform: translateY(-50%) rotate(-5deg); } + 50% { transform: translateY(-50%) rotate(5deg); } + } + + .subtitulo { + font-size: 1.4rem; + font-weight: 300; + opacity: 0.95; + font-style: italic; + } + + .tarjeta-bella { + background: + linear-gradient(145deg, + rgba(255, 255, 255, 0.95) 0%, + rgba(254, 249, 231, 0.95) 100% + ); + border: 2px solid rgba(244, 208, 63, 0.2); + border-radius: 25px; + box-shadow: + 0 20px 40px rgba(0,0,0,0.1), + 0 0 0 1px rgba(255,255,255,0.8), + inset 0 1px 0 rgba(255,255,255,0.9); + overflow: hidden; + transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); + position: relative; + z-index: 5; + } + + .tarjeta-bella::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 6px; + background: linear-gradient(90deg, + var(--campo-verde) 0%, + var(--trigo-dorado) 25%, + var(--oliva-verde) 50%, + var(--sol-naranja) 75%, + var(--campo-verde) 100% + ); + } + + .tarjeta-bella:hover { + transform: translateY(-12px) scale(1.02); + box-shadow: + 0 30px 60px rgba(0,0,0,0.15), + 0 0 0 1px rgba(244, 208, 63, 0.4), + inset 0 1px 0 rgba(255,255,255,1); + border-color: var(--trigo-dorado); + } + + .formulario-seccion { + padding: 3rem; + position: relative; + } + + .formulario-header { + text-align: center; + margin-bottom: 3rem; + position: relative; + } + + .formulario-titulo { + font-family: 'Playfair Display', serif; + font-size: 2.8rem; + font-weight: 700; + color: var(--campo-verde); + margin-bottom: 1rem; + position: relative; + display: inline-block; + } + + .formulario-titulo::before { + content: ''; + position: absolute; + bottom: -10px; + left: 50%; + transform: translateX(-50%); + width: 60%; + height: 3px; + background: linear-gradient(90deg, transparent, var(--trigo-dorado), transparent); + } + + .formulario-descripcion { + color: var(--oliva-verde); + font-size: 1.2rem; + font-style: italic; + margin-bottom: 0; + } + + .campo-formulario { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 2rem; + margin-bottom: 3rem; + } + + .grupo-campo { + position: relative; + } + + .etiqueta-campo { + display: flex; + align-items: center; + gap: 8px; + font-size: 1rem; + font-weight: 600; + color: var(--campo-verde); + margin-bottom: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + .input-campo { + width: 100%; + padding: 1.2rem 1.5rem; + border: 2px solid rgba(107, 142, 35, 0.2); + border-radius: 15px; + font-size: 1.1rem; + font-family: 'Inter', sans-serif; + background: + linear-gradient(145deg, + rgba(255, 255, 255, 0.9) 0%, + rgba(254, 249, 231, 0.7) 100% + ); + transition: all 0.3s ease; + box-shadow: inset 0 2px 4px rgba(0,0,0,0.05); + } + + .input-campo:focus { + outline: none; + border-color: var(--trigo-dorado); + box-shadow: + 0 0 0 4px rgba(244, 208, 63, 0.2), + inset 0 2px 4px rgba(0,0,0,0.05); + transform: translateY(-2px); + background: rgba(255, 255, 255, 1); + } + + .input-campo::placeholder { + color: rgba(107, 142, 35, 0.6); + font-style: italic; + } + + .boton-principal { + background: + linear-gradient(135deg, + var(--campo-verde) 0%, + var(--oliva-verde) 100% + ); + border: none; + padding: 1.5rem 4rem; + border-radius: 50px; + color: white; + font-family: 'Playfair Display', serif; + font-size: 1.3rem; + font-weight: 600; + cursor: pointer; + transition: all 0.4s ease; + position: relative; + overflow: hidden; + box-shadow: + 0 8px 25px rgba(45, 80, 22, 0.3), + inset 0 1px 0 rgba(255,255,255,0.2); + display: block; + margin: 0 auto; + text-transform: uppercase; + letter-spacing: 1px; + } + + .boton-principal::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent); + transition: left 0.5s; + } + + .boton-principal:hover::before { + left: 100%; + } + + .boton-principal:hover { + transform: translateY(-3px) scale(1.05); + box-shadow: + 0 15px 35px rgba(45, 80, 22, 0.4), + inset 0 1px 0 rgba(255,255,255,0.3); + } + + .ofertas-seccion { + padding: 6rem; + padding-top:1rem + position: relative; + } + + .ofertas-header { + text-align: center; + margin-bottom: 3rem; + position: relative; + } + + .ofertas-titulo { + font-family: 'Playfair Display', serif; + font-size: 3rem; + font-weight: 700; + color: var(--campo-verde); + margin-bottom: 1rem; + position: relative; + display: inline-block; + } + + .ofertas-titulo::before { + content: '🛒'; + position: absolute; + left: -50px; + top: 50%; + transform: translateY(-50%); + font-size: 2.5rem; + animation: basket-bounce 2s ease-in-out infinite; + } + + @keyframes basket-bounce { + 0%, 100% { transform: translateY(-50%); } + 50% { transform: translateY(-70%); } + } + + .contador-ofertas { + background: var(--trigo-dorado); + color: var(--campo-verde); + padding: 8px; + border-radius: 25px; + font-weight: 600; + font-size: 1rem; + box-shadow: 0 4px 15px rgba(244, 208, 63, 0.3); + } + + .ofertas-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(450px, 1fr)); + gap: 2rem; + max-height: 70vh; + overflow-y: auto; + padding-right: 1rem; + } + + .ofertas-grid::-webkit-scrollbar { + width: 12px; + } + + .ofertas-grid::-webkit-scrollbar-track { + background: rgba(244, 208, 63, 0.1); + border-radius: 10px; + } + + .ofertas-grid::-webkit-scrollbar-thumb { + background: linear-gradient(135deg, var(--trigo-dorado), var(--sol-naranja)); + border-radius: 10px; + border: 2px solid rgba(255,255,255,0.3); + } + + .oferta-tarjeta { + background: + linear-gradient(145deg, + rgba(255, 255, 255, 0.95) 0%, + rgba(254, 249, 231, 0.95) 100% + ); + border: 2px solid rgba(244, 208, 63, 0.3); + border-radius: 20px; + overflow: hidden; + transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); + position: relative; + box-shadow: + 0 8px 32px rgba(0,0,0,0.1), + inset 0 1px 0 rgba(255,255,255,0.8); + } + + .oferta-tarjeta::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: linear-gradient(90deg, + var(--rosa-primavera) 0%, + var(--trigo-dorado) 50%, + var(--rosa-primavera) 100% + ); + } + + .oferta-tarjeta:hover { + transform: translateY(-8px) rotateX(2deg); + box-shadow: + 0 20px 45px rgba(0,0,0,0.15), + inset 0 1px 0 rgba(255,255,255,1); + border-color: var(--trigo-dorado); + } + + .mapa-contenedor { + height: 220px; + position: relative; + background: linear-gradient(135deg, var(--cielo-azul), var(--lavanda)); + } + + .mapa-overlay { + position: absolute; + top: 15px; + left: 15px; + background: + linear-gradient(135deg, + rgba(255, 255, 255, 0.95) 0%, + rgba(244, 208, 63, 0.9) 100% + ); + color: var(--campo-verde); + padding: 10px 18px; + border-radius: 25px; + font-weight: 600; + font-size: 0.9rem; + z-index: 1000; + box-shadow: + 0 4px 15px rgba(0,0,0,0.2), + inset 0 1px 0 rgba(255,255,255,0.8); + border: 1px solid rgba(244, 208, 63, 0.3); + animation: location-pulse 3s ease-in-out infinite; + } + + @keyframes location-pulse { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.05); } + } + + .oferta-contenido { + padding: 2rem; + } + + .oferta-titulo { + font-family: 'Playfair Display', serif; + font-size: 1.5rem; + font-weight: 600; + color: var(--campo-verde); + margin-bottom: 1rem; + display: flex; + align-items: center; + gap: 12px; + } + + .oferta-descripcion { + color: var(--oliva-verde); + font-size: 1rem; + line-height: 1.6; + margin-bottom: 1.5rem; + font-style: italic; + } + + .oferta-precio { + background: + linear-gradient(135deg, + var(--sol-naranja) 0%, + var(--trigo-dorado) 100% + ); + color: white; + padding: 12px 24px; + border-radius: 30px; + font-family: 'Playfair Display', serif; + font-weight: 700; + font-size: 1.3rem; + display: inline-flex; + align-items: center; + gap: 8px; + box-shadow: + 0 6px 20px rgba(255, 140, 66, 0.4), + inset 0 1px 0 rgba(255,255,255,0.3); + position: relative; + overflow: hidden; + } + + .oferta-precio::before { + content: ''; + position: absolute; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + background: linear-gradient(45deg, transparent, rgba(255,255,255,0.1), transparent); + animation: price-shine 4s ease-in-out infinite; + } + + @keyframes price-shine { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } + } + + .acciones-sidebar { + padding: 3rem; + display: flex; + flex-direction: column; + gap: 2rem; + } + + .acciones-titulo { + font-family: 'Playfair Display', serif; + font-size: 2.2rem; + font-weight: 700; + text-align: center; + color: var(--campo-verde); + margin-bottom: 2rem; + position: relative; + } + + .acciones-titulo::after { + content: ''; + position: absolute; + bottom: -10px; + left: 50%; + transform: translateX(-50%); + width: 50%; + height: 2px; + background: linear-gradient(90deg, transparent, var(--trigo-dorado), transparent); + } + + .boton-accion { + padding: 2.5rem 2rem; + border-radius: 20px; + text-decoration: none; + text-align: center; + font-family: 'Inter', sans-serif; + font-weight: 600; + font-size: 1.2rem; + transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); + position: relative; + overflow: hidden; + border: 2px solid transparent; + box-shadow: 0 8px 25px rgba(0,0,0,0.1); + } + + .boton-accion::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent); + transition: left 0.6s; + } + + .boton-accion:hover::before { + left: 100%; + } + + .btn-comprar { + background: + linear-gradient(135deg, + var(--campo-verde) 0%, + var(--oliva-verde) 100% + ); + color: white; + margin-bottom: 1.5rem; + } + + .btn-comprar:hover { + transform: translateY(-8px) scale(1.03); + box-shadow: 0 15px 35px rgba(45, 80, 22, 0.4); + color: white; + text-decoration: none; + } + + .btn-vender { + background: + linear-gradient(135deg, + var(--sol-naranja) 0%, + var(--trigo-dorado) 100% + ); + color: white; + } + + .btn-vender:hover { + transform: translateY(-8px) scale(1.03); + box-shadow: 0 15px 35px rgba(255, 140, 66, 0.4); + color: white; + text-decoration: none; + } + + .caracteristicas-tarjeta { + background: + linear-gradient(145deg, + rgba(255, 255, 255, 0.9) 0%, + rgba(232, 245, 232, 0.9) 100% + ); + border: 2px solid rgba(107, 142, 35, 0.2); + border-radius: 20px; + padding: 2rem; + margin-top: 2rem; + box-shadow: + 0 8px 25px rgba(0,0,0,0.08), + inset 0 1px 0 rgba(255,255,255,0.8); + } + + .caracteristicas-titulo { + font-family: 'Playfair Display', serif; + font-size: 1.6rem; + font-weight: 600; + color: var(--campo-verde); + text-align: center; + margin-bottom: 1.5rem; + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + } + + .caracteristicas-lista { + list-style: none; + padding: 0; + margin: 0; + } + + .caracteristicas-lista li { + display: flex; + align-items: center; + gap: 15px; + padding: 12px 0; + color: var(--oliva-verde); + font-size: 1rem; + border-bottom: 1px solid rgba(107, 142, 35, 0.1); + transition: all 0.3s ease; + } + + .caracteristicas-lista li:hover { + color: var(--campo-verde); + transform: translateX(8px); + background: rgba(244, 208, 63, 0.05); + padding-left: 8px; + border-radius: 8px; + } + + .caracteristicas-lista li:last-child { + border-bottom: none; + } + + .caracteristicas-lista li span:first-child { + font-size: 1.5rem; + width: 30px; + text-align: center; + } + + .loading-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 4rem; + color: var(--campo-verde); + } + + .campo-spinner { + width: 60px; + height: 60px; + border: 4px solid rgba(244, 208, 63, 0.2); + border-top: 4px solid var(--trigo-dorado); + border-radius: 50%; + animation: campo-spin 1.5s linear infinite; + margin-bottom: 2rem; + box-shadow: 0 0 20px rgba(244, 208, 63, 0.3); + } + + @keyframes campo-spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } + } + + .loading-text { + font-family: 'Playfair Display', serif; + font-size: 1.3rem; + color: var(--oliva-verde); + text-align: center; + font-style: italic; + } + + .alerta-login { + background: + linear-gradient(135deg, + rgba(135, 206, 235, 0.1) 0%, + rgba(230, 230, 250, 0.1) 100% + ); + border: 2px solid rgba(135, 206, 235, 0.3); + border-radius: 20px; + padding: 3rem; + text-align: center; + box-shadow: + 0 8px 25px rgba(0,0,0,0.08), + inset 0 1px 0 rgba(255,255,255,0.8); + } + + .alerta-login h3 { + font-family: 'Playfair Display', serif; + font-size: 2.2rem; + font-weight: 700; + color: var(--campo-verde); + margin-bottom: 1rem; + } + + .alerta-login p { + color: var(--oliva-verde); + font-size: 1.2rem; + margin-bottom: 2rem; + font-style: italic; + } + + .boton-login { + background: + linear-gradient(135deg, + var(--cielo-azul) 0%, + var(--lavanda) 100% + ); + color: var(--campo-verde); + padding: 1.2rem 2.5rem; + border-radius: 25px; + text-decoration: none; + font-family: 'Playfair Display', serif; + font-weight: 600; + font-size: 1.2rem; + transition: all 0.3s ease; + display: inline-block; + box-shadow: + 0 6px 20px rgba(135, 206, 235, 0.4), + inset 0 1px 0 rgba(255,255,255,0.8); + border: 1px solid rgba(135, 206, 235, 0.3); + } + + .boton-login:hover { + transform: translateY(-3px) scale(1.05); + box-shadow: + 0 10px 25px rgba(135, 206, 235, 0.6), + inset 0 1px 0 rgba(255,255,255,1); + color: var(--campo-verde); + text-decoration: none; + } + + .sin-coordenadas { + background: + linear-gradient(135deg, + rgba(255, 193, 7, 0.1) 0%, + rgba(255, 140, 66, 0.1) 100% + ); + border: 2px solid rgba(255, 193, 7, 0.3); + border-radius: 15px; + padding: 1.5rem; + text-align: center; + color: var(--campo-verde); + font-weight: 500; + margin: 1rem; + box-shadow: + 0 4px 15px rgba(255, 193, 7, 0.2), + inset 0 1px 0 rgba(255,255,255,0.6); + } + + @media (max-width: 768px) { + .titulo-principal { + font-size: 2.5rem; + } + + .titulo-principal::before, + .titulo-principal::after { + display: none; + } + + .campo-formulario { + grid-template-columns: 1fr; + } + + .ofertas-grid { + grid-template-columns: 1fr; + } + + .acciones-sidebar { + padding: 2rem; + } + } + + .icono-cultivo { + font-size: 1.8rem; + filter: drop-shadow(2px 2px 4px rgba(0,0,0,0.1)); + } +`; export const Home = () => { - const { store,dispatch } = useGlobalReducer(); + const { store, dispatch } = useGlobalReducer(); const [offers, setOffers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); -console.log(offers) + // Form state - const [form, setForm] = useState({ name: "", seller: "", price: "", unit: "", img: "" }); + const [form, setForm] = useState({ + titulo: "", + descripcion: "", + precio_ud: "", + ud: "", + img_cosecha: "", + esta_realizada: false + }); const [submitting, setSubmitting] = useState(false); const [submitError, setSubmitError] = useState(null); @@ -17,174 +822,533 @@ console.log(offers) const fetchOffers = async () => { try { const backendUrl = import.meta.env.VITE_BACKEND_URL; - if (!backendUrl) throw new Error("VITE_BACKEND_URL is not defined"); + if (!backendUrl) { + throw new Error("VITE_BACKEND_URL is not defined"); + } - const res = await fetch(`https://animated-pancake-x5pjxq9vv4gj2ppgx-3001.app.github.dev/api/user/ofertas`); - console.log(res) + // Use the environment variable instead of hardcoded URL + const res = await fetch(`${backendUrl}/api/user/ofertas`); + + if (!res.ok) { + throw new Error(`HTTP error! status: ${res.status}`); + } const data = await res.json(); - console.log(data) - setOffers(data.ofertas); + setOffers(data.ofertas || []); } catch (err) { + console.error('Error fetching offers:', err); setError(err.message); } finally { setLoading(false); } }; + fetchOffers(); }, []); - // Handle input change - const handleChange = e => { - const { name, value } = e.target; - setForm(prev => ({ ...prev, [name]: value })); + const handleChange = (e) => { + const { name, value, type, checked } = e.target; + setForm(prev => ({ + ...prev, + [name]: type === 'checkbox' ? checked : value + })); }; - // Submit new offer - const handleSubmit = async e => { + const handleSubmit = async (e) => { e.preventDefault(); setSubmitting(true); setSubmitError(null); + try { - const token = store.token - const res = await fetch(`https://animated-pancake-x5pjxq9vv4gj2ppgx-3001.app.github.dev/api/user/ofertas`, { + const token = localStorage.getItem("jwt_token"); + if (!token) { + throw new Error("No authentication token found"); + } + + const backendUrl = import.meta.env.VITE_BACKEND_URL; + const response = await fetch(`${backendUrl}/api/user/ofertas`, { method: 'POST', - headers: { + headers: { 'Content-Type': 'application/json', "Authorization": `Bearer ${token}` - }, + }, body: JSON.stringify(form) }); - const resUser = await fetch(`https://animated-pancake-x5pjxq9vv4gj2ppgx-3001.app.github.dev/api/user`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - "Authorization": `Bearer ${token}` - }, + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.message || `HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + console.log('Offer created successfully:', data); + + // Reset form on success + setForm({ + titulo: "", + descripcion: "", + precio_ud: "", + ud: "", + img_cosecha: "", + esta_realizada: false }); - if (!res.ok) throw new Error(await res.text()); - const newOffer = await res.json(); - const usuarioLogeado = await resUser.json(); - dispatch({ - type : "add_user", - payload : usuarioLogeado - }) - setOffers(prev => [newOffer, ...prev]); - setForm({ name: "", seller: "", price: "", unit: "", img: "" }); + + // Refresh offers list + const updatedOffers = await fetch(`${backendUrl}/api/user/ofertas`); + const updatedData = await updatedOffers.json(); + setOffers(updatedData.ofertas || []); + } catch (err) { + console.error('Error submitting offer:', err); setSubmitError(err.message); } finally { setSubmitting(false); } }; + const isValidCoordinate = (num) => { + return !isNaN(num) && isFinite(num); + }; + + const getValidCoordinates = (coordenates_vendedor) => { + if (!coordenates_vendedor) return null; + + let lat, lng; + + if (typeof coordenates_vendedor === 'string') { + const cleaned = coordenates_vendedor.replace(/[{}]/g, ''); + const coords = cleaned.split(','); + + if (coords.length >= 2) { + lat = parseFloat(coords[0].trim()); + lng = parseFloat(coords[1].trim()); + } else { + return null; + } + } else if (typeof coordenates_vendedor === 'object' && !Array.isArray(coordenates_vendedor)) { + lat = parseFloat(coordenates_vendedor.lat || coordenates_vendedor.latitude); + lng = parseFloat(coordenates_vendedor.lng || coordenates_vendedor.longitude || coordenates_vendedor.lon); + } else if (Array.isArray(coordenates_vendedor) && coordenates_vendedor.length >= 2) { + lat = parseFloat(coordenates_vendedor[0]); + lng = parseFloat(coordenates_vendedor[1]); + } else { + return null; + } + + if (isValidCoordinate(lat) && isValidCoordinate(lng)) { + if (lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180) { + return { lat, lng }; + } + } + + return null; + }; + + const getCropIcon = (title) => { + if (!title) return 🌱; + + const titleLower = title.toLowerCase(); + + const iconMap = { + 'trigo|cereal|avena': '🌾', + 'tomate|verdura|hortaliza': '🍅', + 'fruta|manzana|pera': '🍎', + 'uva|vino|viña': '🍇', + 'oliva|aceite|aceituna': '🫒', + 'naranja|limón|cítrico': '🍊', + 'girasol|flor': '🌻', + 'maíz|grano': '🌽', + 'lechuga|ensalada': '🥬', + 'zanahoria|tubérculo': '🥕' + }; + + for (const [keywords, icon] of Object.entries(iconMap)) { + if (keywords.split('|').some(keyword => titleLower.includes(keyword))) { + return {icon}; + } + } + + return 🌱; + }; + return ( -
-
-
-
- {/* Conditionally show form if user is logged in */} - {store.user ? ( - <> -

Crear Nueva Oferta

- {submitError &&
{submitError}
} -
-
-
- -
-
- + <> + +
+
+ + {/* HEADER */} +
+

+ Mercado del Campo Español +

+

+ Donde la tradición agrícola se encuentra con la innovación digital +

+
+ +
+
+ + {/* FORM SECTION */} +
+ {store.user ? ( + <> +
+

+ Comparte los Frutos de tu Tierra +

+

+ Conecta tu cosecha directamente con quienes la valoran +

+ + {submitError && ( +
+ ⚠️ {submitError} +
+ )} + + +
+ + +
+ +
+ +