diff --git a/bam/forms.py b/bam/forms.py index 0abd6c06c7cea81e5d4291df2312996abd04af25..c2b40c5baa459ed16c210ec034df5b3c415c434c 100644 --- a/bam/forms.py +++ b/bam/forms.py @@ -1,14 +1,21 @@ from flask_wtf import FlaskForm -from wtforms import StringField, PasswordField, SubmitField, BooleanField +from wtforms import StringField, PasswordField, SubmitField, BooleanField, DecimalField from wtforms.fields.html5 import EmailField -from wtforms.validators import DataRequired, Length, Email, EqualTo, ValidationError +from wtforms.validators import ( + DataRequired, + Length, + Email, + EqualTo, + ValidationError, + Regexp, +) from bam.models import User class RegistrationForm(FlaskForm): username = StringField( "Username", - validators=[DataRequired()], + validators=[DataRequired(), Length(max=16)], render_kw={"placeholder": "Enter a username"}, ) email = EmailField( @@ -23,7 +30,10 @@ class RegistrationForm(FlaskForm): ) confirm_password = PasswordField( "Confirm Password", - validators=[DataRequired(), EqualTo("password")], + validators=[ + DataRequired(), + EqualTo("password", message="Passwords do not match."), + ], render_kw={"placeholder": "Re-enter your password"}, ) @@ -51,3 +61,19 @@ class LoginForm(FlaskForm): ) remember = BooleanField("Remember Me") submit = SubmitField("Login") + + +class AddBookForm(FlaskForm): + title = StringField("Title", validators=[DataRequired()]) + author = StringField("Author", validators=[DataRequired()]) + isbn = StringField( + "ISBN", + validators=[ + Regexp( + r"^(\d{10}\d{3}?)?$", message="Invalid ISBN. Must be 10 or 13 digits." + ), + ], + ) + + price = DecimalField("Price", default=0.0) + submit = SubmitField("Add book") diff --git a/bam/models.py b/bam/models.py index 57bff8f31617dbeea747e151b7f8da8457886f4d..76faa81ad78d8cb5184f2824023a10b11ae427f3 100644 --- a/bam/models.py +++ b/bam/models.py @@ -10,9 +10,20 @@ def load_user(user_id): class User(db.Model, UserMixin): id = db.Column(db.Integer, primary_key=True) email = db.Column(db.String(120), unique=True, nullable=False) - username = db.Column(db.String(60), unique=True, nullable=False) + username = db.Column(db.String(16), unique=True, nullable=False) password = db.Column(db.String(60), nullable=False) role = db.Column(db.String(10), default="user") def __repr__(self): return f"User({self.email}, {self.password})" + + +class Book(db.Model): + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(120), nullable=False) + author = db.Column(db.String(120), nullable=False) + isbn = db.Column(db.String(13)) + price = db.Column(db.Float, default=0.0) + addedby = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False) + + users = db.relationship(User) \ No newline at end of file diff --git a/bam/routes.py b/bam/routes.py index 426be328d243db96ed6490d67586abcc4e6851c5..c6fbb023b16cfcaa0a5dc4c6d9eae91e208ccabc 100644 --- a/bam/routes.py +++ b/bam/routes.py @@ -1,15 +1,15 @@ from flask import render_template, url_for, flash, redirect -from flask_login import login_user, current_user, logout_user +from flask_login import login_user, current_user, logout_user, login_required from bam import app, db, bcrypt -from bam.forms import RegistrationForm, LoginForm -from bam.models import User +from bam.forms import RegistrationForm, LoginForm, AddBookForm +from bam.models import User, Book @app.route("/") @app.route("/home") def home(): if current_user.is_authenticated: - return render_template("dash.html") + return render_template("books.html") return render_template("home.html") @@ -50,4 +50,22 @@ def login(): def logout(): logout_user() # flash("You have been successfully logged out", "success") - return redirect(url_for("home")) \ No newline at end of file + return redirect(url_for("home")) + + +@app.route("/add", methods=["GET", "POST"]) +@login_required +def addBook(): + form = AddBookForm() + if form.validate_on_submit(): + book = Book( + title=form.title.data, + author=form.author.data, + isbn=form.isbn.data, + price=form.price.data, + addedby=current_user.id, + ) + db.session.add(book) + db.session.commit() + return redirect(url_for("home")) + return render_template("add.html", title="Add book", form=form) \ No newline at end of file diff --git a/bam/static/bookform.css b/bam/static/bookform.css new file mode 100644 index 0000000000000000000000000000000000000000..4b190c7fa13ffe0e83c3a8856518fc3179ca93fc --- /dev/null +++ b/bam/static/bookform.css @@ -0,0 +1,70 @@ +.content { + align-self: flex-start; +} + +form { + width: 100%; +} + +form { + display: flex; + flex-direction: column; + min-width: min(90vw, 400px); +} + +form > label { + padding-bottom: 5px; +} + +input { + font-family: Roboto; + font-size: 16px; + border-radius: 3px; + padding: 12px; + background-color: rgba(10, 10, 10, 0.5); + border: 1px solid #111; + color: #eee; + transition: background 0.3s; +} +input:hover { + --hcolor: rgba(10, 10, 10, 0.7); + background-color: var(--hcolor); + border: 1px solid var(--hcolor); +} +input:focus, +input:active { + background-color: rgba(30, 30, 30, 0.5); + border: 1px solid grey; + outline: none; +} + +label { + text-transform: uppercase; + font-weight: 700; + font-size: 0.9rem; + letter-spacing: 1px; +} + +.field-error { + font-style: oblique; + color: var(--red); + font-size: 0.9rem; +} + +[type="submit"] { + background-color: var(--red); + border: none; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 1px; +} + +[type="submit"]:hover { + background-color: var(--dark-red); + cursor: pointer; +} + +label, +[type="submit"] { + margin-top: 10px; +} diff --git a/bam/static/dash.css b/bam/static/dash.css new file mode 100644 index 0000000000000000000000000000000000000000..11d35835e99fdf85b58a82e50588bc59bec7ab43 --- /dev/null +++ b/bam/static/dash.css @@ -0,0 +1,104 @@ +@import url("https://fonts.googleapis.com/css2?family=Roboto:wght@100;300;400;500;700;900&display=swap"); +html, +body { + margin: 0; + padding: 0; + min-height: 100vh; +} + +:root { + --red: #e50914; + --dark-red: #bb0009; + --t-dark: rgba(0, 0, 0, 0.8); + --t-darker: rgba(0, 0, 0, 0.9); + --nav-padding-top: 20px; + --nav-padding-side: 50px; + --logo-height: 60px; + --nav-gap: 20px; +} + +@media screen and (max-width: 600px) { + :root { + --nav-padding-top: 15px; + --nav-padding-side: 15px; + --logo-height: 50px; + } + .navbar { + flex-wrap: wrap; + } + + .navbar .space { + display: none; + } +} + +* { + box-sizing: border-box; +} + +body { + background-color: #222; + color: white; + font-family: Roboto; +} + +.maincontainer { + display: flex; + flex-direction: column; + align-items: center; + min-height: 100vh; +} + +.navbar { + background: linear-gradient(to bottom, #040404, transparent); + align-items: center; + padding: var(--nav-padding-top) var(--nav-padding-side); + display: flex; + width: 100%; + gap: var(--nav-gap); +} + +.navbar .logo { + height: var(--logo-height); + margin-right: calc(var(--nav-padding-side) - var(--nav-gap)); +} + +.navbar .logo img { + height: 100%; + filter: drop-shadow(2px 2px 6px #111); +} + +.navbar a { + text-decoration: none; + font-size: 20px; + font-weight: 500; + color: #aaa; + transition: color 0.2s ease; +} + +.navbar a.active { + color: #eee; +} + +.navbar a:hover { + color: #fff; +} + +.navbar .space, +.content { + flex-grow: 1; +} + +.content { + padding: 0 var(--nav-padding-side); +} + +.credits { + padding: 15px; + font-size: 0.7rem; + color: #888; + opacity: 0.5; + background: linear-gradient(to bottom, transparent, #111); + width: 100%; + text-align: center; +} diff --git a/bam/templates/add.html b/bam/templates/add.html new file mode 100644 index 0000000000000000000000000000000000000000..83ab79c1c14cb130b21604ca9b511fadb63ccf46 --- /dev/null +++ b/bam/templates/add.html @@ -0,0 +1,24 @@ +{% extends "dash.html" %} {% set active_page = "add" %} {% block head %} +<link + rel="stylesheet" + href="{{ url_for('static', filename='bookform.css') }}" +/> +{% endblock %} {% block content %} +<form class="addBookForm" action="" method="POST"> + {{ form.hidden_tag() }} + <h1>Add a Book</h1> + {{ form.title.label() }} {{ form.title }} {% for error in form.title.errors %} + <div class="field-error">{{ error }}</div> + {% endfor %} {{ form.author.label() }} {{ form.author }} {% for error in + form.author.errors %} + <div class="field-error">{{ error }}</div> + {% endfor %} {{ form.isbn.label() }} {{ form.isbn }} {% for error in + form.isbn.errors %} + <div class="field-error">{{ error }}</div> + {% endfor %} {{ form.price.label() }} {{ form.price }} {% for error in + form.price.errors %} + <div class="field-error">{{ error }}</div> + {% endfor %} {{ form.submit() }} +</form> + +{% endblock %} diff --git a/bam/templates/books.html b/bam/templates/books.html new file mode 100644 index 0000000000000000000000000000000000000000..b860055b746a66031738846d85113afc0213b35b --- /dev/null +++ b/bam/templates/books.html @@ -0,0 +1,3 @@ +{% extends "dash.html" %} {% set active_page = "books" %} {% block content %} +<div>WIP</div> +{% endblock %} diff --git a/bam/templates/dash.html b/bam/templates/dash.html index 439b8cc766edea2ed31d06659c115492fb0758c3..f5e6510c6a25362128711a76dce1b5863d902e8a 100644 --- a/bam/templates/dash.html +++ b/bam/templates/dash.html @@ -3,9 +3,41 @@ <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> - <title>BAM - Dashboard</title> + {% if title %} + <title>BAM - {{ title }}</title> + {% else %} + <title>BAM</title> + {% endif %} + <link + rel="stylesheet" + href="{{ url_for('static', filename='dash.css') }}" + /> + {% block head %}{% endblock %} </head> <body> - Yaaay - you are authorized! + <div class="maincontainer"> + <div class="navbar"> + <div class="logo"> + <a href="{{ url_for('home') }}"> + <img src="https://i.imgur.com/t2ZlJ06.png" /> + </a> + </div> + <a + href="{{ url_for('home') }}" + class="{{ 'active' if active_page == 'books' else '' }}" + >Home</a + > + <a + href="{{ url_for('addBook') }}" + class="{{ 'active' if active_page == 'add' else '' }}" + >Add a book</a + > + <div class="space"></div> + <a href="#">Settings</a> + <a href="{{ url_for('logout') }}">Logout</a> + </div> + <div class="content">{% block content %}{% endblock %}</div> + <div class="credits">Built with ❤ using Flask</div> + </div> </body> </html> diff --git a/create_db.py b/create_db.py index e6f7d5eb03e48a182b6b0eb43cf7731af9d02ba8..3f5bffef1bbe594b5b8bdcd6b5d6fcd268a8eb24 100644 --- a/create_db.py +++ b/create_db.py @@ -1,3 +1,15 @@ -from bam import db +from bam import db, bcrypt +from bam.models import User +# Create databases db.create_all() + +# Create a test user +db.session.add( + User( + username="bookmaster", + email="bookmaster@example.com", + password=bcrypt.generate_password_hash("masterofbooks").decode("utf-8"), + ) +) +db.session.commit() \ No newline at end of file