From b88a9570a0602c19f1088326eb94ba7cf2e81b2c Mon Sep 17 00:00:00 2001 From: Hun73R717 Date: Mon, 20 Oct 2025 19:32:55 +0000 Subject: [PATCH 01/27] modificacion de navbar --- src/front/components/Navbar.jsx | 36 ++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/src/front/components/Navbar.jsx b/src/front/components/Navbar.jsx index 30d43a2636..0e214e8281 100644 --- a/src/front/components/Navbar.jsx +++ b/src/front/components/Navbar.jsx @@ -3,17 +3,29 @@ import { Link } from "react-router-dom"; export const Navbar = () => { return ( - + ); }; \ No newline at end of file From 4f0a131de8377cde96e579983ee98a7fe479e805 Mon Sep 17 00:00:00 2001 From: 1v4nP4 <1v4nP4@proton.me> Date: Tue, 21 Oct 2025 12:02:00 +0000 Subject: [PATCH 02/27] Avances de backend con modelos --- src/api/models.py | 192 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 189 insertions(+), 3 deletions(-) diff --git a/src/api/models.py b/src/api/models.py index da515f6a1a..14a0cb112c 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -1,19 +1,205 @@ from flask_sqlalchemy import SQLAlchemy -from sqlalchemy import String, Boolean -from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy import String, Boolean, DateTime , TIMESTAMP , Integer , Date , DECIMAL , ForeignKey +from sqlalchemy.orm import Mapped, mapped_column , relationship +from datetime import datetime , timezone db = SQLAlchemy() class User(db.Model): id: Mapped[int] = mapped_column(primary_key=True) + role: Mapped[bool] = mapped_column(Boolean()) + nickname: Mapped[str] = mapped_column(unique=True , nullable=False) + nombre: Mapped[str] = mapped_column(unique=False , nullable=False) + apellido: Mapped[str] = mapped_column(unique=False , nullable=False) + fecha_nacimiento: Mapped[Date] = mapped_column(unique=False , nullable=False) email: Mapped[str] = mapped_column(String(120), unique=True, nullable=False) + address: Mapped[str] = mapped_column(unique=False , nullable=False) + telefono: Mapped[int] = mapped_column(unique=True , nullable=False) password: Mapped[str] = mapped_column(nullable=False) + registro_fecha: Mapped[TIMESTAMP] = mapped_column(Integer()) is_active: Mapped[bool] = mapped_column(Boolean(), nullable=False) + tiendas: Mapped[list['Tienda']] = relationship(back_populates='owner') + + def serialize(self): return { - "id": self.id, + "id": self.id,#id debe conectarse con user_id en class tienda + "role": self.role, + "nickname": self.nickname, + "nombre": self.nombre, + "apellido": self.apellido, + "fecha_nacimiento": self.fecha_nacimiento, + "address": self.address, + "telefono": self.telefono, + "registro_fecha": self.registro_fecha, "email": self.email, # do not serialize the password, its a security breach + } + +class Tienda(db.Model): + id: Mapped[int] = mapped_column(primary_key=True) + user_id: Mapped[int] = mapped_column(ForeignKey('user.id'))#conectar con id de usuario + nombre_tienda: Mapped[str] = mapped_column(unique=False , nullable=False) + descripcion_tienda: Mapped[str] = mapped_column(String(300) , nullable=False , unique=False) + categoría_principal: Mapped[str] = mapped_column(nullable=False , unique=False) + telefono_comercial: Mapped[int] = mapped_column(unique=True , nullable=False) + logo_url: Mapped[str] = mapped_column(String(300),unique=True , nullable=False) + primary_color: Mapped[bool] = mapped_column(Boolean()) + secondary_color: Mapped[bool] = mapped_column(Boolean()) + text_color: Mapped[bool] = mapped_column(Boolean()) + redes_sociales: Mapped[str] = mapped_column(unique=False , nullable=False) + fecha_creacion: Mapped[DateTime] = mapped_column(DateTime(),datetime.now(timezone.utc)) + + owner: Mapped['User'] = relationship(back_populates='tiendas') + + + def serialize(self): + return{ + "id": self.id, + "user_id": self.user_id,#acordarse de preguntar esta parte + "nombre_tienda": self.nombre_tienda, + "descripcion_tienda": self.descripcion_tienda, + "categoria_principal": self.categoría_principal, + "telefono_comercial": self.telefono_comercial, + "logo_url": self.logo_url, + "primary_color": self.primary_color, + "secondary_color": self.secondary_color, + "text_color": self.text_color, + "redes_sociales": self.redes_sociales, + "fecha_creacion": self.fecha_creacion, + } + +class Productos(db.Model): + id: Mapped[int] = mapped_column(primary_key=True) + tienda_id: Mapped[int] = mapped_column(ForeignKey('tienda.id')) + nombre_producto: Mapped[str] = mapped_column(unique=False , nullable=False) + descripcion_producto: Mapped[str] = mapped_column(String(300) , nullable=False , unique=False) + precio: Mapped[DECIMAL] = mapped_column(nullable=False , unique=False) + stock: Mapped[int] = mapped_column(nullable=False , unique=False) + categoría_producto: Mapped[str] = mapped_column(nullable=False , unique=False) + peso: Mapped[DECIMAL] = mapped_column(nullable=False , unique=False) + dimensiones: Mapped[str] = mapped_column(nullable=False , unique=False) + imagenes: Mapped[str] = mapped_column(String(300),unique=True , nullable=False) + estado: Mapped[str] = mapped_column(unique=True , nullable=False) + fecha_subida: Mapped[DateTime] = mapped_column(DateTime(),datetime.now(timezone.utc)) + + + + + + def serialize(self): + return{ + "id": self.id, + "tienda_id": self.tienda_id,#acordarse de preguntar esta parte + "nombre_producto": self.nombre_producto, + "descripcion_producto": self.descripcion_producto, + "precio": self.precio, + "stock": self.stock, + "categoria_producto": self.categoría_producto, + "peso": self.peso, + "dimensiones": self.dimensiones, + "imagenes": self.imagenes, + "estado": self.estado, + "fecha_subida": self.fecha_subida, + } + + +class Favoritos(db.Model): + id: Mapped[int] = mapped_column(primary_key=True) + tienda_id: Mapped[int] = mapped_column(primary_key=True) + user_id: Mapped[int] = mapped_column(ForeignKey('user.id'))#conectar con id de usuario + producto_id: Mapped[int] = mapped_column(ForeignKey('productos.id'))#conectar con id de productos + fecha: Mapped[DateTime] = mapped_column(DateTime(),datetime.now(timezone.utc)) + + + def serialize(self): + return{ + "id": self.id, + "tienda_id": self.tienda_id,#acordarse de preguntar esta parte + "user_id": self.user_id,#conectar con id de usuario + "producto_id": self.producto_id,#conectar con productos id + "fecha": self.fecha, + } + + +class Detalles_pedido(db.Model): + id: Mapped[int] = mapped_column(primary_key=True) + pedido_id: Mapped[int] = mapped_column(primary_key=True) + producto_id: Mapped[int] = mapped_column(ForeignKey('productos.id'))#unir a producto + cantidad: Mapped[int] = mapped_column(primary_key=True) + precio_unitario: Mapped[DECIMAL] = mapped_column(nullable=False , unique=False) + subtotal: Mapped[DECIMAL] = mapped_column(nullable=False , unique=False) + + + def serialize(self): + return{ + "id": self.id, + "pedido_id": self.pedido_id, + "producto_id": self.producto_id, + "cantidad": self.cantidad, + "precio_unitario": self.precio_unitario, + "subtotal": self.subtotal, + } + + +class Notificaciones(db.Model): + user_id: Mapped[int] = mapped_column(ForeignKey('user.id'))#unir a user + tipo: Mapped[str] = mapped_column(unique=False , nullable=False) + mensaje: Mapped[str] = mapped_column(String(300),unique=False,nullable=False) + leido: Mapped[bool] = mapped_column(Boolean()) + fecha: Mapped[DateTime] = mapped_column(DateTime(),datetime.now(timezone.utc)) + + + def serialize(self): + return{ + "user_id": self.user_id, + "tipo": self.tipo, + "mensaje": self.mensaje, + "leido": self.leido, + "fecha": self.fecha, + } + + +class Reseñas(db.Model): + id: Mapped[int] = mapped_column(primary_key=True) + producto_id: Mapped[int] = mapped_column(ForeignKey('productos.id'))#unir a product + cliente_id: Mapped[int] = mapped_column(ForeignKey('user.id'))#unir a user + estrellas: Mapped[int] = mapped_column(Integer()) + comentario: Mapped[str] = mapped_column(String(300)) + fecha: Mapped[DateTime] = mapped_column(DateTime(),datetime.now(timezone.utc)) + respuestas: Mapped[str] = mapped_column(String(300)) + + + def serialize(self): + return{ + "id": self.id, + "producto_id": self.producto_id, + "cliente_id": self.cliente_id, + "estrellas": self.estrellas, + "comentario": self.comentario, + "fecha": self.fecha, + "respuestas": self.respuestas, + } + +class Historial(db.Model): + pedido_id: Mapped[int] = mapped_column(ForeignKey('user.id'))#unir a detalles pedido (user = cliente) + tienda_id: Mapped[int] = mapped_column(ForeignKey('tienda.id'))#unir a tienda id + total: Mapped[DECIMAL] = mapped_column(DECIMAL(),unique=False,nullable=False) + gastos_envio: Mapped[DECIMAL] = mapped_column(DECIMAL(),unique=False,nullable=False) + fecha_pedido: Mapped[DateTime] = mapped_column(DateTime(),datetime.now(timezone.utc)) + direccion: Mapped[str] = mapped_column(String(100),unique=False,nullable=False) + pago: Mapped[bool] = mapped_column(Boolean()) + + + def serialize(self): + return{ + "pedido_id": self.pedido_id, + "tienda_id": self.tienda_id, + "total": self.total, + "gastos_envio": self.gastos_envio, + "fecha_pedido": self.fecha_pedido, + "direccion": self.direccion, + "pago": self.pago, } \ No newline at end of file From aeded3567998b1ecf41fd5b55daafd0952552949 Mon Sep 17 00:00:00 2001 From: Javier Seiglie <96433186+jseiglie@users.noreply.github.com> Date: Tue, 21 Oct 2025 14:20:23 +0000 Subject: [PATCH 03/27] fix: models. feat: seeder --- .gitignore | 1 + migrations/versions/0763d677d453_.py | 35 ----- src/api/admin.py | 10 +- src/api/models.py | 131 +++++++++++------ src/seeder.py | 206 +++++++++++++++++++++++++++ 5 files changed, 303 insertions(+), 80 deletions(-) delete mode 100644 migrations/versions/0763d677d453_.py create mode 100644 src/seeder.py diff --git a/.gitignore b/.gitignore index 80704f4378..6c468afd68 100755 --- a/.gitignore +++ b/.gitignore @@ -80,3 +80,4 @@ database.database database.db diagram.png __pycache__/ +migrations/* \ No newline at end of file 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/src/api/admin.py b/src/api/admin.py index 3eecb64140..e5b5ea92af 100644 --- a/src/api/admin.py +++ b/src/api/admin.py @@ -1,7 +1,7 @@ import os from flask_admin import Admin -from .models import db, User +from .models import db, User, Tienda, Productos, Detalles_pedido, Favoritos, Notificaciones, Historial, Resenas from flask_admin.contrib.sqla import ModelView def setup_admin(app): @@ -12,6 +12,12 @@ def setup_admin(app): # Add your models here, for example this is how we add a the User model to the admin admin.add_view(ModelView(User, db.session)) - + admin.add_view(ModelView(Tienda, db.session)) + admin.add_view(ModelView(Productos, db.session)) + admin.add_view(ModelView(Detalles_pedido, db.session)) + admin.add_view(ModelView(Favoritos, db.session)) + admin.add_view(ModelView(Notificaciones, db.session)) + admin.add_view(ModelView(Historial, db.session)) + admin.add_view(ModelView(Resenas, db.session)) # You can duplicate that line to add mew models # admin.add_view(ModelView(YourModelName, db.session)) \ No newline at end of file diff --git a/src/api/models.py b/src/api/models.py index 14a0cb112c..90283ecd1a 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -1,25 +1,29 @@ from flask_sqlalchemy import SQLAlchemy -from sqlalchemy import String, Boolean, DateTime , TIMESTAMP , Integer , Date , DECIMAL , ForeignKey +from sqlalchemy import String, Boolean, DateTime , TIMESTAMP , Integer , Date , DECIMAL , ForeignKey, Text, Float from sqlalchemy.orm import Mapped, mapped_column , relationship from datetime import datetime , timezone db = SQLAlchemy() - class User(db.Model): + __tablename__ = 'user' id: Mapped[int] = mapped_column(primary_key=True) role: Mapped[bool] = mapped_column(Boolean()) - nickname: Mapped[str] = mapped_column(unique=True , nullable=False) - nombre: Mapped[str] = mapped_column(unique=False , nullable=False) - apellido: Mapped[str] = mapped_column(unique=False , nullable=False) - fecha_nacimiento: Mapped[Date] = mapped_column(unique=False , nullable=False) + nickname: Mapped[str] = mapped_column(unique=True, nullable=False) + nombre: Mapped[str] = mapped_column(nullable=False) + apellido: Mapped[str] = mapped_column(nullable=False) + fecha_nacimiento: Mapped[datetime] = mapped_column(nullable=False) email: Mapped[str] = mapped_column(String(120), unique=True, nullable=False) - address: Mapped[str] = mapped_column(unique=False , nullable=False) - telefono: Mapped[int] = mapped_column(unique=True , nullable=False) + address: Mapped[str] = mapped_column(nullable=False) + telefono: Mapped[int] = mapped_column(unique=True, nullable=False) password: Mapped[str] = mapped_column(nullable=False) - registro_fecha: Mapped[TIMESTAMP] = mapped_column(Integer()) + registro_fecha: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)) is_active: Mapped[bool] = mapped_column(Boolean(), nullable=False) tiendas: Mapped[list['Tienda']] = relationship(back_populates='owner') + resenas: Mapped[list['Resenas']] = relationship(back_populates='autor') + favoritos: Mapped[list['Favoritos']] = relationship(back_populates='user') + pedidos: Mapped[list['Detalles_pedido']] = relationship(back_populates='user') + notificaciones: Mapped[list['Notificaciones']] = relationship(back_populates='user') @@ -35,25 +39,33 @@ def serialize(self): "telefono": self.telefono, "registro_fecha": self.registro_fecha, "email": self.email, + "pedidos": [p.serialize() for p in self.pedidos] if self.pedidos else None, + "resenas": [r.serialize for r in self.resenas] if self.resenas else None, + "favoritos": [f.serialize() for f in self.favoritos] if self.favoritos else None, + "notificaciones": [n.serialize() for n in self.notificaciones] if self.notificaciones else None, # do not serialize the password, its a security breach } class Tienda(db.Model): + __tablename__="tienda" id: Mapped[int] = mapped_column(primary_key=True) - user_id: Mapped[int] = mapped_column(ForeignKey('user.id'))#conectar con id de usuario nombre_tienda: Mapped[str] = mapped_column(unique=False , nullable=False) descripcion_tienda: Mapped[str] = mapped_column(String(300) , nullable=False , unique=False) - categoría_principal: Mapped[str] = mapped_column(nullable=False , unique=False) + categoria_principal: Mapped[str] = mapped_column(nullable=False , unique=False) telefono_comercial: Mapped[int] = mapped_column(unique=True , nullable=False) logo_url: Mapped[str] = mapped_column(String(300),unique=True , nullable=False) - primary_color: Mapped[bool] = mapped_column(Boolean()) - secondary_color: Mapped[bool] = mapped_column(Boolean()) - text_color: Mapped[bool] = mapped_column(Boolean()) + primary_color: Mapped[str] = mapped_column(String(15), nullable=False , unique=False) + secondary_color: Mapped[str] = mapped_column(String(15), nullable=False , unique=False) + text_color: Mapped[str] = mapped_column(String(15), nullable=False , unique=False) redes_sociales: Mapped[str] = mapped_column(unique=False , nullable=False) - fecha_creacion: Mapped[DateTime] = mapped_column(DateTime(),datetime.now(timezone.utc)) + fecha_creacion: Mapped[TIMESTAMP] = mapped_column(DateTime(), default=datetime.now(timezone.utc)) + owner_id: Mapped[int] = mapped_column(ForeignKey('user.id')) owner: Mapped['User'] = relationship(back_populates='tiendas') + productos: Mapped[list['Productos']] = relationship(back_populates='tienda') + favoritos: Mapped['Favoritos'] = relationship(back_populates='tienda') + def serialize(self): return{ @@ -61,7 +73,7 @@ def serialize(self): "user_id": self.user_id,#acordarse de preguntar esta parte "nombre_tienda": self.nombre_tienda, "descripcion_tienda": self.descripcion_tienda, - "categoria_principal": self.categoría_principal, + "categoria_principal": self.categoria_principal, "telefono_comercial": self.telefono_comercial, "logo_url": self.logo_url, "primary_color": self.primary_color, @@ -72,20 +84,27 @@ def serialize(self): } class Productos(db.Model): + __tablename__="productos" id: Mapped[int] = mapped_column(primary_key=True) tienda_id: Mapped[int] = mapped_column(ForeignKey('tienda.id')) nombre_producto: Mapped[str] = mapped_column(unique=False , nullable=False) descripcion_producto: Mapped[str] = mapped_column(String(300) , nullable=False , unique=False) - precio: Mapped[DECIMAL] = mapped_column(nullable=False , unique=False) + precio: Mapped[float] = mapped_column(Float(), nullable=False , unique=False, default=0.0) stock: Mapped[int] = mapped_column(nullable=False , unique=False) - categoría_producto: Mapped[str] = mapped_column(nullable=False , unique=False) - peso: Mapped[DECIMAL] = mapped_column(nullable=False , unique=False) + categoria_producto: Mapped[str] = mapped_column(nullable=False , unique=False) + peso: Mapped[float] = mapped_column(nullable=False , unique=False, default=0.0) dimensiones: Mapped[str] = mapped_column(nullable=False , unique=False) imagenes: Mapped[str] = mapped_column(String(300),unique=True , nullable=False) - estado: Mapped[str] = mapped_column(unique=True , nullable=False) - fecha_subida: Mapped[DateTime] = mapped_column(DateTime(),datetime.now(timezone.utc)) + estado: Mapped[str] = mapped_column(unique=False , nullable=False) + fecha_subida: Mapped[TIMESTAMP] = mapped_column(DateTime(timezone=True), default=datetime.now(timezone.utc)) + + tienda_id: Mapped[int] = mapped_column(ForeignKey('tienda.id')) + tienda: Mapped['Tienda'] = relationship(back_populates='productos', uselist=False) + resenas: Mapped[list['Resenas']] = relationship(back_populates='producto') + favoritos: Mapped['Favoritos'] = relationship(back_populates='producto') + pedidos: Mapped['Detalles_pedido'] = relationship(back_populates="productos") @@ -97,21 +116,29 @@ def serialize(self): "descripcion_producto": self.descripcion_producto, "precio": self.precio, "stock": self.stock, - "categoria_producto": self.categoría_producto, + "categoria_producto": self.categoria_producto, "peso": self.peso, "dimensiones": self.dimensiones, "imagenes": self.imagenes, "estado": self.estado, "fecha_subida": self.fecha_subida, + "tienda": {"nombre": self.tienda.nombre_tienda} if self.tienda else None, #cuando es UN solo objeto, podemos acceder directamente a sus propiedades + "resenas": [r.serialize() for r in self.resenas] if self.resenas else None #cada vez que sea una lista de objetos, tenemos que hacer loop para serializar cada objeto. } class Favoritos(db.Model): + __tablename__="favoritos" id: Mapped[int] = mapped_column(primary_key=True) - tienda_id: Mapped[int] = mapped_column(primary_key=True) + + fecha: Mapped[TIMESTAMP] = mapped_column(DateTime(timezone=True), default=datetime.now(timezone.utc)) + + tienda_id: Mapped[int] = mapped_column(ForeignKey('tienda.id'))#conectar con id de tienda user_id: Mapped[int] = mapped_column(ForeignKey('user.id'))#conectar con id de usuario producto_id: Mapped[int] = mapped_column(ForeignKey('productos.id'))#conectar con id de productos - fecha: Mapped[DateTime] = mapped_column(DateTime(),datetime.now(timezone.utc)) + tienda: Mapped['Tienda'] = relationship(back_populates='favoritos') + user: Mapped['User'] = relationship(back_populates='favoritos') + producto: Mapped['Productos'] = relationship(back_populates='favoritos') def serialize(self): @@ -121,36 +148,46 @@ def serialize(self): "user_id": self.user_id,#conectar con id de usuario "producto_id": self.producto_id,#conectar con productos id "fecha": self.fecha, + "producto": {"nombre_producto": self.producto.nombre_producto} if self.producto else None } class Detalles_pedido(db.Model): + __tablename__="detalles_pedido" id: Mapped[int] = mapped_column(primary_key=True) - pedido_id: Mapped[int] = mapped_column(primary_key=True) + + user_id: Mapped[int] = mapped_column(ForeignKey('user.id'))#unir a producto + user: Mapped['User'] = relationship(back_populates="pedidos") + producto_id: Mapped[int] = mapped_column(ForeignKey('productos.id'))#unir a producto - cantidad: Mapped[int] = mapped_column(primary_key=True) - precio_unitario: Mapped[DECIMAL] = mapped_column(nullable=False , unique=False) - subtotal: Mapped[DECIMAL] = mapped_column(nullable=False , unique=False) + productos: Mapped[list['Productos']] = relationship(back_populates="pedidos") + subtotal: Mapped[float] = mapped_column(nullable=False , unique=False) + + def serialize(self): return{ "id": self.id, - "pedido_id": self.pedido_id, + "user_id": self.user_id, "producto_id": self.producto_id, - "cantidad": self.cantidad, - "precio_unitario": self.precio_unitario, - "subtotal": self.subtotal, + "user": {"email": self.user.email} if self.user else None, + "producto": [p.serialize() for p in self.productos] if self.productos else None, + "subtotal": self.subtotal } class Notificaciones(db.Model): - user_id: Mapped[int] = mapped_column(ForeignKey('user.id'))#unir a user + __tablename__="notificaciones" + id: Mapped[int] = mapped_column(primary_key=True) + tipo: Mapped[str] = mapped_column(unique=False , nullable=False) mensaje: Mapped[str] = mapped_column(String(300),unique=False,nullable=False) leido: Mapped[bool] = mapped_column(Boolean()) - fecha: Mapped[DateTime] = mapped_column(DateTime(),datetime.now(timezone.utc)) + fecha: Mapped[TIMESTAMP] = mapped_column(DateTime(timezone=True), default=datetime.now(timezone.utc)) + user_id: Mapped[int] = mapped_column(ForeignKey('user.id'))#unir a user + user: Mapped['User'] = relationship(back_populates="notificaciones") def serialize(self): return{ @@ -159,19 +196,24 @@ def serialize(self): "mensaje": self.mensaje, "leido": self.leido, "fecha": self.fecha, + "user": {"email": self.user.email, "id": self.user.id} if self.user else None } -class Reseñas(db.Model): +class Resenas(db.Model): + __tablename__="resenas" id: Mapped[int] = mapped_column(primary_key=True) - producto_id: Mapped[int] = mapped_column(ForeignKey('productos.id'))#unir a product - cliente_id: Mapped[int] = mapped_column(ForeignKey('user.id'))#unir a user estrellas: Mapped[int] = mapped_column(Integer()) - comentario: Mapped[str] = mapped_column(String(300)) - fecha: Mapped[DateTime] = mapped_column(DateTime(),datetime.now(timezone.utc)) - respuestas: Mapped[str] = mapped_column(String(300)) + comentario: Mapped[str] = mapped_column(Text()) + fecha: Mapped[TIMESTAMP] = mapped_column(DateTime(timezone=True), default=datetime.now(timezone.utc)) + respuestas: Mapped[str] = mapped_column(Text()) + cliente_id: Mapped[int] = mapped_column(ForeignKey('user.id'))#unir a user + autor: Mapped['User'] = relationship(back_populates='resenas') + producto_id: Mapped[int] = mapped_column(ForeignKey('productos.id'))#unir a product + producto: Mapped['Productos'] = relationship(back_populates='resenas') + def serialize(self): return{ "id": self.id, @@ -181,14 +223,17 @@ def serialize(self): "comentario": self.comentario, "fecha": self.fecha, "respuestas": self.respuestas, + "producto": {"nombre_producto": self.producto.nombre_producto} if self.producto else None } class Historial(db.Model): + __tablename__="historial" + id: Mapped[int] = mapped_column(primary_key=True) pedido_id: Mapped[int] = mapped_column(ForeignKey('user.id'))#unir a detalles pedido (user = cliente) tienda_id: Mapped[int] = mapped_column(ForeignKey('tienda.id'))#unir a tienda id - total: Mapped[DECIMAL] = mapped_column(DECIMAL(),unique=False,nullable=False) - gastos_envio: Mapped[DECIMAL] = mapped_column(DECIMAL(),unique=False,nullable=False) - fecha_pedido: Mapped[DateTime] = mapped_column(DateTime(),datetime.now(timezone.utc)) + total: Mapped[float] = mapped_column(unique=False,nullable=False, default=0.0) + gastos_envio: Mapped[float] = mapped_column(unique=False,nullable=False, default=0.0) + fecha_pedido: Mapped[TIMESTAMP] = mapped_column(DateTime(timezone=True), default=datetime.now(timezone.utc)) direccion: Mapped[str] = mapped_column(String(100),unique=False,nullable=False) pago: Mapped[bool] = mapped_column(Boolean()) diff --git a/src/seeder.py b/src/seeder.py new file mode 100644 index 0000000000..570394f368 --- /dev/null +++ b/src/seeder.py @@ -0,0 +1,206 @@ + +from api.models import db, User, Tienda, Productos, Favoritos, Detalles_pedido, Notificaciones, Resenas, Historial +from datetime import date, datetime, timezone +from app import app +import random + +def seed_data(): + db.drop_all() + db.create_all() + + # === USERS === + users = [ + User( + role=True, + nickname="admin", + nombre="Pepe", + apellido="Lola", + fecha_nacimiento=date(1990, 5, 17), + email="admin@example.com", + address="Calle Sol 123, Sevilla", + telefono=600111222, + password="hashed_password", + registro_fecha=datetime.now(timezone.utc), + is_active=True + ), + User( + role=False, + nickname="maria", + nombre="María", + apellido="Gómez", + fecha_nacimiento=date(1995, 3, 22), + email="maria@example.com", + address="Av. Andalucía 45, Sevilla", + telefono=600333444, + password="hashed_password", + registro_fecha=datetime.now(timezone.utc), + is_active=True + ), + User( + role=False, + nickname="carlos", + nombre="Carlos", + apellido="Ole Ole", + fecha_nacimiento=date(1988, 7, 9), + email="carlos@example.com", + address="Calle Luna 9, Sevilla", + telefono=600555666, + password="hashed_password", + registro_fecha=datetime.now(timezone.utc), + is_active=True + ), + ] + db.session.add_all(users) + db.session.commit() + + # === TIENDAS === + tiendas = [ + Tienda( + owner_id=users[0].id, + nombre_tienda="La Bodega del Sur", + descripcion_tienda="Vinos y productos gourmet del sur de España.", + categoria_principal="Gastronomía", + telefono_comercial=955123456, + logo_url="https://example.com/logos/bodega.png", + primary_color=True, + secondary_color=False, + text_color=True, + redes_sociales="@labodegadelSur", + fecha_creacion=datetime.now(timezone.utc) + ), + Tienda( + owner_id=users[1].id, + nombre_tienda="EcoModa", + descripcion_tienda="Ropa sostenible hecha con materiales reciclados.", + categoria_principal="Moda", + telefono_comercial=955987654, + logo_url="https://example.com/logos/ecomoda.png", + primary_color=False, + secondary_color=True, + text_color=False, + redes_sociales="@ecomoda", + fecha_creacion=datetime.now(timezone.utc) + ), + Tienda( + owner_id=users[2].id, + nombre_tienda="TechZone", + descripcion_tienda="Tienda especializada en gadgets y accesorios electrónicos.", + categoria_principal="Tecnología", + telefono_comercial=955567890, + logo_url="https://example.com/logos/techzone.png", + primary_color=True, + secondary_color=True, + text_color=False, + redes_sociales="@techzone_es", + fecha_creacion=datetime.now(timezone.utc) + ), + ] + db.session.add_all(tiendas) + db.session.commit() + + # === PRODUCTOS === + productos = [] + for tienda in tiendas: + for i in range(1, 5): + productos.append( + Productos( + tienda_id=tienda.id, + nombre_producto=f"Producto {i} de {tienda.nombre_tienda}", + descripcion_producto="Descripción breve del producto.", + precio=random.uniform(10, 200), + stock=random.randint(5, 100), + categoria_producto=tienda.categoria_principal, + peso=random.uniform(0.2, 5.0), + dimensiones="10x20x30 cm", + imagenes=f"https://example.com/img/producto_{i}_{tienda.id}.png", + estado="disponible", + fecha_subida=datetime.now(timezone.utc) + ) + ) + db.session.add_all(productos) + db.session.commit() + + # === FAVORITOS === + favoritos = [ + Favoritos(user_id=users[1].id, producto_id=productos[0].id, tienda_id=tiendas[0].id, fecha=datetime.now(timezone.utc)), + Favoritos(user_id=users[2].id, producto_id=productos[1].id, tienda_id=tiendas[1].id, fecha=datetime.now(timezone.utc)) + ] + db.session.add_all(favoritos) + db.session.commit() + + # === RESEÑAS === + resenas = [ + Resenas( + producto_id=productos[0].id, + cliente_id=users[1].id, + estrellas=5, + comentario="Excelente producto, muy buena calidad.", + fecha=datetime.now(timezone.utc), + respuestas="¡Gracias por tu compra!" + ), + Resenas( + producto_id=productos[2].id, + cliente_id=users[2].id, + estrellas=4, + comentario="Buen servicio, aunque tardó un poco el envío.", + fecha=datetime.now(timezone.utc), + respuestas="Estamos mejorando los tiempos de entrega." + ), + ] + db.session.add_all(resenas) + db.session.commit() + + # === NOTIFICACIONES === + notificaciones = [ + Notificaciones(user_id=users[1].id, tipo="Pedido", mensaje="Tu pedido ha sido enviado", leido=False, fecha=datetime.now(timezone.utc)), + Notificaciones(user_id=users[2].id, tipo="Promoción", mensaje="Descuento del 20% en TechZone", leido=True, fecha=datetime.now(timezone.utc)) + ] + db.session.add_all(notificaciones) + db.session.commit() + + # --- DETALLES_PEDIDO --- + detalles = [] + historiales = [] + + sample_orders = [ + {"user": users[1], "producto": productos[0], "cantidad": 2}, + {"user": users[2], "producto": productos[3], "cantidad": 1}, + {"user": users[1], "producto": productos[4], "cantidad": 3}, + {"user": users[0], "producto": productos[6], "cantidad": 1}, + {"user": users[2], "producto": productos[8], "cantidad": 2}, + {"user": users[0], "producto": productos[11], "cantidad": 1}, + ] + + for idx, ord in enumerate(sample_orders, start=1): + precio_unitario = float(ord["producto"].precio) + cantidad = ord["cantidad"] + subtotal = round(precio_unitario * cantidad, 2) + + det = Detalles_pedido( + user_id=ord["user"].id, + producto_id=ord["producto"].id, + subtotal=subtotal + ) + detalles.append(det) + + hist = Historial( + pedido_id=ord["user"].id, + tienda_id=ord["producto"].tienda_id, + total=subtotal, + gastos_envio=round(random.uniform(0.0, 10.0), 2), + fecha_pedido=datetime.now(timezone.utc), + direccion=ord["user"].address, + pago=True if random.random() > 0.2 else False + ) + historiales.append(hist) + + db.session.add_all(detalles) + db.session.add_all(historiales) + db.session.commit() + + print("✅ Seeder finalizado: users, tiendas, productos, favoritos, reseñas, notificaciones, detalles_pedido e historial creados.") + + +if __name__ == "__main__": + with app.app_context(): + seed_data() From 8b08fe836d3d1b4a64b680ae2d877cbe091003f2 Mon Sep 17 00:00:00 2001 From: 1v4nP4 <1v4nP4@proton.me> Date: Wed, 22 Oct 2025 17:44:27 +0000 Subject: [PATCH 04/27] Modelos terminados --- src/api/models.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/api/models.py b/src/api/models.py index 90283ecd1a..4db4fc16c0 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -66,6 +66,8 @@ class Tienda(db.Model): productos: Mapped[list['Productos']] = relationship(back_populates='tienda') favoritos: Mapped['Favoritos'] = relationship(back_populates='tienda') + historial_tienda: Mapped[list['Historial']] = relationship(back_populates='tienda') + def serialize(self): return{ @@ -155,16 +157,15 @@ def serialize(self): class Detalles_pedido(db.Model): __tablename__="detalles_pedido" id: Mapped[int] = mapped_column(primary_key=True) - + user_id: Mapped[int] = mapped_column(ForeignKey('user.id'))#unir a producto user: Mapped['User'] = relationship(back_populates="pedidos") producto_id: Mapped[int] = mapped_column(ForeignKey('productos.id'))#unir a producto productos: Mapped[list['Productos']] = relationship(back_populates="pedidos") subtotal: Mapped[float] = mapped_column(nullable=False , unique=False) - - - + + historial_pedido: Mapped[list['Historial']] = relationship(back_populates='pedido') def serialize(self): return{ @@ -172,8 +173,8 @@ def serialize(self): "user_id": self.user_id, "producto_id": self.producto_id, "user": {"email": self.user.email} if self.user else None, - "producto": [p.serialize() for p in self.productos] if self.productos else None, - "subtotal": self.subtotal + "productos": [p.serialize() for p in self.productos] if self.productos else None, + "subtotal": self.subtotal, } @@ -229,14 +230,19 @@ def serialize(self): class Historial(db.Model): __tablename__="historial" id: Mapped[int] = mapped_column(primary_key=True) - pedido_id: Mapped[int] = mapped_column(ForeignKey('user.id'))#unir a detalles pedido (user = cliente) - tienda_id: Mapped[int] = mapped_column(ForeignKey('tienda.id'))#unir a tienda id total: Mapped[float] = mapped_column(unique=False,nullable=False, default=0.0) gastos_envio: Mapped[float] = mapped_column(unique=False,nullable=False, default=0.0) fecha_pedido: Mapped[TIMESTAMP] = mapped_column(DateTime(timezone=True), default=datetime.now(timezone.utc)) direccion: Mapped[str] = mapped_column(String(100),unique=False,nullable=False) pago: Mapped[bool] = mapped_column(Boolean()) + pedido_id: Mapped[int] = mapped_column(ForeignKey('user.id'))#unir a detalles pedido (user = cliente) + pedido: Mapped['Detalles_pedido'] = relationship(back_populates='historial_pedido') + + + tienda_id: Mapped[int] = mapped_column(ForeignKey('tienda.id'))#unir a tienda id + tienda: Mapped['Tienda'] = relationship(back_populates='historial_tienda') + def serialize(self): return{ From bb38d5809b7f80eeec33f43b29a78fc37225f23d Mon Sep 17 00:00:00 2001 From: Hun73R717 Date: Wed, 22 Oct 2025 19:27:58 +0000 Subject: [PATCH 05/27] modificacion navbar --- package-lock.json | 43 ++++++++++++++++++++ package.json | 29 +++++++------- src/front/assets/img/Logo.png | Bin 0 -> 60718 bytes src/front/components/Navbar.jsx | 67 +++++++++++++++++++------------- src/front/main.jsx | 2 + 5 files changed, 100 insertions(+), 41 deletions(-) create mode 100644 src/front/assets/img/Logo.png diff --git a/package-lock.json b/package-lock.json index 8d43d98ab7..786897eb13 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.1", "license": "ISC", "dependencies": { + "bootstrap": "^5.3.8", "prop-types": "^15.8.1", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -943,6 +944,17 @@ "node": ">= 8" } }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@remix-run/router": { "version": "1.22.0", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.22.0.tgz", @@ -1264,6 +1276,25 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/bootstrap": { + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz", + "integrity": "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "license": "MIT", + "peerDependencies": { + "@popperjs/core": "^2.11.8" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -4998,6 +5029,12 @@ "fastq": "^1.6.0" } }, + "@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "peer": true + }, "@remix-run/router": { "version": "1.22.0", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.22.0.tgz", @@ -5230,6 +5267,12 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "bootstrap": { + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz", + "integrity": "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==", + "requires": {} + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", diff --git a/package.json b/package.json index 0caab10749..02e5e578f2 100755 --- a/package.json +++ b/package.json @@ -8,10 +8,10 @@ "main": "index.js", "scripts": { "dev": "vite", - "start": "vite", - "build": "vite build", - "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", - "preview": "vite preview" + "start": "vite", + "build": "vite build", + "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" }, "author": { "name": "Alejandro Sanchez", @@ -30,13 +30,13 @@ "license": "ISC", "devDependencies": { "@types/react": "^18.2.18", - "@types/react-dom": "^18.2.7", - "@vitejs/plugin-react": "^4.0.4", - "eslint": "^8.46.0", - "eslint-plugin-react": "^7.33.1", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.3", - "vite": "^4.4.8" + "@types/react-dom": "^18.2.7", + "@vitejs/plugin-react": "^4.0.4", + "eslint": "^8.46.0", + "eslint-plugin-react": "^7.33.1", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.3", + "vite": "^4.4.8" }, "babel": { "presets": [ @@ -54,9 +54,10 @@ ] }, "dependencies": { + "bootstrap": "^5.3.8", "prop-types": "^15.8.1", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-router-dom": "^6.18.0" + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.18.0" } } diff --git a/src/front/assets/img/Logo.png b/src/front/assets/img/Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..90fc7403c38853318feacd502f6dc37aa2fbcfc6 GIT binary patch literal 60718 zcmV(=K-s^EP)s?$}^#j4`%D2@oK(giit_B!rYt@<~Dh0Rkb^ z5PB~L8*Jm=o7}rCTeg}dTe8)qr`&sY=Y0Q|ncbP$*}Z!oJvPbj^;?jiyKUyonKS3S z&)JzXl%^?Tk`7Z4nG?$X^jQvpz{Qt!1+1@;RUv!lrSIr#O!-Z6e&w&*j3IdKtGI7y z<6aGC{T_KF>vj`6j~1FmWVgkmjrILd-vnq6J>y?<1f}#w`B7Gn-4=7Bx7@IPhKgnO zdf9oHG_z3Mp`nI`m>GH{NK5F3drh59WCWG8l~JV0r=2Rw=|x5#vQUfekRXQW8E`q3O?8b%hvd|m92-%bZafU;dMDG-U%_!x2Lw9bLdAvoV;^eOdVW+ z`l2x5!y5(}9i~plDE4HIdr*M>mrEy9l^g7QXi((RTWDLAfcy_UM86t`z2Bv8llQmc z9WMXTyB_Ny_I`88b2&|<0hswaNrxHK9vl1`vH`=@?c*NTgRG|Tdv)}9E5An71@u1r!5X3|)E}Y2 zQw9vG&Ufah;;H>el{F6aXRmr{($#ZJ)M=YKxr(UnGvNrm*C}2PB_E*}%$vS#fQW)Rd)5aHY0QJf9-vQXB zG);I?YVOnfcW~@vxP5-ayTn849rvqt{(3cYq5+69x~kaHSD>|wZYwg-tKMud_t8(4 zQRf;wL9h1+c^Kk<#qqy@)=3M)yo_zFQS^iYl9L%lpY(!DA~`!-eBbYqHSR3BYK8cF zc``f6qnGn5$YdQ=G`?)|SFc@F)z=9+hRd1&BMZu3f0ahHJ?_W8^wYkHmbp_$FJRK^ zcqL@1pSBp9{(tBY6K#J=rgBU81a>j;BP_TDFy z(@`)%J%TYI<>9uMoihEeiv`Cu6rC-px6JYFb*(^uMV+NVpRy=@z^$#VGwKjf$KL-> zca!_3WfFX9C#3QUlJ2&m|KsI0IDX`byc^lz$-h6iQL}&*EiJ>o>?xLhKM#jOXTiZ< z7c7%%nE6*mTR)NH+HI+M>;$H-BzYpA8z#5qh(QWir-Ti#6-I=50!~4!x*sm2fEH-23xO4 z@nlp|=`jZ)uVu8F-~AkfV*Pg8oljccGxq!b{VLiDv=Z{UV2PNN zaqpt42Cvfway|7rji(C6nzH=wiOreANFw@Jodttcbl!uXM6`>|d@k-|03`#q=+Jpf-e z#UUJJ4!6m)f_gkiUcFR@juPAEm&vxh?KyZ{zQZ~5uUHW|cya@!D+*shP<$9XuhsorGFcjidYt!a2L*ml*0HmJ@m?=m1CDL_%Ld%eG^Fy}y+@|u#c*_g zZT3HN1^cK5un#_NM=Xl(6oTkxjN5~f%3eUZJaLcvl{A-9Sf8hLpRa0b6Xc+2R_uw-kJ-uWl;g zb5EQ@fkNsY{PFqF?!TNLLMI~vT^4Db=y~rOlS69=9EZHp zDlF40f*pFU%tVC(T~%yw-{U}OfqL}(Nk4l2eN%gEk zI=|Ck>&xaHPeAyqK3Pmk-6f%-egx%reVE8hVRaGk%Fd;)=^+I_wUk*b_a_C)$$}Tb z;=E{cWw^5D5#+Hs_rjG`Af7BTcjXUK`FjCdnF zXhTs}KLF_)BRe7t?|r8XF4hrhf5@Q^wT&yzT}4(M`pbhuE^sajEQBJ@p#T_p%Fz!% zg&jgW@=V#|hJRebL$XU`fV{TYZMA>+o__Jc;=#S133VD(mQ<^r{Jh%1sibQnN3Doo zS=g!gMc{V|o_l(=UWh9DYqu}JVDf%Y)&0OtB1^$&V*GcqSE*QE)&MIcXNTp8oT@^k zxYc=e1|!`4dD7A;jdyUO`Y#b>G_)81=4 zE6%ToFF8Ru<{CHI(T6=q*vawK&+94zJppe$1Va0zyVpB?m1%`8-K>n+gY(H|0%y4QRo-s>IP@$_>< zPefMxkxiE$;jpeBDG!D#0&RCb6#S2fl39WuVpy4QRLR^VwyAF&fE=ey8isfJQg>RJ zJemRHfJQ3m_g)~qmzQJ}R`oc;>SK%WUf8(T!0*K~2SFJqVuO!K7L*{L0)TKra^dmK ziT=<|l-sKfFW=#loSl2(RqMl+SD3vIL8iw8&8#cseV=Evg`sCKq~iTopiMiK#;y=`q_AEIo_po^WdzHtOH0Lh z##cKCL~y1BhMo`A<2+N7_Fi!TD|*0o_M3z4k)>pz%aVR zwe||G(j}z^pe6teKJOP+*-^&JLSf??aK7l#-*BTqlI8RnC7sa^(zupSit`ezac66J+xLTFTD%kbYOrXJ9;2sN3vks7AMkS@Vh=#hzv-@@MQo z(Fr6+cT~;FF+6a-P&29Ncordk;3h<0B~}XPXeSuvwY$v5*STLsTUFd4zh54a@O@vS zW=imVA*%n1A9cs26^yhu3k#|xtIQ&vc%=#V-s*P_bRE6BGlEs8&fqhS@vy*)SFdx; zvVZEGMw87J}c))84&6LpeKAxMzpAmcCVG@8L}?c zRpNbvI=9uV$O#&p@ch6@X5UU9rSlXm3}#rSU+9l$T{c?=I-f&r zUF%IKawQa&0H@yy%ro9w8CUIyX$|eO8{k&L zfk{7V*-C^Z&3B;L3)*Bfha3Qu521cdSugt&3^avW9vIu9I7z_|?t_Oujs5EMeur7u zN$@~r;lar~CaxHAS1Sk0KFR%-Jej6^B1Zr~+Xf-OQq@r3yX-5#(c>B4F8BjC{7J?0 zXmnfb;7noOwWrVh_L_5lzDrd!*FP^SGsu&GRM@(rpi~@>F6T`Z$gUN6Hszqa9PZ=1 z(C7dWWl@Vzp*c({L9sjSndxU7fMn6&YfeuVeOdFPxdkI+uJxpi^onyW=TbtBjX`eR z3b|z~(#@M8H*bdAx)r=_J9umyoOA(}KqP=h6ErXY8X5*opMq%mbcpFQ5Y3nYnm!d` z$`pu3%jz!9fOrK->i$lFFZ8YhfoGh4us}Z7c)!po)&m7E+XluKPz=cjyt;gz*ll$b zMu^ZN;Y4KNVs-$g85emKC7t3#`xIbezq-I{<=te{5h}p;T&%Wn__@s8*q!qB0Zo%hyMUL`Fr;i4G)y9E#IuYrT=2i}6Z}|A8pj?l*&M|w}{Tog++bYv` z3j8%(bvbCUqnhMCpK%yyJ1|sIMwJIrR&I@>jMm8`%5AMq6ml^5X}~7+Ej?SyCB|U0ph(EA>Mm$ z#JlefvCABY;b92Vd2*ti(}#8p3aQ5=e=3meWLL;pd7Xh#!=C(xdozFE8$gu-*@~uT zz?Yyub9;>_o1@Z4*AxJDov>N)<&vYWRj30w%AWHgBQKlM2msD1(yGwa%cZNu=Rfj;bc_*mLLO{EOQMJzf z_64H5r9m&hOrK~ypEfku>o}X0?F(T8^w#t4ebx9Z2WRGxxHC741-L-7>dS(C6y_UO zQC-(Ymo}7x%qhcwh3f3+K|m%#@8}qnb(O$G;PyDW>t96oi6_y1@F66RJ`TBh4bp8p zz-bByKnMW{0T6)%X^hEEXJ-mo7y?ZsQ-N$wrG8c*YPBF{&qlQG-e??o7#c?$hQ^{j zA*N3UMKQpv9flHS)K&WZ30%?M29<{85%vV*o;Tx9_I4Bod1d4gF(&W4PSyPgsZ*HG zkaF`~V6WrxqZ*8xF_7^*GytnQ+Y9^o5kPye1Ya*aSbj`&ddCETZPu~?`yCyz(?NlH z1=R6>a%p4Bc9L6q2|voDEKFH%riN^1?NS_DqrrG^)xI}Ay?|NBx$rsO4<6b1!>uu^ z&mTfQ(DI3%L{eT3KQTkliiK9-Z>1T?3_YIIXprI8` zrIuNjQ??k;zSbi=RHaL*`o+InUCRUxJ5-D9^C8Ui5+FOws?*8HL5%0oHBaZjX+S<#eA8~>g9w1z2> znHoHz#_g+N2U+^O!JzE^X{YB(LV9 zq3E8uVGq)b$z`FR9>%#!@%TjbR1Wt0R*)+cjc2POJ1XqYMb2KW5%}{#{)9aJiWe$@ ze`sWcf_Q#FD-dPxNmq#jQ|Bl?A0X0?!uhlY+UjExmq5;<_StJzs6i$Py3aj_@!Ri2 z=enEFeR3(}j_p81peO*agn@o45a@w=bBi4zwfl%wbUIOh8X!coW}$WL zQD~iW78=JLgJ|X~n0dEhXUe96+-qgB8S=kup$G_i7TNGSp>kk{Duf%s&eGqwJR&m> z-SEj0W(pMzxQntp(J0VHCBL(C_UH`ovCMz1@~R7T_cn2kkCM@%E3wZD3j%wM*ubc^ z6_1l*!R~LzGSsLCy~lV>@``i<^^{ql=zf)2PhFfF6=Q2W-=^?RuZUq)h{P9Bhb@H{ zdO-icL6(cy2uplqdqrcvi+SJU|(I25vcZyqr>-5&~sDt@ePm+Sjj_4(~pYrw}SEctj2T zSi+NDW%Lt)D66;EIOwNuY9E2Z>|kouTsOA<>n;inN0vaG8yh1G!g`RQ(=yp5TA)t<~pP+R)CWP6vt}upNT$b;8Cf(vkEk=`bU{#<~X9}9afUa z&JCzJ)txTTl@P;I5FdRwS{J?^11Fz^c;+n44%Pu$U2lOr!*eyL@;yOH(6sNAI7GXB zW%Jp0hVm#zo~wrqL)aV00PM)KdCw)!CamN;Syq%Kh_<6&hWSG0*=3)nJ%pi5$p-5T zDRP^EUK!+-Q)QS-$0BF`fZL~BE$9vpy=Y3o%de?ts=!FEd$pA8%ZmZ}?nn zAIb}x2)^#cgP<&0efPiVC;`wrwhN=gk zEc{&AKD&CSe*t@eS$!}y8FsJ^5?A+MiGW)#n_vUy$!fj9xnWf;VW44CRscS2~)3n`xieCgBt zZTMw^R89cWitrf#tudsZ6(Kmy#%dVB6(0LM86CywO}An6M?XjR(MJGj0cQ{BW}8b0 z-Sm>6px01`$UIe<28R0kEf+rPTJ=grE1Rh&nVgZz>R~vVq`=f^Xq|ovhTeKH1`a-0 zEo^jX$s_o%>M1O`Vhi_Z5R|88W5?O?WrM-);xunAZx+cajCk|BONZ}A_&l|Gw8l|% z#K@FoFXjq3)_#}Tz&*T2ku4xOaCL?Q+x*D6kqfbPEi@-Ogt+=fgOdy&Z2*m|+5IUw zSUd}Ip8_vB3@vY-T}OZ6mYriuAkrtHKcIzdWvH1!T5Y}r>b0M&5v}dj@-kPhuATR( zuCn2w#uWVV5>E8A>7Iy{VfCu>aJ@l;SKH9mskf)o;K^DgpM9C=bzb*)S&=#I+)ji! zjguJDRRXR@XRG)xSfj(3|B@~p$&_Qa36D7avmQ7R{;N647< z!nuyKSJ9z;Bas@<^m|xzR}J_2h3S%d6jBYEbM5NCQ>S+SdDH!sJG+*?Sa!{L_HMw% z{h4#Kpl^*jvi;Brj*TFzE3ioxi+R}qW!}EbSD%bBUv8QwTphg(d7vvi2f*#Xe!ZN+ z+|8^Rf%B4jeeb$5YF>)sxfbolyR>O5qqo=nLKzONq~?)UdBWC>GMi9mEZ3p6(&J_n z7k2oxRTWl-yi3|=zgSRf=8pl$CNisu%r-OuvM4E8LdYOjj+iq!Vgo2GOhN`c3qsl+ z$M$Qk$H+Io2f6H7AQljj$e2SmBLcaXvWUMWV8l?7Y5Uw@afaJo%WZbXRmK{niJd?h z0Ft`iNP?I^tidxO1OBWVt1@>q&^qM=48QMP7&!PK?F{4wh{0gy@`#Zkpv)>b!ejRh zAUGeZ06a&8to*7bB5r?4DbnF~Np6|#o!j!h@#^gbz5++Daa8bxbQ>~Wf4DW>;;Dco9=M?{pd-t6^4VZ!yDk0M}}D(#U^S|K=YPFg(g(no<<4q zbDQCVXY$^&d07$?3C&r01rD};mCI`X65LMJB#{d)c=Dqg4Dnq_3yxKA0NVtc9bW$S}r z1pu|@K{K(Q7Oa7wR|b;-djv>i8^>f3RHq-*4@r6lKvq*7x~o@V`& zFo-k&$Vt}cB_4U@mlT4cGdtI9wdXS%T1z~C=Ntfp=16v~rGY8y6czwWGo=F{PQmR2 zJUkVHzke}?-}yF(nX>??3>#4{40cZHe2kDjXSbn7DD44~&Cj;8ji?FZ#VJp0MTgC% z5=D5Zl?!bQ{t$s*t&W1q%}rCZhw%nf^h6dE|DL6xv$T!m=TZ z?jMehD^(^|JN(?<{tF>|;gm(*Gh8IX?PO#|fPq45N)?l(h$ngXh>3(xjBU*N#wV|4 zy-rZUXG2-HpeVA=yMdbpaVW9-&N+&c}&U%52Hv#VW-c=xy30PS(Y@8V4HRWF(^5$E7B&s3j?rM;U>vPHn zZ3b*8o2sCA%@tkkBW=Zrf&<|7X_=_N?_ZzK0W4|jVjJv}afm|T)8=Q^|CxZ6 z!#)ZwDYT3>kFY|_CHOagsnpl&qrJ75QPMp@=5mkcLxfM_QHs&ZWF*vQy~fHb1e{CB z+vz;ee)}psL%;*DWo@sC!pov=?p)5A9$R%)g-^W`aU5)B_4LqufWv|EDn@H&e<}QE zS>IG{@KR>SNP`|wF#_(l$GlTURA{eNb7~Z59ak83TXf{L!UZ-E)zwA0*H!5~GxBJ) zS%^pqMY04~0PV-0z>ZIU9?3%wK{Ukc(DS92B^U+tBC=5ZTRMI_@jm?mxOvwCB?%_K=h9$NtCilE&RLHy4560)*BaZ-OuY*?YpA{!@1FNA3G+ZC#CCjn)-JKdYsVne-PfWf*X8kF1t67<{N&8P`=vbq?xLMlBf#0qUL6ut6T> z*#`=^zO1d+ax({6Q$4Z8d^J7rirI}fjpC^IObF+e5H0R4%$ zUvED~%@Ny*s7-h}vjLOjoI9I3mFM8U5dg^fR}l)H+cY_REvSwv$i*dRrL5lhV`l`^ro{tC{gAj*wsWHG^qIcv@6&U%<7tp=`A&5q-!80o$>2!ekX~#{;jc9Aa$zfCgl33u=?Ii2I z0m=qt8#yu^B6Hcx`%Xud4WPCMkbcJ!Nh``|!v}(FG=PB^$yL{4^eg{`Wa}0+mzqrb z^XQ}#w96VrIqil=noyIYn*ws9GJd!uvmp}G7fI>T;!82ET+9gdLFNUAOctbB1XDl1IE7c4J7y8 z4>8aHiAb4VLY~ge5vm+9`h0XE&P5J-r(E#}du_C%c5X)&K$ndM36|v~m{fMgF)&?_ z$qW;+jmSv=aRX=ybS}FRW8eQVxZMWne9~n9L!R2t0k>JiJeNwc+L*}!<*A-1qcO;q zu;K}fa4Pv-cCB$(gl`rRh1zUe_70M!SkK(fJRMpQ1drOn}D zJ&sNAqAh{Pvvc8J%>5|qA368cxaPbb`ZS(bR_{_Z!x>txPRMBBOnI$q;3D$cT8@xt=8%CwtmtCiVe93W7 z>L{Md{-~jlsOIG$U@I9MKJAw6FSPL$IQqeKl>oX9*!3KwVieVXh63*d$}FqLdB#`- z1p3H4EUa_CW6B~-6&+P4>V%4G8DFgx?xDQa4qze&6FT7E;i2dLh8%<}@Z!-P3OJOF zNxT2-2vOcXatwdL!1;ygMsZyw;4!!8%)0Kn3!>SXH)}_?h;qW9+9tL-)p;LCsi!XR?`LdqBzvJ5BW`f=RpcebmGOVF-1!qL&5d(=W2V z(ZJU%RMlwLnI4^CC0n2yZX9Z&Mm$aK9A_W zF|?ZgHj8Y{bP?Ng&sNp5n!24u(F<~jz@VkzWnU~O6ddVg5NxFv!dbpEpid-R9aiuS zxVq{LGn@CaohkL$vK2X*`{anA!&ib8iFVIT&Ts*;&aL)%eZyA&k?CKUJzXu$X^Xzj z$in^&vc${Lt>lTZMH{TKYM!womDzo(OtZ*V$*1Qu7v^Wb+y^;7z0~M#zuC`uZ#!Qs z2`)d}E~`UH>HQ#XqT)}ZS!|Qy3Naj(7tTq4Jh1Ku1A!=rq;stl_k`LP0GWj`kQL4# z&I2osKCa8$o+7q$GFTYQTRw zxK1aSa|)N!3$xV$$8{V#HVJ)5o`pDJueBzxXIN1z|9A61I2%*m^4oAnY~|1qof zi-d;??IB@(5T)!fWoFeO(%Naz0Z_&On$OW@T9Bp53@*!!Y#wP#b=2 z;!*%9NW__a5)h4G4U8b_OusO6R{eLU> zhz9Tm&=wsV&d=CB*X1=D;F0YZ|Mqv$Ub+-n1R9h%G&u#90(Ly|oIWzt_|+QFT<|lz zkPj8dPldQSWtB=Xp~oPqI7)PuIWKd3dsScMBp06X5Q#4@*#@xbHAb{j5ME@P9UXP)HU{v9}&$NwKgj_bDIl+w>>53H?`Q8tZY~SW~Guc0KN63+9hi7JtcztONsI31P zd7fk6$*m)I@Xf4{xrv83VSp%mprUM0du~D(z|ilBc|#e_W8c0@(9~2ME(maK2>pbM$;Z zG%l>>`fCI1=Qvc3?hZfCey^x<)ffuePUQ7CzL%lRvhij;jYAvYd`%1TU}uhi1L$0@ z+b?B$%l{Vw9s$+)N*og%c5z_KErX=Ad z%^lq*iq4trz!```7VZjOUMt)Ec_+@(cQSnY0==Lk@xxH)g6|W&BeC3)# zpM8F1Z;Ucz#&ZpVt%Z8*FxEuckPDRq78R^!A|#zQ#(sJUlEfbAvnLMTGLz zqC0!|N|f@3`{=J9K`GE`x6td3+9<*z#kb*3<(|GP8zUvMNhm&_VTbVR`aE}-{Qj+C zRq&!^7qhOg&$;6L${T&LI?Vh&9u3<3F!S904EAq*Iq6TLUfzS!Bw-Utnb6p^6qDab z>_kKRrsrki>SPpaE8Ae%lSF&pJSP`_aq)I3sIGD>3ip3k5^piY)#bT6&-y_ht|>*P ztVO<4o|Q7uPD-6QMud?icVpYf{|>xmvyMJYQ=6Z=_Y1vK;gIC0$JWYr zn2g|tH{5#6l$5f*bZx0yjvqF0qP=AGiwRfB6ym@GF!QsY#=ydbu9Lz6eGm%qXh1`j zcbMxZQ-8q&sA6N0-<8*1zN5Of{s&nAm!Z=sm*HU z%$tnNj_dlxDgvvSH;f|(6b}FjzE=AAG%*9SoRly!afiFJGOl{c%ama zEmD@}k`DWkMGzFGmBPhmkz2opi43&TXY6Qii0@W-B*Rix1iUPo77FrFh_8BqjK9}M z74K^cdI-r|3d`qZ9`(&zo=5xLH*9%S(IJLAcVBw8O}(Ht>QJ0h8$bvoJ9eOb$)%7R zH$pVxOm6H5YSvfC7rte6=_omyY#`Y=XN&u?9VzX=n==5dD3e9AJBBT55)9>+AXBRi zK1)b%>0NMA3Y}&Tem9Fd#R78aQyBlvk-0uh*s3 zz^iiEWA_@Y`d*=?glD(m;j7y8^c%rZl zM;^TxM;x_S%_T0OEYvf;&aZ3_18jfsFuRfpwH{%7Y!v_YFJHy2H{FH?H2_jM+65EZ z+`&2Lory1f^&c=iG+01SW+85`%?tC_T%uhapzW~9a$yHH`T{)R2Pce~oENPKKU|wu zNXKoxRwo%jh+hu)Ez$oy4Wvg;m3?*PTWfDWz7UEI*Of(|WDBEYeCn53Rh5*f;8#j* z)nRy<^Sq169U$wyV&l&0<0+-%bjhhH`TvDs#M9VQ5Ez=d{z{5=`-Sgq))u6Izxk{GDStsznCSq@j*1 z*KASjdy}k;JLK@yXC_y@95}BE4h4#b(Fx8}A1;=`filbNc!6dMvrL3fRScvuy%j#0 zoTJmFJo#5p;mc=uOxZ~5clE-SwBL6%YWFJ#tLPw1)Xkg8e%mQ|v#bRr#?a+MvU>}w^x3E|P){*xpM*7@Zbbfmc z8h^4EI7uP5Z3mC+0AvcpF=%KAV#*YV!6BF&9bB5e#>l@(W#*2eJiZxFI2Fx$CG<2` z-XV9;X3k=DK@RODhN!fCA!Qh_)-SAo2_rj3HE;$51eg+CM5Ov!0?)5qmlaW@wfNP* z|5#F>lXDR^+PE0M_L@XMBpMKCfzYt*bqWE3gxXOwW0qBE2vFs;t9Se{VlOtf9WnL| zQK5hPJSI~>nf2NTJ=3cNLnJKo*H##}2 z&drEpWG$P3V0mtZp|+IujolYnz4e=3N{{12v9+{_Ly~2P4v%88iMGB$+AE~fjT_K? z@L_cBz8Br4PeX3n2y{AX;nDCEh7ACIi&A@d-3S~giAA3$I*l1PFevCA$q zi}Ga^?&wx)e!j|3KnSioWVej$k}&;N9l@}#N=Qgf^F@Re2Evbx7#F^{zNi>^cET=` z^$CB1hXY9T6(=RBpGVM+l6bDEBacD-Tzynx2daK26d1B;bHiR%6OgMY-}&k_HmJul zrQ%bHje3`1x2n{6=~YG$JiDBmh(A`>$#HMmR-{NL;!E!hVh$k#pq?fLGsuWz>X0$6+sAV^MQ51GtYpQ+zlES0Fq8N_c=(v zsWs!72mUPv6X(%hw&pw&E8>GO3Jy$Lkx}tQt1|Mg723&O z(Y+c_c*q@``|Y;~`|Q6Lmb~-`nrNy>u-Tx8QlzqjS@UM#Ri_-SMa~+NzGAZ4YIj?~ z0Wh-WG|3Gd*yWlGOi}`Anu3u6h^%VOV6gvrtnlm1&W)T|kxSj-h0U|P_OVTEz!tr% zhf$@D+9>YcnKK~kpg=j4*^XX5LCDVAC5HlYrDX*A2gnyNE4@I@SMI_vumTIANHclw z^cpj@a7~Zr3n05PI#+d&BVN^j{AD<7RTTtQ6y!;Pq@5WPL196C7FtIsfOd8oLP@83 z*Bk-&6ksNRSM>*|KMQ~A`?I>H; zx0VxR_iaGnm_veX7a#Hn?PZmwezRRK!B2VT^oRX3J11A@d^$1`tlb0yeL)ceoB{)l z3=T6$<90pmB~2mMzkv27zee}=d(gV*d6MmDI$*aZ$w52s-LIS~ zW>sa|MTS?fdBz0W5E@5Tu+JmEY?xF5;0!2M0G>Kn!Vf;N71=8DXM1773E;(@KQvL) zgOQ9aTE54*0Bj)SCls(c=S1+nWkSdo%hhq3zYNy*#o|{dIo=_^qfUDro|W=|3gu>B?lS`!*s0L# zi%=hh`lZkwcB7qOfP@`Zuy*AmGYv%j9edzGZ2RI@(S7hCkYI>LoM~`d4d>CKyqrwS znYr1T4l|v?o}j4|Yu;(5KPe~K`?8`26xrJW8;{g5*y}WZU4YLlA5fc3RdUcJ-9ekZ zLz#RQq6Hy=Gw(>s%do5>>BXAvPKLgfW`L#se6M*y1jGV}7;?jUjDPC~NS;}NDIb16 z1`jw;6UPMsEkr+d7MJ*gWirb2xL{*2bHix|yQ1JfLsb<_M*A}&RfXrh@k|U2Ou_g5 z`vpsT8rXZ^J@Dq=KMxoG{y7*N96+|Mh(-xnR}<&EW|r}_Ofdzb(6)nRQ!{!CJ# z@01Om!owx+l<^=}Fs?^$1f`IFnh>0NVZ85N{I+>ue-<_8q<{FqAkvZWR7#BAeh0RH z@heDHtON}-K{5qP6OH9u2DC9I5{7W7RCXFxTlH83sd_VKFOQ06J!<5Pi{mEao5K@w z85HU^tD`az3Takrj!=Htdf8+FA&;+AtTS7>r89hF>9Pw8r3xaJ-7|(lfR)22rCLO& zjYsw#5kL_TGk^r??YCja)=i*~eHa5r9+_{@VPvSMu=fzIss{=nXJ?LpPRP(;cqD}M z8U>%SE!HGi8cSdW0nJteuY1EOIPq0S;kgy-uzB+q3=X%jaIal4Z`YaDHtl*WWdRJz zbR7GQs2P<~a-a8mKF$EMZbIq-Vd~7mky(~1K!_2MfT-9f!NFpFUwNbz-Ve^}vxk`F zpquxzc2)NqmHaLcVToB;Dp#dqELpT0%=){Y=ip3+RAoB4%%*!yayHvx?`F_Hu^f6U zn#&b+A>3uvDx$)0Re@O_yEven13EL0&$#7y#MQx9xh1H1)Btsbal!e>G-j+9ZB)tV zSN@3WK3Mc<{S6*+56uZ0SRPyU3`&GEP^ZI0k9iq}FpT%>MbXO%&4C7b&=L^7D(AiG ze)ypBQ?0DbG=zdeVXwUbo`p8L3VF|VR-%ZDu_8O4coy;Y`ou(W z*gDUq`{En-M`=>q+{;NO3;891~J2@8x%4SBHVlF&md(`AQJl2D@=j9<=o;XJuF`{0W#gXBY?1-UlX#Yh*S=J2~e@ zjx;3S9li2G~J?I?Aw-pNy$+aG1qG#`SUZhhtn` z6biE20$5*Q19s5v0JvjfqHTFOj(8$0QuLRs`S$g^C&$r=ssDG}H=J#VE6{3HqW!W!MbL?$`fQ<+(h7FOU<7|4*suEKa`CpUwC%#}Xk_k1 z*>1QKf3hCcpBLJCeI&$!1%MEcFKs~j z;G^i?d@E>dM1gB^SFHQduOb6&Z^-*$qSTfrTFe090$i|&p2dowAc#Pg4TBBP(1<&mD#DqJWOJ>V)ie!eaGD~TyDM7fpNr_w zLx35xAiD|D70)8Q`DV!b9{`Pw=L5KXs>wV6zTJP4EPdY*X?Xg6QI59JQPyk@H-Y5b~6Mq>@QB{ zAqF7X75V&YO^A2aDd(Ur&kLLz6(3aF9O2wEFN`hYQwm}jR7n(BIz(|%h6E!E347x+ zz~eF?5n2dY$n;iaOU*hA%H@phHfCZ@1(7p=JI`cdt_bT@wwh$gg6VHtm3fWK>;St! zM87NL_k%#c$~bzevX0%i{mzV*D$r@MDrCJ1y0nHZ&1fwH@F-}6^7|f;v>jt-*!Uif zT}FuJiK5~@iYioFg;8oeQ-v8`)}x1Qi0g7Ph;X(k*zQ$o5hR*FWd}qebn(BDdKrk}x8uh?C z9xWDbD%hM5&x%0ZzDZ6aa@^qq6AqN53`pmhW!U!RuOof_d6hv!reH7xGayw&v~k1{ z7FlTjQqz}5&g?Qpt?ksQIexmebx4u3nGl?zWQEZLxxpj?7W8 zBG<7j+7i}IPF^lCIUh}`F__M@kqd?cj|4FyL5M{Rfd$a|C?^?zU8&p$SDfSgBn~kB>y2}~=a}u-RXLe*Hf)LhcbmOpDK)Q4Xz5=b5LnIt> zi{LrjVU@RB{NYaxWOtg0Uz=?Q0`}ENDHrMq+sbO%!p-8q^WpohL*&?Kw$8aIDz#u& zAK;|dqv89-P46AOV!nbHuDfbrVsoBfuA_&@_8oyP*#5E`6n>XWZug?VdB=_(K}8|i zuE75724@_AF;RY&ItacKfv_&@m9)H!Z~n!8Qic}GFzT}2fD7S;a36XeL42Id=XoC6QuQ%Zv^fjc z2qrIXP@AaCoQ2q$BQK^;N8`M65g&LUM!xn9q_^G%=2W$jNQFEF(7E@1jNi2c%?saD zkiFo_{$MzStSc0}ZHki27$0w=)9xZkQYBN&5K)9Uj?ic{&}_zNG#ZvTT1vr01IdD& zl*13<@W~wp#yQRL47s#PV02^*<6~`%kGH|h5Rt&ZKnnwdEesB}AnbIGHZLrFV;ed@ z_+>jt1R$c&m&#(GfOUa62(p2<<$Pq|PW7{fyy$iljE%K1-tHjjBp?DB%?1VrnrO9J zYJ6v3LZG9fqye;mfPi3ulw@WjvQ%si+Og=e9VJgJ*vno+Zc80;>$TYg19}P!mFjB> z$bVNg>0ZKapn&UflXbtMfBh!e zkrPYB_)d@G`Ii9eVk;P>+QNR)BhnZwd&M^*aX3uZ~&PmNJe&``|JuNH{68IbvK~<;)@V*q{^4BXGYqJ7+^vSh)fygnL!Bo zylKNXBE2BetdUG5#MEhMoN^MHXPBo8pMfZfoIYSM(smo&mo}ht_XFts_F5zl zJ_JtM5HVnXPbH}Vbv6db;;Wv88Us8!205}_^&gqZAvu^G+;?x#UwsUtQ-{&M;tG&V z6^~O1!9b_mMt9vhU5Vm9+k-A-X~A$lZP}07QCAHP$H&`P^V~W-_UKc1^6_Wz%rmR7 zVZ$cu*gk@!+XcXAG+P)P8pN!bGqKzJx!7}`1=#<VV~;yn%RQ@|^isz97dPXf2bbc3dmqD-k1xXu z&%cQ6+jl^+L>v>Q&zOe!yUoKvha7;Tk2@HL9J(Lo%$W&6ZWPS?Cm#n|lVm~8JleT= z^H$t<&tvG0cOgWS=|%!Ua*F*A+6(*cyNA;WWGS*NQ%@FZBl`~o6Gpa;VcF9w@xc9$ ze~tfQbMMHXb7Bq+R@l+ulWHRXl1;%{dvz0*wfuj6#4EY zkK^8Z9)UoFC{oj-2m&&ZXb#3W?@ec5mtAJMlI$p2!j{e3@zhf*@%W=l@#K@wXnt+R z*vL52RDy`mXf`lBWe{^_&Bpu%bFt5Ui*Uf=y|K@}yJObu>1efDV95o$}nF94N+J1%*d3O`HRYTqmKK>Y}koyy6M z;sAkYul?Dhl(}AGoJvvZAXIs2K^}6+`z2N9E7kL>#;EtI*Kbt@%n1dhUPq=la1vxE z2Y%KTSnl~kscZ%YFxw$B02IX#d+!NJj2MiF04XI>(Ln3M3o!i0A4Fs33~&4@#V1VE zWBYbM6jM0)9gxSq0KpRIbRhUMTmqT2RkjMN^9tk4M8HiSGwFzu?|o7ZsEL3?qOo`}2HyKF44!ZT z8iRwHt`upaB7IR5XPgnO0W=maL~G$fbWS}9qgP&!&Ub$ZUbh<56groz2C~`-F?qoa zoeVPtBvMYs=r|$;u{C&B=Gx%P!MVF2`jZcVyImw#UI|LN5X|6ig2v!58hh@Ef}?Gn zo9R4c=4X3-o%JWXcSc9YvE+^i@XJfC!ELwPi?wT?$JmZBNTm81k(EGDJ5&-P5Codd z7}KXs!~EUn;+SI&#p!1rj}uQh61(g=O959u~8Wuf>y(KaG*CBapE3E;A&g=0J)@6l2D$893~S zL-58oy%y)3e;O7pm|GZF?CYm2BCIzFCc=spYw#Bz`G45-!dAr8f^sbdm?S#X#>fBe zgZT50y{E`$Z8?eE`m)S7OxU)02X49PZd`K7RakPzec15Q2BaOcL9x*C!H`G*NW?;5 zX!;~r|5kNq=bJI(2W0JB6w z2|n_%Kf>Sszdu48M@5}p71;osDKJL9w{082zkm9_aP<{8Vt|HV=9LIU7?)!>_4MQL zhBv($Wyk83>u}|jx8m~O+=wS0U5d>cH=)z%08mbr4mrVOK9RaT1c_FwiRm+@VekF+ zz%j=jhSN?z4o4qr(y#P&BGLBI>)DC$(R!!W z6pmWppIsdhiqw_~R9#Q7(1uVnw zEdh^@LClzn#v9*+!S}oiV#aj!-J2-;vXQ^9jeffWhzJ4-R0hGb*1T$JE-UanV;15M zeE^L&oP&7E6crJdDToN8+efi<>63W;@kgqScOg3CCNT%&-?mQDDBX{dZX^8ts8U^6=C6@)v)At1iD48#iuNQvg7SF$i}T zJ3vLM4fI!_7ahWu4I|jJ;Tb&n;Bx%(Cs*O{V;1B6fAVIWbN;IlM={KdpnNI;gHuMg zGp@^v^j(P#IvCwP1~Aa=q`2m)JMo2o{|@e3@&II4s>mf8*|Z^}EF+GgBG@S?C2V?W zJ8r&i3GTl2A^hs+*W!=<^ldo%+>h` z#t8z@K^wpQ&2@OkyWW6(_unHB71E(KORnrlq^&cV06~NY?tKDx-hMYk3ZzIOi4k!F zDLRN-gbOZu4Q9`okvSNw9huP`ZT#w&H{xqw`2ik%=rN>S38DGa#DGN>S%DNKE5rl| z32pmSAaw?B%Xedlr6GdfTW;@Gl zTpnY_)=LcCQZqM%KOnL4FQi0-R5WdE}$`sU-xCOUNvoJMYzDcopN!=L1_1oCvQ`<`w|6 zY0%&-kF+!5`3o@h6Q4l$+{cl=v;onA`G}7^0@0LVM9ezUYz$Klz7=eNn!ikDDnsgY zGBE(5*XRKXsHR=e&=kaf`p0Npa311DLtO(wO2&it-;W=B?>o5dmRqo8%N8U_0@Ar@ zMIty{MuO;Zh2fH;mZW7>2aam0~$ z+dJNYQ_g%fnuCL2$!clVDaSwz{3S;J;a`zH^Atp@kb&zz<6eIrv+St5`!gdRQQY87I%&_WBwY^%)9rgaj;D*7*=rfW240K^g6?G!iN zumn#(y%K-@4d|U2MCAfNy8~+A$xLl#9wocZ zOaQ0UWL*D8iEQ=UNX3q*m4PJGeWs*cJu#I(0z%~mB+~Yb^GJqw-w(5v5#zV7q`l@S z^~eI9Q0fU=zYEvA3ao|ju!C!rZ87ioBvvR0qZ-}y?7hAJN@n?u4c@al-VS?buh}=; z`NM@o1p1~R1!|+&a}dA!^lUMkV5$C*HtOU;HOC}ILW5BOM6p`D2AS;?m2H2^5|k$3 z@f6Wp-h|fquScU1LvR8RFg7}hOMdbbeCa>`6KmG2Rng2iM%-wk(TFqHGd?zgM<0F= zPdxr8ZoB1H{KY5!8i&6BUEtNLFn-4pwe2_nHJ@7>H%b+MBmj~bV9g@cx_u@>&RvOU zx7{%1Pd+;H8U`13#eN334)0-6}m&`p6i zq17ovs)eT7QOTCVaiqTlNKLkiqN^#T5>pz3IOFtJq0x%9%(!4m2tWt{&cKic!cgFP z`}Prh{mVbYXFl~+jBjnDfhHtU4GoP9Nj+~@&)0{N4V8t$UOle>=%$TZ@Ws!4AM4j| z!r%Y%pJ4uOyFh{=SY$g&I;j=CK#H>&O7`VL=B%~yTQbSK%_cJkF~-Nb_|kv=1fTo2 zFJolOb_`$|R4hByl;$&VRW)hWLWvYY$KTDo@Q7nP|J)1s=fC|jHodeR?eQcV)vE8* zn3SA?IZ<NjFUOki}uQ5UZ}|1>PvW1e0hsqT+%Y2#o2@$2~Z zH+~H2MrfjmF1p&G5Fi$+ow}`N$`K?%km4DUfUxG1DE`D?Fy`($1E-ySoR(enUBSl+ zl_ANpAy@bThe3|Lb`E!E-cY#(cu*GYbRdGd;LLUQae`$_{kE1yL<2z6zXpU^=Uxk; z)k5OJ!pkN^hj*GgbBU zywAmJRUV6f zkYnRWk_74Kh>p=(bl_R$^q@W=if)27Wps=?>r87CvT_g+!`>8)q3YG9Ht=3#l5*Y9pO*edCw-{HMN-@of@KOw;X9P620M zeIydJ4J_2Ps);`hv=CYI$q5oM8hB~_CVb^{KL87f7%km@-Auq4sRq$zX{XjPoMNwi zcf)I6cRHR~wk%t)&bqcH2oK%+IPP2W2wr#IsoF^>YEu)n^DgTsIFNQzTy^>NSiR~w zG%x^03ZbVPwb8-+1-s#OZ#W$yBCu4BvXBw3_~rHZ;dg%u?gC6HQtl!`GqV&aBocJd zien564I*j@#8IT?aCcI)$J$8RjD*u%PW9IWi3BH{cmxhQWPjZ*YcnJ#(9o}*FMrI5 z7<=prS)$Mp!di}fGUtwMu{<10jRrRY^=w|=t-0fs)f@zyx(j=p zDYMzZbih>7SN1g%%@6WDcar^JW&vN>=XM39K(wTfD#UXl@5E*A1kVdzKwf(JGv#gT z)!AV=%}c4}v9M(zOY7{Pv!$LN1jQwV4u#jJv zH>fhD1e8a`sj3I6y=kYpW##0E+0loNr%ry68dU3TzPszzBf0N>qz~MWbj3=fTekyg zigd>)%;)%gV-f)~r!q?~B?uff(Rkyz7+kaf5vSRrt6Oin5nuiCmvF*Kr{Z7#?X#G- z>#j&y;^Mcy1)u-yXYi9Bd>3gVbzqhhKp>#iXyBW(t{crf}=Rbqt(@(<4&n^Qs zVzuj`0romug914!a;Sc*$eEfqO#n;CRHC{6{%D+a27dhGpWv6j_&FLvKtw{Px>CZI zLL%uVSbWIA__r^79)})wI6#cE&V4QRTf9F$|C!HX?%cU}&j;RzsL=vR1~nTPzTge$ zUUfavwa-Dc8jwO-8(8RQw624hAln^uzw#|?X?zzVjv=N`2kpBzqGOLke8k~s%$KF2if2~i*(X-v`L!>in|88DEpgtq2n3lhe*FE*vF|>6;6s1*ZcuEpfaqx}9(c~S zU)P{rQUb69vxJlhx=AOCcS|x~OBBb2opImtT4vPCxTFv<6%H z^GQ)x8#T?A!7$Fm^J_QYip#GBrz-j@(FUQ31PMAciql?wJoZ0uPe@K+(oVkk{3d+o zTR+3d$PP5nLYKP`h!JV$U<@7XK7SETIqi6y_^KnZ`(E=gW%>{rafCEUFg7xVO&hmh z&FUBM)Z@=$>0{5}xs~g%W9v8wz|e3LXTSbrOr18YBlDT(>FV9y;nUKi?A1_UuXoV_ zjFR1POUaj1Sl7J@`w4D_Yc0}Szb#eIaBbSS;;c4~@y@F-O=)DEI9R6hP6@8-gE3`I zod~lEN4>4Q3*JT^%?dqZx1&txi$E;DTN&Z50&`X>5NC^6YjB6QO)hY$oX9fP)c43x zQHKAH&t9&$PsMWs7V`O?xi@LfvvNxM&UaSA&v=1O6w7rorSv*ND7eSKvA4y}J#3e= zPGG_Jrga^5yCeaf_3JQp#ntFsekJ5H&j90XHEkn`K~V(6F+fok;}h9dQP%uk04y2N z9($m1@(B>cV5#OduYX|!zWt4FU~H_7cf9L8n7`YuU}i)_n6t}lyyf?QAJ<)dCDyN7 zk2sF3g>x#$ZZnQ?*B!Ux$}6tM+h21A=&I|%W8+z41PcXHSw$};sj;R$!JB65NfA*5 zmO$f-ld)?1cKqo3--k>RP@{oF=JOz-BbX^moOSluI7ILC$5Q3qecd_d;L>0I694_p zZ{xTVj>i$l9;fD#GNZ9*Pc%+A4xOK`(h+adX|5o%!RoRLtXqdvCXh)APE*hwcR~E@ zQiy{OLVV#H(K_o4G-u7!^LLH-l!tV(l*`VpGu9Z$QkCRiE7q@j30Gf!6Qm@>h`{J- zz{Kcc6uT{)hrjsi58;A~Ux(RqrXvzzfn@;g@eVd`+=9m+UWS{mS%O=xS%P&NU%>0m zI}MFSLroXSwoo@7biOm5iQ=lcx2!(7v&r~@?1>a9_TPUWyycy*$Jytdf_?Yd69a=y z#5!wAnj{z>Yh%TVwYc`G+wsdEUx_CkUy4K~XrQHI)XGhW5u-iU#W%k8bDVJUQ8@0z zqmb&mA;&nC5|U*W^)_-tNCEBwX#z;I?YMH_BuPEJKz%k}wQ?Ok_iz7=7uRpm^M6$a z5rGC6T}ZSc(ZQ@;X5tNRJO}4rbS4fxY=6w2H50L(%9N%OBO{|&_Vfze@Y_3a+0U-W zv&&XPasndbT&9)|i!xzQbA*io&2#3P^#X?#3Cme#|DNVC~Q9->*$!L#vu<@l$ zc=VyAxbcc5xbf<{FlCn(PCD)AOc$8Ig=0oT#;k@OFALCA1r&p2IB1j$*PM`0zz&#c zMFd+Xl-R(sK$^9WrX+|h$4r0@4s9c5w7Y#{~ez$ zD)P{IrtCbxca&WVwQM4h=#rxzfVPCA5V`Z7@#(Mch;m==1k0d68NQcOhnPOR=c-6^ z#xt+OdbPz-sD2%`7fdGcP?!$3uKDNo!QMM{KLv#v;U-GA{`3;s5RG-cPW!Qf1}kr` zbtU^_rfWzr#vghJBVYb6q<1X=bvr;K1`RZI&gTf(4q!;t+B*#q()b)~d`DQq5UR z6I_1TWq92wCt_%yz0i65Nr)&`%2^mWQQKS#Fw}O507!x;)>8!({1k&z&^+b{+ z*~Gwc$6)O8E7jcWEP1*SY&~B)v&s@`a})s?12R&EJ4(hNAGi;4=`y5u-izde@4>)< z`y+N7drRY%g?K_T=35s4Pc3~0t5&Vm(;ZkxE!BL{)<6p%{n!Wa-VeSRQKT1_7Jx(k zDXBHk!2AVsF@M2aoOaf+Sn<9$;*m!l!(qoBq~u8gQs_mRmeInq`2@)if;F#l2#792yF1+?NCt~W*kkzNEt&Ny5 z&}w1!2{Uobv4`QDH=K&k|I7Dq?UlEolZ+!m3`rdjtG4D-LUKnequ<^&QUX(^4dG1} zzZN%Mbvs74wqdq)VW{aPYuCJltFF8ihaY_qgb=wcu%n{vRF>(Bt(!-1#bwu`GnSx< zsYuX91T}3bL5h=Jbv$16s-v~*E0jZ#B)EUcLl_$y)k)}5s4o0 zI^{4ucNzIq8qV3KcrDQyXkfwa^RRHw`FQnfPQ>F6y&jvlY{723&jX~)9aS6Y`Ua#= zLsIo>c`Vnn`#xR@fn3W`DU{g5{!_{I!YiCp7FIJC>x&MZIXN#6CejSkS&C z0$eq#(&4+0V?uS>vLE6#IjH3j_w#$J8kIl*pqAzHn2^4`yGIc~5QS@(c}zev!hqZpel@-Y-?rjm1; z{_NbZ(2RI*;1{GU5PqQIgKhzS``CS;n1ZL+>&z;8H+I*Z*zy1V1<74Y)YjC41E8n@ z77~<@~BijE5fpbZFO zCXkHPbP@rbEkZ>%5vkwNF1uiS=5*YB=bfru!rm1t-{zM{Wes`gfd{c;`xb3VT#nswlJ>M=bTcTy$Zk zMnGp>P>oenJEC|1Z~cP{@cFO&6^=jl5JVK^5ljYH&v{q<$=V4d9JqKNeDbq@j`x4) z9cVRMI?AmaF(!!!4Mg0;bywYi`|o)K5d>tG8j)O9ve{xp$HVpFRR)1sKtkl3UOAjs z3vgFFy9U4b@nuM}EEf_Hpug~_MSjPhdICQG^}okOZ#)xIhXymAsthB_ku{DVvqT(4 zIO^EL@aeDoCEoa!v%n(Jyfhg?Qsq>_1c}J85x%f6OT6mTqjBW%2O~vCyHo-|s;0su z5w5!8R;*dQF5fNJj3ceA96hPq4*>Vx{RHm3^I^o8s(Uy_gds>YFm-r7-gME$m_B2M z?t6lu2<>(Ut5&boT-LeLVdk~RIN`+Oanvyf=5p!l!E6^y)?WqGC7DVz2AVkPRfpot zb5BJaH}rGXaB*}zih8;RXz|p2&4YvGIOidnvdF8a7y_VL`>(+!gqq0+KU;=!CnZ>^jQ=n9;RE~{0J=|+ zuG?`tSCj)J9Z$)AIWRdrTsZga2)_fV?0%iLP>=Wgh-nSFxa0)~@KPC1U6fkap~<{u zMuE?6KlpEPP|M`6vrm#+Us)5V$Y&~)Vx9Tvl^95gF#hmE82R`AM7sJp(C{Fr*#zr| zvjC~72QmeB6QI)tCkc=;I8DGZK|Uec7)k~jF{0ge%~GjFB+wom#foQ_L4eR{ck$hC z{ui#h`nTA&bt}e4$MERG591sE^)-y_*sd~m2!ar#7n9|w-6BC48y&&&mCMoCeK!>$ z7o^g_u}WQ)<)U^{+T;;K#b1ed)(mXxcCc#Yax`KUwKPT=qyVpo)S{zSvxyt8zX9L) z+E=k=<#KG4d8_r)?v+aYqI(XglNtjh&j7}gn&KGMTvz1 z>BOv3@^v(yQwbqigD8MR3>Hmr+=OgKNFI9(<6rm+Mwcyv%r)0tVV2tA)!X0LwpcBX z?&j(ID7$*b=_lh8|L}*HH*b~(%!&~tb&57^xKz3}GYr%hk6ANk;Lra0y?FfvXY28k z=ddQI{2wo^--s)IbuGHxjxIaP_zSYew{j$e2NoW`oR3Y@!H$*>{H7S zqXCH|1M^_U7{;*gLHpw~pZ{wdbNIgcMN^i{av|F?r5s4r#)S*!;jjPcgE;Bb!X#tPoSC@r&97BoiM+hg4MbS_#4=oe%^eEZQ%(=Lkq}GX z2xH?3F1z#^Y~Hj55t@*cf=FeY?V^igjyoJ@yyjSt=|2sU(^O()bS$G&IX^K>MwMyP zrXX&pZ#1Om)2qe2CjBA-4&N-YSWW9-nP$McM74&mO&}6I(PuK;X8NfT%zFwVSKaC~ zWYZ>aRN)0%|4=q=iZ8i9+spM|gJGqQRP zyA11^336q8_dPbhy@9q=Ie+#wyXe9m6do<1oZb0+DVyj30q)Q20#^w@MPC_!Ve{GU z8>ejyuplJQuf^!szKL|za}avlS}9V9h(W2^-Hw@61a#^Y&_D~=F#>Gg1|a~BDBBoB z<=HicU8rZFqtt@X>2$Dd+g6pPE{^c*Gtc6${`}9d_@IN(8ff9^r=P)d&pn5Rj`$Eq z`G6CY&G#01zU{`1n}NOe(U~k%OI5HlpUa!In+YuPbcD>TBF=*?jE;?BbjL`Riq9;B z6d*vD%pD3yciJ6%`HNq|Z+~+Irca-W7hYJ0wQJWxkU$*A=(M}owr#t4TTwMMfN1(u zBu;3E)aDgt{)1!yF|crVq;Ug0z5`-p2gJyTnwJ_y3N#9UARuBw`p}~o|L^Z3`s+W( zz>HY{=ee&<4pB$I+`yYPU?}Iab{?9ohK`(wJZ%{P5l47#^>cV}{Y#iOeX8a!7si4D z!dZMeBZLKmbm|zX5H1L$WRCURbOTb^^r6l=qNAOx^B$+%#V)hw;g3H0R_wm}uE=(p zG=Q9>4vZ6|Fp+ST?IOzv%@VuJnS~F1^!IV!l85m8>K75w0Mx{RuHrcpZoB1Ptb6`N zEZlRUrzs52^zQN*5H%5L^DRSK<4;3WQt9nRcXV*$wYQ<2bTEXeU?hn2)QuEfw1!%E z&!1e3V~#uk(D|&*yl~Y{4bWM5214;(GGnj3_rOO!{vJH`*b~^eVFOx-kmocuy&ORh zK?o)9%=rSwtQQG5^X!vx(BZ$r14|x9j6o>JNlgW5kGJugUtWU?E_yX)?J_6R3wFQS znn)1g*{7D{wi~ZU%o38u0a6QAQyRhGU>6s@^#shFHx+6{q-h!#1{`*K?Ipjkk_s7~*(H}}z51fB`xZY7edFPrAPxw$ zztDdHN?z$mv^m6tYW6^+JrAeMaj;&kQ+&7#Pmt_~^XMc8nNGbbs7Y79uliunFR)M^ z)H2+C_kDPN?emDESS?i3 zp(>fC84XId6)07;sJygbnFHrULbkgQ^LIn@U;YtOzW$$>`d?qc(0}|p;`hD}wENx= ztRixP)O>Vi0!NJW=G!oK)wPfrc!twrr(u2 z>?#Cd{j;DMOl+6fNe-#`qG(*ZaxI>GbSau>K}~^5HM~!epo1fiJ_v8R;0&ECLF7RQ zlbaeUPqq7n$(CtF>&d4cgR{;)#Ss8C7drx}Er1|Mz@B8H)(Yej#VoPgZu9Vli(ieX zDKhx27Eg8&fpPCW58}?-A8-I4TfkoFAmv`9l5y=7H{$tK&m%??sc38c19UKo{SVy> zXPv<3#T$D)OZsRe;1%vTbXT5k{!-}e|k|Ed4R%9U%iPA2!CLz@|= zIrrG(qZu6TlWKPCtP>))i}<-XppygorNH%Uj0DIDP=>_kwIcfLm$^bjkKtl!DKX8#DD+d@+( z^zF56XAT!bxg+K^kiSL|`x_88;--EV?GQZN*?5 zVR(25EK{{!GDq1t{Zgtqxv2tUjYfoKvjGuBdV>hn8F3@V;Lrd7HBCW6NAWvtl?6cq z!30WeYmpPNoCQv>PIKaIlr3K?;^=bVIT(}$2?6e7zT-338nbaVud zJ@RD6EA}9Qa`fC8&Kk0-Dfjk$H*Er*edam5_|gWQ58KGBg3l1ZX=fde`SW(sV_P|C zLgwYjIgx8&WI)(A1P=l{ z2*PZf{kjXW`+`05Xui%lPf&Cme(V8=2L-z50z`=PE|~%XX&d;?*MEsWe9Oo2umALA zTzl;;SiXEEwrt&sZrT9}%A7RYQFgJHQ37^2fC5U_QOlJ_m1z!R1rG>>PQ3sOQ}OL; zn?s&N7cqY}ugG z`^4&qiqh1+w@ocnfvScTeO6(;)G&u-7tT*`Hlqf$PQrJHbgozh_n~N2{l(&GzZW);W#u~tvrg! zj93ujL-q&#<)0!+6UaMm2O_oDP(_4+^r@#Ye%1A8ec(MRg2=gi7522SeuuS}dT5Wm zcE=g7IS$L7`6Y;@&b6!-^2Ma)=|2A0Gg$ig3JibkCz!u*F80`S0rpz72lm-_Pb}Jd zckHofH_V+s2Q#KkMXS}y$|o~_m!txMAO)nQ%JnQY=q0^FqspaXJQ@WsC3msQ?72An zi2WVeRw)e0i##I-(#DgtH*C?2*njcfn77+ZY}~RzFBnw0oC#_ZomDH>B27{y^ELyC zKq3N@+BH#`8NE9o5kN6m#JN^v&Znm^Zvvs|haejtI{ZoVInJ+@RYQ0)L51BlQ; zf<5 zK3KNj>BF{81n2m)2ASpXJiu7fFCk&@qkj{{3jIcIYaEjXpv!?;IsJ9s0jfc8!4dO1 zk|x0LD#5Q?RAm_fVYl1jc&x5%E6$s?c;xfcr=v<4Fv7Zr%j(SruA2+ip}lM{!7F?+ z54PyH`0PZ{y$)d_v>$sMojdLV>5U!;0)kjAUOVP!wEp1tA*M|MF`KO`6y5`XDZ^-< zaXQ9sxEY+v+&Gf@#4bocBn-KHCAiZ8H5&j+G+Rv^bL=s=^pam7NmK+iSCpYJ1OTab z+9L+5SJ3Ttu+O5su+J`Y&{_Jl2Dy5XnFKhpz%$H(waD30TJI-~WYZ>0S-l2FA9ECL zx$$PCGR-%rG59_E3L?~oEzt8#v#^@l8LiVzu+RScVb|Rjz}_FQyJjuYjhpoTcZpgI zr8j~wB}aQKLi6aOP5zKfLJgR2?LHsrAHNqPYgR+9cm@#(9YvQwr-Sa**P?UYYca5J z4{PzA$4lSnhX#kL6|Gha@A$(v;g*~4#p6#bM~l?Xb;_9{M2J=91!jzHk$85?Iz0W@ z8br7cjX1)Zx)i*eWyi?Ppsdtk z0~68C94d*KGp1th+!-)vl=YlvH7!Kvme%FCo3s?$&&-%PYZ~V6HXDyT@dO%3tm!EN zK#Z3*Zbhfl#n4bA2Umu*q|&9E2@qyKPX-W~#jx2M<{kqnW5bIZkjezjEUlcH5+TvS z^cgd;aQAr*?MAWycX|q}=`Y1YUEgKb*;uq_Ay%z;0YId;v(F>XoC!^?ZP_>_S+@|H z%_h#j;5E49hgV?z^DiMrOZQ5O1{!$b`E|JBH#gvjqZUI7ldU6nVn`6SZ5zdvmtBsr zu}v7DsYs9@rU5V#q_hoZoO2ouI(Q*Wh7&!wbKT2~-4`yz2R`&RJpJTK?ASPt2+eG( zb4h9^LxBj8jP0Apv32umJoVUW5blC#5{3o`Fk|*q?73(m7BAipM;&)Cjy(1t?6ueK z7#J9Ul#KAi;HoHBtxI0dlmKiFKTKr?tSp*~;7L!6Rc0%>q>PZc3j|-?I{cSCf5G{J z=Hr|av(f9Qijr47pFQK#Y7@*g3$7Ci4+6THKw^@?GtlQ04z78v>OM_UIazsu$R?<$ zH|=z8RdsO1J)e%y^Vu~nz8n0kQAL-Q3xB-(GF8Q;($8ZR@>YCDnz3Zo-&M~xBeblyUmAS&N97{^NE1a zIR1Dv7as%=gG7`snG*zJ32=(;V~?Tz!dft-+9DVdC!BN=_T6uPjCaQM&TX85Nnx^n zKvBkFwQym988%q6(%!@ zOcS&pd>FKChpG=r)*l3b1Q3Y^#L2Hha}TvZjqRS*JRwdoc<}yc{oaKDML^nBSvw?y zq6q1-WoX}dw+0#|PF=}BVDC0VEj&Byh(qx4zkWYvx>+H%kw3>rBvSdMzeugv!>4hMWJ#- z&OQGOy#G(%jDhJbBsv$g;YWlx>q|9BB|=OQK#a7TV*Az+tXjSnx88UUzW$XT;3I$h z4|vB#AI1Ou_~&u=oe!eh?dma6f@F8%2-yWcnBy!g^B**}-#)J@d=ic_E>N!*IQ%jr zPyl)V%4qNcMfUw(xTROS>h(4iX$;Z46Mf-NJhk;h%>pL%=nFuHfLm2mxvXg+XQYY+ zRrd=~Me|M|y4#D*mCsm)r|L6J!BE>aDii@NDI@>EmCv}%bRn1ttXYHZ?RSA>0>~~XQ2<(Y+7L$^f!47{BVxS? ziTkvQF5dRecY-NGk|r5gXQ`M*y6tAO4?)n28kI`vCK5*-djd{7<1Bz!1-OaOeQ^W2 zH{J|%#=uDjNb*m@#CnQIqXC{b2N)mE1WV0IQ2;?r3}D0r44ribqWuqq?4}SBD!NVp zx5v@G`8K2@JF?HWzGGXaP5p>h7A1@#yy?y7;`3kp8ytK5A!^YgpuiWYPxS(k&UtL+ zovS_&BoITOfd*nUkhX#6*Sv_!e{&N){wJTohd=mFxbMD)AxJHRlXm3UkitNcrrPn5 zU^WXr6;O*B5u!NC+fbPEjBF8HHfSq?H$;Rujy*7$>VFb4P4srop`ZaFfk4Xka>5vl z7?Oo+2s6iAeHH>*P5=OG=}AOERAESDf^3U$=MmBiR5i$D%ZU{>ze3TA@F9diqtS!{ zy2c!uRCKHz&3ufIO;P5QC?|SoXb9(D^cqZ?F$HP1Q>BuD1{zrQ)C$~u{oUDC<@r}C zM!PNXo6By(3omU&gy~>3Ab1eO18AoajyUdA9DVc&IVDob2@``fkTϚL(*KlX?C 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 Hu0mjfQ|V#5 literal 0 HcmV?d00001 diff --git a/src/front/components/Navbar.jsx b/src/front/components/Navbar.jsx index 0e214e8281..c88c7bbd7f 100644 --- a/src/front/components/Navbar.jsx +++ b/src/front/components/Navbar.jsx @@ -1,31 +1,44 @@ import { Link } from "react-router-dom"; - +import logo from "../assets/img/Logo.png" export const Navbar = () => { - return ( - - ); + return ( + + + + ); }; \ No newline at end of file diff --git a/src/front/main.jsx b/src/front/main.jsx index a5a3c781dc..409393c505 100644 --- a/src/front/main.jsx +++ b/src/front/main.jsx @@ -1,3 +1,5 @@ +import 'bootstrap/dist/css/bootstrap.min.css' +import 'bootstrap/dist/js/bootstrap.bundle.min.js'; import React from 'react' import ReactDOM from 'react-dom/client' import './index.css' // Global styles for your application From e016207c0266ef594af7b7c49ea73639ec8f756a Mon Sep 17 00:00:00 2001 From: 1v4nP4 <1v4nP4@proton.me> Date: Thu, 23 Oct 2025 17:15:24 +0000 Subject: [PATCH 06/27] Modelos revisados --- src/api/models.py | 33 +++++++++++++++------------------ src/api/routes.py | 2 ++ 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/api/models.py b/src/api/models.py index 4db4fc16c0..6d5d945922 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -1,7 +1,8 @@ from flask_sqlalchemy import SQLAlchemy -from sqlalchemy import String, Boolean, DateTime , TIMESTAMP , Integer , Date , DECIMAL , ForeignKey, Text, Float +from sqlalchemy import String, Boolean, DateTime , TIMESTAMP , Integer , Date , DECIMAL , ForeignKey, Text, Float , JSON from sqlalchemy.orm import Mapped, mapped_column , relationship from datetime import datetime , timezone +from typing import List , Dict , Any db = SQLAlchemy() class User(db.Model): @@ -19,11 +20,11 @@ class User(db.Model): registro_fecha: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)) is_active: Mapped[bool] = mapped_column(Boolean(), nullable=False) - tiendas: Mapped[list['Tienda']] = relationship(back_populates='owner') - resenas: Mapped[list['Resenas']] = relationship(back_populates='autor') - favoritos: Mapped[list['Favoritos']] = relationship(back_populates='user') - pedidos: Mapped[list['Detalles_pedido']] = relationship(back_populates='user') - notificaciones: Mapped[list['Notificaciones']] = relationship(back_populates='user') + tiendas: Mapped[List['Tienda']] = relationship(back_populates='owner') + resenas: Mapped[List['Resenas']] = relationship(back_populates='autor') + favoritos: Mapped[List['Favoritos']] = relationship(back_populates='user') + pedidos: Mapped[List['Detalles_pedido']] = relationship(back_populates='user') + notificaciones: Mapped[List['Notificaciones']] = relationship(back_populates='user') @@ -54,19 +55,17 @@ class Tienda(db.Model): categoria_principal: Mapped[str] = mapped_column(nullable=False , unique=False) telefono_comercial: Mapped[int] = mapped_column(unique=True , nullable=False) logo_url: Mapped[str] = mapped_column(String(300),unique=True , nullable=False) - primary_color: Mapped[str] = mapped_column(String(15), nullable=False , unique=False) - secondary_color: Mapped[str] = mapped_column(String(15), nullable=False , unique=False) - text_color: Mapped[str] = mapped_column(String(15), nullable=False , unique=False) + estilos: Mapped[Dict[str,Any]] = mapped_column(JSON , nullable=False , default=dict)#my sql colocar JSON y si es postgress JSONb redes_sociales: Mapped[str] = mapped_column(unique=False , nullable=False) fecha_creacion: Mapped[TIMESTAMP] = mapped_column(DateTime(), default=datetime.now(timezone.utc)) owner_id: Mapped[int] = mapped_column(ForeignKey('user.id')) owner: Mapped['User'] = relationship(back_populates='tiendas') - productos: Mapped[list['Productos']] = relationship(back_populates='tienda') + productos: Mapped[List['Productos']] = relationship(back_populates='tienda') favoritos: Mapped['Favoritos'] = relationship(back_populates='tienda') - historial_tienda: Mapped[list['Historial']] = relationship(back_populates='tienda') + historial_tienda: Mapped[List['Historial']] = relationship(back_populates='tienda') def serialize(self): @@ -78,9 +77,7 @@ def serialize(self): "categoria_principal": self.categoria_principal, "telefono_comercial": self.telefono_comercial, "logo_url": self.logo_url, - "primary_color": self.primary_color, - "secondary_color": self.secondary_color, - "text_color": self.text_color, + "estilos": self.estilos, "redes_sociales": self.redes_sociales, "fecha_creacion": self.fecha_creacion, } @@ -103,7 +100,7 @@ class Productos(db.Model): tienda_id: Mapped[int] = mapped_column(ForeignKey('tienda.id')) tienda: Mapped['Tienda'] = relationship(back_populates='productos', uselist=False) - resenas: Mapped[list['Resenas']] = relationship(back_populates='producto') + resenas: Mapped[List['Resenas']] = relationship(back_populates='producto') favoritos: Mapped['Favoritos'] = relationship(back_populates='producto') pedidos: Mapped['Detalles_pedido'] = relationship(back_populates="productos") @@ -162,10 +159,10 @@ class Detalles_pedido(db.Model): user: Mapped['User'] = relationship(back_populates="pedidos") producto_id: Mapped[int] = mapped_column(ForeignKey('productos.id'))#unir a producto - productos: Mapped[list['Productos']] = relationship(back_populates="pedidos") + productos: Mapped[List['Productos']] = relationship(back_populates="pedidos") subtotal: Mapped[float] = mapped_column(nullable=False , unique=False) - historial_pedido: Mapped[list['Historial']] = relationship(back_populates='pedido') + historial_pedido: Mapped[List['Historial']] = relationship(back_populates='pedido') def serialize(self): return{ @@ -224,7 +221,7 @@ def serialize(self): "comentario": self.comentario, "fecha": self.fecha, "respuestas": self.respuestas, - "producto": {"nombre_producto": self.producto.nombre_producto} if self.producto else None + "producto": {"nombre_producto": self.producto.nombre_producto} if self.producto else None #posible recursividad } class Historial(db.Model): diff --git a/src/api/routes.py b/src/api/routes.py index 029589a3a1..0759c25cdd 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -20,3 +20,5 @@ def handle_hello(): } return jsonify(response_body), 200 + + From 031e1bee0ff802c204e88238730237e98832cc76 Mon Sep 17 00:00:00 2001 From: Lucia Date: Fri, 24 Oct 2025 16:58:02 +0000 Subject: [PATCH 07/27] footer y header --- src/front/components/Footer.css | 93 ++++++++++++++++++++++ src/front/components/Footer.jsx | 69 +++++++++++++--- src/front/components/Header.css | 137 ++++++++++++++++++++++++++++++++ src/front/components/Header.jsx | 59 ++++++++++++++ src/front/pages/Home.jsx | 98 ++++++++++++----------- src/front/pages/Layout.jsx | 2 +- 6 files changed, 398 insertions(+), 60 deletions(-) create mode 100644 src/front/components/Footer.css create mode 100644 src/front/components/Header.css create mode 100644 src/front/components/Header.jsx diff --git a/src/front/components/Footer.css b/src/front/components/Footer.css new file mode 100644 index 0000000000..cd8ae150a8 --- /dev/null +++ b/src/front/components/Footer.css @@ -0,0 +1,93 @@ +.footer { + background-color: #ffe2e2; + color: #919191; + padding: 60px 0 40px; + margin-top: 40px; + font-family: Arial, sans-serif; + display: flex; + flex-direction: column; + align-items: center; +} + +.footer-container { + width: 100%; + max-width: 1100px; + margin: 0 auto; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 30px; + text-align: center; + padding: 0 20px; + box-sizing: border-box; +} + +.footer-section h2 { + color: #000000; + font-size: 18px; + margin-bottom: 15px; + border-bottom: 2px solid #bd1a1a; + display: inline-block; + padding-bottom: 5px; +} + +.footer-section ul { + list-style: none; + padding: 0; + margin: 0; +} + +.footer-section ul li { + margin-bottom: 8px; +} + +.footer-section ul li a { + color: #919191; + text-decoration: none; + transition: color 0.3s; +} + +.footer-section ul li a:hover { + color: #000000; +} + +.footer-section p { + margin: 5px 0; + font-size: 14px; +} + +.footer-brand { + margin-top: 50px; + text-align: center; + padding: 0 20px; +} + +.footer-brand img { + width: 120px; + height: auto; + border-radius: 8px; + transition: transform 0.3s ease; +} + +.footer-brand img:hover { + transform: scale(1.05); +} + +.footer-brand p { + margin-top: 15px; + font-size: 15px; + color: #4d4d4d; + max-width: 600px; + margin-left: auto; + margin-right: auto; + line-height: 1.5; +} + +.footer-bottom { + text-align: center; + font-size: 13px; + color: #4d4d4d; + border-top: 1px solid #bd1a1a; + padding-top: 15px; + margin-top: 40px; + width: 100%; +} diff --git a/src/front/components/Footer.jsx b/src/front/components/Footer.jsx index f06302dbd2..69a35ac062 100644 --- a/src/front/components/Footer.jsx +++ b/src/front/components/Footer.jsx @@ -1,11 +1,58 @@ -export const Footer = () => ( - -); +import React from "react"; +import "./Footer.css"; + +export default function Footer() { + return ( + + ); +} diff --git a/src/front/components/Header.css b/src/front/components/Header.css new file mode 100644 index 0000000000..957bff1605 --- /dev/null +++ b/src/front/components/Header.css @@ -0,0 +1,137 @@ +.hero { + background: linear-gradient(to right, #ffe2e2, #fbeeee); + padding: 60px 40px; + font-family: Arial, sans-serif; +} + +.hero-content { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 40px; + max-width: 1200px; + margin: 0 auto; +} + +.hero-text { + flex: 1; +} + +.hero-text h1 { + font-size: 48px; + color: #1e1e1e; + margin-bottom: 20px; +} + +.hero-text h1 span { + color: #bd1a1a; +} + +.hero-text p { + font-size: 18px; + color: #4a4a4a; + margin-bottom: 30px; + max-width: 550px; +} + +.hero-buttons { + display: flex; + gap: 20px; + margin-bottom: 40px; +} + +.btn { + display: inline-block; + padding: 12px 24px; + border-radius: 25px; + text-decoration: none; + font-weight: 600; + transition: background-color 0.3s ease, color 0.3s ease; +} + +.btn.primary { + background-color: #bd1a1a; + color: white; +} + +.btn.primary:hover { + background-color: #a11212; +} + +.btn.secondary { + background-color: transparent; + color: #bd1a1a; + border: 2px solid #bd1a1a; +} + +.btn.secondary:hover { + background-color: #bd1a1a; + color: white; +} + +.hero-stats { + display: flex; + gap: 40px; +} + +.hero-stats div h3 { + color: #bd1a1a; + font-size: 24px; +} + +.hero-stats div p { + color: #4a4a4a; + font-size: 14px; +} + +.hero-image { + flex: 1; + display: flex; + justify-content: center; + align-items: center; +} + +.checkout-wrapper { + position: relative; + display: flex; + flex-direction: column; + align-items: center; +} + +.checkout-bg { + width: 300px; + height: auto; + opacity: 0.30; + margin-bottom: -40px; +} + +.checkout-card { + background-color: white; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); + padding: 15px 25px; + border-radius: 15px; + display: flex; + align-items: center; + gap: 10px; + z-index: 2; + transform: translateY(-10px); +} + +.cart-icon { + font-size: 22px; + color: #bd1a1a; +} + +.checkout-card h4 { + margin: 0; + font-size: 16px; + font-weight: 600; + color: #1e1e1e; +} + +.checkout-card p { + margin: 0; + font-size: 13px; + color: #4a4a4a; +} diff --git a/src/front/components/Header.jsx b/src/front/components/Header.jsx new file mode 100644 index 0000000000..b09e656aa4 --- /dev/null +++ b/src/front/components/Header.jsx @@ -0,0 +1,59 @@ +import React from "react"; +import { Link } from "react-router-dom"; +import "./Header.css"; + +export const Header = () => { + return ( +
+
+
+

+ Shop Smart with Spidys +

+

+ Experience seamless online shopping with our curated collection. Browse products, manage your cart, and enjoy a personalized shopping experience. +

+ +
+ Browse Products ➝ + + Login to Account + +
+ +
+
+

500+

+

Products

+
+
+

24/7

+

Support

+
+
+

Fast

+

Delivery

+
+
+
+ +
+
+ Decorative background +
+ +
+

Easy Checkout

+

Secure & Fast

+
+
+
+
+
+
+ ); +}; diff --git a/src/front/pages/Home.jsx b/src/front/pages/Home.jsx index 341ed21768..2bf5a5e114 100644 --- a/src/front/pages/Home.jsx +++ b/src/front/pages/Home.jsx @@ -1,52 +1,54 @@ -import React, { useEffect } from "react" +import React, { useEffect } from "react"; import rigoImageUrl from "../assets/img/rigo-baby.jpg"; import useGlobalReducer from "../hooks/useGlobalReducer.jsx"; +import { Header } from "../components/Header"; // 👈 importa el Header export const Home = () => { - - 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 + 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)... + + )} +
+
+ + ); +}; diff --git a/src/front/pages/Layout.jsx b/src/front/pages/Layout.jsx index 9bfa31325c..ebfc1cffb7 100644 --- a/src/front/pages/Layout.jsx +++ b/src/front/pages/Layout.jsx @@ -1,7 +1,7 @@ import { Outlet } from "react-router-dom/dist" import ScrollToTop from "../components/ScrollToTop" import { Navbar } from "../components/Navbar" -import { Footer } from "../components/Footer" +import Footer from "../components/Footer" // Base component that maintains the navbar and footer throughout the page and the scroll to top functionality. export const Layout = () => { From 1b95de4be88bdb96a87f6be9cd473e7212d4b5cc Mon Sep 17 00:00:00 2001 From: 1v4nP4 <1v4nP4@proton.me> Date: Fri, 24 Oct 2025 16:59:33 +0000 Subject: [PATCH 08/27] Arreglo del bootstrap --- src/front/main.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/front/main.jsx b/src/front/main.jsx index 409393c505..d61386aa9d 100644 --- a/src/front/main.jsx +++ b/src/front/main.jsx @@ -1,5 +1,5 @@ import 'bootstrap/dist/css/bootstrap.min.css' -import 'bootstrap/dist/js/bootstrap.bundle.min.js'; +import 'bootstrap/dist/js/bootstrap.bundle.min.js' import React from 'react' import ReactDOM from 'react-dom/client' import './index.css' // Global styles for your application From 013e2f482f36706c767c152ba73a810668232f20 Mon Sep 17 00:00:00 2001 From: 1v4nP4 <1v4nP4@proton.me> Date: Fri, 24 Oct 2025 17:21:45 +0000 Subject: [PATCH 09/27] vino sergio --- src/front/main.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/front/main.jsx b/src/front/main.jsx index d61386aa9d..168d0e5f34 100644 --- a/src/front/main.jsx +++ b/src/front/main.jsx @@ -1,5 +1,4 @@ -import 'bootstrap/dist/css/bootstrap.min.css' -import 'bootstrap/dist/js/bootstrap.bundle.min.js' + import React from 'react' import ReactDOM from 'react-dom/client' import './index.css' // Global styles for your application From faab48d902f01d22013eedb9c6e408be74fc875f Mon Sep 17 00:00:00 2001 From: Hun73R717 Date: Fri, 24 Oct 2025 18:39:43 +0000 Subject: [PATCH 10/27] crear pag login --- src/front/components/Navbar.jsx | 4 ++- src/front/pages/Login.jsx | 62 +++++++++++++++++++++++++++++++++ src/front/routes.jsx | 2 ++ 3 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 src/front/pages/Login.jsx diff --git a/src/front/components/Navbar.jsx b/src/front/components/Navbar.jsx index c88c7bbd7f..c7718b19ff 100644 --- a/src/front/components/Navbar.jsx +++ b/src/front/components/Navbar.jsx @@ -9,6 +9,7 @@ export const Navbar = () => {
+ Logo { height="56" className="d-inline-block align-text-top me-2" /> +
Products @@ -32,7 +34,7 @@ export const Navbar = () => {
  • - Login + Login
  • diff --git a/src/front/pages/Login.jsx b/src/front/pages/Login.jsx new file mode 100644 index 0000000000..e95d3ab0b5 --- /dev/null +++ b/src/front/pages/Login.jsx @@ -0,0 +1,62 @@ +import React from "react"; + + + + +export default function LoginPage() { + return ( +
    +
    +

    Bienvenido

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

    + + + ¿Olvidaste la contraseña? + +

    + +
    + + + +

    + + +

    +
    +
    + ); +} diff --git a/src/front/routes.jsx b/src/front/routes.jsx index 0557df6141..4a263ae9c7 100644 --- a/src/front/routes.jsx +++ b/src/front/routes.jsx @@ -9,6 +9,7 @@ import { Layout } from "./pages/Layout"; import { Home } from "./pages/Home"; import { Single } from "./pages/Single"; import { Demo } from "./pages/Demo"; +import Login from "./pages/Login"; export const router = createBrowserRouter( createRoutesFromElements( @@ -25,6 +26,7 @@ export const router = createBrowserRouter( } /> } /> {/* Dynamic route for single items */} } /> + } /> ) ); \ No newline at end of file From 0be3e2f20e5d2fcff9783afcb48184d2c5934c1f Mon Sep 17 00:00:00 2001 From: 1v4nP4 <1v4nP4@proton.me> Date: Fri, 24 Oct 2025 18:44:28 +0000 Subject: [PATCH 11/27] Register endpoint hecho --- Pipfile | 1 + Pipfile.lock | 72 ++++++++++++++++++++++++++++++++++++++++++++++- src/api/routes.py | 15 ++++++++++ 3 files changed, 87 insertions(+), 1 deletion(-) 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..0098ce3dd6 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "d2e672e650278aeeee2fe49bd76d76497d8b65a50f8b5dbb121d265cbc6ef4e5" + "sha256": "dc64a1f0dd7551608933a91632e5eb1245b4768aa8ddf4f8f02467a6f71e4744" }, "pipfile-spec": 6, "requires": { @@ -24,6 +24,76 @@ "markers": "python_version >= '3.8'", "version": "==1.14.1" }, + "bcrypt": { + "hashes": [ + "sha256:046ad6db88edb3c5ece4369af997938fb1c19d6a699b9c1b27b0db432faae4c4", + "sha256:0c418ca99fd47e9c59a301744d63328f17798b5947b0f791e9af3c1c499c2d0a", + "sha256:0c8e093ea2532601a6f686edbc2c6b2ec24131ff5c52f7610dd64fa4553b5464", + "sha256:0cae4cb350934dfd74c020525eeae0a5f79257e8a201c0c176f4b84fdbf2a4b4", + "sha256:137c5156524328a24b9fac1cb5db0ba618bc97d11970b39184c1d87dc4bf1746", + "sha256:200af71bc25f22006f4069060c88ed36f8aa4ff7f53e67ff04d2ab3f1e79a5b2", + "sha256:212139484ab3207b1f0c00633d3be92fef3c5f0af17cad155679d03ff2ee1e41", + "sha256:2b732e7d388fa22d48920baa267ba5d97cca38070b69c0e2d37087b381c681fd", + "sha256:35a77ec55b541e5e583eb3436ffbbf53b0ffa1fa16ca6782279daf95d146dcd9", + "sha256:38cac74101777a6a7d3b3e3cfefa57089b5ada650dce2baf0cbdd9d65db22a9e", + "sha256:3abeb543874b2c0524ff40c57a4e14e5d3a66ff33fb423529c88f180fd756538", + "sha256:3ca8a166b1140436e058298a34d88032ab62f15aae1c598580333dc21d27ef10", + "sha256:3cf67a804fc66fc217e6914a5635000259fbbbb12e78a99488e4d5ba445a71eb", + "sha256:4870a52610537037adb382444fefd3706d96d663ac44cbb2f37e3919dca3d7ef", + "sha256:48f753100931605686f74e27a7b49238122aa761a9aefe9373265b8b7aa43ea4", + "sha256:4bfd2a34de661f34d0bda43c3e4e79df586e4716ef401fe31ea39d69d581ef23", + "sha256:560ddb6ec730386e7b3b26b8b4c88197aaed924430e7b74666a586ac997249ef", + "sha256:5b1589f4839a0899c146e8892efe320c0fa096568abd9b95593efac50a87cb75", + "sha256:5feebf85a9cefda32966d8171f5db7e3ba964b77fdfe31919622256f80f9cf42", + "sha256:611f0a17aa4a25a69362dcc299fda5c8a3d4f160e2abb3831041feb77393a14a", + "sha256:61afc381250c3182d9078551e3ac3a41da14154fbff647ddf52a769f588c4172", + "sha256:64d7ce196203e468c457c37ec22390f1a61c85c6f0b8160fd752940ccfb3a683", + "sha256:64ee8434b0da054d830fa8e89e1c8bf30061d539044a39524ff7dec90481e5c2", + "sha256:6b8f520b61e8781efee73cba14e3e8c9556ccfb375623f4f97429544734545b4", + "sha256:741449132f64b3524e95cd30e5cd3343006ce146088f074f31ab26b94e6c75ba", + "sha256:744d3c6b164caa658adcb72cb8cc9ad9b4b75c7db507ab4bc2480474a51989da", + "sha256:79cfa161eda8d2ddf29acad370356b47f02387153b11d46042e93a0a95127493", + "sha256:7aeef54b60ceddb6f30ee3db090351ecf0d40ec6e2abf41430997407a46d2254", + "sha256:7edda91d5ab52b15636d9c30da87d2cc84f426c72b9dba7a9b4fe142ba11f534", + "sha256:7f277a4b3390ab4bebe597800a90da0edae882c6196d3038a73adf446c4f969f", + "sha256:7f4c94dec1b5ab5d522750cb059bb9409ea8872d4494fd152b53cca99f1ddd8c", + "sha256:801cad5ccb6b87d1b430f183269b94c24f248dddbbc5c1f78b6ed231743e001c", + "sha256:83e787d7a84dbbfba6f250dd7a5efd689e935f03dd83b0f919d39349e1f23f83", + "sha256:89042e61b5e808b67daf24a434d89bab164d4de1746b37a8d173b6b14f3db9ff", + "sha256:92864f54fb48b4c718fc92a32825d0e42265a627f956bc0361fe869f1adc3e7d", + "sha256:9d52ed507c2488eddd6a95bccee4e808d3234fa78dd370e24bac65a21212b861", + "sha256:9fffdb387abe6aa775af36ef16f55e318dcda4194ddbf82007a6f21da29de8f5", + "sha256:a28bc05039bdf3289d757f49d616ab3efe8cf40d8e8001ccdd621cd4f98f4fc9", + "sha256:a5393eae5722bcef046a990b84dff02b954904c36a194f6cfc817d7dca6c6f0b", + "sha256:a71f70ee269671460b37a449f5ff26982a6f2ba493b3eabdd687b4bf35f875ac", + "sha256:b17366316c654e1ad0306a6858e189fc835eca39f7eb2cafd6aaca8ce0c40a2e", + "sha256:baade0a5657654c2984468efb7d6c110db87ea63ef5a4b54732e7e337253e44f", + "sha256:c2388ca94ffee269b6038d48747f4ce8df0ffbea43f31abfa18ac72f0218effb", + "sha256:c58b56cdfb03202b3bcc9fd8daee8e8e9b6d7e3163aa97c631dfcfcc24d36c86", + "sha256:cde08734f12c6a4e28dc6755cd11d3bdfea608d93d958fffbe95a7026ebe4980", + "sha256:d79e5c65dcc9af213594d6f7f1fa2c98ad3fc10431e7aa53c176b441943efbdd", + "sha256:d8d65b564ec849643d9f7ea05c6d9f0cd7ca23bdd4ac0c2dbef1104ab504543d", + "sha256:db99dca3b1fdc3db87d7c57eac0c82281242d1eabf19dcb8a6b10eb29a2e72d1", + "sha256:dcd58e2b3a908b5ecc9b9df2f0085592506ac2d5110786018ee5e160f28e0911", + "sha256:dd19cf5184a90c873009244586396a6a884d591a5323f0e8a5922560718d4993", + "sha256:ddb4e1500f6efdd402218ffe34d040a1196c072e07929b9820f363a1fd1f4191", + "sha256:e3cf5b2560c7b5a142286f69bde914494b6d8f901aaa71e453078388a50881c4", + "sha256:ed2e1365e31fc73f1825fa830f1c8f8917ca1b3ca6185773b349c20fd606cec2", + "sha256:edfcdcedd0d0f05850c52ba3127b1fce70b9f89e0fe5ff16517df7e81fa3cbb8", + "sha256:f0ce778135f60799d89c9693b9b398819d15f1921ba15fe719acb3178215a7db", + "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927", + "sha256:f3c08197f3039bec79cee59a606d62b96b16669cff3949f21e74796b6e3cd2be", + "sha256:f632fd56fc4e61564f78b46a2269153122db34988e78b6be8b32d28507b7eaeb", + "sha256:f6984a24db30548fd39a44360532898c33528b74aedf81c26cf29c51ee47057e", + "sha256:f70aadb7a809305226daedf75d90379c397b094755a710d7014b8b117df1ebbf", + "sha256:f748f7c2d6fd375cc93d3fba7ef4a9e3a092421b8dbf34d8d4dc06be9492dfdd", + "sha256:f8429e1c410b4073944f03bd778a9e066e7fad723564a52ff91841d278dfc822", + "sha256:fc746432b951e92b58317af8e0ca746efe93e66555f1b40888865ef5bf56446b" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==5.0.0" + }, "blinker": { "hashes": [ "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", diff --git a/src/api/routes.py b/src/api/routes.py index 0759c25cdd..e973ae2282 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -5,6 +5,7 @@ from api.models import db, User from api.utils import generate_sitemap, APIException from flask_cors import CORS +from bcrypt import bcrypt api = Blueprint('api', __name__) @@ -22,3 +23,17 @@ def handle_hello(): return jsonify(response_body), 200 +@api.route('/register', method=['POST']) +def handle_register(): + + body = request.get_json() + + hashed_password = bcrypt.generate_password_hash( + body['password']).decode('utf-8') + + new_user = User(role=body['role'], nickname=body['nickname'], nombre=body['nombre'], apellido=body['apellido'], fecha_nacimiento=body['fecha_nacimiento'], email=body['email'], + address=body['address'], telefono=body['telefono'], password=body[hashed_password], registro_fecha=body['registro_fecha'], is_active=body['is_active']) + + db.session.add(new_user) + db.session.commit() + return jsonify({'msg': 'Nuevo usuario creado con exito'}), 201 From 3cf33638d6c5ff87199cee53f1afa224b4278950 Mon Sep 17 00:00:00 2001 From: 1v4nP4 <1v4nP4@proton.me> Date: Fri, 24 Oct 2025 18:52:05 +0000 Subject: [PATCH 12/27] registro empezado --- src/front/components/Register.jsx | 4 ++++ src/front/pages/RegisterPage.jsx | 1 + 2 files changed, 5 insertions(+) create mode 100644 src/front/components/Register.jsx create mode 100644 src/front/pages/RegisterPage.jsx diff --git a/src/front/components/Register.jsx b/src/front/components/Register.jsx new file mode 100644 index 0000000000..9036ce799e --- /dev/null +++ b/src/front/components/Register.jsx @@ -0,0 +1,4 @@ +import React from "react"; +import { Link } from "react-router-dom"; + + diff --git a/src/front/pages/RegisterPage.jsx b/src/front/pages/RegisterPage.jsx new file mode 100644 index 0000000000..4eee36c687 --- /dev/null +++ b/src/front/pages/RegisterPage.jsx @@ -0,0 +1 @@ +import React from "react"; \ No newline at end of file From dcddb1b918cd2a3680ca660ec75c1b2ee737eb1b Mon Sep 17 00:00:00 2001 From: Hun73R717 Date: Fri, 24 Oct 2025 19:19:37 +0000 Subject: [PATCH 13/27] login cambio elemnto --- src/front/{pages => components}/Login.jsx | 10 +++++----- src/front/components/Navbar.jsx | 2 +- src/front/pages/LoginPage.jsx | 12 ++++++++++++ src/front/routes.jsx | 4 ++-- 4 files changed, 20 insertions(+), 8 deletions(-) rename src/front/{pages => components}/Login.jsx (95%) create mode 100644 src/front/pages/LoginPage.jsx diff --git a/src/front/pages/Login.jsx b/src/front/components/Login.jsx similarity index 95% rename from src/front/pages/Login.jsx rename to src/front/components/Login.jsx index e95d3ab0b5..ff4232e4a3 100644 --- a/src/front/pages/Login.jsx +++ b/src/front/components/Login.jsx @@ -1,9 +1,7 @@ import React from "react"; +import { Link } from "react-router-dom"; - - - -export default function LoginPage() { +const Login = () => { return (
    @@ -58,5 +56,7 @@ export default function LoginPage() {

    - ); + + ) } +export default Login \ No newline at end of file diff --git a/src/front/components/Navbar.jsx b/src/front/components/Navbar.jsx index c7718b19ff..cfbcfecbf4 100644 --- a/src/front/components/Navbar.jsx +++ b/src/front/components/Navbar.jsx @@ -34,7 +34,7 @@ export const Navbar = () => {
  • - Login + Login
  • diff --git a/src/front/pages/LoginPage.jsx b/src/front/pages/LoginPage.jsx new file mode 100644 index 0000000000..210e9234c0 --- /dev/null +++ b/src/front/pages/LoginPage.jsx @@ -0,0 +1,12 @@ +import React from "react"; +import login from "../components/Login"; + + + + +const LoginPage = () => { + return( + + ) +} +export default LoginPage \ No newline at end of file diff --git a/src/front/routes.jsx b/src/front/routes.jsx index 4a263ae9c7..cdcbca8724 100644 --- a/src/front/routes.jsx +++ b/src/front/routes.jsx @@ -9,7 +9,7 @@ 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 LoginPage from "./pages/LoginPage"; export const router = createBrowserRouter( createRoutesFromElements( @@ -26,7 +26,7 @@ export const router = createBrowserRouter( } /> } /> {/* Dynamic route for single items */} } /> - } /> + } /> ) ); \ No newline at end of file From 2fb9340671ab86f50f9c1eae40851415e806f131 Mon Sep 17 00:00:00 2001 From: Hun73R717 Date: Mon, 27 Oct 2025 14:22:25 +0000 Subject: [PATCH 14/27] correcion de llamado del elemento cuando se le da click al boton --- src/front/components/Login.jsx | 4 +++- src/front/pages/LoginPage.jsx | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/front/components/Login.jsx b/src/front/components/Login.jsx index ff4232e4a3..8f01cb7a12 100644 --- a/src/front/components/Login.jsx +++ b/src/front/components/Login.jsx @@ -18,6 +18,7 @@ const Login = () => { id="email" placeholder="tuemail@ejemplo.com" required + style={{ borderColor: '#a00' }} /> @@ -31,6 +32,7 @@ const Login = () => { id="password" placeholder="********" required + style={{ borderColor: '#a00' }} /> @@ -50,7 +52,7 @@ const Login = () => {

    -

    diff --git a/src/front/pages/LoginPage.jsx b/src/front/pages/LoginPage.jsx index 210e9234c0..ee4121271c 100644 --- a/src/front/pages/LoginPage.jsx +++ b/src/front/pages/LoginPage.jsx @@ -1,12 +1,12 @@ import React from "react"; -import login from "../components/Login"; +import Login from "../components/Login"; const LoginPage = () => { return( - + ) } export default LoginPage \ No newline at end of file From aef890cf76056f0d3a047b7ddd1f29fa39526bde Mon Sep 17 00:00:00 2001 From: 1v4nP4 <1v4nP4@proton.me> Date: Mon, 27 Oct 2025 17:58:49 +0000 Subject: [PATCH 15/27] avanzado el register --- src/front/components/Register.css | 3 +++ src/front/components/Register.jsx | 12 ++++++++++++ src/front/pages/RegisterPage.jsx | 11 ++++++++++- src/front/routes.jsx | 4 +++- 4 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 src/front/components/Register.css diff --git a/src/front/components/Register.css b/src/front/components/Register.css new file mode 100644 index 0000000000..59e845316c --- /dev/null +++ b/src/front/components/Register.css @@ -0,0 +1,3 @@ +body{ + background-color: white; +} diff --git a/src/front/components/Register.jsx b/src/front/components/Register.jsx index 9036ce799e..6446bb5cfc 100644 --- a/src/front/components/Register.jsx +++ b/src/front/components/Register.jsx @@ -1,4 +1,16 @@ import React from "react"; import { Link } from "react-router-dom"; +import "./Register.css"; +export const Register = () => { + return( + <> + +
    + +
    + + + ); +} \ No newline at end of file diff --git a/src/front/pages/RegisterPage.jsx b/src/front/pages/RegisterPage.jsx index 4eee36c687..245ef31c04 100644 --- a/src/front/pages/RegisterPage.jsx +++ b/src/front/pages/RegisterPage.jsx @@ -1 +1,10 @@ -import React from "react"; \ No newline at end of file +import React from "react"; +import { Register } from "../components/Register"; + +export const RegisterPage = () => { + + return( + + ) + +} \ No newline at end of file diff --git a/src/front/routes.jsx b/src/front/routes.jsx index 4a263ae9c7..a39787d8ab 100644 --- a/src/front/routes.jsx +++ b/src/front/routes.jsx @@ -9,7 +9,8 @@ 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 { Login } from "./pages/Login"; +import { RegisterPage } from "./pages/RegisterPage"; export const router = createBrowserRouter( createRoutesFromElements( @@ -27,6 +28,7 @@ export const router = createBrowserRouter( } /> {/* Dynamic route for single items */} } /> } /> + }/> ) ); \ No newline at end of file From bf78875b5307971e02296f069e120e1b258a8dd3 Mon Sep 17 00:00:00 2001 From: Lucia Date: Mon, 27 Oct 2025 17:59:37 +0000 Subject: [PATCH 16/27] Update HeroSection component --- src/front/components/HeroSection.css | 83 ++++++++++++++++++++++++++++ src/front/components/HeroSection.jsx | 50 +++++++++++++++++ src/front/main.jsx | 3 + src/front/pages/Home.jsx | 12 ++-- 4 files changed, 142 insertions(+), 6 deletions(-) create mode 100644 src/front/components/HeroSection.css create mode 100644 src/front/components/HeroSection.jsx diff --git a/src/front/components/HeroSection.css b/src/front/components/HeroSection.css new file mode 100644 index 0000000000..5b0c5ba409 --- /dev/null +++ b/src/front/components/HeroSection.css @@ -0,0 +1,83 @@ +.features { + background: linear-gradient(to right, #ffe2e2, #fbeeee); + padding: 40px 20px 50px 20px; /* menos margen arriba y abajo */ + font-family: Arial, sans-serif; + text-align: center; +} + +.features-header h2 { + font-size: 34px; + color: #1e1e1e; + margin-bottom: 6px; +} + +.features-header p { + color: #4a4a4a; + font-size: 16px; + max-width: 600px; + margin: 0 auto 30px auto; +} + +.features-row { + display: flex; + justify-content: center; + align-items: flex-start; + gap: 20px; /* menos espacio entre tarjetas */ + flex-wrap: nowrap; +} + +.feature-card { + background-color: white; + border-radius: 14px; + padding: 25px 18px; + width: 230px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08); + transition: transform 0.3s ease, box-shadow 0.3s ease; + flex-shrink: 0; +} + +.feature-card:hover { + transform: translateY(-5px); + box-shadow: 0 8px 18px rgba(0, 0, 0, 0.15); +} + +.feature-icon { + font-size: 28px; + width: 55px; + height: 55px; + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + color: white; + margin: 0 auto 15px auto; +} + +.feature-icon.red { + background-color: #bd1a1a; +} + +.feature-icon.coral { + background-color: #e65a5a; +} + +.feature-icon.yellow { + background-color: #f9b233; +} + +.feature-icon.blue { + background-color: #4a90e2; +} + +.feature-card h3 { + font-size: 18px; + color: #1e1e1e; + margin-bottom: 8px; +} + +.feature-card p { + color: #4a4a4a; + font-size: 14px; + line-height: 1.5; +} + diff --git a/src/front/components/HeroSection.jsx b/src/front/components/HeroSection.jsx new file mode 100644 index 0000000000..3a1188fcbd --- /dev/null +++ b/src/front/components/HeroSection.jsx @@ -0,0 +1,50 @@ +import React from "react"; +import "./HeroSection.css"; + +export const HeroSection = () => { + return ( +
    +
    +

    Why Choose Spidys?

    +

    + We provide a seamless shopping experience with powerful features designed for your convenience. +

    +
    + +
    +
    +
    + +
    +

    Wide Selection

    +

    Browse through hundreds of quality products across multiple categories.

    +
    + +
    +
    + +
    +

    Smart Cart

    +

    Manage your purchases easily with our intuitive shopping cart system.

    +
    + +
    +
    + +
    +

    QR Generator

    +

    Create custom QR codes for your shop to enhance customer engagement.

    +
    + +
    +
    + +
    +

    Secure Login

    +

    Access your personalized account with our secure authentication system.

    +
    +
    +
    + ); +}; + diff --git a/src/front/main.jsx b/src/front/main.jsx index 168d0e5f34..ffc5e48bd1 100644 --- a/src/front/main.jsx +++ b/src/front/main.jsx @@ -1,4 +1,7 @@ +<<<<<<< Updated upstream +======= +>>>>>>> Stashed changes 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 2bf5a5e114..a1bacc4b34 100644 --- a/src/front/pages/Home.jsx +++ b/src/front/pages/Home.jsx @@ -1,7 +1,8 @@ import React, { useEffect } from "react"; import rigoImageUrl from "../assets/img/rigo-baby.jpg"; import useGlobalReducer from "../hooks/useGlobalReducer.jsx"; -import { Header } from "../components/Header"; // 👈 importa el Header +import { Header } from "../components/Header"; +import { HeroSection } from "../components/HeroSection"; export const Home = () => { const { store, dispatch } = useGlobalReducer(); @@ -9,14 +10,10 @@ export const Home = () => { 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) @@ -33,7 +30,8 @@ export const Home = () => { return ( <> -
    +
    +

    Hello Rigo!!

    @@ -52,3 +50,5 @@ export const Home = () => { ); }; + + From 1aebfc248f1941d07cc1555d2fb0278b06aef7e4 Mon Sep 17 00:00:00 2001 From: 1v4nP4 <1v4nP4@proton.me> Date: Mon, 27 Oct 2025 18:34:07 +0000 Subject: [PATCH 17/27] arreglo de conflictos y password --- src/front/components/Login.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/front/components/Login.jsx b/src/front/components/Login.jsx index 8f01cb7a12..0353e8f1f0 100644 --- a/src/front/components/Login.jsx +++ b/src/front/components/Login.jsx @@ -30,7 +30,7 @@ const Login = () => { type="password" className="form-control" id="password" - placeholder="********" + placeholder="contraseña" required style={{ borderColor: '#a00' }} /> From 07b807a3511acf4e7d60875572a02447157c4bff Mon Sep 17 00:00:00 2001 From: 1v4nP4 <1v4nP4@proton.me> Date: Mon, 27 Oct 2025 18:43:28 +0000 Subject: [PATCH 18/27] ya funciona el login --- src/front/components/Register.jsx | 60 ++++++++++++++++++++++++++----- src/front/pages/LoginPage.jsx | 3 +- 2 files changed, 53 insertions(+), 10 deletions(-) diff --git a/src/front/components/Register.jsx b/src/front/components/Register.jsx index 6446bb5cfc..ea26d2c4a5 100644 --- a/src/front/components/Register.jsx +++ b/src/front/components/Register.jsx @@ -3,14 +3,58 @@ import { Link } from "react-router-dom"; import "./Register.css"; export const Register = () => { + return ( + <> +

    +
    +

    Bienvenido

    - return( - <> - -
    +
    +
    + + +
    -
    +
    + + +
    - - ); -} \ No newline at end of file + +

    + + ¿Olvidaste la contraseña? + +

    + + +

    + +

    +
    +
    + + ); +}; diff --git a/src/front/pages/LoginPage.jsx b/src/front/pages/LoginPage.jsx index ee4121271c..3dea35b191 100644 --- a/src/front/pages/LoginPage.jsx +++ b/src/front/pages/LoginPage.jsx @@ -4,9 +4,8 @@ import Login from "../components/Login"; -const LoginPage = () => { +export const LoginPage = () => { return( ) } -export default LoginPage \ No newline at end of file From d11adef2cc4864a76795ab6deb4e17df150ce2bf Mon Sep 17 00:00:00 2001 From: 1v4nP4 <1v4nP4@proton.me> Date: Mon, 27 Oct 2025 19:12:17 +0000 Subject: [PATCH 19/27] registro empezado --- src/front/components/Register.jsx | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/front/components/Register.jsx b/src/front/components/Register.jsx index ea26d2c4a5..8baa6d4578 100644 --- a/src/front/components/Register.jsx +++ b/src/front/components/Register.jsx @@ -6,13 +6,13 @@ export const Register = () => { return ( <>
    -
    -

    Bienvenido

    +
    +

    Crear Cuenta

    { style={{ borderColor: "#a00" }} />
    + -

    - - ¿Olvidaste la contraseña? - -

    +
    -

    - -

    +
    From c535bc6e1fde5d17c4db93c2db682e7c80e97df7 Mon Sep 17 00:00:00 2001 From: Lucia Date: Mon, 27 Oct 2025 19:20:40 +0000 Subject: [PATCH 20/27] Features HS --- src/front/components/Features1.css | 101 +++++++++++++++++++++++++++ src/front/components/Features1.jsx | 52 ++++++++++++++ src/front/components/HeroSection.css | 2 +- src/front/main.jsx | 44 +++++------- src/front/pages/Home.jsx | 3 +- 5 files changed, 175 insertions(+), 27 deletions(-) create mode 100644 src/front/components/Features1.css create mode 100644 src/front/components/Features1.jsx diff --git a/src/front/components/Features1.css b/src/front/components/Features1.css new file mode 100644 index 0000000000..247c3b0b68 --- /dev/null +++ b/src/front/components/Features1.css @@ -0,0 +1,101 @@ +.features1 { + background: linear-gradient(to right, #fbe3e3, #f8dada); + padding: 60px 20px; + font-family: Arial, sans-serif; +} + +.features1-container { + display: flex; + align-items: center; + justify-content: center; + max-width: 1100px; + margin: 0 auto; + gap: 50px; + flex-wrap: wrap; +} + +.features1-images { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 15px; + width: 400px; +} + +.features1-images img { + width: 100%; + border-radius: 12px; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); + object-fit: cover; + background-color: white; +} + +.features1-content { + max-width: 450px; +} + +.features1-content h2 { + font-size: 30px; + color: #1e1e1e; + margin-bottom: 12px; +} + +.features1-content p { + color: #4a4a4a; + font-size: 16px; + line-height: 1.5; + margin-bottom: 20px; +} + +.features1-content ul { + list-style: none; + padding: 0; + margin: 0 0 25px 0; +} + +.features1-content li { + display: flex; + align-items: flex-start; + gap: 12px; + margin-bottom: 16px; +} + +.features1-content .number { + background-color: #d63c3c; + color: white; + font-weight: bold; + font-size: 15px; + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + flex-shrink: 0; +} + +.features1-content h4 { + font-size: 17px; + margin: 0 0 4px 0; + color: #1e1e1e; +} + +.features1-content li p { + margin: 0; + font-size: 14px; + color: #4a4a4a; +} + +.features1-button { + background-color: #d63c3c; + color: white; + font-size: 15px; + padding: 10px 20px; + border: none; + border-radius: 8px; + cursor: pointer; + transition: background 0.3s ease; +} + +.features1-button:hover { + background-color: #a91d1d; +} diff --git a/src/front/components/Features1.jsx b/src/front/components/Features1.jsx new file mode 100644 index 0000000000..ecc38dcb25 --- /dev/null +++ b/src/front/components/Features1.jsx @@ -0,0 +1,52 @@ +import React from "react"; +import "./Features1.css"; + +export const Features1 = () => { + return ( +
    +
    +
    + Shopping icon 1 + Shopping icon 2 + Shopping icon 3 + Shopping list concept +
    + +
    +

    Your Complete Shopping Experience

    +

    + From browsing to checkout, we’ve streamlined every step of your shopping journey to make it effortless and enjoyable. +

    + +
      +
    • + 1 +
      +

      Browse Products

      +

      Explore our curated collection and find exactly what you need.

      +
      +
    • + +
    • + 2 +
      +

      Add to Cart

      +

      Easily manage your selections with our smart shopping cart.

      +
      +
    • + +
    • + 3 +
      +

      Secure Checkout

      +

      Complete your purchase with confidence using our secure system.

      +
      +
    • +
    + + +
    +
    +
    + ); +}; diff --git a/src/front/components/HeroSection.css b/src/front/components/HeroSection.css index 5b0c5ba409..b563dfd819 100644 --- a/src/front/components/HeroSection.css +++ b/src/front/components/HeroSection.css @@ -1,5 +1,5 @@ .features { - background: linear-gradient(to right, #ffe2e2, #fbeeee); + background: linear-gradient(to right, #ffffff, #fbeeee); padding: 40px 20px 50px 20px; /* menos margen arriba y abajo */ font-family: Arial, sans-serif; text-align: center; diff --git a/src/front/main.jsx b/src/front/main.jsx index ffc5e48bd1..e86de40340 100644 --- a/src/front/main.jsx +++ b/src/front/main.jsx @@ -1,33 +1,27 @@ -<<<<<<< Updated upstream - -======= ->>>>>>> Stashed changes import React from 'react' import ReactDOM from 'react-dom/client' -import './index.css' // Global styles for your application -import { RouterProvider } from "react-router-dom"; // Import RouterProvider to use the router -import { router } from "./routes"; // Import the router configuration -import { StoreProvider } from './hooks/useGlobalReducer'; // Import the StoreProvider for global state management -import { BackendURL } from './components/BackendURL'; +import './index.css' +import { RouterProvider } from "react-router-dom" +import { router } from "./routes" +import { StoreProvider } from './hooks/useGlobalReducer' +import { BackendURL } from './components/BackendURL' const Main = () => { - - if(! import.meta.env.VITE_BACKEND_URL || import.meta.env.VITE_BACKEND_URL == "") return ( - - - - ); + if (!import.meta.env.VITE_BACKEND_URL || import.meta.env.VITE_BACKEND_URL == "") return ( - - {/* Provide global state to all components */} - - {/* Set up routing for the application */} - - - - - ); + + + + ) + + return ( + + + + + + ) } -// Render the Main component into the root DOM element. ReactDOM.createRoot(document.getElementById('root')).render(
    ) + diff --git a/src/front/pages/Home.jsx b/src/front/pages/Home.jsx index a1bacc4b34..68670e7f9b 100644 --- a/src/front/pages/Home.jsx +++ b/src/front/pages/Home.jsx @@ -3,6 +3,7 @@ import rigoImageUrl from "../assets/img/rigo-baby.jpg"; import useGlobalReducer from "../hooks/useGlobalReducer.jsx"; import { Header } from "../components/Header"; import { HeroSection } from "../components/HeroSection"; +import { Features1 } from "../components/Features1"; export const Home = () => { const { store, dispatch } = useGlobalReducer(); @@ -32,6 +33,7 @@ export const Home = () => { <>
    +

    Hello Rigo!!

    @@ -51,4 +53,3 @@ export const Home = () => { ); }; - From 47027580631e66c7b6c2ef55520e10c86357955b Mon Sep 17 00:00:00 2001 From: 1v4nP4 <1v4nP4@proton.me> Date: Thu, 30 Oct 2025 13:49:38 +0000 Subject: [PATCH 21/27] endpoint login creado --- Pipfile | 3 +- Pipfile.lock | 183 +++++++++++++++++++------------- src/api/routes.py | 44 +++++++- src/front/components/Navbar.jsx | 2 +- src/front/routes.jsx | 2 +- 5 files changed, 152 insertions(+), 82 deletions(-) diff --git a/Pipfile b/Pipfile index 921f9d2b89..cccb30eeeb 100644 --- a/Pipfile +++ b/Pipfile @@ -17,10 +17,11 @@ gunicorn = "*" cloudinary = "*" flask-admin = "*" typing-extensions = "*" -flask-jwt-extended = "==4.6.0" wtforms = "==3.1.2" sqlalchemy = "*" bcrypt = "*" +flask-jwt-extended = "*" +werkzeug = "*" [requires] python_version = "3.13" diff --git a/Pipfile.lock b/Pipfile.lock index 0098ce3dd6..51a921bf5c 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "dc64a1f0dd7551608933a91632e5eb1245b4768aa8ddf4f8f02467a6f71e4744" + "sha256": "9ecadd12d36f704d67f6ce68bd2f55b4617c0c4b754aeffac862c088ff91e5a1" }, "pipfile-spec": 6, "requires": { @@ -112,11 +112,11 @@ }, "click": { "hashes": [ - "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", - "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a" + "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", + "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4" ], - "markers": "python_version >= '3.7'", - "version": "==8.1.8" + "markers": "python_version >= '3.10'", + "version": "==8.3.0" }, "cloudinary": { "hashes": [ @@ -128,11 +128,12 @@ }, "flask": { "hashes": [ - "sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac", - "sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136" + "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", + "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c" ], "index": "pypi", - "version": "==3.1.0" + "markers": "python_version >= '3.9'", + "version": "==3.1.2" }, "flask-admin": { "hashes": [ @@ -152,11 +153,12 @@ }, "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": [ @@ -279,11 +281,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": [ @@ -295,70 +297,98 @@ }, "markupsafe": { "hashes": [ - "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", - "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", - "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", - "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", - "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", - "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", - "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", - "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", - "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", - "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", - "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", - "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", - "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", - "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", - "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", - "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", - "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", - "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", - "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", - "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", - "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", - "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", - "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", - "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", - "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", - "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", - "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", - "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", - "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", - "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", - "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", - "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", - "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", - "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", - "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", - "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", - "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", - "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", - "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", - "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", - "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", - "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", - "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", - "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", - "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", - "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", - "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", - "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", - "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", - "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", - "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", - "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", - "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", - "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", - "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", - "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", - "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", - "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", - "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", - "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", - "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50" + "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", + "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", + "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", + "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", + "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", + "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c", + "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", + "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", + "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", + "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", + "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", + "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26", + "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", + "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", + "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", + "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", + "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", + "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", + "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", + "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", + "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", + "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", + "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", + "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", + "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", + "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758", + "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", + "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8", + "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", + "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", + "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", + "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", + "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", + "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", + "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", + "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", + "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2", + "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", + "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", + "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", + "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", + "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", + "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", + "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", + "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", + "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e", + "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", + "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", + "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", + "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", + "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", + "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", + "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b", + "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", + "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", + "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", + "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d", + "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", + "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", + "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", + "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", + "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", + "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", + "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", + "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", + "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", + "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", + "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", + "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", + "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", + "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", + "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", + "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", + "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7", + "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", + "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", + "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", + "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", + "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", + "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42", + "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", + "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", + "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", + "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", + "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", + "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", + "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc", + "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", + "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50" ], "markers": "python_version >= '3.9'", - "version": "==3.0.2" + "version": "==3.0.3" }, "packaging": { "hashes": [ @@ -609,6 +639,7 @@ "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746" ], + "index": "pypi", "markers": "python_version >= '3.9'", "version": "==3.1.3" }, diff --git a/src/api/routes.py b/src/api/routes.py index e973ae2282..b353418e87 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -5,7 +5,9 @@ from api.models import db, User from api.utils import generate_sitemap, APIException from flask_cors import CORS -from bcrypt import bcrypt +from werkzeug.security import generate_password_hash, check_password_hash +from flask_jwt_extended import create_access_token, get_jwt_identity, jwt_required +from sqlalchemy import select api = Blueprint('api', __name__) @@ -28,8 +30,8 @@ def handle_register(): body = request.get_json() - hashed_password = bcrypt.generate_password_hash( - body['password']).decode('utf-8') + hashed_password = generate_password_hash( + body['password']) new_user = User(role=body['role'], nickname=body['nickname'], nombre=body['nombre'], apellido=body['apellido'], fecha_nacimiento=body['fecha_nacimiento'], email=body['email'], address=body['address'], telefono=body['telefono'], password=body[hashed_password], registro_fecha=body['registro_fecha'], is_active=body['is_active']) @@ -37,3 +39,39 @@ def handle_register(): db.session.add(new_user) db.session.commit() return jsonify({'msg': 'Nuevo usuario creado con exito'}), 201 + +@api.route('/update', methods=['PUT']) +@jwt_required() +def handle_update(): + + id=get_jwt_identity() + body = request.get_json() + hashed_password = generate_password_hash(body['password']).decode('utf-8') + + user=db.session.get(User,id) + user.role=body.get('role', user.role) + user.nickname=body['nickname'] + user.nombre=body['nombre'] + user.apellido=body['apellido'] + user.fecha_nacimiento=body['fecha_nacimiento'] + user.email=body['email'] + user.address=body['address'] + user.telefono=body['telefono'] + user.password=body[hashed_password] + db.session.commit() + return jsonify({'user':user.serialize()}) + +@api.route('/login', methods=['POST']) +def handle_login(): + body= request.json + stm= select(User).where(User.email==body['email']) + user=db.session.execute(stm).scalar_one_or_none() + + if not user : + return jsonify({'msg':'email no encontrado'}),404 + + if not check_password_hash(user.password , body['password']): + return jsonify({'msg':'email y/o contraseña no valido'}),400 + + token=create_access_token(identity=str(user.id)) + return jsonify({'user':user.serialize(),'token':token}),200 \ No newline at end of file diff --git a/src/front/components/Navbar.jsx b/src/front/components/Navbar.jsx index cfbcfecbf4..cb435efe09 100644 --- a/src/front/components/Navbar.jsx +++ b/src/front/components/Navbar.jsx @@ -34,7 +34,7 @@ export const Navbar = () => {

  • - Login + Login
  • diff --git a/src/front/routes.jsx b/src/front/routes.jsx index 348f53022e..9abb102542 100644 --- a/src/front/routes.jsx +++ b/src/front/routes.jsx @@ -27,7 +27,7 @@ export const router = createBrowserRouter( } /> } /> {/* Dynamic route for single items */} } /> - } /> + } /> }/> ) From 75343cf0860e7726763a8da0430502e1898db506 Mon Sep 17 00:00:00 2001 From: 1v4nP4 <1v4nP4@proton.me> Date: Thu, 30 Oct 2025 16:26:34 +0000 Subject: [PATCH 22/27] fix login --- src/api/models.py | 255 ++++++++++++++++----------- src/api/routes.py | 34 ++-- src/front/components/Login.jsx | 32 +++- src/front/services/auth.services.jsx | 56 ++++++ src/front/services/user.services.jsx | 28 +++ src/front/store.js | 13 +- 6 files changed, 294 insertions(+), 124 deletions(-) create mode 100644 src/front/services/auth.services.jsx create mode 100644 src/front/services/user.services.jsx diff --git a/src/api/models.py b/src/api/models.py index 6d5d945922..29f9a38cf8 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -1,36 +1,40 @@ from flask_sqlalchemy import SQLAlchemy -from sqlalchemy import String, Boolean, DateTime , TIMESTAMP , Integer , Date , DECIMAL , ForeignKey, Text, Float , JSON -from sqlalchemy.orm import Mapped, mapped_column , relationship -from datetime import datetime , timezone -from typing import List , Dict , Any +from sqlalchemy import String, Boolean, DateTime, TIMESTAMP, Integer, Date, DECIMAL, ForeignKey, Text, Float, JSON +from sqlalchemy.orm import Mapped, mapped_column, relationship +from datetime import datetime, timezone +from typing import List, Dict, Any db = SQLAlchemy() + + class User(db.Model): __tablename__ = 'user' id: Mapped[int] = mapped_column(primary_key=True) role: Mapped[bool] = mapped_column(Boolean()) - nickname: Mapped[str] = mapped_column(unique=True, nullable=False) - nombre: Mapped[str] = mapped_column(nullable=False) - apellido: Mapped[str] = mapped_column(nullable=False) - fecha_nacimiento: Mapped[datetime] = mapped_column(nullable=False) - email: Mapped[str] = mapped_column(String(120), unique=True, nullable=False) - address: Mapped[str] = mapped_column(nullable=False) - telefono: Mapped[int] = mapped_column(unique=True, nullable=False) + nickname: Mapped[str] = mapped_column(unique=True, nullable=True) + nombre: Mapped[str] = mapped_column(nullable=True) + apellido: Mapped[str] = mapped_column(nullable=True) + fecha_nacimiento: Mapped[datetime] = mapped_column(nullable=True) + email: Mapped[str] = mapped_column( + String(120), unique=True, nullable=False) + address: Mapped[str] = mapped_column(nullable=True) + telefono: Mapped[int] = mapped_column(unique=True, nullable=True) password: Mapped[str] = mapped_column(nullable=False) - registro_fecha: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)) - is_active: Mapped[bool] = mapped_column(Boolean(), nullable=False) + registro_fecha: Mapped[datetime] = mapped_column( + DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)) + is_active: Mapped[bool] = mapped_column(Boolean(), default=True) tiendas: Mapped[List['Tienda']] = relationship(back_populates='owner') resenas: Mapped[List['Resenas']] = relationship(back_populates='autor') favoritos: Mapped[List['Favoritos']] = relationship(back_populates='user') - pedidos: Mapped[List['Detalles_pedido']] = relationship(back_populates='user') - notificaciones: Mapped[List['Notificaciones']] = relationship(back_populates='user') - - + pedidos: Mapped[List['Detalles_pedido'] + ] = relationship(back_populates='user') + notificaciones: Mapped[List['Notificaciones'] + ] = relationship(back_populates='user') def serialize(self): return { - "id": self.id,#id debe conectarse con user_id en class tienda + "id": self.id, # id debe conectarse con user_id en class tienda "role": self.role, "nickname": self.nickname, "nombre": self.nombre, @@ -41,37 +45,46 @@ def serialize(self): "registro_fecha": self.registro_fecha, "email": self.email, "pedidos": [p.serialize() for p in self.pedidos] if self.pedidos else None, - "resenas": [r.serialize for r in self.resenas] if self.resenas else None, + "resenas": [r.serialize() for r in self.resenas] if self.resenas else None, "favoritos": [f.serialize() for f in self.favoritos] if self.favoritos else None, "notificaciones": [n.serialize() for n in self.notificaciones] if self.notificaciones else None, # do not serialize the password, its a security breach } + class Tienda(db.Model): - __tablename__="tienda" + __tablename__ = "tienda" id: Mapped[int] = mapped_column(primary_key=True) - nombre_tienda: Mapped[str] = mapped_column(unique=False , nullable=False) - descripcion_tienda: Mapped[str] = mapped_column(String(300) , nullable=False , unique=False) - categoria_principal: Mapped[str] = mapped_column(nullable=False , unique=False) - telefono_comercial: Mapped[int] = mapped_column(unique=True , nullable=False) - logo_url: Mapped[str] = mapped_column(String(300),unique=True , nullable=False) - estilos: Mapped[Dict[str,Any]] = mapped_column(JSON , nullable=False , default=dict)#my sql colocar JSON y si es postgress JSONb - redes_sociales: Mapped[str] = mapped_column(unique=False , nullable=False) - fecha_creacion: Mapped[TIMESTAMP] = mapped_column(DateTime(), default=datetime.now(timezone.utc)) + nombre_tienda: Mapped[str] = mapped_column(unique=False, nullable=False) + descripcion_tienda: Mapped[str] = mapped_column( + String(300), nullable=False, unique=False) + categoria_principal: Mapped[str] = mapped_column( + nullable=False, unique=False) + telefono_comercial: Mapped[int] = mapped_column( + unique=True, nullable=False) + logo_url: Mapped[str] = mapped_column( + String(300), unique=True, nullable=False) + estilos: Mapped[Dict[str, Any]] = mapped_column( + JSON, nullable=True, default=dict) # my sql colocar JSON y si es postgress JSONb + redes_sociales: Mapped[str] = mapped_column(unique=False, nullable=False) + fecha_creacion: Mapped[TIMESTAMP] = mapped_column( + DateTime(), default=datetime.now(timezone.utc)) owner_id: Mapped[int] = mapped_column(ForeignKey('user.id')) owner: Mapped['User'] = relationship(back_populates='tiendas') - productos: Mapped[List['Productos']] = relationship(back_populates='tienda') - favoritos: Mapped['Favoritos'] = relationship(back_populates='tienda') - - historial_tienda: Mapped[List['Historial']] = relationship(back_populates='tienda') + productos: Mapped[List['Productos']] = relationship( + back_populates='tienda') + favoritos: Mapped[List['Favoritos']] = relationship( + back_populates='tienda') + historial_tienda: Mapped[List['Historial'] + ] = relationship(back_populates='tienda') def serialize(self): - return{ + return { "id": self.id, - "user_id": self.user_id,#acordarse de preguntar esta parte + "user_id": self.owner_id, # corregido para usar el campo correcto "nombre_tienda": self.nombre_tienda, "descripcion_tienda": self.descripcion_tienda, "categoria_principal": self.categoria_principal, @@ -80,37 +93,48 @@ def serialize(self): "estilos": self.estilos, "redes_sociales": self.redes_sociales, "fecha_creacion": self.fecha_creacion, + "favoritos": [f.serialize() for f in self.favoritos] if self.favoritos else None, + "productos": [p.serialize() for p in self.productos] if self.productos else None, + "historial_tienda": [h.serialize() for h in self.historial_tienda] if self.historial_tienda else None } + class Productos(db.Model): - __tablename__="productos" + __tablename__ = "productos" id: Mapped[int] = mapped_column(primary_key=True) tienda_id: Mapped[int] = mapped_column(ForeignKey('tienda.id')) - nombre_producto: Mapped[str] = mapped_column(unique=False , nullable=False) - descripcion_producto: Mapped[str] = mapped_column(String(300) , nullable=False , unique=False) - precio: Mapped[float] = mapped_column(Float(), nullable=False , unique=False, default=0.0) - stock: Mapped[int] = mapped_column(nullable=False , unique=False) - categoria_producto: Mapped[str] = mapped_column(nullable=False , unique=False) - peso: Mapped[float] = mapped_column(nullable=False , unique=False, default=0.0) - dimensiones: Mapped[str] = mapped_column(nullable=False , unique=False) - imagenes: Mapped[str] = mapped_column(String(300),unique=True , nullable=False) - estado: Mapped[str] = mapped_column(unique=False , nullable=False) - fecha_subida: Mapped[TIMESTAMP] = mapped_column(DateTime(timezone=True), default=datetime.now(timezone.utc)) - + nombre_producto: Mapped[str] = mapped_column(unique=False, nullable=False) + descripcion_producto: Mapped[str] = mapped_column( + String(300), nullable=False, unique=False) + precio: Mapped[float] = mapped_column( + Float(), nullable=False, unique=False, default=0.0) + stock: Mapped[int] = mapped_column(nullable=False, unique=False) + categoria_producto: Mapped[str] = mapped_column( + nullable=False, unique=False) + peso: Mapped[float] = mapped_column( + nullable=False, unique=False, default=0.0) + dimensiones: Mapped[str] = mapped_column(nullable=False, unique=False) + imagenes: Mapped[str] = mapped_column( + String(300), unique=True, nullable=False) + estado: Mapped[str] = mapped_column(unique=False, nullable=False) + fecha_subida: Mapped[TIMESTAMP] = mapped_column( + DateTime(timezone=True), default=datetime.now(timezone.utc)) + tienda_id: Mapped[int] = mapped_column(ForeignKey('tienda.id')) - tienda: Mapped['Tienda'] = relationship(back_populates='productos', uselist=False) + tienda: Mapped['Tienda'] = relationship( + back_populates='productos', uselist=False) resenas: Mapped[List['Resenas']] = relationship(back_populates='producto') - favoritos: Mapped['Favoritos'] = relationship(back_populates='producto') - - pedidos: Mapped['Detalles_pedido'] = relationship(back_populates="productos") - + favoritos: Mapped[List['Favoritos']] = relationship( + back_populates='producto') + pedidos: Mapped[List['Detalles_pedido']] = relationship( + back_populates="productos") def serialize(self): - return{ + return { "id": self.id, - "tienda_id": self.tienda_id,#acordarse de preguntar esta parte + "tienda_id": self.tienda_id, # acordarse de preguntar esta parte "nombre_producto": self.nombre_producto, "descripcion_producto": self.descripcion_producto, "precio": self.precio, @@ -121,74 +145,90 @@ def serialize(self): "imagenes": self.imagenes, "estado": self.estado, "fecha_subida": self.fecha_subida, - "tienda": {"nombre": self.tienda.nombre_tienda} if self.tienda else None, #cuando es UN solo objeto, podemos acceder directamente a sus propiedades - "resenas": [r.serialize() for r in self.resenas] if self.resenas else None #cada vez que sea una lista de objetos, tenemos que hacer loop para serializar cada objeto. + "tienda": {"nombre": self.tienda.nombre_tienda} if self.tienda else None, + "resenas": [r.serialize() for r in self.resenas] if self.resenas else None, + "favoritos": [f.serialize() for f in self.favoritos] if self.favoritos else None, + "pedidos": [p.serialize() for p in self.pedidos] if self.pedidos else None } class Favoritos(db.Model): - __tablename__="favoritos" + __tablename__ = "favoritos" id: Mapped[int] = mapped_column(primary_key=True) - - fecha: Mapped[TIMESTAMP] = mapped_column(DateTime(timezone=True), default=datetime.now(timezone.utc)) - - tienda_id: Mapped[int] = mapped_column(ForeignKey('tienda.id'))#conectar con id de tienda - user_id: Mapped[int] = mapped_column(ForeignKey('user.id'))#conectar con id de usuario - producto_id: Mapped[int] = mapped_column(ForeignKey('productos.id'))#conectar con id de productos + + fecha: Mapped[TIMESTAMP] = mapped_column( + DateTime(timezone=True), default=datetime.now(timezone.utc)) + + tienda_id: Mapped[int] = mapped_column( + ForeignKey('tienda.id')) # conectar con id de tienda + user_id: Mapped[int] = mapped_column( + ForeignKey('user.id')) # conectar con id de usuario + producto_id: Mapped[int] = mapped_column(ForeignKey( + 'productos.id')) # conectar con id de productos tienda: Mapped['Tienda'] = relationship(back_populates='favoritos') user: Mapped['User'] = relationship(back_populates='favoritos') producto: Mapped['Productos'] = relationship(back_populates='favoritos') - def serialize(self): - return{ + return { "id": self.id, - "tienda_id": self.tienda_id,#acordarse de preguntar esta parte - "user_id": self.user_id,#conectar con id de usuario - "producto_id": self.producto_id,#conectar con productos id + "tienda_id": self.tienda_id, # acordarse de preguntar esta parte + "user_id": self.user_id, # conectar con id de usuario + "producto_id": self.producto_id, # conectar con productos id "fecha": self.fecha, "producto": {"nombre_producto": self.producto.nombre_producto} if self.producto else None } - + class Detalles_pedido(db.Model): - __tablename__="detalles_pedido" + __tablename__ = "detalles_pedido" id: Mapped[int] = mapped_column(primary_key=True) - user_id: Mapped[int] = mapped_column(ForeignKey('user.id'))#unir a producto + user_id: Mapped[int] = mapped_column( + ForeignKey('user.id')) # unir a producto user: Mapped['User'] = relationship(back_populates="pedidos") - producto_id: Mapped[int] = mapped_column(ForeignKey('productos.id'))#unir a producto - productos: Mapped[List['Productos']] = relationship(back_populates="pedidos") - subtotal: Mapped[float] = mapped_column(nullable=False , unique=False) - - historial_pedido: Mapped[List['Historial']] = relationship(back_populates='pedido') + producto_id: Mapped[int] = mapped_column( + ForeignKey('productos.id')) # unir a producto + productos: Mapped['Productos'] = relationship(back_populates="pedidos") + subtotal: Mapped[float] = mapped_column(nullable=False, unique=False) + + historial_pedido: Mapped[List['Historial'] + ] = relationship(back_populates='pedido') def serialize(self): - return{ + return { "id": self.id, "user_id": self.user_id, "producto_id": self.producto_id, "user": {"email": self.user.email} if self.user else None, - "productos": [p.serialize() for p in self.productos] if self.productos else None, + "productos": {'id': self.productos.id, + 'nombre_producto': self.productos.nombre_producto, + 'descripcion_producto': self.productos.descripcion_producto, + 'precio': self.productos.precio, + 'tienda': { + "id": self.productos.tienda.id + }} if self.productos else None, "subtotal": self.subtotal, } class Notificaciones(db.Model): - __tablename__="notificaciones" + __tablename__ = "notificaciones" id: Mapped[int] = mapped_column(primary_key=True) - - tipo: Mapped[str] = mapped_column(unique=False , nullable=False) - mensaje: Mapped[str] = mapped_column(String(300),unique=False,nullable=False) + + tipo: Mapped[str] = mapped_column(unique=False, nullable=False) + mensaje: Mapped[str] = mapped_column( + String(300), unique=False, nullable=False) leido: Mapped[bool] = mapped_column(Boolean()) - fecha: Mapped[TIMESTAMP] = mapped_column(DateTime(timezone=True), default=datetime.now(timezone.utc)) + fecha: Mapped[TIMESTAMP] = mapped_column( + DateTime(timezone=True), default=datetime.now(timezone.utc)) - user_id: Mapped[int] = mapped_column(ForeignKey('user.id'))#unir a user + user_id: Mapped[int] = mapped_column(ForeignKey('user.id')) # unir a user user: Mapped['User'] = relationship(back_populates="notificaciones") def serialize(self): - return{ + return { "user_id": self.user_id, "tipo": self.tipo, "mensaje": self.mensaje, @@ -199,21 +239,23 @@ def serialize(self): class Resenas(db.Model): - __tablename__="resenas" + __tablename__ = "resenas" id: Mapped[int] = mapped_column(primary_key=True) estrellas: Mapped[int] = mapped_column(Integer()) comentario: Mapped[str] = mapped_column(Text()) - fecha: Mapped[TIMESTAMP] = mapped_column(DateTime(timezone=True), default=datetime.now(timezone.utc)) + fecha: Mapped[TIMESTAMP] = mapped_column( + DateTime(timezone=True), default=datetime.now(timezone.utc)) respuestas: Mapped[str] = mapped_column(Text()) - - cliente_id: Mapped[int] = mapped_column(ForeignKey('user.id'))#unir a user + cliente_id: Mapped[int] = mapped_column( + ForeignKey('user.id')) # unir a user autor: Mapped['User'] = relationship(back_populates='resenas') - producto_id: Mapped[int] = mapped_column(ForeignKey('productos.id'))#unir a product + producto_id: Mapped[int] = mapped_column( + ForeignKey('productos.id')) # unir a product producto: Mapped['Productos'] = relationship(back_populates='resenas') def serialize(self): - return{ + return { "id": self.id, "producto_id": self.producto_id, "cliente_id": self.cliente_id, @@ -221,28 +263,35 @@ def serialize(self): "comentario": self.comentario, "fecha": self.fecha, "respuestas": self.respuestas, - "producto": {"nombre_producto": self.producto.nombre_producto} if self.producto else None #posible recursividad + # posible recursividad + "producto": {"nombre_producto": self.producto.nombre_producto} if self.producto else None } + class Historial(db.Model): - __tablename__="historial" + __tablename__ = "historial" id: Mapped[int] = mapped_column(primary_key=True) - total: Mapped[float] = mapped_column(unique=False,nullable=False, default=0.0) - gastos_envio: Mapped[float] = mapped_column(unique=False,nullable=False, default=0.0) - fecha_pedido: Mapped[TIMESTAMP] = mapped_column(DateTime(timezone=True), default=datetime.now(timezone.utc)) - direccion: Mapped[str] = mapped_column(String(100),unique=False,nullable=False) + total: Mapped[float] = mapped_column( + unique=False, nullable=False, default=0.0) + gastos_envio: Mapped[float] = mapped_column( + unique=False, nullable=False, default=0.0) + fecha_pedido: Mapped[TIMESTAMP] = mapped_column( + DateTime(timezone=True), default=datetime.now(timezone.utc)) + direccion: Mapped[str] = mapped_column( + String(100), unique=False, nullable=False) pago: Mapped[bool] = mapped_column(Boolean()) - pedido_id: Mapped[int] = mapped_column(ForeignKey('user.id'))#unir a detalles pedido (user = cliente) - pedido: Mapped['Detalles_pedido'] = relationship(back_populates='historial_pedido') - - - tienda_id: Mapped[int] = mapped_column(ForeignKey('tienda.id'))#unir a tienda id - tienda: Mapped['Tienda'] = relationship(back_populates='historial_tienda') + # unir a detalles pedido (user = cliente) + pedido_id: Mapped[int] = mapped_column(ForeignKey('detalles_pedido.id')) + pedido: Mapped['Detalles_pedido'] = relationship( + back_populates='historial_pedido') + tienda_id: Mapped[int] = mapped_column( + ForeignKey('tienda.id')) # unir a tienda id + tienda: Mapped['Tienda'] = relationship(back_populates='historial_tienda') def serialize(self): - return{ + return { "pedido_id": self.pedido_id, "tienda_id": self.tienda_id, "total": self.total, diff --git a/src/api/routes.py b/src/api/routes.py index b353418e87..c37e453940 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -1,6 +1,3 @@ -""" -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.utils import generate_sitemap, APIException @@ -25,7 +22,7 @@ def handle_hello(): return jsonify(response_body), 200 -@api.route('/register', method=['POST']) +@api.route('/register', methods=['POST']) def handle_register(): body = request.get_json() @@ -33,14 +30,15 @@ def handle_register(): hashed_password = generate_password_hash( body['password']) - new_user = User(role=body['role'], nickname=body['nickname'], nombre=body['nombre'], apellido=body['apellido'], fecha_nacimiento=body['fecha_nacimiento'], email=body['email'], - address=body['address'], telefono=body['telefono'], password=body[hashed_password], registro_fecha=body['registro_fecha'], is_active=body['is_active']) + new_user = User(email=body['email'], + password=body[hashed_password], + is_active=True) db.session.add(new_user) db.session.commit() - return jsonify({'msg': 'Nuevo usuario creado con exito'}), 201 + return jsonify({'success': True ,'msg': 'Nuevo usuario creado con exito'}), 201 -@api.route('/update', methods=['PUT']) +@api.route('/profile_update', methods=['PUT']) @jwt_required() def handle_update(): @@ -50,16 +48,16 @@ def handle_update(): user=db.session.get(User,id) user.role=body.get('role', user.role) - user.nickname=body['nickname'] - user.nombre=body['nombre'] - user.apellido=body['apellido'] - user.fecha_nacimiento=body['fecha_nacimiento'] - user.email=body['email'] - user.address=body['address'] - user.telefono=body['telefono'] - user.password=body[hashed_password] + user.nickname=body.get('nickname', user.nickname) + user.nombre=body.get('nombre', user.nombre) + user.apellido=body.get('apellido', user.apellido) + user.fecha_nacimiento=body.get('fecha_nacimiento', user.fecha_nacimiento) + user.email=body.get('email', user.email) + user.address=body.get('address', user.address) + user.telefono=body.get('telefono', user.telefono) + user.password=body.get(hashed_password, user.password) db.session.commit() - return jsonify({'user':user.serialize()}) + return jsonify({'user':user.serialize()}), 200 @api.route('/login', methods=['POST']) def handle_login(): @@ -74,4 +72,4 @@ def handle_login(): return jsonify({'msg':'email y/o contraseña no valido'}),400 token=create_access_token(identity=str(user.id)) - return jsonify({'user':user.serialize(),'token':token}),200 \ No newline at end of file + return jsonify({"user":user.serialize(),"token":token}),200 \ No newline at end of file diff --git a/src/front/components/Login.jsx b/src/front/components/Login.jsx index 0353e8f1f0..e830b35054 100644 --- a/src/front/components/Login.jsx +++ b/src/front/components/Login.jsx @@ -1,13 +1,35 @@ -import React from "react"; +import React, { useState } from "react"; import { Link } from "react-router-dom"; +import AuthServices from "../services/auth.services"; +import useGlobalReducer from "../hooks/useGlobalReducer"; const Login = () => { + const {store, dispatch} = useGlobalReducer() + const [formData, setFormData] = useState({ + email: '', + password: '' + }) + + const handleChange = e => { + const {value, name} = e.target; + setFormData({...formData, [name]: value}); + } + + + const handleSubmit = e => { + e.preventDefault(); + AuthServices.login(formData).then(data => { + console.log(data) + dispatch({type: 'login', payload: data.user}) + }) + } + return (

    Bienvenido

    -
    +
    @@ -30,8 +55,11 @@ const Login = () => { type="password" className="form-control" id="password" + name='password' placeholder="contraseña" required + value={formData.password} + onChange={handleChange} style={{ borderColor: '#a00' }} />
    diff --git a/src/front/services/auth.services.jsx b/src/front/services/auth.services.jsx new file mode 100644 index 0000000000..54c2f14d1b --- /dev/null +++ b/src/front/services/auth.services.jsx @@ -0,0 +1,56 @@ +const AuthServices = {} +const url = import.meta.env.VITE_BACKEND_URL + + +AuthServices.login = async (formData) => { + try { + const resp = await fetch(url + '/api/login', { + method: "POST", + headers: { + "Content-Type": 'application/json' + }, + body: JSON.stringify(formData) + }) + if (!resp.ok) throw new Error('error logging in') + + const data = await resp.json() + + localStorage.setItem('token', data.token) + localStorage.setItem('user', JSON.stringify(data.user)) + return data + + } catch (error) { + console.log(error) + } +} + + +AuthServices.register = async (formData) => { + try { + const resp = await fetch(url + '/api/register', { + method: "POST", + headers: { + "Content-Type": 'application/json' + }, + body: JSON.stringify(formData) + }) + if (!resp.ok) throw new Error('error logging in') + + const data = resp.json() + return data.success + + } catch (error) { + console.log(error) + } +} + + + +AuthServices.logout = () => { + localStorage.removeItem('token') + localStorage.removeItem('user') + return true +} + + +export default AuthServices \ No newline at end of file diff --git a/src/front/services/user.services.jsx b/src/front/services/user.services.jsx new file mode 100644 index 0000000000..9b9c47a948 --- /dev/null +++ b/src/front/services/user.services.jsx @@ -0,0 +1,28 @@ +const url = import.meta.env.VITE_BACKEND_URL + +const userServices = {} + +userServices.profile_update = async (formData) => { + try { + const resp = await fetch(url + '/api/profile_update', { + method: "PUT", + headers: { + "Content-Type": 'application/json', + 'Authorization': 'Bearer ' + localStorage.getItem('token') + }, + body: JSON.stringify(formData) + }) + if (!resp.ok) throw new Error('error logging in') + + const data = resp.json() + return data + + } catch (error) { + console.log(error) + } +} + + + + +export default userServices \ No newline at end of file diff --git a/src/front/store.js b/src/front/store.js index 3062cd222d..d0131cd777 100644 --- a/src/front/store.js +++ b/src/front/store.js @@ -18,6 +18,17 @@ export const initialStore=()=>{ export default function storeReducer(store, action = {}) { switch(action.type){ + case "login": + return { + ...store, + user: action.payload + } + case "logout": + return { + ...store, + user: null, + cart: [] // no recuerdo ahora!!!! verificar + } case 'set_hello': return { ...store, @@ -35,4 +46,4 @@ export default function storeReducer(store, action = {}) { default: throw Error('Unknown action.'); } -} +} \ No newline at end of file From 6606624888b752f7770b4ed769f88537c9ed4535 Mon Sep 17 00:00:00 2001 From: 1v4nP4 <1v4nP4@proton.me> Date: Fri, 31 Oct 2025 18:31:40 +0000 Subject: [PATCH 23/27] register ya funciona --- src/api/routes.py | 57 ++++++++++++++++--------------- src/front/components/Register.jsx | 29 ++++++++++++++-- 2 files changed, 57 insertions(+), 29 deletions(-) diff --git a/src/api/routes.py b/src/api/routes.py index c37e453940..9b0e96f697 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -31,45 +31,48 @@ def handle_register(): body['password']) new_user = User(email=body['email'], - password=body[hashed_password], + password=hashed_password, + role=body['role'], is_active=True) db.session.add(new_user) db.session.commit() - return jsonify({'success': True ,'msg': 'Nuevo usuario creado con exito'}), 201 + return jsonify({'success': True, 'msg': 'Nuevo usuario creado con exito'}), 201 + @api.route('/profile_update', methods=['PUT']) @jwt_required() def handle_update(): - id=get_jwt_identity() + id = get_jwt_identity() body = request.get_json() hashed_password = generate_password_hash(body['password']).decode('utf-8') - user=db.session.get(User,id) - user.role=body.get('role', user.role) - user.nickname=body.get('nickname', user.nickname) - user.nombre=body.get('nombre', user.nombre) - user.apellido=body.get('apellido', user.apellido) - user.fecha_nacimiento=body.get('fecha_nacimiento', user.fecha_nacimiento) - user.email=body.get('email', user.email) - user.address=body.get('address', user.address) - user.telefono=body.get('telefono', user.telefono) - user.password=body.get(hashed_password, user.password) + user = db.session.get(User, id) + user.role = body.get('role', user.role) + user.nickname = body.get('nickname', user.nickname) + user.nombre = body.get('nombre', user.nombre) + user.apellido = body.get('apellido', user.apellido) + user.fecha_nacimiento = body.get('fecha_nacimiento', user.fecha_nacimiento) + user.email = body.get('email', user.email) + user.address = body.get('address', user.address) + user.telefono = body.get('telefono', user.telefono) + user.password = body.get(hashed_password, user.password) db.session.commit() - return jsonify({'user':user.serialize()}), 200 - + return jsonify({'user': user.serialize()}), 200 + + @api.route('/login', methods=['POST']) def handle_login(): - body= request.json - stm= select(User).where(User.email==body['email']) - user=db.session.execute(stm).scalar_one_or_none() - - if not user : - return jsonify({'msg':'email no encontrado'}),404 - - if not check_password_hash(user.password , body['password']): - return jsonify({'msg':'email y/o contraseña no valido'}),400 - - token=create_access_token(identity=str(user.id)) - return jsonify({"user":user.serialize(),"token":token}),200 \ No newline at end of file + body = request.json + stm = select(User).where(User.email == body['email']) + user = db.session.execute(stm).scalar_one_or_none() + + if not user: + return jsonify({'msg': 'email no encontrado'}), 404 + + if not check_password_hash(user.password, body['password']): + return jsonify({'msg': 'email y/o contraseña no valido'}), 400 + + token = create_access_token(identity=str(user.id)) + return jsonify({"user": user.serialize(), "token": token}), 200 diff --git a/src/front/components/Register.jsx b/src/front/components/Register.jsx index 8baa6d4578..1277cfddc7 100644 --- a/src/front/components/Register.jsx +++ b/src/front/components/Register.jsx @@ -2,14 +2,34 @@ import React from "react"; import { Link } from "react-router-dom"; import "./Register.css"; -export const Register = () => { +const Register = () => { + const {store, dispatch} = useGlobalReducer() + const [formData, setFormData] = useState({ + email: '', + password: '' + }) + + const handleChange = e => { + const {value, name} = e.target; + setFormData({...formData, [name]: value}); + } + + + const handleSubmit = e => { + e.preventDefault(); + AuthServices.login(formData).then(data => { + console.log(data) + dispatch({type: 'register', payload: data.user}) + }) + } + return ( <>

    Crear Cuenta

    - +
    @@ -34,6 +56,8 @@ export const Register = () => { id="password" placeholder="contraseña" required + value={formData.password} + onChange={handleChange} style={{ borderColor: "#a00" }} />
    @@ -51,3 +75,4 @@ export const Register = () => { ); }; +export default Register \ No newline at end of file From 262d369b6f7eecd4f6099795d37d26b811186a58 Mon Sep 17 00:00:00 2001 From: 1v4nP4 <1v4nP4@proton.me> Date: Fri, 31 Oct 2025 19:23:38 +0000 Subject: [PATCH 24/27] fix register --- Pipfile.lock | 586 ++++++++++++++------------- src/api/admin.py | 2 +- src/api/models.py | 2 +- src/api/routes.py | 1 - src/app.py | 3 + src/front/components/Register.jsx | 15 +- src/front/pages/RegisterPage.jsx | 2 +- src/front/routes.jsx | 2 +- src/front/services/auth.services.jsx | 4 +- src/front/store.js | 7 + 10 files changed, 325 insertions(+), 299 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 51a921bf5c..7b2cd81764 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -18,11 +18,11 @@ "default": { "alembic": { "hashes": [ - "sha256:1acdd7a3a478e208b0503cd73614d5e4c6efafa4e73518bb60e4f2846a37b1c5", - "sha256:496e888245a53adf1498fcab31713a469c65836f8de76e01399aa1c3e90dd213" + "sha256:8a289f6778262df31571d29cca4c7fbacd2f0f582ea0816f4c399b6da7528486", + "sha256:cbc2386e60f89608bb63f30d2d6cc66c7aaed1fe105bd862828600e5ad167023" ], - "markers": "python_version >= '3.8'", - "version": "==1.14.1" + "markers": "python_version >= '3.10'", + "version": "==1.17.1" }, "bcrypt": { "hashes": [ @@ -104,11 +104,11 @@ }, "certifi": { "hashes": [ - "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", - "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe" + "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", + "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43" ], - "markers": "python_version >= '3.6'", - "version": "==2025.1.31" + "markers": "python_version >= '3.7'", + "version": "==2025.10.5" }, "click": { "hashes": [ @@ -120,11 +120,11 @@ }, "cloudinary": { "hashes": [ - "sha256:ba223705409b2aaddd5196c2184d65f50a83dffcba3b94f3727658ff6a0172a3", - "sha256:e4191b470c5bae55542b64e0a78659af42971880294456dca480bc974fa9280a" + "sha256:62d4374b79d5476de2a86cb6a1da709a5429e02aef474bfc5d99f3e38a1a62ff", + "sha256:b4785031179a5ec7010f46665e5c8fad2cae022c18405546f01d257e02f78b1c" ], "index": "pypi", - "version": "==1.42.2" + "version": "==1.44.1" }, "flask": { "hashes": [ @@ -137,19 +137,21 @@ }, "flask-admin": { "hashes": [ - "sha256:24cae2af832b6a611a01d7dc35f42d266c1d6c75a426b869d8cb241b78233369", - "sha256:fd8190f1ec3355913a22739c46ed3623f1d82b8112cde324c60a6fc9b21c9406" + "sha256:3b2dc1f13c2ec450e2c9197f58cd684be5b96a9497e1ffb9d9efb9f7f59825f9", + "sha256:c5878ab1ea92c9e8345c0e1a2cd15851f5ecf2e1616d7fb9158487769f188e81" ], "index": "pypi", - "version": "==1.6.1" + "markers": "python_version >= '3.10'", + "version": "==2.0.0" }, "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": [ @@ -166,6 +168,7 @@ "sha256:24d8051af161782e0743af1b04a152d007bad9772b2bca67b7ec1e8ceeb3910d" ], "index": "pypi", + "markers": "python_version >= '3.6'", "version": "==4.1.0" }, "flask-sqlalchemy": { @@ -174,6 +177,7 @@ "sha256:e4b68bb881802dda1a7d878b2fc84c06d1ee57fb40b874d3dc97dabfa36b8312" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==3.1.1" }, "flask-swagger": { @@ -186,82 +190,63 @@ }, "greenlet": { "hashes": [ - "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e", - "sha256:03a088b9de532cbfe2ba2034b2b85e82df37874681e8c470d6fb2f8c04d7e4b7", - "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01", - "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1", - "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159", - "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563", - "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83", - "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9", - "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395", - "sha256:1d3755bcb2e02de341c55b4fca7a745a24a9e7212ac953f6b3a48d117d7257aa", - "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942", - "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1", - "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441", - "sha256:3319aa75e0e0639bc15ff54ca327e8dc7a6fe404003496e3c6925cd3142e0e22", - "sha256:346bed03fe47414091be4ad44786d1bd8bef0c3fcad6ed3dee074a032ab408a9", - "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0", - "sha256:37b9de5a96111fc15418819ab4c4432e4f3c2ede61e660b1e33971eba26ef9ba", - "sha256:396979749bd95f018296af156201d6211240e7a23090f50a8d5d18c370084dc3", - "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1", - "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6", - "sha256:47da355d8687fd65240c364c90a31569a133b7b60de111c255ef5b606f2ae291", - "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39", - "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d", - "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467", - "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475", - "sha256:54558ea205654b50c438029505def3834e80f0869a70fb15b871c29b4575ddef", - "sha256:5e06afd14cbaf9e00899fae69b24a32f2196c19de08fcb9f4779dd4f004e5e7c", - "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511", - "sha256:63e4844797b975b9af3a3fb8f7866ff08775f5426925e1e0bbcfe7932059a12c", - "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822", - "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a", - "sha256:6ef9ea3f137e5711f0dbe5f9263e8c009b7069d8a1acea822bd5e9dae0ae49c8", - "sha256:7017b2be767b9d43cc31416aba48aab0d2309ee31b4dbf10a1d38fb7972bdf9d", - "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01", - "sha256:73aaad12ac0ff500f62cebed98d8789198ea0e6f233421059fa68a5aa7220145", - "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80", - "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13", - "sha256:7939aa3ca7d2a1593596e7ac6d59391ff30281ef280d8632fa03d81f7c5f955e", - "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b", - "sha256:85f3ff71e2e60bd4b4932a043fbbe0f499e263c628390b285cb599154a3b03b1", - "sha256:8b8b36671f10ba80e159378df9c4f15c14098c4fd73a36b9ad715f057272fbef", - "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc", - "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff", - "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120", - "sha256:94ebba31df2aa506d7b14866fed00ac141a867e63143fe5bca82a8e503b36437", - "sha256:95ffcf719966dd7c453f908e208e14cde192e09fde6c7186c8f1896ef778d8cd", - "sha256:98884ecf2ffb7d7fe6bd517e8eb99d31ff7855a840fa6d0d63cd07c037f6a981", - "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36", - "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a", - "sha256:a0dfc6c143b519113354e780a50381508139b07d2177cb6ad6a08278ec655798", - "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7", - "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761", - "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0", - "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e", - "sha256:b8da394b34370874b4572676f36acabac172602abf054cbc4ac910219f3340af", - "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa", - "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c", - "sha256:ca9d0ff5ad43e785350894d97e13633a66e2b50000e8a183a50a88d834752d42", - "sha256:d0028e725ee18175c6e422797c407874da24381ce0690d6b9396c204c7f7276e", - "sha256:d21e10da6ec19b457b82636209cbe2331ff4306b54d06fa04b7c138ba18c8a81", - "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e", - "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617", - "sha256:db32b5348615a04b82240cc67983cb315309e88d444a288934ee6ceaebcad6cc", - "sha256:dcc62f31eae24de7f8dce72134c8651c58000d3b1868e01392baea7c32c247de", - "sha256:dfc59d69fc48664bc693842bd57acfdd490acafda1ab52c7836e3fc75c90a111", - "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383", - "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70", - "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6", - "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4", - "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011", - "sha256:f1d4aeb8891338e60d1ab6127af1fe45def5259def8094b9c7e34690c8858803", - "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79", - "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f" + "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", + "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", + "sha256:0db5594dce18db94f7d1650d7489909b57afde4c580806b8d9203b6e79cdc079", + "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d", + "sha256:16458c245a38991aa19676900d48bd1a6f2ce3e16595051a4db9d012154e8433", + "sha256:18d9260df2b5fbf41ae5139e1be4e796d99655f023a636cd0e11e6406cca7d58", + "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52", + "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", + "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246", + "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f", + "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", + "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8", + "sha256:27890167f55d2387576d1f41d9487ef171849ea0359ce1510ca6e06c8bece11d", + "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", + "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0", + "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd", + "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", + "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", + "sha256:4d1378601b85e2e5171b99be8d2dc85f594c79967599328f95c1dc1a40f1c633", + "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", + "sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa", + "sha256:58b97143c9cc7b86fc458f215bd0932f1757ce649e05b640fea2e79b54cedb31", + "sha256:5c9320971821a7cb77cfab8d956fa8e39cd07ca44b6070db358ceb7f8797c8c9", + "sha256:65458b409c1ed459ea899e939f0e1cdb14f58dbc803f2f93c5eab5694d32671b", + "sha256:671df96c1f23c4a0d4077a325483c1503c96a1b7d9db26592ae770daa41233d4", + "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", + "sha256:73f49b5368b5359d04e18d15828eecc1806033db5233397748f4ca813ff1056c", + "sha256:81701fd84f26330f0d5f4944d4e92e61afe6319dcd9775e39396e39d7c3e5f98", + "sha256:8854167e06950ca75b898b104b63cc646573aa5fef1353d4508ecdd1ee76254f", + "sha256:8c68325b0d0acf8d91dde4e6f930967dd52a5302cd4062932a6b2e7c2969f47c", + "sha256:94385f101946790ae13da500603491f04a76b6e4c059dab271b3ce2e283b2590", + "sha256:94abf90142c2a18151632371140b3dba4dee031633fe614cb592dbb6c9e17bc3", + "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2", + "sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9", + "sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5", + "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02", + "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0", + "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", + "sha256:b6a7c19cf0d2742d0809a4c05975db036fdff50cd294a93632d6a310bf9ac02c", + "sha256:b90654e092f928f110e0007f572007c9727b5265f7632c2fa7415b4689351594", + "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", + "sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d", + "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", + "sha256:c60a6d84229b271d44b70fb6e5fa23781abb5d742af7b808ae3f6efd7c9c60f6", + "sha256:c8c9e331e58180d0d83c5b7999255721b725913ff6bc6cf39fa2a45841a4fd4b", + "sha256:c9913f1a30e4526f432991f89ae263459b1c64d1608c0d22a5c79c287b3c70df", + "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", + "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", + "sha256:d2e685ade4dafd447ede19c31277a224a239a0a1a4eca4e6390efedf20260cfb", + "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", + "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb", + "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", + "sha256:f10fd42b5ee276335863712fa3da6608e93f70629c631bf77145021600abc23c", + "sha256:f28588772bb5fb869a8eb331374ec06f24a83a9c25bfa1f38b6993afe9c1e968" ], - "markers": "python_version < '3.14' and (platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32'))))))", - "version": "==3.1.1" + "markers": "python_version >= '3.9'", + "version": "==3.2.4" }, "gunicorn": { "hashes": [ @@ -269,6 +254,7 @@ "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==23.0.0" }, "itsdangerous": { @@ -289,11 +275,11 @@ }, "mako": { "hashes": [ - "sha256:95920acccb578427a9aa38e37a186b1e43156c87260d7ba18ca63aa4c7cbd3a1", - "sha256:b5d65ff3462870feec922dbccf38f6efb44e5714d7b593a656be86663d8600ac" + "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", + "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59" ], "markers": "python_version >= '3.8'", - "version": "==1.3.9" + "version": "==1.3.10" }, "markupsafe": { "hashes": [ @@ -392,85 +378,85 @@ }, "packaging": { "hashes": [ - "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", - "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f" + "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", + "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f" ], "markers": "python_version >= '3.8'", - "version": "==24.2" + "version": "==25.0" }, "psycopg2-binary": { "hashes": [ - "sha256:04392983d0bb89a8717772a193cfaac58871321e3ec69514e1c4e0d4957b5aff", - "sha256:056470c3dc57904bbf63d6f534988bafc4e970ffd50f6271fc4ee7daad9498a5", - "sha256:0ea8e3d0ae83564f2fc554955d327fa081d065c8ca5cc6d2abb643e2c9c1200f", - "sha256:155e69561d54d02b3c3209545fb08938e27889ff5a10c19de8d23eb5a41be8a5", - "sha256:18c5ee682b9c6dd3696dad6e54cc7ff3a1a9020df6a5c0f861ef8bfd338c3ca0", - "sha256:19721ac03892001ee8fdd11507e6a2e01f4e37014def96379411ca99d78aeb2c", - "sha256:1a6784f0ce3fec4edc64e985865c17778514325074adf5ad8f80636cd029ef7c", - "sha256:2286791ececda3a723d1910441c793be44625d86d1a4e79942751197f4d30341", - "sha256:230eeae2d71594103cd5b93fd29d1ace6420d0b86f4778739cb1a5a32f607d1f", - "sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7", - "sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d", - "sha256:270934a475a0e4b6925b5f804e3809dd5f90f8613621d062848dd82f9cd62007", - "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142", - "sha256:2ad26b467a405c798aaa1458ba09d7e2b6e5f96b1ce0ac15d82fd9f95dc38a92", - "sha256:2b3d2491d4d78b6b14f76881905c7a8a8abcf974aad4a8a0b065273a0ed7a2cb", - "sha256:2ce3e21dc3437b1d960521eca599d57408a695a0d3c26797ea0f72e834c7ffe5", - "sha256:30e34c4e97964805f715206c7b789d54a78b70f3ff19fbe590104b71c45600e5", - "sha256:3216ccf953b3f267691c90c6fe742e45d890d8272326b4a8b20850a03d05b7b8", - "sha256:32581b3020c72d7a421009ee1c6bf4a131ef5f0a968fab2e2de0c9d2bb4577f1", - "sha256:35958ec9e46432d9076286dda67942ed6d968b9c3a6a2fd62b48939d1d78bf68", - "sha256:3abb691ff9e57d4a93355f60d4f4c1dd2d68326c968e7db17ea96df3c023ef73", - "sha256:3c18f74eb4386bf35e92ab2354a12c17e5eb4d9798e4c0ad3a00783eae7cd9f1", - "sha256:3c4745a90b78e51d9ba06e2088a2fe0c693ae19cc8cb051ccda44e8df8a6eb53", - "sha256:3c4ded1a24b20021ebe677b7b08ad10bf09aac197d6943bfe6fec70ac4e4690d", - "sha256:3e9c76f0ac6f92ecfc79516a8034a544926430f7b080ec5a0537bca389ee0906", - "sha256:48b338f08d93e7be4ab2b5f1dbe69dc5e9ef07170fe1f86514422076d9c010d0", - "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2", - "sha256:512d29bb12608891e349af6a0cccedce51677725a921c07dba6342beaf576f9a", - "sha256:5a507320c58903967ef7384355a4da7ff3f28132d679aeb23572753cbf2ec10b", - "sha256:5c370b1e4975df846b0277b4deba86419ca77dbc25047f535b0bb03d1a544d44", - "sha256:6b269105e59ac96aba877c1707c600ae55711d9dcd3fc4b5012e4af68e30c648", - "sha256:6d4fa1079cab9018f4d0bd2db307beaa612b0d13ba73b5c6304b9fe2fb441ff7", - "sha256:6dc08420625b5a20b53551c50deae6e231e6371194fa0651dbe0fb206452ae1f", - "sha256:73aa0e31fa4bb82578f3a6c74a73c273367727de397a7a0f07bd83cbea696baa", - "sha256:7559bce4b505762d737172556a4e6ea8a9998ecac1e39b5233465093e8cee697", - "sha256:79625966e176dc97ddabc142351e0409e28acf4660b88d1cf6adb876d20c490d", - "sha256:7a813c8bdbaaaab1f078014b9b0b13f5de757e2b5d9be6403639b298a04d218b", - "sha256:7b2c956c028ea5de47ff3a8d6b3cc3330ab45cf0b7c3da35a2d6ff8420896526", - "sha256:7f4152f8f76d2023aac16285576a9ecd2b11a9895373a1f10fd9db54b3ff06b4", - "sha256:7f5d859928e635fa3ce3477704acee0f667b3a3d3e4bb109f2b18d4005f38287", - "sha256:851485a42dbb0bdc1edcdabdb8557c09c9655dfa2ca0460ff210522e073e319e", - "sha256:8608c078134f0b3cbd9f89b34bd60a943b23fd33cc5f065e8d5f840061bd0673", - "sha256:880845dfe1f85d9d5f7c412efea7a08946a46894537e4e5d091732eb1d34d9a0", - "sha256:8aabf1c1a04584c168984ac678a668094d831f152859d06e055288fa515e4d30", - "sha256:8aecc5e80c63f7459a1a2ab2c64df952051df196294d9f739933a9f6687e86b3", - "sha256:8cd9b4f2cfab88ed4a9106192de509464b75a906462fb846b936eabe45c2063e", - "sha256:8de718c0e1c4b982a54b41779667242bc630b2197948405b7bd8ce16bcecac92", - "sha256:9440fa522a79356aaa482aa4ba500b65f28e5d0e63b801abf6aa152a29bd842a", - "sha256:b5f86c56eeb91dc3135b3fd8a95dc7ae14c538a2f3ad77a19645cf55bab1799c", - "sha256:b73d6d7f0ccdad7bc43e6d34273f70d587ef62f824d7261c4ae9b8b1b6af90e8", - "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909", - "sha256:c3cc28a6fd5a4a26224007712e79b81dbaee2ffb90ff406256158ec4d7b52b47", - "sha256:ce5ab4bf46a211a8e924d307c1b1fcda82368586a19d0a24f8ae166f5c784864", - "sha256:d00924255d7fc916ef66e4bf22f354a940c67179ad3fd7067d7a0a9c84d2fbfc", - "sha256:d7cd730dfa7c36dbe8724426bf5612798734bff2d3c3857f36f2733f5bfc7c00", - "sha256:e217ce4d37667df0bc1c397fdcd8de5e81018ef305aed9415c3b093faaeb10fb", - "sha256:e3923c1d9870c49a2d44f795df0c889a22380d36ef92440ff618ec315757e539", - "sha256:e5720a5d25e3b99cd0dc5c8a440570469ff82659bb09431c1439b92caf184d3b", - "sha256:e8b58f0a96e7a1e341fc894f62c1177a7c83febebb5ff9123b579418fdc8a481", - "sha256:e984839e75e0b60cfe75e351db53d6db750b00de45644c5d1f7ee5d1f34a1ce5", - "sha256:eb09aa7f9cecb45027683bb55aebaaf45a0df8bf6de68801a6afdc7947bb09d4", - "sha256:ec8a77f521a17506a24a5f626cb2aee7850f9b69a0afe704586f63a464f3cd64", - "sha256:ecced182e935529727401b24d76634a357c71c9275b356efafd8a2a91ec07392", - "sha256:ee0e8c683a7ff25d23b55b11161c2663d4b099770f6085ff0a20d4505778d6b4", - "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1", - "sha256:f758ed67cab30b9a8d2833609513ce4d3bd027641673d4ebc9c067e4d208eec1", - "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567", - "sha256:ffe8ed017e4ed70f68b7b371d84b7d4a790368db9203dfc2d222febd3a9c8863" + "sha256:00ce1830d971f43b667abe4a56e42c1e2d594b32da4802e44a73bacacb25535f", + "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1", + "sha256:0da4de5c1ac69d94ed4364b6cbe7190c1a70d325f112ba783d83f8440285f152", + "sha256:0e8480afd62362d0a6a27dd09e4ca2def6fa50ed3a4e7c09165266106b2ffa10", + "sha256:20e7fb94e20b03dcc783f76c0865f9da39559dcc0c28dd1a3fce0d01902a6b9c", + "sha256:2c226ef95eb2250974bf6fa7a842082b31f68385c4f3268370e3f3870e7859ee", + "sha256:2d11098a83cca92deaeaed3d58cfd150d49b3b06ee0d0852be466bf87596899e", + "sha256:2e164359396576a3cc701ba8af4751ae68a07235d7a380c631184a611220d9a4", + "sha256:304fd7b7f97eef30e91b8f7e720b3db75fee010b520e434ea35ed1ff22501d03", + "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a", + "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b", + "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee", + "sha256:37d8412565a7267f7d79e29ab66876e55cb5e8e7b3bbf94f8206f6795f8f7e7e", + "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", + "sha256:41360b01c140c2a03d346cec3280cf8a71aa07d94f3b1509fa0161c366af66b4", + "sha256:44fc5c2b8fa871ce7f0023f619f1349a0aa03a0857f2c96fbc01c657dcbbdb49", + "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c", + "sha256:4bdab48575b6f870f465b397c38f1b415520e9879fdf10a53ee4f49dcbdf8a21", + "sha256:4dca1f356a67ecb68c81a7bc7809f1569ad9e152ce7fd02c2f2036862ca9f66b", + "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3", + "sha256:5f3f2732cf504a1aa9e9609d02f79bea1067d99edf844ab92c247bbca143303b", + "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d", + "sha256:691c807d94aecfbc76a14e1408847d59ff5b5906a04a23e12a89007672b9e819", + "sha256:763c93ef1df3da6d1a90f86ea7f3f806dc06b21c198fa87c3c25504abec9404a", + "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f", + "sha256:865f9945ed1b3950d968ec4690ce68c55019d79e4497366d36e090327ce7db14", + "sha256:875039274f8a2361e5207857899706da840768e2a775bf8c65e82f60b197df02", + "sha256:8b81627b691f29c4c30a8f322546ad039c40c328373b11dff7490a3e1b517855", + "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0", + "sha256:91537a8df2bde69b1c1db01d6d944c831ca793952e4f57892600e96cee95f2cd", + "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1", + "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5", + "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f", + "sha256:9c55460033867b4622cda1b6872edf445809535144152e5d14941ef591980edf", + "sha256:9d3a9edcfbe77a3ed4bc72836d466dfce4174beb79eda79ea155cc77237ed9e8", + "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757", + "sha256:a28d8c01a7b27a1e3265b11250ba7557e5f72b5ee9e5f3a2fa8d2949c29bf5d2", + "sha256:a311f1edc9967723d3511ea7d2708e2c3592e3405677bf53d5c7246753591fbb", + "sha256:a6c0e4262e089516603a09474ee13eabf09cb65c332277e39af68f6233911087", + "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a", + "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c", + "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d", + "sha256:b637d6d941209e8d96a072d7977238eea128046effbf37d1d8b2c0764750017d", + "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", + "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c", + "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4", + "sha256:be9b840ac0525a283a96b556616f5b4820e0526addb8dcf6525a0fa162730be4", + "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e", + "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766", + "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d", + "sha256:c47676e5b485393f069b4d7a811267d3168ce46f988fa602658b8bb901e9e64d", + "sha256:c665f01ec8ab273a61c62beeb8cce3014c214429ced8a308ca1fc410ecac3a39", + "sha256:cffe9d7697ae7456649617e8bb8d7a45afb71cd13f7ab22af3e5c61f04840908", + "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60", + "sha256:d57c9c387660b8893093459738b6abddbb30a7eab058b77b0d0d1c7d521ddfd7", + "sha256:d6fe6b47d0b42ce1c9f1fa3e35bb365011ca22e39db37074458f27921dca40f2", + "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8", + "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f", + "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f", + "sha256:ebb415404821b6d1c47353ebe9c8645967a5235e6d88f914147e7fd411419e6f", + "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34", + "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3", + "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa", + "sha256:f07c9c4a5093258a03b28fab9b4f151aa376989e7f35f855088234e656ee6a94", + "sha256:f090b7ddd13ca842ebfe301cd587a76a4cf0913b1e429eb92c1be5dbeb1a19bc", + "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db", + "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747" ], "index": "pypi", - "version": "==2.9.10" + "markers": "python_version >= '3.9'", + "version": "==2.9.11" }, "pyjwt": { "hashes": [ @@ -482,70 +468,91 @@ }, "python-dotenv": { "hashes": [ - "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", - "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a" + "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", + "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61" ], "index": "pypi", - "version": "==1.0.1" + "markers": "python_version >= '3.9'", + "version": "==1.2.1" }, "pyyaml": { "hashes": [ - "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff", - "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", - "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", - "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", - "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", - "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", - "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", - "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", - "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", - "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", - "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a", - "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", - "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", - "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", - "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", - "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", - "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", - "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a", - "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", - "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", - "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", - "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", - "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", - "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", - "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", - "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", - "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", - "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", - "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", - "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706", - "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", - "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", - "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", - "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083", - "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", - "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", - "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", - "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", - "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", - "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", - "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", - "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", - "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", - "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", - "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5", - "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d", - "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", - "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", - "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", - "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", - "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", - "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", - "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4" + "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", + "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a", + "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", + "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", + "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", + "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", + "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", + "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", + "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0", + "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", + "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", + "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6", + "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7", + "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", + "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007", + "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", + "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", + "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9", + "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295", + "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", + "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", + "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", + "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", + "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", + "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", + "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", + "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", + "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b", + "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", + "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5", + "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", + "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", + "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369", + "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", + "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", + "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", + "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", + "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", + "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", + "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", + "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", + "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", + "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", + "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", + "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", + "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", + "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", + "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", + "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", + "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4", + "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", + "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", + "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", + "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", + "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", + "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", + "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da", + "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", + "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", + "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", + "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", + "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f", + "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917", + "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", + "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", + "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", + "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", + "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", + "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", + "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3", + "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", + "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926", + "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0" ], "markers": "python_version >= '3.8'", - "version": "==6.0.2" + "version": "==6.0.3" }, "six": { "hashes": [ @@ -557,82 +564,84 @@ }, "sqlalchemy": { "hashes": [ - "sha256:0398361acebb42975deb747a824b5188817d32b5c8f8aba767d51ad0cc7bb08d", - "sha256:0561832b04c6071bac3aad45b0d3bb6d2c4f46a8409f0a7a9c9fa6673b41bc03", - "sha256:07258341402a718f166618470cde0c34e4cec85a39767dce4e24f61ba5e667ea", - "sha256:0a826f21848632add58bef4f755a33d45105d25656a0c849f2dc2df1c71f6f50", - "sha256:1052723e6cd95312f6a6eff9a279fd41bbae67633415373fdac3c430eca3425d", - "sha256:12d5b06a1f3aeccf295a5843c86835033797fea292c60e72b07bcb5d820e6dd3", - "sha256:12f5c9ed53334c3ce719155424dc5407aaa4f6cadeb09c5b627e06abb93933a1", - "sha256:2a0ef3f98175d77180ffdc623d38e9f1736e8d86b6ba70bff182a7e68bed7727", - "sha256:2f2951dc4b4f990a4b394d6b382accb33141d4d3bd3ef4e2b27287135d6bdd68", - "sha256:3868acb639c136d98107c9096303d2d8e5da2880f7706f9f8c06a7f961961149", - "sha256:386b7d136919bb66ced64d2228b92d66140de5fefb3c7df6bd79069a269a7b06", - "sha256:3d3043375dd5bbcb2282894cbb12e6c559654c67b5fffb462fda815a55bf93f7", - "sha256:3e35d5565b35b66905b79ca4ae85840a8d40d31e0b3e2990f2e7692071b179ca", - "sha256:402c2316d95ed90d3d3c25ad0390afa52f4d2c56b348f212aa9c8d072a40eee5", - "sha256:40310db77a55512a18827488e592965d3dec6a3f1e3d8af3f8243134029daca3", - "sha256:40e9cdbd18c1f84631312b64993f7d755d85a3930252f6276a77432a2b25a2f3", - "sha256:49aa2cdd1e88adb1617c672a09bf4ebf2f05c9448c6dbeba096a3aeeb9d4d443", - "sha256:57dd41ba32430cbcc812041d4de8d2ca4651aeefad2626921ae2a23deb8cd6ff", - "sha256:5dba1cdb8f319084f5b00d41207b2079822aa8d6a4667c0f369fce85e34b0c86", - "sha256:5e1d9e429028ce04f187a9f522818386c8b076723cdbe9345708384f49ebcec6", - "sha256:63178c675d4c80def39f1febd625a6333f44c0ba269edd8a468b156394b27753", - "sha256:6493bc0eacdbb2c0f0d260d8988e943fee06089cd239bd7f3d0c45d1657a70e2", - "sha256:64aa8934200e222f72fcfd82ee71c0130a9c07d5725af6fe6e919017d095b297", - "sha256:665255e7aae5f38237b3a6eae49d2358d83a59f39ac21036413fab5d1e810578", - "sha256:6db316d6e340f862ec059dc12e395d71f39746a20503b124edc255973977b728", - "sha256:70065dfabf023b155a9c2a18f573e47e6ca709b9e8619b2e04c54d5bcf193178", - "sha256:8455aa60da49cb112df62b4721bd8ad3654a3a02b9452c783e651637a1f21fa2", - "sha256:8b0ac78898c50e2574e9f938d2e5caa8fe187d7a5b69b65faa1ea4648925b096", - "sha256:8bf312ed8ac096d674c6aa9131b249093c1b37c35db6a967daa4c84746bc1bc9", - "sha256:92f99f2623ff16bd4aaf786ccde759c1f676d39c7bf2855eb0b540e1ac4530c8", - "sha256:9c8bcad7fc12f0cc5896d8e10fdf703c45bd487294a986903fe032c72201596b", - "sha256:9cd136184dd5f58892f24001cdce986f5d7e96059d004118d5410671579834a4", - "sha256:9eb4fa13c8c7a2404b6a8e3772c17a55b1ba18bc711e25e4d6c0c9f5f541b02a", - "sha256:a2bc4e49e8329f3283d99840c136ff2cd1a29e49b5624a46a290f04dff48e079", - "sha256:a5645cd45f56895cfe3ca3459aed9ff2d3f9aaa29ff7edf557fa7a23515a3725", - "sha256:a9afbc3909d0274d6ac8ec891e30210563b2c8bdd52ebbda14146354e7a69373", - "sha256:aa498d1392216fae47eaf10c593e06c34476ced9549657fca713d0d1ba5f7248", - "sha256:afd776cf1ebfc7f9aa42a09cf19feadb40a26366802d86c1fba080d8e5e74bdd", - "sha256:b335a7c958bc945e10c522c069cd6e5804f4ff20f9a744dd38e748eb602cbbda", - "sha256:b3c4817dff8cef5697f5afe5fec6bc1783994d55a68391be24cb7d80d2dbc3a6", - "sha256:b79ee64d01d05a5476d5cceb3c27b5535e6bb84ee0f872ba60d9a8cd4d0e6579", - "sha256:b87a90f14c68c925817423b0424381f0e16d80fc9a1a1046ef202ab25b19a444", - "sha256:bf89e0e4a30714b357f5d46b6f20e0099d38b30d45fa68ea48589faf5f12f62d", - "sha256:c058b84c3b24812c859300f3b5abf300daa34df20d4d4f42e9652a4d1c48c8a4", - "sha256:c09a6ea87658695e527104cf857c70f79f14e9484605e205217aae0ec27b45fc", - "sha256:c57b8e0841f3fce7b703530ed70c7c36269c6d180ea2e02e36b34cb7288c50c7", - "sha256:c9cea5b756173bb86e2235f2f871b406a9b9d722417ae31e5391ccaef5348f2c", - "sha256:cb39ed598aaf102251483f3e4675c5dd6b289c8142210ef76ba24aae0a8f8aba", - "sha256:e036549ad14f2b414c725349cce0772ea34a7ab008e9cd67f9084e4f371d1f32", - "sha256:e185ea07a99ce8b8edfc788c586c538c4b1351007e614ceb708fd01b095ef33e", - "sha256:e5a4d82bdb4bf1ac1285a68eab02d253ab73355d9f0fe725a97e1e0fa689decb", - "sha256:eae27ad7580529a427cfdd52c87abb2dfb15ce2b7a3e0fc29fbb63e2ed6f8120", - "sha256:ecef029b69843b82048c5b347d8e6049356aa24ed644006c9a9d7098c3bd3bfd", - "sha256:ee3bee874cb1fadee2ff2b79fc9fc808aa638670f28b2145074538d4a6a5028e", - "sha256:f0d3de936b192980209d7b5149e3c98977c3810d401482d05fb6d668d53c1c63", - "sha256:f53c0d6a859b2db58332e0e6a921582a02c1677cc93d4cbb36fdf49709b327b2", - "sha256:f9d57f1b3061b3e21476b0ad5f0397b112b94ace21d1f439f2db472e568178ae" + "sha256:0765e318ee9179b3718c4fd7ba35c434f4dd20332fbc6857a5e8df17719c24d7", + "sha256:0ae7454e1ab1d780aee69fd2aae7d6b8670a581d8847f2d1e0f7ddfbf47e5a22", + "sha256:0b1af8392eb27b372ddb783b317dea0f650241cea5bd29199b22235299ca2e45", + "sha256:0fe3917059c7ab2ee3f35e77757062b1bea10a0b6ca633c58391e3f3c6c488dd", + "sha256:119dc41e7a7defcefc57189cfa0e61b1bf9c228211aba432b53fb71ef367fda1", + "sha256:11bac86b0deada30b6b5f93382712ff0e911fe8d31cb9bf46e6b149ae175eff0", + "sha256:15f3326f7f0b2bfe406ee562e17f43f36e16167af99c4c0df61db668de20002d", + "sha256:17835885016b9e4d0135720160db3095dc78c583e7b902b6be799fb21035e749", + "sha256:19de7ca1246fbef9f9d1bff8f1ab25641569df226364a0e40457dc5457c54b05", + "sha256:1df4763760d1de0dfc8192cc96d8aa293eb1a44f8f7a5fbe74caf1b551905c5e", + "sha256:1e77faf6ff919aa8cd63f1c4e561cac1d9a454a191bb864d5dd5e545935e5a40", + "sha256:22be14009339b8bc16d6b9dc8780bacaba3402aa7581658e246114abbd2236e3", + "sha256:253e2f29843fb303eca6b2fc645aca91fa7aa0aa70b38b6950da92d44ff267f3", + "sha256:2b61188657e3a2b9ac4e8f04d6cf8e51046e28175f79464c67f2fd35bceb0976", + "sha256:2bf4bb6b3d6228fcf3a71b50231199fb94d2dd2611b66d33be0578ea3e6c2726", + "sha256:2e7b5b079055e02d06a4308d0481658e4f06bc7ef211567edc8f7d5dce52018d", + "sha256:2f19644f27c76f07e10603580a47278abb2a70311136a7f8fd27dc2e096b9013", + "sha256:2fc44e5965ea46909a416fff0af48a219faefd5773ab79e5f8a5fcd5d62b2667", + "sha256:2fcc4901a86ed81dc76703f3b93ff881e08761c63263c46991081fd7f034b165", + "sha256:3255d821ee91bdf824795e936642bbf43a4c7cedf5d1aed8d24524e66843aa74", + "sha256:329aa42d1be9929603f406186630135be1e7a42569540577ba2c69952b7cf399", + "sha256:357bade0e46064f88f2c3a99808233e67b0051cdddf82992379559322dfeb183", + "sha256:3caef1ff89b1caefc28f0368b3bde21a7e3e630c2eddac16abd9e47bd27cc36a", + "sha256:3cf6872a23601672d61a68f390e44703442639a12ee9dd5a88bbce52a695e46e", + "sha256:3fe166c7d00912e8c10d3a9a0ce105569a31a3d0db1a6e82c4e0f4bf16d5eca9", + "sha256:471733aabb2e4848d609141a9e9d56a427c0a038f4abf65dd19d7a21fd563632", + "sha256:4848395d932e93c1595e59a8672aa7400e8922c39bb9b0668ed99ac6fa867822", + "sha256:48bf7d383a35e668b984c805470518b635d48b95a3c57cb03f37eaa3551b5f9f", + "sha256:4c26ef74ba842d61635b0152763d057c8d48215d5be9bb8b7604116a059e9985", + "sha256:4d18cd0e9a0f37c9f4088e50e3839fcb69a380a0ec957408e0b57cff08ee0a26", + "sha256:585c0c852a891450edbb1eaca8648408a3cc125f18cf433941fa6babcc359e29", + "sha256:70e03833faca7166e6a9927fbee7c27e6ecde436774cd0b24bbcc96353bce06b", + "sha256:72fea91746b5890f9e5e0997f16cbf3d53550580d76355ba2d998311b17b2250", + "sha256:78e6c137ba35476adb5432103ae1534f2f5295605201d946a4198a0dea4b38e7", + "sha256:7a8694107eb4308a13b425ca8c0e67112f8134c846b6e1f722698708741215d5", + "sha256:7c77f3080674fc529b1bd99489378c7f63fcb4ba7f8322b79732e0258f0ea3ce", + "sha256:7cbcb47fd66ab294703e1644f78971f6f2f1126424d2b300678f419aa73c7b6e", + "sha256:846541e58b9a81cce7dee8329f352c318de25aa2f2bbe1e31587eb1f057448b4", + "sha256:8e0e4e66fd80f277a8c3de016a81a554e76ccf6b8d881ee0b53200305a8433f6", + "sha256:9919e77403a483ab81e3423151e8ffc9dd992c20d2603bf17e4a8161111e55f5", + "sha256:9b94843a102efa9ac68a7a30cd46df3ff1ed9c658100d30a725d10d9c60a2f44", + "sha256:9e9018544ab07614d591a26c1bd4293ddf40752cc435caf69196740516af7100", + "sha256:b87e7b91a5d5973dda5f00cd61ef72ad75a1db73a386b62877d4875a8840959c", + "sha256:c1c80faaee1a6c3428cecf40d16a2365bcf56c424c92c2b6f0f9ad204b899e9e", + "sha256:c3678a0fb72c8a6a29422b2732fe423db3ce119c34421b5f9955873eb9b62c1e", + "sha256:cbe4f85f50c656d753890f39468fcd8190c5f08282caf19219f684225bfd5fd2", + "sha256:cc2856d24afa44295735e72f3c75d6ee7fdd4336d8d3a8f3d44de7aa6b766df2", + "sha256:d733dec0614bb8f4bcb7c8af88172b974f685a31dc3a65cca0527e3120de5606", + "sha256:dc8b3850d2a601ca2320d081874033684e246d28e1c5e89db0864077cfc8f5a9", + "sha256:de4387a354ff230bc979b46b2207af841dc8bf29847b6c7dbe60af186d97aefa", + "sha256:e998cf7c29473bd077704cea3577d23123094311f59bdc4af551923b168332b1", + "sha256:ebac3f0b5732014a126b43c2b7567f2f0e0afea7d9119a3378bde46d3dcad88e", + "sha256:ee51625c2d51f8baadf2829fae817ad0b66b140573939dd69284d2ba3553ae73", + "sha256:f4a172b31785e2f00780eccab00bc240ccdbfdb8345f1e6063175b3ff12ad1b0", + "sha256:f7027414f2b88992877573ab780c19ecb54d3a536bef3397933573d6b5068be4", + "sha256:f9480c0740aabd8cb29c329b422fb65358049840b34aba0adf63162371d2a96e", + "sha256:ff486e183d151e51b1d694c7aa1695747599bb00b9f5f604092b54b74c64a8e1" ], "index": "pypi", - "version": "==2.0.38" + "markers": "python_version >= '3.7'", + "version": "==2.0.44" }, "typing-extensions": { "hashes": [ - "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", - "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" + "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", + "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548" ], "index": "pypi", - "version": "==4.12.2" + "markers": "python_version >= '3.9'", + "version": "==4.15.0" }, "urllib3": { "hashes": [ - "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", - "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d" + "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", + "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc" ], "markers": "python_version >= '3.9'", - "version": "==2.3.0" + "version": "==2.5.0" }, "werkzeug": { "hashes": [ @@ -649,6 +658,7 @@ "sha256:f8d76180d7239c94c6322f7990ae1216dae3659b7aa1cee94b6318bdffb474b9" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==3.1.2" } }, diff --git a/src/api/admin.py b/src/api/admin.py index e5b5ea92af..6d40d72dc9 100644 --- a/src/api/admin.py +++ b/src/api/admin.py @@ -7,7 +7,7 @@ def setup_admin(app): app.secret_key = os.environ.get('FLASK_APP_KEY', 'sample key') app.config['FLASK_ADMIN_SWATCH'] = 'cerulean' - admin = Admin(app, name='4Geeks Admin', template_mode='bootstrap3') + admin = Admin(app, name='4Geeks Admin')# template_mode='bootstrap3') # Add your models here, for example this is how we add a the User model to the admin diff --git a/src/api/models.py b/src/api/models.py index 29f9a38cf8..c694f39063 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -10,7 +10,7 @@ class User(db.Model): __tablename__ = 'user' id: Mapped[int] = mapped_column(primary_key=True) - role: Mapped[bool] = mapped_column(Boolean()) + role: Mapped[bool] = mapped_column(Boolean(), nullable=True) nickname: Mapped[str] = mapped_column(unique=True, nullable=True) nombre: Mapped[str] = mapped_column(nullable=True) apellido: Mapped[str] = mapped_column(nullable=True) diff --git a/src/api/routes.py b/src/api/routes.py index 9b0e96f697..f7a5b7b44a 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -32,7 +32,6 @@ def handle_register(): new_user = User(email=body['email'], password=hashed_password, - role=body['role'], is_active=True) db.session.add(new_user) diff --git a/src/app.py b/src/app.py index 1b3340c0fa..6a6bdc56bc 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 @@ -19,6 +20,8 @@ app = Flask(__name__) app.url_map.strict_slashes = False +jwt = JWTManager(app) + # database condiguration db_url = os.getenv("DATABASE_URL") if db_url is not None: diff --git a/src/front/components/Register.jsx b/src/front/components/Register.jsx index 1277cfddc7..fe88f2d3b5 100644 --- a/src/front/components/Register.jsx +++ b/src/front/components/Register.jsx @@ -1,6 +1,9 @@ import React from "react"; -import { Link } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import "./Register.css"; +import useGlobalReducer from "../hooks/useGlobalReducer"; +import { useState } from "react"; +import AuthServices from "../services/auth.services"; const Register = () => { const {store, dispatch} = useGlobalReducer() @@ -8,6 +11,7 @@ const Register = () => { email: '', password: '' }) + const navigate = useNavigate() const handleChange = e => { const {value, name} = e.target; @@ -17,9 +21,10 @@ const Register = () => { const handleSubmit = e => { e.preventDefault(); - AuthServices.login(formData).then(data => { + AuthServices.register(formData).then(data => { console.log(data) dispatch({type: 'register', payload: data.user}) + navigate('/login') }) } @@ -41,6 +46,7 @@ const Register = () => { placeholder="tuemail@ejemplo.com" required value={formData.email} + name="email" onChange={handleChange} style={{ borderColor: "#a00" }} /> @@ -57,16 +63,17 @@ const Register = () => { placeholder="contraseña" required value={formData.password} + name="password" onChange={handleChange} style={{ borderColor: "#a00" }} />
    - - + diff --git a/src/front/pages/RegisterPage.jsx b/src/front/pages/RegisterPage.jsx index 245ef31c04..351f383d4c 100644 --- a/src/front/pages/RegisterPage.jsx +++ b/src/front/pages/RegisterPage.jsx @@ -1,5 +1,5 @@ import React from "react"; -import { Register } from "../components/Register"; +import Register from "../components/Register"; export const RegisterPage = () => { diff --git a/src/front/routes.jsx b/src/front/routes.jsx index 9abb102542..f4995c9c6c 100644 --- a/src/front/routes.jsx +++ b/src/front/routes.jsx @@ -28,7 +28,7 @@ export const router = createBrowserRouter( } /> {/* Dynamic route for single items */} } /> } /> - }/> + }/> ) ); \ No newline at end of file diff --git a/src/front/services/auth.services.jsx b/src/front/services/auth.services.jsx index 54c2f14d1b..46fca76194 100644 --- a/src/front/services/auth.services.jsx +++ b/src/front/services/auth.services.jsx @@ -36,8 +36,8 @@ AuthServices.register = async (formData) => { }) if (!resp.ok) throw new Error('error logging in') - const data = resp.json() - return data.success + const data = await resp.json() + return data } catch (error) { console.log(error) diff --git a/src/front/store.js b/src/front/store.js index d0131cd777..d95c759afb 100644 --- a/src/front/store.js +++ b/src/front/store.js @@ -1,5 +1,6 @@ export const initialStore=()=>{ return{ + user: null, message: null, todos: [ { @@ -23,6 +24,11 @@ export default function storeReducer(store, action = {}) { ...store, user: action.payload } + case "register": + return { + ...store, + user: action.payload + } case "logout": return { ...store, @@ -45,5 +51,6 @@ export default function storeReducer(store, action = {}) { }; default: throw Error('Unknown action.'); + } } \ No newline at end of file From d3b1d4243b3c06ae4931fe0df16a93b238acc1a0 Mon Sep 17 00:00:00 2001 From: Hun73R717 Date: Fri, 31 Oct 2025 19:47:34 +0000 Subject: [PATCH 25/27] edit creado --- aa | 5 + src/front/components/CreateEditClient.jsx | 172 ++++++++++++++++++++++ src/front/components/Login.jsx | 8 +- src/front/pages/CreateEditClient.jsx | 10 ++ src/front/routes.jsx | 4 +- 5 files changed, 194 insertions(+), 5 deletions(-) create mode 100644 aa create mode 100644 src/front/components/CreateEditClient.jsx create mode 100644 src/front/pages/CreateEditClient.jsx diff --git a/aa b/aa new file mode 100644 index 0000000000..6ed19a0134 --- /dev/null +++ b/aa @@ -0,0 +1,5 @@ +* develop + login + main + navbar + register diff --git a/src/front/components/CreateEditClient.jsx b/src/front/components/CreateEditClient.jsx new file mode 100644 index 0000000000..e11c775a2a --- /dev/null +++ b/src/front/components/CreateEditClient.jsx @@ -0,0 +1,172 @@ +import React from "react"; + +import logo from "../assets/img/Logo.png" + +export const CreateEditClient = () => { + + return ( + <> +
    +
    +
    +

    🧾 Crear Cliente

    +
    + +
    +
    + {/* --- Datos personales --- */} +
    Datos Personales
    +
    +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    +
    + + {/* --- Dirección --- */} +
    Dirección
    +
    +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    +
    + + {/* --- Empresa y estado --- */} +
    Información Adicional
    +
    +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + +