├── .idea ├── .gitignore ├── blog-with-users-end.iml ├── inspectionProfiles │ └── profiles_settings.xml ├── misc.xml ├── modules.xml └── vcs.xml ├── blog.db ├── forms.py ├── main.py ├── poetry.lock ├── pyproject.toml ├── requirements.txt ├── static ├── css │ ├── clean-blog.css │ └── clean-blog.min.css ├── img │ ├── about-bg.jpg │ ├── contact-bg.jpg │ └── edit-bg.jpg ├── js │ ├── clean-blog.js │ ├── clean-blog.min.js │ ├── contact_me.js │ └── jqBootstrapValidation.js ├── scss │ ├── _bootstrap-overrides.scss │ ├── _contact.scss │ ├── _footer.scss │ ├── _global.scss │ ├── _masthead.scss │ ├── _mixins.scss │ ├── _navbar.scss │ ├── _post.scss │ ├── _variables.scss │ └── clean-blog.scss └── vendor │ ├── bootstrap │ ├── css │ │ ├── bootstrap-grid.css │ │ ├── bootstrap-grid.css.map │ │ ├── bootstrap-grid.min.css │ │ ├── bootstrap-grid.min.css.map │ │ ├── bootstrap-reboot.css │ │ ├── bootstrap-reboot.css.map │ │ ├── bootstrap-reboot.min.css │ │ ├── bootstrap-reboot.min.css.map │ │ ├── bootstrap.css │ │ ├── bootstrap.css.map │ │ ├── bootstrap.min.css │ │ └── bootstrap.min.css.map │ └── js │ │ ├── bootstrap.bundle.js │ │ ├── bootstrap.bundle.js.map │ │ ├── bootstrap.bundle.min.js │ │ ├── bootstrap.bundle.min.js.map │ │ ├── bootstrap.js │ │ ├── bootstrap.js.map │ │ ├── bootstrap.min.js │ │ └── bootstrap.min.js.map │ ├── fontawesome-free │ ├── css │ │ ├── all.css │ │ ├── all.min.css │ │ ├── brands.css │ │ ├── brands.min.css │ │ ├── fontawesome.css │ │ ├── fontawesome.min.css │ │ ├── regular.css │ │ ├── regular.min.css │ │ ├── solid.css │ │ ├── solid.min.css │ │ ├── svg-with-js.css │ │ ├── svg-with-js.min.css │ │ ├── v4-shims.css │ │ └── v4-shims.min.css │ └── webfonts │ │ ├── fa-brands-400.eot │ │ ├── fa-brands-400.svg │ │ ├── fa-brands-400.woff │ │ ├── fa-brands-400.woff2 │ │ ├── fa-regular-400.svg │ │ ├── fa-regular-400.ttf │ │ ├── fa-regular-400.woff │ │ ├── fa-regular-400.woff2 │ │ ├── fa-solid-900.eot │ │ ├── fa-solid-900.svg │ │ ├── fa-solid-900.ttf │ │ ├── fa-solid-900.woff │ │ └── fa-solid-900.woff2 │ └── jquery │ ├── jquery.js │ ├── jquery.min.js │ ├── jquery.min.map │ ├── jquery.slim.js │ ├── jquery.slim.min.js │ └── jquery.slim.min.map └── templates ├── about.html ├── contact.html ├── footer.html ├── header.html ├── index.html ├── login.html ├── make-post.html ├── post.html └── register.html /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/blog-with-users-end.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /blog.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angelabauer/flask-blog/cf103d4e4f0f5528cfce0818b927f885abaf8423/blog.db -------------------------------------------------------------------------------- /forms.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import StringField, SubmitField, PasswordField 3 | from wtforms.validators import DataRequired, URL 4 | from flask_ckeditor import CKEditorField 5 | 6 | 7 | ##WTForm 8 | class CreatePostForm(FlaskForm): 9 | title = StringField("Blog Post Title", validators=[DataRequired()]) 10 | subtitle = StringField("Subtitle", validators=[DataRequired()]) 11 | img_url = StringField("Blog Image URL", validators=[DataRequired(), URL()]) 12 | body = CKEditorField("Blog Content", validators=[DataRequired()]) 13 | submit = SubmitField("Submit Post") 14 | 15 | 16 | class RegisterForm(FlaskForm): 17 | email = StringField("Email", validators=[DataRequired()]) 18 | password = PasswordField("Password", validators=[DataRequired()]) 19 | name = StringField("Name", validators=[DataRequired()]) 20 | submit = SubmitField("Sign Me Up!") 21 | 22 | 23 | class LoginForm(FlaskForm): 24 | email = StringField("Email", validators=[DataRequired()]) 25 | password = PasswordField("Password", validators=[DataRequired()]) 26 | submit = SubmitField("Let Me In!") 27 | 28 | 29 | class CommentForm(FlaskForm): 30 | comment_text = CKEditorField("Comment", validators=[DataRequired()]) 31 | submit = SubmitField("Submit Comment") 32 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, redirect, url_for, flash, abort 2 | from flask_bootstrap import Bootstrap 3 | from flask_ckeditor import CKEditor 4 | from datetime import date 5 | from functools import wraps 6 | from werkzeug.security import generate_password_hash, check_password_hash 7 | from flask_sqlalchemy import SQLAlchemy 8 | from sqlalchemy.orm import relationship 9 | from flask_login import UserMixin, login_user, LoginManager, login_required, current_user, logout_user 10 | from forms import LoginForm, RegisterForm, CreatePostForm, CommentForm 11 | from flask_gravatar import Gravatar 12 | 13 | app = Flask(__name__) 14 | app.config['SECRET_KEY'] = '8BYkEfBA6O6donzWlSihBXox7C0sKR6b' 15 | ckeditor = CKEditor(app) 16 | Bootstrap(app) 17 | gravatar = Gravatar(app, size=100, rating='g', default='retro', force_default=False, force_lower=False, use_ssl=False, base_url=None) 18 | 19 | ##CONNECT TO DB 20 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///blog.db' 21 | app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False 22 | db = SQLAlchemy(app) 23 | login_manager = LoginManager() 24 | login_manager.init_app(app) 25 | 26 | 27 | @login_manager.user_loader 28 | def load_user(user_id): 29 | return User.query.get(int(user_id)) 30 | 31 | 32 | ##CONFIGURE TABLE 33 | class User(UserMixin, db.Model): 34 | __tablename__ = "users" 35 | id = db.Column(db.Integer, primary_key=True) 36 | email = db.Column(db.String(100), unique=True) 37 | password = db.Column(db.String(100)) 38 | name = db.Column(db.String(100)) 39 | posts = relationship("BlogPost", back_populates="author") 40 | comments = relationship("Comment", back_populates="comment_author") 41 | 42 | 43 | class BlogPost(db.Model): 44 | __tablename__ = "blog_posts" 45 | id = db.Column(db.Integer, primary_key=True) 46 | author_id = db.Column(db.Integer, db.ForeignKey("users.id")) 47 | author = relationship("User", back_populates="posts") 48 | title = db.Column(db.String(250), unique=True, nullable=False) 49 | subtitle = db.Column(db.String(250), nullable=False) 50 | date = db.Column(db.String(250), nullable=False) 51 | body = db.Column(db.Text, nullable=False) 52 | img_url = db.Column(db.String(250), nullable=False) 53 | comments = relationship("Comment", back_populates="parent_post") 54 | 55 | 56 | class Comment(db.Model): 57 | __tablename__ = "comments" 58 | id = db.Column(db.Integer, primary_key=True) 59 | post_id = db.Column(db.Integer, db.ForeignKey("blog_posts.id")) 60 | author_id = db.Column(db.Integer, db.ForeignKey("users.id")) 61 | parent_post = relationship("BlogPost", back_populates="comments") 62 | comment_author = relationship("User", back_populates="comments") 63 | text = db.Column(db.Text, nullable=False) 64 | db.create_all() 65 | 66 | 67 | def admin_only(f): 68 | @wraps(f) 69 | def decorated_function(*args, **kwargs): 70 | if current_user.id != 1: 71 | return abort(403) 72 | return f(*args, **kwargs) 73 | return decorated_function 74 | 75 | 76 | @app.route('/') 77 | def get_all_posts(): 78 | posts = BlogPost.query.all() 79 | return render_template("index.html", all_posts=posts, current_user=current_user) 80 | 81 | 82 | @app.route('/register', methods=["GET", "POST"]) 83 | def register(): 84 | form = RegisterForm() 85 | if form.validate_on_submit(): 86 | 87 | if User.query.filter_by(email=form.email.data).first(): 88 | print(User.query.filter_by(email=form.email.data).first()) 89 | #User already exists 90 | flash("You've already signed up with that email, log in instead!") 91 | return redirect(url_for('login')) 92 | 93 | hash_and_salted_password = generate_password_hash( 94 | form.password.data, 95 | method='pbkdf2:sha256', 96 | salt_length=8 97 | ) 98 | new_user = User( 99 | email=form.email.data, 100 | name=form.name.data, 101 | password=hash_and_salted_password, 102 | ) 103 | db.session.add(new_user) 104 | db.session.commit() 105 | login_user(new_user) 106 | return redirect(url_for("get_all_posts")) 107 | 108 | return render_template("register.html", form=form, current_user=current_user) 109 | 110 | 111 | @app.route('/login', methods=["GET", "POST"]) 112 | def login(): 113 | form = LoginForm() 114 | if form.validate_on_submit(): 115 | email = form.email.data 116 | password = form.password.data 117 | 118 | user = User.query.filter_by(email=email).first() 119 | # Email doesn't exist or password incorrect. 120 | if not user: 121 | flash("That email does not exist, please try again.") 122 | return redirect(url_for('login')) 123 | elif not check_password_hash(user.password, password): 124 | flash('Password incorrect, please try again.') 125 | return redirect(url_for('login')) 126 | else: 127 | login_user(user) 128 | return redirect(url_for('get_all_posts')) 129 | return render_template("login.html", form=form, current_user=current_user) 130 | 131 | 132 | @app.route('/logout') 133 | def logout(): 134 | logout_user() 135 | return redirect(url_for('get_all_posts')) 136 | 137 | 138 | @app.route("/post/", methods=["GET", "POST"]) 139 | def show_post(post_id): 140 | form = CommentForm() 141 | requested_post = BlogPost.query.get(post_id) 142 | 143 | if form.validate_on_submit(): 144 | if not current_user.is_authenticated: 145 | flash("You need to login or register to comment.") 146 | return redirect(url_for("login")) 147 | 148 | new_comment = Comment( 149 | text=form.comment_text.data, 150 | comment_author=current_user, 151 | parent_post=requested_post 152 | ) 153 | db.session.add(new_comment) 154 | db.session.commit() 155 | 156 | return render_template("post.html", post=requested_post, form=form, current_user=current_user) 157 | 158 | 159 | @app.route("/about") 160 | def about(): 161 | return render_template("about.html", current_user=current_user) 162 | 163 | 164 | @app.route("/contact") 165 | def contact(): 166 | return render_template("contact.html", current_user=current_user) 167 | 168 | 169 | @app.route("/new-post", methods=["GET", "POST"]) 170 | @admin_only 171 | def add_new_post(): 172 | form = CreatePostForm() 173 | if form.validate_on_submit(): 174 | new_post = BlogPost( 175 | title=form.title.data, 176 | subtitle=form.subtitle.data, 177 | body=form.body.data, 178 | img_url=form.img_url.data, 179 | author=current_user, 180 | date=date.today().strftime("%B %d, %Y") 181 | ) 182 | db.session.add(new_post) 183 | db.session.commit() 184 | return redirect(url_for("get_all_posts")) 185 | 186 | return render_template("make-post.html", form=form, current_user=current_user) 187 | 188 | 189 | 190 | 191 | @app.route("/edit-post/", methods=["GET", "POST"]) 192 | @admin_only 193 | def edit_post(post_id): 194 | post = BlogPost.query.get(post_id) 195 | edit_form = CreatePostForm( 196 | title=post.title, 197 | subtitle=post.subtitle, 198 | img_url=post.img_url, 199 | author=current_user, 200 | body=post.body 201 | ) 202 | if edit_form.validate_on_submit(): 203 | post.title = edit_form.title.data 204 | post.subtitle = edit_form.subtitle.data 205 | post.img_url = edit_form.img_url.data 206 | post.body = edit_form.body.data 207 | db.session.commit() 208 | return redirect(url_for("show_post", post_id=post.id)) 209 | 210 | return render_template("make-post.html", form=edit_form, is_edit=True, current_user=current_user) 211 | 212 | 213 | @app.route("/delete/") 214 | @admin_only 215 | def delete_post(post_id): 216 | post_to_delete = BlogPost.query.get(post_id) 217 | db.session.delete(post_to_delete) 218 | db.session.commit() 219 | return redirect(url_for('get_all_posts')) 220 | 221 | 222 | if __name__ == "__main__": 223 | app.run(host='0.0.0.0', port=5000) 224 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | category = "main" 3 | description = "Composable command line interface toolkit" 4 | name = "click" 5 | optional = false 6 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 7 | version = "7.1.2" 8 | 9 | [[package]] 10 | category = "main" 11 | description = "A simple framework for building complex web applications." 12 | name = "flask" 13 | optional = false 14 | python-versions = "*" 15 | version = "1.0.2" 16 | 17 | [package.dependencies] 18 | Jinja2 = ">=2.10" 19 | Werkzeug = ">=0.14" 20 | click = ">=5.1" 21 | itsdangerous = ">=0.24" 22 | 23 | [package.extras] 24 | dev = ["pytest (>=3)", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet"] 25 | docs = ["sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet"] 26 | dotenv = ["python-dotenv"] 27 | 28 | [[package]] 29 | category = "main" 30 | description = "Various helpers to pass data to untrusted environments and back." 31 | name = "itsdangerous" 32 | optional = false 33 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 34 | version = "1.1.0" 35 | 36 | [[package]] 37 | category = "main" 38 | description = "A very fast and expressive template engine." 39 | name = "jinja2" 40 | optional = false 41 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 42 | version = "2.11.2" 43 | 44 | [package.dependencies] 45 | MarkupSafe = ">=0.23" 46 | 47 | [package.extras] 48 | i18n = ["Babel (>=0.8)"] 49 | 50 | [[package]] 51 | category = "main" 52 | description = "Safely add untrusted strings to HTML/XML markup." 53 | name = "markupsafe" 54 | optional = false 55 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" 56 | version = "1.1.1" 57 | 58 | [[package]] 59 | category = "main" 60 | description = "The comprehensive WSGI web application library." 61 | name = "werkzeug" 62 | optional = false 63 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 64 | version = "1.0.1" 65 | 66 | [package.extras] 67 | dev = ["pytest", "pytest-timeout", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinx-issues"] 68 | watchdog = ["watchdog"] 69 | 70 | [metadata] 71 | content-hash = "063f8c74154580826be2e32820af9c11e8f1fb9bf35b7fddc1a48acd748fcb20" 72 | python-versions = "^3.8" 73 | 74 | [metadata.files] 75 | click = [ 76 | {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, 77 | {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, 78 | ] 79 | flask = [ 80 | {file = "Flask-1.0.2-py2.py3-none-any.whl", hash = "sha256:a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05"}, 81 | {file = "Flask-1.0.2.tar.gz", hash = "sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48"}, 82 | ] 83 | itsdangerous = [ 84 | {file = "itsdangerous-1.1.0-py2.py3-none-any.whl", hash = "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"}, 85 | {file = "itsdangerous-1.1.0.tar.gz", hash = "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19"}, 86 | ] 87 | jinja2 = [ 88 | {file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"}, 89 | {file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"}, 90 | ] 91 | markupsafe = [ 92 | {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, 93 | {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"}, 94 | {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"}, 95 | {file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"}, 96 | {file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"}, 97 | {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"}, 98 | {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"}, 99 | {file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"}, 100 | {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"}, 101 | {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"}, 102 | {file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"}, 103 | {file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"}, 104 | {file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"}, 105 | {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"}, 106 | {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"}, 107 | {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, 108 | {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, 109 | {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, 110 | {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, 111 | {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, 112 | {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, 113 | {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, 114 | {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, 115 | {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, 116 | {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, 117 | {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, 118 | {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, 119 | {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, 120 | {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, 121 | {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, 122 | {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, 123 | {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, 124 | {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, 125 | ] 126 | werkzeug = [ 127 | {file = "Werkzeug-1.0.1-py2.py3-none-any.whl", hash = "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43"}, 128 | {file = "Werkzeug-1.0.1.tar.gz", hash = "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"}, 129 | ] 130 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool] 2 | [tool.poetry] 3 | authors = ["Your Name "] 4 | name = "root" 5 | version = "0.0.0" 6 | description="flask template" 7 | [tool.poetry.dependencies] 8 | flask = "==1.0.2" 9 | python = "^3.8" 10 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2020.6.20 2 | chardet==3.0.4 3 | click==7.1.2 4 | dominate==2.5.2 5 | Flask==1.1.2 6 | Flask-Bootstrap==3.3.7.1 7 | Flask-CKEditor==0.4.4.1 8 | Flask-Gravatar==0.5.0 9 | Flask-Login==0.5.0 10 | Flask-SQLAlchemy==2.4.4 11 | Flask-WTF==0.14.3 12 | idna==2.10 13 | itsdangerous==1.1.0 14 | Jinja2==2.11.2 15 | MarkupSafe==1.1.1 16 | requests==2.24.0 17 | SQLAlchemy==1.3.19 18 | urllib3==1.25.10 19 | visitor==0.1.3 20 | Werkzeug==1.0.1 21 | WTForms==2.3.3 22 | -------------------------------------------------------------------------------- /static/css/clean-blog.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Start Bootstrap - Clean Blog v5.0.9 (https://startbootstrap.com/themes/clean-blog) 3 | * Copyright 2013-2020 Start Bootstrap 4 | * Licensed under MIT (https://github.com/StartBootstrap/startbootstrap-clean-blog/blob/master/LICENSE) 5 | */ 6 | 7 | body { 8 | font-size: 20px; 9 | color: #212529; 10 | font-family: 'Lora', 'Times New Roman', serif; 11 | } 12 | 13 | p { 14 | line-height: 1.5; 15 | margin: 30px 0; 16 | } 17 | 18 | p a { 19 | text-decoration: underline; 20 | } 21 | 22 | h1, 23 | h2, 24 | h3, 25 | h4, 26 | h5, 27 | h6 { 28 | font-weight: 800; 29 | font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; 30 | } 31 | 32 | a { 33 | color: #212529; 34 | transition: all 0.2s; 35 | } 36 | 37 | a:focus, a:hover { 38 | color: #0085A1; 39 | } 40 | 41 | blockquote { 42 | font-style: italic; 43 | color: #868e96; 44 | } 45 | 46 | .section-heading { 47 | font-size: 36px; 48 | font-weight: 700; 49 | margin-top: 60px; 50 | } 51 | 52 | .caption { 53 | font-size: 14px; 54 | font-style: italic; 55 | display: block; 56 | margin: 0; 57 | padding: 10px; 58 | text-align: center; 59 | border-bottom-right-radius: 5px; 60 | border-bottom-left-radius: 5px; 61 | } 62 | content p { 63 | font-style: italic; 64 | color:red; 65 | } 66 | 67 | ::-moz-selection { 68 | color: #fff; 69 | background: #0085A1; 70 | text-shadow: none; 71 | } 72 | 73 | ::selection { 74 | color: #fff; 75 | background: #0085A1; 76 | text-shadow: none; 77 | } 78 | 79 | img::-moz-selection { 80 | color: #fff; 81 | background: transparent; 82 | } 83 | 84 | img::selection { 85 | color: #fff; 86 | background: transparent; 87 | } 88 | 89 | img::-moz-selection { 90 | color: #fff; 91 | background: transparent; 92 | } 93 | .comment { 94 | padding-top: 20px; 95 | } 96 | .detailBox { 97 | width:320px; 98 | border:1px solid #bbb; 99 | margin:50px; 100 | } 101 | .titleBox { 102 | background-color:#fdfdfd; 103 | padding:10px; 104 | } 105 | .titleBox label{ 106 | color:#444; 107 | margin:0; 108 | display:inline-block; 109 | } 110 | 111 | .commentBox { 112 | padding:10px; 113 | border-top:1px dotted #bbb; 114 | } 115 | .commentBox .form-group:first-child, .actionBox .form-group:first-child { 116 | width:80%; 117 | } 118 | .commentBox .form-group:nth-child(2), .actionBox .form-group:nth-child(2) { 119 | width:18%; 120 | } 121 | .actionBox .form-group * { 122 | width:100%; 123 | } 124 | .taskDescription { 125 | margin-top:10px 0; 126 | } 127 | .commentList { 128 | padding:0; 129 | list-style:none; 130 | max-height:200px; 131 | overflow:auto; 132 | } 133 | .commentList li { 134 | margin:0; 135 | margin-top:10px; 136 | } 137 | .commentList li > div { 138 | display:table-cell; 139 | } 140 | .commenterImage { 141 | width:30px; 142 | margin-right:5px; 143 | height:100%; 144 | float:left; 145 | } 146 | .commenterImage img { 147 | width:100%; 148 | border-radius:50%; 149 | } 150 | .commentText p { 151 | margin:0; 152 | } 153 | .sub-text { 154 | color:#aaa; 155 | font-family:verdana; 156 | font-size:11px; 157 | } 158 | .actionBox { 159 | border-top:1px dotted #bbb; 160 | padding:10px; 161 | } 162 | 163 | #mainNav { 164 | position: absolute; 165 | border-bottom: 1px solid #e9ecef; 166 | background-color: white; 167 | font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; 168 | } 169 | 170 | #mainNav .navbar-brand { 171 | font-weight: 800; 172 | color: #343a40; 173 | } 174 | 175 | #mainNav .navbar-toggler { 176 | font-size: 12px; 177 | font-weight: 800; 178 | padding: 13px; 179 | text-transform: uppercase; 180 | color: #343a40; 181 | } 182 | 183 | #mainNav .navbar-nav > li.nav-item > a { 184 | font-size: 12px; 185 | font-weight: 800; 186 | letter-spacing: 1px; 187 | text-transform: uppercase; 188 | } 189 | 190 | @media only screen and (min-width: 992px) { 191 | #mainNav { 192 | border-bottom: 1px solid transparent; 193 | background: transparent; 194 | } 195 | #mainNav .navbar-brand { 196 | padding: 10px 20px; 197 | color: #fff; 198 | } 199 | #mainNav .navbar-brand:focus, #mainNav .navbar-brand:hover { 200 | color: rgba(255, 255, 255, 0.8); 201 | } 202 | #mainNav .navbar-nav > li.nav-item > a { 203 | padding: 10px 20px; 204 | color: #fff; 205 | } 206 | #mainNav .navbar-nav > li.nav-item > a:focus, #mainNav .navbar-nav > li.nav-item > a:hover { 207 | color: rgba(255, 255, 255, 0.8); 208 | } 209 | } 210 | 211 | @media only screen and (min-width: 992px) { 212 | #mainNav { 213 | transition: background-color 0.2s; 214 | /* Force Hardware Acceleration in WebKit */ 215 | transform: translate3d(0, 0, 0); 216 | -webkit-backface-visibility: hidden; 217 | } 218 | #mainNav.is-fixed { 219 | /* when the user scrolls down, we hide the header right above the viewport */ 220 | position: fixed; 221 | top: -67px; 222 | transition: transform 0.2s; 223 | border-bottom: 1px solid white; 224 | background-color: rgba(255, 255, 255, 0.9); 225 | } 226 | #mainNav.is-fixed .navbar-brand { 227 | color: #212529; 228 | } 229 | #mainNav.is-fixed .navbar-brand:focus, #mainNav.is-fixed .navbar-brand:hover { 230 | color: #0085A1; 231 | } 232 | #mainNav.is-fixed .navbar-nav > li.nav-item > a { 233 | color: #212529; 234 | } 235 | #mainNav.is-fixed .navbar-nav > li.nav-item > a:focus, #mainNav.is-fixed .navbar-nav > li.nav-item > a:hover { 236 | color: #0085A1; 237 | } 238 | #mainNav.is-visible { 239 | /* if the user changes the scrolling direction, we show the header */ 240 | transform: translate3d(0, 100%, 0); 241 | } 242 | } 243 | 244 | header.masthead { 245 | margin-bottom: 50px; 246 | background: no-repeat center center; 247 | background-color: #868e96; 248 | background-attachment: scroll; 249 | position: relative; 250 | background-size: cover; 251 | } 252 | 253 | header.masthead .overlay { 254 | position: absolute; 255 | top: 0; 256 | left: 0; 257 | height: 100%; 258 | width: 100%; 259 | background-color: #212529; 260 | opacity: 0.5; 261 | } 262 | 263 | header.masthead .page-heading, 264 | header.masthead .post-heading, 265 | header.masthead .site-heading { 266 | padding: 200px 0 150px; 267 | color: white; 268 | } 269 | 270 | @media only screen and (min-width: 768px) { 271 | header.masthead .page-heading, 272 | header.masthead .post-heading, 273 | header.masthead .site-heading { 274 | padding: 200px 0; 275 | } 276 | } 277 | 278 | header.masthead .page-heading, 279 | header.masthead .site-heading { 280 | text-align: center; 281 | } 282 | 283 | header.masthead .page-heading h1, 284 | header.masthead .site-heading h1 { 285 | font-size: 50px; 286 | margin-top: 0; 287 | } 288 | 289 | header.masthead .page-heading .subheading, 290 | header.masthead .site-heading .subheading { 291 | font-size: 24px; 292 | font-weight: 300; 293 | line-height: 1.1; 294 | display: block; 295 | margin: 10px 0 0; 296 | font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; 297 | } 298 | 299 | @media only screen and (min-width: 768px) { 300 | header.masthead .page-heading h1, 301 | header.masthead .site-heading h1 { 302 | font-size: 80px; 303 | } 304 | } 305 | 306 | header.masthead .post-heading h1 { 307 | font-size: 35px; 308 | } 309 | 310 | header.masthead .post-heading .meta, 311 | header.masthead .post-heading .subheading { 312 | line-height: 1.1; 313 | display: block; 314 | } 315 | 316 | header.masthead .post-heading .subheading { 317 | font-size: 24px; 318 | font-weight: 600; 319 | margin: 10px 0 30px; 320 | font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; 321 | } 322 | 323 | header.masthead .post-heading .meta { 324 | font-size: 20px; 325 | font-weight: 300; 326 | font-style: italic; 327 | font-family: 'Lora', 'Times New Roman', serif; 328 | } 329 | 330 | header.masthead .post-heading .meta a { 331 | color: #fff; 332 | } 333 | 334 | @media only screen and (min-width: 768px) { 335 | header.masthead .post-heading h1 { 336 | font-size: 55px; 337 | } 338 | header.masthead .post-heading .subheading { 339 | font-size: 30px; 340 | } 341 | } 342 | 343 | .post-preview > a { 344 | color: #212529; 345 | } 346 | 347 | .post-preview > a:focus, .post-preview > a:hover { 348 | text-decoration: none; 349 | color: #0085A1; 350 | } 351 | 352 | .post-preview > a > .post-title { 353 | font-size: 30px; 354 | margin-top: 30px; 355 | margin-bottom: 10px; 356 | } 357 | 358 | .post-preview > a > .post-subtitle { 359 | font-weight: 300; 360 | margin: 0 0 10px; 361 | } 362 | 363 | .post-preview > .post-meta { 364 | font-size: 18px; 365 | font-style: italic; 366 | margin-top: 0; 367 | color: #868e96; 368 | } 369 | 370 | .post-preview > .post-meta > a { 371 | text-decoration: none; 372 | color: #212529; 373 | } 374 | 375 | .post-preview > .post-meta > a:focus, .post-preview > .post-meta > a:hover { 376 | text-decoration: underline; 377 | color: #0085A1; 378 | } 379 | 380 | @media only screen and (min-width: 768px) { 381 | .post-preview > a > .post-title { 382 | font-size: 36px; 383 | } 384 | } 385 | .form-control { 386 | font-size: 1.2rem; 387 | font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; 388 | } 389 | .floating-label-form-group { 390 | font-size: 14px; 391 | position: relative; 392 | margin-bottom: 0; 393 | padding-bottom: 0.5em; 394 | border-bottom: 1px solid #dee2e6; 395 | } 396 | 397 | .floating-label-form-group input, 398 | .floating-label-form-group textarea { 399 | font-size: 1.5em; 400 | position: relative; 401 | z-index: 1; 402 | padding: 0; 403 | resize: none; 404 | border: none; 405 | border-radius: 0; 406 | background: none; 407 | box-shadow: none !important; 408 | font-family: 'Lora', 'Times New Roman', serif; 409 | } 410 | 411 | .floating-label-form-group input::-webkit-input-placeholder, 412 | .floating-label-form-group textarea::-webkit-input-placeholder { 413 | color: #868e96; 414 | font-family: 'Lora', 'Times New Roman', serif; 415 | } 416 | 417 | .floating-label-form-group label { 418 | font-size: 0.85em; 419 | line-height: 1.764705882em; 420 | position: relative; 421 | z-index: 0; 422 | top: 2em; 423 | display: block; 424 | margin: 0; 425 | transition: top 0.3s ease, opacity 0.3s ease; 426 | opacity: 0; 427 | } 428 | 429 | .floating-label-form-group .help-block { 430 | margin: 15px 0; 431 | } 432 | 433 | .floating-label-form-group-with-value label { 434 | top: 0; 435 | opacity: 1; 436 | } 437 | 438 | .floating-label-form-group-with-focus label { 439 | color: #0085A1; 440 | } 441 | 442 | form .form-group:first-child .floating-label-form-group { 443 | border-top: 1px solid #dee2e6; 444 | } 445 | 446 | footer { 447 | padding: 50px 0 65px; 448 | } 449 | 450 | footer .list-inline { 451 | margin: 0; 452 | padding: 0; 453 | } 454 | 455 | footer .copyright { 456 | font-size: 14px; 457 | margin-bottom: 0; 458 | text-align: center; 459 | } 460 | 461 | .btn { 462 | font-size: 14px; 463 | font-weight: 800; 464 | padding: 15px 25px; 465 | letter-spacing: 1px; 466 | text-transform: uppercase; 467 | border-radius: 0; 468 | font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; 469 | } 470 | 471 | .btn-primary { 472 | background-color: #0085A1; 473 | border-color: #0085A1; 474 | } 475 | 476 | .btn-primary:hover, .btn-primary:focus, .btn-primary:active { 477 | color: #fff; 478 | background-color: #00657b !important; 479 | border-color: #00657b !important; 480 | } 481 | 482 | .btn-lg { 483 | font-size: 16px; 484 | padding: 25px 35px; 485 | } 486 | -------------------------------------------------------------------------------- /static/css/clean-blog.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Start Bootstrap - Clean Blog v5.0.9 (https://startbootstrap.com/themes/clean-blog) 3 | * Copyright 2013-2020 Start Bootstrap 4 | * Licensed under MIT (https://github.com/StartBootstrap/startbootstrap-clean-blog/blob/master/LICENSE) 5 | */body{font-size:20px;color:#212529;font-family:Lora,'Times New Roman',serif}p{line-height:1.5;margin:30px 0}p a{text-decoration:underline}h1,h2,h3,h4,h5,h6{font-weight:800;font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif}a{color:#212529;transition:all .2s}a:focus,a:hover{color:#0085a1}blockquote{font-style:italic;color:#868e96}.section-heading{font-size:36px;font-weight:700;margin-top:60px}.comment { 6 | padding-top: 20px; 7 | }.detailBox { 8 | width:320px; 9 | border:1px solid #bbb; 10 | margin:50px; 11 | } 12 | .titleBox { 13 | background-color:#fdfdfd; 14 | padding:10px; 15 | } 16 | .titleBox label{ 17 | color:#444; 18 | margin:0; 19 | display:inline-block; 20 | } 21 | 22 | .commentBox { 23 | padding:10px; 24 | border-top:1px dotted #bbb; 25 | } 26 | .commentBox .form-group:first-child, .actionBox .form-group:first-child { 27 | width:80%; 28 | } 29 | .commentBox .form-group:nth-child(2), .actionBox .form-group:nth-child(2) { 30 | width:18%; 31 | } 32 | .actionBox .form-group * { 33 | width:100%; 34 | } 35 | .taskDescription { 36 | margin-top:10px 0; 37 | } 38 | .commentList { 39 | padding:0; 40 | list-style:none; 41 | max-height:200px; 42 | overflow:auto; 43 | } 44 | .commentList li { 45 | margin:0; 46 | margin-top:10px; 47 | } 48 | .commentList li > div { 49 | display:table-cell; 50 | } 51 | .commenterImage { 52 | width:30px; 53 | margin-right:5px; 54 | height:100%; 55 | float:left; 56 | } 57 | .commenterImage img { 58 | width:100%; 59 | border-radius:50%; 60 | } 61 | .commentText p { 62 | margin:0; 63 | } 64 | .sub-text { 65 | color:#aaa; 66 | font-family:verdana; 67 | font-size:11px; 68 | } 69 | .actionBox { 70 | border-top:1px dotted #bbb; 71 | padding:10px; 72 | }.content p{color:red;font-style:italic;font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;}.caption{font-size:14px;font-style:italic;display:block;margin:0;padding:10px;text-align:center;border-bottom-right-radius:5px;border-bottom-left-radius:5px}::-moz-selection{color:#fff;background:#0085a1;text-shadow:none}::selection{color:#fff;background:#0085a1;text-shadow:none}img::-moz-selection{color:#fff;background:0 0}img::selection{color:#fff;background:0 0}img::-moz-selection{color:#fff;background:0 0}#mainNav{position:absolute;border-bottom:1px solid #e9ecef;background-color:#fff;font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif}#mainNav .navbar-brand{font-weight:800;color:#343a40}#mainNav .navbar-toggler{font-size:12px;font-weight:800;padding:13px;text-transform:uppercase;color:#343a40}#mainNav .navbar-nav>li.nav-item>a{font-size:12px;font-weight:800;letter-spacing:1px;text-transform:uppercase}@media only screen and (min-width:992px){#mainNav{border-bottom:1px solid transparent;background:0 0}#mainNav .navbar-brand{padding:10px 20px;color:#fff}#mainNav .navbar-brand:focus,#mainNav .navbar-brand:hover{color:rgba(255,255,255,.8)}#mainNav .navbar-nav>li.nav-item>a{padding:10px 20px;color:#fff}#mainNav .navbar-nav>li.nav-item>a:focus,#mainNav .navbar-nav>li.nav-item>a:hover{color:rgba(255,255,255,.8)}}@media only screen and (min-width:992px){#mainNav{transition:background-color .2s;transform:translate3d(0,0,0);-webkit-backface-visibility:hidden}#mainNav.is-fixed{position:fixed;top:-67px;transition:transform .2s;border-bottom:1px solid #fff;background-color:rgba(255,255,255,.9)}#mainNav.is-fixed .navbar-brand{color:#212529}#mainNav.is-fixed .navbar-brand:focus,#mainNav.is-fixed .navbar-brand:hover{color:#0085a1}#mainNav.is-fixed .navbar-nav>li.nav-item>a{color:#212529}#mainNav.is-fixed .navbar-nav>li.nav-item>a:focus,#mainNav.is-fixed .navbar-nav>li.nav-item>a:hover{color:#0085a1}#mainNav.is-visible{transform:translate3d(0,100%,0)}}header.masthead{margin-bottom:50px;background:no-repeat center center;background-color:#868e96;background-attachment:scroll;position:relative;background-size:cover}header.masthead .overlay{position:absolute;top:0;left:0;height:100%;width:100%;background-color:#212529;opacity:.5}header.masthead .page-heading,header.masthead .post-heading,header.masthead .site-heading{padding:200px 0 150px;color:#fff}@media only screen and (min-width:768px){header.masthead .page-heading,header.masthead .post-heading,header.masthead .site-heading{padding:200px 0}}header.masthead .page-heading,header.masthead .site-heading{text-align:center}header.masthead .page-heading h1,header.masthead .site-heading h1{font-size:50px;margin-top:0}header.masthead .page-heading .subheading,header.masthead .site-heading .subheading{font-size:24px;font-weight:300;line-height:1.1;display:block;margin:10px 0 0;font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif}@media only screen and (min-width:768px){header.masthead .page-heading h1,header.masthead .site-heading h1{font-size:80px}}header.masthead .post-heading h1{font-size:35px}header.masthead .post-heading .meta,header.masthead .post-heading .subheading{line-height:1.1;display:block}header.masthead .post-heading .subheading{font-size:24px;font-weight:600;margin:10px 0 30px;font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif}header.masthead .post-heading .meta{font-size:20px;font-weight:300;font-style:italic;font-family:Lora,'Times New Roman',serif}header.masthead .post-heading .meta a{color:#fff}@media only screen and (min-width:768px){header.masthead .post-heading h1{font-size:55px}header.masthead .post-heading .subheading{font-size:30px}}.post-preview>a{color:#212529}.post-preview>a:focus,.post-preview>a:hover{text-decoration:none;color:#0085a1}.post-preview>a>.post-title{font-size:30px;margin-top:30px;margin-bottom:10px}.post-preview>a>.post-subtitle{font-weight:300;margin:0 0 10px}.post-preview>.post-meta{font-size:18px;font-style:italic;margin-top:0;color:#868e96}.post-preview>.post-meta>a{text-decoration:none;color:#212529}.post-preview>.post-meta>a:focus,.post-preview>.post-meta>a:hover{text-decoration:underline;color:#0085a1}@media only screen and (min-width:768px){.post-preview>a>.post-title{font-size:36px}}.floating-label-form-group{font-size:14px;position:relative;margin-bottom:0;padding-bottom:.5em;border-bottom:1px solid #dee2e6}.floating-label-form-group input,.floating-label-form-group textarea{font-size:1.5em;position:relative;z-index:1;padding:0;resize:none;border:none;border-radius:0;background:0 0;box-shadow:none!important;font-family:Lora,'Times New Roman',serif}.floating-label-form-group input::-webkit-input-placeholder,.floating-label-form-group textarea::-webkit-input-placeholder{color:#868e96;font-family:Lora,'Times New Roman',serif}.floating-label-form-group label{font-size:.85em;line-height:1.764705882em;position:relative;z-index:0;top:2em;display:block;margin:0;transition:top .3s ease,opacity .3s ease;opacity:0}.floating-label-form-group .help-block{margin:15px 0}.floating-label-form-group-with-value label{top:0;opacity:1}.floating-label-form-group-with-focus label{color:#0085a1}form .form-group:first-child .floating-label-form-group{border-top:1px solid #dee2e6}footer{padding:50px 0 65px}footer .list-inline{margin:0;padding:0}footer .copyright{font-size:14px;margin-bottom:0;text-align:center}.btn{font-size:14px;font-weight:800;padding:15px 25px;letter-spacing:1px;text-transform:uppercase;border-radius:0;font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif}.btn-primary{background-color:#0085a1;border-color:#0085a1}.btn-primary:active,.btn-primary:focus,.btn-primary:hover{color:#fff;background-color:#00657b!important;border-color:#00657b!important}.btn-lg{font-size:16px;padding:25px 35px}.form-control{font-size: 1.2rem;font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;} -------------------------------------------------------------------------------- /static/img/about-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angelabauer/flask-blog/cf103d4e4f0f5528cfce0818b927f885abaf8423/static/img/about-bg.jpg -------------------------------------------------------------------------------- /static/img/contact-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angelabauer/flask-blog/cf103d4e4f0f5528cfce0818b927f885abaf8423/static/img/contact-bg.jpg -------------------------------------------------------------------------------- /static/img/edit-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angelabauer/flask-blog/cf103d4e4f0f5528cfce0818b927f885abaf8423/static/img/edit-bg.jpg -------------------------------------------------------------------------------- /static/js/clean-blog.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | "use strict"; // Start of use strict 3 | 4 | // Floating label headings for the contact form 5 | $("body").on("input propertychange", ".floating-label-form-group", function(e) { 6 | $(this).toggleClass("floating-label-form-group-with-value", !!$(e.target).val()); 7 | }).on("focus", ".floating-label-form-group", function() { 8 | $(this).addClass("floating-label-form-group-with-focus"); 9 | }).on("blur", ".floating-label-form-group", function() { 10 | $(this).removeClass("floating-label-form-group-with-focus"); 11 | }); 12 | 13 | // Show the navbar when the page is scrolled up 14 | var MQL = 992; 15 | 16 | //primary navigation slide-in effect 17 | if ($(window).width() > MQL) { 18 | var headerHeight = $('#mainNav').height(); 19 | $(window).on('scroll', { 20 | previousTop: 0 21 | }, 22 | function() { 23 | var currentTop = $(window).scrollTop(); 24 | //check if user is scrolling up 25 | if (currentTop < this.previousTop) { 26 | //if scrolling up... 27 | if (currentTop > 0 && $('#mainNav').hasClass('is-fixed')) { 28 | $('#mainNav').addClass('is-visible'); 29 | } else { 30 | $('#mainNav').removeClass('is-visible is-fixed'); 31 | } 32 | } else if (currentTop > this.previousTop) { 33 | //if scrolling down... 34 | $('#mainNav').removeClass('is-visible'); 35 | if (currentTop > headerHeight && !$('#mainNav').hasClass('is-fixed')) $('#mainNav').addClass('is-fixed'); 36 | } 37 | this.previousTop = currentTop; 38 | }); 39 | } 40 | 41 | })(jQuery); // End of use strict 42 | -------------------------------------------------------------------------------- /static/js/clean-blog.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Start Bootstrap - Clean Blog v5.0.9 (https://startbootstrap.com/themes/clean-blog) 3 | * Copyright 2013-2020 Start Bootstrap 4 | * Licensed under MIT (https://github.com/StartBootstrap/startbootstrap-clean-blog/blob/master/LICENSE) 5 | */ 6 | 7 | !function(o){"use strict";o("body").on("input propertychange",".floating-label-form-group",function(i){o(this).toggleClass("floating-label-form-group-with-value",!!o(i.target).val())}).on("focus",".floating-label-form-group",function(){o(this).addClass("floating-label-form-group-with-focus")}).on("blur",".floating-label-form-group",function(){o(this).removeClass("floating-label-form-group-with-focus")});if(992this.previousTop&&(o("#mainNav").removeClass("is-visible"),s= 0) { 18 | firstName = name.split(' ').slice(0, -1).join(' '); 19 | } 20 | $this = $("#sendMessageButton"); 21 | $this.prop("disabled", true); // Disable submit button until AJAX call is complete to prevent duplicate messages 22 | $.ajax({ 23 | url: "././mail/contact_me.php", 24 | type: "POST", 25 | data: { 26 | name: name, 27 | phone: phone, 28 | email: email, 29 | message: message 30 | }, 31 | cache: false, 32 | success: function() { 33 | // Success message 34 | $('#success').html("
"); 35 | $('#success > .alert-success').html(""); 37 | $('#success > .alert-success') 38 | .append("Your message has been sent. "); 39 | $('#success > .alert-success') 40 | .append('
'); 41 | //clear all fields 42 | $('#contactForm').trigger("reset"); 43 | }, 44 | error: function() { 45 | // Fail message 46 | $('#success').html("
"); 47 | $('#success > .alert-danger').html(""); 49 | $('#success > .alert-danger').append($("").text("Sorry " + firstName + ", it seems that my mail server is not responding. Please try again later!")); 50 | $('#success > .alert-danger').append('
'); 51 | //clear all fields 52 | $('#contactForm').trigger("reset"); 53 | }, 54 | complete: function() { 55 | setTimeout(function() { 56 | $this.prop("disabled", false); // Re-enable submit button when AJAX call is complete 57 | }, 1000); 58 | } 59 | }); 60 | }, 61 | filter: function() { 62 | return $(this).is(":visible"); 63 | }, 64 | }); 65 | 66 | $("a[data-toggle=\"tab\"]").click(function(e) { 67 | e.preventDefault(); 68 | $(this).tab("show"); 69 | }); 70 | }); 71 | 72 | /*When clicking on Full hide fail/success boxes */ 73 | $('#name').focus(function() { 74 | $('#success').html(''); 75 | }); 76 | -------------------------------------------------------------------------------- /static/js/jqBootstrapValidation.js: -------------------------------------------------------------------------------- 1 | /* jqBootstrapValidation 2 | * A plugin for automating validation on Twitter Bootstrap formatted forms. 3 | * 4 | * v1.3.6 5 | * 6 | * License: MIT - see LICENSE file 7 | * 8 | * http://ReactiveRaven.github.com/jqBootstrapValidation/ 9 | */ 10 | 11 | (function($) { 12 | 13 | var createdElements = []; 14 | 15 | var defaults = { 16 | options: { 17 | prependExistingHelpBlock: false, 18 | sniffHtml: true, // sniff for 'required', 'maxlength', etc 19 | preventSubmit: true, // stop the form submit event from firing if validation fails 20 | submitError: false, // function called if there is an error when trying to submit 21 | submitSuccess: false, // function called just before a successful submit event is sent to the server 22 | semanticallyStrict: false, // set to true to tidy up generated HTML output 23 | autoAdd: { 24 | helpBlocks: true 25 | }, 26 | filter: function() { 27 | // return $(this).is(":visible"); // only validate elements you can see 28 | return true; // validate everything 29 | } 30 | }, 31 | methods: { 32 | init: function(options) { 33 | 34 | var settings = $.extend(true, {}, defaults); 35 | 36 | settings.options = $.extend(true, settings.options, options); 37 | 38 | var $siblingElements = this; 39 | 40 | var uniqueForms = $.unique( 41 | $siblingElements.map(function() { 42 | return $(this).parents("form")[0]; 43 | }).toArray() 44 | ); 45 | 46 | $(uniqueForms).bind("submit", function(e) { 47 | var $form = $(this); 48 | var warningsFound = 0; 49 | var $inputs = $form.find("input,textarea,select").not("[type=submit],[type=image]").filter(settings.options.filter); 50 | $inputs.trigger("submit.validation").trigger("validationLostFocus.validation"); 51 | 52 | $inputs.each(function(i, el) { 53 | var $this = $(el), 54 | $controlGroup = $this.parents(".form-group").first(); 55 | if ( 56 | $controlGroup.hasClass("warning") 57 | ) { 58 | $controlGroup.removeClass("warning").addClass("error"); 59 | warningsFound++; 60 | } 61 | }); 62 | 63 | $inputs.trigger("validationLostFocus.validation"); 64 | 65 | if (warningsFound) { 66 | if (settings.options.preventSubmit) { 67 | e.preventDefault(); 68 | } 69 | $form.addClass("error"); 70 | if ($.isFunction(settings.options.submitError)) { 71 | settings.options.submitError($form, e, $inputs.jqBootstrapValidation("collectErrors", true)); 72 | } 73 | } else { 74 | $form.removeClass("error"); 75 | if ($.isFunction(settings.options.submitSuccess)) { 76 | settings.options.submitSuccess($form, e); 77 | } 78 | } 79 | }); 80 | 81 | return this.each(function() { 82 | 83 | // Get references to everything we're interested in 84 | var $this = $(this), 85 | $controlGroup = $this.parents(".form-group").first(), 86 | $helpBlock = $controlGroup.find(".help-block").first(), 87 | $form = $this.parents("form").first(), 88 | validatorNames = []; 89 | 90 | // create message container if not exists 91 | if (!$helpBlock.length && settings.options.autoAdd && settings.options.autoAdd.helpBlocks) { 92 | $helpBlock = $('
'); 93 | $controlGroup.find('.controls').append($helpBlock); 94 | createdElements.push($helpBlock[0]); 95 | } 96 | 97 | // ============================================================= 98 | // SNIFF HTML FOR VALIDATORS 99 | // ============================================================= 100 | 101 | // *snort sniff snuffle* 102 | 103 | if (settings.options.sniffHtml) { 104 | var message = ""; 105 | // --------------------------------------------------------- 106 | // PATTERN 107 | // --------------------------------------------------------- 108 | if ($this.attr("pattern") !== undefined) { 109 | message = "Not in the expected format"; 110 | if ($this.data("validationPatternMessage")) { 111 | message = $this.data("validationPatternMessage"); 112 | } 113 | $this.data("validationPatternMessage", message); 114 | $this.data("validationPatternRegex", $this.attr("pattern")); 115 | } 116 | // --------------------------------------------------------- 117 | // MAX 118 | // --------------------------------------------------------- 119 | if ($this.attr("max") !== undefined || $this.attr("aria-valuemax") !== undefined) { 120 | var max = ($this.attr("max") !== undefined ? $this.attr("max") : $this.attr("aria-valuemax")); 121 | message = "Too high: Maximum of '" + max + "'"; 122 | if ($this.data("validationMaxMessage")) { 123 | message = $this.data("validationMaxMessage"); 124 | } 125 | $this.data("validationMaxMessage", message); 126 | $this.data("validationMaxMax", max); 127 | } 128 | // --------------------------------------------------------- 129 | // MIN 130 | // --------------------------------------------------------- 131 | if ($this.attr("min") !== undefined || $this.attr("aria-valuemin") !== undefined) { 132 | var min = ($this.attr("min") !== undefined ? $this.attr("min") : $this.attr("aria-valuemin")); 133 | message = "Too low: Minimum of '" + min + "'"; 134 | if ($this.data("validationMinMessage")) { 135 | message = $this.data("validationMinMessage"); 136 | } 137 | $this.data("validationMinMessage", message); 138 | $this.data("validationMinMin", min); 139 | } 140 | // --------------------------------------------------------- 141 | // MAXLENGTH 142 | // --------------------------------------------------------- 143 | if ($this.attr("maxlength") !== undefined) { 144 | message = "Too long: Maximum of '" + $this.attr("maxlength") + "' characters"; 145 | if ($this.data("validationMaxlengthMessage")) { 146 | message = $this.data("validationMaxlengthMessage"); 147 | } 148 | $this.data("validationMaxlengthMessage", message); 149 | $this.data("validationMaxlengthMaxlength", $this.attr("maxlength")); 150 | } 151 | // --------------------------------------------------------- 152 | // MINLENGTH 153 | // --------------------------------------------------------- 154 | if ($this.attr("minlength") !== undefined) { 155 | message = "Too short: Minimum of '" + $this.attr("minlength") + "' characters"; 156 | if ($this.data("validationMinlengthMessage")) { 157 | message = $this.data("validationMinlengthMessage"); 158 | } 159 | $this.data("validationMinlengthMessage", message); 160 | $this.data("validationMinlengthMinlength", $this.attr("minlength")); 161 | } 162 | // --------------------------------------------------------- 163 | // REQUIRED 164 | // --------------------------------------------------------- 165 | if ($this.attr("required") !== undefined || $this.attr("aria-required") !== undefined) { 166 | message = settings.builtInValidators.required.message; 167 | if ($this.data("validationRequiredMessage")) { 168 | message = $this.data("validationRequiredMessage"); 169 | } 170 | $this.data("validationRequiredMessage", message); 171 | } 172 | // --------------------------------------------------------- 173 | // NUMBER 174 | // --------------------------------------------------------- 175 | if ($this.attr("type") !== undefined && $this.attr("type").toLowerCase() === "number") { 176 | message = settings.builtInValidators.number.message; 177 | if ($this.data("validationNumberMessage")) { 178 | message = $this.data("validationNumberMessage"); 179 | } 180 | $this.data("validationNumberMessage", message); 181 | } 182 | // --------------------------------------------------------- 183 | // EMAIL 184 | // --------------------------------------------------------- 185 | if ($this.attr("type") !== undefined && $this.attr("type").toLowerCase() === "email") { 186 | message = "Not a valid email address"; 187 | if ($this.data("validationValidemailMessage")) { 188 | message = $this.data("validationValidemailMessage"); 189 | } else if ($this.data("validationEmailMessage")) { 190 | message = $this.data("validationEmailMessage"); 191 | } 192 | $this.data("validationValidemailMessage", message); 193 | } 194 | // --------------------------------------------------------- 195 | // MINCHECKED 196 | // --------------------------------------------------------- 197 | if ($this.attr("minchecked") !== undefined) { 198 | message = "Not enough options checked; Minimum of '" + $this.attr("minchecked") + "' required"; 199 | if ($this.data("validationMincheckedMessage")) { 200 | message = $this.data("validationMincheckedMessage"); 201 | } 202 | $this.data("validationMincheckedMessage", message); 203 | $this.data("validationMincheckedMinchecked", $this.attr("minchecked")); 204 | } 205 | // --------------------------------------------------------- 206 | // MAXCHECKED 207 | // --------------------------------------------------------- 208 | if ($this.attr("maxchecked") !== undefined) { 209 | message = "Too many options checked; Maximum of '" + $this.attr("maxchecked") + "' required"; 210 | if ($this.data("validationMaxcheckedMessage")) { 211 | message = $this.data("validationMaxcheckedMessage"); 212 | } 213 | $this.data("validationMaxcheckedMessage", message); 214 | $this.data("validationMaxcheckedMaxchecked", $this.attr("maxchecked")); 215 | } 216 | } 217 | 218 | // ============================================================= 219 | // COLLECT VALIDATOR NAMES 220 | // ============================================================= 221 | 222 | // Get named validators 223 | if ($this.data("validation") !== undefined) { 224 | validatorNames = $this.data("validation").split(","); 225 | } 226 | 227 | // Get extra ones defined on the element's data attributes 228 | $.each($this.data(), function(i, el) { 229 | var parts = i.replace(/([A-Z])/g, ",$1").split(","); 230 | if (parts[0] === "validation" && parts[1]) { 231 | validatorNames.push(parts[1]); 232 | } 233 | }); 234 | 235 | // ============================================================= 236 | // NORMALISE VALIDATOR NAMES 237 | // ============================================================= 238 | 239 | var validatorNamesToInspect = validatorNames; 240 | var newValidatorNamesToInspect = []; 241 | 242 | do // repeatedly expand 'shortcut' validators into their real validators 243 | { 244 | // Uppercase only the first letter of each name 245 | $.each(validatorNames, function(i, el) { 246 | validatorNames[i] = formatValidatorName(el); 247 | }); 248 | 249 | // Remove duplicate validator names 250 | validatorNames = $.unique(validatorNames); 251 | 252 | // Pull out the new validator names from each shortcut 253 | newValidatorNamesToInspect = []; 254 | $.each(validatorNamesToInspect, function(i, el) { 255 | if ($this.data("validation" + el + "Shortcut") !== undefined) { 256 | // Are these custom validators? 257 | // Pull them out! 258 | $.each($this.data("validation" + el + "Shortcut").split(","), function(i2, el2) { 259 | newValidatorNamesToInspect.push(el2); 260 | }); 261 | } else if (settings.builtInValidators[el.toLowerCase()]) { 262 | // Is this a recognised built-in? 263 | // Pull it out! 264 | var validator = settings.builtInValidators[el.toLowerCase()]; 265 | if (validator.type.toLowerCase() === "shortcut") { 266 | $.each(validator.shortcut.split(","), function(i, el) { 267 | el = formatValidatorName(el); 268 | newValidatorNamesToInspect.push(el); 269 | validatorNames.push(el); 270 | }); 271 | } 272 | } 273 | }); 274 | 275 | validatorNamesToInspect = newValidatorNamesToInspect; 276 | 277 | } while (validatorNamesToInspect.length > 0) 278 | 279 | // ============================================================= 280 | // SET UP VALIDATOR ARRAYS 281 | // ============================================================= 282 | 283 | var validators = {}; 284 | 285 | $.each(validatorNames, function(i, el) { 286 | // Set up the 'override' message 287 | var message = $this.data("validation" + el + "Message"); 288 | var hasOverrideMessage = (message !== undefined); 289 | var foundValidator = false; 290 | message = 291 | ( 292 | message ? 293 | message : 294 | "'" + el + "' validation failed " 295 | ); 296 | 297 | $.each( 298 | settings.validatorTypes, 299 | function(validatorType, validatorTemplate) { 300 | if (validators[validatorType] === undefined) { 301 | validators[validatorType] = []; 302 | } 303 | if (!foundValidator && $this.data("validation" + el + formatValidatorName(validatorTemplate.name)) !== undefined) { 304 | validators[validatorType].push( 305 | $.extend( 306 | true, { 307 | name: formatValidatorName(validatorTemplate.name), 308 | message: message 309 | }, 310 | validatorTemplate.init($this, el) 311 | ) 312 | ); 313 | foundValidator = true; 314 | } 315 | } 316 | ); 317 | 318 | if (!foundValidator && settings.builtInValidators[el.toLowerCase()]) { 319 | 320 | var validator = $.extend(true, {}, settings.builtInValidators[el.toLowerCase()]); 321 | if (hasOverrideMessage) { 322 | validator.message = message; 323 | } 324 | var validatorType = validator.type.toLowerCase(); 325 | 326 | if (validatorType === "shortcut") { 327 | foundValidator = true; 328 | } else { 329 | $.each( 330 | settings.validatorTypes, 331 | function(validatorTemplateType, validatorTemplate) { 332 | if (validators[validatorTemplateType] === undefined) { 333 | validators[validatorTemplateType] = []; 334 | } 335 | if (!foundValidator && validatorType === validatorTemplateType.toLowerCase()) { 336 | $this.data("validation" + el + formatValidatorName(validatorTemplate.name), validator[validatorTemplate.name.toLowerCase()]); 337 | validators[validatorType].push( 338 | $.extend( 339 | validator, 340 | validatorTemplate.init($this, el) 341 | ) 342 | ); 343 | foundValidator = true; 344 | } 345 | } 346 | ); 347 | } 348 | } 349 | 350 | if (!foundValidator) { 351 | $.error("Cannot find validation info for '" + el + "'"); 352 | } 353 | }); 354 | 355 | // ============================================================= 356 | // STORE FALLBACK VALUES 357 | // ============================================================= 358 | 359 | $helpBlock.data( 360 | "original-contents", 361 | ( 362 | $helpBlock.data("original-contents") ? 363 | $helpBlock.data("original-contents") : 364 | $helpBlock.html() 365 | ) 366 | ); 367 | 368 | $helpBlock.data( 369 | "original-role", 370 | ( 371 | $helpBlock.data("original-role") ? 372 | $helpBlock.data("original-role") : 373 | $helpBlock.attr("role") 374 | ) 375 | ); 376 | 377 | $controlGroup.data( 378 | "original-classes", 379 | ( 380 | $controlGroup.data("original-clases") ? 381 | $controlGroup.data("original-classes") : 382 | $controlGroup.attr("class") 383 | ) 384 | ); 385 | 386 | $this.data( 387 | "original-aria-invalid", 388 | ( 389 | $this.data("original-aria-invalid") ? 390 | $this.data("original-aria-invalid") : 391 | $this.attr("aria-invalid") 392 | ) 393 | ); 394 | 395 | // ============================================================= 396 | // VALIDATION 397 | // ============================================================= 398 | 399 | $this.bind( 400 | "validation.validation", 401 | function(event, params) { 402 | 403 | var value = getValue($this); 404 | 405 | // Get a list of the errors to apply 406 | var errorsFound = []; 407 | 408 | $.each(validators, function(validatorType, validatorTypeArray) { 409 | if (value || value.length || (params && params.includeEmpty) || (!!settings.validatorTypes[validatorType].blockSubmit && params && !!params.submitting)) { 410 | $.each(validatorTypeArray, function(i, validator) { 411 | if (settings.validatorTypes[validatorType].validate($this, value, validator)) { 412 | errorsFound.push(validator.message); 413 | } 414 | }); 415 | } 416 | }); 417 | 418 | return errorsFound; 419 | } 420 | ); 421 | 422 | $this.bind( 423 | "getValidators.validation", 424 | function() { 425 | return validators; 426 | } 427 | ); 428 | 429 | // ============================================================= 430 | // WATCH FOR CHANGES 431 | // ============================================================= 432 | $this.bind( 433 | "submit.validation", 434 | function() { 435 | return $this.triggerHandler("change.validation", { 436 | submitting: true 437 | }); 438 | } 439 | ); 440 | $this.bind( 441 | [ 442 | "keyup", 443 | "focus", 444 | "blur", 445 | "click", 446 | "keydown", 447 | "keypress", 448 | "change" 449 | ].join(".validation ") + ".validation", 450 | function(e, params) { 451 | 452 | var value = getValue($this); 453 | 454 | var errorsFound = []; 455 | 456 | $controlGroup.find("input,textarea,select").each(function(i, el) { 457 | var oldCount = errorsFound.length; 458 | $.each($(el).triggerHandler("validation.validation", params), function(j, message) { 459 | errorsFound.push(message); 460 | }); 461 | if (errorsFound.length > oldCount) { 462 | $(el).attr("aria-invalid", "true"); 463 | } else { 464 | var original = $this.data("original-aria-invalid"); 465 | $(el).attr("aria-invalid", (original !== undefined ? original : false)); 466 | } 467 | }); 468 | 469 | $form.find("input,select,textarea").not($this).not("[name=\"" + $this.attr("name") + "\"]").trigger("validationLostFocus.validation"); 470 | 471 | errorsFound = $.unique(errorsFound.sort()); 472 | 473 | // Were there any errors? 474 | if (errorsFound.length) { 475 | // Better flag it up as a warning. 476 | $controlGroup.removeClass("success error").addClass("warning"); 477 | 478 | // How many errors did we find? 479 | if (settings.options.semanticallyStrict && errorsFound.length === 1) { 480 | // Only one? Being strict? Just output it. 481 | $helpBlock.html(errorsFound[0] + 482 | (settings.options.prependExistingHelpBlock ? $helpBlock.data("original-contents") : "")); 483 | } else { 484 | // Multiple? Being sloppy? Glue them together into an UL. 485 | $helpBlock.html("
  • " + errorsFound.join("
  • ") + "
" + 486 | (settings.options.prependExistingHelpBlock ? $helpBlock.data("original-contents") : "")); 487 | } 488 | } else { 489 | $controlGroup.removeClass("warning error success"); 490 | if (value.length > 0) { 491 | $controlGroup.addClass("success"); 492 | } 493 | $helpBlock.html($helpBlock.data("original-contents")); 494 | } 495 | 496 | if (e.type === "blur") { 497 | $controlGroup.removeClass("success"); 498 | } 499 | } 500 | ); 501 | $this.bind("validationLostFocus.validation", function() { 502 | $controlGroup.removeClass("success"); 503 | }); 504 | }); 505 | }, 506 | destroy: function() { 507 | 508 | return this.each( 509 | function() { 510 | 511 | var 512 | $this = $(this), 513 | $controlGroup = $this.parents(".form-group").first(), 514 | $helpBlock = $controlGroup.find(".help-block").first(); 515 | 516 | // remove our events 517 | $this.unbind('.validation'); // events are namespaced. 518 | // reset help text 519 | $helpBlock.html($helpBlock.data("original-contents")); 520 | // reset classes 521 | $controlGroup.attr("class", $controlGroup.data("original-classes")); 522 | // reset aria 523 | $this.attr("aria-invalid", $this.data("original-aria-invalid")); 524 | // reset role 525 | $helpBlock.attr("role", $this.data("original-role")); 526 | // remove all elements we created 527 | if (createdElements.indexOf($helpBlock[0]) > -1) { 528 | $helpBlock.remove(); 529 | } 530 | 531 | } 532 | ); 533 | 534 | }, 535 | collectErrors: function(includeEmpty) { 536 | 537 | var errorMessages = {}; 538 | this.each(function(i, el) { 539 | var $el = $(el); 540 | var name = $el.attr("name"); 541 | var errors = $el.triggerHandler("validation.validation", { 542 | includeEmpty: true 543 | }); 544 | errorMessages[name] = $.extend(true, errors, errorMessages[name]); 545 | }); 546 | 547 | $.each(errorMessages, function(i, el) { 548 | if (el.length === 0) { 549 | delete errorMessages[i]; 550 | } 551 | }); 552 | 553 | return errorMessages; 554 | 555 | }, 556 | hasErrors: function() { 557 | 558 | var errorMessages = []; 559 | 560 | this.each(function(i, el) { 561 | errorMessages = errorMessages.concat( 562 | $(el).triggerHandler("getValidators.validation") ? $(el).triggerHandler("validation.validation", { 563 | submitting: true 564 | }) : [] 565 | ); 566 | }); 567 | 568 | return (errorMessages.length > 0); 569 | }, 570 | override: function(newDefaults) { 571 | defaults = $.extend(true, defaults, newDefaults); 572 | } 573 | }, 574 | validatorTypes: { 575 | callback: { 576 | name: "callback", 577 | init: function($this, name) { 578 | return { 579 | validatorName: name, 580 | callback: $this.data("validation" + name + "Callback"), 581 | lastValue: $this.val(), 582 | lastValid: true, 583 | lastFinished: true 584 | }; 585 | }, 586 | validate: function($this, value, validator) { 587 | if (validator.lastValue === value && validator.lastFinished) { 588 | return !validator.lastValid; 589 | } 590 | 591 | if (validator.lastFinished === true) { 592 | validator.lastValue = value; 593 | validator.lastValid = true; 594 | validator.lastFinished = false; 595 | 596 | var rrjqbvValidator = validator; 597 | var rrjqbvThis = $this; 598 | executeFunctionByName( 599 | validator.callback, 600 | window, 601 | $this, 602 | value, 603 | function(data) { 604 | if (rrjqbvValidator.lastValue === data.value) { 605 | rrjqbvValidator.lastValid = data.valid; 606 | if (data.message) { 607 | rrjqbvValidator.message = data.message; 608 | } 609 | rrjqbvValidator.lastFinished = true; 610 | rrjqbvThis.data("validation" + rrjqbvValidator.validatorName + "Message", rrjqbvValidator.message); 611 | // Timeout is set to avoid problems with the events being considered 'already fired' 612 | setTimeout(function() { 613 | rrjqbvThis.trigger("change.validation"); 614 | }, 1); // doesn't need a long timeout, just long enough for the event bubble to burst 615 | } 616 | } 617 | ); 618 | } 619 | 620 | return false; 621 | 622 | } 623 | }, 624 | ajax: { 625 | name: "ajax", 626 | init: function($this, name) { 627 | return { 628 | validatorName: name, 629 | url: $this.data("validation" + name + "Ajax"), 630 | lastValue: $this.val(), 631 | lastValid: true, 632 | lastFinished: true 633 | }; 634 | }, 635 | validate: function($this, value, validator) { 636 | if ("" + validator.lastValue === "" + value && validator.lastFinished === true) { 637 | return validator.lastValid === false; 638 | } 639 | 640 | if (validator.lastFinished === true) { 641 | validator.lastValue = value; 642 | validator.lastValid = true; 643 | validator.lastFinished = false; 644 | $.ajax({ 645 | url: validator.url, 646 | data: "value=" + value + "&field=" + $this.attr("name"), 647 | dataType: "json", 648 | success: function(data) { 649 | if ("" + validator.lastValue === "" + data.value) { 650 | validator.lastValid = !!(data.valid); 651 | if (data.message) { 652 | validator.message = data.message; 653 | } 654 | validator.lastFinished = true; 655 | $this.data("validation" + validator.validatorName + "Message", validator.message); 656 | // Timeout is set to avoid problems with the events being considered 'already fired' 657 | setTimeout(function() { 658 | $this.trigger("change.validation"); 659 | }, 1); // doesn't need a long timeout, just long enough for the event bubble to burst 660 | } 661 | }, 662 | failure: function() { 663 | validator.lastValid = true; 664 | validator.message = "ajax call failed"; 665 | validator.lastFinished = true; 666 | $this.data("validation" + validator.validatorName + "Message", validator.message); 667 | // Timeout is set to avoid problems with the events being considered 'already fired' 668 | setTimeout(function() { 669 | $this.trigger("change.validation"); 670 | }, 1); // doesn't need a long timeout, just long enough for the event bubble to burst 671 | } 672 | }); 673 | } 674 | 675 | return false; 676 | 677 | } 678 | }, 679 | regex: { 680 | name: "regex", 681 | init: function($this, name) { 682 | return { 683 | regex: regexFromString($this.data("validation" + name + "Regex")) 684 | }; 685 | }, 686 | validate: function($this, value, validator) { 687 | return (!validator.regex.test(value) && !validator.negative) || 688 | (validator.regex.test(value) && validator.negative); 689 | } 690 | }, 691 | required: { 692 | name: "required", 693 | init: function($this, name) { 694 | return {}; 695 | }, 696 | validate: function($this, value, validator) { 697 | return !!(value.length === 0 && !validator.negative) || 698 | !!(value.length > 0 && validator.negative); 699 | }, 700 | blockSubmit: true 701 | }, 702 | match: { 703 | name: "match", 704 | init: function($this, name) { 705 | var element = $this.parents("form").first().find("[name=\"" + $this.data("validation" + name + "Match") + "\"]").first(); 706 | element.bind("validation.validation", function() { 707 | $this.trigger("change.validation", { 708 | submitting: true 709 | }); 710 | }); 711 | return { 712 | "element": element 713 | }; 714 | }, 715 | validate: function($this, value, validator) { 716 | return (value !== validator.element.val() && !validator.negative) || 717 | (value === validator.element.val() && validator.negative); 718 | }, 719 | blockSubmit: true 720 | }, 721 | max: { 722 | name: "max", 723 | init: function($this, name) { 724 | return { 725 | max: $this.data("validation" + name + "Max") 726 | }; 727 | }, 728 | validate: function($this, value, validator) { 729 | return (parseFloat(value, 10) > parseFloat(validator.max, 10) && !validator.negative) || 730 | (parseFloat(value, 10) <= parseFloat(validator.max, 10) && validator.negative); 731 | } 732 | }, 733 | min: { 734 | name: "min", 735 | init: function($this, name) { 736 | return { 737 | min: $this.data("validation" + name + "Min") 738 | }; 739 | }, 740 | validate: function($this, value, validator) { 741 | return (parseFloat(value) < parseFloat(validator.min) && !validator.negative) || 742 | (parseFloat(value) >= parseFloat(validator.min) && validator.negative); 743 | } 744 | }, 745 | maxlength: { 746 | name: "maxlength", 747 | init: function($this, name) { 748 | return { 749 | maxlength: $this.data("validation" + name + "Maxlength") 750 | }; 751 | }, 752 | validate: function($this, value, validator) { 753 | return ((value.length > validator.maxlength) && !validator.negative) || 754 | ((value.length <= validator.maxlength) && validator.negative); 755 | } 756 | }, 757 | minlength: { 758 | name: "minlength", 759 | init: function($this, name) { 760 | return { 761 | minlength: $this.data("validation" + name + "Minlength") 762 | }; 763 | }, 764 | validate: function($this, value, validator) { 765 | return ((value.length < validator.minlength) && !validator.negative) || 766 | ((value.length >= validator.minlength) && validator.negative); 767 | } 768 | }, 769 | maxchecked: { 770 | name: "maxchecked", 771 | init: function($this, name) { 772 | var elements = $this.parents("form").first().find("[name=\"" + $this.attr("name") + "\"]"); 773 | elements.bind("click.validation", function() { 774 | $this.trigger("change.validation", { 775 | includeEmpty: true 776 | }); 777 | }); 778 | return { 779 | maxchecked: $this.data("validation" + name + "Maxchecked"), 780 | elements: elements 781 | }; 782 | }, 783 | validate: function($this, value, validator) { 784 | return (validator.elements.filter(":checked").length > validator.maxchecked && !validator.negative) || 785 | (validator.elements.filter(":checked").length <= validator.maxchecked && validator.negative); 786 | }, 787 | blockSubmit: true 788 | }, 789 | minchecked: { 790 | name: "minchecked", 791 | init: function($this, name) { 792 | var elements = $this.parents("form").first().find("[name=\"" + $this.attr("name") + "\"]"); 793 | elements.bind("click.validation", function() { 794 | $this.trigger("change.validation", { 795 | includeEmpty: true 796 | }); 797 | }); 798 | return { 799 | minchecked: $this.data("validation" + name + "Minchecked"), 800 | elements: elements 801 | }; 802 | }, 803 | validate: function($this, value, validator) { 804 | return (validator.elements.filter(":checked").length < validator.minchecked && !validator.negative) || 805 | (validator.elements.filter(":checked").length >= validator.minchecked && validator.negative); 806 | }, 807 | blockSubmit: true 808 | } 809 | }, 810 | builtInValidators: { 811 | email: { 812 | name: "Email", 813 | type: "shortcut", 814 | shortcut: "validemail" 815 | }, 816 | validemail: { 817 | name: "Validemail", 818 | type: "regex", 819 | regex: "[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\\.[A-Za-z]{2,4}", 820 | message: "Not a valid email address" 821 | }, 822 | passwordagain: { 823 | name: "Passwordagain", 824 | type: "match", 825 | match: "password", 826 | message: "Does not match the given password" 827 | }, 828 | positive: { 829 | name: "Positive", 830 | type: "shortcut", 831 | shortcut: "number,positivenumber" 832 | }, 833 | negative: { 834 | name: "Negative", 835 | type: "shortcut", 836 | shortcut: "number,negativenumber" 837 | }, 838 | number: { 839 | name: "Number", 840 | type: "regex", 841 | regex: "([+-]?\\\d+(\\\.\\\d*)?([eE][+-]?[0-9]+)?)?", 842 | message: "Must be a number" 843 | }, 844 | integer: { 845 | name: "Integer", 846 | type: "regex", 847 | regex: "[+-]?\\\d+", 848 | message: "No decimal places allowed" 849 | }, 850 | positivenumber: { 851 | name: "Positivenumber", 852 | type: "min", 853 | min: 0, 854 | message: "Must be a positive number" 855 | }, 856 | negativenumber: { 857 | name: "Negativenumber", 858 | type: "max", 859 | max: 0, 860 | message: "Must be a negative number" 861 | }, 862 | required: { 863 | name: "Required", 864 | type: "required", 865 | message: "This is required" 866 | }, 867 | checkone: { 868 | name: "Checkone", 869 | type: "minchecked", 870 | minchecked: 1, 871 | message: "Check at least one option" 872 | } 873 | } 874 | }; 875 | 876 | var formatValidatorName = function(name) { 877 | return name 878 | .toLowerCase() 879 | .replace( 880 | /(^|\s)([a-z])/g, 881 | function(m, p1, p2) { 882 | return p1 + p2.toUpperCase(); 883 | } 884 | ); 885 | }; 886 | 887 | var getValue = function($this) { 888 | // Extract the value we're talking about 889 | var value = $this.val(); 890 | var type = $this.attr("type"); 891 | if (type === "checkbox") { 892 | value = ($this.is(":checked") ? value : ""); 893 | } 894 | if (type === "radio") { 895 | value = ($('input[name="' + $this.attr("name") + '"]:checked').length > 0 ? value : ""); 896 | } 897 | return value; 898 | }; 899 | 900 | function regexFromString(inputstring) { 901 | return new RegExp("^" + inputstring + "$"); 902 | } 903 | 904 | /** 905 | * Thanks to Jason Bunting via StackOverflow.com 906 | * 907 | * http://stackoverflow.com/questions/359788/how-to-execute-a-javascript-function-when-i-have-its-name-as-a-string#answer-359910 908 | * Short link: http://tinyurl.com/executeFunctionByName 909 | **/ 910 | function executeFunctionByName(functionName, context /*, args*/ ) { 911 | var args = Array.prototype.slice.call(arguments).splice(2); 912 | var namespaces = functionName.split("."); 913 | var func = namespaces.pop(); 914 | for (var i = 0; i < namespaces.length; i++) { 915 | context = context[namespaces[i]]; 916 | } 917 | return context[func].apply(this, args); 918 | } 919 | 920 | $.fn.jqBootstrapValidation = function(method) { 921 | 922 | if (defaults.methods[method]) { 923 | return defaults.methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); 924 | } else if (typeof method === 'object' || !method) { 925 | return defaults.methods.init.apply(this, arguments); 926 | } else { 927 | $.error('Method ' + method + ' does not exist on jQuery.jqBootstrapValidation'); 928 | return null; 929 | } 930 | 931 | }; 932 | 933 | $.jqBootstrapValidation = function(options) { 934 | $(":input").not("[type=image],[type=submit]").jqBootstrapValidation.apply(this, arguments); 935 | }; 936 | 937 | })(jQuery); 938 | -------------------------------------------------------------------------------- /static/scss/_bootstrap-overrides.scss: -------------------------------------------------------------------------------- 1 | // Bootstrap overrides for this template 2 | .btn { 3 | font-size: 14px; 4 | font-weight: 800; 5 | padding: 15px 25px; 6 | letter-spacing: 1px; 7 | text-transform: uppercase; 8 | border-radius: 0; 9 | @include sans-serif-font; 10 | } 11 | 12 | .btn-primary { 13 | background-color: $primary; 14 | border-color: $primary; 15 | &:hover, 16 | &:focus, 17 | &:active { 18 | color: $white; 19 | background-color: darken($primary, 7.5) !important; 20 | border-color: darken($primary, 7.5) !important; 21 | } 22 | } 23 | 24 | .btn-lg { 25 | font-size: 16px; 26 | padding: 25px 35px; 27 | } 28 | -------------------------------------------------------------------------------- /static/scss/_contact.scss: -------------------------------------------------------------------------------- 1 | // Styling for the contact page 2 | .floating-label-form-group { 3 | font-size: 14px; 4 | position: relative; 5 | margin-bottom: 0; 6 | padding-bottom: 0.5em; 7 | border-bottom: 1px solid $gray-300; 8 | input, 9 | textarea { 10 | font-size: 1.5em; 11 | position: relative; 12 | z-index: 1; 13 | padding: 0; 14 | resize: none; 15 | border: none; 16 | border-radius: 0; 17 | background: none; 18 | box-shadow: none !important; 19 | @include serif-font; 20 | &::-webkit-input-placeholder { 21 | color: $gray-600; 22 | @include serif-font; 23 | } 24 | } 25 | label { 26 | font-size: 0.85em; 27 | line-height: 1.764705882em; 28 | position: relative; 29 | z-index: 0; 30 | top: 2em; 31 | display: block; 32 | margin: 0; 33 | -webkit-transition: top 0.3s ease, opacity 0.3s ease; 34 | -moz-transition: top 0.3s ease, opacity 0.3s ease; 35 | -ms-transition: top 0.3s ease, opacity 0.3s ease; 36 | transition: top 0.3s ease, opacity 0.3s ease; 37 | opacity: 0; 38 | } 39 | .help-block { 40 | margin: 15px 0; 41 | } 42 | } 43 | 44 | .floating-label-form-group-with-value { 45 | label { 46 | top: 0; 47 | opacity: 1; 48 | } 49 | } 50 | 51 | .floating-label-form-group-with-focus { 52 | label { 53 | color: $primary; 54 | } 55 | } 56 | form .form-group:first-child .floating-label-form-group { 57 | border-top: 1px solid $gray-300; 58 | } 59 | -------------------------------------------------------------------------------- /static/scss/_footer.scss: -------------------------------------------------------------------------------- 1 | // Styling for the footer 2 | footer { 3 | padding: 50px 0 65px; 4 | .list-inline { 5 | margin: 0; 6 | padding: 0; 7 | } 8 | .copyright { 9 | font-size: 14px; 10 | margin-bottom: 0; 11 | text-align: center; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /static/scss/_global.scss: -------------------------------------------------------------------------------- 1 | // Global styling for this template 2 | body { 3 | font-size: 20px; 4 | color: $gray-900; 5 | @include serif-font; 6 | } 7 | 8 | p { 9 | line-height: 1.5; 10 | margin: 30px 0; 11 | a { 12 | text-decoration: underline; 13 | } 14 | } 15 | 16 | h1, 17 | h2, 18 | h3, 19 | h4, 20 | h5, 21 | h6 { 22 | font-weight: 800; 23 | @include sans-serif-font; 24 | } 25 | 26 | a { 27 | color: $gray-900; 28 | @include transition-all; 29 | &:focus, 30 | &:hover { 31 | color: $primary; 32 | } 33 | } 34 | 35 | blockquote { 36 | font-style: italic; 37 | color: $gray-600; 38 | } 39 | 40 | .section-heading { 41 | font-size: 36px; 42 | font-weight: 700; 43 | margin-top: 60px; 44 | } 45 | 46 | .caption { 47 | font-size: 14px; 48 | font-style: italic; 49 | display: block; 50 | margin: 0; 51 | padding: 10px; 52 | text-align: center; 53 | border-bottom-right-radius: 5px; 54 | border-bottom-left-radius: 5px; 55 | } 56 | 57 | ::-moz-selection { 58 | color: $white; 59 | background: $primary; 60 | text-shadow: none; 61 | } 62 | 63 | ::selection { 64 | color: $white; 65 | background: $primary; 66 | text-shadow: none; 67 | } 68 | 69 | img::selection { 70 | color: $white; 71 | background: transparent; 72 | } 73 | 74 | img::-moz-selection { 75 | color: $white; 76 | background: transparent; 77 | } 78 | -------------------------------------------------------------------------------- /static/scss/_masthead.scss: -------------------------------------------------------------------------------- 1 | // Styling for the masthead 2 | header.masthead { 3 | // TIP: Background images are set within the HTML using inline CSS! 4 | margin-bottom: 50px; 5 | background: no-repeat center center; 6 | background-color: $gray-600; 7 | background-attachment: scroll; 8 | position: relative; 9 | @include background-cover; 10 | .overlay { 11 | position: absolute; 12 | top: 0; 13 | left: 0; 14 | height: 100%; 15 | width: 100%; 16 | background-color: $gray-900; 17 | opacity: 0.5; 18 | } 19 | .page-heading, 20 | .post-heading, 21 | .site-heading { 22 | padding: 200px 0 150px; 23 | color: white; 24 | @media only screen and (min-width: 768px) { 25 | padding: 200px 0; 26 | } 27 | } 28 | .page-heading, 29 | .site-heading { 30 | text-align: center; 31 | h1 { 32 | font-size: 50px; 33 | margin-top: 0; 34 | } 35 | .subheading { 36 | font-size: 24px; 37 | font-weight: 300; 38 | line-height: 1.1; 39 | display: block; 40 | margin: 10px 0 0; 41 | @include sans-serif-font; 42 | } 43 | @media only screen and (min-width: 768px) { 44 | h1 { 45 | font-size: 80px; 46 | } 47 | } 48 | } 49 | .post-heading { 50 | h1 { 51 | font-size: 35px; 52 | } 53 | .meta, 54 | .subheading { 55 | line-height: 1.1; 56 | display: block; 57 | } 58 | .subheading { 59 | font-size: 24px; 60 | font-weight: 600; 61 | margin: 10px 0 30px; 62 | @include sans-serif-font; 63 | } 64 | .meta { 65 | font-size: 20px; 66 | font-weight: 300; 67 | font-style: italic; 68 | @include serif-font; 69 | a { 70 | color: $white; 71 | } 72 | } 73 | @media only screen and (min-width: 768px) { 74 | h1 { 75 | font-size: 55px; 76 | } 77 | .subheading { 78 | font-size: 30px; 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /static/scss/_mixins.scss: -------------------------------------------------------------------------------- 1 | // Mixins 2 | // Bootstrap Button Variant 3 | @mixin button-variant($color, $background, $border) { 4 | color: $color; 5 | border-color: $border; 6 | background-color: $background; 7 | &.focus, 8 | &:focus { 9 | color: $color; 10 | border-color: darken($border, 25%); 11 | background-color: darken($background, 10%); 12 | } 13 | &:hover { 14 | color: $color; 15 | border-color: darken($border, 12%); 16 | background-color: darken($background, 10%); 17 | } 18 | &.active, 19 | &:active, 20 | .open > &.dropdown-toggle { 21 | color: $color; 22 | border-color: darken($border, 12%); 23 | background-color: darken($background, 10%); 24 | &.focus, 25 | &:focus, 26 | &:hover { 27 | color: $color; 28 | border-color: darken($border, 25%); 29 | background-color: darken($background, 17%); 30 | } 31 | } 32 | &.active, 33 | &:active, 34 | .open > &.dropdown-toggle { 35 | background-image: none; 36 | } 37 | &.disabled, 38 | &[disabled], 39 | fieldset[disabled] & { 40 | &.focus, 41 | &:focus, 42 | &:hover { 43 | border-color: $border; 44 | background-color: $background; 45 | } 46 | } 47 | .badge { 48 | color: $background; 49 | background-color: $color; 50 | } 51 | } 52 | @mixin transition-all() { 53 | -webkit-transition: all 0.2s; 54 | -moz-transition: all 0.2s; 55 | transition: all 0.2s; 56 | } 57 | @mixin background-cover() { 58 | -webkit-background-size: cover; 59 | -moz-background-size: cover; 60 | -o-background-size: cover; 61 | background-size: cover; 62 | } 63 | @mixin serif-font() { 64 | font-family: 'Lora', 'Times New Roman', serif; 65 | } 66 | @mixin sans-serif-font() { 67 | font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; 68 | } 69 | -------------------------------------------------------------------------------- /static/scss/_navbar.scss: -------------------------------------------------------------------------------- 1 | // Styling for the navbar 2 | #mainNav { 3 | position: absolute; 4 | border-bottom: 1px solid $gray-200; 5 | background-color: white; 6 | @include sans-serif-font; 7 | .navbar-brand { 8 | font-weight: 800; 9 | color: $gray-800; 10 | } 11 | .navbar-toggler { 12 | font-size: 12px; 13 | font-weight: 800; 14 | padding: 13px; 15 | text-transform: uppercase; 16 | color: $gray-800; 17 | } 18 | .navbar-nav { 19 | > li.nav-item { 20 | > a { 21 | font-size: 12px; 22 | font-weight: 800; 23 | letter-spacing: 1px; 24 | text-transform: uppercase; 25 | } 26 | } 27 | } 28 | @media only screen and (min-width: 992px) { 29 | border-bottom: 1px solid transparent; 30 | background: transparent; 31 | .navbar-brand { 32 | padding: 10px 20px; 33 | color: $white; 34 | &:focus, 35 | &:hover { 36 | color: fade-out($white, .2); 37 | } 38 | } 39 | .navbar-nav { 40 | > li.nav-item { 41 | > a { 42 | padding: 10px 20px; 43 | color: $white; 44 | &:focus, 45 | &:hover { 46 | color: fade-out($white, .2); 47 | } 48 | } 49 | } 50 | } 51 | } 52 | @media only screen and (min-width: 992px) { 53 | -webkit-transition: background-color 0.2s; 54 | -moz-transition: background-color 0.2s; 55 | transition: background-color 0.2s; 56 | /* Force Hardware Acceleration in WebKit */ 57 | -webkit-transform: translate3d(0, 0, 0); 58 | -moz-transform: translate3d(0, 0, 0); 59 | -ms-transform: translate3d(0, 0, 0); 60 | -o-transform: translate3d(0, 0, 0); 61 | transform: translate3d(0, 0, 0); 62 | -webkit-backface-visibility: hidden; 63 | &.is-fixed { 64 | /* when the user scrolls down, we hide the header right above the viewport */ 65 | position: fixed; 66 | top: -67px; 67 | -webkit-transition: -webkit-transform 0.2s; 68 | -moz-transition: -moz-transform 0.2s; 69 | transition: transform 0.2s; 70 | border-bottom: 1px solid darken($white, .05); 71 | background-color: fade-out($white, .1); 72 | .navbar-brand { 73 | color: $gray-900; 74 | &:focus, 75 | &:hover { 76 | color: $primary; 77 | } 78 | } 79 | .navbar-nav { 80 | > li.nav-item { 81 | > a { 82 | color: $gray-900; 83 | &:focus, 84 | &:hover { 85 | color: $primary; 86 | } 87 | } 88 | } 89 | } 90 | } 91 | &.is-visible { 92 | /* if the user changes the scrolling direction, we show the header */ 93 | -webkit-transform: translate3d(0, 100%, 0); 94 | -moz-transform: translate3d(0, 100%, 0); 95 | -ms-transform: translate3d(0, 100%, 0); 96 | -o-transform: translate3d(0, 100%, 0); 97 | transform: translate3d(0, 100%, 0); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /static/scss/_post.scss: -------------------------------------------------------------------------------- 1 | // Styling for the post page 2 | .post-preview { 3 | > a { 4 | color: $gray-900; 5 | &:focus, 6 | &:hover { 7 | text-decoration: none; 8 | color: $primary; 9 | } 10 | > .post-title { 11 | font-size: 30px; 12 | margin-top: 30px; 13 | margin-bottom: 10px; 14 | } 15 | > .post-subtitle { 16 | font-weight: 300; 17 | margin: 0 0 10px; 18 | } 19 | } 20 | > .post-meta { 21 | font-size: 18px; 22 | font-style: italic; 23 | margin-top: 0; 24 | color: $gray-600; 25 | > a { 26 | text-decoration: none; 27 | color: $gray-900; 28 | &:focus, 29 | &:hover { 30 | text-decoration: underline; 31 | color: $primary; 32 | } 33 | } 34 | } 35 | @media only screen and (min-width: 768px) { 36 | > a { 37 | > .post-title { 38 | font-size: 36px; 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /static/scss/_variables.scss: -------------------------------------------------------------------------------- 1 | // Variables 2 | 3 | $white: #fff !default; 4 | $gray-100: #f8f9fa !default; 5 | $gray-200: #e9ecef !default; 6 | $gray-300: #dee2e6 !default; 7 | $gray-400: #ced4da !default; 8 | $gray-500: #adb5bd !default; 9 | $gray-600: #868e96 !default; 10 | $gray-700: #495057 !default; 11 | $gray-800: #343a40 !default; 12 | $gray-900: #212529 !default; 13 | $black: #000 !default; 14 | 15 | $blue: #007bff !default; 16 | $indigo: #6610f2 !default; 17 | $purple: #6f42c1 !default; 18 | $pink: #e83e8c !default; 19 | $red: #dc3545 !default; 20 | $orange: #fd7e14 !default; 21 | $yellow: #ffc107 !default; 22 | $green: #28a745 !default; 23 | $teal: #0085A1 !default; 24 | $cyan: #17a2b8 !default; 25 | 26 | $primary: $teal !default; 27 | $secondary: $gray-600 !default; 28 | $success: $green !default; 29 | $info: $cyan !default; 30 | $warning: $yellow !default; 31 | $danger: $red !default; 32 | $light: $gray-100 !default; 33 | $dark: $gray-800 !default; 34 | -------------------------------------------------------------------------------- /static/scss/clean-blog.scss: -------------------------------------------------------------------------------- 1 | @import "variables.scss"; 2 | @import "mixins.scss"; 3 | @import "global.scss"; 4 | @import "navbar.scss"; 5 | @import "masthead.scss"; 6 | @import "post.scss"; 7 | @import "contact.scss"; 8 | @import "footer.scss"; 9 | @import "bootstrap-overrides.scss"; 10 | -------------------------------------------------------------------------------- /static/vendor/bootstrap/css/bootstrap-reboot.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v4.5.0 (https://getbootstrap.com/) 3 | * Copyright 2011-2020 The Bootstrap Authors 4 | * Copyright 2011-2020 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */ 8 | *, 9 | *::before, 10 | *::after { 11 | box-sizing: border-box; 12 | } 13 | 14 | html { 15 | font-family: sans-serif; 16 | line-height: 1.15; 17 | -webkit-text-size-adjust: 100%; 18 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 19 | } 20 | 21 | article, aside, figcaption, figure, footer, header, hgroup, main, nav, section { 22 | display: block; 23 | } 24 | 25 | body { 26 | margin: 0; 27 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 28 | font-size: 1rem; 29 | font-weight: 400; 30 | line-height: 1.5; 31 | color: #212529; 32 | text-align: left; 33 | background-color: #fff; 34 | } 35 | 36 | [tabindex="-1"]:focus:not(:focus-visible) { 37 | outline: 0 !important; 38 | } 39 | 40 | hr { 41 | box-sizing: content-box; 42 | height: 0; 43 | overflow: visible; 44 | } 45 | 46 | h1, h2, h3, h4, h5, h6 { 47 | margin-top: 0; 48 | margin-bottom: 0.5rem; 49 | } 50 | 51 | p { 52 | margin-top: 0; 53 | margin-bottom: 1rem; 54 | } 55 | 56 | abbr[title], 57 | abbr[data-original-title] { 58 | text-decoration: underline; 59 | -webkit-text-decoration: underline dotted; 60 | text-decoration: underline dotted; 61 | cursor: help; 62 | border-bottom: 0; 63 | -webkit-text-decoration-skip-ink: none; 64 | text-decoration-skip-ink: none; 65 | } 66 | 67 | address { 68 | margin-bottom: 1rem; 69 | font-style: normal; 70 | line-height: inherit; 71 | } 72 | 73 | ol, 74 | ul, 75 | dl { 76 | margin-top: 0; 77 | margin-bottom: 1rem; 78 | } 79 | 80 | ol ol, 81 | ul ul, 82 | ol ul, 83 | ul ol { 84 | margin-bottom: 0; 85 | } 86 | 87 | dt { 88 | font-weight: 700; 89 | } 90 | 91 | dd { 92 | margin-bottom: .5rem; 93 | margin-left: 0; 94 | } 95 | 96 | blockquote { 97 | margin: 0 0 1rem; 98 | } 99 | 100 | b, 101 | strong { 102 | font-weight: bolder; 103 | } 104 | 105 | small { 106 | font-size: 80%; 107 | } 108 | 109 | sub, 110 | sup { 111 | position: relative; 112 | font-size: 75%; 113 | line-height: 0; 114 | vertical-align: baseline; 115 | } 116 | 117 | sub { 118 | bottom: -.25em; 119 | } 120 | 121 | sup { 122 | top: -.5em; 123 | } 124 | 125 | a { 126 | color: #007bff; 127 | text-decoration: none; 128 | background-color: transparent; 129 | } 130 | 131 | a:hover { 132 | color: #0056b3; 133 | text-decoration: underline; 134 | } 135 | 136 | a:not([href]) { 137 | color: inherit; 138 | text-decoration: none; 139 | } 140 | 141 | a:not([href]):hover { 142 | color: inherit; 143 | text-decoration: none; 144 | } 145 | 146 | pre, 147 | code, 148 | kbd, 149 | samp { 150 | font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 151 | font-size: 1em; 152 | } 153 | 154 | pre { 155 | margin-top: 0; 156 | margin-bottom: 1rem; 157 | overflow: auto; 158 | -ms-overflow-style: scrollbar; 159 | } 160 | 161 | figure { 162 | margin: 0 0 1rem; 163 | } 164 | 165 | img { 166 | vertical-align: middle; 167 | border-style: none; 168 | } 169 | 170 | svg { 171 | overflow: hidden; 172 | vertical-align: middle; 173 | } 174 | 175 | table { 176 | border-collapse: collapse; 177 | } 178 | 179 | caption { 180 | padding-top: 0.75rem; 181 | padding-bottom: 0.75rem; 182 | color: #6c757d; 183 | text-align: left; 184 | caption-side: bottom; 185 | } 186 | 187 | th { 188 | text-align: inherit; 189 | } 190 | 191 | label { 192 | display: inline-block; 193 | margin-bottom: 0.5rem; 194 | } 195 | 196 | button { 197 | border-radius: 0; 198 | } 199 | 200 | button:focus { 201 | outline: 1px dotted; 202 | outline: 5px auto -webkit-focus-ring-color; 203 | } 204 | 205 | input, 206 | button, 207 | select, 208 | optgroup, 209 | textarea { 210 | margin: 0; 211 | font-family: inherit; 212 | font-size: inherit; 213 | line-height: inherit; 214 | } 215 | 216 | button, 217 | input { 218 | overflow: visible; 219 | } 220 | 221 | button, 222 | select { 223 | text-transform: none; 224 | } 225 | 226 | [role="button"] { 227 | cursor: pointer; 228 | } 229 | 230 | select { 231 | word-wrap: normal; 232 | } 233 | 234 | button, 235 | [type="button"], 236 | [type="reset"], 237 | [type="submit"] { 238 | -webkit-appearance: button; 239 | } 240 | 241 | button:not(:disabled), 242 | [type="button"]:not(:disabled), 243 | [type="reset"]:not(:disabled), 244 | [type="submit"]:not(:disabled) { 245 | cursor: pointer; 246 | } 247 | 248 | button::-moz-focus-inner, 249 | [type="button"]::-moz-focus-inner, 250 | [type="reset"]::-moz-focus-inner, 251 | [type="submit"]::-moz-focus-inner { 252 | padding: 0; 253 | border-style: none; 254 | } 255 | 256 | input[type="radio"], 257 | input[type="checkbox"] { 258 | box-sizing: border-box; 259 | padding: 0; 260 | } 261 | 262 | textarea { 263 | overflow: auto; 264 | resize: vertical; 265 | } 266 | 267 | fieldset { 268 | min-width: 0; 269 | padding: 0; 270 | margin: 0; 271 | border: 0; 272 | } 273 | 274 | legend { 275 | display: block; 276 | width: 100%; 277 | max-width: 100%; 278 | padding: 0; 279 | margin-bottom: .5rem; 280 | font-size: 1.5rem; 281 | line-height: inherit; 282 | color: inherit; 283 | white-space: normal; 284 | } 285 | 286 | progress { 287 | vertical-align: baseline; 288 | } 289 | 290 | [type="number"]::-webkit-inner-spin-button, 291 | [type="number"]::-webkit-outer-spin-button { 292 | height: auto; 293 | } 294 | 295 | [type="search"] { 296 | outline-offset: -2px; 297 | -webkit-appearance: none; 298 | } 299 | 300 | [type="search"]::-webkit-search-decoration { 301 | -webkit-appearance: none; 302 | } 303 | 304 | ::-webkit-file-upload-button { 305 | font: inherit; 306 | -webkit-appearance: button; 307 | } 308 | 309 | output { 310 | display: inline-block; 311 | } 312 | 313 | summary { 314 | display: list-item; 315 | cursor: pointer; 316 | } 317 | 318 | template { 319 | display: none; 320 | } 321 | 322 | [hidden] { 323 | display: none !important; 324 | } 325 | /*# sourceMappingURL=bootstrap-reboot.css.map */ -------------------------------------------------------------------------------- /static/vendor/bootstrap/css/bootstrap-reboot.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v4.5.0 (https://getbootstrap.com/) 3 | * Copyright 2011-2020 The Bootstrap Authors 4 | * Copyright 2011-2020 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]){color:inherit;text-decoration:none}a:not([href]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important} 8 | /*# sourceMappingURL=bootstrap-reboot.min.css.map */ -------------------------------------------------------------------------------- /static/vendor/bootstrap/css/bootstrap-reboot.min.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["../../scss/bootstrap-reboot.scss","../../scss/_reboot.scss","dist/css/bootstrap-reboot.css","../../scss/vendor/_rfs.scss","bootstrap-reboot.css","../../scss/mixins/_hover.scss"],"names":[],"mappings":"AAAA;;;;;;ACkBA,ECTA,QADA,SDaE,WAAA,WAGF,KACE,YAAA,WACA,YAAA,KACA,yBAAA,KACA,4BAAA,YAMF,QAAA,MAAA,WAAA,OAAA,OAAA,OAAA,OAAA,KAAA,IAAA,QACE,QAAA,MAUF,KACE,OAAA,EACA,YAAA,aAAA,CAAA,kBAAA,CAAA,UAAA,CAAA,MAAA,CAAA,gBAAA,CAAA,KAAA,CAAA,WAAA,CAAA,UAAA,CAAA,mBAAA,CAAA,gBAAA,CAAA,iBAAA,CAAA,mBEgFI,UAAA,KF9EJ,YAAA,IACA,YAAA,IACA,MAAA,QACA,WAAA,KACA,iBAAA,KGlBF,0CH+BE,QAAA,YASF,GACE,WAAA,YACA,OAAA,EACA,SAAA,QAaF,GAAA,GAAA,GAAA,GAAA,GAAA,GACE,WAAA,EACA,cAAA,MAOF,EACE,WAAA,EACA,cAAA,KC9CF,0BDyDA,YAEE,gBAAA,UACA,wBAAA,UAAA,OAAA,gBAAA,UAAA,OACA,OAAA,KACA,cAAA,EACA,iCAAA,KAAA,yBAAA,KAGF,QACE,cAAA,KACA,WAAA,OACA,YAAA,QCnDF,GDsDA,GCvDA,GD0DE,WAAA,EACA,cAAA,KAGF,MCtDA,MACA,MAFA,MD2DE,cAAA,EAGF,GACE,YAAA,IAGF,GACE,cAAA,MACA,YAAA,EAGF,WACE,OAAA,EAAA,EAAA,KAGF,ECvDA,ODyDE,YAAA,OAGF,MExFI,UAAA,IFiGJ,IC5DA,ID8DE,SAAA,SEnGE,UAAA,IFqGF,YAAA,EACA,eAAA,SAGF,IAAM,OAAA,OACN,IAAM,IAAA,MAON,EACE,MAAA,QACA,gBAAA,KACA,iBAAA,YIhLA,QJmLE,MAAA,QACA,gBAAA,UASJ,cACE,MAAA,QACA,gBAAA,KI/LA,oBJkME,MAAA,QACA,gBAAA,KC7DJ,KACA,IDqEA,ICpEA,KDwEE,YAAA,cAAA,CAAA,KAAA,CAAA,MAAA,CAAA,QAAA,CAAA,iBAAA,CAAA,aAAA,CAAA,UEpJE,UAAA,IFwJJ,IAEE,WAAA,EAEA,cAAA,KAEA,SAAA,KAGA,mBAAA,UAQF,OAEE,OAAA,EAAA,EAAA,KAQF,IACE,eAAA,OACA,aAAA,KAGF,IAGE,SAAA,OACA,eAAA,OAQF,MACE,gBAAA,SAGF,QACE,YAAA,OACA,eAAA,OACA,MAAA,QACA,WAAA,KACA,aAAA,OAGF,GAGE,WAAA,QAQF,MAEE,QAAA,aACA,cAAA,MAMF,OAEE,cAAA,EAOF,aACE,QAAA,IAAA,OACA,QAAA,IAAA,KAAA,yBC1GF,OD6GA,MC3GA,SADA,OAEA,SD+GE,OAAA,EACA,YAAA,QExPE,UAAA,QF0PF,YAAA,QAGF,OC7GA,MD+GE,SAAA,QAGF,OC7GA,OD+GE,eAAA,KG7GF,cHoHE,OAAA,QAMF,OACE,UAAA,OChHF,cACA,aACA,cDqHA,OAIE,mBAAA,OCpHF,6BACA,4BACA,6BDuHE,sBAKI,OAAA,QCvHN,gCACA,+BACA,gCD2HA,yBAIE,QAAA,EACA,aAAA,KC1HF,qBD6HA,kBAEE,WAAA,WACA,QAAA,EAIF,SACE,SAAA,KAEA,OAAA,SAGF,SAME,UAAA,EAEA,QAAA,EACA,OAAA,EACA,OAAA,EAKF,OACE,QAAA,MACA,MAAA,KACA,UAAA,KACA,QAAA,EACA,cAAA,ME/RI,UAAA,OFiSJ,YAAA,QACA,MAAA,QACA,YAAA,OAGF,SACE,eAAA,SGvIF,yCFGA,yCD0IE,OAAA,KGxIF,cHgJE,eAAA,KACA,mBAAA,KG5IF,yCHoJE,mBAAA,KAQF,6BACE,KAAA,QACA,mBAAA,OAOF,OACE,QAAA,aAGF,QACE,QAAA,UACA,OAAA,QAGF,SACE,QAAA,KGzJF,SH+JE,QAAA","sourcesContent":["/*!\n * Bootstrap Reboot v4.5.0 (https://getbootstrap.com/)\n * Copyright 2011-2020 The Bootstrap Authors\n * Copyright 2011-2020 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)\n */\n\n@import \"functions\";\n@import \"variables\";\n@import \"mixins\";\n@import \"reboot\";\n","// stylelint-disable at-rule-no-vendor-prefix, declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix\n\n// Reboot\n//\n// Normalization of HTML elements, manually forked from Normalize.css to remove\n// styles targeting irrelevant browsers while applying new styles.\n//\n// Normalize is licensed MIT. https://github.com/necolas/normalize.css\n\n\n// Document\n//\n// 1. Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.\n// 2. Change the default font family in all browsers.\n// 3. Correct the line height in all browsers.\n// 4. Prevent adjustments of font size after orientation changes in IE on Windows Phone and in iOS.\n// 5. Change the default tap highlight to be completely transparent in iOS.\n\n*,\n*::before,\n*::after {\n box-sizing: border-box; // 1\n}\n\nhtml {\n font-family: sans-serif; // 2\n line-height: 1.15; // 3\n -webkit-text-size-adjust: 100%; // 4\n -webkit-tap-highlight-color: rgba($black, 0); // 5\n}\n\n// Shim for \"new\" HTML5 structural elements to display correctly (IE10, older browsers)\n// TODO: remove in v5\n// stylelint-disable-next-line selector-list-comma-newline-after\narticle, aside, figcaption, figure, footer, header, hgroup, main, nav, section {\n display: block;\n}\n\n// Body\n//\n// 1. Remove the margin in all browsers.\n// 2. As a best practice, apply a default `background-color`.\n// 3. Set an explicit initial text-align value so that we can later use\n// the `inherit` value on things like `` elements.\n\nbody {\n margin: 0; // 1\n font-family: $font-family-base;\n @include font-size($font-size-base);\n font-weight: $font-weight-base;\n line-height: $line-height-base;\n color: $body-color;\n text-align: left; // 3\n background-color: $body-bg; // 2\n}\n\n// Future-proof rule: in browsers that support :focus-visible, suppress the focus outline\n// on elements that programmatically receive focus but wouldn't normally show a visible\n// focus outline. In general, this would mean that the outline is only applied if the\n// interaction that led to the element receiving programmatic focus was a keyboard interaction,\n// or the browser has somehow determined that the user is primarily a keyboard user and/or\n// wants focus outlines to always be presented.\n//\n// See https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible\n// and https://developer.paciellogroup.com/blog/2018/03/focus-visible-and-backwards-compatibility/\n[tabindex=\"-1\"]:focus:not(:focus-visible) {\n outline: 0 !important;\n}\n\n\n// Content grouping\n//\n// 1. Add the correct box sizing in Firefox.\n// 2. Show the overflow in Edge and IE.\n\nhr {\n box-sizing: content-box; // 1\n height: 0; // 1\n overflow: visible; // 2\n}\n\n\n//\n// Typography\n//\n\n// Remove top margins from headings\n//\n// By default, `

`-`

` all receive top and bottom margins. We nuke the top\n// margin for easier control within type scales as it avoids margin collapsing.\n// stylelint-disable-next-line selector-list-comma-newline-after\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: $headings-margin-bottom;\n}\n\n// Reset margins on paragraphs\n//\n// Similarly, the top margin on `

`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n// Abbreviations\n//\n// 1. Duplicate behavior to the data-* attribute for our tooltip plugin\n// 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Remove the bottom border in Firefox 39-.\n// 5. Prevent the text-decoration to be skipped.\n\nabbr[title],\nabbr[data-original-title] { // 1\n text-decoration: underline; // 2\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n border-bottom: 0; // 4\n text-decoration-skip-ink: none; // 5\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // Undo browser default\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\nb,\nstrong {\n font-weight: $font-weight-bolder; // Add the correct font weight in Chrome, Edge, and Safari\n}\n\nsmall {\n @include font-size(80%); // Add the correct font size in all browsers\n}\n\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n//\n\nsub,\nsup {\n position: relative;\n @include font-size(75%);\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n//\n// Links\n//\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n background-color: transparent; // Remove the gray background on active links in IE 10.\n\n @include hover() {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]) {\n color: inherit;\n text-decoration: none;\n\n @include hover() {\n color: inherit;\n text-decoration: none;\n }\n}\n\n\n//\n// Code\n//\n\npre,\ncode,\nkbd,\nsamp {\n font-family: $font-family-monospace;\n @include font-size(1em); // Correct the odd `em` font sizing in all browsers.\n}\n\npre {\n // Remove browser default top margin\n margin-top: 0;\n // Reset browser default of `1em` to use `rem`s\n margin-bottom: 1rem;\n // Don't allow content to break outside\n overflow: auto;\n // Disable auto-hiding scrollbar in IE & legacy Edge to avoid overlap,\n // making it impossible to interact with the content\n -ms-overflow-style: scrollbar;\n}\n\n\n//\n// Figures\n//\n\nfigure {\n // Apply a consistent margin strategy (matches our type styles).\n margin: 0 0 1rem;\n}\n\n\n//\n// Images and content\n//\n\nimg {\n vertical-align: middle;\n border-style: none; // Remove the border on images inside links in IE 10-.\n}\n\nsvg {\n // Workaround for the SVG overflow bug in IE10/11 is still required.\n // See https://github.com/twbs/bootstrap/issues/26878\n overflow: hidden;\n vertical-align: middle;\n}\n\n\n//\n// Tables\n//\n\ntable {\n border-collapse: collapse; // Prevent double borders\n}\n\ncaption {\n padding-top: $table-cell-padding;\n padding-bottom: $table-cell-padding;\n color: $table-caption-color;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n // Matches default `` alignment by inheriting from the ``, or the\n // closest parent with a set `text-align`.\n text-align: inherit;\n}\n\n\n//\n// Forms\n//\n\nlabel {\n // Allow labels to use `margin` for spacing.\n display: inline-block;\n margin-bottom: $label-margin-bottom;\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n//\n// Details at https://github.com/twbs/bootstrap/issues/24093\nbutton {\n // stylelint-disable-next-line property-blacklist\n border-radius: 0;\n}\n\n// Work around a Firefox/IE bug where the transparent `button` background\n// results in a loss of the default `button` focus styles.\n//\n// Credit: https://github.com/suitcss/base/\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // Remove the margin in Firefox and Safari\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible; // Show the overflow in Edge\n}\n\nbutton,\nselect {\n text-transform: none; // Remove the inheritance of text transform in Firefox\n}\n\n// Set the cursor for non-` 58 | 59 |

60 | 61 | 62 | 63 |
64 | 65 | {% include "footer.html" %} -------------------------------------------------------------------------------- /templates/footer.html: -------------------------------------------------------------------------------- 1 | 2 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /templates/header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Angela's Blog 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 63 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | {% include "header.html" %} 2 | 3 | 4 |
5 |
6 |
7 |
8 |
9 |
10 |

Angela's Blog

11 | A collection of random musings. 12 |
13 |
14 |
15 |
16 |
17 | 18 | 19 |
20 |
21 |
22 | {% for post in all_posts %} 23 |
24 | 25 |

26 | {{post.title}} 27 |

28 |

29 | {{post.subtitle}} 30 |

31 |
32 | 39 |
40 | {% endfor %} 41 | 42 | 43 | 44 | {% if current_user.id == 1: %} 45 |
46 | Create New Post 47 |
48 | {% endif %} 49 |
50 |
51 |
52 |
53 | 54 | {% include "footer.html" %} -------------------------------------------------------------------------------- /templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'bootstrap/base.html' %} 2 | {% import "bootstrap/wtf.html" as wtf %} 3 | 4 | {% block content %} 5 | {% include "header.html" %} 6 | 7 | 8 |
9 |
10 |
11 |
12 |
13 |
14 |

Log In

15 | Welcome Back! 16 |
17 |
18 |
19 |
20 |
21 | 22 |
23 |
24 | 25 |
26 | {% with messages = get_flashed_messages() %} 27 | {% if messages %} 28 | {% for message in messages %} 29 |

{{ message }}

30 | {% endfor %} 31 | {% endif %} 32 | {% endwith %} 33 | {{ wtf.quick_form(form, novalidate=True, button_map={"submit": "primary"}) }} 34 |
35 |
36 |
37 | 38 | 39 | {% include "footer.html" %} 40 | {% endblock %} -------------------------------------------------------------------------------- /templates/make-post.html: -------------------------------------------------------------------------------- 1 | {% extends 'bootstrap/base.html' %} 2 | {% import "bootstrap/wtf.html" as wtf %} 3 | 4 | {% block content %} 5 | {% include "header.html" %} 6 | 7 |
8 |
9 |
10 |
11 |
12 |
13 | {% if is_edit: %} 14 |

Edit Post

15 | {% else: %} 16 |

New Post

17 | {% endif %} 18 | You're going to make a great blog post! 19 |
20 |
21 |
22 |
23 |
24 | 25 |
26 |
27 |
28 | {{ ckeditor.load() }} 29 | {{ ckeditor.config(name='body') }} 30 | {{ wtf.quick_form(form, novalidate=True, button_map={"submit": "primary"}) }} 31 |
32 |
33 |
34 | 35 | {% include "footer.html" %} 36 | {% endblock %} 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /templates/post.html: -------------------------------------------------------------------------------- 1 | {% extends 'bootstrap/base.html' %} 2 | {% import "bootstrap/wtf.html" as wtf %} 3 | 4 | {% block content %} 5 | {% include "header.html" %} 6 | 7 | 8 |
9 |
10 |
11 |
12 |
13 |
14 |

{{post.title}}

15 |

{{post.subtitle}}

16 | Posted by 17 | {{post.author.name}} 18 | on {{post.date}} 19 |
20 |
21 |
22 |
23 |
24 | 25 | 26 |
27 |
28 |
29 |
30 | {{ post.body|safe }} 31 |
32 | {% if current_user.id == 1 %} 33 |
34 | Edit 35 | Post 36 |
37 | {% endif %} 38 | 39 | 40 | 41 | 42 | {{ ckeditor.load() }} 43 | {{ ckeditor.config(name='comment_text') }} 44 | {{ wtf.quick_form(form, novalidate=True, button_map={"submit": "primary"}) }} 45 | 46 | 47 |
48 | {% for comment in post.comments: %} 49 |
    50 |
  • 51 |
    52 | 53 |
    54 |
    55 | {{comment.text|safe}} 56 | {{comment.comment_author.name}} 57 | 58 |
    59 |
  • 60 |
61 | {% endfor %} 62 |
63 | 64 | 65 |
66 |
67 |
68 |
69 | 70 |
71 | {% include "footer.html" %} 72 | {% endblock %} -------------------------------------------------------------------------------- /templates/register.html: -------------------------------------------------------------------------------- 1 | {% extends 'bootstrap/base.html' %} 2 | {% import "bootstrap/wtf.html" as wtf %} 3 | 4 | {% block content %} 5 | {% include "header.html" %} 6 | 7 | 8 |
9 |
10 |
11 |
12 |
13 |
14 |

Register

15 | Start Contributing to the Blog! 16 |
17 |
18 |
19 |
20 |
21 | 22 |
23 |
24 |
25 | {{ wtf.quick_form(form, novalidate=True, button_map={"submit": "primary"}) }} 26 |
27 |
28 |
29 | 30 | {% include "footer.html" %} 31 | {% endblock %} --------------------------------------------------------------------------------