diff --git a/.learn/config.json b/.learn/config.json new file mode 100644 index 000000000..5a3c721fd --- /dev/null +++ b/.learn/config.json @@ -0,0 +1,9 @@ +{ + "config": { + "editor": { + "agent": "vscode" + }, + "autoPlay": true + }, + "currentExercise": null +} \ No newline at end of file diff --git a/backend/__pycache__/app.cpython-313.pyc b/backend/__pycache__/app.cpython-313.pyc new file mode 100644 index 000000000..b2301b3ae Binary files /dev/null and b/backend/__pycache__/app.cpython-313.pyc differ diff --git a/backend/__pycache__/models.cpython-313.pyc b/backend/__pycache__/models.cpython-313.pyc new file mode 100644 index 000000000..f1bc17552 Binary files /dev/null and b/backend/__pycache__/models.cpython-313.pyc differ diff --git a/backend/app.py b/backend/app.py new file mode 100644 index 000000000..c85131c2d --- /dev/null +++ b/backend/app.py @@ -0,0 +1,148 @@ +from flask import Flask, jsonify +from models import db, People, Planet, User, Favorite + +app = Flask(__name__) + +app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///starwars.db" +app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False + +db.init_app(app) + +with app.app_context(): + db.create_all() + +@app.route("/") +def home(): + return jsonify({"msg": "Star Wars API running 🚀"}) + + +@app.route("/people", methods=["GET"]) +def get_people(): + people = People.query.all() + return jsonify([p.serialize() for p in people]), 200 + + +@app.route("/people/", methods=["GET"]) +def get_single_person(people_id): + person = People.query.get(people_id) + if not person: + return jsonify({"error": "Person not found"}), 404 + return jsonify(person.serialize()), 200 + + +@app.route("/planets", methods=["GET"]) +def get_planets(): + planets = Planet.query.all() + return jsonify([p.serialize() for p in planets]), 200 + + +@app.route("/planets/", methods=["GET"]) +def get_single_planet(planet_id): + planet = Planet.query.get(planet_id) + if not planet: + return jsonify({"error": "Planet not found"}), 404 + return jsonify(planet.serialize()), 200 + + +@app.route("/users", methods=["GET"]) +def get_users(): + users = User.query.all() + return jsonify([u.serialize() for u in users]), 200 + + +def get_current_user(): + return User.query.first() + + +@app.route("/users/favorites", methods=["GET"]) +def get_user_favorites(): + user = get_current_user() + if not user: + return jsonify([]), 200 + + favorites = [] + + for fav in user.favorites: + if fav.people: + favorites.append({ + "type": "people", + "item": fav.people.serialize() + }) + if fav.planet: + favorites.append({ + "type": "planet", + "item": fav.planet.serialize() + }) + + return jsonify(favorites), 200 + + +@app.route("/favorite/people/", methods=["POST"]) +def add_favorite_people(people_id): + user = get_current_user() + person = People.query.get(people_id) + + if not user or not person: + return jsonify({"error": "User or person not found"}), 404 + + favorite = Favorite(user_id=user.id, people_id=person.id) + db.session.add(favorite) + db.session.commit() + + return jsonify({"msg": "Favorite person added"}), 201 + + +@app.route("/favorite/planet/", methods=["POST"]) +def add_favorite_planet(planet_id): + user = get_current_user() + planet = Planet.query.get(planet_id) + + if not user or not planet: + return jsonify({"error": "User or planet not found"}), 404 + + favorite = Favorite(user_id=user.id, planet_id=planet.id) + db.session.add(favorite) + db.session.commit() + + return jsonify({"msg": "Favorite planet added"}), 201 + + +@app.route("/favorite/people/", methods=["DELETE"]) +def delete_favorite_people(people_id): + user = get_current_user() + + favorite = Favorite.query.filter_by( + user_id=user.id, + people_id=people_id + ).first() + + if not favorite: + return jsonify({"error": "Favorite not found"}), 404 + + db.session.delete(favorite) + db.session.commit() + + return jsonify({"msg": "Favorite person deleted"}), 200 + + +@app.route("/favorite/planet/", methods=["DELETE"]) +def delete_favorite_planet(planet_id): + user = get_current_user() + + favorite = Favorite.query.filter_by( + user_id=user.id, + planet_id=planet_id + ).first() + + if not favorite: + return jsonify({"error": "Favorite not found"}), 404 + + db.session.delete(favorite) + db.session.commit() + + return jsonify({"msg": "Favorite planet deleted"}), 200 + + +if __name__ == "__main__": + app.run(debug=True) + diff --git a/backend/instance/starwars.db b/backend/instance/starwars.db new file mode 100644 index 000000000..b5a6c8bf8 Binary files /dev/null and b/backend/instance/starwars.db differ diff --git a/backend/models.py b/backend/models.py new file mode 100644 index 000000000..41a75fd14 --- /dev/null +++ b/backend/models.py @@ -0,0 +1,73 @@ +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() + + +class User(db.Model): + __tablename__ = "user" + + id = db.Column(db.Integer, primary_key=True) + email = db.Column(db.String(120), unique=True, nullable=False) + + favorites = db.relationship("Favorite", backref="user", lazy=True) + + def serialize(self): + return { + "id": self.id, + "email": self.email + } + + +class People(db.Model): + __tablename__ = "people" + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(120), nullable=False) + gender = db.Column(db.String(50)) + birth_year = db.Column(db.String(20)) + + favorites = db.relationship("Favorite", backref="people", lazy=True) + + def serialize(self): + return { + "id": self.id, + "name": self.name, + "gender": self.gender, + "birth_year": self.birth_year + } + + +class Planet(db.Model): + __tablename__ = "planet" + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(120), nullable=False) + climate = db.Column(db.String(50)) + population = db.Column(db.String(50)) + + favorites = db.relationship("Favorite", backref="planet", lazy=True) + + def serialize(self): + return { + "id": self.id, + "name": self.name, + "climate": self.climate, + "population": self.population + } + + +class Favorite(db.Model): + __tablename__ = "favorite" + + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False) + people_id = db.Column(db.Integer, db.ForeignKey("people.id"), nullable=True) + planet_id = db.Column(db.Integer, db.ForeignKey("planet.id"), nullable=True) + + def serialize(self): + return { + "id": self.id, + "user_id": self.user_id, + "people_id": self.people_id, + "planet_id": self.planet_id + } diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 000000000..9f444c897 Binary files /dev/null and b/backend/requirements.txt differ diff --git a/package-lock.json b/package-lock.json index 40ae560d5..68109659f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,9 @@ "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.3", "vite": "^4.4.8" + }, + "engines": { + "node": ">=20.0.0" } }, "node_modules/@aashutoshrathi/word-wrap": { diff --git a/src/assets/img/iconosw.png b/src/assets/img/iconosw.png new file mode 100644 index 000000000..35a2f7b83 Binary files /dev/null and b/src/assets/img/iconosw.png differ diff --git a/src/assets/img/rigo-baby.jpg b/src/assets/img/rigo-baby.jpg deleted file mode 100644 index da566a74a..000000000 Binary files a/src/assets/img/rigo-baby.jpg and /dev/null differ diff --git a/src/components/Card.jsx b/src/components/Card.jsx new file mode 100644 index 000000000..a7253e465 --- /dev/null +++ b/src/components/Card.jsx @@ -0,0 +1,33 @@ +import useGlobalReducer from "../hooks/useGlobalReducer.jsx"; + +export const Card = ({ imgURL, title, children }) => { + const { store, dispatch } = useGlobalReducer(); + + const addFavorite = () => { + dispatch({ type: "add_favorite", payload: { name: title } }); + }; + + return ( + <> +
+ Luke Skywalker +
+
{title}
+ {children} + +
+
+ + ); +}; diff --git a/src/components/Favorites.jsx b/src/components/Favorites.jsx new file mode 100644 index 000000000..124ad5b4c --- /dev/null +++ b/src/components/Favorites.jsx @@ -0,0 +1,45 @@ +import useGlobalReducer from "../hooks/useGlobalReducer.jsx"; +export const Favorites = () => { + const { store, dispatch } = useGlobalReducer(); + + const favorites = store.favoriteList || []; + + const removeFavorite = (name) => { + dispatch({ + type: "remove_favorite", + payload: { name: item }, + }); + }; + + return ( + <> +
+ + +
    + {favorites.map((item, index) => ( +
  • + {item} + +
  • + ))} +
+
+ + ); +}; diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx index 30d43a263..5d951b2b7 100644 --- a/src/components/Navbar.jsx +++ b/src/components/Navbar.jsx @@ -1,19 +1,27 @@ +import React from "react"; import { Link } from "react-router-dom"; +import starWarsLogo from "../assets/img/iconosw.png"; +import { Favorites } from "./Favorites"; -export const Navbar = () => { +// ... el resto del componente - return ( - - ); -}; \ No newline at end of file +export const Navbar = () => { + return ( + + ); +}; diff --git a/src/index.css b/src/index.css index e69de29bb..40b898472 100644 --- a/src/index.css +++ b/src/index.css @@ -0,0 +1,3 @@ +.card { + width: 18rem; +} \ No newline at end of file diff --git a/src/main.jsx b/src/main.jsx index 3a122d76a..88d0623bd 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,22 +1,21 @@ -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 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.jsx"; // Import the StoreProvider for global state management const Main = () => { - return ( - - {/* Provide global state to all components */} - - {/* Set up routing for the application */} - - - - - ); -} + 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/pages/Character.jsx b/src/pages/Character.jsx new file mode 100644 index 000000000..afc7bc9c4 --- /dev/null +++ b/src/pages/Character.jsx @@ -0,0 +1,3 @@ +export const Character = () => { + return <>Character; +}; diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx index 3e9f1aefa..42a32fd42 100644 --- a/src/pages/Home.jsx +++ b/src/pages/Home.jsx @@ -1,16 +1,62 @@ -import rigoImageUrl from "../assets/img/rigo-baby.jpg"; import useGlobalReducer from "../hooks/useGlobalReducer.jsx"; +import { Card } from "../components/Card"; +import { useEffect } from "react"; +import { getCharacterList } from "../swapiFetch.js"; + +const MOCK_IMG_SRC = + "https://lumiere-a.akamaihd.net/v1/images/luke-skywalker-main_7ffe21c7.jpeg?region=130%2C147%2C1417%2C796"; export const Home = () => { + const { store, dispatch } = useGlobalReducer(); + + const IMAGE_MAP = { + "Luke Skywalker": + "https://lumiere-a.akamaihd.net/v1/images/luke-skywalker-main_7ffe21c7.jpeg?region=130%2C147%2C1417%2C796", + "C-3PO": + "https://lumiere-a.akamaihd.net/v1/images/c-3po-main_d6850e28.jpeg?region=176%2C0%2C951%2C536", + "R2-D2": + "https://lumiere-a.akamaihd.net/v1/images/r2-d2-main_f315b094.jpeg?region=273%2C0%2C951%2C536", + "Darth Vader": + "https://lumiere-a.akamaihd.net/v1/images/darth-vader-main_4560aff7.jpeg?region=0%2C67%2C1280%2C720", + "Leia Organa": + "https://lumiere-a.akamaihd.net/v1/images/leia-organa-main_9af6ff81.jpeg?region=187%2C157%2C1400%2C786", + "Owen Lars": + "https://lumiere-a.akamaihd.net/v1/images/owen-lars-main_08c717c8.jpeg?region=0%2C34%2C1053%2C593", + "Beru Whitesun lars": + "https://lumiere-a.akamaihd.net/v1/images/beru-lars-main_fa680a4c.png?region=342%2C0%2C938%2C527", + "R5-D4": + "https://lumiere-a.akamaihd.net/v1/images/r5-d4_main_image_7d5f078e.jpeg?region=374%2C0%2C1186%2C666", + "Biggs Darklighter": + "https://lumiere-a.akamaihd.net/v1/images/image_606ff7f7.jpeg?region=0%2C0%2C1560%2C878", + "Obi-Wan Kenobi": + "https://lumiere-a.akamaihd.net/v1/images/obi-wan-kenobi-main_3286c63c.jpeg?region=0%2C0%2C1280%2C721", + }; + + const updateData = async () => { + const characterList = await getCharacterList(); + dispatch({ type: "update_characterList", payload: characterList }); + }; - const {store, dispatch} =useGlobalReducer() + useEffect(() => { + updateData(); + }, []); - return ( -
-

Hello Rigo!!

-

- -

-
- ); -}; \ No newline at end of file + return ( + <> + Home + {store.characterList.map((character, index) => ( + +
    +
  • Eye color: {character?.eye_color}
  • +
  • Hair Color: {character?.hair_color}
  • +
  • size: {character?.height}cm
  • +
+
+ ))} + + ); +}; diff --git a/src/pages/Layout.jsx b/src/pages/Layout.jsx index 9bfa31325..818e60ec1 100644 --- a/src/pages/Layout.jsx +++ b/src/pages/Layout.jsx @@ -1,15 +1,14 @@ -import { Outlet } from "react-router-dom/dist" -import ScrollToTop from "../components/ScrollToTop" -import { Navbar } from "../components/Navbar" -import { Footer } from "../components/Footer" +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 ( - - - -