diff --git a/database.py b/database.py index 44e9e7aa64e4a5bd0a0f6c0964372d8af6a7bcb9..298894d9ecee62ba3794fbe03767b930ca2d7a06 100644 --- a/database.py +++ b/database.py @@ -1,6 +1,6 @@ import os import sqlite3 -from typing import List +from typing import List, Tuple DATABASE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "database.db") @@ -50,8 +50,25 @@ with Connection() as conn: url text NOT NULL, workspace_id integer NOT NULL, FOREIGN KEY (workspace_id) REFERENCES Workspaces(id))''') + conn.execute('''CREATE TABLE IF NOT EXISTS AddressToLatLong ( + id integer NOT NULL PRIMARY KEY AUTOINCREMENT, + address text NOT NULL, + latlong text NOT NULL)''') conn.commit() +def lookup_address(address: str): + with Connection() as conn: + conn.execute("SELECT latlong FROM AddressToLatLong WHERE address = ?", (address,)) + res = conn.cursor.fetchone()[0] + (lat, long) = res.split(",") + return (float(lat), float(long)) + +def set_address_latlong(address: str, latlong: Tuple[float, float]): + with Connection() as conn: + latlong_str = f"{ repr(latlong[0]) },{ repr(latlong[1]) }" + conn.execute("INSERT INTO AddressToLatLong (address, latlong) VALUES (?, ?)", (address, latlong_str)) + conn.commit() + class Workspace: def __init__(self, name: str, address: str, main_photo: str, additional_photos: List[str], description: str, @@ -68,6 +85,31 @@ class Workspace: self.opening_hours = opening_hours self.checkin_instructions = checkin_instructions + def validate(self): + errors = [] + if len(self.name.strip()) <= 0: + errors.append("Name must not be empty") + if len(self.address.strip()) <= 0: + errors.append("Address must not be empty") + if len(self.main_photo.strip()) <= 0: + errors.append("Main photo must not be empty") + if len(self.description.strip()) <= 0: + errors.append("Description must not be empty") + if len(self.website.strip()) <= 0: + errors.append("Website must not be empty") + if len(self.email.strip()) <= 0: + errors.append("Email must not be empty") + if len(self.phone_number.strip()) <= 0: + errors.append("Phone number must not be empty") + if len(self.opening_hours.strip()) <= 0: + errors.append("Opening hours must not be empty") + + for x in self.additional_photos: + if len(x.strip()) <= 0: + errors.append("Each edditional photos must not be empty") + + return errors + def from_query(conn, tuple): (id, name, address, main_photo, description, website, email, phone_number, opening_hours, checkin_instructions) = tuple additional_photos = [] @@ -96,5 +138,3 @@ def get_workspaces(): with Connection() as conn: conn.execute("SELECT * FROM Workspaces") return [Workspace.from_query(conn, x) for x in conn.cursor.fetchall()] - -print(get_workspaces()) \ No newline at end of file diff --git a/import_database.py b/import_database.py index deceed40a826f436a7db3a2546563523db90b263..e5a0e235ee5cc29abc3b0cf5d8d7215f7abc555f 100644 --- a/import_database.py +++ b/import_database.py @@ -1,14 +1,32 @@ import database +import os +import json -print(database.add_workspace(database.Workspace( - "CodeBase", - "CodeBase Edinburgh, 37a Castle Terrace, Edinburgh, EH1 2EL", - "https://images.squarespace-cdn.com/content/v1/55439320e4b0f92b5d6c4c8b/1505921023376-PAHUDHVOOKIYF4XQPHOO/5951229048_3e3d50fcb1_o.jpg?format=750w", - ["https://images.squarespace-cdn.com/content/v1/55439320e4b0f92b5d6c4c8b/1636387943314-W9JWMS6ZX4DEZSPUV7T0/Copy+of+Unfiltered_square+%281%29.png?format=500w"], - "We've been exploring the world of startups...", - "https://www.thisiscodebase.com", - "hannah@thisiscodebase.com", - "+44 131 560 2003", - "Monday - Friday, 9am - 5pm", - "Tramsched members should contact Hannah using the email address listed." -))) +def import_workspace(data): + workspace = database.Workspace( + data["name"], + data["address"], + data["main_photo"], + data["additional_photos"], + data["description"], + data["website"], + data["email"], + data["phone_number"], + data["opening_hours"], + data["checkin_instructions"] + ) + + latlong = data["latlong"] + if latlong != None: + database.set_address_latlong(workspace.address, (latlong[0], latlong[1])) + + database.add_workspace(workspace) + +file = os.path.join(os.path.dirname(os.path.abspath(__file__)), "workspaces.json") + +with open(file, "r", encoding="utf8") as stream: + data = json.load(stream) + for workspace in data: + import_workspace(workspace) + + print(f"Imported {len(data)} workspace(s)") diff --git a/main.py b/main.py index dc32f70e33060652797c6da10c685ca13c1bac44..03362b52eb47bbf559c48682ecc3126ab02c34b8 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,7 @@ from flask import Flask, request, render_template import database +import json +import re app = Flask(__name__) @@ -9,5 +11,54 @@ def home(): workspaces = database.get_workspaces() return render_template("home.html", workspaces = workspaces) +@app.route("/map", methods = ["GET"]) +def map(): + if request.method == "GET": + workspaces = database.get_workspaces() + data = [{"id": x.id, "name": x.name, "address": x.address, "phoneNumber": x.phone_number, "email": x.email, "website": x.website, "latlong": database.lookup_address(x.address)} for x in workspaces] + dumped = json.dumps(data) + return render_template("map.html", json = re.sub(r"(?i)\</script\>", "<\/script>", dumped)) # escape </script> + +@app.route("/admin/add-workspace", methods = ["GET", "POST"]) +def add_workspace(): + if request.method == "GET": + return render_template("add-workspace.html") + + if request.method == "POST": + additional_photos = request.form["additional_photos"] + additional_photos = [x.strip() for x in additional_photos.split("\n")] if len(additional_photos) > 0 else [] + workspace = database.Workspace( + request.form["name"], + request.form["address"], + request.form["main_photo"], + additional_photos, + request.form["description"], + request.form["website"], + request.form["email"], + request.form["phone_number"], + request.form["opening_hours"], + request.form["checkin_instructions"] + ) + + errors = workspace.validate() + try: + latitude = float(request.form["latitude"]) + except ValueError: + errors.append("Latitude must be a number") + try: + longitude = float(request.form["longitude"]) + except ValueError: + errors.append("Longitude must be a number") + + if len(errors) > 0: + messages = ["Errors were found:"] + messages.extend(errors) + else: + database.set_address_latlong(workspace.address, (latitude, longitude)) + database.add_workspace(workspace) + messages = ["Workspace added successfully"] + + return render_template("add-workspace-result.html", messages = messages) + if __name__ == "__main__": app.run(debug = True) diff --git a/static/fonts/CircularXX-Bold.woff b/static/fonts/CircularXX-Bold.woff new file mode 100644 index 0000000000000000000000000000000000000000..363c73f37720a9c6aa80c0b6523f4ceae4195d34 Binary files /dev/null and b/static/fonts/CircularXX-Bold.woff differ diff --git a/static/fonts/CircularXX-Bold.woff2 b/static/fonts/CircularXX-Bold.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..93ece32448b60caa683680347ae2ce656a7f4b4a Binary files /dev/null and b/static/fonts/CircularXX-Bold.woff2 differ diff --git a/static/fonts/CircularXX-Book.woff b/static/fonts/CircularXX-Book.woff new file mode 100644 index 0000000000000000000000000000000000000000..114a921b73659433cb184da36f82241ed3412c84 Binary files /dev/null and b/static/fonts/CircularXX-Book.woff differ diff --git a/static/fonts/CircularXX-Book.woff2 b/static/fonts/CircularXX-Book.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..ddf961385466cbe561073b1c3f55106b1fccadf0 Binary files /dev/null and b/static/fonts/CircularXX-Book.woff2 differ diff --git a/static/fonts/CircularXX-Medium.woff b/static/fonts/CircularXX-Medium.woff new file mode 100644 index 0000000000000000000000000000000000000000..76031ed22d7736b5b634d33327c00c3ec2f803c9 Binary files /dev/null and b/static/fonts/CircularXX-Medium.woff differ diff --git a/static/fonts/CircularXX-Medium.woff2 b/static/fonts/CircularXX-Medium.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..c188c2464203863c5dad9f7f38d50a64b82c8dee Binary files /dev/null and b/static/fonts/CircularXX-Medium.woff2 differ diff --git a/static/scripts/map.js b/static/scripts/map.js new file mode 100644 index 0000000000000000000000000000000000000000..b914908779ccf53be1a13621990d7dc31d58ebda --- /dev/null +++ b/static/scripts/map.js @@ -0,0 +1,27 @@ +var map = L.map("map").setView([54.004, -2.55], 5); + +L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { + attribution: "© <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> contributors" +}).addTo(map); + +for (let workspace of mapData) { + L.marker(workspace.latlong).addTo(map) + .bindPopup(`<div class="popup"><div><a href="/workspace/${ encodeURIComponent(workspace.id) }" class="popup-title">${ + escapeHTML(workspace.name) + }</a></div><div>Phone: <a href="tel:${ escapeHTML(workspace.phoneNumber) }">${ + escapeHTML(workspace.phoneNumber) + }</a></div><div>Email: <a href="mailto:${ escapeHTML(workspace.email) }">${ + escapeHTML(workspace.email) + }</a></div><div>Website: <a href="${ escapeHTML(workspace.website) }" target="_blank">${ + escapeHTML(workspace.website) + }</a></div><div>${ escapeHTML(workspace.address) }</div></div>`); +} + +function escapeHTML(html) { + let lookup = Object.create(null); + lookup["<"] = "<" + lookup[">"] = ">" + lookup["\""] = """ + lookup["&"] = "&"; + return html.replace(/[<>"&]/g, v => lookup[v] || ""); +} \ No newline at end of file diff --git a/static/style.css b/static/style.css index 640879bec84c1a5f8b9b2c04f308c0a357fdc125..099283c39bd291ca3fb9935f599ae1db27515911 100644 --- a/static/style.css +++ b/static/style.css @@ -1,3 +1,40 @@ +/* fonts obtained from and used by https://members.tramshedtech.co.uk/ */ + +@font-face { + font-family: CircularXX; + src: url(/static/fonts/CircularXX-Book.woff2) format("woff2"); + src: url(/static/fonts/CircularXX-Book.woff) format("woff"); + font-weight: 400; + font-style: normal +} + +@font-face { + font-family: CircularXX; + src: url(/static/fonts/CircularXX-Medium.woff2) format("woff2"); + src: url(/static/fonts/CircularXX-Medium.woff) format("woff"); + font-weight: 500; + font-style: normal +} + +@font-face { + font-family: CircularXX; + src: url(/static/fonts/CircularXX-Bold.woff2) format("woff2"); + src: url(/static/fonts/CircularXX-Bold.woff) format("woff"); + font-weight: 700; + font-style: normal +} + +body { + margin: 0; + font-family: CircularXX,sans-serif; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: #111; + text-align: left; + background-color: #f5f5f5 +} + h1 { color: blue;} @@ -50,3 +87,60 @@ nav li a:hover:last-child { background-color: #111; } + +.workspace-item { + background-color: #fff; + border-radius: 0.5rem; + box-shadow: 0 2px 2px 0 rgb(17 17 17 / 5%); + padding: 1.5rem; + margin: 0.75rem; + display: flex; +} + +.workspace-item .image-col { + width: 200px; +} + +.workspace-item .image-col .image { + max-width: 100%; +} + +.workspace-item .main-col { + flex: 1 0; + margin-left: 1rem; +} + +.workspace-item .title { + color: inherit; + display: inline-block; + font-size: 1.25rem; + margin-bottom: 1rem; + font-weight: 600; + line-height: 1.2; +} + +.workspace-item .meta { + color: #666; +} + +.workspace-item .dot { + color: #666; + font-weight: 700; +} + +#map { + height: 600px; +} + +#map .popup { + font-family: CircularXX,sans-serif; + font-size: 0.8rem; + font-weight: 400; + line-height: 1.5; +} + +#map .popup-title { + color: inherit; + font-size: 1rem; + font-weight: 500; +} diff --git a/templates/add-workspace-result.html b/templates/add-workspace-result.html new file mode 100644 index 0000000000000000000000000000000000000000..8695077fbfe71aff340eb153dffb015776bc3966 --- /dev/null +++ b/templates/add-workspace-result.html @@ -0,0 +1,7 @@ +{% extends "main.html" %} +{% block title %}Add Workspace{% endblock %} +{% block mainBlock %} +{% for message in messages %} +<div>{{ message }}</div> +{% endfor %} +{% endblock %} \ No newline at end of file diff --git a/templates/add-workspace.html b/templates/add-workspace.html new file mode 100644 index 0000000000000000000000000000000000000000..160593e5fb1b5a0b39201471bf287259cf612cfb --- /dev/null +++ b/templates/add-workspace.html @@ -0,0 +1,57 @@ +{% extends "main.html" %} +{% block title %}Add Workspace{% endblock %} +{% block mainBlock %} +<form action="/admin/add-workspace" method="POST"> + <table> + <tr> + <td><label for="name">Name:</label></td> + <td><input id="name" type="text" name="name"></td> + </tr> + <tr> + <td><label for="name">Address:</label></td> + <td><input id="address" type="text" name="address"></td> + </tr> + <tr> + <td><label for="main-photo">Main Photo:</label></td> + <td><input id="main-photo" type="text" name="main_photo"></td> + </tr> + <tr> + <td><label for="additional-photos">Additional Photos:</label></td> + <td><div><textarea id="additional-photos" name="additional_photos"></textarea></div></td> + </tr> + <tr> + <td><label for="description">Description:</label></td> + <td><div><textarea id="description" name="description"></textarea></div></td> + </tr> + <tr> + <td><label for="website">Website:</label></td> + <td><input id="website" type="text" name="website"></td> + </tr> + <tr> + <td><label for="email">Email:</label></td> + <td><input id="email" type="text" name="email"></td> + </tr> + <tr> + <td><label for="phone-number">Phone Number:</label></td> + <td><input id="phone-number" type="text" name="phone_number"></td> + </tr> + <tr> + <td><label for="opening-hours">Opening Hours:</label></td> + <td><input id="opening-hours" type="text" name="opening_hours"></td> + </tr> + <tr> + <td><label for="checkin-instructions">Check-in Instructions:</label></td> + <td><div><textarea id="checkin-instructions" name="checkin_instructions"></textarea></div></td> + </tr> + <tr> + <td><label for="latitude">Latitude:</label></td> + <td><input id="latitude" type="text" name="latitude"></td> + </tr> + <tr> + <td><label for="longitude">Longitude:</label></td> + <td><input id="longitude" type="text" name="longitude"></td> + </tr> + </table> + <input type="submit"> +</form> +{% endblock %} \ No newline at end of file diff --git a/templates/home.html b/templates/home.html index 9a1af7ec00074ae49b10865085a71494a7d8b8d3..d0e1565d3f9eb62d8cee21967f719837f8a44147 100644 --- a/templates/home.html +++ b/templates/home.html @@ -3,16 +3,23 @@ {% block mainBlock %} <ul> {% for workspace in workspaces %} - <li><a href="{{ workspace.website }}">{{ workspace.name }}</a> - <img src="{{ workspace.main_photo }}"></li> - <ul> - <li>{{ workspace.address }}</li> - <li>{{ workspace.description }}</li> - <li>{{ workspace.email }}</li> - <li>{{ workspace.phone_number }}</li> - <li>{{ workspace.opening_hours }}</li> - <li>{{ workspace.checkin_instructions }}</li> - </ul> + <div class="workspace-item"> + <div class="image-col"> + <a href="/workspace/{{ workspace.id }}"><img class="image" src="{{ workspace.main_photo }}"></a> + </div> + <div class="main-col"> + <div><a href="/workspace/{{ workspace.id }}" class="title">{{ workspace.name }}</a></div> + <div> + <span class="meta"><a href="mailto:{{ workspace.email }}">{{ workspace.email }}</a></li> <span class="dot">·</span> + <span class="meta"><a href="tel:{{ workspace.phone_number }}">{{ workspace.phone_number }}</a></span> <span class="dot">·</span> + <span class="meta"><a href="{{ workspace.website }}" target="_blank">{{ workspace.website }}</a></span> + </div> + <div> + <span class="meta">{{ workspace.address }}</span> <span class="dot">·</span> + <span class="meta">{{ workspace.opening_hours }}</span> + </div> + </div> + </div> {% endfor %} </ul> {% endblock %} \ No newline at end of file diff --git a/templates/main.html b/templates/main.html index 34903741b4d4bcc011c8909b6474a4f16a9e6add..4557b91d74188f5b7b04fecb70fa3f43c18c51cc 100644 --- a/templates/main.html +++ b/templates/main.html @@ -2,8 +2,6 @@ <html lang="en"> <head> - <link rel="stylesheet" href="static/style.css"> - <!-- Bootstrap CSS --> <link rel="stylesheet" href= "https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" @@ -11,19 +9,21 @@ "sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> + <link rel="stylesheet" href="/static/style.css"> + <title>{% block title %}{% endblock %} - Tramshed Workspaces</title> </head> <div class="parallax"> <body class="body"> - <img src="static/Tramshed_Logo.jpg" alt="Logo" height="50" width="100"> + <img src="/static/Tramshed_Logo.jpg" alt="Logo" height="50" width="100"> <!-- navigation --> <nav> <ul> - <li><a href="#home">Home</a></li> - <li><a href="#Map">Map</a></li> - <li><a href="#contactUs">Contact Us</a></li> - <li style="float:right"><a class="active" href="#about">About</a></li> + <li><a href="/">Home</a></li> + <li><a href="/map">Map</a></li> + <li><a href="/contact-us">Contact Us</a></li> + <li style="float:right"><a class="active" href="/about">About</a></li> </ul> </nav> {% block mainBlock %}{% endblock %} diff --git a/templates/map.html b/templates/map.html new file mode 100644 index 0000000000000000000000000000000000000000..510eda426f53213e3922df5fec0070c33fecbb0e --- /dev/null +++ b/templates/map.html @@ -0,0 +1,14 @@ +{% extends "main.html" %} +{% block title %}Map{% endblock %} +{% block mainBlock %} +<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" + integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A==" + crossorigin=""/> +<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js" + integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA==" + crossorigin=""></script> +<noscript>JavaScript must be supported and enabled in the browser to support interactive maps!</noscript> +<script>var mapData = {{ json|safe }};</script> +<div id="map"></div> +<script src="/static/scripts/map.js"></script> +{% endblock %} \ No newline at end of file diff --git a/workspaces.json b/workspaces.json new file mode 100644 index 0000000000000000000000000000000000000000..2d7bd8d739332d2c6cfd3f996668e5d1b553bb4f --- /dev/null +++ b/workspaces.json @@ -0,0 +1,13 @@ +[{ + "name": "CodeBase", + "address": "CodeBase Edinburgh, 37a Castle Terrace, Edinburgh, EH1 2EL", + "main_photo": "https://images.squarespace-cdn.com/content/v1/55439320e4b0f92b5d6c4c8b/1505921023376-PAHUDHVOOKIYF4XQPHOO/5951229048_3e3d50fcb1_o.jpg?format=750w", + "additional_photos": ["https://images.squarespace-cdn.com/content/v1/55439320e4b0f92b5d6c4c8b/1636387943314-W9JWMS6ZX4DEZSPUV7T0/Copy+of+Unfiltered_square+%281%29.png?format=500w"], + "description": "We've been exploring the world of startups...", + "website": "https://www.thisiscodebase.com", + "email": "hannah@thisiscodebase.com", + "phone_number": "+44 131 560 2003", + "opening_hours": "Monday - Friday, 9am - 5pm", + "checkin_instructions": "Tramsched members should contact Hannah using the email address listed.", + "latlong": [55.9471623, -3.203928] +}] \ No newline at end of file