├── .gitignore ├── README.md ├── app.py ├── helpers.py ├── requirements.txt ├── static ├── Eastwood.woff2 ├── PinkChicken-Regular.ttf ├── Welcome Magic.otf ├── dashboard.js ├── logo.png ├── main.js └── styles.css ├── tables.sql └── templates ├── bestsellers.html ├── dashboard.html ├── layout.html ├── login.html ├── mybooks.html ├── readlist.html ├── search.html └── signup.html /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/python 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=python 3 | 4 | ### Python ### 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | songs/ 33 | flask_session/ 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .nox/ 49 | .coverage 50 | .coverage.* 51 | .cache 52 | nosetests.xml 53 | coverage.xml 54 | *.cover 55 | *.py,cover 56 | .hypothesis/ 57 | .pytest_cache/ 58 | cover/ 59 | 60 | # Translations 61 | *.mo 62 | *.pot 63 | 64 | # Django stuff: 65 | *.log 66 | local_settings.py 67 | db.sqlite3 68 | db.sqlite3-journal 69 | 70 | # Flask stuff: 71 | instance/ 72 | .webassets-cache 73 | 74 | # Scrapy stuff: 75 | .scrapy 76 | 77 | # Sphinx documentation 78 | docs/_build/ 79 | 80 | # PyBuilder 81 | .pybuilder/ 82 | target/ 83 | 84 | # Jupyter Notebook 85 | .ipynb_checkpoints 86 | 87 | # IPython 88 | profile_default/ 89 | ipython_config.py 90 | 91 | # pyenv 92 | # For a library or package, you might want to ignore these files since the code is 93 | # intended to run in multiple environments; otherwise, check them in: 94 | # .python-version 95 | 96 | # pipenv 97 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 98 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 99 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 100 | # install all needed dependencies. 101 | #Pipfile.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | 110 | # pdm 111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 112 | #pdm.lock 113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 114 | # in version control. 115 | # https://pdm.fming.dev/#use-with-ide 116 | .pdm.toml 117 | 118 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 119 | __pypackages__/ 120 | 121 | # Celery stuff 122 | celerybeat-schedule 123 | celerybeat.pid 124 | 125 | # SageMath parsed files 126 | *.sage.py 127 | 128 | # Environments 129 | .env 130 | .venv 131 | env/ 132 | venv/ 133 | ENV/ 134 | env.bak/ 135 | venv.bak/ 136 | 137 | # Spyder project settings 138 | .spyderproject 139 | .spyproject 140 | 141 | # Rope project settings 142 | .ropeproject 143 | 144 | # mkdocs documentation 145 | /site 146 | 147 | # mypy 148 | .mypy_cache/ 149 | .dmypy.json 150 | dmypy.json 151 | 152 | # Pyre type checker 153 | .pyre/ 154 | 155 | # pytype static type analyzer 156 | .pytype/ 157 | 158 | # Cython debug symbols 159 | cython_debug/ 160 | 161 | # PyCharm 162 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 163 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 164 | # and can be added to the global gitignore or merged into this file. For a more nuclear 165 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 166 | #.idea/ 167 | 168 | ### Python Patch ### 169 | # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration 170 | poetry.toml 171 | 172 | # ruff 173 | .ruff_cache/ 174 | 175 | # LSP config files 176 | pyrightconfig.json 177 | 178 | #database files 179 | *.sqlite3 180 | *.sqlite 181 | *.db 182 | # /home/prayas35/genreizz/genreizz.db 183 | # End of https://www.toptal.com/developers/gitignore/api/python -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GENREIZZ 2 | 3 | ## Description: 4 | Genreizz is a web application using Python Flask, Jinja, and SQLite that recommends books to users based on their reading history, utilizing the Google Books API for data and featuring a frontend built with HTML, CSS, JavaScript, and jQuery. 5 | ## Usage: 6 | First, you should **Fork/Clone** this repository. 7 | 8 | Then, you should install all required libraries for this application to run by executing the following command in the terminal: 9 | 10 | pip install -r requirements.txt 11 | 12 | To start the web application server, in the project directory, type the following command in your terminal: 13 | 14 | flask run 15 | 16 | Click the link shown in the terminal to access the application. 17 | 18 | If you are a new user, you will need to register first. To do so, simply click on the `Register` option in the navbar menu. After successfully registering, you should see the `Best Sellers` page where you can find the best-selling books. 19 | 20 | In the navbar, you can find the `search` field where you can search books by its name and can add books to your bookshelf and readlist accordingly. 21 | 22 | There is a offcanvas navbar using which you can navigate throughout the application. You can find the options `Best Sellers`, `Get Recommendations` _where you can get recommendations based on your reading history_, `Bookshelf` _where you can see your reading histroy_, `Readlist` _where you will find the books that you want to read in the future._ 23 | 24 | You are most welcome to play around in the application. 25 | 26 | Finally in the offcanvas navbar, you can find the `Log Out` button clicking which you can log out from the application. The next time you want to use the application, you need to first log in using your credentials. However, if you simply close the tab or shut down the server without logging out, you will not need to log in again the next time you use it. 27 | 28 | If you want to close the server simply press `Ctrl+C` in the terminal. 29 | 30 | ## Files and Folders: 31 | 32 | ### ***1. helpers.py*** 33 | This is the main project file that does most of the heavy lifting in the application including reading API responses, formatting API response, generating recommendations, checking if user is logged in. 34 | 35 | You can run this file by typing the following command in your project directory: 36 | 37 | python3 helpers.py 38 | 39 | ### ***2. app.py*** 40 | app.py contains the Flask framework including routes, session configurations and database management. On running the command 41 | 42 | flask run 43 | 44 | the app.py file is automatically run as the server using Flask framework. 45 | 46 | ### ***3. tables.sql*** 47 | This is the file that contains the schema of the database. 48 | 49 | ### ***4. genreizz.db*** 50 | This is the database of the application that stores hashed user credentials and book information. 51 | 52 | ### ***7. requirements.txt*** 53 | This file contains the pip installable packages that must be installed before running the application. 54 | 55 | ### ***5. templates*** 56 | The templates folder contains the html templates that are rendered using Flask in app.py 57 | 58 | ### ***6. static*** 59 | The static folder contains all the static files like the `CSS` file, `Font` files, `JavaScript` files, `favicon` file and more. 60 | 61 | ## 62 | > **ℹ️ Note:** 63 | > All the book data shown in this application is provided by the Google Books API and I do not own any of this data. 64 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, request, redirect, session, jsonify 2 | from flask_session import Session 3 | from helpers import search_books, login_required, best_sellers, fetch_books 4 | from cs50 import SQL 5 | from werkzeug.security import check_password_hash, generate_password_hash 6 | from datetime import timedelta 7 | import random 8 | import asyncio 9 | 10 | 11 | app = Flask(__name__) 12 | 13 | app.config["SESSION_PERMANENT"] = False 14 | app.config["SESSION_TYPE"] = "filesystem" 15 | Session(app) 16 | app.permanent_session_lifetime = timedelta(days=5) 17 | 18 | db = SQL("sqlite:///genreizz.db") 19 | 20 | @app.after_request 21 | def after_request(response): 22 | """Ensure responses aren't cached""" 23 | response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate" 24 | response.headers["Expires"] = 0 25 | response.headers["Pragma"] = "no-cache" 26 | return response 27 | 28 | @app.route("/") 29 | def index(): 30 | '''redirects to the login page if not signed in else shows bestseller books''' 31 | 32 | if session.permanent: 33 | return redirect('/bestsellers') 34 | 35 | return redirect('/login') 36 | 37 | @app.route('/login', methods=["GET", "POST"]) 38 | def login(): 39 | '''log users in''' 40 | 41 | if request.method == 'GET': 42 | if session.permanent: 43 | return redirect('/bestsellers') 44 | 45 | else: 46 | return render_template('login.html') 47 | 48 | if request.method == 'POST': 49 | username = request.form.get("username") 50 | password = request.form.get("password") 51 | 52 | users = db.execute("SELECT username FROM users") 53 | for i in users: 54 | if username == i['username']: 55 | user = db.execute("SELECT password, user_id FROM users WHERE username = ?", username) 56 | if check_password_hash(user[0]['password'], password): 57 | session['user_id'] = user[0]['user_id'] 58 | session.permanent = True 59 | return redirect('/bestsellers') 60 | 61 | else: 62 | alert = "Incorrect username or password!" 63 | return render_template('login.html', alert = alert) #to add a msg if login credentials are incorrect 64 | 65 | alert = "Incorrect username or password!" 66 | return render_template('login.html', alert = alert) 67 | 68 | @app.route('/register', methods=["GET", "POST"]) 69 | def register(): 70 | '''register users''' 71 | 72 | if request.method == "POST": 73 | username = request.form.get("username") 74 | email = request.form.get("email") 75 | password = generate_password_hash(request.form.get("password")) 76 | confirm_password = request.form.get("confirm_password") 77 | 78 | if not check_password_hash(password, confirm_password): 79 | return render_template('signup.html', alert = "Passwords do not match. Please try again.") 80 | 81 | users = db.execute("SELECT username, email FROM users") 82 | 83 | for user in users: 84 | if username == user['username']: 85 | return render_template('signup.html', alert = "Username already exists. Try a different username.") #to add a msg if username already exists 86 | 87 | elif email == user['email']: 88 | return render_template('signup.html', alert = "The account already exists with this email.") #to add a msg if email already exists 89 | 90 | db.execute("INSERT INTO users (username, email, password) VALUES(?, ?, ?)", username, email, password) 91 | id = db.execute("SELECT user_id FROM users WHERE username = ?", username) 92 | session['user_id'] = id[0]['user_id'] 93 | session.permanent = True 94 | return redirect('/bestsellers') 95 | 96 | return render_template("signup.html") 97 | 98 | @app.route('/recommendations') 99 | @login_required 100 | def dashboard(): 101 | '''gives recommendations based on users most read genres and authors''' 102 | 103 | user_id = session['user_id'] 104 | user_books = db.execute("SELECT genre, author FROM books WHERE user_id = ?", user_id) 105 | 106 | genres = [] 107 | for book in user_books: 108 | genres.append(book['genre']) 109 | unique_genres = list(set(genres)) 110 | 111 | authors = [] 112 | for book in user_books: 113 | authors.append(book['author']) 114 | unique_authors = list(set(authors)) 115 | 116 | books_genres, books_authors = asyncio.run(fetch_books(unique_genres, unique_authors)) 117 | 118 | books = books_genres + books_authors 119 | random.shuffle(books) 120 | 121 | unique_books = [] 122 | seen_ids = set() 123 | for book in books: 124 | if book['id'] not in seen_ids: 125 | unique_books.append(book) 126 | seen_ids.add(book['id']) 127 | 128 | # return unique_books 129 | return render_template('dashboard.html', books=unique_books, length=len(books)) 130 | 131 | @app.route('/bookshelf') 132 | @login_required 133 | def bookshelf(): 134 | '''shows users books''' 135 | 136 | user_id = session['user_id'] 137 | books = db.execute("SELECT * FROM books WHERE user_id = ?", user_id) 138 | return render_template('mybooks.html', books = books) 139 | 140 | @app.route('/search', methods=["GET", "POST"]) 141 | @login_required 142 | def search(): 143 | '''search books on given query''' 144 | 145 | if request.method == "POST": 146 | query = request.form.get("query") 147 | books = search_books(query) 148 | 149 | # return books 150 | return render_template('search.html', books = books) 151 | 152 | return render_template('search.html') 153 | 154 | @app.route('/add', methods=["POST"]) 155 | @login_required 156 | def add_book(): 157 | '''adds books to bookshelf''' 158 | 159 | user_id = session['user_id'] 160 | title = request.form.get("title") 161 | author = request.form.get("author") 162 | genre = request.form.get("genre") 163 | isbn = request.form.get("isbn") 164 | image = request.form.get("image") 165 | 166 | try: 167 | db.execute("INSERT INTO books (user_id, title, author, genre, isbn, image) VALUES (?, ?, ?, ?, ?, ?)", 168 | user_id, title, author, genre, isbn, image) 169 | return jsonify({"success": True, "message": 'Book added to your library!'}), 200 170 | except Exception as e: 171 | print(e) 172 | return jsonify({"success": False, "error": str(e)}), 500 173 | 174 | @app.route('/readlist') 175 | def readlist(): 176 | '''shows users readlist''' 177 | 178 | user_id = session['user_id'] 179 | books = db.execute("SELECT * FROM readlist WHERE user_id = ?", user_id) 180 | return render_template('readlist.html', books = books) 181 | 182 | @app.route('/add_read', methods=["POST"]) 183 | @login_required 184 | def add_book_readlist(): 185 | '''adds book to readlist''' 186 | 187 | user_id = session['user_id'] 188 | title = request.form.get("title") 189 | author = request.form.get("author") 190 | genre = request.form.get("genre") 191 | isbn = request.form.get("isbn") 192 | image = request.form.get("image") 193 | 194 | try: 195 | db.execute("INSERT INTO readlist (user_id, title, author, genre, isbn, image) VALUES (?, ?, ?, ?, ?, ?)", 196 | user_id, title, author, genre, isbn, image) 197 | return jsonify({"success": True, "message": 'Book added to readlist!'}), 200 198 | except Exception as e: 199 | print(e) 200 | return jsonify({"success": False, "error": str(e)}), 500 201 | 202 | @app.route('/delete/', methods=["POST"]) 203 | @login_required 204 | def delete_book(id): 205 | '''removes book from bookshelf''' 206 | 207 | db.execute("DELETE FROM books WHERE id = ?", id) 208 | return redirect('/bookshelf') 209 | 210 | @app.route('/delete_readlist/', methods=["POST"]) 211 | @login_required 212 | def delete_read(id): 213 | '''removes book from readlist''' 214 | 215 | db.execute("DELETE FROM readlist WHERE id = ?", id) 216 | return redirect('/readlist') 217 | 218 | @app.route('/bestsellers') 219 | @login_required 220 | def bestsellers(): 221 | '''shows best-selling books''' 222 | 223 | bestbooks = best_sellers() 224 | random.shuffle(bestbooks) 225 | unique_books = [] 226 | 227 | seen_isbns = set() 228 | seen_ids = set() 229 | seen_authors = set() 230 | seen_title = set() 231 | for book in bestbooks: 232 | if book['id'] not in seen_ids and book['isbn'] not in seen_isbns and book['authors'][0] not in seen_authors and book['title'] not in seen_title: 233 | unique_books.append(book) 234 | seen_ids.add(book['id']) 235 | seen_isbns.add(book['isbn']) 236 | seen_authors.add(book['authors'][0]) 237 | seen_title.add(book['title']) 238 | 239 | return render_template('bestsellers.html', books=unique_books, length=len(bestbooks)) 240 | 241 | @app.route('/logout') 242 | def logout(): 243 | '''log users out''' 244 | 245 | session.clear() 246 | return redirect('/') 247 | 248 | if __name__ == '__main__': 249 | app.run(debug=True) 250 | -------------------------------------------------------------------------------- /helpers.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from functools import wraps 3 | from flask import session, redirect 4 | import random 5 | import math 6 | import asyncio 7 | import aiohttp 8 | 9 | 10 | async def fetch_books(unique_genres, unique_authors): 11 | books_genres_task = get_books_by_genre(unique_genres) 12 | books_authors_task = get_books_by_authors(unique_authors) 13 | 14 | books_genres, books_authors = await asyncio.gather(books_genres_task, books_authors_task) 15 | 16 | return books_genres, books_authors 17 | 18 | 19 | async def fetch(session, url): 20 | async with session.get(url) as response: 21 | if response.status == 200: 22 | return await response.json() 23 | else: 24 | return {"error": response.status} 25 | 26 | 27 | async def get_books_by_genre(genres): 28 | """This function takes a list of genres as an argument and returns a list of books in that genre.""" 29 | 30 | books = [] 31 | print("getting books by genre") 32 | async with aiohttp.ClientSession() as session: 33 | tasks = [] 34 | for genre in genres: 35 | url = f"https://www.googleapis.com/books/v1/volumes?q=subject:{genre}&printType=books" 36 | tasks.append(fetch(session, url)) 37 | 38 | responses = await asyncio.gather(*tasks) 39 | 40 | for response, genre in zip(responses, genres): 41 | if "error" in response: 42 | continue 43 | 44 | total_items = response.get('totalItems', 0) 45 | if total_items == 0: 46 | continue 47 | 48 | results = random.randint(10, 20) 49 | max_results = results if results % 2 == 0 else results + 1 50 | start_index = random.randint(0, max(math.floor((total_items - max_results) / 10), 0)) 51 | # start_index = random.randint(0,10) 52 | 53 | url = f"https://www.googleapis.com/books/v1/volumes?q=subject:{genre}&maxResults={max_results}&printType=books&startIndex={start_index}" 54 | second_response = await fetch(session, url) 55 | 56 | if 'items' in second_response: 57 | for item in second_response['items']: 58 | volume_info = item['volumeInfo'] 59 | book = { 60 | 'title': volume_info['title'], 61 | 'image': volume_info.get('imageLinks', {}).get('thumbnail', "https://upload.wikimedia.org/wikipedia/commons/6/65/No-Image-Placeholder.svg"), 62 | 'id': item['id'], 63 | 'authors': volume_info.get('authors', []), 64 | 'genre': volume_info.get('categories', []), 65 | 'isbn': next((identifier['identifier'] for identifier in volume_info.get('industryIdentifiers', [])), None) 66 | } 67 | books.append(book) 68 | print("got books by genre") 69 | return books 70 | 71 | async def get_books_by_authors(authors): 72 | """This function takes a list of authors as an argument and returns a list of books by those authors.""" 73 | 74 | books = [] 75 | print("getting books by authors") 76 | async with aiohttp.ClientSession() as session: 77 | tasks = [] 78 | for author in authors: 79 | url = f"https://www.googleapis.com/books/v1/volumes?q=inauthor:{author}&printType=books" 80 | tasks.append(fetch(session, url)) 81 | 82 | responses = await asyncio.gather(*tasks) 83 | 84 | for response, author in zip(responses, authors): 85 | if "error" in response: 86 | continue 87 | 88 | total_items = response.get('totalItems', 0) 89 | if total_items == 0: 90 | continue 91 | 92 | results = random.randint(10, 20) 93 | max_results = results if results % 2 == 0 else results + 1 94 | # start_index = random.randint(0, max(math.floor((total_items - max_results) / 10), 0)) 95 | start_index = random.randint(0,10) 96 | 97 | url = f"https://www.googleapis.com/books/v1/volumes?q=inauthor:{author}&maxResults={max_results}&printType=books&startIndex={start_index}" 98 | second_response = await fetch(session, url) 99 | 100 | if 'items' in second_response: 101 | for item in second_response['items']: 102 | volume_info = item['volumeInfo'] 103 | book = { 104 | 'title': volume_info['title'], 105 | 'image': volume_info.get('imageLinks', {}).get('thumbnail', "https://upload.wikimedia.org/wikipedia/commons/6/65/No-Image-Placeholder.svg"), 106 | 'id': item['id'], 107 | 'authors': volume_info.get('authors', []), 108 | 'genre': volume_info.get('categories', []), 109 | 'isbn': next((identifier['identifier'] for identifier in volume_info.get('industryIdentifiers', [])), None) 110 | } 111 | books.append(book) 112 | print("got books by authors") 113 | return books 114 | 115 | 116 | def search_books(query): 117 | """this function takes a query as an argument and returns a list of books that match the query.""" 118 | 119 | results = random.randint(10, 20) 120 | url = f"https://www.googleapis.com/books/v1/volumes?q={query}&maxResults={results if results%2 == 0 else results+1}&printType=books" 121 | 122 | response = requests.get(url) 123 | 124 | if response.status_code == 200: 125 | print('here1') 126 | data = response.json() 127 | books = [] 128 | if 'items' in data: 129 | for item in data['items']: 130 | volume_info = item['volumeInfo'] 131 | book = {} 132 | 133 | book['message'] = None 134 | book['code'] = 200 135 | title = volume_info['title'] 136 | book['title'] = title 137 | isbn = volume_info.get('industryIdentifiers', []) 138 | id = item['id'] 139 | book['id'] = id 140 | book['image'] = volume_info['imageLinks']['thumbnail'] if 'imageLinks' in volume_info else "https://upload.wikimedia.org/wikipedia/commons/6/65/No-Image-Placeholder.svg" 141 | book['genre'] = volume_info.get('categories', []) 142 | 143 | authors = volume_info.get('authors') if 'authors' in volume_info else [] 144 | book['authors'] = authors 145 | 146 | if isbn: 147 | isbn = isbn[0].get('identifier') 148 | book['isbn'] = isbn 149 | 150 | else: 151 | book['isbn'] = None 152 | 153 | books.append(book) 154 | 155 | return books 156 | 157 | elif response.status_code == 400: 158 | print('here') 159 | data = response.json() 160 | books = [] 161 | book = {} 162 | book['message'] = "Missing Book Name" 163 | book['code'] = data['error']['code'] 164 | book['title'] = None 165 | book['authors'] = None 166 | book['genre'] = None 167 | book['isbn'] = None 168 | book['image'] = None 169 | book['id'] = None 170 | books.append(book) 171 | 172 | return books 173 | 174 | else: 175 | return f"Error: {response.status_code}" 176 | 177 | def best_sellers(): 178 | '''this function returns a list of bestselling books''' 179 | 180 | url = "https://www.googleapis.com/books/v1/volumes?q=best+sellers&orderBy=newest&langRestrict=en&maxResults=40&printType=books" 181 | response = requests.get(url) 182 | 183 | if response.status_code == 200: 184 | data = response.json() 185 | books = [] 186 | if 'items' in data: 187 | for item in data['items']: 188 | volume_info = item['volumeInfo'] 189 | book = {} 190 | 191 | title = volume_info['title'] 192 | book['title'] = title 193 | isbn = volume_info.get('industryIdentifiers', []) 194 | id = item['id'] 195 | book['id'] = id 196 | book['image'] = volume_info['imageLinks']['thumbnail'] if 'imageLinks' in volume_info else "https://upload.wikimedia.org/wikipedia/commons/6/65/No-Image-Placeholder.svg" 197 | book['genre'] = volume_info.get('categories', []) 198 | 199 | authors = volume_info.get('authors') if 'authors' in volume_info else [] 200 | book['authors'] = authors 201 | 202 | if isbn: 203 | isbn = isbn[0].get('identifier') 204 | book['isbn'] = isbn 205 | 206 | else: 207 | book['isbn'] = None 208 | 209 | books.append(book) 210 | 211 | return books 212 | 213 | def login_required(f): 214 | @wraps(f) 215 | def decorated_function(*args, **kwargs): 216 | if session.get('user_id') is None: 217 | return redirect('/login') 218 | return f(*args, **kwargs) 219 | return decorated_function 220 | 221 | if __name__ == '__main__': 222 | pass -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.9.3 2 | cs50==9.2.5 3 | Flask==2.3.2 4 | Requests==2.32.3 5 | Werkzeug==2.3.6 6 | -------------------------------------------------------------------------------- /static/Eastwood.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prayas-35/Genreizz/7108ebc627012961dec3878728e2167f4a59ebf4/static/Eastwood.woff2 -------------------------------------------------------------------------------- /static/PinkChicken-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prayas-35/Genreizz/7108ebc627012961dec3878728e2167f4a59ebf4/static/PinkChicken-Regular.ttf -------------------------------------------------------------------------------- /static/Welcome Magic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prayas-35/Genreizz/7108ebc627012961dec3878728e2167f4a59ebf4/static/Welcome Magic.otf -------------------------------------------------------------------------------- /static/dashboard.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function () { 2 | $('.add-book-form').on('submit', function (event) { 3 | event.preventDefault(); 4 | 5 | const form = $(this); 6 | const cardBody = form.closest('.card-body'); 7 | const alert = cardBody.find('.alert'); 8 | const data = form.serialize(); 9 | 10 | $.ajax({ 11 | url: '/add', 12 | method: 'POST', 13 | data: data, 14 | success: function (response) { 15 | alert.text(response.message); 16 | alert.show(); 17 | alert.css('color', 'green'); 18 | setTimeout(() => { 19 | alert.hide(); 20 | }, 3000); 21 | }, 22 | error: function () { 23 | alert.text('An error occurred while adding the book.'); 24 | alert.show(); 25 | alert.css('color', 'red'); 26 | setTimeout(() => { 27 | alert.hide(); 28 | }, 3000); 29 | } 30 | }); 31 | }); 32 | }); 33 | 34 | $(document).ready(function () { 35 | $('.add-book-readlist').on('submit', function (event) { 36 | event.preventDefault(); 37 | 38 | const form = $(this); 39 | const cardBody = form.closest('.card-body'); 40 | const alert = cardBody.find('.alert'); 41 | const data = form.serialize(); 42 | 43 | $.ajax({ 44 | url: '/add_read', 45 | method: 'POST', 46 | data: data, 47 | success: function (response) { 48 | alert.text(response.message); 49 | alert.show(); 50 | alert.css('color', 'green'); 51 | setTimeout(() => { 52 | alert.hide(); 53 | }, 3000); 54 | }, 55 | error: function () { 56 | alert.text('An error occurred while adding the book.'); 57 | alert.show(); 58 | alert.css('color', 'red'); 59 | setTimeout(() => { 60 | alert.hide(); 61 | }, 3000); 62 | } 63 | }); 64 | }); 65 | }); -------------------------------------------------------------------------------- /static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prayas-35/Genreizz/7108ebc627012961dec3878728e2167f4a59ebf4/static/logo.png -------------------------------------------------------------------------------- /static/main.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function () { 2 | // Function to show the spinner 3 | function showSpinner() { 4 | $('#navlogin').css('visibility', 'hidden'); 5 | $('#searchbtn').css('visibility', 'hidden'); 6 | $('.spinner-wrapper').css('opacity', '1'); 7 | $('.spinner-wrapper').css('visibility', 'visible'); 8 | } 9 | 10 | // Function to hide the spinner 11 | function hideSpinner() { 12 | $('#navlogin').css('visibility', 'visible'); 13 | $('#searchbtn').css('visibility', 'visible'); 14 | $('.spinner-wrapper').css('opacity', '0'); 15 | $('.spinner-wrapper').css('visibility', 'hidden'); 16 | } 17 | 18 | // Show the spinner when the page is loading 19 | $(window).on('load', function () { 20 | hideSpinner(); 21 | }); 22 | 23 | // Show the spinner when the document is loading 24 | $(document).ajaxStart(function () { 25 | showSpinner(); 26 | }); 27 | 28 | // Hide the spinner when the document has finished loading 29 | $(document).ajaxStop(function () { 30 | hideSpinner(); 31 | }); 32 | 33 | // Example of triggering the spinner manually 34 | $('form').on('submit', function (event) { 35 | showSpinner(); 36 | }); 37 | 38 | // Show the spinner when the page is reloading 39 | $(window).on('beforeunload', function () { 40 | showSpinner(); 41 | }); 42 | 43 | }); 44 | 45 | $(document).ready(function () { 46 | 47 | $(window).scroll(function () { 48 | if ($(this).scrollTop() > 100) { 49 | $('#scrollToTopBtn').fadeIn(); 50 | } else { 51 | $('#scrollToTopBtn').fadeOut(); 52 | } 53 | }); 54 | 55 | $('#scrollToTopBtn').click(function () { 56 | $('html, body').animate({ 57 | scrollTop: 0 58 | }, 'slow'); 59 | return false; 60 | }); 61 | }) 62 | 63 | $(document).ready(function () { 64 | $('#password').keyup(function () { 65 | if ($('#password').val() == '') { 66 | $('#eye').css('visibility', 'hidden'); 67 | } else { 68 | $('#eye').css('visibility', 'visible'); 69 | } 70 | }); 71 | 72 | // Add click event listener to #eye element 73 | $('#eye').click(function () { 74 | if ($('#password').attr('type') == 'password') { 75 | $('#password').attr('type', 'text'); 76 | $('#eye').removeClass('fa-eye').addClass('fa-eye-slash'); 77 | } else { 78 | $('#password').attr('type', 'password'); 79 | $('#eye').removeClass('fa-eye-slash').addClass('fa-eye'); 80 | } 81 | }); 82 | }); 83 | 84 | const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]') 85 | const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl)) -------------------------------------------------------------------------------- /static/styles.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Eastwood'; 3 | src: url("/static/Eastwood.woff2"); 4 | font-weight: normal; 5 | font-style: normal; 6 | } 7 | 8 | @font-face { 9 | font-family: 'PinkChicken'; 10 | src: url("/static/PinkChicken-Regular.ttf"); 11 | font-weight: 500; 12 | font-style: normal; 13 | } 14 | 15 | @font-face { 16 | font-family: 'Welcome_magic'; 17 | src: url(/static/Welcome\ Magic.otf); 18 | font-weight: normal; 19 | font-style: normal; 20 | } 21 | 22 | body { 23 | background: linear-gradient(135deg, #bdc3c7 0%, #99a2ab 100%); 24 | background-repeat: no-repeat; 25 | background-size: contain; 26 | } 27 | 28 | #customh1 { 29 | font-family: Eastwood; 30 | letter-spacing: 1px; 31 | font-size: 3rem; 32 | } 33 | 34 | ::-webkit-scrollbar { 35 | width: 8px; 36 | margin-right: 2px; 37 | } 38 | 39 | ::-webkit-scrollbar-track { 40 | background: #bdc3c7; 41 | } 42 | 43 | ::-webkit-scrollbar-thumb { 44 | background: #DAA520; 45 | border-radius: 50px; 46 | background-clip: content-box; 47 | } 48 | 49 | ::-webkit-scrollbar-thumb:hover { 50 | background: #f3bd1c; 51 | } 52 | 53 | ::-webkit-scrollbar-button:single-button { 54 | background: #e0e0e0; 55 | border: none; 56 | display: block; 57 | height: 8px; 58 | width: 8px; 59 | background-repeat: no-repeat; 60 | background-position: center; 61 | } 62 | 63 | /* Up arrow */ 64 | ::-webkit-scrollbar-button:single-button:vertical:decrement { 65 | background-image: url('data:image/svg+xml;utf8,'); 66 | } 67 | 68 | /* Down arrow */ 69 | ::-webkit-scrollbar-button:single-button:vertical:increment { 70 | background-image: url('data:image/svg+xml;utf8,'); 71 | } 72 | 73 | /* Left arrow */ 74 | ::-webkit-scrollbar-button:single-button:horizontal:decrement { 75 | background-image: url('data:image/svg+xml;utf8,'); 76 | } 77 | 78 | /* Right arrow */ 79 | ::-webkit-scrollbar-button:single-button:horizontal:increment { 80 | background-image: url('data:image/svg+xml;utf8,'); 81 | } 82 | 83 | #login { 84 | width: 18rem; 85 | min-height: fit-content; 86 | border: 2px solid #DAA520; 87 | background-color: transparent; 88 | height: 20rem; 89 | 90 | input[type="text"], 91 | input[type="password"] { 92 | width: 100%; 93 | padding: 0.5rem; 94 | margin: 0.5rem 0; 95 | border-radius: 5px; 96 | background-color: transparent; 97 | /* color: #DAA520; */ 98 | } 99 | } 100 | 101 | #login h2, 102 | #signup h2 { 103 | font-family: Eastwood; 104 | font-size: 2.5rem; 105 | } 106 | 107 | #loginbtn { 108 | font-family: PinkChicken; 109 | background-color: #f3bd1c; 110 | color: #000; 111 | border: none; 112 | transition: all 0.3s ease; 113 | font-size: 1.2rem; 114 | } 115 | 116 | #loginbtn:hover { 117 | background-color: #DAA520; 118 | } 119 | 120 | #signup { 121 | width: 20rem; 122 | min-height: fit-content; 123 | border: 2px solid #DAA520; 124 | background-color: transparent; 125 | height: 26rem; 126 | 127 | input[type="text"], 128 | input[type="password"], 129 | input[type="email"] { 130 | width: 13rem; 131 | padding: 0.5rem; 132 | margin: 0.5rem 0; 133 | border-radius: 5px; 134 | background-color: transparent; 135 | } 136 | } 137 | 138 | #search { 139 | min-height: fit-content; 140 | border: 2px solid #DAA520; 141 | background-color: transparent; 142 | 143 | input[type="text"] { 144 | padding: 0.5rem; 145 | margin: 0.5rem 0; 146 | border-radius: 5px; 147 | background-color: transparent; 148 | } 149 | } 150 | 151 | #searchbtn { 152 | font-family: PinkChicken; 153 | border: 2px solid #DAA520; 154 | color: black; 155 | transition: all 0.3s ease; 156 | font-size: 1.2rem; 157 | } 158 | 159 | #searchbtn:hover { 160 | background-color: #DAA520; 161 | color: whitesmoke; 162 | } 163 | 164 | .logged-in { 165 | margin-top: 80px; 166 | } 167 | 168 | .custom-table { 169 | background-color: transparent; 170 | --bs-table-bg: transparent; 171 | } 172 | 173 | .custom-table th, 174 | .custom-table td { 175 | border-right: none !important; 176 | border-left: none !important; 177 | } 178 | 179 | .custom-table thead th { 180 | border-bottom: 2px solid var(--bs-border-color); 181 | } 182 | 183 | .custom-table tbody td, 184 | .custom-table tbody th { 185 | border-top: 1px solid var(--bs-border-color); 186 | } 187 | 188 | .custom-table { 189 | border: 1px solid var(--bs-border-color); 190 | background-color: transparent; 191 | --bs-table-bg: transparent; 192 | font-size: 1.1rem; 193 | } 194 | 195 | .custom-table tbody td, 196 | .custom-table tbody th { 197 | color: #000; 198 | text-align: center; 199 | vertical-align: middle; 200 | } 201 | 202 | .custom-table .mybooks { 203 | width: 8%; 204 | } 205 | 206 | .custom-table thead th { 207 | color: #000; 208 | text-align: center; 209 | vertical-align: middle; 210 | } 211 | 212 | .image_db { 213 | height: 10rem; 214 | width: 7rem 215 | } 216 | 217 | .index { 218 | font-family: Welcome_magic; 219 | } 220 | 221 | #navlogin { 222 | border-bottom: 1px solid whitesmoke; 223 | backdrop-filter: blur(10px); 224 | min-height: 80px; 225 | } 226 | 227 | #navlogedout { 228 | border-bottom: 1px solid whitesmoke; 229 | backdrop-filter: blur(10px); 230 | min-height: fit-content; 231 | } 232 | 233 | #book-card { 234 | min-height: 11.5rem; 235 | background-color: transparent; 236 | max-height: fit-content; 237 | } 238 | 239 | /* Spinner styles */ 240 | .spinner-wrapper { 241 | position: fixed; 242 | top: 0; 243 | left: 0; 244 | width: 100%; 245 | height: 100%; 246 | background-color: black; 247 | z-index: 1000; 248 | display: flex; 249 | justify-content: center; 250 | align-items: center; 251 | visibility: hidden; 252 | /* Initially hidden */ 253 | transition: all 0.3s ease; 254 | } 255 | 256 | #scrollToTopBtn { 257 | position: fixed; 258 | bottom: 20px; 259 | right: 20px; 260 | display: none; 261 | background-color: transparent; 262 | border: 2px solid yellow; 263 | color: yellow; 264 | backdrop-filter: blur(10px); 265 | padding: 10px 20px; 266 | cursor: pointer; 267 | border-radius: 5px; 268 | z-index: 1000; 269 | font-weight: 900; 270 | transition: all 0.3s ease; 271 | } 272 | 273 | #scrollToTopBtn:hover { 274 | background-color: yellow; 275 | color: black; 276 | } 277 | 278 | #eye { 279 | position: absolute; 280 | transform: translateX(4.4rem) translateY(-2.3rem); 281 | cursor: pointer; 282 | color: white; 283 | visibility: hidden; 284 | } 285 | 286 | .custom-tooltip { 287 | position: fixed; 288 | border-radius: 10px; 289 | border: 3px dotted rgb(210, 210, 43); 290 | --bs-tooltip-bg: whitesmoke; 291 | --bs-tooltip-color: black; 292 | --bs-tooltip-opacity: 0.7; 293 | } 294 | 295 | #alerticon { 296 | height: 1.2rem; 297 | width: 1.2rem; 298 | margin-right: 0.2rem !important; 299 | transform: translateY(-0.2rem); 300 | } 301 | 302 | .alert { 303 | position: absolute; 304 | width: 100%; 305 | z-index: 1000; 306 | } 307 | 308 | :root { 309 | --color: #DAA520; 310 | --background: transparent; 311 | --duration: 6.8s; 312 | } 313 | 314 | .book { 315 | width: 32px; 316 | height: 12px; 317 | position: relative; 318 | margin: 32px 0 0 -16px; 319 | transform-origin: 50% 100%; 320 | transform-style: preserve-3d; 321 | animation: book-outer var(--duration) ease infinite; 322 | zoom: 2; 323 | } 324 | 325 | .book .inner { 326 | width: 32px; 327 | height: 12px; 328 | position: relative; 329 | transform-origin: 2px 2px; 330 | transform: rotateZ(-90deg); 331 | transform-style: preserve-3d; 332 | animation: book var(--duration) ease infinite; 333 | } 334 | 335 | .book .inner .left, 336 | .book .inner .right { 337 | width: 60px; 338 | height: 4px; 339 | top: 0; 340 | background: var(--color); 341 | position: absolute; 342 | transform-style: preserve-3d; 343 | } 344 | 345 | .book .inner .left:before, 346 | .book .inner .right:before { 347 | content: ''; 348 | width: 48px; 349 | height: 4px; 350 | background: inherit; 351 | position: absolute; 352 | top: -10px; 353 | left: 6px; 354 | } 355 | 356 | .book .inner .left:after, 357 | .book .inner .right:after { 358 | --x: 0; 359 | content: ''; 360 | width: 70px; 361 | height: 80px; 362 | background: var(--background); 363 | border: 4px solid var(--color); 364 | position: absolute; 365 | left: 0; 366 | top: 0; 367 | transform: rotateX(90deg) translateZ(36px) translateY(-40px) translateX(var(--x)); 368 | } 369 | 370 | .book .inner .left { 371 | right: 28px; 372 | transform-origin: 58px 2px; 373 | transform: rotateZ(90deg); 374 | animation: left var(--duration) ease infinite; 375 | } 376 | 377 | .book .inner .right { 378 | left: 28px; 379 | transform-origin: 2px 2px; 380 | transform: rotateZ(-90deg); 381 | animation: right var(--duration) ease infinite; 382 | } 383 | 384 | .book .inner .right:after { 385 | --x: -10px; 386 | } 387 | 388 | .book .inner .middle { 389 | width: 32px; 390 | height: 12px; 391 | border: 4px solid var(--color); 392 | border-top: 0; 393 | transform: translateY(2px); 394 | } 395 | 396 | .book .inner .middle:after { 397 | content: ''; 398 | bottom: 0; 399 | left: 0; 400 | right: 0; 401 | height: 70px; 402 | position: absolute; 403 | transform: translateZ(1px); 404 | } 405 | 406 | .book ul { 407 | margin: 0; 408 | padding: 0; 409 | list-style: none; 410 | position: absolute; 411 | left: 50%; 412 | top: 0; 413 | } 414 | 415 | .book ul li { 416 | transform-style: preserve-3d; 417 | height: 4px; 418 | transform-origin: 100% 2px; 419 | width: 48px; 420 | right: 0; 421 | top: -10px; 422 | position: absolute; 423 | background: var(--color); 424 | transform: rotateZ(0deg) translateX(-18px); 425 | animation-duration: var(--duration); 426 | animation-timing-function: ease; 427 | animation-iteration-count: infinite; 428 | } 429 | 430 | .book ul li:before { 431 | content: ''; 432 | transform-origin: 0 0; 433 | transform: rotateX(90deg) translateY(-80px); 434 | position: absolute; 435 | display: block; 436 | width: 60px; 437 | height: 80px; 438 | left: -6px; 439 | border: 4px solid var(--color); 440 | background: var(--background); 441 | } 442 | 443 | /* Manually create keyframes for each page */ 444 | @keyframes page-0 { 445 | 4% { 446 | transform: rotateZ(0deg) translateX(-18px); 447 | } 448 | 449 | 13%, 450 | 54% { 451 | transform: rotateZ(180deg) translateX(-18px); 452 | } 453 | 454 | 63% { 455 | transform: rotateZ(0deg) translateX(-18px); 456 | } 457 | } 458 | 459 | @keyframes page-1 { 460 | 5.86% { 461 | transform: rotateZ(0deg) translateX(-18px); 462 | } 463 | 464 | 14.74%, 465 | 55.86% { 466 | transform: rotateZ(180deg) translateX(-18px); 467 | } 468 | 469 | 64.74% { 470 | transform: rotateZ(0deg) translateX(-18px); 471 | } 472 | } 473 | 474 | /* Repeat for each page up to page-20 */ 475 | 476 | @keyframes page-2 { 477 | 7.72% { 478 | transform: rotateZ(0deg) translateX(-18px); 479 | } 480 | 481 | 16.48%, 482 | 57.72% { 483 | transform: rotateZ(180deg) translateX(-18px); 484 | } 485 | 486 | 66.48% { 487 | transform: rotateZ(0deg) translateX(-18px); 488 | } 489 | } 490 | 491 | /* Add similar keyframes for page-3 to page-20 */ 492 | 493 | /* Add keyframes for all remaining pages */ 494 | @keyframes page-3 { 495 | 9.58% { 496 | transform: rotateZ(0deg) translateX(-18px); 497 | } 498 | 499 | 18.22%, 500 | 59.58% { 501 | transform: rotateZ(180deg) translateX(-18px); 502 | } 503 | 504 | 68.22% { 505 | transform: rotateZ(0deg) translateX(-18px); 506 | } 507 | } 508 | 509 | @keyframes page-4 { 510 | 11.44% { 511 | transform: rotateZ(0deg) translateX(-18px); 512 | } 513 | 514 | 19.96%, 515 | 61.44% { 516 | transform: rotateZ(180deg) translateX(-18px); 517 | } 518 | 519 | 69.96% { 520 | transform: rotateZ(0deg) translateX(-18px); 521 | } 522 | } 523 | 524 | @keyframes page-5 { 525 | 13.3% { 526 | transform: rotateZ(0deg) translateX(-18px); 527 | } 528 | 529 | 21.7%, 530 | 63.3% { 531 | transform: rotateZ(180deg) translateX(-18px); 532 | } 533 | 534 | 71.7% { 535 | transform: rotateZ(0deg) translateX(-18px); 536 | } 537 | } 538 | 539 | @keyframes page-6 { 540 | 15.16% { 541 | transform: rotateZ(0deg) translateX(-18px); 542 | } 543 | 544 | 23.44%, 545 | 65.16% { 546 | transform: rotateZ(180deg) translateX(-18px); 547 | } 548 | 549 | 73.44% { 550 | transform: rotateZ(0deg) translateX(-18px); 551 | } 552 | } 553 | 554 | @keyframes page-7 { 555 | 17.02% { 556 | transform: rotateZ(0deg) translateX(-18px); 557 | } 558 | 559 | 25.18%, 560 | 67.02% { 561 | transform: rotateZ(180deg) translateX(-18px); 562 | } 563 | 564 | 75.18% { 565 | transform: rotateZ(0deg) translateX(-18px); 566 | } 567 | } 568 | 569 | @keyframes page-8 { 570 | 18.88% { 571 | transform: rotateZ(0deg) translateX(-18px); 572 | } 573 | 574 | 26.92%, 575 | 68.88% { 576 | transform: rotateZ(180deg) translateX(-18px); 577 | } 578 | 579 | 76.92% { 580 | transform: rotateZ(0deg) translateX(-18px); 581 | } 582 | } 583 | 584 | @keyframes page-9 { 585 | 20.74% { 586 | transform: rotateZ(0deg) translateX(-18px); 587 | } 588 | 589 | 28.66%, 590 | 70.74% { 591 | transform: rotateZ(180deg) translateX(-18px); 592 | } 593 | 594 | 78.66% { 595 | transform: rotateZ(0deg) translateX(-18px); 596 | } 597 | } 598 | 599 | @keyframes page-10 { 600 | 22.6% { 601 | transform: rotateZ(0deg) translateX(-18px); 602 | } 603 | 604 | 30.4%, 605 | 72.6% { 606 | transform: rotateZ(180deg) translateX(-18px); 607 | } 608 | 609 | 80.4% { 610 | transform: rotateZ(0deg) translateX(-18px); 611 | } 612 | } 613 | 614 | @keyframes page-11 { 615 | 24.46% { 616 | transform: rotateZ(0deg) translateX(-18px); 617 | } 618 | 619 | 32.14%, 620 | 74.46% { 621 | transform: rotateZ(180deg) translateX(-18px); 622 | } 623 | 624 | 82.14% { 625 | transform: rotateZ(0deg) translateX(-18px); 626 | } 627 | } 628 | 629 | @keyframes page-12 { 630 | 26.32% { 631 | transform: rotateZ(0deg) translateX(-18px); 632 | } 633 | 634 | 33.88%, 635 | 76.32% { 636 | transform: rotateZ(180deg) translateX(-18px); 637 | } 638 | 639 | 83.88% { 640 | transform: rotateZ(0deg) translateX(-18px); 641 | } 642 | } 643 | 644 | @keyframes page-13 { 645 | 28.18% { 646 | transform: rotateZ(0deg) translateX(-18px); 647 | } 648 | 649 | 35.62%, 650 | 78.18% { 651 | transform: rotateZ(180deg) translateX(-18px); 652 | } 653 | 654 | 85.62% { 655 | transform: rotateZ(0deg) translateX(-18px); 656 | } 657 | } 658 | 659 | @keyframes page-14 { 660 | 30.04% { 661 | transform: rotateZ(0deg) translateX(-18px); 662 | } 663 | 664 | 37.36%, 665 | 80.04% { 666 | transform: rotateZ(180deg) translateX(-18px); 667 | } 668 | 669 | 87.36% { 670 | transform: rotateZ(0deg) translateX(-18px); 671 | } 672 | } 673 | 674 | @keyframes page-15 { 675 | 31.9% { 676 | transform: rotateZ(0deg) translateX(-18px); 677 | } 678 | 679 | 39.1%, 680 | 81.9% { 681 | transform: rotateZ(180deg) translateX(-18px); 682 | } 683 | 684 | 89.1% { 685 | transform: rotateZ(0deg) translateX(-18px); 686 | } 687 | } 688 | 689 | @keyframes page-16 { 690 | 33.76% { 691 | transform: rotateZ(0deg) translateX(-18px); 692 | } 693 | 694 | 40.84%, 695 | 83.76% { 696 | transform: rotateZ(180deg) translateX(-18px); 697 | } 698 | 699 | 90.84% { 700 | transform: rotateZ(0deg) translateX(-18px); 701 | } 702 | } 703 | 704 | @keyframes page-17 { 705 | 35.62% { 706 | transform: rotateZ(0deg) translateX(-18px); 707 | } 708 | 709 | 42.58%, 710 | 85.62% { 711 | transform: rotateZ(180deg) translateX(-18px); 712 | } 713 | 714 | 92.58% { 715 | transform: rotateZ(0deg) translateX(-18px); 716 | } 717 | } 718 | 719 | @keyframes page-18 { 720 | 37.48% { 721 | transform: rotateZ(0deg) translateX(-18px); 722 | } 723 | 724 | 44.32%, 725 | 87.48% { 726 | transform: rotateZ(180deg) translateX(-18px); 727 | } 728 | 729 | 94.32% { 730 | transform: rotateZ(0deg) translateX(-18px); 731 | } 732 | } 733 | 734 | @keyframes page-19 { 735 | 39.34% { 736 | transform: rotateZ(0deg) translateX(-18px); 737 | } 738 | 739 | 46.06%, 740 | 89.34% { 741 | transform: rotateZ(180deg) translateX(-18px); 742 | } 743 | 744 | 96.06% { 745 | transform: rotateZ(0deg) translateX(-18px); 746 | } 747 | } 748 | 749 | @keyframes page-20 { 750 | 41.2% { 751 | transform: rotateZ(0deg) translateX(-18px); 752 | } 753 | 754 | 47.8%, 755 | 91.2% { 756 | transform: rotateZ(180deg) translateX(-18px); 757 | } 758 | 759 | 97.8% { 760 | transform: rotateZ(0deg) translateX(-18px); 761 | } 762 | } 763 | 764 | .book ul li:nth-child(1) { 765 | animation-name: page-0; 766 | } 767 | 768 | .book ul li:nth-child(2) { 769 | animation-name: page-1; 770 | } 771 | 772 | .book ul li:nth-child(3) { 773 | animation-name: page-2; 774 | } 775 | 776 | .book ul li:nth-child(4) { 777 | animation-name: page-3; 778 | } 779 | 780 | .book ul li:nth-child(5) { 781 | animation-name: page-4; 782 | } 783 | 784 | .book ul li:nth-child(6) { 785 | animation-name: page-5; 786 | } 787 | 788 | .book ul li:nth-child(7) { 789 | animation-name: page-6; 790 | } 791 | 792 | .book ul li:nth-child(8) { 793 | animation-name: page-7; 794 | } 795 | 796 | .book ul li:nth-child(9) { 797 | animation-name: page-8; 798 | } 799 | 800 | .book ul li:nth-child(10) { 801 | animation-name: page-9; 802 | } 803 | 804 | .book ul li:nth-child(11) { 805 | animation-name: page-10; 806 | } 807 | 808 | .book ul li:nth-child(12) { 809 | animation-name: page-11; 810 | } 811 | 812 | .book ul li:nth-child(13) { 813 | animation-name: page-12; 814 | } 815 | 816 | .book ul li:nth-child(14) { 817 | animation-name: page-13; 818 | } 819 | 820 | .book ul li:nth-child(15) { 821 | animation-name: page-14; 822 | } 823 | 824 | .book ul li:nth-child(16) { 825 | animation-name: page-15; 826 | } 827 | 828 | .book ul li:nth-child(17) { 829 | animation-name: page-16; 830 | } 831 | 832 | .book ul li:nth-child(18) { 833 | animation-name: page-17; 834 | } 835 | 836 | .book ul li:nth-child(19) { 837 | animation-name: page-18; 838 | } 839 | 840 | .book ul li:nth-child(20) { 841 | animation-name: page-19; 842 | } 843 | 844 | .book ul li:nth-child(21) { 845 | animation-name: page-20; 846 | } 847 | 848 | @keyframes left { 849 | 4% { 850 | transform: rotateZ(90deg); 851 | } 852 | 853 | 10%, 854 | 40% { 855 | transform: rotateZ(0deg); 856 | } 857 | 858 | 46%, 859 | 54% { 860 | transform: rotateZ(90deg); 861 | } 862 | 863 | 60%, 864 | 90% { 865 | transform: rotateZ(0deg); 866 | } 867 | 868 | 96% { 869 | transform: rotateZ(90deg); 870 | } 871 | } 872 | 873 | @keyframes right { 874 | 4% { 875 | transform: rotateZ(-90deg); 876 | } 877 | 878 | 10%, 879 | 40% { 880 | transform: rotateZ(0deg); 881 | } 882 | 883 | 46%, 884 | 54% { 885 | transform: rotateZ(-90deg); 886 | } 887 | 888 | 60%, 889 | 90% { 890 | transform: rotateZ(0deg); 891 | } 892 | 893 | 96% { 894 | transform: rotateZ(-90deg); 895 | } 896 | } 897 | 898 | @keyframes book-outer { 899 | 43% { 900 | transform: rotateX(0deg); 901 | } 902 | 903 | 50%, 904 | 92% { 905 | transform: rotateX(-90deg); 906 | } 907 | } 908 | 909 | @keyframes book { 910 | 4% { 911 | transform: rotateZ(-90deg); 912 | } 913 | 914 | 10%, 915 | 40% { 916 | transform: rotateZ(0deg); 917 | transform-origin: 2px 2px; 918 | } 919 | 920 | 40.01%, 921 | 59.99% { 922 | transform-origin: 30px 2px; 923 | } 924 | 925 | 46%, 926 | 54% { 927 | transform: rotateZ(90deg); 928 | } 929 | 930 | 60%, 931 | 90% { 932 | transform: rotateZ(0deg); 933 | transform-origin: 2px 2px; 934 | } 935 | 936 | 96% { 937 | transform: rotateZ(-90deg); 938 | } 939 | } 940 | 941 | #addalert { 942 | display: none; 943 | margin-top: 5px; 944 | position: relative; 945 | margin-bottom: 0; 946 | padding-bottom: 0; 947 | } -------------------------------------------------------------------------------- /tables.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE users ( 2 | user_id INTEGER PRIMARY KEY AUTOINCREMENT, 3 | username TEXT NOT NULL, 4 | email TEXT NOT NULL, 5 | password TEXT NOT NULL 6 | ); 7 | 8 | CREATE TABLE books ( 9 | id INTEGER PRIMARY KEY AUTOINCREMENT, 10 | title TEXT NOT NULL, 11 | author TEXT NOT NULL, 12 | genre TEXT NOT NULL, 13 | user_id INTEGER NOT NULL, 14 | isbn TEXT NOT NULL, 15 | image TEXT NOT NULL, 16 | FOREIGN KEY (user_id) REFERENCES users(user_id) 17 | ); 18 | 19 | CREATE TABLE readlist ( 20 | id INTEGER PRIMARY KEY AUTOINCREMENT, 21 | title TEXT NOT NULL, 22 | author TEXT NOT NULL, 23 | genre TEXT NOT NULL, 24 | user_id INTEGER NOT NULL, 25 | isbn TEXT NOT NULL, 26 | image TEXT NOT NULL, 27 | FOREIGN KEY (user_id) REFERENCES users(user_id) 28 | ); -------------------------------------------------------------------------------- /templates/bestsellers.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}: Best Sellers 4 | {% endblock %} 5 | 6 | {% block main %} 7 | 8 | {% if length == 0 %} 9 |

No Best Sellers

10 | {% else %} 11 | 12 |

Best Sellers

13 |
14 |
15 | {% for book in books %} 16 |
17 |
18 |
19 |
20 | 22 |
23 |
24 |
25 |
{{ book.title }}
26 |

Author: {% if book.authors|length == 0 %}Unknown{% else %}{{ 27 | book.authors[0] }}{% endif %}

28 |
29 |
30 | 31 | 33 | 34 | 35 | 36 | 45 |
46 | 47 |
48 | 49 | 51 | 52 | 53 | 54 | 63 |
64 |
65 | 66 |
67 |
68 |
69 |
70 |
71 | {% endfor %} 72 |
73 |
74 | {% endif %} 75 | 76 | 77 | 78 | 79 | {% endblock %} -------------------------------------------------------------------------------- /templates/dashboard.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}: Recommendations 4 | {% endblock %} 5 | 6 | {% block main %} 7 | 8 | {% if length == 0 %} 9 |

Add Books To Your Bookshelf To Get 10 | Recommendations

11 | {% else %} 12 | 13 |

Recommended Books

14 |
15 |
16 | {% for book in books %} 17 |
18 |
19 |
20 |
21 | 23 |
24 |
25 |
26 |
{{ book.title }}
27 |

Author: {% if book.authors|length == 0 %}Unknown{% else %}{{ 28 | book.authors[0] }}{% endif %}

29 |
30 |
31 | 32 | 34 | 35 | 36 | 37 | 46 |
47 | 48 |
49 | 50 | 52 | 53 | 54 | 55 | 64 |
65 |
66 | 67 |
68 |
69 |
70 |
71 |
72 | {% endfor %} 73 |
74 |
75 | {% endif %} 76 | 77 | 78 | 79 | 80 | {% endblock %} -------------------------------------------------------------------------------- /templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 12 | 15 | 16 | 17 | Genreizz{% block title %}{% endblock %} 18 | 19 | 20 | 21 | {% if alert %} 22 | 32 | {% endif %} 33 | 34 | {% if session["user_id"] %} 35 | 82 | 84 | 85 | {% else %} 86 | 103 | {% endif %} 104 | 105 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
    115 | 116 |
  • 117 |
  • 118 |
  • 119 |
  • 120 |
  • 121 |
  • 122 |
  • 123 |
  • 124 |
  • 125 |
  • 126 |
  • 127 |
  • 128 |
  • 129 |
  • 130 |
  • 131 |
  • 132 |
  • 133 |
  • 134 |
135 |
136 |
137 | {% block main %}{% endblock %} 138 |
139 | 140 |
141 |
142 |

Data provided by Google Books API

143 |
144 |
145 | 146 | 147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}: Log In 4 | {% endblock %} 5 | 6 | {% block main %} 7 |
8 |
9 |

Login

10 |
11 |
12 |
13 | 15 |
16 |
17 | 19 |
20 | 21 |
22 |
23 |
24 | {% endblock %} -------------------------------------------------------------------------------- /templates/mybooks.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}: Bookshelf 4 | {% endblock %} 5 | 6 | {% block main %} 7 | 8 | {% if books|length == 0 %} 9 | 10 |

You Have No Books In Your Bookshelf

11 | 12 | {% else %} 13 |

Bookshelf

14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | {% for book in books %} 27 | 28 | 29 | 30 | 31 | 32 | 37 | 38 | {% endfor %} 39 | 40 | 41 |
#TitleAuthor
{{ loop.index }}{{ book.title }}{{ book.author }} 33 |
34 | 35 |
36 |
42 | {% endif %} 43 | {% endblock %} -------------------------------------------------------------------------------- /templates/readlist.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}: Readlist 4 | {% endblock %} 5 | 6 | {% block main %} 7 | 8 | {% if books|length == 0 %} 9 | 10 |

You Have No Books In Your Readlist

11 | 12 | {% else %} 13 |

Readlist

14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | {% for book in books %} 27 | 28 | 29 | 30 | 31 | 32 | 37 | 38 | {% endfor %} 39 | 40 | 41 |
#TitleAuthor
{{ loop.index }}{{ book.title }}{{ book.author }} 33 |
34 | 35 |
36 |
42 | {% endif %} 43 | {% endblock %} -------------------------------------------------------------------------------- /templates/search.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}: Search 4 | {% endblock %} 5 | 6 | {% block main %} 7 | 8 | {% if books %} 9 | {% if books.0.code == 400 %} 10 |

{{ books.0.message }}

11 | {% else %} 12 |

Search Results

13 | 14 |
15 |
16 | {% for book in books %} 17 |
18 |
19 |
20 |
21 | 22 |
23 |
24 |
25 |
{{ book.title }}
26 |

Author: {% if book.authors|length == 0 %}Unknown{% else %}{{ 27 | book.authors[0] }}{% endif %}

28 |
29 |
30 | 31 | 33 | 34 | 35 | 36 | 45 |
46 | 47 |
48 | 49 | 51 | 52 | 53 | 54 | 63 |
64 |
65 | 66 |
67 |
68 |
69 |
70 |
71 | {% endfor %} 72 |
73 |
74 | 75 | {% endif %} 76 | 77 | {% else %} 78 |

Missing Book Name

79 | {% endif %} 80 | 81 | 82 | 83 | 84 | {% endblock %} -------------------------------------------------------------------------------- /templates/signup.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}: Register 4 | {% endblock %} 5 | 6 | {% block main %} 7 |
8 |
9 |

Sign Up

10 |
11 |
12 |
13 | 15 |
16 |
17 | 19 |
20 |
21 | 23 |
24 |
25 | 27 |
28 | 29 |
30 |
31 |
32 | {% endblock %} --------------------------------------------------------------------------------