From b30979a812d70156047448dd312267ecc6632d60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergi=20Villalobos=20Gasc=C3=B3n?= <56490583+Sergidev@users.noreply.github.com> Date: Fri, 24 Jul 2026 11:08:36 +0200 Subject: [PATCH 01/21] Create gitflow.txt --- gitflow.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 gitflow.txt diff --git a/gitflow.txt b/gitflow.txt new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/gitflow.txt @@ -0,0 +1 @@ + From d73818eae159cc2669054bf60f882e041bb60212 Mon Sep 17 00:00:00 2001 From: Pascual March <97188313+FullPas@users.noreply.github.com> Date: Fri, 24 Jul 2026 09:39:50 +0000 Subject: [PATCH 02/21] cambios --- gitflow.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitflow.txt b/gitflow.txt index 8b13789179..1bf8eeadc2 100644 --- a/gitflow.txt +++ b/gitflow.txt @@ -1 +1 @@ - +Pascual From f28ce7fbe89910e4c6b17ed9ca4fa9d624064c50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergi=20Villalobos=20Gasc=C3=B3n?= <56490583+Sergidev@users.noreply.github.com> Date: Fri, 24 Jul 2026 09:45:03 +0000 Subject: [PATCH 03/21] Add Sergi to gitflow.txt --- gitflow.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitflow.txt b/gitflow.txt index 8b13789179..52a879f641 100644 --- a/gitflow.txt +++ b/gitflow.txt @@ -1 +1 @@ - +Sergi From 3d2aaff0a497fdc54492c548fc828a165277717e Mon Sep 17 00:00:00 2001 From: Nataly-04 Date: Fri, 24 Jul 2026 09:54:19 +0000 Subject: [PATCH 04/21] Nataly cambios --- gitflow.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitflow.txt b/gitflow.txt index 8b13789179..d7b9ce47f6 100644 --- a/gitflow.txt +++ b/gitflow.txt @@ -1 +1 @@ - +Nataly From 456274f046060d55af0d29185cd64b6d1ebe8b36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergi=20Villalobos=20Gasc=C3=B3n?= <56490583+Sergidev@users.noreply.github.com> Date: Fri, 24 Jul 2026 10:01:36 +0000 Subject: [PATCH 05/21] apellido --- gitflow.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/gitflow.txt b/gitflow.txt index 1bf8eeadc2..d3f30ff647 100644 --- a/gitflow.txt +++ b/gitflow.txt @@ -1 +1,2 @@ Pascual +March \ No newline at end of file From b4e81c83c246487d4d7eba31a7862a59d6a1e049 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergi=20Villalobos=20Gasc=C3=B3n?= <56490583+Sergidev@users.noreply.github.com> Date: Fri, 24 Jul 2026 10:20:21 +0000 Subject: [PATCH 06/21] conflicts solved --- gitflow.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gitflow.txt b/gitflow.txt index 112d3e935f..4c0f50c267 100644 --- a/gitflow.txt +++ b/gitflow.txt @@ -2,4 +2,5 @@ Pascual Sergi Nataly Zuluaga -March \ No newline at end of file +March +Villalobos \ No newline at end of file From d4a23c855d1b35a58145a1905771689f3226a336 Mon Sep 17 00:00:00 2001 From: Nataly-04 Date: Fri, 24 Jul 2026 11:26:31 +0000 Subject: [PATCH 07/21] Add breed --- src/api/models.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/api/models.py b/src/api/models.py index da515f6a1a..0cbeb6695c 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -4,16 +4,29 @@ db = SQLAlchemy() -class User(db.Model): + +class user(db.Model): id: Mapped[int] = mapped_column(primary_key=True) - email: Mapped[str] = mapped_column(String(120), unique=True, nullable=False) + 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) - def serialize(self): return { "id": self.id, "email": self.email, # do not serialize the password, its a security breach - } \ No newline at end of file + } + + class Breed(db.Model): + __tablename__ = "breed" + id: Mapped[int] = mapped_column(primary_key=True) + breedName: Mapped[str] = mapped_column( + String(50), unique=True, nullable=False) + + def serialize(self): + return { + "id": self.id, + "breedName": self.breedName + } From cde0912d707c953004eaefa1b0eab8c5445d92ff Mon Sep 17 00:00:00 2001 From: Nataly-04 Date: Fri, 24 Jul 2026 11:31:55 +0000 Subject: [PATCH 08/21] Add routes --- src/api/routes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/routes.py b/src/api/routes.py index 029589a3a1..8d475d771c 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -2,7 +2,7 @@ This module takes care of starting the API Server, Loading the DB and Adding the endpoints """ from flask import Flask, request, jsonify, url_for, Blueprint -from api.models import db, User +from api.models import db, User, Breed from api.utils import generate_sitemap, APIException from flask_cors import CORS From da8cbea9f6f0017cf7b53dd90110503dd96c1f1e Mon Sep 17 00:00:00 2001 From: Nataly-04 Date: Fri, 24 Jul 2026 11:46:37 +0000 Subject: [PATCH 09/21] Add routes-models --- src/api/models.py | 15 +++++++++++++-- src/api/routes.py | 2 +- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/api/models.py b/src/api/models.py index da515f6a1a..41857e4624 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -4,16 +4,27 @@ 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) + 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) - def serialize(self): return { "id": self.id, "email": self.email, # do not serialize the password, its a security breach + } + +class Breed(db.Model): + id: Mapped[int] = mapped_column(primary_key=True) + breedName: Mapped[str] = mapped_column(String(50), unique=True, nullable=False) + + def serialize(self): + return { + "id": self.id, + "breedName": self.breedName } \ No newline at end of file diff --git a/src/api/routes.py b/src/api/routes.py index 029589a3a1..8d475d771c 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -2,7 +2,7 @@ This module takes care of starting the API Server, Loading the DB and Adding the endpoints """ from flask import Flask, request, jsonify, url_for, Blueprint -from api.models import db, User +from api.models import db, User, Breed from api.utils import generate_sitemap, APIException from flask_cors import CORS From 8c37ff46ce47989649e1a312125de163600f3fe0 Mon Sep 17 00:00:00 2001 From: Nataly-04 Date: Fri, 24 Jul 2026 20:33:46 +0000 Subject: [PATCH 10/21] Add CRUD for Breed --- .../versions/0e4bcc15720e_add_breed_table.py | 33 ++++++++ src/api/routes.py | 71 ++++++++++++++++ src/front/pages/Breed.jsx | 83 +++++++++++++++++++ 3 files changed, 187 insertions(+) create mode 100644 migrations/versions/0e4bcc15720e_add_breed_table.py create mode 100644 src/front/pages/Breed.jsx diff --git a/migrations/versions/0e4bcc15720e_add_breed_table.py b/migrations/versions/0e4bcc15720e_add_breed_table.py new file mode 100644 index 0000000000..89fa3bb668 --- /dev/null +++ b/migrations/versions/0e4bcc15720e_add_breed_table.py @@ -0,0 +1,33 @@ +"""add breed table + +Revision ID: 0e4bcc15720e +Revises: 0763d677d453 +Create Date: 2026-07-24 19:23:39.571576 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '0e4bcc15720e' +down_revision = '0763d677d453' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('breed', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('breedName', sa.String(length=50), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('breedName') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('breed') + # ### end Alembic commands ### diff --git a/src/api/routes.py b/src/api/routes.py index 8d475d771c..8380eaf99b 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -20,3 +20,74 @@ def handle_hello(): } return jsonify(response_body), 200 + + +# GET - Obtener todas las razas +@api.route('/breed', methods=['GET']) +def get_breeds(): + breeds = Breed.query.all() + return jsonify([breed.serialize() for breed in breeds]), 200 + + +# GET - Obtener una raza por ID + +@api.route('/breed/', methods=['GET']) +def get_breed(breed_id): + breed = Breed.query.get(breed_id) + + if breed is None: + return jsonify({"message": "Breed not found"}), 404 + + return jsonify(breed.serialize()), 200 + + +# POST - Crear una raza + +@api.route('/breed', methods=['POST']) +def create_breed(): + body = request.get_json() + + if "breedName" not in body: + return jsonify({"message": "breedName is required"}), 400 + + new_breed = Breed( + breedName=body["breedName"] + ) + + db.session.add(new_breed) + db.session.commit() + + return jsonify(new_breed.serialize()), 201 + + +# PUT - Actualizar una raza + +@api.route('/breed/', methods=['PUT']) +def update_breed(breed_id): + breed = Breed.query.get(breed_id) + + if breed is None: + return jsonify({"message": "Breed not found"}), 404 + + body = request.get_json() + + breed.breedName = body.get("breedName", breed.breedName) + + db.session.commit() + + return jsonify(breed.serialize()), 200 + + +# DELETE - Eliminar una raza + +@api.route('/breed/', methods=['DELETE']) +def delete_breed(breed_id): + breed = Breed.query.get(breed_id) + + if breed is None: + return jsonify({"message": "Breed not found"}), 404 + + db.session.delete(breed) + db.session.commit() + + return jsonify({"message": "Breed deleted successfully"}), 200 diff --git a/src/front/pages/Breed.jsx b/src/front/pages/Breed.jsx new file mode 100644 index 0000000000..d18915f3c3 --- /dev/null +++ b/src/front/pages/Breed.jsx @@ -0,0 +1,83 @@ +import React, { useEffect, useState } from "react"; + +const Breed = () => { + const [breeds, setBreeds] = useState([]); + const [breedName, setBreedName] = useState(""); + + const API = process.env.BACKEND_URL + "/api/breed"; + + const getBreeds = async () => { + try { + const response = await fetch(API); + const data = await response.json(); + setBreeds(data); + } catch (error) { + console.log(error); + } + }; + + const createBreed = async () => { + if (!breedName.trim()) return; + + try { + const response = await fetch(API, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + breedName: breedName + }) + }); + + if (response.ok) { + setBreedName(""); + getBreeds(); + } + } catch (error) { + console.log(error); + } + }; + + const updateBreed = async (id, newBreedName) => { + try { + await fetch(`${API}/${id}`, { + method: "PUT", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + breedName: newBreedName + }) + }); + + getBreeds(); + + } catch (error) { + console.log(error); + } + }; + const deleteBreed = async (id) => { + try { + await fetch(`${API}/${id}`, { + method: "DELETE" + }); + + getBreeds(); + + } catch (error) { + console.log(error); + } + }; + + useEffect(() => { + getBreeds(); + }, []); + + return ( +
+

Breed CRUD

+
+ ); +}; +export default Breed; \ No newline at end of file From 2bf28e29d743913d22e494c7548c593708cc2863 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergi=20Villalobos=20Gasc=C3=B3n?= <56490583+Sergidev@users.noreply.github.com> Date: Sat, 25 Jul 2026 20:07:10 +0000 Subject: [PATCH 11/21] CRUD done --- .vscode/settings.json | 17 +- migrations/versions/40947dca7989_.py | 60 ++++++ migrations/versions/61a604619e63_.py | 42 ++++ public/index.html | 36 +++- src/api/models.py | 44 +++- src/api/routes.py | 99 ++++++++- src/app.py | 2 + src/front/pages/Pets.jsx | 291 +++++++++++++++++++++++++++ src/front/routes.jsx | 2 + 9 files changed, 569 insertions(+), 24 deletions(-) create mode 100644 migrations/versions/40947dca7989_.py create mode 100644 migrations/versions/61a604619e63_.py create mode 100644 src/front/pages/Pets.jsx diff --git a/.vscode/settings.json b/.vscode/settings.json index 246b0419d0..232031c800 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,10 +1,11 @@ { - "editor.formatOnSave": true, - "editor.defaultFormatter": "esbenp.prettier-vscode", - "workbench.editorAssociations": { - "*.md": "vscode.markdown.preview.editor" - }, - "[javascriptreact]": { - "editor.defaultFormatter": "vscode.typescript-language-features" - } + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "workbench.editorAssociations": { + "*.md": "vscode.markdown.preview.editor" + }, + "[javascriptreact]": { + "editor.defaultFormatter": "vscode.typescript-language-features" + }, + "python-envs.defaultEnvManager": "ms-python.python:pipenv" } diff --git a/migrations/versions/40947dca7989_.py b/migrations/versions/40947dca7989_.py new file mode 100644 index 0000000000..8531804af9 --- /dev/null +++ b/migrations/versions/40947dca7989_.py @@ -0,0 +1,60 @@ +"""empty message + +Revision ID: 40947dca7989 +Revises: 61a604619e63 +Create Date: 2026-07-25 20:03:08.258031 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '40947dca7989' +down_revision = '61a604619e63' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('pet', schema=None) as batch_op: + batch_op.alter_column('breed_id', + existing_type=sa.INTEGER(), + nullable=True) + batch_op.alter_column('name', + existing_type=sa.VARCHAR(length=20), + type_=sa.String(length=50), + existing_nullable=False) + batch_op.alter_column('chip_number', + existing_type=sa.VARCHAR(length=20), + type_=sa.String(length=50), + existing_nullable=True) + batch_op.alter_column('color', + existing_type=sa.VARCHAR(length=20), + type_=sa.String(length=30), + existing_nullable=False) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('pet', schema=None) as batch_op: + batch_op.alter_column('color', + existing_type=sa.String(length=30), + type_=sa.VARCHAR(length=20), + existing_nullable=False) + batch_op.alter_column('chip_number', + existing_type=sa.String(length=50), + type_=sa.VARCHAR(length=20), + existing_nullable=True) + batch_op.alter_column('name', + existing_type=sa.String(length=50), + type_=sa.VARCHAR(length=20), + existing_nullable=False) + batch_op.alter_column('breed_id', + existing_type=sa.INTEGER(), + nullable=False) + + # ### end Alembic commands ### diff --git a/migrations/versions/61a604619e63_.py b/migrations/versions/61a604619e63_.py new file mode 100644 index 0000000000..d05c10001d --- /dev/null +++ b/migrations/versions/61a604619e63_.py @@ -0,0 +1,42 @@ +"""empty message + +Revision ID: 61a604619e63 +Revises: 0763d677d453 +Create Date: 2026-07-25 19:56:20.540852 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '61a604619e63' +down_revision = '0763d677d453' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('pet', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('shelter_id', sa.Integer(), nullable=True), + sa.Column('breed_id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=20), nullable=False), + sa.Column('genre', sa.String(length=20), nullable=False), + sa.Column('birth_date', sa.Date(), nullable=True), + sa.Column('castrated', sa.Boolean(), nullable=False), + sa.Column('chip_number', sa.String(length=20), nullable=True), + sa.Column('color', sa.String(length=20), nullable=False), + sa.Column('photo_url', sa.String(length=255), nullable=True), + sa.Column('size', sa.String(length=20), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('pet') + # ### end Alembic commands ### diff --git a/public/index.html b/public/index.html index 9462644fe9..72b3abc6a3 100644 --- a/public/index.html +++ b/public/index.html @@ -1 +1,35 @@ -Hello Rigo with Vanilla.js
\ No newline at end of file + + + + + + Hello Rigo with Vanilla.js + + + + + + +
+ + + + diff --git a/src/api/models.py b/src/api/models.py index da515f6a1a..80de3e4d6a 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -1,19 +1,55 @@ from flask_sqlalchemy import SQLAlchemy -from sqlalchemy import String, Boolean +from typing import Optional +from sqlalchemy import String, Boolean, Date, Integer 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) + 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) - def serialize(self): return { "id": self.id, "email": self.email, # do not serialize the password, its a security breach - } \ No newline at end of file + } + + +class Pet(db.Model): + id: Mapped[int] = mapped_column(primary_key=True) + user_id: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) + shelter_id: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) + breed_id: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) + name: Mapped[str] = mapped_column(String(50), nullable=False) + genre: Mapped[str] = mapped_column(String(20), nullable=False) + birth_date: Mapped[Optional[Date]] = mapped_column(Date, nullable=True) + castrated: Mapped[bool] = mapped_column( + Boolean(), nullable=False, default=False) + chip_number: Mapped[Optional[str]] = mapped_column( + String(50), nullable=True) + color: Mapped[str] = mapped_column(String(30), nullable=False) + photo_url: Mapped[Optional[str]] = mapped_column( + String(255), nullable=True) + size: Mapped[str] = mapped_column(String(20), nullable=False) + + def serialize(self): + return { + "id": self.id, + "idUser": self.user_id, + "idShelter": self.shelter_id, + "idBreed": self.breed_id, + "name": self.name, + "genre": self.genre, + "birthDate": self.birth_date, + "castrated": self.castrated, + "chipNumber": self.chip_number, + "color": self.color, + "photoUrl": self.photo_url, + "size": self.size + } diff --git a/src/api/routes.py b/src/api/routes.py index 029589a3a1..210a8a331a 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -1,22 +1,99 @@ """ 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 import Flask, request, jsonify, Blueprint +from api.models import db, Pet +from api.utils import APIException from flask_cors import CORS api = Blueprint('api', __name__) - -# Allow CORS requests to this API CORS(api) -@api.route('/hello', methods=['POST', 'GET']) -def handle_hello(): +@api.route('/pets', methods=['GET']) +def get_pets(): + pets = Pet.query.all() + results = [pet.serialize() for pet in pets] + return jsonify(results), 200 + + +@api.route('/pets', methods=['POST']) +def create_pet(): + body = request.get_json() + + if not body: + raise APIException("You must send a request body", status_code=400) + if not body.get('name'): + raise APIException("Pet name is required", status_code=400) + + user_id = int(body['idUser']) if body.get('idUser') else None + shelter_id = int(body['idShelter']) if body.get('idShelter') else None + breed_id = int(body['idBreed']) if body.get('idBreed') else None + + new_pet = Pet( + user_id=user_id, + shelter_id=shelter_id, + breed_id=breed_id, + name=body.get('name'), + genre=body.get('genre'), + birth_date=body.get('birthDate') if body.get('birthDate') else None, + castrated=body.get('castrated', False), + chip_number=body.get('chipNumber') if body.get('chipNumber') else None, + color=body.get('color'), + photo_url=body.get('photoUrl') if body.get('photoUrl') else None, + size=body.get('size') + ) + + db.session.add(new_pet) + db.session.commit() + + return jsonify({"message": "Pet created successfully", "pet": new_pet.serialize()}), 201 + + +@api.route('/pets/', methods=['PUT']) +def update_pet(pet_id): + body = request.get_json() + pet = db.session.get(Pet, pet_id) + + if pet is None: + raise APIException("Pet not found.", status_code=404) + + if 'name' in body: + pet.name = body['name'] + if 'genre' in body: + pet.genre = body['genre'] + if 'color' in body: + pet.color = body['color'] + if 'size' in body: + pet.size = body['size'] + if 'castrated' in body: + pet.castrated = body['castrated'] + if 'chipNumber' in body: + pet.chip_number = body['chipNumber'] + if 'photoUrl' in body: + pet.photo_url = body['photoUrl'] + if 'idUser' in body: + pet.user_id = body['idUser'] + if 'idShelter' in body: + pet.shelter_id = body['idShelter'] + if 'idBreed' in body: + pet.breed_id = body['idBreed'] + if 'birthDate' in body: + pet.birth_date = body['birthDate'] + + db.session.commit() + + return jsonify({"message": "Pet successfully updated", "pet": pet.serialize()}), 200 + + +@api.route('/pets/', methods=['DELETE']) +def delete_pet(pet_id): + pet = db.session.get(Pet, pet_id) + + if pet is None: + raise APIException("Pet not found", status_code=404) - 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" - } + db.session.delete(pet) + db.session.commit() - return jsonify(response_body), 200 + return jsonify({"message": "Pet successfully deleted"}), 200 diff --git a/src/app.py b/src/app.py index 1b3340c0fa..9774dd4f5e 100644 --- a/src/app.py +++ b/src/app.py @@ -2,6 +2,7 @@ This module takes care of starting the API Server, Loading the DB and Adding the endpoints """ import os +from flask_cors import CORS from flask import Flask, request, jsonify, url_for, send_from_directory from flask_migrate import Migrate from flask_swagger import swagger @@ -17,6 +18,7 @@ static_file_dir = os.path.join(os.path.dirname( os.path.realpath(__file__)), '../dist/') app = Flask(__name__) +CORS(app) app.url_map.strict_slashes = False # database condiguration diff --git a/src/front/pages/Pets.jsx b/src/front/pages/Pets.jsx new file mode 100644 index 0000000000..a4a85631f2 --- /dev/null +++ b/src/front/pages/Pets.jsx @@ -0,0 +1,291 @@ +import React, { useState, useEffect } from "react"; + +export const Pets = () => { + const [pets, setPets] = useState([]); + + const [formData, setFormData] = useState({ + idUser: 1, + idShelter: 1, + idBreed: 1, + name: "", + genre: "male", + birthDate: "", + castrated: false, + chipNumber: "", + color: "", + photoUrl: "", + size: "medium", + idUser: "", + idShelter: "", + idBreed: "" + }); + + const [editingPet, setEditingPet] = useState(null); + + const backendUrl = import.meta.env.VITE_BACKEND_URL; + + const fetchPets = async () => { + try { + const response = await fetch(`${backendUrl}/api/pets`); + if (response.ok) { + const data = await response.json(); + setPets(data); + } + } catch (error) { + console.error("Error fetching pets:", error); + } + }; + + useEffect(() => { + fetchPets(); + }, []); + + const handleChange = (e) => { + const { name, value, type, checked } = e.target; + setFormData({ + ...formData, + [name]: type === "checkbox" ? checked : value + }); + }; + + const resetForm = () => { + setFormData({ + name: "", + genre: "male", + birthDate: "", + castrated: false, + chipNumber: "", + color: "", + photoUrl: "", + size: "medium", + idUser: "", + idShelter: "", + idBreed: "" + }); + }; + + const handleCreate = async (e) => { + e.preventDefault(); + try { + const response = await fetch(`${backendUrl}/api/pets`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(formData) + }); + + if (response.ok) { + resetForm(); + fetchPets(); + } + } catch (error) { + console.error("Error creating pet:", error); + } + }; + + const handleDelete = async (id) => { + if (!window.confirm("Are you sure you want to delete this pet?")) return; + + try { + const response = await fetch(`${backendUrl}/api/pets/${id}`, { + method: "DELETE" + }); + + if (response.ok) { + fetchPets(); + } + } catch (error) { + console.error("Error deleting pet:", error); + } + }; + + const handleOpenEdit = (pet) => { + setEditingPet(pet); + setFormData({ + name: pet.name || "", + genre: pet.genre || "male", + birthDate: pet.birthDate || "", + castrated: pet.castrated || false, + chipNumber: pet.chipNumber || "", + color: pet.color || "", + photoUrl: pet.photoUrl || "", + size: pet.size || "medium", + idUser: pet.idUser || "", + idShelter: pet.idShelter || "", + idBreed: pet.idBreed || "" + }); + }; + + const handleUpdate = async (e) => { + e.preventDefault(); + try { + const response = await fetch(`${backendUrl}/api/pets/${editingPet.id}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(formData) + }); + + if (response.ok) { + setEditingPet(null); + resetForm(); + fetchPets(); + } + } catch (error) { + console.error("Error updating pet:", error); + } + }; + + if (editingPet) { + return ( +
+

Edit Pet (ID: {editingPet.id})

+
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ + +
+
+
+ ); + } + + return ( +
+

Pet Management

+ +
+

Add New Pet

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

Pets List in Database

+ {pets.length === 0 ? ( +

No pets registered yet.

+ ) : ( +
+ {pets.map((pet) => ( +
+
+ {pet.photoUrl && ( + {pet.name} + )} +
+
{pet.name}
+

Gender: {pet.genre}

+

Size: {pet.size}

+

Color: {pet.color}

+

Birth Date: {pet.birthDate || "Not provided"}

+

Chip: {pet.chipNumber || "No chip"}

+

Castrated: {pet.castrated ? "Yes" : "No"}

+
+
+ + +
+
+
+ ))} +
+ )} +
+ ); +}; \ No newline at end of file diff --git a/src/front/routes.jsx b/src/front/routes.jsx index 0557df6141..f49625f4a1 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 { Pets } from "./pages/Pets"; 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 a80254969ad4a01b3318e55e375b7e99185f1de9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergi=20Villalobos=20Gasc=C3=B3n?= <56490583+Sergidev@users.noreply.github.com> Date: Sat, 25 Jul 2026 20:13:04 +0000 Subject: [PATCH 12/21] minor fixes --- src/front/pages/Pets.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/front/pages/Pets.jsx b/src/front/pages/Pets.jsx index a4a85631f2..f447387e6f 100644 --- a/src/front/pages/Pets.jsx +++ b/src/front/pages/Pets.jsx @@ -206,7 +206,7 @@ export const Pets = () => {
- +
From 305a60e6ca34666a9add1543594270975d334f89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergi=20Villalobos=20Gasc=C3=B3n?= <56490583+Sergidev@users.noreply.github.com> Date: Mon, 27 Jul 2026 09:35:09 +0000 Subject: [PATCH 13/21] pets CRUD corrected --- src/api/models.py | 2 +- src/api/routes.py | 10 +- src/front/components/Navbar.jsx | 20 ++- src/front/components/PetCard.jsx | 38 ++++ src/front/pages/CreatePet.jsx | 118 +++++++++++++ src/front/pages/PetDetail.jsx | 197 +++++++++++++++++++++ src/front/pages/Pets.jsx | 291 ------------------------------- src/front/pages/PetsList.jsx | 61 +++++++ src/front/routes.jsx | 32 ++-- 9 files changed, 455 insertions(+), 314 deletions(-) create mode 100644 src/front/components/PetCard.jsx create mode 100644 src/front/pages/CreatePet.jsx create mode 100644 src/front/pages/PetDetail.jsx delete mode 100644 src/front/pages/Pets.jsx create mode 100644 src/front/pages/PetsList.jsx diff --git a/src/api/models.py b/src/api/models.py index 80de3e4d6a..2838983070 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -46,7 +46,7 @@ def serialize(self): "idBreed": self.breed_id, "name": self.name, "genre": self.genre, - "birthDate": self.birth_date, + "birthDate": self.birth_date.strftime('%Y-%m-%d') if self.birth_date else None, "castrated": self.castrated, "chipNumber": self.chip_number, "color": self.color, diff --git a/src/api/routes.py b/src/api/routes.py index 210a8a331a..4a339b2eaf 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -12,10 +12,18 @@ @api.route('/pets', methods=['GET']) def get_pets(): - pets = Pet.query.all() + pets = Pet.query.order_by(Pet.id.asc()).all() results = [pet.serialize() for pet in pets] return jsonify(results), 200 +@api.route('/pets/', methods=['GET']) +def get_single_pet(pet_id): + pet = db.session.get(Pet, pet_id) + + if pet is None: + raise APIException("Pet not found", status_code=404) + + return jsonify(pet.serialize()), 200 @api.route('/pets', methods=['POST']) def create_pet(): diff --git a/src/front/components/Navbar.jsx b/src/front/components/Navbar.jsx index 30d43a2636..88ca194a80 100644 --- a/src/front/components/Navbar.jsx +++ b/src/front/components/Navbar.jsx @@ -1,16 +1,22 @@ import { Link } from "react-router-dom"; export const Navbar = () => { - return (