diff --git a/todos/django/Pipfile b/todos/django/Pipfile
new file mode 100644
index 0000000..d040849
--- /dev/null
+++ b/todos/django/Pipfile
@@ -0,0 +1,18 @@
+[[source]]
+url = "https://pypi.org/simple"
+verify_ssl = true
+name = "pypi"
+
+[packages]
+django = "*"
+
+[dev-packages]
+"flake8" = "*"
+"flake8-django" = "*"
+"autopep8" = "*"
+
+[requires]
+python_version = "3.6"
+
+[scripts]
+start = "django-admin runserver --pythonpath=. --settings=todomvp"
diff --git a/todos/django/Pipfile.lock b/todos/django/Pipfile.lock
new file mode 100644
index 0000000..d9145b2
--- /dev/null
+++ b/todos/django/Pipfile.lock
@@ -0,0 +1,81 @@
+{
+ "_meta": {
+ "hash": {
+ "sha256": "7faae15d2b677edd6ad93507f7316e0b566cfa0ae521694e2750cc2585e62f9b"
+ },
+ "pipfile-spec": 6,
+ "requires": {
+ "python_version": "3.6"
+ },
+ "sources": [
+ {
+ "name": "pypi",
+ "url": "https://pypi.org/simple",
+ "verify_ssl": true
+ }
+ ]
+ },
+ "default": {
+ "django": {
+ "hashes": [
+ "sha256:1ffab268ada3d5684c05ba7ce776eaeedef360712358d6a6b340ae9f16486916",
+ "sha256:dd46d87af4c1bf54f4c926c3cfa41dc2b5c15782f15e4329752ce65f5dad1c37"
+ ],
+ "index": "pypi",
+ "version": "==2.1.3"
+ },
+ "pytz": {
+ "hashes": [
+ "sha256:31cb35c89bd7d333cd32c5f278fca91b523b0834369e757f4c5641ea252236ca",
+ "sha256:8e0f8568c118d3077b46be7d654cc8167fa916092e28320cde048e54bfc9f1e6"
+ ],
+ "version": "==2018.7"
+ }
+ },
+ "develop": {
+ "autopep8": {
+ "hashes": [
+ "sha256:33d2b5325b7e1afb4240814fe982eea3a92ebea712869bfd08b3c0393404248c"
+ ],
+ "index": "pypi",
+ "version": "==1.4.3"
+ },
+ "flake8": {
+ "hashes": [
+ "sha256:6a35f5b8761f45c5513e3405f110a86bea57982c3b75b766ce7b65217abe1670",
+ "sha256:c01f8a3963b3571a8e6bd7a4063359aff90749e160778e03817cd9b71c9e07d2"
+ ],
+ "index": "pypi",
+ "version": "==3.6.0"
+ },
+ "flake8-django": {
+ "hashes": [
+ "sha256:44ae68fe251a7affd39445acc78965d48f86bf37a04f5f647eba318cac1d29e4",
+ "sha256:af4b240de5a8bac27ea2596b9b4860aad052922fa8a347e28d09a898fee29f42"
+ ],
+ "index": "pypi",
+ "version": "==0.0.3"
+ },
+ "mccabe": {
+ "hashes": [
+ "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
+ "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
+ ],
+ "version": "==0.6.1"
+ },
+ "pycodestyle": {
+ "hashes": [
+ "sha256:cbc619d09254895b0d12c2c691e237b2e91e9b2ecf5e84c26b35400f93dcfb83",
+ "sha256:cbfca99bd594a10f674d0cd97a3d802a1fdef635d4361e1a2658de47ed261e3a"
+ ],
+ "version": "==2.4.0"
+ },
+ "pyflakes": {
+ "hashes": [
+ "sha256:9a7662ec724d0120012f6e29d6248ae3727d821bba522a0e6b356eff19126a49",
+ "sha256:f661252913bc1dbe7fcfcbf0af0db3f42ab65aabd1a6ca68fe5d466bace94dae"
+ ],
+ "version": "==2.0.0"
+ }
+ }
+}
diff --git a/todos/django/static/check.svg b/todos/django/static/check.svg
new file mode 100644
index 0000000..2df5dee
--- /dev/null
+++ b/todos/django/static/check.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/todos/django/static/plus.svg b/todos/django/static/plus.svg
new file mode 100644
index 0000000..23c27d8
--- /dev/null
+++ b/todos/django/static/plus.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/todos/django/static/tick.png b/todos/django/static/tick.png
new file mode 100644
index 0000000..71fb4ad
Binary files /dev/null and b/todos/django/static/tick.png differ
diff --git a/todos/django/static/todo.css b/todos/django/static/todo.css
new file mode 100644
index 0000000..a4594b7
--- /dev/null
+++ b/todos/django/static/todo.css
@@ -0,0 +1,153 @@
+:root {
+ --text-color: rgb(20,20,20);
+ --border-color: black;
+ --title-color: rgb(60,60,60);
+ --done-color: rgb(200,200,200);
+ --background-color: white;
+ box-sizing: border-box;
+ padding: 0;
+ margin: 0;
+}
+
+body {
+ min-height: 100vh;
+ padding: 0;
+ align-content: center;
+ margin: 0;
+ font-family: sans-serif;
+ line-height: 1.3rem;
+ font-size: 24px;
+
+ background-color: var(--background-color);
+ color: var(--text-color);
+}
+
+label {
+ font-size: 0.8rem;
+}
+
+input {
+ box-sizing: border-box;
+ border: var(--border-color) solid 2px;
+ background-color: var(--background-color);
+}
+
+section {
+ padding: 1.5rem;
+ margin-left: auto;
+ margin-right: auto;
+ max-width: 40rem;
+}
+
+.new-todo form {
+ display: grid;
+ grid-template-columns: auto 29px;
+ grid-template-rows: auto;
+ column-gap: 1rem;
+}
+
+.new-todo label[for="new-item"] {
+ grid-row: 1 / span 1;
+ grid-column: 1 / span 2;
+ margin: auto 1rem;
+}
+
+.new-todo input#new-item {
+ grid-row: 1 / span 1;
+ grid-column: 1 / span 1;
+ padding: 0.5rem;
+ font-size: 1rem;
+}
+
+.new-todo form input#add-new-item {
+ grid-row: 1 / span 1;
+ grid-column: 2 / span 1;
+ margin: auto;
+}
+
+.button-container {
+ float: right;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+}
+
+ul {
+ margin: 0;
+ padding: 0;
+}
+
+ul:empty::after {
+ content: "You've not added any todos to the list. Use the box above to add a new todo.";
+ font-size: 1rem;
+ margin: 1rem;
+ font-style: italic;
+ border: 2px dotted black;
+ padding: 0.5rem;
+ display: block;
+ text-align: justify;
+ text-align-last: center;
+}
+
+li {
+ display: flex;
+ border-bottom: 1px dotted var(--border-color);
+}
+
+li:last-child {
+ border-bottom: none;
+}
+
+li .item-name {
+ flex: auto;
+ margin: auto;
+ padding: 0 1rem;
+}
+
+li form input,
+.new-todo form input#add-new-item {
+ width: 30px;
+ height: 30px;
+ padding: 0;
+
+ background-repeat: no-repeat;
+ background-size: contain;
+ background-position: center;
+
+ /* hide text */
+ text-indent: 100%;
+ white-space: nowrap;
+ overflow: hidden;
+}
+
+.todo form input {
+ margin: 1em;
+}
+
+.todo form input.delete {
+ background-image: url('trashcan.svg');
+ border: none;
+}
+input#add-new-item { background-image: url('plus.svg'); }
+.todo form input.complete { background-image: none; }
+.todo form input.uncomplete { background-image: url('check.svg'); }
+
+.done {
+ color: var(--done-color);
+}
+
+.todo form {
+ display: contents;
+}
+
+h1 {
+ font-weight: 200;
+ color: var(--title-color);
+ margin: 2rem;
+ display: none;
+}
+
+h2 {
+ border-bottom: 1px solid black;
+ padding: 0 0 1rem 1rem;
+}
diff --git a/todos/django/static/trashcan.svg b/todos/django/static/trashcan.svg
new file mode 100644
index 0000000..3d8c051
--- /dev/null
+++ b/todos/django/static/trashcan.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/todos/django/static/x.svg b/todos/django/static/x.svg
new file mode 100644
index 0000000..e377314
--- /dev/null
+++ b/todos/django/static/x.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/todos/django/templates/home.html b/todos/django/templates/home.html
new file mode 100644
index 0000000..5edd943
--- /dev/null
+++ b/todos/django/templates/home.html
@@ -0,0 +1,75 @@
+{% load static %}
+
+
+
+ Todo MVP
+
+
+
+
+ Todo MVP
+
+
+
+ Todo list
+ {% for todo in todos %}
+ {% if todo.done %}
+ -
+
+
{{ todo.name }}
+
+
+
+
+ {% else %}
+ -
+ {{ todo.name }}
+
+
+
+ {% endif %}
+ {% endfor %}
+
+
+
diff --git a/todos/django/todomvp/__init__.py b/todos/django/todomvp/__init__.py
new file mode 100644
index 0000000..de30fa2
--- /dev/null
+++ b/todos/django/todomvp/__init__.py
@@ -0,0 +1,38 @@
+# Note: I am manually creating a tiny project here since Django is much more
+# powerful and a bit too much of a powerhouse for this tiny project ;)
+# Django's new project command adds a whole lot more stuff that make sense for a
+# typical server, but not here.
+import os
+
+from todomvp.views import IndexView, todo_done, todo_undone, todo_delete
+from django.urls import path
+
+DEBUG = True
+SECRET_KEY = '4l0ngs3cr3tstr1ngw3lln0ts0l0ngw41tn0w1tsl0ng3n0ugh'
+ROOT_URLCONF = __name__
+BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+TEMPLATES = [
+ {
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
+ 'DIRS': [os.path.join(BASE_DIR, "templates")]
+ },
+]
+
+INSTALLED_APPS = [
+ "django.contrib.staticfiles"
+]
+
+STATIC_URL = "/static/"
+STATICFILES_FINDERS = [
+ 'django.contrib.staticfiles.finders.FileSystemFinder'
+]
+STATICFILES_DIRS = [
+ os.path.join(BASE_DIR, "static")
+]
+
+urlpatterns = [
+ path("", IndexView.as_view(), name="index"),
+ path("done", todo_done, name="done"),
+ path("not-done", todo_undone, name="not-done"),
+ path("delete", todo_delete, name="delete")
+]
diff --git a/todos/django/todomvp/views.py b/todos/django/todomvp/views.py
new file mode 100644
index 0000000..cc7ce2e
--- /dev/null
+++ b/todos/django/todomvp/views.py
@@ -0,0 +1,62 @@
+from django.http import HttpResponse
+from django.shortcuts import redirect, reverse
+from django.views.generic import TemplateView
+from django.views.generic.base import View
+
+todos = []
+
+
+def get_next_id() -> int:
+ """WARNING: Not thread-safe, and shouldn't be used in production. I'm
+ basically mimicking basic DB utility functions since I don't have any.
+
+ Use a DB, and use their utility functions!"""
+ if len(todos) == 0:
+ return 0
+ else:
+ return max([t["id"] for t in todos]) + 1
+
+def get_todo_index(request):
+ return [t["id"] for t in todos].index(int(request.POST.get("item")))
+
+
+class IndexView(TemplateView):
+ # If GET, will render template. If POST, will use the post function below.
+ # All other methods aren't allowed.
+ template_name = "home.html"
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ context["todos"] = todos
+ return context
+
+ def post(self, request, *args, **kwargs):
+ todo = {
+ "id": get_next_id(),
+ "name": request.POST.get("item"),
+ "done": False
+ }
+ todos.append(todo)
+
+ return redirect(reverse("index"))
+
+
+def todo_done(request):
+ if request.method == "POST":
+ idx = get_todo_index(request)
+ todos[idx]["done"] = True
+ return redirect(reverse("index"))
+
+
+def todo_undone(request):
+ if request.method == "POST":
+ idx = get_todo_index(request)
+ todos[idx]["done"] = False
+ return redirect(reverse("index"))
+
+
+def todo_delete(request):
+ if request.method == "POST":
+ idx = get_todo_index(request)
+ del todos[idx]
+ return redirect(reverse("index"))
diff --git a/todos/flask/.env b/todos/flask/.env
new file mode 100644
index 0000000..3904141
--- /dev/null
+++ b/todos/flask/.env
@@ -0,0 +1,9 @@
+# dotenvs get automatically applied by pipenv when running scripts or dropping
+# into virtualenv shell
+FLASK_SKIP_DOTENV=1
+FLASK_DEBUG=1
+FLASK_RUN_IP=0.0.0.0
+FLASK_RUN_PORT=3000
+FLASK_ENV=development
+FLASK_APP=todomvp
+FLASK_SECRET=fcd3f4b28b12b75488c3305222d7fd20
diff --git a/todos/flask/Pipfile b/todos/flask/Pipfile
new file mode 100644
index 0000000..ef99c0b
--- /dev/null
+++ b/todos/flask/Pipfile
@@ -0,0 +1,17 @@
+[[source]]
+url = "https://pypi.org/simple"
+verify_ssl = true
+name = "pypi"
+
+[packages]
+flask = "*"
+
+[dev-packages]
+"flake8" = "*"
+"autopep8" = "*"
+
+[requires]
+python_version = "3.6"
+
+[scripts]
+start = "flask run"
diff --git a/todos/flask/Pipfile.lock b/todos/flask/Pipfile.lock
new file mode 100644
index 0000000..cc1b954
--- /dev/null
+++ b/todos/flask/Pipfile.lock
@@ -0,0 +1,127 @@
+{
+ "_meta": {
+ "hash": {
+ "sha256": "cef3e31ae481a675c82aa02c4c343d754358189b5dea415e042a3395a787e6d4"
+ },
+ "pipfile-spec": 6,
+ "requires": {
+ "python_version": "3.6"
+ },
+ "sources": [
+ {
+ "name": "pypi",
+ "url": "https://pypi.org/simple",
+ "verify_ssl": true
+ }
+ ]
+ },
+ "default": {
+ "click": {
+ "hashes": [
+ "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
+ "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
+ ],
+ "version": "==7.0"
+ },
+ "flask": {
+ "hashes": [
+ "sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48",
+ "sha256:a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05"
+ ],
+ "index": "pypi",
+ "version": "==1.0.2"
+ },
+ "itsdangerous": {
+ "hashes": [
+ "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",
+ "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"
+ ],
+ "version": "==1.1.0"
+ },
+ "jinja2": {
+ "hashes": [
+ "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
+ "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
+ ],
+ "version": "==2.10"
+ },
+ "markupsafe": {
+ "hashes": [
+ "sha256:048ef924c1623740e70204aa7143ec592504045ae4429b59c30054cb31e3c432",
+ "sha256:130f844e7f5bdd8e9f3f42e7102ef1d49b2e6fdf0d7526df3f87281a532d8c8b",
+ "sha256:19f637c2ac5ae9da8bfd98cef74d64b7e1bb8a63038a3505cd182c3fac5eb4d9",
+ "sha256:1b8a7a87ad1b92bd887568ce54b23565f3fd7018c4180136e1cf412b405a47af",
+ "sha256:1c25694ca680b6919de53a4bb3bdd0602beafc63ff001fea2f2fc16ec3a11834",
+ "sha256:1f19ef5d3908110e1e891deefb5586aae1b49a7440db952454b4e281b41620cd",
+ "sha256:1fa6058938190ebe8290e5cae6c351e14e7bb44505c4a7624555ce57fbbeba0d",
+ "sha256:31cbb1359e8c25f9f48e156e59e2eaad51cd5242c05ed18a8de6dbe85184e4b7",
+ "sha256:3e835d8841ae7863f64e40e19477f7eb398674da6a47f09871673742531e6f4b",
+ "sha256:4e97332c9ce444b0c2c38dd22ddc61c743eb208d916e4265a2a3b575bdccb1d3",
+ "sha256:525396ee324ee2da82919f2ee9c9e73b012f23e7640131dd1b53a90206a0f09c",
+ "sha256:52b07fbc32032c21ad4ab060fec137b76eb804c4b9a1c7c7dc562549306afad2",
+ "sha256:52ccb45e77a1085ec5461cde794e1aa037df79f473cbc69b974e73940655c8d7",
+ "sha256:5c3fbebd7de20ce93103cb3183b47671f2885307df4a17a0ad56a1dd51273d36",
+ "sha256:5e5851969aea17660e55f6a3be00037a25b96a9b44d2083651812c99d53b14d1",
+ "sha256:5edfa27b2d3eefa2210fb2f5d539fbed81722b49f083b2c6566455eb7422fd7e",
+ "sha256:7d263e5770efddf465a9e31b78362d84d015cc894ca2c131901a4445eaa61ee1",
+ "sha256:83381342bfc22b3c8c06f2dd93a505413888694302de25add756254beee8449c",
+ "sha256:857eebb2c1dc60e4219ec8e98dfa19553dae33608237e107db9c6078b1167856",
+ "sha256:98e439297f78fca3a6169fd330fbe88d78b3bb72f967ad9961bcac0d7fdd1550",
+ "sha256:bf54103892a83c64db58125b3f2a43df6d2cb2d28889f14c78519394feb41492",
+ "sha256:d9ac82be533394d341b41d78aca7ed0e0f4ba5a2231602e2f05aa87f25c51672",
+ "sha256:e982fe07ede9fada6ff6705af70514a52beb1b2c3d25d4e873e82114cf3c5401",
+ "sha256:edce2ea7f3dfc981c4ddc97add8a61381d9642dc3273737e756517cc03e84dd6",
+ "sha256:efdc45ef1afc238db84cb4963aa689c0408912a0239b0721cb172b4016eb31d6",
+ "sha256:f137c02498f8b935892d5c0172560d7ab54bc45039de8805075e19079c639a9c",
+ "sha256:f82e347a72f955b7017a39708a3667f106e6ad4d10b25f237396a7115d8ed5fd",
+ "sha256:fb7c206e01ad85ce57feeaaa0bf784b97fa3cad0d4a5737bc5295785f5c613a1"
+ ],
+ "version": "==1.1.0"
+ },
+ "werkzeug": {
+ "hashes": [
+ "sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c",
+ "sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b"
+ ],
+ "version": "==0.14.1"
+ }
+ },
+ "develop": {
+ "autopep8": {
+ "hashes": [
+ "sha256:33d2b5325b7e1afb4240814fe982eea3a92ebea712869bfd08b3c0393404248c"
+ ],
+ "index": "pypi",
+ "version": "==1.4.3"
+ },
+ "flake8": {
+ "hashes": [
+ "sha256:6a35f5b8761f45c5513e3405f110a86bea57982c3b75b766ce7b65217abe1670",
+ "sha256:c01f8a3963b3571a8e6bd7a4063359aff90749e160778e03817cd9b71c9e07d2"
+ ],
+ "index": "pypi",
+ "version": "==3.6.0"
+ },
+ "mccabe": {
+ "hashes": [
+ "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
+ "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
+ ],
+ "version": "==0.6.1"
+ },
+ "pycodestyle": {
+ "hashes": [
+ "sha256:cbc619d09254895b0d12c2c691e237b2e91e9b2ecf5e84c26b35400f93dcfb83",
+ "sha256:cbfca99bd594a10f674d0cd97a3d802a1fdef635d4361e1a2658de47ed261e3a"
+ ],
+ "version": "==2.4.0"
+ },
+ "pyflakes": {
+ "hashes": [
+ "sha256:9a7662ec724d0120012f6e29d6248ae3727d821bba522a0e6b356eff19126a49",
+ "sha256:f661252913bc1dbe7fcfcbf0af0db3f42ab65aabd1a6ca68fe5d466bace94dae"
+ ],
+ "version": "==2.0.0"
+ }
+ }
+}
diff --git a/todos/flask/todomvp/__init__.py b/todos/flask/todomvp/__init__.py
new file mode 100644
index 0000000..358683a
--- /dev/null
+++ b/todos/flask/todomvp/__init__.py
@@ -0,0 +1,11 @@
+import os
+
+from flask import Flask
+
+app = Flask(__name__)
+app.config["SECRET_KEY"] = os.environ.get("FLASK_SECRET")
+
+from todomvp import routes
+
+if __name__ == "__main__":
+ app.run(debug=bool(os.environ.get("FLASK_DEBUG")))
diff --git a/todos/flask/todomvp/routes.py b/todos/flask/todomvp/routes.py
new file mode 100644
index 0000000..2d06b23
--- /dev/null
+++ b/todos/flask/todomvp/routes.py
@@ -0,0 +1,54 @@
+from flask import render_template, redirect, url_for
+from flask.globals import request
+from todomvp import app
+
+todos = []
+
+def get_next_id() -> int:
+ """WARNING: Not thread-safe, and shouldn't be used in production. I'm
+ basically mimicking basic DB utility functions since I don't have any.
+
+ Use a DB, and use their utility functions!"""
+ if len(todos) == 0:
+ return 0
+ else:
+ return max([t["id"] for t in todos]) + 1
+
+
+@app.route("/", methods=["GET", "POST"])
+def index():
+ if request.method == "POST":
+ todo = {
+ "id": get_next_id(),
+ "name": request.form.get("item"),
+ "done": False
+ }
+ todos.append(todo)
+
+ return render_template("home.html", todos=todos)
+
+
+
+def get_todo_index() -> int:
+ return [t["id"] for t in todos].index(int(request.form.get("item")))
+
+
+@app.route("/done", methods=["POST"])
+def done():
+ idx = get_todo_index()
+ todos[idx]["done"] = True
+ return redirect(url_for("index"))
+
+
+@app.route("/not-done", methods=["POST"])
+def not_done():
+ idx = get_todo_index()
+ todos[idx]["done"] = False
+ return redirect(url_for("index"))
+
+
+@app.route("/delete", methods=["POST"])
+def delete():
+ idx = get_todo_index()
+ del todos[idx]
+ return redirect(url_for("index"))
diff --git a/todos/flask/todomvp/static/check.svg b/todos/flask/todomvp/static/check.svg
new file mode 100644
index 0000000..2df5dee
--- /dev/null
+++ b/todos/flask/todomvp/static/check.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/todos/flask/todomvp/static/plus.svg b/todos/flask/todomvp/static/plus.svg
new file mode 100644
index 0000000..23c27d8
--- /dev/null
+++ b/todos/flask/todomvp/static/plus.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/todos/flask/todomvp/static/tick.png b/todos/flask/todomvp/static/tick.png
new file mode 100644
index 0000000..71fb4ad
Binary files /dev/null and b/todos/flask/todomvp/static/tick.png differ
diff --git a/todos/flask/todomvp/static/todo.css b/todos/flask/todomvp/static/todo.css
new file mode 100644
index 0000000..a4594b7
--- /dev/null
+++ b/todos/flask/todomvp/static/todo.css
@@ -0,0 +1,153 @@
+:root {
+ --text-color: rgb(20,20,20);
+ --border-color: black;
+ --title-color: rgb(60,60,60);
+ --done-color: rgb(200,200,200);
+ --background-color: white;
+ box-sizing: border-box;
+ padding: 0;
+ margin: 0;
+}
+
+body {
+ min-height: 100vh;
+ padding: 0;
+ align-content: center;
+ margin: 0;
+ font-family: sans-serif;
+ line-height: 1.3rem;
+ font-size: 24px;
+
+ background-color: var(--background-color);
+ color: var(--text-color);
+}
+
+label {
+ font-size: 0.8rem;
+}
+
+input {
+ box-sizing: border-box;
+ border: var(--border-color) solid 2px;
+ background-color: var(--background-color);
+}
+
+section {
+ padding: 1.5rem;
+ margin-left: auto;
+ margin-right: auto;
+ max-width: 40rem;
+}
+
+.new-todo form {
+ display: grid;
+ grid-template-columns: auto 29px;
+ grid-template-rows: auto;
+ column-gap: 1rem;
+}
+
+.new-todo label[for="new-item"] {
+ grid-row: 1 / span 1;
+ grid-column: 1 / span 2;
+ margin: auto 1rem;
+}
+
+.new-todo input#new-item {
+ grid-row: 1 / span 1;
+ grid-column: 1 / span 1;
+ padding: 0.5rem;
+ font-size: 1rem;
+}
+
+.new-todo form input#add-new-item {
+ grid-row: 1 / span 1;
+ grid-column: 2 / span 1;
+ margin: auto;
+}
+
+.button-container {
+ float: right;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+}
+
+ul {
+ margin: 0;
+ padding: 0;
+}
+
+ul:empty::after {
+ content: "You've not added any todos to the list. Use the box above to add a new todo.";
+ font-size: 1rem;
+ margin: 1rem;
+ font-style: italic;
+ border: 2px dotted black;
+ padding: 0.5rem;
+ display: block;
+ text-align: justify;
+ text-align-last: center;
+}
+
+li {
+ display: flex;
+ border-bottom: 1px dotted var(--border-color);
+}
+
+li:last-child {
+ border-bottom: none;
+}
+
+li .item-name {
+ flex: auto;
+ margin: auto;
+ padding: 0 1rem;
+}
+
+li form input,
+.new-todo form input#add-new-item {
+ width: 30px;
+ height: 30px;
+ padding: 0;
+
+ background-repeat: no-repeat;
+ background-size: contain;
+ background-position: center;
+
+ /* hide text */
+ text-indent: 100%;
+ white-space: nowrap;
+ overflow: hidden;
+}
+
+.todo form input {
+ margin: 1em;
+}
+
+.todo form input.delete {
+ background-image: url('trashcan.svg');
+ border: none;
+}
+input#add-new-item { background-image: url('plus.svg'); }
+.todo form input.complete { background-image: none; }
+.todo form input.uncomplete { background-image: url('check.svg'); }
+
+.done {
+ color: var(--done-color);
+}
+
+.todo form {
+ display: contents;
+}
+
+h1 {
+ font-weight: 200;
+ color: var(--title-color);
+ margin: 2rem;
+ display: none;
+}
+
+h2 {
+ border-bottom: 1px solid black;
+ padding: 0 0 1rem 1rem;
+}
diff --git a/todos/flask/todomvp/static/trashcan.svg b/todos/flask/todomvp/static/trashcan.svg
new file mode 100644
index 0000000..3d8c051
--- /dev/null
+++ b/todos/flask/todomvp/static/trashcan.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/todos/flask/todomvp/static/x.svg b/todos/flask/todomvp/static/x.svg
new file mode 100644
index 0000000..e377314
--- /dev/null
+++ b/todos/flask/todomvp/static/x.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/todos/flask/todomvp/templates/home.html b/todos/flask/todomvp/templates/home.html
new file mode 100644
index 0000000..60ffc66
--- /dev/null
+++ b/todos/flask/todomvp/templates/home.html
@@ -0,0 +1,74 @@
+
+
+
+ Todo MVP
+
+
+
+
+ Todo MVP
+
+
+
+ Todo list
+ {% for todo in todos %}
+ {% if todo.done %}
+ -
+
+
{{ todo.name }}
+
+
+
+
+ {% else %}
+ -
+ {{ todo.name }}
+
+
+
+ {% endif %}
+ {% endfor %}
+
+
+