From f3221c8458c70c914cea96c610c0137a8baf397f Mon Sep 17 00:00:00 2001 From: Rafael Mata <133695535+rafataxxx@users.noreply.github.com> Date: Mon, 11 May 2026 21:17:30 -0400 Subject: [PATCH 01/13] backend migrado exitosamente con rutas de auth y jwt --- src/api/models.py | 37 ++++++-- src/api/routes.py | 70 +++++++++++--- src/app.py | 20 ++-- src/instance/mascotas.db | Bin 0 -> 24576 bytes src/migrations/README | 1 + src/migrations/alembic.ini | 50 ++++++++++ src/migrations/env.py | 113 +++++++++++++++++++++++ src/migrations/script.py.mako | 24 +++++ src/migrations/versions/52f6e8fdf17f_.py | 47 ++++++++++ 9 files changed, 330 insertions(+), 32 deletions(-) create mode 100644 src/instance/mascotas.db create mode 100644 src/migrations/README create mode 100644 src/migrations/alembic.ini create mode 100644 src/migrations/env.py create mode 100644 src/migrations/script.py.mako create mode 100644 src/migrations/versions/52f6e8fdf17f_.py diff --git a/src/api/models.py b/src/api/models.py index da515f6a1a..d59be01292 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -1,19 +1,40 @@ from flask_sqlalchemy import SQLAlchemy -from sqlalchemy import String, Boolean -from sqlalchemy.orm import Mapped, mapped_column db = SQLAlchemy() class User(db.Model): - id: Mapped[int] = mapped_column(primary_key=True) - email: Mapped[str] = mapped_column(String(120), unique=True, nullable=False) - password: Mapped[str] = mapped_column(nullable=False) - is_active: Mapped[bool] = mapped_column(Boolean(), nullable=False) - + id = db.Column(db.Integer, primary_key=True) + email = db.Column(db.String(120), unique=True, nullable=False) + password = db.Column(db.String(255), nullable=False) + is_active = db.Column(db.Boolean(), default=True) + + pets = db.relationship('Pet', backref='owner', lazy=True) def serialize(self): return { "id": self.id, "email": self.email, - # do not serialize the password, its a security breach + "is_active": self.is_active + # ¡Nunca serialices la contraseña por seguridad! + } + +class Pet(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(100), nullable=False) + breed = db.Column(db.String(100)) + clinical_info = db.Column(db.Text) + photo_url = db.Column(db.String(255)) + qr_code_url = db.Column(db.String(255)) + + owner_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + + def serialize(self): + return { + "id": self.id, + "name": self.name, + "breed": self.breed, + "clinical_info": self.clinical_info, + "photo_url": self.photo_url, + "qr_code_url": self.qr_code_url, + "owner_id": self.owner_id } \ No newline at end of file diff --git a/src/api/routes.py b/src/api/routes.py index 029589a3a1..41e3b686fd 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -1,22 +1,62 @@ -""" -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 -from flask_cors import CORS +from flask import request, jsonify, Blueprint +from api.models import db, User, Pet +# ¡Agrupamos todo lo de JWT aquí arriba! +from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity +from werkzeug.security import generate_password_hash, check_password_hash api = Blueprint('api', __name__) -# Allow CORS requests to this API -CORS(api) +@api.route('/signup', methods=['POST']) +def signup(): + body = request.get_json() + email = body.get('email') + password = body.get('password') + user_exists = User.query.filter_by(email=email).first() + if user_exists: + return jsonify({"msg": "Ese email ya está registrado"}), 400 + + if not email or not password: + return jsonify({"msg": "El email y la contraseña son obligatorios"}), 400 -@api.route('/hello', methods=['POST', 'GET']) -def handle_hello(): + # Usamos la herramienta nativa de Flask para encriptar + hashed_password = generate_password_hash(password) - response_body = { - "message": "Hello! I'm a message that came from the backend, check the network tab on the google inspector and you will see the GET request" - } + new_user = User(email=email, password=hashed_password) + db.session.add(new_user) + db.session.commit() - return jsonify(response_body), 200 + return jsonify({"msg": "Usuario creado exitosamente"}), 201 + +@api.route('/login', methods=['POST']) +def login(): + body = request.get_json() + email = body.get('email') + password = body.get('password') + + if not email or not password: + return jsonify({"msg": "Faltan el email o la contraseña"}), 400 + + user = User.query.filter_by(email=email).first() + + # Verificamos con la herramienta nativa + if not user or not check_password_hash(user.password, password): + return jsonify({"msg": "Email o contraseña incorrectos"}), 401 + + # Creamos el Token + access_token = create_access_token(identity=str(user.id)) + + return jsonify({"access_token": access_token}), 200 + +@api.route('/profile', methods=['GET']) +@jwt_required() # <--- El candado +def get_profile(): + # El token nos dice quién es el usuario + user_id = get_jwt_identity() + user = User.query.get(user_id) + + return jsonify({"msg": f"Hola {user.email}, bienvenido a tu panel seguro"}), 200 + +@api.route('/hello', methods=['GET']) +def home(): + return jsonify({"msg": "Servidor de Mascota Activo"}), 200 \ No newline at end of file diff --git a/src/app.py b/src/app.py index 1b3340c0fa..45fa980e71 100644 --- a/src/app.py +++ b/src/app.py @@ -10,14 +10,21 @@ 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 +# 1. INICIALIZAMOS LA APP UNA SOLA VEZ +app = Flask(__name__) +app.url_map.strict_slashes = False + +# 2. CONFIGURAMOS JWT +app.config['JWT_SECRET_KEY'] = os.getenv('JWT_SECRET_KEY') +jwt = JWTManager(app) + ENV = "development" if os.getenv("FLASK_DEBUG") == "1" else "production" static_file_dir = os.path.join(os.path.dirname( os.path.realpath(__file__)), '../dist/') -app = Flask(__name__) -app.url_map.strict_slashes = False # database condiguration db_url = os.getenv("DATABASE_URL") @@ -34,22 +41,18 @@ # add the admin setup_admin(app) -# add the admin +# add the commands setup_commands(app) # Add all endpoints form the API with a "api" prefix app.register_blueprint(api, url_prefix='/api') # Handle/serialize errors like a JSON object - - @app.errorhandler(APIException) def handle_invalid_usage(error): return jsonify(error.to_dict()), error.status_code # generate sitemap with all your endpoints - - @app.route('/') def sitemap(): if ENV == "development": @@ -65,8 +68,7 @@ def serve_any_other_file(path): response.cache_control.max_age = 0 # avoid cache memory return response - # this only runs if `$ python src/main.py` is executed if __name__ == '__main__': PORT = int(os.environ.get('PORT', 3001)) - app.run(host='0.0.0.0', port=PORT, debug=True) + app.run(host='0.0.0.0', port=PORT, debug=True) \ No newline at end of file diff --git a/src/instance/mascotas.db b/src/instance/mascotas.db new file mode 100644 index 0000000000000000000000000000000000000000..c51b0cea692277c9e33f8dd500e9e5fbcb564014 GIT binary patch literal 24576 zcmeI)O>dh>7zc1jVrOkBaiwnL!)m2Dw7a%aRSXy~>8@02k*s%JyCHVfoX`M+7OgQZ zkiHxf_0(_F@6$t%d)(s=PSeI#e6AY(M_>>0%#3+{a2X@}{n$|RJN`T#Jha0zOGu49=`u^r}3Xu+4(_g>@+^?{<8Z~R8b%R0SG_< z0uX=z1Rwwb2z)bvi=Qgn&AWHycRv@*pYk~`#8&OOjy-nB*nTo_h-e_KdMyq~e>irI z9G477V=|l!1`kNRHe(4V&uzE&hwZkgqO_Nye#UvY6fVaE(|8sKZ0g0cC?#X(#kg#p z|D6`8ca}}R^jCGgT}EDJUXX_T##s7##xrlt$vt!XPi^;<{N?=~4^patmdn_p8SDZWi3Om^LT$G0!R}vcv1Rwwb2tWV=5P$##AOHafKmY=_L||9mxUZ{`!MjlyQ8TKGys-YiB^MWI zKmY;|fB*y_009U<00Izz00ce@G&b+ouCMAm z^ZuW2?kSe9=s~1}yc0#t&{W2KwaYslQw`NfHS})B2%?T=s;b3Jh0{RmhRmXYZdksm zC_42c8c}BY%u)@_R3e>HMGce=^G(waLTc$u{FFjh#W-$Qp%xjI-i-oF%%^D9<@5hf zQsdLw3=x0;1Rwwb2tWV=5P$##AOHafK;SzOXm08^enU|{{}+GyhXMfzKmY;|fB*y_ Q009U<00Izzz%3T|AF3iW_5c6? literal 0 HcmV?d00001 diff --git a/src/migrations/README b/src/migrations/README new file mode 100644 index 0000000000..0e04844159 --- /dev/null +++ b/src/migrations/README @@ -0,0 +1 @@ +Single-database configuration for Flask. diff --git a/src/migrations/alembic.ini b/src/migrations/alembic.ini new file mode 100644 index 0000000000..ec9d45c26a --- /dev/null +++ b/src/migrations/alembic.ini @@ -0,0 +1,50 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic,flask_migrate + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[logger_flask_migrate] +level = INFO +handlers = +qualname = flask_migrate + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/src/migrations/env.py b/src/migrations/env.py new file mode 100644 index 0000000000..4c9709271b --- /dev/null +++ b/src/migrations/env.py @@ -0,0 +1,113 @@ +import logging +from logging.config import fileConfig + +from flask import current_app + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + + +def get_engine(): + try: + # this works with Flask-SQLAlchemy<3 and Alchemical + return current_app.extensions['migrate'].db.get_engine() + except (TypeError, AttributeError): + # this works with Flask-SQLAlchemy>=3 + return current_app.extensions['migrate'].db.engine + + +def get_engine_url(): + try: + return get_engine().url.render_as_string(hide_password=False).replace( + '%', '%%') + except AttributeError: + return str(get_engine().url).replace('%', '%%') + + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +config.set_main_option('sqlalchemy.url', get_engine_url()) +target_db = current_app.extensions['migrate'].db + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def get_metadata(): + if hasattr(target_db, 'metadatas'): + return target_db.metadatas[None] + return target_db.metadata + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=get_metadata(), literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + conf_args = current_app.extensions['migrate'].configure_args + if conf_args.get("process_revision_directives") is None: + conf_args["process_revision_directives"] = process_revision_directives + + connectable = get_engine() + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=get_metadata(), + **conf_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/src/migrations/script.py.mako b/src/migrations/script.py.mako new file mode 100644 index 0000000000..2c0156303a --- /dev/null +++ b/src/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/src/migrations/versions/52f6e8fdf17f_.py b/src/migrations/versions/52f6e8fdf17f_.py new file mode 100644 index 0000000000..069ee509b0 --- /dev/null +++ b/src/migrations/versions/52f6e8fdf17f_.py @@ -0,0 +1,47 @@ +"""empty message + +Revision ID: 52f6e8fdf17f +Revises: +Create Date: 2026-05-11 21:08:47.619362 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '52f6e8fdf17f' +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(length=255), nullable=False), + sa.Column('is_active', sa.Boolean(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('email') + ) + op.create_table('pet', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=100), nullable=False), + sa.Column('breed', sa.String(length=100), nullable=True), + sa.Column('clinical_info', sa.Text(), nullable=True), + sa.Column('photo_url', sa.String(length=255), nullable=True), + sa.Column('qr_code_url', sa.String(length=255), nullable=True), + sa.Column('owner_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['owner_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('pet') + op.drop_table('user') + # ### end Alembic commands ### From 8317fed0d7583a5f09b7785eeaede8a6cb29fd72 Mon Sep 17 00:00:00 2001 From: Matheo Flores Loor Date: Mon, 11 May 2026 21:53:37 -0500 Subject: [PATCH 02/13] Parte de Matheo migrada al nuevo repo --- src/front/App.jsx | 22 +++++++++ src/front/assets/img/rigo-baby.jpg | Bin 31327 -> 0 bytes src/front/components/BackendURL.jsx | 23 --------- src/front/components/ComoFunciona.jsx | 45 +++++++++++++++++ src/front/components/Footer.jsx | 11 ----- src/front/components/Hero.jsx | 28 +++++++++++ src/front/components/Navbar.jsx | 42 ++++++++++------ src/front/components/ScrollToTop.jsx | 26 ---------- src/front/hooks/useGlobalReducer.jsx | 24 --------- src/front/index.css | 0 src/front/main.jsx | 37 ++++---------- src/front/pages/Demo.jsx | 43 ----------------- src/front/pages/Home.jsx | 67 ++++++-------------------- src/front/pages/Layout.jsx | 15 ------ src/front/pages/Login.jsx | 41 ++++++++++++++++ src/front/pages/Single.jsx | 37 -------------- 16 files changed, 186 insertions(+), 275 deletions(-) create mode 100644 src/front/App.jsx delete mode 100644 src/front/assets/img/rigo-baby.jpg delete mode 100644 src/front/components/BackendURL.jsx create mode 100644 src/front/components/ComoFunciona.jsx delete mode 100644 src/front/components/Footer.jsx create mode 100644 src/front/components/Hero.jsx delete mode 100644 src/front/components/ScrollToTop.jsx delete mode 100644 src/front/hooks/useGlobalReducer.jsx delete mode 100644 src/front/index.css delete mode 100644 src/front/pages/Demo.jsx delete mode 100644 src/front/pages/Layout.jsx create mode 100644 src/front/pages/Login.jsx delete mode 100644 src/front/pages/Single.jsx diff --git a/src/front/App.jsx b/src/front/App.jsx new file mode 100644 index 0000000000..7e626ce443 --- /dev/null +++ b/src/front/App.jsx @@ -0,0 +1,22 @@ +import { BrowserRouter, Route, Routes } from "react-router-dom"; + +//Importamos las paginas +import Home from "./pages/Home"; +import Login from "./pages/Login"; +import RegistrerQr from "./components/RegisterQR/RegistrerQr"; +import File from "./components/File/File" + +function App() { + return ( + + + } /> + } /> + } /> + } /> + + + ); +} + +export default App; diff --git a/src/front/assets/img/rigo-baby.jpg b/src/front/assets/img/rigo-baby.jpg deleted file mode 100644 index da566a74a07f685e99494fd9890b45cdb02bbb2d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31327 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 Eg0VoU&;S4c diff --git a/src/front/components/BackendURL.jsx b/src/front/components/BackendURL.jsx deleted file mode 100644 index ae80b8e43a..0000000000 --- a/src/front/components/BackendURL.jsx +++ /dev/null @@ -1,23 +0,0 @@ -import React, { Component } from "react"; -import envFile from "../../../docs/assets/env-file.png" - -const Dark = ({children}) => {children}; -export const BackendURL = () => ( -
-

Missing BACKEND_URL env variable

-

Here's a video tutorial on how to update your backend URL environment variable.

-

There's a file called .env that contains the environmental variables for your project.

-

There's one variable called BACKEND_URL that needs to be manually set by yourself.

-
    -
  1. Make sure you backend is running on port 3001.
  2. -
  3. Open your API and copy the API host.
  4. -
  5. Open the .env file (do not open the .env.example)
  6. -
  7. Add a new variable VITE_BACKEND_URL=your api host
  8. -
  9. Replace your api host with the public API URL of your flask backend sever running at port 3001
  10. -
-
- -
-

Note: If you are publishing your website to Heroku, Render.com or any other hosting you probably need to follow other steps.

-
-); \ No newline at end of file diff --git a/src/front/components/ComoFunciona.jsx b/src/front/components/ComoFunciona.jsx new file mode 100644 index 0000000000..860f4bd636 --- /dev/null +++ b/src/front/components/ComoFunciona.jsx @@ -0,0 +1,45 @@ +function ComoFunciona() { + return ( +
+
+

Cómo funciona

+ +
+
+
+

1. Regístrate

+ +

+ Crea una cuenta y agrega la información de tu mascota. +

+
+
+ + +
+
+

2. Genera el QR

+ +

+ Genera un código QR único vinculado al perfil de tu mascota. +

+
+
+ +
+
+

3. Escanea e identifica

+ +

+ Cualquier persona puede escanear el código QR para acceder al + perfil de tu mascota. +

+
+
+
+
+
+ ); +} + +export default ComoFunciona; diff --git a/src/front/components/Footer.jsx b/src/front/components/Footer.jsx deleted file mode 100644 index f06302dbd2..0000000000 --- a/src/front/components/Footer.jsx +++ /dev/null @@ -1,11 +0,0 @@ -export const Footer = () => ( - -); diff --git a/src/front/components/Hero.jsx b/src/front/components/Hero.jsx new file mode 100644 index 0000000000..300b032ff5 --- /dev/null +++ b/src/front/components/Hero.jsx @@ -0,0 +1,28 @@ +import { Link } from "react-router-dom"; + +function Hero() { + return ( +
+
+

+ Identificación inteligente con QR +
+ para tus mascotas +

+ +

+ Crea un perfil público para tu mascota y ayuda a que las personas la + identifiquen al instante usando tecnología QR. +

+ + + + +
+
+ ); +} + +export default Hero; diff --git a/src/front/components/Navbar.jsx b/src/front/components/Navbar.jsx index 30d43a2636..89a60c3229 100644 --- a/src/front/components/Navbar.jsx +++ b/src/front/components/Navbar.jsx @@ -1,19 +1,29 @@ import { Link } from "react-router-dom"; -export const Navbar = () => { +function Navbar() { + return ( + + ); +} + +export default Navbar; diff --git a/src/front/components/ScrollToTop.jsx b/src/front/components/ScrollToTop.jsx deleted file mode 100644 index fe79dc5be2..0000000000 --- a/src/front/components/ScrollToTop.jsx +++ /dev/null @@ -1,26 +0,0 @@ -import { useEffect, useRef } from "react"; -import PropTypes from "prop-types"; - -// This component allows the scroll to go to the beginning when changing the view, -// otherwise it would remain in the position of the previous view. -// Investigate more about this React behavior :D - -const ScrollToTop = ({ location, children }) => { - const prevLocation = useRef(location); - - useEffect(() => { - if (location !== prevLocation.current) { - window.scrollTo(0, 0); - } - prevLocation.current = location; - }, [location]); - - return children; -}; - -export default ScrollToTop; - -ScrollToTop.propTypes = { - location: PropTypes.object, - children: PropTypes.any -}; \ No newline at end of file diff --git a/src/front/hooks/useGlobalReducer.jsx b/src/front/hooks/useGlobalReducer.jsx deleted file mode 100644 index 6aeb9d768e..0000000000 --- a/src/front/hooks/useGlobalReducer.jsx +++ /dev/null @@ -1,24 +0,0 @@ -// Import necessary hooks and functions from React. -import { useContext, useReducer, createContext } from "react"; -import storeReducer, { initialStore } from "../store" // Import the reducer and the initial state. - -// Create a context to hold the global state of the application -// We will call this global state the "store" to avoid confusion while using local states -const StoreContext = createContext() - -// Define a provider component that encapsulates the store and warps it in a context provider to -// broadcast the information throught all the app pages and components. -export function StoreProvider({ children }) { - // Initialize reducer with the initial state. - const [store, dispatch] = useReducer(storeReducer, initialStore()) - // Provide the store and dispatch method to all child components. - return - {children} - -} - -// Custom hook to access the global state and dispatch function. -export default function useGlobalReducer() { - const { dispatch, store } = useContext(StoreContext) - return { dispatch, store }; -} \ No newline at end of file diff --git a/src/front/index.css b/src/front/index.css deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/front/main.jsx b/src/front/main.jsx index a5a3c781dc..d25fc0bd35 100644 --- a/src/front/main.jsx +++ b/src/front/main.jsx @@ -1,29 +1,10 @@ -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 React from "react"; +import ReactDOM from "react-dom/client"; +import "bootstrap/dist/css/bootstrap.min.css"; +import App from "./App"; -const Main = () => { - - if(! import.meta.env.VITE_BACKEND_URL || import.meta.env.VITE_BACKEND_URL == "") return ( - - - - ); - return ( - - {/* Provide global state to all components */} - - {/* Set up routing for the application */} - - - - - ); -} - -// Render the Main component into the root DOM element. -ReactDOM.createRoot(document.getElementById('root')).render(
) +ReactDOM.createRoot(document.getElementById("root")).render( + + + , +); diff --git a/src/front/pages/Demo.jsx b/src/front/pages/Demo.jsx deleted file mode 100644 index 34250a45b7..0000000000 --- a/src/front/pages/Demo.jsx +++ /dev/null @@ -1,43 +0,0 @@ -// Import necessary components from react-router-dom and other parts of the application. -import { Link } from "react-router-dom"; -import useGlobalReducer from "../hooks/useGlobalReducer"; // Custom hook for accessing the global state. - -export const Demo = () => { - // Access the global state and dispatch function using the useGlobalReducer hook. - const { store, dispatch } = useGlobalReducer() - - return ( -
-
    - {/* Map over the 'todos' array from the store and render each item as a list element */} - {store && store.todos?.map((item) => { - return ( -
  • - - {/* Link to the detail page of this todo. */} - Link to: {item.title} - -

    Open file ./store.js to see the global store that contains and updates the list of colors

    - - -
  • - ); - })} -
-
- - - - -
- ); -}; diff --git a/src/front/pages/Home.jsx b/src/front/pages/Home.jsx index 341ed21768..25737b64a8 100644 --- a/src/front/pages/Home.jsx +++ b/src/front/pages/Home.jsx @@ -1,52 +1,15 @@ -import React, { useEffect } from "react" -import rigoImageUrl from "../assets/img/rigo-baby.jpg"; -import useGlobalReducer from "../hooks/useGlobalReducer.jsx"; - -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 +import Navbar from "../components/Navbar"; +import Hero from "../components/Hero"; +import ComoFunciona from "../components/ComoFunciona"; + +function Home() { + return ( + <> + + + + + ); +} + +export default Home; diff --git a/src/front/pages/Layout.jsx b/src/front/pages/Layout.jsx deleted file mode 100644 index 9bfa31325c..0000000000 --- a/src/front/pages/Layout.jsx +++ /dev/null @@ -1,15 +0,0 @@ -import { Outlet } from "react-router-dom/dist" -import ScrollToTop from "../components/ScrollToTop" -import { Navbar } from "../components/Navbar" -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 = () => { - return ( - - - -