diff --git a/app/__init__.py b/app/__init__.py index 113ea27..51bf06e 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -4,6 +4,7 @@ from flask import Flask def create_app(): app = Flask(__name__) app.config['SECRET_KEY'] = "raleicuu0Engohh3iageephoh3looge0okupha2omeiph7Nooyeey1tiewooxu7phaeshi0ohlaaThai2eth1oapong5iroo4fieleekaidohmoh1eYahjei9Yi6aema" + app.config['SQLALCHEMY_ECHO'] = True app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db' from . import db diff --git a/app/css/main.css b/app/css/main.css index 3e26e15..7ee4514 100644 --- a/app/css/main.css +++ b/app/css/main.css @@ -40,10 +40,55 @@ form input[type=password]:focus { } form input[type=submit] { - @apply bg-blue-500 text-white font-bold py-2 px-4 rounded w-full mt-2; + @apply font-bold py-2 px-4 rounded w-full mt-2; } -form input[type=submit]:hover { +form.auth-form input[type=submit] { + @apply bg-blue-500 text-white; +} + +.green-btn { + @apply bg-green-500 text-white; +} + +.accomplishment .difficulty { + @apply font-bold; +} + +.accomplishment .difficulty-easy { + @apply text-green-700; +} + +.accomplishment .difficulty-medium { + @apply text-orange-700; +} + +.accomplishment .difficulty-hard { + @apply text-red-700; +} + +.green-btn:hover { + @apply bg-green-700; +} + +.orange-btn { + @apply bg-orange-500 text-white; +} + +.orange-btn:hover { + @apply bg-orange-700; +} + +.red-btn { + @apply bg-red-500 text-white; +} + +.red-btn:hover { + @apply bg-red-700; +} + + +form.auth-form input[type=submit]:hover { @apply bg-blue-700; } @@ -59,3 +104,10 @@ form input[type=submit]:focus { @apply text-blue-500; } +.card { + @apply bg-white shadow-md rounded px-8 pt-6 pb-6 mb-4 mt-4; +} + +.auth-form.card { + @apply pb-4; +} diff --git a/app/db.py b/app/db.py index 78ca825..605830b 100644 --- a/app/db.py +++ b/app/db.py @@ -3,6 +3,9 @@ from flask_sqlalchemy import SQLAlchemy from flask_login import UserMixin from flask_bcrypt import generate_password_hash, check_password_hash from flask_migrate import Migrate +from datetime import datetime, timedelta + +from . import timeutils db = SQLAlchemy() migrate = Migrate() @@ -22,6 +25,9 @@ class User(UserMixin, db.Model): last_login = db.Column(db.DateTime, index=False, unique=False, nullable=True) + accomplishments = db.relationship( + 'Accomplishment', backref='user', lazy=True) + def set_password(self, password): self.password = generate_password_hash(password) @@ -30,3 +36,37 @@ class User(UserMixin, db.Model): def __repr__(self): return ''.format(self.username) + + +class Accomplishment(db.Model): + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + time = db.Column(db.DateTime(), nullable=False, + default=db.func.current_timestamp()) + text = db.Column(db.String(128), nullable=False) + difficulty = db.Column(db.Integer) + + @property + def difficulty_class(self): + if self.difficulty <= 5: + return "easy" + if self.difficulty <= 10: + return "medium" + return "hard" + + @staticmethod + def get_time_range(user_id, start, end): + return Accomplishment.query.filter( + Accomplishment.time >= start, Accomplishment.time < end, Accomplishment.user_id == user_id).all() + + @staticmethod + def get_day(user_id, day): + # TODO: allow setting custom "start of day" hour + start = timeutils.day(day) + end = timeutils.day_after(day) + return Accomplishment.get_time_range(user_id, start, end) + + @staticmethod + def get_today(user_id): + today = datetime.now() + return Accomplishment.get_day(user_id, today) diff --git a/app/main.py b/app/main.py index 87bb7a2..a6199ef 100644 --- a/app/main.py +++ b/app/main.py @@ -1,8 +1,78 @@ -from flask import Blueprint, render_template +from . import timeutils +from flask import Blueprint, render_template, redirect, url_for +from flask_login import current_user +from flask_wtf import FlaskForm +from wtforms import StringField, SubmitField +from wtforms.validators import DataRequired, Length +from .db import db, Accomplishment +from datetime import datetime, timedelta main = Blueprint('main', __name__) -@main.route('/') -def index(): - return render_template('index.html') +class NewAccomplishementForm(FlaskForm): + text = StringField('Accomplishment', validators=[ + DataRequired(), Length(max=128)]) + submit_5 = SubmitField('5 XP') + submit_10 = SubmitField('10 XP') + submit_15 = SubmitField('15 XP') + + +def handle_accomplishment_submission(form): + accomplishment = Accomplishment() + accomplishment.user_id = current_user.id + accomplishment.text = form.text.data + accomplishment.difficulty = 5 + if form.submit_10.data: + accomplishment.difficulty = 10 + elif form.submit_15.data: + accomplishment.difficulty = 15 + # the timestamp should be set by the database + db.session.add(accomplishment) + db.session.commit() + return redirect(url_for('main.index')) + + +@main.route('/', defaults={'day': 'today'}, methods=['GET', 'POST']) +@main.route('/day/') +def index(day): + day_datetime = None + day_string = None + is_today = False + if day == "today": + day_datetime = timeutils.today() + day_string = "Today" + is_today = True + else: + day_datetime = timeutils.from_str(day) + if timeutils.is_today(day_datetime): + return redirect('/') + day_string = timeutils.as_fancy_str(day_datetime) + + if not current_user.is_authenticated: + return render_template('index.html') + + form = NewAccomplishementForm() + if form.validate_on_submit(): + return handle_accomplishment_submission(form) + + accomplishments = list(reversed(Accomplishment.get_day( + current_user.id, day_datetime))) + total = sum(a.difficulty for a in accomplishments) + + tomorrow = timeutils.day_after(day_datetime) + yesterday = timeutils.day_before(day_datetime) + + if timeutils.is_future(tomorrow): + tomorrow = None + + return render_template( + 'main/app.html', + form=form, + day=day_string, + accomplishments=accomplishments, + total=total, + tomorrow=timeutils.as_str(tomorrow), + yesterday=timeutils.as_str(yesterday), + is_today=is_today, + ) diff --git a/app/static/style.css b/app/static/style.css index 5f4b3f3..3386be7 100644 --- a/app/static/style.css +++ b/app/static/style.css @@ -22083,12 +22083,6 @@ form input[type=password]:focus { } form input[type=submit] { - --bg-opacity: 1; - background-color: #4299e1; - background-color: rgba(66, 153, 225, var(--bg-opacity)); - --text-opacity: 1; - color: #fff; - color: rgba(255, 255, 255, var(--text-opacity)); font-weight: 700; padding-top: 0.5rem; padding-bottom: 0.5rem; @@ -22099,7 +22093,83 @@ form input[type=submit] { margin-top: 0.5rem; } -form input[type=submit]:hover { +form.auth-form input[type=submit] { + --bg-opacity: 1; + background-color: #4299e1; + background-color: rgba(66, 153, 225, var(--bg-opacity)); + --text-opacity: 1; + color: #fff; + color: rgba(255, 255, 255, var(--text-opacity)); +} + +.green-btn { + --bg-opacity: 1; + background-color: #48bb78; + background-color: rgba(72, 187, 120, var(--bg-opacity)); + --text-opacity: 1; + color: #fff; + color: rgba(255, 255, 255, var(--text-opacity)); +} + +.accomplishment .difficulty { + font-weight: 700; +} + +.accomplishment .difficulty-easy { + --text-opacity: 1; + color: #2f855a; + color: rgba(47, 133, 90, var(--text-opacity)); +} + +.accomplishment .difficulty-medium { + --text-opacity: 1; + color: #c05621; + color: rgba(192, 86, 33, var(--text-opacity)); +} + +.accomplishment .difficulty-hard { + --text-opacity: 1; + color: #c53030; + color: rgba(197, 48, 48, var(--text-opacity)); +} + +.green-btn:hover { + --bg-opacity: 1; + background-color: #2f855a; + background-color: rgba(47, 133, 90, var(--bg-opacity)); +} + +.orange-btn { + --bg-opacity: 1; + background-color: #ed8936; + background-color: rgba(237, 137, 54, var(--bg-opacity)); + --text-opacity: 1; + color: #fff; + color: rgba(255, 255, 255, var(--text-opacity)); +} + +.orange-btn:hover { + --bg-opacity: 1; + background-color: #c05621; + background-color: rgba(192, 86, 33, var(--bg-opacity)); +} + +.red-btn { + --bg-opacity: 1; + background-color: #f56565; + background-color: rgba(245, 101, 101, var(--bg-opacity)); + --text-opacity: 1; + color: #fff; + color: rgba(255, 255, 255, var(--text-opacity)); +} + +.red-btn:hover { + --bg-opacity: 1; + background-color: #c53030; + background-color: rgba(197, 48, 48, var(--bg-opacity)); +} + +form.auth-form input[type=submit]:hover { --bg-opacity: 1; background-color: #2b6cb0; background-color: rgba(43, 108, 176, var(--bg-opacity)); @@ -22123,6 +22193,24 @@ form input[type=submit]:focus { color: rgba(66, 153, 225, var(--text-opacity)); } +.card { + --bg-opacity: 1; + background-color: #fff; + background-color: rgba(255, 255, 255, var(--bg-opacity)); + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + border-radius: 0.25rem; + padding-left: 2rem; + padding-right: 2rem; + padding-top: 1.5rem; + padding-bottom: 1.5rem; + margin-bottom: 1rem; + margin-top: 1rem; +} + +.auth-form.card { + padding-bottom: 1rem; +} + @media (min-width: 640px) { .sm\:container { width: 100%; @@ -107634,4 +107722,3 @@ form input[type=submit]:focus { animation: bounce 1s infinite; } } - diff --git a/app/templates/_skel.html b/app/templates/_skel.html index c29c3ce..3688938 100644 --- a/app/templates/_skel.html +++ b/app/templates/_skel.html @@ -13,6 +13,17 @@ {% block body %}
+
+

DoneTh.at

+ {% block header_user %} + {% if current_user.is_authenticated %} +

Hi {{ current_user.username }}!

+

+ Log out. +

+ {% endif %} + {% endblock %} +
{% block content %} {% endblock %}
diff --git a/app/templates/auth/login.html b/app/templates/auth/login.html index 2d12621..85869a9 100644 --- a/app/templates/auth/login.html +++ b/app/templates/auth/login.html @@ -1,8 +1,9 @@ {% extends "_skel.html" %} +{% block header_width %}max-w-xs{% endblock %} {% from "_formhelpers.html" import render_field %} {% block content %}
-
+ {{ form.csrf_token }} {{ render_field(form.username) }} {{ render_field(form.password) }} diff --git a/app/templates/auth/logout.html b/app/templates/auth/logout.html index 40d516c..3cf3f45 100644 --- a/app/templates/auth/logout.html +++ b/app/templates/auth/logout.html @@ -1,8 +1,10 @@ {% extends "_skel.html" %} +{% block header_width %}max-w-xs{% endblock %} +{% block header_user %}{% endblock %} {% from "_formhelpers.html" import render_field %} {% block content %}
- +

Are you sure you want to log out?

{{ form.csrf_token }} {{ render_field(form.submit, False) }} diff --git a/app/templates/auth/register.html b/app/templates/auth/register.html index 73b324e..886265d 100644 --- a/app/templates/auth/register.html +++ b/app/templates/auth/register.html @@ -1,8 +1,9 @@ {% extends "_skel.html" %} +{% block header_width %}max-w-xs{% endblock %} {% from "_formhelpers.html" import render_field %} {% block content %}
- + {{ form.csrf_token }} {{ render_field(form.username) }} {{ render_field(form.password) }} diff --git a/app/templates/index.html b/app/templates/index.html index bf5df85..98cec16 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -1,3 +1,8 @@ -{% if current_user.is_authenticated %} -Hi {{ current_user.username }}! Log out. -{% endif %} +{% extends "_skel.html" %} +{% block content %} +
+ {% if current_user.is_authenticated %} + Hi {{ current_user.username }}! Log out. + {% endif %} +
+{% endblock %} diff --git a/app/templates/main/app.html b/app/templates/main/app.html new file mode 100644 index 0000000..e45fbf6 --- /dev/null +++ b/app/templates/main/app.html @@ -0,0 +1,45 @@ +{% extends "_skel.html" %} +{% block content %} +
+ + {{ form.csrf_token }} + {{ form.text(placeholder="What did you accomplish today?", class_="placeholder-black", autofocus=True) }} +
+ {{ form.submit_5(class_="w-1/3 mr-1 green-btn") }} + {{ form.submit_10(class_="w-1/3 mx-1 orange-btn") }} + {{ form.submit_15(class_="w-1/3 ml-1 red-btn") }} +
+ +
+
+

{{ day }}

+ {% for accomplishment in accomplishments %} +
+
{{ accomplishment.text }}
+
{{ accomplishment.difficulty }} XP +
+
+
+ {% else %} +
+ {% if false %} + {% endif %} + {% if is_today %} +

No accomplishments today... yet!

+ {% else %} +

Nothing logged that day... but it's okay to take a break!

+ {% endif %} +
+ {% endfor %} +
+
total: {{ total }} XP
+
+
+ + {% if tomorrow %}Next day +
{% endif %} +
+ +
+
+{% endblock %} diff --git a/app/timeutils.py b/app/timeutils.py new file mode 100644 index 0000000..dd989e1 --- /dev/null +++ b/app/timeutils.py @@ -0,0 +1,52 @@ +""" +The timeutils module is supposed to be where ALL time related logic goes. +This is meant to ease handling timezones and custom day-start-hours later. +""" + +from datetime import datetime, timedelta + +# TODO: make it all custom-day-start-hour aware + + +def from_str(string): + return datetime.strptime(string, "%Y-%m-%d") + + +def as_str(day_): + if day_ is None: + return None + return day(day_).strftime("%Y-%m-%d") + + +def _suffix(d): + return 'th' if 11 <= d <= 13 else {1: 'st', 2: 'nd', 3: 'rd'}.get(d % 10, 'th') + + +def as_fancy_str(day_): + if day_ is None: + return None + return day_.strftime("%B {S}, %Y").replace('{S}', str(day_.day) + _suffix(day_.day)) + + +def day(timestamp): + return datetime(timestamp.year, timestamp.month, timestamp.day) + + +def today(): + return day(datetime.now()) + + +def day_after(day_): + return day(day_) + timedelta(days=1) + + +def day_before(day_): + return day(day_) - timedelta(days=1) + + +def is_future(day_): + return day(day_) > today() + + +def is_today(day_): + return day(day_) == today() diff --git a/migrations/versions/cd3b1ad8c50b_add_accomplishement_model.py b/migrations/versions/cd3b1ad8c50b_add_accomplishement_model.py new file mode 100644 index 0000000..74a5ec7 --- /dev/null +++ b/migrations/versions/cd3b1ad8c50b_add_accomplishement_model.py @@ -0,0 +1,36 @@ +"""Add Accomplishement model + +Revision ID: cd3b1ad8c50b +Revises: 8b55f5add4f1 +Create Date: 2020-08-29 14:20:28.454292 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'cd3b1ad8c50b' +down_revision = '8b55f5add4f1' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('accomplishment', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('time', sa.DateTime(), nullable=False), + sa.Column('text', sa.String(length=128), nullable=False), + sa.Column('difficulty', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('accomplishment') + # ### end Alembic commands ###