From cbe24792e52856fa139fc20a024e77ab7be6176d Mon Sep 17 00:00:00 2001 From: zoscra Date: Thu, 17 Jul 2025 20:58:51 +0000 Subject: [PATCH 1/7] 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 2/7] 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 3/7] 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 4/7] 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 d0539703bfebb7fd0c14addf76cad24acaebca67 Mon Sep 17 00:00:00 2001 From: daviidgodino Date: Sat, 19 Jul 2025 19:10:56 +0000 Subject: [PATCH 5/7] 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 6/7] 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 ccaf61268c17800a01f734eb79f6854cceea699a Mon Sep 17 00:00:00 2001 From: daviidgodino Date: Wed, 23 Jul 2025 10:08:11 +0000 Subject: [PATCH 7/7] ofertasfiltradas no terminado --- Pipfile | 7 +- Pipfile.lock | 95 +++++++++++++--- .../{244e2e3c2edb_.py => 6c03423b038c_.py} | 10 +- migrations/versions/d4369418a589_.py | 34 ------ src/app.py | 5 +- src/front/pages/ComprarVender.jsx | 81 ++++++-------- src/front/pages/Layout.jsx | 2 - src/front/pages/Login.jsx | 46 +------- src/front/pages/OfertasFiltradas.jsx | 105 ++++++++++++++++++ src/front/pages/Register.jsx | 104 +---------------- 10 files changed, 232 insertions(+), 257 deletions(-) rename migrations/versions/{244e2e3c2edb_.py => 6c03423b038c_.py} (90%) delete mode 100644 migrations/versions/d4369418a589_.py create mode 100644 src/front/pages/OfertasFiltradas.jsx diff --git a/Pipfile b/Pipfile index 44e04f14ff..925a43d9f9 100644 --- a/Pipfile +++ b/Pipfile @@ -6,20 +6,21 @@ verify_ssl = true [dev-packages] [packages] -flask = "*" flask-sqlalchemy = "*" flask-migrate = "*" flask-swagger = "*" psycopg2-binary = "*" python-dotenv = "*" -flask-cors = "*" gunicorn = "*" cloudinary = "*" flask-admin = "*" typing-extensions = "*" -flask-jwt-extended = "==4.6.0" wtforms = "==3.1.2" sqlalchemy = "*" +flask = "*" +flask-cors = "*" +flask-jwt-extended = "*" +bcrypt = "*" [requires] python_version = "3.13" diff --git a/Pipfile.lock b/Pipfile.lock index b201c3decc..ccaf49cf1f 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "d2e672e650278aeeee2fe49bd76d76497d8b65a50f8b5dbb121d265cbc6ef4e5" + "sha256": "926f505edae7c99df596df8209ecf0b93048eb4854e659597533a4dea59ff212" }, "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", @@ -42,11 +100,11 @@ }, "click": { "hashes": [ - "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", - "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a" + "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", + "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b" ], - "markers": "python_version >= '3.7'", - "version": "==8.1.8" + "markers": "python_version >= '3.10'", + "version": "==8.2.1" }, "cloudinary": { "hashes": [ @@ -58,11 +116,12 @@ }, "flask": { "hashes": [ - "sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac", - "sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136" + "sha256:07aae2bb5eaf77993ef57e357491839f5fd9f4dc281593a81a9e4d79a24f295c", + "sha256:284c7b8f2f58cb737f0cf1c30fd7eaf0ccfcde196099d24ecede3fc2005aa59e" ], "index": "pypi", - "version": "==3.1.0" + "markers": "python_version >= '3.9'", + "version": "==3.1.1" }, "flask-admin": { "hashes": [ @@ -74,19 +133,21 @@ }, "flask-cors": { "hashes": [ - "sha256:6ccb38d16d6b72bbc156c1c3f192bc435bfcc3c2bc864b2df1eb9b2d97b2403c", - "sha256:fa5cb364ead54bbf401a26dbf03030c6b18fb2fcaf70408096a572b409586b0c" + "sha256:c7b2cbfb1a31aa0d2e5341eea03a6805349f7a61647daee1a15c46bbe981494c", + "sha256:d81bcb31f07b0985be7f48406247e9243aced229b7747219160a0559edd678db" ], "index": "pypi", - "version": "==5.0.1" + "markers": "python_version >= '3.9' and python_version < '4.0'", + "version": "==6.0.1" }, "flask-jwt-extended": { "hashes": [ - "sha256:63a28fc9731bcc6c4b8815b6f954b5904caa534fc2ae9b93b1d3ef12930dca95", - "sha256:9215d05a9413d3855764bcd67035e75819d23af2fafb6b55197eb5a3313fdfb2" + "sha256:52f35bf0985354d7fb7b876e2eb0e0b141aaff865a22ff6cc33d9a18aa987978", + "sha256:8085d6757505b6f3291a2638c84d207e8f0ad0de662d1f46aa2f77e658a0c976" ], "index": "pypi", - "version": "==4.6.0" + "markers": "python_version >= '3.9' and python_version < '4'", + "version": "==4.7.1" }, "flask-migrate": { "hashes": [ @@ -209,11 +270,11 @@ }, "jinja2": { "hashes": [ - "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb", - "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb" + "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", + "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67" ], "markers": "python_version >= '3.7'", - "version": "==3.1.5" + "version": "==3.1.6" }, "mako": { "hashes": [ diff --git a/migrations/versions/244e2e3c2edb_.py b/migrations/versions/6c03423b038c_.py similarity index 90% rename from migrations/versions/244e2e3c2edb_.py rename to migrations/versions/6c03423b038c_.py index 38a9da9ae8..8ce75e3594 100644 --- a/migrations/versions/244e2e3c2edb_.py +++ b/migrations/versions/6c03423b038c_.py @@ -1,8 +1,8 @@ """empty message -Revision ID: 244e2e3c2edb +Revision ID: 6c03423b038c Revises: -Create Date: 2025-07-17 19:17:05.336777 +Create Date: 2025-07-19 20:47:59.556619 """ from alembic import op @@ -10,7 +10,7 @@ # revision identifiers, used by Alembic. -revision = '244e2e3c2edb' +revision = '6c03423b038c' down_revision = None branch_labels = None depends_on = None @@ -43,9 +43,7 @@ def upgrade(): 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/app.py b/src/app.py index f470e9e3e5..6bd1db1e75 100644 --- a/src/app.py +++ b/src/app.py @@ -1,6 +1,3 @@ -""" -This module takes care of starting the API Server, Loading the DB and Adding the endpoints -""" import os from flask import Flask, request, jsonify, url_for, send_from_directory from flask_migrate import Migrate @@ -75,4 +72,4 @@ def serve_any_other_file(path): # this only runs if `$ python src/main.py` is executed if __name__ == '__main__': PORT = int(os.environ.get('PORT', 3001)) - app.run(host='0.0.0.0', port=PORT, debug=True) + app.run(host='0.0.0.0', port=PORT, debug=True) \ No newline at end of file diff --git a/src/front/pages/ComprarVender.jsx b/src/front/pages/ComprarVender.jsx index 6aa1bd4d93..1a1368cffb 100644 --- a/src/front/pages/ComprarVender.jsx +++ b/src/front/pages/ComprarVender.jsx @@ -1,6 +1,9 @@ import React, { useState } from "react"; +import { useNavigate } from "react-router-dom"; export const CompraVenta = () => { + const navigate = useNavigate(); + const [filters, setFilters] = useState({ cereal: "", precioMin: "", @@ -13,53 +16,43 @@ export const CompraVenta = () => { }; const handleSearch = () => { - console.log("Filtros aplicados:", filters); + navigate("/ofertas-filtradas", { state: filters }); }; return ( -
-

Búsqueda de cereales para comprar o vender

- -
- - - -
- - -
- - - - -
+
+

Buscar cereales para comprar o vender

+ + + + + + + + +
); }; diff --git a/src/front/pages/Layout.jsx b/src/front/pages/Layout.jsx index 9bfa31325c..0da6a30d64 100644 --- a/src/front/pages/Layout.jsx +++ b/src/front/pages/Layout.jsx @@ -7,9 +7,7 @@ import { Footer } from "../components/Footer" export const Layout = () => { return ( - -