diff --git a/app/airq/__init__.py b/app/airq/__init__.py index 8ed28d9..4ffdf6a 100644 --- a/app/airq/__init__.py +++ b/app/airq/__init__.py @@ -3,6 +3,7 @@ from airq.controllers import admin from airq.controllers import api +from airq.controllers import home from airq import management @@ -16,6 +17,7 @@ # TODO: Change '/' to be the admin page, # and repoint healthcheck /healthcheck app.route("/", methods=["GET"])(api.healthcheck) +app.route("/home", methods=["GET"])(home.home) app.route("/healthcheck", methods=["GET"])(api.healthcheck) app.route("/test/", methods=["GET"])(api.test_command) app.route("/sms/", methods=["POST"])(api.sms_reply) diff --git a/app/airq/controllers/admin.py b/app/airq/controllers/admin.py index a3d9e60..f079c25 100644 --- a/app/airq/controllers/admin.py +++ b/app/airq/controllers/admin.py @@ -42,7 +42,7 @@ def login() -> typing.Union[Response, str]: return redirect(url_for("login")) login_user(user, remember=True) return redirect(url_for("admin_summary")) - return render_template("login.html", title="Sign In", form=form) + return render_template("admin/login.html", title="Sign In", form=form) @login_required @@ -54,7 +54,7 @@ def logout() -> Response: @admin_required def admin_summary() -> str: return render_template( - "admin.html", + "admin/admin.html", title="Admin", summary={ "Total Alerts Sent": Client.query.get_total_num_sends(), @@ -86,7 +86,7 @@ def admin_bulk_sms(): flash("Sent!") return redirect(url_for("admin_summary")) return render_template( - "bulk_sms.html", + "admin/bulk_sms.html", form=form, num_inactive=Client.query.filter_inactive_since(timestamp()).count(), ) @@ -102,7 +102,7 @@ def admin_sms(): client.send_message(form.data["message"]) flash("Sent!") return redirect(url_for("admin_summary")) - return render_template("sms.html", form=form) + return render_template("admin/sms.html", form=form) @admin_required @@ -156,4 +156,4 @@ def upload_users(): return redirect(url_for("upload_users")) - return render_template("bulk_upload.html", form=form) + return render_template("admin/bulk_upload.html", form=form) diff --git a/app/airq/controllers/home.py b/app/airq/controllers/home.py new file mode 100644 index 0000000..0e658de --- /dev/null +++ b/app/airq/controllers/home.py @@ -0,0 +1,15 @@ +import json +from flask import render_template + +from airq.models.sensors import Sensor + +def home() -> str: + points = { + sensor_id: { + "latitude": latitude, + "longitude": longitude, + } for sensor_id, latitude, longitude + in Sensor.query.with_entities(Sensor.id, Sensor.latitude, Sensor.longitude).all() + } + return render_template("home.html", points=json.dumps(points)) + diff --git a/app/airq/static/home.css b/app/airq/static/home.css new file mode 100644 index 0000000..f6ccb19 --- /dev/null +++ b/app/airq/static/home.css @@ -0,0 +1,124 @@ +html * { + font-family: 'Montserrat', sans-serif; + font-size: 62.5%; +} + +/* Navbar */ +.navbar { + background-color: white; + border-bottom: solid; +} + +.navbar-brand { + font-size: 2rem !important; +} + +.nav-item { + padding-left: 0.5rem; + font-size: 1.6rem; +} + +/* Main page */ +.section { + font-size: 1.6rem; + text-align: center; + margin: 1rem; + padding-bottom: 1rem; +} + +.section-title { + font-size: 2rem; +} + +a.section-title { + text-decoration: none !important; + color: inherit !important; +} + +.section-text { + font-size: 1rem; +} + +.section-list { + text-align: left; + font-size: 1rem; +} + +.intro-list { + padding-inline-start: 0px; +} + +.intro-text { + display: block; + list-style: none; + font-size: 2rem; +} + +.sms-button { + margin-top: 1rem; + margin-bottom: 2rem; + font-size: 1.6rem; +} + +/* how-to map */ +#map { + width: 400px; + height: 300px; + margin: 0.5rem; +} + +.geocoder-dropdown-text { + font-size: 1rem; +} + +.marker { + background-image: url('icon.svg'); + width: 10px; + height: 10px; + } + +#footer { + position: fixed; + left: 0; + bottom: 0; + width: 100%; +} + +@media (min-width: 576px) { + html { + font-size: 62.5%; + } + + .nav-item { + visibility: hidden; + } + + .section { + border-bottom: none; + } + + .section-title { + font-size: 3rem; + } + + .section-text { + font-size: 1.6rem; + } + + .section-list { + font-size: 1.6rem; + } + + .intro-text { + font-size: 3rem; + } + + .sms-button { + font-size: 2rem !important; + } + + #map { + width: 500px; + height: 400px; + } +} \ No newline at end of file diff --git a/app/airq/static/icon.svg b/app/airq/static/icon.svg new file mode 100644 index 0000000..c3efe8e --- /dev/null +++ b/app/airq/static/icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/airq/static/map.js b/app/airq/static/map.js new file mode 100644 index 0000000..f5faa70 --- /dev/null +++ b/app/airq/static/map.js @@ -0,0 +1,86 @@ +mapboxgl.accessToken = 'pk.eyJ1IjoiZTNrbHVuZCIsImEiOiJja2c1bDJ5ZW8wYnp4MnNuenRwc3diZ2w4In0.7pXGkCpKkhFHDm3TMwjgAg'; +var map = new mapboxgl.Map({ + container: 'map', + style: 'mapbox://styles/mapbox/streets-v11', + center: [-122.25948, 37.87221], + zoom: 10 +}); + +map.addControl( + new MapboxGeocoder({ + accessToken: mapboxgl.accessToken, + render: function (item) { + return ( + "
" + "" + item.place_name + '
' + ); + }, + mapboxgl: mapboxgl, + placeholder: 'Enter a zipcode' + }) +); + +// After the map style has loaded on the page, +// add a source layer and default styling for a single point +map.on('load', function() { + map.addSource('single-point', { + type: 'geojson', + data: { + type: 'FeatureCollection', + features: [] + } + }); + + map.addLayer({ + id: 'point', + source: 'single-point', + type: 'circle', + paint: { + 'circle-radius': 9, + 'circle-color': '#448ee4' + } + }); + +}); + +// static lat/long for the markers in development +var geojson = { + type: 'FeatureCollection', + features: [{ + type: 'Feature', + geometry: { + type: 'Point', + coordinates: ['',''] + } + }] +}; + + +const sensors = JSON.parse({{points|tojson}}); + +for (var key in sensors) { + if (sensors.hasOwnProperty(key)) { + feature = {} + const lnglat = [sensors[key]["longitude"], sensors[key]["latitude"]] + feature['type'] = 'Feature' + feature['geometry'] = + { + 'type': 'Point', + 'coordinates': lnglat + } + geojson.features.push(feature) + } +} +console.log(geojson) + +// add markers to map +geojson.features.forEach(function(marker) { + + // create a HTML element for each feature + var el = document.createElement('div'); + el.className = 'marker'; + + // make a marker for each feature and add to the map + new mapboxgl.Marker(el) + .setLngLat(marker.geometry.coordinates) + .addTo(map); +}); \ No newline at end of file diff --git a/app/airq/templates/admin.html b/app/airq/templates/admin/admin.html similarity index 98% rename from app/airq/templates/admin.html rename to app/airq/templates/admin/admin.html index b12b0d8..c4483e7 100644 --- a/app/airq/templates/admin.html +++ b/app/airq/templates/admin/admin.html @@ -1,4 +1,4 @@ -{% extends 'base.html' %} +{% extends 'admin/base.html' %} {% block header %}

{% block title %}Stats{% endblock %}

diff --git a/app/airq/templates/base.html b/app/airq/templates/admin/base.html similarity index 100% rename from app/airq/templates/base.html rename to app/airq/templates/admin/base.html diff --git a/app/airq/templates/bulk_sms.html b/app/airq/templates/admin/bulk_sms.html similarity index 99% rename from app/airq/templates/bulk_sms.html rename to app/airq/templates/admin/bulk_sms.html index d1facb2..9a33fc6 100644 --- a/app/airq/templates/bulk_sms.html +++ b/app/airq/templates/admin/bulk_sms.html @@ -1,4 +1,4 @@ -{% extends 'base.html' %} +{% extends 'admin/base.html' %} {% block head %}