diff --git a/README.md b/README.md index f1d72de6355..b620853354f 100644 --- a/README.md +++ b/README.md @@ -158,5 +158,10 @@ Alexa Orrico - [Github](https://github.com/alexaorrico) / [Twitter](https://twit Jennifer Huang - [Github](https://github.com/jhuang10123) / [Twitter](https://twitter.com/earthtojhuang) Second part of Airbnb: Joann Vuong + +AirBnB -- RESTFUL API part +Deantosh Daiddoh - [Github] (https://github.com/deantosh) / [X] (https://x.com/daiddoh) +Lucky Archibong - [Github] (https://github.com/luckys-lnz) / [X] (https://x.com/) + ## License Public Domain. No copy write protection. diff --git a/__pycache__/console.cpython-312.pyc b/__pycache__/console.cpython-312.pyc new file mode 100644 index 00000000000..069a5901076 Binary files /dev/null and b/__pycache__/console.cpython-312.pyc differ diff --git a/api/__init__.py b/api/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/api/v1/__init__.py b/api/v1/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/api/v1/app.py b/api/v1/app.py new file mode 100644 index 00000000000..8c58f9fc0d8 --- /dev/null +++ b/api/v1/app.py @@ -0,0 +1,31 @@ +#!/usr/bin/python3 +""" +Module defines a Flask application +""" + +from flask import Flask, jsonify +from models import storage +from api.v1.views import app_views +import os + + +app = Flask(__name__) +app.register_blueprint(app_views) + + +@app.errorhandler(404) +def not_found_error(error): + """ Handles error 404 """ + return jsonify({"error": "Not found"}), 404 + + +@app.teardown_appcontext +def teardown(exception=None): + """ Closes the storage """ + storage.close() + + +if __name__ == '__main__': + host = os.getenv('HBNB_API_HOST', '0.0.0.0') + port = int(os.getenv('HBNB_API_PORT', 5000)) + app.run(host=host, port=port, threaded=True, debug=True) diff --git a/api/v1/views/__init__.py b/api/v1/views/__init__.py new file mode 100755 index 00000000000..7b10816d9c0 --- /dev/null +++ b/api/v1/views/__init__.py @@ -0,0 +1,14 @@ +#!/usr/bin/python3 +"""python package""" + +from flask import Blueprint + +app_views = Blueprint('app_views', __name__, url_prefix='/api/v1') + +from api.v1.views.index import * +from api.v1.views.states import * +from api.v1.views.cities import * +from api.v1.views.amenities import * +from api.v1.views.users import * +from api.v1.views.places import * +from api.v1.views.places_reviews import * diff --git a/api/v1/views/amenities.py b/api/v1/views/amenities.py new file mode 100755 index 00000000000..2eae40f80b4 --- /dev/null +++ b/api/v1/views/amenities.py @@ -0,0 +1,81 @@ +#!/usr/bin/python3 +""" +Module defines view for Amenity objects that handles all default +RESTFUL API actions. +""" +from models import storage +from models.amenity import Amenity +from flask import jsonify, abort, request +from werkzeug.exceptions import BadRequest +from api.v1.views import app_views + + +@app_views.route('/amenities', methods=['GET'], strict_slashes=False) +def get_all_amenities(): + """ Retrieves all Amenity objects """ + amenity_objs = storage.all(Amenity).values() + amenity_list = [amenity.to_dict() for amenity in amenity_objs] + + return jsonify(amenity_list) + + +@app_views.route('/amenities/', methods=['GET']) +def get_amenity(amenity_id): + """ Retrieves Amenity object by amenity_id """ + amenity_obj = storage.get(Amenity, amenity_id) + if amenity_obj is None: + abort(404) + + return jsonify(amenity_obj.to_dict()) + + +@app_views.route('/amenities/', methods=['DELETE']) +def delete_amenity(amenity_id): + """ Deletes Amenity object by amenity_id """ + amenity_obj = storage.get(Amenity, amenity_id) + if amenity_obj is None: + abort(404) + + storage.delete(amenity_obj) + storage.save() + + return jsonify({}), 200 + + +@app_views.route('/amenities', methods=['POST'], strict_slashes=False) +def create_amenity(): + """ Creates Amenity object """ + try: + data = request.get_json() + if 'name' not in data: + abort(400, description='Missing name') + except BadRequest: + abort(400, description='Not a JSON') + + amenity_obj = Amenity(**data) + storage.new(amenity_obj) + storage.save() + + return jsonify(amenity_obj.to_dict()), 201 + + +@app_views.route('/amenities/', methods=['PUT']) +def update_amenity(amenity_id): + """ Updates Amenity object by amenity_id """ + amenity_obj = storage.get(Amenity, amenity_id) + if amenity_obj is None: + abort(404) + + try: + data = request.get_json() + except BadRequest: + abort(400, description='Not a JSON') + + ignore_keys = ['id', 'state_id', 'created_at', 'updated_at'] + for key, value in data.items(): + if key not in ignore_keys: + setattr(amenity_obj, key, value) + + storage.save() + + return jsonify(amenity_obj.to_dict()), 200 diff --git a/api/v1/views/cities.py b/api/v1/views/cities.py new file mode 100644 index 00000000000..dd03993cdd8 --- /dev/null +++ b/api/v1/views/cities.py @@ -0,0 +1,90 @@ +#!/usr/bin/python3 +""" +Module defines view for City objects that handles all default +RESTFUL API actions. +""" +from models import storage +from models.city import City +from models.state import State +from flask import jsonify, abort, request +from werkzeug.exceptions import BadRequest +from api.v1.views import app_views + + +@app_views.route('/states//cities', + methods=['GET'], strict_slashes=False) +def get_all_cities(state_id): + """ Retrieves all the cities of a state """ + state_obj = storage.get(State, state_id) + if state_obj is None: + abort(404) + city_list = [city.to_dict() for city in state_obj.cities] + return jsonify(city_list) + + +@app_views.route('/cities/', methods=['GET']) +def get_city(city_id): + """ Retrieves city object of a specified city_id """ + city_obj = storage.get(City, city_id) + if city_obj is None: + abort(404) + + return jsonify(city_obj.to_dict()) + + +@app_views.route('/cities/', methods=['DELETE']) +def delete_city(city_id): + """ Deletes city object """ + city_obj = storage.get(City, city_id) + if city_obj is None: + abort(404) + + storage.delete(city_obj) + storage.save() + + return jsonify({}), 200 + + +@app_views.route('/states//cities', + methods=['POST'], strict_slashes=False) +def create_city(state_id): + """ Creates city object""" + state_obj = storage.get(State, state_id) + if state_obj is None: + abort(404) + + try: + data = request.get_json() + if 'name' not in data: + abort(400, description='Missing name') + except BadRequest: + abort(400, description='Not a JSON') + + data['state_id'] = state_id + city_obj = City(**data) + storage.new(city_obj) + storage.save() + + return jsonify(city_obj.to_dict()), 201 + + +@app_views.route('/cities/', methods=['PUT']) +def update_city(city_id): + """ Updates city object """ + city_obj = storage.get(City, city_id) + if city_obj is None: + abort(404) + + try: + data = request.get_json() + except BadRequest: + abort(400, description='Not a JSON') + + ignore_keys = ['id', 'state_id', 'created_at', 'updated_at'] + for key, value in data.items(): + if key not in ignore_keys: + setattr(city_obj, key, value) + + storage.save() + + return jsonify(city_obj.to_dict()), 200 diff --git a/api/v1/views/index.py b/api/v1/views/index.py new file mode 100755 index 00000000000..07e352c4e73 --- /dev/null +++ b/api/v1/views/index.py @@ -0,0 +1,35 @@ +#!/usr/bin/python3 +""" +Create an endpoint that retrieves the number +of each objects by type: +""" + +from flask import jsonify +from api.v1.views import app_views +from models import storage +from models.amenity import Amenity +from models.city import City +from models.state import State +from models.place import Place +from models.user import User +from models.review import Review + + +@app_views.route('/status', methods=['GET']) +def get_status(): + """Check status of file""" + return jsonify({"status": "OK"}) + + +@app_views.route('/stats', methods=['GET'], strict_slashes=False) +def get_stats(): + """Create an endpoint""" + statistics = { + "amenities": storage.count(Amenity), + "cities": storage.count(City), + "places": storage.count(Place), + "reviews": storage.count(Review), + "states": storage.count(State), + "users": storage.count(User), + } + return jsonify(statistics) diff --git a/api/v1/views/places.py b/api/v1/views/places.py new file mode 100755 index 00000000000..0e648cd3716 --- /dev/null +++ b/api/v1/views/places.py @@ -0,0 +1,91 @@ +#!/usr/bin/python3 +""" +Module defines view for Place objects that handles all default +RESTFUL API actions. +""" +from models import storage +from models.city import City +from models.place import Place +from flask import jsonify, abort, request +from werkzeug.exceptions import BadRequest +from api.v1.views import app_views + + +@app_views.route('/cities//places', + methods=['GET'], strict_slashes=False) +def get_all_places(city_id): + """ Retrieves the list of all Place objects of a city by city_id""" + city = storage.get(City, city_id) + if city is None: + abort(404) + place_list = [place.to_dict() for place in city.places] + return jsonify(place_list) + + +@app_views.route('/places/', methods=['GET'], strict_slashes=False) +def get_place(place_id): + """ Retrieves Place object by place_id """ + place_obj = storage.get(Place, place_id) + if place_obj is None: + abort(404) + + return jsonify(place_obj.to_dict()) + + +@app_views.route('/places/', methods=['DELETE'], + strict_slashes=False) +def delete_place(place_id): + """ Deletes Place object by place_id """ + place_obj = storage.get(Place, place_id) + if place_obj is None: + abort(404) + + storage.delete(place_obj) + storage.save() + + return jsonify({}), 200 + + +@app_views.route('/cities//places', + methods=['POST'], strict_slashes=False) +def create_place(city_id): + """ Creates Place object in a city by city_id""" + city_obj = storage.get(City, city_id) + if city_obj is None: + abort(404) + + try: + data = request.get_json() + if 'name' not in data: + abort(400, description='Missing name') + except BadRequest: + abort(400, description='Not a JSON') + + data['city_id'] = city_id + place_obj = Place(**data) + storage.new(place_obj) + storage.save() + + return jsonify(place_obj.to_dict()), 201 + + +@app_views.route('/places/', methods=['PUT'], strict_slashes=False) +def update_place(place_id): + """ Updates Place object by place_id """ + place_obj = storage.get(Place, place_id) + if place_obj is None: + abort(404) + + try: + data = request.get_json() + except BadRequest: + abort(400, description='Not a JSON') + + ignore_keys = ['id', 'state_id', 'created_at', 'updated_at'] + for key, value in data.items(): + if key not in ignore_keys: + setattr(place_obj, key, value) + + storage.save() + + return jsonify(place_obj.to_dict()), 200 diff --git a/api/v1/views/places_reviews.py b/api/v1/views/places_reviews.py new file mode 100755 index 00000000000..d4583df023e --- /dev/null +++ b/api/v1/views/places_reviews.py @@ -0,0 +1,90 @@ +#!/usr/bin/python3 +""" +Creates a new view for the link between Place objects and Amenity +objects that handles all default RESTFul API actions +""" + +from flask import jsonify, request, abort +from api.v1.views import app_views +from models import storage +from models.review import Review +from models.place import Place +from models.user import User + + +@app_views.route('/places//reviews', methods=['GET'], + strict_slashes=False) +def get_reviews(place_id): + """Retrieves the list of all review objects of place""" + place = storage.get(Place, place_id) + if not place: + abort(404) + reviews = [review.to_dict() for review in place.reviews] + return jsonify(reviews) + + +@app_views.route('/reviews/', methods=['GET'], + strict_slashes=False) +def get_review(review_id): + """Retrieve a specific Review object""" + review = storage.get(Review, review_id) + if not review: + abort(404) + return jsonify(review.to_dict()) + + +@app_views.route('/reviews/', methods=['DELETE'], + strict_slashes=False) +def delete_review(review_id): + """Delete a Review object""" + review = storage.get(Review, review_id) + if not review: + abort(404) + review.delete() + storage.save() + return jsonify({}), 200 + + +@app_views.route('/places//reviews', methods=['POST'], + strict_slashes=False) +def create_review(place_id): + """Create a new Review object""" + place = storage.get(Place, place_id) + if not place: + abort(404) + if not request.json: + abort(400, description="Not a JSON") + if 'user_id' not in request.json: + abort(400, description="Missing user_id") + if 'text' not in request.json: + abort(400, description="Missing text") + + user_id = request.json.get('user_id') + user = storage.get(User, user_id) + if not user: + abort(404) + + data = request.json + data['place_id'] = place_id + new_review = Review(**data) + new_review.save() + return jsonify(new_review.to_dict()), 201 + + +@app_views.route('/reviews/', methods=['PUT'], + strict_slashes=False) +def update_review(review_id): + """Update a Review object""" + review = storage.get(Review, review_id) + if not review: + abort(404) + if not request.json: + abort(400, description="Not a JSON") + + ignore_fields = ['id', 'user_id', 'place_id', 'created_at', 'updated_at'] + data = request.json + for key, value in data.items(): + if key not in ignore_fields: + setattr(review, key, value) + review.save() + return jsonify(review.to_dict()) diff --git a/api/v1/views/states.py b/api/v1/views/states.py new file mode 100755 index 00000000000..8a33e86050d --- /dev/null +++ b/api/v1/views/states.py @@ -0,0 +1,77 @@ +#!/usr/bin/python3 +""" +Module defines view for State objects that handles all default +RESTFul API actions. +""" +from api.v1.views import app_views +from models import storage +from models.state import State +from flask import jsonify, abort, request +from werkzeug.exceptions import BadRequest + + +@app_views.route('/states', methods=['GET'], strict_slashes=False) +def get_all_states(): + """ Retrievs a list of all State objects """ + states_list = [state.to_dict() for state in storage.all(State).values()] + return jsonify(states_list) + + +@app_views.route('/states/', methods=['GET'], strict_slashes=False) +def get_state(state_id): + """ Retrieves a State object of a specified state_id """ + state_obj = storage.get(State, state_id) + if state_obj is None: + abort(404) + else: + return jsonify(state_obj.to_dict()) + + +@app_views.route('states/', methods=['DELETE'], strict_slashes=False) +def delete_state(state_id): + """ Deletes state object of a specified state_id """ + state_obj = storage.get(State, state_id) + if state_obj is None: + abort(404) + else: + storage.delete(state_obj) + storage.save() + return jsonify({}), 200 + + +@app_views.route('/states', methods=['POST'], strict_slashes=False) +def create_state(): + """ Creates a state object """ + + try: + data = request.get_json() + except BadRequest: + abort(400, description="Not a JSON") + + if 'name' not in data: + abort(400, description="Missing name") + else: + state_obj = State(**data) + storage.new(state_obj) + storage.save() + return jsonify(state_obj.to_dict()), 201 + + +@app_views.route('states/', methods=['PUT'], strict_slashes=False) +def update_state(state_id): + """ Updates state """ + state_obj = storage.get(State, state_id) + if state_obj is None: + abort(404) + + try: + data = request.get_json() + except BadRequest: + abort(400, description="Not a JSON") + + ignored_keys = ['id', 'created_at', 'updated_at'] + for key, value in data.items(): + if key not in ignored_keys: + setattr(state_obj, key, value) + storage.save() + return jsonify(state_obj.to_dict()), 200 diff --git a/api/v1/views/users.py b/api/v1/views/users.py new file mode 100755 index 00000000000..a9cbad31e4a --- /dev/null +++ b/api/v1/views/users.py @@ -0,0 +1,79 @@ +#!/usr/bin/python3 +""" +Module defines view for User objects that handles all default +RESTFUL API actions. +""" +from models import storage +from models.user import User +from flask import jsonify, abort, request +from werkzeug.exceptions import BadRequest +from api.v1.views import app_views + + +@app_views.route('/users', methods=['GET'], strict_slashes=False) +def get_all_users(): + """ Retrieves all User objects """ + user_list = [user.to_dict() for user in storage.all(User).values()] + return jsonify(user_list) + + +@app_views.route('/users/', methods=['GET']) +def get_user(user_id): + """ Retrieves User object by user_id """ + user_obj = storage.get(User, user_id) + if user_obj is None: + abort(404) + + return jsonify(user_obj.to_dict()) + + +@app_views.route('/users/', methods=['DELETE']) +def delete_user(user_id): + """ Deletes User object by user_id """ + user_obj = storage.get(User, user_id) + if user_obj is None: + abort(404) + + storage.delete(user_obj) + storage.save() + + return jsonify({}), 200 + + +@app_views.route('/users', methods=['POST'], strict_slashes=False) +def create_user(): + """ Creates User object """ + try: + data = request.get_json() + if 'email' not in data or 'password' not in data: + abort(400, description='Missing name') + except BadRequest: + abort(400, description='Not a JSON') + + user_obj = User(**data) + storage.new(user_obj) + storage.save() + + return jsonify(user_obj.to_dict()), 201 + + +@app_views.route('/users/', methods=['PUT']) +def update_user(user_id): + """ Updates User object by user_id """ + user_obj = storage.get(User, user_id) + if user_obj is None: + abort(404) + + try: + data = request.get_json() + except BadRequest: + abort(400, description='Not a JSON') + + ignore_keys = ['id', 'state_id', 'created_at', 'updated_at'] + for key, value in data.items(): + if key not in ignore_keys: + setattr(user_obj, key, value) + + storage.save() + + return jsonify(user_obj.to_dict()), 200 diff --git a/console.py b/console.py index 4798f9ac76b..1b7b77104c6 100755 --- a/console.py +++ b/console.py @@ -1,164 +1,248 @@ #!/usr/bin/python3 -""" console """ - +""" +Defines a program `console` that contains the entry point of the command +interpreter. +Requirements: + - You must use the module cmd + - Your class definition must be: class HBNBCommand(cmd.Cmd): + - Your command interpreter should implement: + - quit and EOF to exit the program + - help (this action is provided by default by cmd but you should keep it + updated and documented as you work through tasks) a custom prompt: (hbnb) + - an empty line + ENTER shouldn’t execute anything + - Your code should not be executed when imported +""" import cmd -from datetime import datetime -import models -from models.amenity import Amenity -from models.base_model import BaseModel +import sys +from models import storage +from models.user import User from models.city import City from models.place import Place -from models.review import Review from models.state import State -from models.user import User -import shlex # for splitting the line along spaces except in double quotes +from models.review import Review +from models.amenity import Amenity +from models.base_model import BaseModel -classes = {"Amenity": Amenity, "BaseModel": BaseModel, "City": City, - "Place": Place, "Review": Review, "State": State, "User": User} +# define global class dict +cls_dict = {"BaseModel": BaseModel, "User": User, "State": State, + "City": City, "Amenity": Amenity, "Place": Place, "Review": Review} class HBNBCommand(cmd.Cmd): - """ HBNH console """ - prompt = '(hbnb) ' + """ + Defines the command interpreter + """ - def do_EOF(self, arg): - """Exits console""" - return True - - def emptyline(self): - """ overwriting the emptyline method """ - return False + prompt = '(hbnb) ' def do_quit(self, arg): """Quit command to exit the program""" return True - def _key_value_parser(self, args): - """creates a dictionary from a list of strings""" - new_dict = {} - for arg in args: - if "=" in arg: - kvp = arg.split('=', 1) - key = kvp[0] - value = kvp[1] - if value[0] == value[-1] == '"': - value = shlex.split(value)[0].replace('_', ' ') - else: - try: - value = int(value) - except: - try: - value = float(value) - except: - continue - new_dict[key] = value - return new_dict + def do_EOF(self, arg): + """EOF command to exit the program""" + return True + + def emptyline(self): + """Do nothing when an empty line is entered""" + pass def do_create(self, arg): - """Creates a new instance of a class""" - args = arg.split() - if len(args) == 0: + """ + Creates a new instance of BaseModel, save it (to JSON file) + and prints the id + """ + # get class name + cls = cls_dict.get(arg) + + # validate input + if not arg: print("** class name missing **") - return False - if args[0] in classes: - new_dict = self._key_value_parser(args[1:]) - instance = classes[args[0]](**new_dict) - else: + elif cls is None: print("** class doesn't exist **") - return False - print(instance.id) - instance.save() - - def do_show(self, arg): - """Prints an instance as a string based on the class and id""" - args = shlex.split(arg) - if len(args) == 0: - print("** class name missing **") - return False - if args[0] in classes: - if len(args) > 1: - key = args[0] + "." + args[1] - if key in models.storage.all(): - print(models.storage.all()[key]) + else: + obj = cls() # create instance + storage.new(obj) + storage.save() + print(obj.id) + + def do_show(self, line): + """ + Prints the string representation of an instance based on the + class name and id + """ + args = line.split() + if len(args) != 0: + if len(args) == 2: + cls = cls_dict.get(args[0]) + if cls: + objs = storage.all() + # create key + obj_key = "{}.{}".format(args[0], args[1]) + if obj_key in objs: + obj = objs.get(obj_key) + print(str(obj)) + else: + print("** no instance found **") else: - print("** no instance found **") + print("** class doesn't exist **") else: print("** instance id missing **") else: - print("** class doesn't exist **") - - def do_destroy(self, arg): - """Deletes an instance based on the class and id""" - args = shlex.split(arg) - if len(args) == 0: print("** class name missing **") - elif args[0] in classes: - if len(args) > 1: - key = args[0] + "." + args[1] - if key in models.storage.all(): - models.storage.all().pop(key) - models.storage.save() + + def do_destroy(self, line): + """ + Deletes an instance based on the class name and id (save the change + into the JSON file) + """ + args = line.split() + if len(args) != 0: + if len(args) == 2: + cls = cls_dict.get(args[0]) + if cls: + objs = storage.all() + obj_key = "{}.{}".format(args[0], args[1]) + if obj_key in objs: + del objs[obj_key] + storage.save() + else: + print("** no instance found **") else: - print("** no instance found **") + print("** class doesn't exist **") else: print("** instance id missing **") else: - print("** class doesn't exist **") + print("** class name missing **") def do_all(self, arg): - """Prints string representations of instances""" - args = shlex.split(arg) - obj_list = [] - if len(args) == 0: - obj_dict = models.storage.all() - elif args[0] in classes: - obj_dict = models.storage.all(classes[args[0]]) + """ + Prints all string representation of all instances based or not on + the class name + """ + objs = storage.all() + objs_list = [] + if arg: + # print object of the class provided + cls = cls_dict.get(arg) + if cls: + for key, obj in objs.items(): + cls_name = key.split('.')[0] + if arg == cls_name: + objs_list.append(str(obj)) + print(objs_list) + else: + print("** class doesn't exist **") else: - print("** class doesn't exist **") - return False - for key in obj_dict: - obj_list.append(str(obj_dict[key])) - print("[", end="") - print(", ".join(obj_list), end="") - print("]") - - def do_update(self, arg): - """Update an instance based on the class name, id, attribute & value""" - args = shlex.split(arg) - integers = ["number_rooms", "number_bathrooms", "max_guest", - "price_by_night"] - floats = ["latitude", "longitude"] - if len(args) == 0: - print("** class name missing **") - elif args[0] in classes: - if len(args) > 1: - k = args[0] + "." + args[1] - if k in models.storage.all(): - if len(args) > 2: - if len(args) > 3: - if args[0] == "Place": - if args[2] in integers: - try: - args[3] = int(args[3]) - except: - args[3] = 0 - elif args[2] in floats: - try: - args[3] = float(args[3]) - except: - args[3] = 0.0 - setattr(models.storage.all()[k], args[2], args[3]) - models.storage.all()[k].save() + # print all objects + for obj in objs.values(): + objs_list.append(str(obj)) + print(objs_list) + + def do_update(self, line): + """ + Updates an instance based on the class name and id by adding or + updating attribute (save the change into the JSON file + """ + args = line.split() + if len(args) >= 4: + cls = cls_dict.get(args[0]) + if cls: + objs = storage.all() + obj_key = "{}.{}".format(args[0], args[1]) + if obj_key in objs: + obj = objs.get(obj_key) + + # convert attribute_value to correct data type + try: + value = float(args[3]) + if value.is_integer(): + attr_value = int(value) else: - print("** value missing **") - else: - print("** attribute name missing **") + attr_value = value + except ValueError: + # remove quotes + value = args[3].strip('\'"') + attr_value = str(value) + + # update object + setattr(obj, args[2], attr_value) + + # save update to file + storage.save() else: print("** no instance found **") else: - print("** instance id missing **") + print("** class doesn't exist **") else: - print("** class doesn't exist **") + if len(args) == 0: + print("** class name missing **") + if len(args) == 1: + print("** instance id missing **") + if len(args) == 2: + print("** attribute name missing **") + if len(args) == 3: + print("** value missing **") + + def default(self, line): + """executes command methods not defined""" + + # list of commands + cmd_list = ["all", "count", "show", "destroy", "update"] + + # extract command name ,class name + args = line.split('.') + cls_name = args[0] + cmd = args[1].split('(') + cmd_name = cmd[0] + + # extract function arguments + args_list = cmd[1].split(',') + if len(args_list) > 0: + id = args_list[0].strip("\"'") + # string to pass to function + cmd_string = "{} {}".format(cls_name, id) + + if len(args_list) > 2: + attr_name = args_list[1].strip("\"'") + attr_value = args_list[2].strip("\"'") + # string to pass to function + cmd_string = "{} {} {} {}".format(cls_name, id, + attr_name, attr_value) + + cls = cls_dict.get(cls_name) + if cls: + if cmd_name in cmd_list: + if cmd_name == "all": + # execute .all() command + self.do_all(cls_name) + + elif cmd_name == "count": + # execute .count() command + objs = storage.all() + cls_objs_dict = {} + for key, obj in objs.items(): + name = key.split('.')[0] + if name == cls_name: + cls_objs_dict[key] = obj + # print count + print(len(cls_objs_dict)) + + elif cmd_name == "show": + # execute .show() command + print(cmd_string) + self.do_show(cmd_string) + + elif cmd_name == "destroy": + # execute .destroy() command + self.do_destroy(cmd_string) + + elif cmd_name == "update": + # execute .update(, , + # ) + print(cmd_string) + self.do_update(cmd_string) + if __name__ == '__main__': HBNBCommand().cmdloop() diff --git a/models/engine/db_storage.py b/models/engine/db_storage.py index b8e7d291e6f..ac3f6006b2d 100755 --- a/models/engine/db_storage.py +++ b/models/engine/db_storage.py @@ -74,3 +74,19 @@ def reload(self): def close(self): """call remove() method on the private session attribute""" self.__session.remove() + + def get(self, cls, id): + """ Retrieves a specified object from storage """ + if cls is None or id is None: + return None + return self.__session.query(cls).get(id) + + def count(self, cls=None): + """ Counts the number of objects in storage """ + num_objs = 0 + if cls is None: + for cls in classes.values(): + num_objs += self.__session.query(cls).count() + else: + num_objs += self.__session.query(cls).count() + return num_objs diff --git a/models/engine/file_storage.py b/models/engine/file_storage.py index c8cb8c1764d..f5a7b87cc46 100755 --- a/models/engine/file_storage.py +++ b/models/engine/file_storage.py @@ -2,7 +2,6 @@ """ Contains the FileStorage class """ - import json from models.amenity import Amenity from models.base_model import BaseModel @@ -55,7 +54,7 @@ def reload(self): jo = json.load(f) for key in jo: self.__objects[key] = classes[jo[key]["__class__"]](**jo[key]) - except: + except FileNotFoundError: pass def delete(self, obj=None): @@ -68,3 +67,19 @@ def delete(self, obj=None): def close(self): """call reload() method for deserializing the JSON file to objects""" self.reload() + + def get(self, cls, id): + """ Gets a specified object using its class and id """ + if cls is None or id is None: + return None + all_objs = self.all(cls) + key = "{}.{}".format(cls.__name__, id) + + return all_objs.get(key, None) + + def count(self, cls=None): + """ Count all objects or objects of specified class """ + if cls is None: + return len(self.all()) + else: + return len(self.all(cls)) diff --git a/tests/test_models/test_amenity.py b/tests/test_models/test_amenity.py index 66b0bb69bc2..09ce5b06305 100755 --- a/tests/test_models/test_amenity.py +++ b/tests/test_models/test_amenity.py @@ -79,7 +79,6 @@ def test_name_attr(self): def test_to_dict_creates_dict(self): """test to_dict method creates a dictionary with proper attrs""" am = Amenity() - print(am.__dict__) new_d = am.to_dict() self.assertEqual(type(new_d), dict) self.assertFalse("_sa_instance_state" in new_d) diff --git a/tests/test_models/test_engine/test_db_storage.py b/tests/test_models/test_engine/test_db_storage.py index 766e625b5af..c33a29f7823 100755 --- a/tests/test_models/test_engine/test_db_storage.py +++ b/tests/test_models/test_engine/test_db_storage.py @@ -86,3 +86,11 @@ def test_new(self): @unittest.skipIf(models.storage_t != 'db', "not testing db storage") def test_save(self): """Test that save properly saves objects to file.json""" + + @unittest.skipIf(models.storage_t != 'db', "not testing db storage") + def test_get(self): + """ Test if get method returns a specified object from storage """ + + @unittest.skipIf(models.storage_t != 'db', "not testing db storage") + def test_count(self): + """ Test if count methods return the number of objects """ diff --git a/tests/test_models/test_engine/test_file_storage.py b/tests/test_models/test_engine/test_file_storage.py index 1474a34fec0..294adc24d70 100755 --- a/tests/test_models/test_engine/test_file_storage.py +++ b/tests/test_models/test_engine/test_file_storage.py @@ -113,3 +113,11 @@ def test_save(self): with open("file.json", "r") as f: js = f.read() self.assertEqual(json.loads(string), json.loads(js)) + + @unittest.skipIf(models.storage_t == 'db', "not testing file storage") + def test_get(self): + """ Test if get method returns a specified object from storage """ + + @unittest.skipIf(models.storage_t == 'db', "not testing file storage") + def test_count(self): + """ Test if count methods return the number of objects """ diff --git a/web_flask/0-hello_route.py b/web_flask/0-hello_route.py index 194749fecd4..6a3c51ec8b3 100755 --- a/web_flask/0-hello_route.py +++ b/web_flask/0-hello_route.py @@ -1,16 +1,18 @@ #!/usr/bin/python3 """ -starts a Flask web application +Module starts a Flask web applicatio and displays a 'Hello HBNBN' +message. """ - from flask import Flask + + app = Flask(__name__) @app.route('/', strict_slashes=False) -def index(): - """returns Hello HBNB!""" - return 'Hello HBNB!' +def hello(): + return "Hello HBNB!" + -if __name__ == '__main__': - app.run(host='0.0.0.0', port='5000') +if __name__ == "__main__": + app.run(host='0.0.0.0', port=5000, debug=True) diff --git a/web_flask/1-hbnb_route.py b/web_flask/1-hbnb_route.py old mode 100755 new mode 100644 index f1829cfc6dc..5847def1681 --- a/web_flask/1-hbnb_route.py +++ b/web_flask/1-hbnb_route.py @@ -1,22 +1,23 @@ #!/usr/bin/python3 """ -starts a Flask web application +Module starts a Flask web applicatio and displays a 'Hello HBNBN' +message. """ - from flask import Flask + + app = Flask(__name__) @app.route('/', strict_slashes=False) -def index(): - """returns Hello HBNB!""" - return 'Hello HBNB!' +def hello(): + return "Hello HBNB!" @app.route('/hbnb', strict_slashes=False) def hbnb(): - """returns HBNB""" - return 'HBNB' + return "HBNB" + -if __name__ == '__main__': - app.run(host='0.0.0.0', port='5000') +if __name__ == "__main__": + app.run(host='0.0.0.0', port=5000, debug=True) diff --git a/web_flask/10-hbnb_filters.py b/web_flask/10-hbnb_filters.py index b6d8e50f797..26c20ae6af1 100755 --- a/web_flask/10-hbnb_filters.py +++ b/web_flask/10-hbnb_filters.py @@ -1,27 +1,37 @@ #!/usr/bin/python3 """ -starts a Flask web application -""" +Web flask application displays a AirBnB web page with data of states, +cities and amenities loaded from the storage. +Route: + /hbnb_filters - displays a HTML page like 6-index.html done on: + web_static repository +""" from flask import Flask, render_template -from models import * from models import storage +from models.state import State +from models.amenity import Amenity + app = Flask(__name__) @app.route('/hbnb_filters', strict_slashes=False) -def filters(): - """display a HTML page like 6-index.html from static""" - states = storage.all("State").values() - amenities = storage.all("Amenity").values() - return render_template('10-hbnb_filters.html', states=states, - amenities=amenities) +def hbnb_filters(): + """ + Display a page where the states, cities and amenities objects are + loaded from the database storage. + """ + states = storage.all(State) + amenities = storage.all(Amenity) + return render_template( + '10-hbnb_filters.html', states=states, amenities=amenities) @app.teardown_appcontext -def teardown_db(exception): - """closes the storage on teardown""" +def remove_session(exception=None): + """ Close storage session """ storage.close() + if __name__ == '__main__': - app.run(host='0.0.0.0', port='5000') + app.run(host='0.0.0.0', port=5000, debug=True) diff --git a/web_flask/100-hbnb.py b/web_flask/100-hbnb.py new file mode 100644 index 00000000000..b5db729047a --- /dev/null +++ b/web_flask/100-hbnb.py @@ -0,0 +1,34 @@ +#!/usr/bin/python3 +""" +Web flask application that displays the AirBnB clone webpage. Loads the +data from the database storage. +""" +from flask import Flask, render_template +from models import storage +from models.state import State +from models.place import Place +from models.amenity import Amenity + + +app = Flask(__name__) + + +@app.route('/hbnb', strict_slashes=False) +def hbnb(): + """ + Display a page where the states, cities, places and amenities objects are + loaded from the database storage. + """ + states = storage.all(State) + amenities = storage.all(Amenity) + places = storage.all(Place) + return render_template('100-hbnb.html', states=states, amenities=amenities, places=places) + +@app.teardown_appcontext +def remove_session(exception=None): + """ Close storage session """ + storage.close() + + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=5000, debug=True) \ No newline at end of file diff --git a/web_flask/2-c_route.py b/web_flask/2-c_route.py old mode 100755 new mode 100644 index bb3818a09b2..84d367109bb --- a/web_flask/2-c_route.py +++ b/web_flask/2-c_route.py @@ -1,28 +1,29 @@ #!/usr/bin/python3 """ -starts a Flask web application +Module starts a Flask web applicatio and displays a 'Hello HBNBN' +message. """ - from flask import Flask + + app = Flask(__name__) @app.route('/', strict_slashes=False) -def index(): - """returns Hello HBNB!""" - return 'Hello HBNB!' +def hello(): + return "Hello HBNB!" @app.route('/hbnb', strict_slashes=False) def hbnb(): - """returns HBNB""" - return 'HBNB' + return "HBNB" @app.route('/c/', strict_slashes=False) -def cisfun(text): - """display “C ” followed by the value of the text variable""" - return 'C ' + text.replace('_', ' ') +def display_text(text): + text = text.replace('_', ' ') + return f"C {text}" + -if __name__ == '__main__': - app.run(host='0.0.0.0', port='5000') +if __name__ == "__main__": + app.run(host='0.0.0.0', port=5000, debug=True) diff --git a/web_flask/3-python_route.py b/web_flask/3-python_route.py old mode 100755 new mode 100644 index caf0f632694..c1bbd9c72c7 --- a/web_flask/3-python_route.py +++ b/web_flask/3-python_route.py @@ -1,35 +1,36 @@ #!/usr/bin/python3 """ -starts a Flask web application +Module starts a Flask web applicatio and displays a 'Hello HBNBN' +message. """ - from flask import Flask + + app = Flask(__name__) @app.route('/', strict_slashes=False) -def index(): - """returns Hello HBNB!""" - return 'Hello HBNB!' +def hello(): + return "Hello HBNB!" @app.route('/hbnb', strict_slashes=False) def hbnb(): - """returns HBNB""" - return 'HBNB' + return "HBNB" @app.route('/c/', strict_slashes=False) -def cisfun(text): - """display “C ” followed by the value of the text variable""" - return 'C ' + text.replace('_', ' ') +def display_text(text): + text = text.replace('_', ' ') + return f"C {text}" -@app.route('/python', strict_slashes=False) +@app.route('/python/') @app.route('/python/', strict_slashes=False) -def pythoniscool(text='is cool'): - """display “Python ”, followed by the value of the text variable""" - return 'Python ' + text.replace('_', ' ') +def default(text='is cool'): + text = text.replace('_', ' ') + return f"Python {text}" + -if __name__ == '__main__': - app.run(host='0.0.0.0', port='5000') +if __name__ == "__main__": + app.run(host='0.0.0.0', port=5000, debug=True) diff --git a/web_flask/4-number_route.py b/web_flask/4-number_route.py old mode 100755 new mode 100644 index f2948b12e11..d2b7f3b7d92 --- a/web_flask/4-number_route.py +++ b/web_flask/4-number_route.py @@ -1,41 +1,45 @@ #!/usr/bin/python3 """ -starts a Flask web application +Module starts a Flask web applicatio and displays a 'Hello HBNBN' +message. """ +from flask import Flask, abort + -from flask import Flask app = Flask(__name__) @app.route('/', strict_slashes=False) -def index(): - """returns Hello HBNB!""" - return 'Hello HBNB!' +def hello(): + return "Hello HBNB!" @app.route('/hbnb', strict_slashes=False) def hbnb(): - """returns HBNB""" - return 'HBNB' + return "HBNB" @app.route('/c/', strict_slashes=False) -def cisfun(text): - """display “C ” followed by the value of the text variable""" - return 'C ' + text.replace('_', ' ') +def display_text(text): + text = text.replace('_', ' ') + return f"C {text}" -@app.route('/python', strict_slashes=False) +@app.route('/python/') @app.route('/python/', strict_slashes=False) -def pythoniscool(text='is cool'): - """display “Python ”, followed by the value of the text variable""" - return 'Python ' + text.replace('_', ' ') +def default(text='is cool'): + text = text.replace('_', ' ') + return f"Python {text}" + +@app.route('/number/', strict_slashes=False) +def number(n): + try: + num = int(n) + return f"{n} is a number" + except ValueError: + abort(404) -@app.route('/number/', strict_slashes=False) -def imanumber(n): - """display “n is a number” only if n is an integer""" - return "{:d} is a number".format(n) -if __name__ == '__main__': - app.run(host='0.0.0.0', port='5000') +if __name__ == "__main__": + app.run(host='0.0.0.0', port=5000, debug=True) diff --git a/web_flask/5-number_template.py b/web_flask/5-number_template.py old mode 100755 new mode 100644 index 17841a1a3a7..f45f20db593 --- a/web_flask/5-number_template.py +++ b/web_flask/5-number_template.py @@ -1,47 +1,54 @@ #!/usr/bin/python3 """ -starts a Flask web application +Module starts a Flask web applicatio and displays a 'Hello HBNBN' +message. """ +from flask import Flask, abort, render_template + -from flask import Flask, render_template app = Flask(__name__) @app.route('/', strict_slashes=False) -def index(): - """returns Hello HBNB!""" - return 'Hello HBNB!' +def hello(): + return "Hello HBNB!" @app.route('/hbnb', strict_slashes=False) def hbnb(): - """returns HBNB""" - return 'HBNB' + return "HBNB" @app.route('/c/', strict_slashes=False) -def cisfun(text): - """display “C ” followed by the value of the text variable""" - return 'C ' + text.replace('_', ' ') +def display_text(text): + text = text.replace('_', ' ') + return f"C {text}" -@app.route('/python', strict_slashes=False) +@app.route('/python/') @app.route('/python/', strict_slashes=False) -def pythoniscool(text='is cool'): - """display “Python ”, followed by the value of the text variable""" - return 'Python ' + text.replace('_', ' ') +def default(text='is cool'): + text = text.replace('_', ' ') + return f"Python {text}" + +@app.route('/number/', strict_slashes=False) +def number(n): + try: + num = int(n) + return f"{n} is a number" + except ValueError: + abort(404) -@app.route('/number/', strict_slashes=False) -def imanumber(n): - """display “n is a number” only if n is an integer""" - return "{:d} is a number".format(n) +@app.route('/number_template/', strict_slashes=False) +def display_page(n): + try: + number = int(n) + return render_template('5-number.html', number=number) + except ValueError: + abort(404) -@app.route('/number_template/', strict_slashes=False) -def numbersandtemplates(n): - """display a HTML page only if n is an integer""" - return render_template('5-number.html', n=n) -if __name__ == '__main__': - app.run(host='0.0.0.0', port='5000') +if __name__ == "__main__": + app.run(host='0.0.0.0', port=5000, debug=True) diff --git a/web_flask/6-number_odd_or_even.py b/web_flask/6-number_odd_or_even.py old mode 100755 new mode 100644 index 7875c4c7b32..956a252f3ff --- a/web_flask/6-number_odd_or_even.py +++ b/web_flask/6-number_odd_or_even.py @@ -1,58 +1,63 @@ #!/usr/bin/python3 """ -starts a Flask web application +Module starts a Flask web applicatio and displays a 'Hello HBNBN' +message. """ +from flask import Flask, abort, render_template + -from flask import Flask, render_template app = Flask(__name__) @app.route('/', strict_slashes=False) -def index(): - """returns Hello HBNB!""" - return 'Hello HBNB!' +def hello(): + return "Hello HBNB!" @app.route('/hbnb', strict_slashes=False) def hbnb(): - """returns HBNB""" - return 'HBNB' + return "HBNB" @app.route('/c/', strict_slashes=False) -def cisfun(text): - """display “C ” followed by the value of the text variable""" - return 'C ' + text.replace('_', ' ') +def display_text(text): + text = text.replace('_', ' ') + return f"C {text}" -@app.route('/python', strict_slashes=False) +@app.route('/python/') @app.route('/python/', strict_slashes=False) -def pythoniscool(text='is cool'): - """display “Python ”, followed by the value of the text variable""" - return 'Python ' + text.replace('_', ' ') +def default(text='is cool'): + text = text.replace('_', ' ') + return f"Python {text}" + +@app.route('/number/', strict_slashes=False) +def number(n): + try: + num = int(n) + return f"{n} is a number" + except ValueError: + abort(404) -@app.route('/number/', strict_slashes=False) -def imanumber(n): - """display “n is a number” only if n is an integer""" - return "{:d} is a number".format(n) +@app.route('/number_template/', strict_slashes=False) +def display_page(n): + try: + number = int(n) + return render_template('5-number.html', number=number) + except ValueError: + abort(404) -@app.route('/number_template/', strict_slashes=False) -def numbersandtemplates(n): - """display a HTML page only if n is an integer""" - return render_template('5-number.html', n=n) +@app.route('/number_odd_or_even/', strict_slashes=False) +def number_dd_or_even(n): + try: + number = int(n) + return render_template('6-number_odd_or_even.html', number=number) + except ValueError: + abort(404) -@app.route('/number_odd_or_even/', strict_slashes=False) -def numbersandevenness(n): - """display a HTML page only if n is an integer""" - if n % 2 == 0: - evenness = 'even' - else: - evenness = 'odd' - return render_template('6-number_odd_or_even.html', n=n, - evenness=evenness) -if __name__ == '__main__': - app.run(host='0.0.0.0', port='5000') +if __name__ == "__main__": + app.run(host='0.0.0.0', port=5000, debug=True) diff --git a/web_flask/7-states_list.py b/web_flask/7-states_list.py index fa9ae29ae54..8481c4d4bd3 100755 --- a/web_flask/7-states_list.py +++ b/web_flask/7-states_list.py @@ -1,25 +1,41 @@ #!/usr/bin/python3 """ -starts a Flask web application +Script starts a Flask web application: +Requirements: + - Your web application must be listening on 0.0.0.0, port 5000 + - You must use storage for fetching data from the storage engine + (FileStorage or DBStorage) => from models import storage and + storage.all(...) + - After each request you must remove the current SQLAlchemy Session: + - Declare a method to handle @app.teardown_appcontext + - Call in this method storage.close() + - Routes: + /states_list: display a HTML page: (inside the tag BODY) + H1 tag: “States” + UL tag: with the list of all State objects present in DBStorage + sorted by name (A->Z) tip + LI tag: description of one State: : + - Import this 7-dump to have some data + - You must use the option strict_slashes=False in your route definition """ - from flask import Flask, render_template -from models import * from models import storage -app = Flask(__name__) +from models.state import State -@app.route('/states_list', strict_slashes=False) -def states_list(): - """display a HTML page with the states listed in alphabetical order""" - states = sorted(list(storage.all("State").values()), key=lambda x: x.name) - return render_template('7-states_list.html', states=states) +app = Flask(__name__) @app.teardown_appcontext -def teardown_db(exception): - """closes the storage on teardown""" +def delete_session(exception=None): storage.close() + +@app.route('/states_list', strict_slashes=False) +def list_states(): + states = storage.all(State).values() + return render_template('7-states_list.html', states=states) + + if __name__ == '__main__': - app.run(host='0.0.0.0', port='5000') + app.run(host='0.0.0.0', port=5000) diff --git a/web_flask/8-cities_by_states.py b/web_flask/8-cities_by_states.py index 3d2ae2b733a..af566f61b21 100755 --- a/web_flask/8-cities_by_states.py +++ b/web_flask/8-cities_by_states.py @@ -1,25 +1,36 @@ #!/usr/bin/python3 """ -starts a Flask web application +Script starts a Flask web application: +Requirements: + - Routes: + /states_list: display a HTML page: (inside the tag BODY) + H1 tag: “States” + UL tag: with the list of all State objects present in DBStorage + sorted by name (A->Z) tip + LI tag: description of one State: : """ - from flask import Flask, render_template -from models import * from models import storage -app = Flask(__name__) +from models.state import State -@app.route('/cities_by_states', strict_slashes=False) -def cities_by_states(): - """display the states and cities listed in alphabetical order""" - states = storage.all("State").values() - return render_template('8-cities_by_states.html', states=states) +app = Flask(__name__) @app.teardown_appcontext -def teardown_db(exception): - """closes the storage on teardown""" +def delete_session(exception=None): + """ Closes the current session after each request """ storage.close() + +@app.route('/cities_by_states', strict_slashes=False) +def list_cities_states(): + """ Displays a HTML that contains a list of all State objects """ + # Get records of the states + states = storage.all(State).values() + return render_template( + '8-cities_by_states.html', states=states) + + if __name__ == '__main__': - app.run(host='0.0.0.0', port='5000') + app.run(host='0.0.0.0', port=5000, debug=True) diff --git a/web_flask/9-states.py b/web_flask/9-states.py index cf9f5a704ce..f4d24dede0e 100755 --- a/web_flask/9-states.py +++ b/web_flask/9-states.py @@ -1,28 +1,37 @@ #!/usr/bin/python3 """ -starts a Flask web application +Script starts a Flask web application: +Routes: + /states - list all states from storage + /states/ - list all cities in a specified state """ - from flask import Flask, render_template -from models import * from models import storage -app = Flask(__name__) +from models.state import State -@app.route('/states', strict_slashes=False) -@app.route('/states/', strict_slashes=False) -def states(state_id=None): - """display the states and cities listed in alphabetical order""" - states = storage.all("State") - if state_id is not None: - state_id = 'State.' + state_id - return render_template('9-states.html', states=states, state_id=state_id) +app = Flask(__name__) @app.teardown_appcontext -def teardown_db(exception): - """closes the storage on teardown""" +def delete_session(exception=None): storage.close() + +@app.route('/states', strict_slashes=False) +@app.route('/states/', strict_slashes=False) +def list_states_cities(id=None): + """ + Displays a HTML page witha lists of all states if id=None or + a list of cities of the specified states. + """ + # Get list of states + states = storage.all(State) + state_key = None + if id is not None: + state_key = "State." + id + return render_template('9-states.html', state_key=state_key, states=states) + + if __name__ == '__main__': - app.run(host='0.0.0.0', port='5000') + app.run(host='0.0.0.0', port=5000, debug=True) diff --git a/web_flask/README.md b/web_flask/README.md index 01a9f866f51..4dd05d3f2dd 100644 --- a/web_flask/README.md +++ b/web_flask/README.md @@ -1 +1,15 @@ -# 0x04. AirBnB clone - Web framework +

HBNB Web Flask

+11;rgb:0000/0000/0000 +This repository is a continuation of the already existing HBNB project. This stage of the project implements a Flask application that provides an easy way to define the route of our web application. The Jinja2 template which is readily availabe in Flask allows the application to generate dynamic contents. In the previous project, a data storage was implemented. This project the data stored is queried, filtered and used within our templates. When a valid route is accessed the page generated can be displayed back to the user that made the request. + +N:B GET request method is used in all the application routes. + +--- + +

Repository Contents by Project Task

+ +|Tasks | Files | Description | +| ------ | ----- | ----- | +| 0. route: / | 0-hello_route.py | display "Hello HBNB!" +| 1. route: /hbnb | 1-hbnb_route.py | display "HBNB" +| 2. route: /c/ | 2.c_route.py | display "C" followed by the value of the text variable(replacing undersore _ symbols with a space) diff --git a/web_static/images/icon_bath.png b/web_flask/static/images/icon_bath.png similarity index 100% rename from web_static/images/icon_bath.png rename to web_flask/static/images/icon_bath.png diff --git a/web_static/images/icon_bed.png b/web_flask/static/images/icon_bed.png similarity index 100% rename from web_static/images/icon_bed.png rename to web_flask/static/images/icon_bed.png diff --git a/web_static/images/icon_group.png b/web_flask/static/images/icon_group.png similarity index 100% rename from web_static/images/icon_group.png rename to web_flask/static/images/icon_group.png diff --git a/web_flask/static/images/icon_pets.png b/web_flask/static/images/icon_pets.png new file mode 100644 index 00000000000..6d5c0b9a0c4 Binary files /dev/null and b/web_flask/static/images/icon_pets.png differ diff --git a/web_flask/static/images/icon_tv.png b/web_flask/static/images/icon_tv.png new file mode 100644 index 00000000000..1cf25087ed5 Binary files /dev/null and b/web_flask/static/images/icon_tv.png differ diff --git a/web_flask/static/images/icon_wifi.png b/web_flask/static/images/icon_wifi.png new file mode 100644 index 00000000000..abbb643c9c1 Binary files /dev/null and b/web_flask/static/images/icon_wifi.png differ diff --git a/web_flask/static/styles/3-footer.css b/web_flask/static/styles/3-footer.css index 19fe711abab..05030ebbeea 100644 --- a/web_flask/static/styles/3-footer.css +++ b/web_flask/static/styles/3-footer.css @@ -1,11 +1,10 @@ -footer { - position: fixed; - bottom: 0; - width: 100%; +footer{ background-color: white; height: 60px; - border-top: 1px solid #CCCCCC; - display: flex; - justify-content: center; - align-items: center; -} + width: 100%; + border-top: 1px solid #cccccc; + text-align: center; + line-height: 60px; + flex: 0 0 auto; /*ensures footer does not stretch*/ + margin-top: auto; +} \ No newline at end of file diff --git a/web_flask/static/styles/3-header.css b/web_flask/static/styles/3-header.css index 70dd644d3ed..a2776b201dc 100644 --- a/web_flask/static/styles/3-header.css +++ b/web_flask/static/styles/3-header.css @@ -1,15 +1,11 @@ -header { - width: 100%; +header{ background-color: white; height: 70px; - border-bottom: 1px solid #CCCCCC; - display: flex; - align-items: center; -} - -.logo { - width: 142px; - height: 60px; - background: url("../images/logo.png") no-repeat center; + width: 100%; padding-left: 20px; + border-bottom: 1px solid #cccccc; + background-image: url("../images/logo.png"); + background-position: 0 center; + background-repeat: no-repeat; + flex: 0 0 auto; /*ensures header does not stretch*/ } diff --git a/web_flask/static/styles/4-common.css b/web_flask/static/styles/4-common.css index 1f2f05d3853..f500192a4b2 100644 --- a/web_flask/static/styles/4-common.css +++ b/web_flask/static/styles/4-common.css @@ -1,15 +1,19 @@ -body { +*{ + box-sizing: border-box; +} +body{ + display: flex; + flex-direction: column; + height: 100vh; margin: 0; padding: 0; color: #484848; font-size: 14px; font-family: Circular,"Helvetica Neue",Helvetica,Arial,sans-serif; } - -.container { +.container{ + width: 100%; max-width: 1000px; - margin-top: 30px; - margin-bottom: 30px; - margin-left: auto; - margin-right: auto; -} + margin: 30px auto; + flex: 0 0 auto; +} \ No newline at end of file diff --git a/web_flask/static/styles/6-filters.css b/web_flask/static/styles/6-filters.css index 1c23a0fde82..980fa35efe6 100644 --- a/web_flask/static/styles/6-filters.css +++ b/web_flask/static/styles/6-filters.css @@ -1,91 +1,69 @@ -.filters { +section.filters{ + display: flex; + align-items: center; background-color: white; height: 70px; width: 100%; - border: 1px solid #DDDDDD; - border-radius: 4px; - display: flex; - align-items: center; + border: 1px solid #dddddd; + padding-right: 30px; } - -section.filters > button{ +section.filters button{ font-size: 18px; - color: white; - background-color: #FF5A5F; + background-color: #ff5a5f; + color: #ffffff; height: 48px; - border: 0px; - border-radius: 4px; width: 20%; - margin-left: auto; - margin-right: 30px; - opacity: 1; + border: 0; + border-radius: 4px; + margin-left: auto; /*push button to the left*/ } - -section.filters > button:hover { - opacity: 0.9; +section.filters button:hover{ + opacity: 90%; } - -.locations, .amenities { +section.filters div.locations, section.filters div.amenities{ height: 100%; width: 25%; - padding-left: 50px; } - -.locations { - border-right: 1px solid #DDDDDD; +section.filters div.locations{ + border-right: 1px solid #dddddd; } -.locations > h3, .amenities > h3 { + +section.filters div.locations h3, section.filters div.amenities h3{ font-weight: 600; - margin: 12px 0 5px 0; + margin: 17px; } -.locations > h4, .amenities > h4 { +section.filters div.locations h4, section.filters div.amenities h4{ font-weight: 400; font-size: 14px; - margin: 0 0 5px 0; + margin: 0 0 0 30px; } - -.popover { - display: none; +ul.popover{ position: relative; - left: -51px; - background-color: #FAFAFA; + display: none; width: 100%; - border: 1px solid #DDDDDD; - border-radius: 4px; + background-color: #fafafa; + border: 1px solid #dddddd; + padding: 10px 15px; z-index: 1; - padding: 30px 50px 30px 0; - margin-top: 17px; -} - -.popover, .popover ul { - list-style-type: none; -} -.locations:hover > .popover { - display: block; -} - -.amenities:hover > .popover { - display: block; + overflow-y: auto; + max-height: 300px; + margin-top: -1px } - -.popover h2 { - margin-top: 0px; - margin-bottom: 5px; +ul.popover h2{ + font-size: 16px; + margin: 0; } - -.locations > .popover > li { - margin-bottom: 30px; - margin-left: 30px; +ul.popover ul{ + margin: 0; + padding: 4px 0 10px 10px; } -.locations > .popover > li > ul { - padding-left: 20px; -} -.locations > .popover > li > ul > li { - margin-bottom: 10px; +ul.popover, ul.popover ul{ + list-style-type: none; } -.amenities > .popover > li { - margin-left: 50px; - margin-bottom: 10px; +/*hide the filter popover*/ +div.locations:hover ul.popover, +div.amenities:hover ul.popover{ + display: block; } diff --git a/web_flask/static/styles/8-places.css b/web_flask/static/styles/8-places.css new file mode 100644 index 00000000000..37442be7843 --- /dev/null +++ b/web_flask/static/styles/8-places.css @@ -0,0 +1,61 @@ +section.places h1{ + font-size: 30px; + text-align: left; +} +.articles-section{ + display: flex; + flex-wrap: wrap; + justify-content: center; +} +section.places article{ + position: relative; + width: 390px; + padding: 20px; + margin: 20px; + border: 1px solid #ff5a5f; + border-radius: 4px; +} +section.places article h2{ + font-size: 30px; + text-align: center; + margin: 5px 60px 40px 0; +} +article div.price_by_night{ + position: absolute; + top: 15px; + right: 15px; + color: #ff5a5f; + border: 4px solid #ff5a5f; + border-radius: 50%; + min-width: 60px; + height: 60px; + font-size: 20px; + text-align: center; + line-height: 60px; +} +.information{ + display: flex; + align-items: center; + justify-content: center; + height: 80px; + border-top: 1px solid #dddddd; + border-bottom: 1px solid #dddddd; +} +.information div{ + width: 100px; + height: 35px; + text-align: center; +} +.information div .icon{ + width: 100%; + height: 80%; +} +.information div .icon i{ + font-size: 20px; +} +article div.user{ + margin: 10px 0; +} +article div.user span{ + font-weight: bold; +} diff --git a/web_flask/templates/10-hbnb_filters.html b/web_flask/templates/10-hbnb_filters.html index 261634b693f..a6b6d825530 100644 --- a/web_flask/templates/10-hbnb_filters.html +++ b/web_flask/templates/10-hbnb_filters.html @@ -1,55 +1,48 @@ - - - - - - - - AirBnb Clone - - -
- -
-
-
-
-

States

-

 

-
    - {% for state in states|sort(attribute='name') %} -
  • -

    {{ state.name }}:

    -
      - {% for city in state.cities|sort(attribute='name') %} -
    • {{ city.name }}
    • - {% endfor %} + + AirBnB clone + + + + + + + + + + + + +
      +
      +
      +

      States

      +

       

      +
        + {% for state in states.values()|sort(attribute='name')%} +
      • {{ state.name }}

        +
          + {% for city in state.cities|sort(attribute='name') %} +
        • {{ city.name }}
        • + {% endfor %} +
        +
      • + {% endfor %}
      - - {% endfor %} -
    -
-
-

Amenities

-

 

-
    - {% for amenity in amenities|sort(attribute='name') %} -
  • {{ amenity.name }}
  • - {% endfor %} -
+
+
+

Amenities

+

 

+
    + {% for amenity in amenities.values()|sort(attribute='name') %} +
  • {{ amenity.name }}
  • + {% endfor %} +
+
+ +
- - - -
-

- Holberton School -

-
- - +
Best School
+ + \ No newline at end of file diff --git a/web_flask/templates/100-hbnb.html b/web_flask/templates/100-hbnb.html new file mode 100644 index 00000000000..ef51b2529f8 --- /dev/null +++ b/web_flask/templates/100-hbnb.html @@ -0,0 +1,70 @@ + + + + AirBnB + + + + + + + + + + + + + +
+
+
+
+

States

+

 

+
    + {% for state in states.values()|sort(attribute='name')%} +
  • {{ state.name }}

    +
      + {% for city in state.cities|sort(attribute='name') %} +
    • {{ city.name }}
    • + {% endfor %} +
    +
  • + {% endfor %} +
+
+
+

Amenities

+

 

+
    + {% for amenity in amenities.values()|sort(attribute='name') %} +
  • {{ amenity.name }}
  • + {% endfor %} +
+
+ +
+
+

Places

+
+ {% for place in places.values()|sort(attribute='name')%} +
+

{{ place.name }}

+
${{ place.price_by_night }}
+
+
{{ place.max_guest }} Guests
+
{{ place.number_rooms }} Bedroom
+
{{ place.number_bathrooms }} Bathroom
+
+
Owner: John Lennon
+
+

{{ place.description|safe }}

+
+
+ {% endfor %} +
+
+
+
Best School
+ + diff --git a/web_flask/templates/5-number.html b/web_flask/templates/5-number.html index 57887ff7469..2b196a9c567 100644 --- a/web_flask/templates/5-number.html +++ b/web_flask/templates/5-number.html @@ -4,6 +4,6 @@ HBNB -

Number: {{ n }}

+

Number: {{ number }}

diff --git a/web_flask/templates/6-number_odd_or_even.html b/web_flask/templates/6-number_odd_or_even.html index 188839655c4..b8d5f084455 100644 --- a/web_flask/templates/6-number_odd_or_even.html +++ b/web_flask/templates/6-number_odd_or_even.html @@ -4,6 +4,10 @@ HBNB -

Number: {{ n }} is {{ evenness }}

+ {% if number%2 == 0 %} +

Number: {{ number }} is even

+ {% else %} +

Number: {{ number }} is odd

+ {% endif %} diff --git a/web_flask/templates/7-states_list.html b/web_flask/templates/7-states_list.html index a274a32608f..79ad52db0a1 100644 --- a/web_flask/templates/7-states_list.html +++ b/web_flask/templates/7-states_list.html @@ -6,9 +6,9 @@

States

    - {% for state in states %} -
  • {{ state.id }}: {{ state.name }}
  • - {% endfor %} + {% for state in states|sort(attribute='name') %} +
  • {{ state.id }}: {{ state.name }}
  • + {% endfor %}
diff --git a/web_flask/templates/8-cities_by_states.html b/web_flask/templates/8-cities_by_states.html index 94993e8860c..ff5a6c6cbf1 100644 --- a/web_flask/templates/8-cities_by_states.html +++ b/web_flask/templates/8-cities_by_states.html @@ -6,15 +6,15 @@

States

    - {% for state in states|sort(attribute='name') %} -
  • {{ state.id }}: {{ state.name }} -
      - {% for city in state.cities|sort(attribute='name') %} -
    • {{ city.id }}: {{ city.name }}
    • - {% endfor %} + {% for state in states|sort(attribute='name') %} +
    • {{ state.id }}: {{ state.name }} +
        {{ city.id }}: {{ city.name }} + {% endfor %}
    • - {% endfor %} -
    + {% endfor %} +
diff --git a/web_flask/templates/9-states.html b/web_flask/templates/9-states.html index 34d40291c01..6c1dc24eb4a 100644 --- a/web_flask/templates/9-states.html +++ b/web_flask/templates/9-states.html @@ -4,24 +4,26 @@ HBNB - {% if not state_id %} + + {% if not state_key %}

States

-
    +
      {% for state in states.values()|sort(attribute='name') %} -
    • {{ state.id }}: {{ state.name }}
    • - {% endfor %} +
    • {{ state.id }}: {{ state.name }}
    • + {% endfor %}
    - {% elif state_id in states%} - {% set state = states[state_id] %} + {% elif state_key in states %} + {% set state = states[state_key] %}

    State: {{ state.name }}

    -

    Cities

    -
      - {% for city in state.cities|sort(attribute='name') %} -
    • {{ city.id }}: {{ city.name }}
    • - {% endfor %} -
    +

    Cities:

    +
      + {% for city in state.cities|sort(attribute='name') %} +
    • {{ city.id }}: {{city.name}}
    • + {% endfor %} +
    {% else %}

    Not found!

    {% endif %} + diff --git a/web_static/0-index.html b/web_static/0-index.html index a79ac87bffa..efd2060b7c9 100644 --- a/web_static/0-index.html +++ b/web_static/0-index.html @@ -1,16 +1,10 @@ - + - - AirBnB Clone + AirBnB - -
    -
    -
    -

    - Holberton School -

    -
    + +
    +
    Best School
    diff --git a/web_static/1-index.html b/web_static/1-index.html index 26f13b490f9..706f9c34be8 100644 --- a/web_static/1-index.html +++ b/web_static/1-index.html @@ -1,22 +1,30 @@ - + - - AirBnb Clone + AirBnB -
    -
    -
    -

    - Holberton School -

    -
    +
    +
    Best School
    diff --git a/web_static/100-index.html b/web_static/100-index.html new file mode 100644 index 00000000000..5c0c93aa0bf --- /dev/null +++ b/web_static/100-index.html @@ -0,0 +1,157 @@ + + + + AirBnB + + + + + + + + + + + +
    +
    +
    +
    +

    States

    +

    California, Arizona...

    +
      +

      Arizona:

      +
        +
      • Phoenix
      • +
      • Tucson
      • +
      +

      California:

      +
        +
      • San Francisco
      • +
      • San Diego
      • +
      +
    +
    +
    +

    Amenities

    +

    Internet, Kitchen...

    +
      +
    • Internet
    • +
    • TV
    • +
    • Kitchen
    • +
    • Iron
    • +
    +
    + +
    +
    +

    Places

    +
    +
    +

    My home

    +
    $80
    +
    +
    2 Guests
    +
    1 Bedroom
    +
    1 Bathroom
    +
    +
    Owner: John Lennon
    +
    +

    This is a lovely 1 bedroom 1 bathroom apartment that can accomodate 2 people. It is located + at the center of Shanghai and next to the subway line 1. 1 stop to People Square, 2 stops to + Bund, 3 stops to Jingnan Temple. +

    +
    +
    +

    Amenities

    +
      +
    • TV
    • +
    • Wifi
    • +
    +
    +
    +

    2 Reviews

    +
      +
    • +

      From Bob Dylan the 27th January 2024

      +

      John is an epic host. Nothing more to add.

      +
    • +
    • +

      From Sharlene Pie the 12th May 2023

      +

      Fantastic place, to spend quality time with a loved one!

      +
    • +
    +
    +
    +
    +

    Tiny house

    +
    $65
    +
    +
    4 Guests
    +
    2 Bedroom
    +
    1 Bathroom
    +
    +
    Owner: Adrienne
    +
    +

    Our place is a private, affordable stand-alone guests house centrally located just minutes to French Quarters, downtown, the Fair Grounds + (Jazz Fest) and City Park. The guest house is a quaint 400 square ft, with a full bath, mini kitchen & living room. The + extra high ceilings make the home feel more spacious. The sofa converts to a bed also. We have a hand made counter area that adds character + to the room and a great porch/deck to relax on and have a glass of Meriot. +

    +
    +
    +

    Amenities

    +
      +
    • TV
    • +
    +
    +
    +

    Reviews

    +
      +
    • +

      From Chris bethrsed the 14th February 2022

      +

      I love this tiny, quite spacious on the inside. Definitely coming again

      +
    • +
    +
    +
    +
    +

    A suite

    +
    $190
    +
    +
    6 Guests
    +
    5 Bedroom
    +
    3 Bathroom
    +
    +
    Owner: Salita Emmanuel
    +
    +

    Our place is a suite, affordable stand-alone guests house centrally located just minutes to French Quarters, downtown, the Fair Grounds + (Jazz Fest) and City Park. The guest house is a quaint 400 square ft, with a full bath, mini kitchen & living room. The + extra high ceilings make the home feel more spacious. The sofa converts to a bed also. We have a hand made counter area that adds character + to the room and a great porch/deck to relax on and have a glass of Meriot. +

    +
    +
    +

    Amenities

    +
      +
    • TV
    • +
    • Wifi
    • +
    • Pets
    • +
    +
    +
    +

    Reviews

    +
      +
    • +

      From James Gordon the 26th December 2023

      +

      This is truly an amazing family vacation house, I was able to accomodate everyone and my family had a great time.

      +
    • +
    +
    +
    +
    +
    +
    +
    Best School
    + + diff --git a/web_static/101-index.html b/web_static/101-index.html new file mode 100644 index 00000000000..b3442f3f331 --- /dev/null +++ b/web_static/101-index.html @@ -0,0 +1,157 @@ + + + + AirBnB + + + + + + + + + + + +
    +
    +
    +
    +

    States

    +

    California, Arizona...

    +
      +

      Arizona:

      +
        +
      • Phoenix
      • +
      • Tucson
      • +
      +

      California:

      +
        +
      • San Francisco
      • +
      • San Diego
      • +
      +
    +
    +
    +

    Amenities

    +

    Internet, Kitchen...

    +
      +
    • Internet
    • +
    • TV
    • +
    • Kitchen
    • +
    • Iron
    • +
    +
    + +
    +
    +

    Places

    +
    +
    +

    My home

    +
    $80
    +
    +
    2 Guests
    +
    1 Bedroom
    +
    1 Bathroom
    +
    +
    Owner: John Lennon
    +
    +

    This is a lovely 1 bedroom 1 bathroom apartment that can accomodate 2 people. It is located + at the center of Shanghai and next to the subway line 1. 1 stop to People Square, 2 stops to + Bund, 3 stops to Jingnan Temple. +

    +
    +
    +

    Amenities

    +
      +
    • TV
    • +
    • Wifi
    • +
    +
    +
    +

    2 Reviews

    +
      +
    • +

      From Bob Dylan the 27th January 2024

      +

      John is an epic host. Nothing more to add.

      +
    • +
    • +

      From Sharlene Pie the 12th May 2023

      +

      Fantastic place, to spend quality time with a loved one!

      +
    • +
    +
    +
    +
    +

    Tiny house

    +
    $65
    +
    +
    4 Guests
    +
    2 Bedroom
    +
    1 Bathroom
    +
    +
    Owner: Adrienne
    +
    +

    Our place is a private, affordable stand-alone guests house centrally located just minutes to French Quarters, downtown, the Fair Grounds + (Jazz Fest) and City Park. The guest house is a quaint 400 square ft, with a full bath, mini kitchen & living room. The + extra high ceilings make the home feel more spacious. The sofa converts to a bed also. We have a hand made counter area that adds character + to the room and a great porch/deck to relax on and have a glass of Meriot. +

    +
    +
    +

    Amenities

    +
      +
    • TV
    • +
    +
    +
    +

    Reviews

    +
      +
    • +

      From Chris bethrsed the 14th February 2022

      +

      I love this tiny, quite spacious on the inside. Definitely coming again

      +
    • +
    +
    +
    +
    +

    A suite

    +
    $190
    +
    +
    6 Guests
    +
    5 Bedroom
    +
    3 Bathroom
    +
    +
    Owner: Salita Emmanuel
    +
    +

    Our place is a suite, affordable stand-alone guests house centrally located just minutes to French Quarters, downtown, the Fair Grounds + (Jazz Fest) and City Park. The guest house is a quaint 400 square ft, with a full bath, mini kitchen & living room. The + extra high ceilings make the home feel more spacious. The sofa converts to a bed also. We have a hand made counter area that adds character + to the room and a great porch/deck to relax on and have a glass of Meriot. +

    +
    +
    +

    Amenities

    +
      +
    • TV
    • +
    • Wifi
    • +
    • Pets
    • +
    +
    +
    +

    Reviews

    +
      +
    • +

      From James Gordon the 26th December 2023

      +

      This is truly an amazing family vacation house, I was able to accomodate everyone and my family had a great time.

      +
    • +
    +
    +
    +
    +
    +
    +
    Best School
    + + diff --git a/web_static/102-index.html b/web_static/102-index.html new file mode 100644 index 00000000000..dd70319fc25 --- /dev/null +++ b/web_static/102-index.html @@ -0,0 +1,161 @@ + + + + AirBnB + + + + + + + + + + + + + +
    +
    +
    +
    +
    +

    States

    +

    California, Arizona...

    +
      +

      Arizona:

      +
        +
      • Phoenix
      • +
      • Tucson
      • +
      +

      California:

      +
        +
      • San Francisco
      • +
      • San Diego
      • +
      +
    +
    +
    +

    Amenities

    +

    Internet, Kitchen...

    +
      +
    • Internet
    • +
    • TV
    • +
    • Kitchen
    • +
    • Iron
    • +
    +
    +
    + +
    +
    +

    Places

    +
    +
    +

    My home

    +
    $80
    +
    +
    2 Guests
    +
    1 Bedroom
    +
    1 Bathroom
    +
    +
    Owner: John Lennon
    +
    +

    This is a lovely 1 bedroom 1 bathroom apartment that can accomodate 2 people. It is located + at the center of Shanghai and next to the subway line 1. 1 stop to People Square, 2 stops to + Bund, 3 stops to Jingnan Temple. +

    +
    +
    +

    Amenities

    +
      +
    • TV
    • +
    • Wifi
    • +
    +
    +
    +

    2 Reviews

    +
      +
    • +

      From Bob Dylan the 27th January 2024

      +

      John is an epic host. Nothing more to add.

      +
    • +
    • +

      From Sharlene Pie the 12th May 2023

      +

      Fantastic place, to spend quality time with a loved one!

      +
    • +
    +
    +
    +
    +

    Tiny house

    +
    $65
    +
    +
    4 Guests
    +
    2 Bedroom
    +
    1 Bathroom
    +
    +
    Owner: Adrienne
    +
    +

    Our place is a private, affordable stand-alone guests house centrally located just minutes to French Quarters, downtown, the Fair Grounds + (Jazz Fest) and City Park. The guest house is a quaint 400 square ft, with a full bath, mini kitchen & living room. The + extra high ceilings make the home feel more spacious. The sofa converts to a bed also. We have a hand made counter area that adds character + to the room and a great porch/deck to relax on and have a glass of Meriot. +

    +
    +
    +

    Amenities

    +
      +
    • TV
    • +
    +
    +
    +

    Reviews

    +
      +
    • +

      From Chris bethrsed the 14th February 2022

      +

      I love this tiny, quite spacious on the inside. Definitely coming again

      +
    • +
    +
    +
    +
    +

    A suite

    +
    $190
    +
    +
    6 Guests
    +
    5 Bedroom
    +
    3 Bathroom
    +
    +
    Owner: Salita Emmanuel
    +
    +

    Our place is a suite, affordable stand-alone guests house centrally located just minutes to French Quarters, downtown, the Fair Grounds + (Jazz Fest) and City Park. The guest house is a quaint 400 square ft, with a full bath, mini kitchen & living room. The + extra high ceilings make the home feel more spacious. The sofa converts to a bed also. We have a hand made counter area that adds character + to the room and a great porch/deck to relax on and have a glass of Meriot. +

    +
    +
    +

    Amenities

    +
      +
    • TV
    • +
    • Wifi
    • +
    • Pets
    • +
    +
    +
    +

    Reviews

    +
      +
    • +

      From James Gordon the 26th December 2023

      +

      This is truly an amazing family vacation house, I was able to accomodate everyone and my family had a great time.

      +
    • +
    +
    +
    +
    +
    +
    +
    Best School
    + + diff --git a/web_static/103-index.html b/web_static/103-index.html new file mode 100644 index 00000000000..b4b14c7aab8 --- /dev/null +++ b/web_static/103-index.html @@ -0,0 +1,161 @@ + + + + AirBnB + + + + + + + + + + + + + +
    +
    +
    +
    +
    +

    States

    +

    California, Arizona...

    +
      +

      Arizona:

      +
        +
      • Phoenix
      • +
      • Tucson
      • +
      +

      California:

      +
        +
      • San Francisco
      • +
      • San Diego
      • +
      +
    +
    +
    +

    Amenities

    +

    Internet, Kitchen...

    +
      +
    • Internet
    • +
    • TV
    • +
    • Kitchen
    • +
    • Iron
    • +
    +
    +
    + +
    +
    +

    Places

    +
    +
    +

    My home

    +
    $80
    +
    +
    2 Guests
    +
    1 Bedroom
    +
    1 Bathroom
    +
    +
    Owner: John Lennon
    +
    +

    This is a lovely 1 bedroom 1 bathroom apartment that can accomodate 2 people. It is located + at the center of Shanghai and next to the subway line 1. 1 stop to People Square, 2 stops to + Bund, 3 stops to Jingnan Temple. +

    +
    +
    +

    Amenities

    +
      +
    • TV
    • +
    • Wifi
    • +
    +
    +
    +

    2 Reviews

    +
      +
    • +

      From Bob Dylan the 27th January 2024

      +

      John is an epic host. Nothing more to add.

      +
    • +
    • +

      From Sharlene Pie the 12th May 2023

      +

      Fantastic place, to spend quality time with a loved one!

      +
    • +
    +
    +
    +
    +

    Tiny house

    +
    $65
    +
    +
    4 Guests
    +
    2 Bedroom
    +
    1 Bathroom
    +
    +
    Owner: Adrienne
    +
    +

    Our place is a private, affordable stand-alone guests house centrally located just minutes to French Quarters, downtown, the Fair Grounds + (Jazz Fest) and City Park. The guest house is a quaint 400 square ft, with a full bath, mini kitchen & living room. The + extra high ceilings make the home feel more spacious. The sofa converts to a bed also. We have a hand made counter area that adds character + to the room and a great porch/deck to relax on and have a glass of Meriot. +

    +
    +
    +

    Amenities

    +
      +
    • TV
    • +
    +
    +
    +

    Reviews

    +
      +
    • +

      From Chris bethrsed the 14th February 2022

      +

      I love this tiny, quite spacious on the inside. Definitely coming again

      +
    • +
    +
    +
    +
    +

    A suite

    +
    $190
    +
    +
    6 Guests
    +
    5 Bedroom
    +
    3 Bathroom
    +
    +
    Owner: Salita Emmanuel
    +
    +

    Our place is a suite, affordable stand-alone guests house centrally located just minutes to French Quarters, downtown, the Fair Grounds + (Jazz Fest) and City Park. The guest house is a quaint 400 square ft, with a full bath, mini kitchen & living room. The + extra high ceilings make the home feel more spacious. The sofa converts to a bed also. We have a hand made counter area that adds character + to the room and a great porch/deck to relax on and have a glass of Meriot. +

    +
    +
    +

    Amenities

    +
      +
    • TV
    • +
    • Wifi
    • +
    • Pets
    • +
    +
    +
    +

    Reviews

    +
      +
    • +

      From James Gordon the 26th December 2023

      +

      This is truly an amazing family vacation house, I was able to accomodate everyone and my family had a great time.

      +
    • +
    +
    +
    +
    +
    +
    +
    Best School
    + + diff --git a/web_static/2-index.html b/web_static/2-index.html index b84d4b89f49..0358c2696a0 100644 --- a/web_static/2-index.html +++ b/web_static/2-index.html @@ -1,19 +1,13 @@ - + - - AirBnb Clone - - - + AirBnB + + + -
    -
    -
    -

    - Holberton School -

    -
    +
    +
    Best School
    diff --git a/web_static/3-index.html b/web_static/3-index.html index 2ff10b13403..1c537e59972 100644 --- a/web_static/3-index.html +++ b/web_static/3-index.html @@ -1,22 +1,17 @@ - + - - AirBnb Clone - - - - + AirBnB + + + + + + + -
    - -
    -
    -

    - Holberton School -

    -
    +
    +
    Best School
    diff --git a/web_static/4-index.html b/web_static/4-index.html index f09cdcd9d21..1d8a228198f 100644 --- a/web_static/4-index.html +++ b/web_static/4-index.html @@ -1,30 +1,23 @@ - + - - - - - - - AirBnb Clone + AirBnB + + + + + + + + -
    - -
    +
    - +
    -
    -

    - Holberton School -

    -
    +
    Best School
    diff --git a/web_static/5-index.html b/web_static/5-index.html index 21511ed13c9..9b9cf91c420 100644 --- a/web_static/5-index.html +++ b/web_static/5-index.html @@ -1,38 +1,31 @@ - + - - - - - - - AirBnb Clone + AirBnB + + + + + + + + -
    - -
    +

    States

    -

    California, New York...

    +

    California, Arizona...

    Amenities

    -

    Laundry, Internet...

    +

    Internet, Kitchen...

    - +
    -
    -

    - Holberton School -

    -
    +
    Best School
    diff --git a/web_static/6-index.html b/web_static/6-index.html index 7850206c7ee..63cc8aac678 100644 --- a/web_static/6-index.html +++ b/web_static/6-index.html @@ -1,60 +1,49 @@ - + - - - - - - - AirBnb Clone + AirBnB + + + + + + + + -
    - -
    +

    States

    -

    California, New York...

    +

    California, Arizona...

      -
    • -

      California:

      -
        -
      • San Francisco
      • -
      • Mountain View
      • -
      -
    • -
    • -

      New York:

      -
        -
      • Manhattan
      • -
      • Brooklyn
      • -
      -
    • +

      Arizona:

      +
        +
      • Phoenix
      • +
      • Tucson
      • +
      +

      California:

      +
        +
      • San Francisco
      • +
      • San Diego
      • +

    Amenities

    -

    Laundry, Internet...

    +

    Internet, Kitchen...

      -
    • Laundry
    • Internet
    • -
    • Television
    • -
    • Pool
    • +
    • TV
    • +
    • Kitchen
    • +
    • Iron
    - +
    -
    -

    - Holberton School -

    -
    +
    Best School
    diff --git a/web_static/7-index.html b/web_static/7-index.html index 65dd2f57d5b..0459bc98412 100644 --- a/web_static/7-index.html +++ b/web_static/7-index.html @@ -1,73 +1,64 @@ - + - - - - - - - - AirBnb Clone + AirBnB + + + + + + + + + -
    - -
    +

    States

    -

    California, New York...

    +

    California, Arizona...

      -
    • -

      California:

      -
        -
      • San Francisco
      • -
      • Mountain View
      • -
      -
    • -
    • -

      New York:

      -
        -
      • Manhattan
      • -
      • Brooklyn
      • -
      -
    • +

      Arizona:

      +
        +
      • Phoenix
      • +
      • Tucson
      • +
      +

      California:

      +
        +
      • San Francisco
      • +
      • San Diego
      • +

    Amenities

    -

    Laundry, Internet...

    +

    Internet, Kitchen...

      -
    • Laundry
    • Internet
    • -
    • Television
    • -
    • Pool
    • +
    • TV
    • +
    • Kitchen
    • +
    • Iron
    - +

    Places

    -
    -

    My home

    -
    -
    -

    Tiny house

    -
    -
    -

    A suite

    -
    -
    +
    +
    +

    My home

    +
    +
    +

    Tiny house

    +
    +
    +

    A suite

    +
    + +
    -
    -

    - Holberton School -

    -
    +
    Best School
    diff --git a/web_static/8-index.html b/web_static/8-index.html index c9518a774e4..c0e6716537e 100644 --- a/web_static/8-index.html +++ b/web_static/8-index.html @@ -1,142 +1,105 @@ - + - - - - - - - - AirBnb Clone + AirBnB + + + + + + + + + -
    - -
    +
    -
    -

    States

    -

    California, New York...

    -
      -
    • -

      California:

      -
        -
      • San Francisco
      • -
      • Mountain View
      • -
      -
    • -
    • -

      New York:

      -
        -
      • Manhattan
      • -
      • Brooklyn
      • -
      -
    • -
    -
    -
    -

    Amenities

    -

    Laundry, Internet...

    -
      -
    • Laundry
    • -
    • Internet
    • -
    • Television
    • -
    • Pool
    • -
    -
    - +
    +

    States

    +

    California, Arizona...

    +
      +

      Arizona:

      +
        +
      • Phoenix
      • +
      • Tucson
      • +
      +

      California:

      +
        +
      • San Francisco
      • +
      • San Diego
      • +
      +
    +
    +
    +

    Amenities

    +

    Internet, Kitchen...

    +
      +
    • Internet
    • +
    • TV
    • +
    • Kitchen
    • +
    • Iron
    • +
    +
    +
    -

    Places

    -
    -

    My home

    -
    -

    $80

    -
    -
    -
    -
    -

    2 Guests

    -
    -
    -
    -

    1 Bedroom

    -
    -
    -
    -

    1 Bathroom

    -
    -
    -
    -

    Owner: Jon Snow

    -
    -
    -

    Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

    -
    -
    -
    -

    Tiny house

    -
    -

    $65

    -
    -
    -
    -
    -

    4 Guests

    -
    -
    -
    -

    2 Bedroom

    -
    -
    -
    -

    1 Bathroom

    -
    -
    -
    -

    Owner: Daenerys Targaryen

    -
    -
    -

    Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

    -
    -
    -
    -

    A suite

    -
    -

    $190

    -
    -
    -
    -
    -

    6 Guests

    -
    -
    -
    -

    3 Bedroom

    -
    -
    -
    -

    2 Bathroom

    -
    -
    -
    -

    Owner: Azor Ahai

    -
    -
    -

    Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

    -
    -
    +

    Places

    +
    +
    +

    My home

    +
    $80
    +
    +
    2 Guests
    +
    1 Bedroom
    +
    1 Bathroom
    +
    +
    Owner: John Lennon
    +
    +

    This is a lovely 1 bedroom 1 bathroom apartment that can accomodate 2 people. It is located + at the center of Shanghai and next to the subway line 1. 1 stop to People Square, 2 stops to + Bund, 3 stops to Jingnan Temple. +

    +
    +
    +
    +

    Tiny house

    +
    $65
    +
    +
    4 Guests
    +
    2 Bedroom
    +
    1 Bathroom
    +
    +
    Owner: Adrienne
    +
    +

    Our place is a private, affordable stand-alone guests house centrally located just minutes to French Quarters, downtown, the Fair Grounds + (Jazz Fest) and City Park. The guest house is a quaint 400 square ft, with a full bath, mini kitchen & living room. The + extra high ceilings make the home feel more spacious. The sofa converts to a bed also. We have a hand made counter area that adds character + to the room and a great porch/deck to relax on and have a glass of Meriot. +

    +
    +
    +
    +

    A suite

    +
    $190
    +
    +
    6 Guests
    +
    5 Bedroom
    +
    3 Bathroom
    +
    +
    Owner: Salita Emmanuel
    +
    +

    Our place is a suite, affordable stand-alone guests house centrally located just minutes to French Quarters, downtown, the Fair Grounds + (Jazz Fest) and City Park. The guest house is a quaint 400 square ft, with a full bath, mini kitchen & living room. The + extra high ceilings make the home feel more spacious. The sofa converts to a bed also. We have a hand made counter area that adds character + to the room and a great porch/deck to relax on and have a glass of Meriot. +

    +
    +
    +
    -
    -

    - Holberton School -

    -
    + diff --git a/web_static/README.md b/web_static/README.md deleted file mode 100644 index 5c4cc7dc35e..00000000000 --- a/web_static/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# 0x01. AirBnB clone - Web static -At the end of this project you are expected to be able to explain to anyone, without the help of Google: -* What is HTML? -* How do you create an HTML page? -* What is a markup language? -* What is the DOM? -* What is an element / tag? -* What is an attribute? -* How does the browser load a webpage? -* What is CSS? -* How do you add style to an element? -* What is a class? -* What is a selector? -* How do you compute CSS Specificity Value? -* What are Box properties in CSS? diff --git a/web_static/styles/100-places.css b/web_static/styles/100-places.css new file mode 100644 index 00000000000..a724d4ecd9e --- /dev/null +++ b/web_static/styles/100-places.css @@ -0,0 +1,119 @@ +section.places h1{ + font-size: 30px; + text-align: left; +} +.articles-section{ + display: flex; + flex-wrap: wrap; + justify-content: center; +} +section.places article{ + position: relative; + width: 390px; + padding: 20px; + margin: 20px; + border: 1px solid #ff5a5f; + border-radius: 4px; +} +section.places article h2{ + font-size: 30px; + text-align: center; + margin: 5px 0 40px 0; +} +article div.price_by_night{ + position: absolute; + top: 15px; + right: 15px; + color: #ff5a5f; + border: 4px solid #ff5a5f; + border-radius: 50%; + min-width: 60px; + height: 60px; + font-size: 20px; + text-align: center; + line-height: 60px; +} +.information{ + display: flex; + align-items: center; + height: 80px; + border-top: 1px solid #dddddd; + border-bottom: 1px solid #dddddd; +} +.information div{ + width: 100px; + height: 50px; + text-align: center; +} +.information div .icon{ + width: 100%; + height: 80%; + background-repeat: no-repeat; + background-size: contain; + background-position: center; +} +.max_guest div.icon{ + background-image: url("../images/guest_icon.png"); +} +.number_rooms div.icon{ + background-image: url("../images/bedroom_icon.png"); +} +.number_bathrooms div.icon{ + background-image: url("../images/bathroom_icon.png"); +} +article div.user{ + margin: 10px 0; +} +article div.user span{ + font-weight: bold; +} + +/*amenities/reviews section*/ +.amenities{ + padding: 10px 0; +} +article div.reviews h2, +article div.amenities h2{ + border-bottom: 1px solid #dddddd; +} +article div.amenities ul li{ + background-size: 25px 25px; + background-repeat: no-repeat; + background-position: left center; + width: 60px; + height: 25px; + margin-bottom: 5px; +} +ul li span{ + width: 50%; + float: right; + line-height: 25px; +} +ul li.icon1{ + background-image: url("../images/tv_icon.png"); +} +ul li.icon2{ + background-image: url("../images/wifi_icon.png"); +} +ul li.icon3{ + background-image: url("../images/pet_icon.png"); +} +article div.amenities h2, +article div.reviews h2{ + font-size: 16px; + text-align: left; + margin: 0; + padding: 10px 0; +} +article div.amenities ul, +article div.reviews ul{ + list-style-type: none; + margin: 15px 0; + padding: 0; +} +.reviews h3{ + font-size: 14px; +} +.reviews p{ + font-size: 12px; +} \ No newline at end of file diff --git a/web_static/styles/101-places.css b/web_static/styles/101-places.css new file mode 100644 index 00000000000..a724d4ecd9e --- /dev/null +++ b/web_static/styles/101-places.css @@ -0,0 +1,119 @@ +section.places h1{ + font-size: 30px; + text-align: left; +} +.articles-section{ + display: flex; + flex-wrap: wrap; + justify-content: center; +} +section.places article{ + position: relative; + width: 390px; + padding: 20px; + margin: 20px; + border: 1px solid #ff5a5f; + border-radius: 4px; +} +section.places article h2{ + font-size: 30px; + text-align: center; + margin: 5px 0 40px 0; +} +article div.price_by_night{ + position: absolute; + top: 15px; + right: 15px; + color: #ff5a5f; + border: 4px solid #ff5a5f; + border-radius: 50%; + min-width: 60px; + height: 60px; + font-size: 20px; + text-align: center; + line-height: 60px; +} +.information{ + display: flex; + align-items: center; + height: 80px; + border-top: 1px solid #dddddd; + border-bottom: 1px solid #dddddd; +} +.information div{ + width: 100px; + height: 50px; + text-align: center; +} +.information div .icon{ + width: 100%; + height: 80%; + background-repeat: no-repeat; + background-size: contain; + background-position: center; +} +.max_guest div.icon{ + background-image: url("../images/guest_icon.png"); +} +.number_rooms div.icon{ + background-image: url("../images/bedroom_icon.png"); +} +.number_bathrooms div.icon{ + background-image: url("../images/bathroom_icon.png"); +} +article div.user{ + margin: 10px 0; +} +article div.user span{ + font-weight: bold; +} + +/*amenities/reviews section*/ +.amenities{ + padding: 10px 0; +} +article div.reviews h2, +article div.amenities h2{ + border-bottom: 1px solid #dddddd; +} +article div.amenities ul li{ + background-size: 25px 25px; + background-repeat: no-repeat; + background-position: left center; + width: 60px; + height: 25px; + margin-bottom: 5px; +} +ul li span{ + width: 50%; + float: right; + line-height: 25px; +} +ul li.icon1{ + background-image: url("../images/tv_icon.png"); +} +ul li.icon2{ + background-image: url("../images/wifi_icon.png"); +} +ul li.icon3{ + background-image: url("../images/pet_icon.png"); +} +article div.amenities h2, +article div.reviews h2{ + font-size: 16px; + text-align: left; + margin: 0; + padding: 10px 0; +} +article div.amenities ul, +article div.reviews ul{ + list-style-type: none; + margin: 15px 0; + padding: 0; +} +.reviews h3{ + font-size: 14px; +} +.reviews p{ + font-size: 12px; +} \ No newline at end of file diff --git a/web_static/styles/102-common.css b/web_static/styles/102-common.css new file mode 100644 index 00000000000..77d84bbdf2b --- /dev/null +++ b/web_static/styles/102-common.css @@ -0,0 +1,20 @@ + +*{ + box-sizing: border-box; +} +body{ + display: flex; + flex-direction: column; + height: 100vh; + margin: 0; + padding: 0; + color: #484848; + font-size: 14px; + font-family: Circular,"Helvetica Neue",Helvetica,Arial,sans-serif; +} +.container{ + width: 100%; + max-width: 1000px; + margin: 30px auto; + flex: 0 0 auto; +} \ No newline at end of file diff --git a/web_static/styles/102-filters.css b/web_static/styles/102-filters.css new file mode 100644 index 00000000000..2326bb4e99c --- /dev/null +++ b/web_static/styles/102-filters.css @@ -0,0 +1,100 @@ +section.filters{ + display: flex; + align-items: center; + background-color: white; + height: 70px; + width: 100%; + border: 1px solid #dddddd; + padding-right: 30px; +} +section.filters button{ + font-size: 18px; + background-color: #ff5a5f; + color: #ffffff; + height: 48px; + width: 20%; + border: 0; + border-radius: 4px; + margin-left: auto; /*push button to the left*/ +} +section.filters button:hover{ + opacity: 90%; +} +.location_amenity{ + display: flex; + width: 50%; + height: 100%; +} +.locations, +div.location_amenity .amenities{ + height: 100%; + width: 50%; + padding: 15px 0; +} +.locations{ + border-right: 1px solid #dddddd; +} + +.locations h3, +div.location_amenity .amenities h3{ + font-weight: 600; + margin: 0; + margin-left: 30px; +} + +.locations h4, +div.location_amenity .amenities h4{ + font-weight: 400; + font-size: 14px; + margin: 0 0 0 30px; +} +ul.popover{ + position: relative; + display: none; + width: 100%; + background-color: #fafafa; + border: 1px solid #dddddd; + margin-top: 18px; + padding: 10px 15px; + z-index: 1; +} +ul.popover h2{ + font-size: 16px; + margin: 0; +} +ul.popover ul{ + margin: 0; + padding: 4px 0 10px 10px; +} +ul.popover, ul.popover ul{ + list-style-type: none; +} + +/*hide the filter popover*/ +div.locations:hover ul.popover, +div.amenities:hover ul.popover{ + position: relative; + display: block; +} + + +/*Media queries for small devices*/ +@media(max-width: 600px) +{ + section.filters{ + height: 135px; + flex-wrap: wrap; + flex: 0, 0, auto; + padding: 0 10px; + border: none; + } + .location_amenity{ + width: 100%; + height: 50%; + border-bottom: 4px solid #dddddd; + } + section.filters button{ + height: 30px; + margin: 0 auto; + } +} \ No newline at end of file diff --git a/web_static/styles/102-footer.css b/web_static/styles/102-footer.css new file mode 100644 index 00000000000..05030ebbeea --- /dev/null +++ b/web_static/styles/102-footer.css @@ -0,0 +1,10 @@ +footer{ + background-color: white; + height: 60px; + width: 100%; + border-top: 1px solid #cccccc; + text-align: center; + line-height: 60px; + flex: 0 0 auto; /*ensures footer does not stretch*/ + margin-top: auto; +} \ No newline at end of file diff --git a/web_static/styles/102-header.css b/web_static/styles/102-header.css new file mode 100644 index 00000000000..1341096daef --- /dev/null +++ b/web_static/styles/102-header.css @@ -0,0 +1,11 @@ +header{ + background-color: white; + height: 70px; + width: 100%; + padding-left: 20px; + border-bottom: 1px solid #cccccc; + background-image: url("../images/logo.png"); + background-position: 0 center; + background-repeat: no-repeat; + flex: 0 0 auto; /*ensures header does not stretch*/ +} \ No newline at end of file diff --git a/web_static/styles/102-places.css b/web_static/styles/102-places.css new file mode 100644 index 00000000000..a0d15b219b5 --- /dev/null +++ b/web_static/styles/102-places.css @@ -0,0 +1,126 @@ +section.places h1{ + font-size: 30px; + text-align: left; +} +.articles-section{ + display: flex; + flex-wrap: wrap; + justify-content: center; +} +section.places article{ + position: relative; + width: 390px; + padding: 20px; + margin: 20px; + border: 1px solid #ff5a5f; + border-radius: 4px; +} +section.places article h2{ + font-size: 30px; + text-align: center; + margin: 5px 0 40px 0; +} +article div.price_by_night{ + position: absolute; + top: 15px; + right: 15px; + color: #ff5a5f; + border: 4px solid #ff5a5f; + border-radius: 50%; + min-width: 60px; + height: 60px; + font-size: 20px; + text-align: center; + line-height: 60px; +} +.information{ + display: flex; + align-items: center; + height: 80px; + border-top: 1px solid #dddddd; + border-bottom: 1px solid #dddddd; +} +.information div{ + width: 100px; + height: 50px; + text-align: center; +} +.information div .icon{ + width: 100%; + height: 80%; + background-repeat: no-repeat; + background-size: contain; + background-position: center; +} +.max_guest div.icon{ + background-image: url("../images/guest_icon.png"); +} +.number_rooms div.icon{ + background-image: url("../images/bedroom_icon.png"); +} +.number_bathrooms div.icon{ + background-image: url("../images/bathroom_icon.png"); +} +article div.user{ + margin: 10px 0; +} +article div.user span{ + font-weight: bold; +} + +/*amenities/reviews section*/ +.amenities{ + padding: 10px 0; +} +article div.reviews h2, +article div.amenities h2{ + border-bottom: 1px solid #dddddd; +} +article div.amenities ul li{ + background-size: 25px 25px; + background-repeat: no-repeat; + background-position: left center; + width: 60px; + height: 25px; + margin-bottom: 5px; +} +ul li span{ + width: 50%; + float: right; + line-height: 25px; +} +ul li.icon1{ + background-image: url("../images/tv_icon.png"); +} +ul li.icon2{ + background-image: url("../images/wifi_icon.png"); +} +ul li.icon3{ + background-image: url("../images/pet_icon.png"); +} +article div.amenities h2, +article div.reviews h2{ + font-size: 16px; + text-align: left; + margin: 0; + padding: 10px 0; +} +article div.amenities ul, +article div.reviews ul{ + list-style-type: none; + margin: 15px 0; + padding: 0; +} +.reviews h3{ + font-size: 14px; +} +.reviews p{ + font-size: 12px; +} + +/*Media queries for small devices*/ +@media(max-width: 600px){ + section.places h1{ + padding-left: 10px; + } +} \ No newline at end of file diff --git a/web_static/styles/103-common.css b/web_static/styles/103-common.css new file mode 100644 index 00000000000..77d84bbdf2b --- /dev/null +++ b/web_static/styles/103-common.css @@ -0,0 +1,20 @@ + +*{ + box-sizing: border-box; +} +body{ + display: flex; + flex-direction: column; + height: 100vh; + margin: 0; + padding: 0; + color: #484848; + font-size: 14px; + font-family: Circular,"Helvetica Neue",Helvetica,Arial,sans-serif; +} +.container{ + width: 100%; + max-width: 1000px; + margin: 30px auto; + flex: 0 0 auto; +} \ No newline at end of file diff --git a/web_static/styles/103-filters.css b/web_static/styles/103-filters.css new file mode 100644 index 00000000000..b36708fcf83 --- /dev/null +++ b/web_static/styles/103-filters.css @@ -0,0 +1,100 @@ +section.filters{ + display: flex; + align-items: center; + background-color: white; + height: 70px; + width: 100%; + border: 1px solid #dddddd; + padding-right: 30px; +} +section.filters button{ + font-size: 18px; + background-color: #cc3333; + color: #ffffff; + height: 48px; + width: 20%; + border: 0; + border-radius: 4px; + margin-left: auto; /*push button to the left*/ +} +section.filters button:hover{ + opacity: 90%; +} +.location_amenity{ + display: flex; + width: 50%; + height: 100%; +} +.locations, +div.location_amenity .amenities{ + height: 100%; + width: 50%; + padding: 15px 0; +} +.locations{ + border-right: 1px solid #dddddd; +} + +.locations h3, +div.location_amenity .amenities h3{ + font-weight: 600; + margin: 0; + margin-left: 30px; +} + +.locations h4, +div.location_amenity .amenities h4{ + font-weight: 400; + font-size: 14px; + margin: 0 0 0 30px; +} +ul.popover{ + position: relative; + display: none; + width: 100%; + background-color: #fafafa; + border: 1px solid #dddddd; + margin-top: 18px; + padding: 10px 15px; + z-index: 1; +} +ul.popover h2{ + font-size: 16px; + margin: 0; +} +ul.popover ul{ + margin: 0; + padding: 4px 0 10px 10px; +} +ul.popover, ul.popover ul{ + list-style-type: none; +} + +/*hide the filter popover*/ +div.locations:hover ul.popover, +div.amenities:hover ul.popover{ + position: relative; + display: block; +} + + +/*Media queries for small devices*/ +@media(max-width: 600px) +{ + section.filters{ + height: 135px; + flex-wrap: wrap; + flex: 0, 0, auto; + padding: 0 10px; + border: none; + } + .location_amenity{ + width: 100%; + height: 50%; + border-bottom: 4px solid #dddddd; + } + section.filters button{ + height: 30px; + margin: 0 auto; + } +} \ No newline at end of file diff --git a/web_static/styles/103-footer.css b/web_static/styles/103-footer.css new file mode 100644 index 00000000000..05030ebbeea --- /dev/null +++ b/web_static/styles/103-footer.css @@ -0,0 +1,10 @@ +footer{ + background-color: white; + height: 60px; + width: 100%; + border-top: 1px solid #cccccc; + text-align: center; + line-height: 60px; + flex: 0 0 auto; /*ensures footer does not stretch*/ + margin-top: auto; +} \ No newline at end of file diff --git a/web_static/styles/103-header.css b/web_static/styles/103-header.css new file mode 100644 index 00000000000..1341096daef --- /dev/null +++ b/web_static/styles/103-header.css @@ -0,0 +1,11 @@ +header{ + background-color: white; + height: 70px; + width: 100%; + padding-left: 20px; + border-bottom: 1px solid #cccccc; + background-image: url("../images/logo.png"); + background-position: 0 center; + background-repeat: no-repeat; + flex: 0 0 auto; /*ensures header does not stretch*/ +} \ No newline at end of file diff --git a/web_static/styles/103-places.css b/web_static/styles/103-places.css new file mode 100644 index 00000000000..f6866f2a060 --- /dev/null +++ b/web_static/styles/103-places.css @@ -0,0 +1,126 @@ +section.places h1{ + font-size: 30px; + text-align: left; +} +.articles-section{ + display: flex; + flex-wrap: wrap; + justify-content: center; +} +section.places article{ + position: relative; + width: 390px; + padding: 20px; + margin: 20px; + border: 1px solid #ff5a5f; + border-radius: 4px; +} +section.places article h2{ + font-size: 30px; + text-align: center; + margin: 5px 0 40px 0; +} +article div.price_by_night{ + position: absolute; + top: 15px; + right: 15px; + color: #cc3333; + border: 4px solid #cc3333; + border-radius: 50%; + min-width: 60px; + height: 60px; + font-size: 20px; + text-align: center; + line-height: 60px; +} +.information{ + display: flex; + align-items: center; + height: 80px; + border-top: 1px solid #dddddd; + border-bottom: 1px solid #dddddd; +} +.information div{ + width: 100px; + height: 50px; + text-align: center; +} +.information div .icon{ + width: 100%; + height: 80%; + background-repeat: no-repeat; + background-size: contain; + background-position: center; +} +.max_guest div.icon{ + background-image: url("../images/guest_icon.png"); +} +.number_rooms div.icon{ + background-image: url("../images/bedroom_icon.png"); +} +.number_bathrooms div.icon{ + background-image: url("../images/bathroom_icon.png"); +} +article div.user{ + margin: 10px 0; +} +article div.user span{ + font-weight: bold; +} + +/*amenities/reviews section*/ +.amenities{ + padding: 10px 0; +} +article div.reviews h2, +article div.amenities h2{ + border-bottom: 1px solid #dddddd; +} +article div.amenities ul li{ + background-size: 25px 25px; + background-repeat: no-repeat; + background-position: left center; + width: 60px; + height: 25px; + margin-bottom: 5px; +} +ul li span{ + width: 50%; + float: right; + line-height: 25px; +} +ul li.icon1{ + background-image: url("../images/tv_icon.png"); +} +ul li.icon2{ + background-image: url("../images/wifi_icon.png"); +} +ul li.icon3{ + background-image: url("../images/pet_icon.png"); +} +article div.amenities h2, +article div.reviews h2{ + font-size: 16px; + text-align: left; + margin: 0; + padding: 10px 0; +} +article div.amenities ul, +article div.reviews ul{ + list-style-type: none; + margin: 15px 0; + padding: 0; +} +.reviews h3{ + font-size: 14px; +} +.reviews p{ + font-size: 12px; +} + +/*Media queries for small devices*/ +@media(max-width: 600px){ + section.places h1{ + padding-left: 10px; + } +} \ No newline at end of file diff --git a/web_static/styles/2-common.css b/web_static/styles/2-common.css index 9798fcc7b53..55ce75057e3 100644 --- a/web_static/styles/2-common.css +++ b/web_static/styles/2-common.css @@ -1,4 +1,4 @@ -body { +body{ margin: 0; padding: 0; -} +} \ No newline at end of file diff --git a/web_static/styles/2-footer.css b/web_static/styles/2-footer.css index 26f80771cc2..ed8304e5617 100644 --- a/web_static/styles/2-footer.css +++ b/web_static/styles/2-footer.css @@ -1,12 +1,9 @@ -footer { - position: fixed; - bottom: 0; - width: 100%; +footer{ background-color: #00FF00; height: 60px; -} - -footer p { + width: 100%; text-align: center; - margin: 20px; -} + line-height: 60px; + position: absolute; + bottom: 0; +} \ No newline at end of file diff --git a/web_static/styles/2-header.css b/web_static/styles/2-header.css index bbf4f03a835..4b853d54396 100644 --- a/web_static/styles/2-header.css +++ b/web_static/styles/2-header.css @@ -1,5 +1,5 @@ -header { - width: 100%; +header{ background-color: #FF0000; height: 70px; -} + width: 100%; +} \ No newline at end of file diff --git a/web_static/styles/3-common.css b/web_static/styles/3-common.css index 94b11981615..5bc3d449e28 100644 --- a/web_static/styles/3-common.css +++ b/web_static/styles/3-common.css @@ -1,7 +1,10 @@ -body { +body{ + display: flex; + height: 100vh; + flex-direction: column; margin: 0; padding: 0; color: #484848; font-size: 14px; font-family: Circular,"Helvetica Neue",Helvetica,Arial,sans-serif; -} +} \ No newline at end of file diff --git a/web_static/styles/3-footer.css b/web_static/styles/3-footer.css index 19fe711abab..05030ebbeea 100644 --- a/web_static/styles/3-footer.css +++ b/web_static/styles/3-footer.css @@ -1,11 +1,10 @@ -footer { - position: fixed; - bottom: 0; - width: 100%; +footer{ background-color: white; height: 60px; - border-top: 1px solid #CCCCCC; - display: flex; - justify-content: center; - align-items: center; -} + width: 100%; + border-top: 1px solid #cccccc; + text-align: center; + line-height: 60px; + flex: 0 0 auto; /*ensures footer does not stretch*/ + margin-top: auto; +} \ No newline at end of file diff --git a/web_static/styles/3-header.css b/web_static/styles/3-header.css index 70dd644d3ed..1341096daef 100644 --- a/web_static/styles/3-header.css +++ b/web_static/styles/3-header.css @@ -1,15 +1,11 @@ -header { - width: 100%; +header{ background-color: white; height: 70px; - border-bottom: 1px solid #CCCCCC; - display: flex; - align-items: center; -} - -.logo { - width: 142px; - height: 60px; - background: url("../images/logo.png") no-repeat center; + width: 100%; padding-left: 20px; -} + border-bottom: 1px solid #cccccc; + background-image: url("../images/logo.png"); + background-position: 0 center; + background-repeat: no-repeat; + flex: 0 0 auto; /*ensures header does not stretch*/ +} \ No newline at end of file diff --git a/web_static/styles/4-common.css b/web_static/styles/4-common.css index 1f2f05d3853..77d84bbdf2b 100644 --- a/web_static/styles/4-common.css +++ b/web_static/styles/4-common.css @@ -1,15 +1,20 @@ -body { + +*{ + box-sizing: border-box; +} +body{ + display: flex; + flex-direction: column; + height: 100vh; margin: 0; padding: 0; color: #484848; font-size: 14px; font-family: Circular,"Helvetica Neue",Helvetica,Arial,sans-serif; } - -.container { +.container{ + width: 100%; max-width: 1000px; - margin-top: 30px; - margin-bottom: 30px; - margin-left: auto; - margin-right: auto; -} + margin: 30px auto; + flex: 0 0 auto; +} \ No newline at end of file diff --git a/web_static/styles/4-filters.css b/web_static/styles/4-filters.css index be2f3aa36d8..e6c7a0eaae1 100644 --- a/web_static/styles/4-filters.css +++ b/web_static/styles/4-filters.css @@ -1,26 +1,22 @@ -.filters { +section.filters{ + display: flex; + align-items: center; background-color: white; height: 70px; width: 100%; - border: 1px solid #DDDDDD; - border-radius: 4px; - display: flex; - justify-content: flex-end; - align-items: center; + border: 1px solid #dddddd; + padding-right: 30px; } - section.filters button{ font-size: 18px; - color: white; - background-color: #FF5A5F; + background-color: #ff5a5f; + color: #ffffff; height: 48px; - border: 0px; - border-radius: 4px; width: 20%; - margin-right: 30px; - opacity: 1; -} - -section.filters button:hover { - opacity: 0.9; + border: 0; + border-radius: 4px; + margin-left: auto; /*push button to the left*/ } +section.filters button:hover{ + opacity: 90%; +} \ No newline at end of file diff --git a/web_static/styles/5-filters.css b/web_static/styles/5-filters.css index 2b500f19e77..35009d3ead7 100644 --- a/web_static/styles/5-filters.css +++ b/web_static/styles/5-filters.css @@ -1,48 +1,42 @@ -.filters { +section.filters{ + display: flex; + align-items: center; background-color: white; height: 70px; width: 100%; - border: 1px solid #DDDDDD; - border-radius: 4px; - display: flex; - justify-content: flex-start; - align-items: center; + border: 1px solid #dddddd; + padding-right: 30px; } - section.filters button{ font-size: 18px; - color: white; - background-color: #FF5A5F; + background-color: #ff5a5f; + color: #ffffff; height: 48px; - border: 0px; - border-radius: 4px; width: 20%; - margin-left: auto; - margin-right: 30px; - opacity: 1; + border: 0; + border-radius: 4px; + margin-left: auto; /*push button to the left*/ } - -section.filters button:hover { - opacity: 0.9; +section.filters button:hover{ + opacity: 90%; } - -.locations, .amenities { +.locations, .amenities{ + box-sizing: border-box; height: 100%; width: 25%; - padding-left: 50px; + padding: 15px 10px; } - -.locations { - border-right: 1px solid #DDDDDD; +.locations{ + border-right: 1px solid #dddddd; } -.locations h3, .amenities h3 { +.locations h3, .amenities h3{ font-weight: 600; - margin: 12px 0 5px 0; + margin: 0; } -.locations h4, .amenities h4 { +.locations h4, .amenities h4{ font-weight: 400; font-size: 14px; - margin: 0 0 0 0; -} + margin: 0; +} \ No newline at end of file diff --git a/web_static/styles/6-filters.css b/web_static/styles/6-filters.css index 1c23a0fde82..6023469e0f0 100644 --- a/web_static/styles/6-filters.css +++ b/web_static/styles/6-filters.css @@ -1,91 +1,69 @@ -.filters { +section.filters{ + display: flex; + align-items: center; background-color: white; height: 70px; width: 100%; - border: 1px solid #DDDDDD; - border-radius: 4px; - display: flex; - align-items: center; + border: 1px solid #dddddd; + padding-right: 30px; } - -section.filters > button{ +section.filters button{ font-size: 18px; - color: white; - background-color: #FF5A5F; + background-color: #ff5a5f; + color: #ffffff; height: 48px; - border: 0px; - border-radius: 4px; width: 20%; - margin-left: auto; - margin-right: 30px; - opacity: 1; + border: 0; + border-radius: 4px; + margin-left: auto; /*push button to the left*/ } - -section.filters > button:hover { - opacity: 0.9; +section.filters button:hover{ + opacity: 90%; } - -.locations, .amenities { +section.filters div.locations, section.filters div.amenities{ height: 100%; width: 25%; - padding-left: 50px; + padding: 15px 0; } - -.locations { - border-right: 1px solid #DDDDDD; +section.filters div.locations{ + border-right: 1px solid #dddddd; } -.locations > h3, .amenities > h3 { + +section.filters div.locations h3, section.filters div.amenities h3{ font-weight: 600; - margin: 12px 0 5px 0; + margin: 0; + margin-left: 30px; } -.locations > h4, .amenities > h4 { +section.filters div.locations h4, section.filters div.amenities h4{ font-weight: 400; font-size: 14px; - margin: 0 0 5px 0; + margin: 0 0 0 30px; } - -.popover { - display: none; +ul.popover{ position: relative; - left: -51px; - background-color: #FAFAFA; + display: none; width: 100%; - border: 1px solid #DDDDDD; - border-radius: 4px; + background-color: #fafafa; + border: 1px solid #dddddd; + margin-top: 18px; + padding: 10px 15px; z-index: 1; - padding: 30px 50px 30px 0; - margin-top: 17px; -} - -.popover, .popover ul { - list-style-type: none; } -.locations:hover > .popover { - display: block; +ul.popover h2{ + font-size: 16px; + margin: 0; } - -.amenities:hover > .popover { - display: block; +ul.popover ul{ + margin: 0; + padding: 4px 0 10px 10px; } - -.popover h2 { - margin-top: 0px; - margin-bottom: 5px; -} - -.locations > .popover > li { - margin-bottom: 30px; - margin-left: 30px; -} -.locations > .popover > li > ul { - padding-left: 20px; -} -.locations > .popover > li > ul > li { - margin-bottom: 10px; +ul.popover, ul.popover ul{ + list-style-type: none; } -.amenities > .popover > li { - margin-left: 50px; - margin-bottom: 10px; +/*hide the filter popover*/ +div.locations:hover ul.popover, +div.amenities:hover ul.popover{ + display: block; } diff --git a/web_static/styles/7-places.css b/web_static/styles/7-places.css index 04ede61303e..eed2eb14ec2 100644 --- a/web_static/styles/7-places.css +++ b/web_static/styles/7-places.css @@ -1,29 +1,20 @@ -.places { - width: 100%; - border: 0; +section.places h1{ + font-size: 30px; + text-align: left; +} +div.articles-section{ display: flex; - flex-direction: row; flex-wrap: wrap; justify-content: center; } - -.places > h1 { - font-size: 30px; - padding-left: 20px; - padding-top: 20px; - margin-bottom: 0px; - flex: 0 1 100%; -} -.places > article { +section.places article{ width: 390px; - padding: 20px 20px 20px 20px; - margin: 20px 20px 20px 20px; - border: 1px solid #FF5A5F; + padding: 20px; + margin: 20px; + border: 1px solid #ff5a5f; border-radius: 4px; - display: flex; - justify-content: center; } - -.places > article > h2 { +section.places article h2{ font-size: 30px; -} + text-align: center; +} \ No newline at end of file diff --git a/web_static/styles/8-places.css b/web_static/styles/8-places.css index f0507c9ddc5..c04002c5d7e 100644 --- a/web_static/styles/8-places.css +++ b/web_static/styles/8-places.css @@ -1,107 +1,69 @@ -.places { - width: 100%; - border: 0; +section.places h1{ + font-size: 30px; + text-align: left; +} +.articles-section{ display: flex; - flex-direction: row; flex-wrap: wrap; justify-content: center; } - -.places > h1 { - font-size: 30px; - padding-left: 20px; - padding-top: 20px; - margin-bottom: 0px; - flex: 0 1 100%; -} -.places > article { +section.places article{ + position: relative; width: 390px; - padding: 20px 20px 20px 20px; - margin: 20px 20px 20px 20px; - border: 1px solid #FF5A5F; + padding: 20px; + margin: 20px; + border: 1px solid #ff5a5f; border-radius: 4px; - display: flex; - flex-direction: column; - justify-content: flex-start; - position: relative; } - -.places > article > h2 { +section.places article h2{ font-size: 30px; - margin: 0 0 0 0; - align-self: center; + text-align: center; + margin: 5px 0 40px 0; } - -.price_by_night { - color: #FF5A5F; - border: 4px solid #FF5A5F; +article div.price_by_night{ + position: absolute; + top: 15px; + right: 15px; + color: #ff5a5f; + border: 4px solid #ff5a5f; + border-radius: 50%; min-width: 60px; height: 60px; - font-size: 30px; - border-radius: 50%; - display: flex; - justify-content: center; - align-items: center; - margin-left: auto; - position: absolute; - top: 10px; - right: 20px; -} -.price_by_night > p { - margin: 0 0 0 0; + font-size: 20px; + text-align: center; + line-height: 60px; } - -.information { - align-self: center; - height: 80px; - width: 100%; - border-top: 1px solid #DDDDDD; - border-bottom: 1px solid #DDDDDD; - margin-top: 30px; +.information{ display: flex; - justify-content: center; align-items: center; + height: 80px; + border-top: 1px solid #dddddd; + border-bottom: 1px solid #dddddd; } - -.max_guest, .number_rooms, .number_bathrooms { +.information div{ width: 100px; + height: 50px; text-align: center; } - -.max_guest > p, .number_rooms > p, .number_bathrooms > p{ - margin-top: auto; - margin-bottom: auto; +.information div .icon{ + width: 100%; + height: 80%; + background-repeat: no-repeat; + background-size: contain; + background-position: center; } - -.guest_image { - margin-left: auto; - margin-right: auto; - height: 50px; - width: 50px; - background: url("../images/icon_group.png") no-repeat center; +.max_guest div.icon{ + background-image: url("../images/guest_icon.png"); } - -.bed_image { - margin-left: auto; - margin-right: auto; - height: 50px; - width: 50px; - background: url("../images/icon_bed.png") no-repeat center; +.number_rooms div.icon{ + background-image: url("../images/bedroom_icon.png"); } - -.bath_image { - margin-left: auto; - margin-right: auto; - height: 50px; - width: 50px; - background: url("../images/icon_bath.png") no-repeat center; +.number_bathrooms div.icon{ + background-image: url("../images/bathroom_icon.png"); } - -.user > p { - margin-top: 20px; - margin-bottom: 0px; +article div.user{ + margin: 10px 0; } -.description > p { - margin-top: 7px; - margin-bottom: 0px; +article div.user span{ + font-weight: bold; } diff --git a/web_static/w3c_validator.py b/web_static/w3c_validator.py new file mode 100755 index 00000000000..ec7ec714074 --- /dev/null +++ b/web_static/w3c_validator.py @@ -0,0 +1,131 @@ +#!/usr/bin/python3 +""" +W3C validator for Holberton School + +For HTML and CSS files. + +Based on 1 API: +- https://validator.w3.org/docs/api.html + +Usage: + +Simple file: + +``` +./w3c_validator.py index.html +``` + +Multiple files: + +``` +./w3c_validator.py index.html header.html styles/common.css +``` + +All errors are printed in `STDERR` + +Return: +Exit status is the # of errors, 0 on Success +""" +import sys +import requests +import os + + +def __print_stdout(msg): + """Print message in STDOUT + """ + sys.stdout.buffer.write(msg.encode('utf-8')) + + +def __print_stderr(msg): + """Print message in STDERR + """ + sys.stderr.buffer.write(msg.encode('utf-8')) + + +def __is_empty(file): + if os.path.getsize(file) == 0: + raise OSError("File '{}' is empty.".format(file)) + + +def __validate(file_path, type): + """ + Start validation of files + """ + h = {'Content-Type': "{}; charset=utf-8".format(type)} + # Open files in binary mode: + # https://requests.readthedocs.io/en/master/user/advanced/ + d = open(file_path, "rb").read() + u = "https://validator.w3.org/nu/?out=json" + r = requests.post(u, headers=h, data=d) + + if not r.status_code < 400: + raise ConnectionError("Unable to connect to API endpoint.") + + res = [] + messages = r.json().get('messages', []) + for m in messages: + # Capture files that have incomplete or broken HTML + if m['type'] == 'error' or m['type'] == 'info': + res.append("'{}' => {}".format(file_path, m['message'])) + else: + res.append("[{}:{}] {}".format( + file_path, m['lastLine'], m['message'])) + return res + + +def __analyse(file_path): + """Start analyse of a file and print the result + """ + nb_errors = 0 + try: + result = None + + if file_path.endswith(".css"): + __is_empty(file_path) + result = __validate(file_path, "text/css") + elif file_path.endswith((".html", ".htm")): + __is_empty(file_path) + result = __validate(file_path, "text/html") + elif file_path.endswith(".svg"): + __is_empty(file_path) + result = __validate(file_path, "image/svg+xml") + else: + allowed_files = "'.css', '.html', '.htm' and '.svg'" + raise OSError( + "File {} does not have a valid file extension.\nOnly {} are " + "allowed.".format(file_path, allowed_files) + ) + + if len(result) > 0: + for msg in result: + __print_stderr("{}\n".format(msg)) + nb_errors += 1 + else: + __print_stdout("'{}' => OK\n".format(file_path)) + + except Exception as e: + __print_stderr("'{}' => {}\n".format(e.__class__.__name__, e)) + return nb_errors + + +def __files_loop(): + """Loop that analyses for each file from input arguments + """ + nb_errors = 0 + for file_path in sys.argv[1:]: + nb_errors += __analyse(file_path) + + return nb_errors + + +if __name__ == "__main__": + """Main + """ + if len(sys.argv) < 2: + __print_stderr("usage: w3c_validator.py file1 file2 ...\n") + exit(1) + + """execute tests, then exit. Exit status = # of errors (0 on success) + """ + sys.exit(__files_loop())