Containerization basics
@ -1,30 +0,0 @@
|
||||
from flask import Flask, render_template
|
||||
import os
|
||||
import random
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# list of cat images
|
||||
images = [
|
||||
"https://firebasestorage.googleapis.com/v0/b/docker-curriculum.appspot.com/o/catnip%2F0.gif?alt=media&token=0fff4b31-b3d8-44fb-be39-723f040e57fb",
|
||||
"https://firebasestorage.googleapis.com/v0/b/docker-curriculum.appspot.com/o/catnip%2F1.gif?alt=media&token=2328c855-572f-4a10-af8c-23a6e1db574c",
|
||||
"https://firebasestorage.googleapis.com/v0/b/docker-curriculum.appspot.com/o/catnip%2F10.gif?alt=media&token=647fd422-c8d1-4879-af3e-fea695da79b2",
|
||||
"https://firebasestorage.googleapis.com/v0/b/docker-curriculum.appspot.com/o/catnip%2F11.gif?alt=media&token=900cce1f-55c0-4e02-80c6-ee587d1e9b6e",
|
||||
"https://firebasestorage.googleapis.com/v0/b/docker-curriculum.appspot.com/o/catnip%2F2.gif?alt=media&token=8a108bd4-8dfc-4dbc-9b8c-0db0e626f65b",
|
||||
"https://firebasestorage.googleapis.com/v0/b/docker-curriculum.appspot.com/o/catnip%2F3.gif?alt=media&token=4e270d85-0be3-4048-99bd-696ece8070ea",
|
||||
"https://firebasestorage.googleapis.com/v0/b/docker-curriculum.appspot.com/o/catnip%2F4.gif?alt=media&token=e7daf297-e615-4dfc-aa19-bee959204774",
|
||||
"https://firebasestorage.googleapis.com/v0/b/docker-curriculum.appspot.com/o/catnip%2F5.gif?alt=media&token=a8e472e6-94da-45f9-aab8-d51ec499e5ed",
|
||||
"https://firebasestorage.googleapis.com/v0/b/docker-curriculum.appspot.com/o/catnip%2F7.gif?alt=media&token=9e449089-9f94-4002-a92a-3e44c6bd18a9",
|
||||
"https://firebasestorage.googleapis.com/v0/b/docker-curriculum.appspot.com/o/catnip%2F8.gif?alt=media&token=80a48714-7aaa-45fa-a36b-a7653dc3292b",
|
||||
"https://firebasestorage.googleapis.com/v0/b/docker-curriculum.appspot.com/o/catnip%2F9.gif?alt=media&token=a57a1c71-a8af-4170-8fee-bfe11809f0b3",
|
||||
]
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
url = random.choice(images)
|
||||
return render_template("index.html", url=url)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 5000)))
|
@ -1 +0,0 @@
|
||||
Flask==2.0.2
|
@ -1,27 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<style type="text/css">
|
||||
body {
|
||||
background: black;
|
||||
color: white;
|
||||
}
|
||||
div.container {
|
||||
max-width: 500px;
|
||||
margin: 100px auto;
|
||||
border: 20px solid white;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
h4 {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h4>Cat Gif of the day</h4>
|
||||
<img src="{{url}}" />
|
||||
<p><small>Courtesy: <a href="http://www.buzzfeed.com/copyranter/the-best-cat-gif-post-in-the-history-of-cat-gifs">Buzzfeed</a></small></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,33 @@
|
||||
from flask import Flask, render_template, make_response, send_file
|
||||
import os
|
||||
import random
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
req_count = 0
|
||||
|
||||
static_dir = os.path.join(os.path.dirname(__file__), 'static/')
|
||||
|
||||
@app.route('/static/<path:path>')
|
||||
def static_file(path):
|
||||
return send_file(os.path.join(static_dir, path))
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
global req_count
|
||||
cat_files = os.listdir(os.path.join(static_dir, 'img/'))
|
||||
file = random.choice(cat_files)
|
||||
is_stoner = os.path.basename(file) == 'stoner_cat.gif'
|
||||
url = '/static/img/' + file
|
||||
req_count += 1
|
||||
return render_template("index.html", url=url, is_stoner=is_stoner)
|
||||
|
||||
@app.route("/request_count")
|
||||
def request_count():
|
||||
response = make_response(str(req_count), 200)
|
||||
response.mimetype = "text/plain"
|
||||
return response
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.config['TEMPLATES_AUTO_RELOAD'] = True
|
||||
app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 5000)))
|
@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
pip install -r ./requirements.txt
|
@ -0,0 +1,51 @@
|
||||
user nginx;
|
||||
worker_processes auto;
|
||||
|
||||
error_log /var/log/nginx/error.log notice;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
sendfile on;
|
||||
#tcp_nopush on;
|
||||
|
||||
#gzip on;
|
||||
|
||||
keepalive_timeout 65;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
location / {
|
||||
proxy_set_header X-Forwarded-Host $http_host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
proxy_buffers 8 64k;
|
||||
proxy_busy_buffers_size 128k;
|
||||
proxy_buffer_size 64k;
|
||||
|
||||
client_max_body_size 10m;
|
||||
|
||||
proxy_http_version 1.1;
|
||||
proxy_pass http://cats_app:5000;
|
||||
}
|
||||
|
||||
location /request_count {
|
||||
deny all;
|
||||
return 404;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
Flask==3.0.0
|
@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
python3 ./app.py
|
After Width: | Height: | Size: 372 B |
After Width: | Height: | Size: 894 B |
After Width: | Height: | Size: 2.0 MiB |
After Width: | Height: | Size: 6.1 MiB |
After Width: | Height: | Size: 1.6 MiB |
After Width: | Height: | Size: 124 KiB |
After Width: | Height: | Size: 31 MiB |
After Width: | Height: | Size: 6.6 MiB |
After Width: | Height: | Size: 6.5 MiB |
@ -0,0 +1,77 @@
|
||||
<!DOCTYPE HTML>
|
||||
|
||||
<html lang="en" class="h-100">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Cats</title>
|
||||
<link rel="icon" type="image/png" href="/static/favicon.png">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
|
||||
crossorigin="anonymous"></script>
|
||||
<style lang="text/css">
|
||||
.footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.stoner-video {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.stoner-wave {
|
||||
animation: wave 5s cubic-bezier(0.36, 0.45, 0.63, 0.53) infinite;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
|
||||
@keyframes wave {
|
||||
0% {
|
||||
transform: rotate(-20deg);
|
||||
background-color: greenyellow;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(20deg);
|
||||
background-color: fuchsia;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="h-100">
|
||||
{% if is_stoner %}
|
||||
<video autoplay loop class="stoner-video">
|
||||
<source src="/static/vid/stoner_cat.mp4" type="video/mp4" />
|
||||
</video>
|
||||
{% endif %}
|
||||
<div class="h-100 d-flex flex-row align-items-center">
|
||||
<div class="container">
|
||||
<div class="row p-3 justify-content-center">
|
||||
<div class="col col-lg-4">
|
||||
<div class="card {{ 'stoner-wave' if is_stoner }}">
|
||||
<img src="{{url}}" class="card-img-top" alt="cat">
|
||||
<div class="card-body">
|
||||
<button id="#next-button" class="btn btn-primary {{ 'bg-black' if is_stoner }}"
|
||||
onclick="location.reload()">
|
||||
<img src="/static/icons/icon-paw-32.png" alt="paw" />
|
||||
Gimme another cat!
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="footer p-1">
|
||||
<span>Icons by <a href="https://icons8.com">icons8.com</a></span>
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,14 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# build the flask container
|
||||
docker build -t studX/foodtrucks-web ./
|
||||
|
||||
# create the network
|
||||
docker network create foodtrucks-network
|
||||
|
||||
# start the ES container, specify password beforehand
|
||||
docker run -d --net foodtrucks-network --name elastic -e ELASTIC_PASSWORD=v7SLsbtXticPLADei5vS elasticsearch:8.4.3
|
||||
|
||||
# start the flask app container
|
||||
# point to elastic host: https://elastic:9200 user: elastic password: v7SLsbtXticPLADei5vS
|
||||
docker run -d -p 80:5000 --net foodtrucks-network studX/foodtrucks-web https elastic 9200 elastic v7SLsbtXticPLADei5vS
|
@ -1,13 +0,0 @@
|
||||
version: "3"
|
||||
services:
|
||||
elastic:
|
||||
image: elasticsearch:8.4.3
|
||||
environment:
|
||||
- ELASTIC_PASSWORD=v7SLsbtXticPLADei5vS
|
||||
foodtrucks-web:
|
||||
image: stud15/foodtrucks-web
|
||||
command: https elastic 9200 elastic v7SLsbtXticPLADei5vS
|
||||
depends_on:
|
||||
- elastic
|
||||
ports:
|
||||
- 80:5000
|
@ -1,3 +0,0 @@
|
||||
{
|
||||
"presets": ["env"]
|
||||
}
|
@ -1,135 +0,0 @@
|
||||
from elasticsearch import Elasticsearch, exceptions
|
||||
import os
|
||||
import time
|
||||
from flask import Flask, jsonify, request, render_template
|
||||
import sys
|
||||
import requests
|
||||
|
||||
es_scheme = sys.argv[1]
|
||||
es_host = sys.argv[2]
|
||||
es_port = int(sys.argv[3])
|
||||
es_user = sys.argv[4]
|
||||
es_password = sys.argv[5]
|
||||
connection_string = f"{es_scheme}://{es_host}:{es_port}"
|
||||
|
||||
print(f"Elastic server: {es_scheme}://{es_host}:{es_port}, auth: {es_user}:{es_password}")
|
||||
|
||||
es = Elasticsearch(
|
||||
connection_string,
|
||||
basic_auth=(es_user, es_password),
|
||||
verify_certs=False
|
||||
)
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
def load_data_in_es():
|
||||
""" creates an index in elasticsearch """
|
||||
url = "http://data.sfgov.org/resource/rqzj-sfat.json"
|
||||
r = requests.get(url)
|
||||
data = r.json()
|
||||
print("Loading data in elasticsearch ...")
|
||||
for id, truck in enumerate(data):
|
||||
res = es.index(index="sfdata", id=id, body=truck)
|
||||
print("Total trucks loaded: ", len(data))
|
||||
|
||||
def safe_check_index(index, retry=60):
|
||||
""" connect to ES with retry """
|
||||
if not retry:
|
||||
print("Out of retries. Bailing out...")
|
||||
sys.exit(1)
|
||||
try:
|
||||
status = es.indices.exists(index=index)
|
||||
return status
|
||||
except exceptions.ConnectionError as e:
|
||||
print(f"Unable to connect to {connection_string}. Retrying in 5 secs...", flush=True)
|
||||
time.sleep(5)
|
||||
safe_check_index(index, retry-1)
|
||||
|
||||
def format_fooditems(string):
|
||||
items = [x.strip().lower() for x in string.split(":")]
|
||||
return items[1:] if items[0].find("cold truck") > -1 else items
|
||||
|
||||
def check_and_load_index():
|
||||
""" checks if index exits and loads the data accordingly """
|
||||
if not safe_check_index('sfdata'):
|
||||
print("Index not found...")
|
||||
load_data_in_es()
|
||||
|
||||
###########
|
||||
### APP ###
|
||||
###########
|
||||
@app.route('/')
|
||||
def index():
|
||||
return render_template('index.html')
|
||||
|
||||
@app.route('/debug')
|
||||
def test_es():
|
||||
resp = {}
|
||||
try:
|
||||
msg = es.cat.indices()
|
||||
resp["msg"] = msg
|
||||
resp["status"] = "success"
|
||||
except:
|
||||
resp["status"] = "failure"
|
||||
resp["msg"] = "Unable to reach ES"
|
||||
return jsonify(resp)
|
||||
|
||||
@app.route('/search')
|
||||
def search():
|
||||
key = request.args.get('q')
|
||||
if not key:
|
||||
return jsonify({
|
||||
"status": "failure",
|
||||
"msg": "Please provide a query"
|
||||
})
|
||||
try:
|
||||
res = es.search(
|
||||
index="sfdata",
|
||||
body={
|
||||
"query": {"match": {"fooditems": key}},
|
||||
"size": 750 # max document size
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
"status": "failure",
|
||||
"msg": "error in reaching elasticsearch"
|
||||
})
|
||||
# filtering results
|
||||
vendors = set([x["_source"]["applicant"] for x in res["hits"]["hits"]])
|
||||
temp = {v: [] for v in vendors}
|
||||
fooditems = {v: "" for v in vendors}
|
||||
for r in res["hits"]["hits"]:
|
||||
applicant = r["_source"]["applicant"]
|
||||
if "location" in r["_source"]:
|
||||
truck = {
|
||||
"hours" : r["_source"].get("dayshours", "NA"),
|
||||
"schedule" : r["_source"].get("schedule", "NA"),
|
||||
"address" : r["_source"].get("address", "NA"),
|
||||
"location" : r["_source"]["location"]
|
||||
}
|
||||
fooditems[applicant] = r["_source"]["fooditems"]
|
||||
temp[applicant].append(truck)
|
||||
|
||||
# building up results
|
||||
results = {"trucks": []}
|
||||
for v in temp:
|
||||
results["trucks"].append({
|
||||
"name": v,
|
||||
"fooditems": format_fooditems(fooditems[v]),
|
||||
"branches": temp[v],
|
||||
"drinks": fooditems[v].find("COLD TRUCK") > -1
|
||||
})
|
||||
hits = len(results["trucks"])
|
||||
locations = sum([len(r["branches"]) for r in results["trucks"]])
|
||||
|
||||
return jsonify({
|
||||
"trucks": results["trucks"],
|
||||
"hits": hits,
|
||||
"locations": locations,
|
||||
"status": "success"
|
||||
})
|
||||
|
||||
if __name__ == "__main__":
|
||||
ENVIRONMENT_DEBUG = os.environ.get("DEBUG", False)
|
||||
check_and_load_index()
|
||||
app.run(host='0.0.0.0', port=5000, debug=ENVIRONMENT_DEBUG)
|
@ -1,26 +0,0 @@
|
||||
{
|
||||
"name": "sf-food",
|
||||
"version": "0.0.1",
|
||||
"description": "SF food app",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "webpack --progress --colors --watch",
|
||||
"build": "NODE_ENV='production' webpack -p",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "Prakhar Srivastav",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"superagent": "^5.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-core": "^6.3.26",
|
||||
"babel-loader": "^6.2.0",
|
||||
"babel-preset-env": "^1.7.0",
|
||||
"babel-preset-es2015": "^6.3.13",
|
||||
"babel-preset-react": "^6.3.13",
|
||||
"webpack": "^1.12.9"
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
elasticsearch===8.4.3
|
||||
Flask==2.1.0
|
||||
requests==2.23.0
|
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 8.2 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 6.6 KiB |
Before Width: | Height: | Size: 8.2 KiB |
Before Width: | Height: | Size: 8.8 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 11 KiB |
@ -1,2 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig><msapplication><tile><square70x70logo src="/ms-icon-70x70.png"/><square150x150logo src="/ms-icon-150x150.png"/><square310x310logo src="/ms-icon-310x310.png"/><TileColor>#ffffff</TileColor></tile></msapplication></browserconfig>
|
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 1.1 KiB |
@ -1,41 +0,0 @@
|
||||
{
|
||||
"name": "App",
|
||||
"icons": [
|
||||
{
|
||||
"src": "\/android-icon-36x36.png",
|
||||
"sizes": "36x36",
|
||||
"type": "image\/png",
|
||||
"density": "0.75"
|
||||
},
|
||||
{
|
||||
"src": "\/android-icon-48x48.png",
|
||||
"sizes": "48x48",
|
||||
"type": "image\/png",
|
||||
"density": "1.0"
|
||||
},
|
||||
{
|
||||
"src": "\/android-icon-72x72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image\/png",
|
||||
"density": "1.5"
|
||||
},
|
||||
{
|
||||
"src": "\/android-icon-96x96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image\/png",
|
||||
"density": "2.0"
|
||||
},
|
||||
{
|
||||
"src": "\/android-icon-144x144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image\/png",
|
||||
"density": "3.0"
|
||||
},
|
||||
{
|
||||
"src": "\/android-icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image\/png",
|
||||
"density": "4.0"
|
||||
}
|
||||
]
|
||||
}
|
Before Width: | Height: | Size: 8.2 KiB |
Before Width: | Height: | Size: 8.5 KiB |
Before Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 3.8 KiB |
@ -1,50 +0,0 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import Sidebar from "./components/Sidebar";
|
||||
|
||||
// setting up mapbox
|
||||
mapboxgl.accessToken =
|
||||
"pk.eyJ1IjoicHJha2hhciIsImEiOiJjaWZlbzQ1M2I3Nmt2cnhrbnlxcTQyN3VkIn0.uOaUAUqN2VS7dC7XKS0KkQ";
|
||||
|
||||
var map = new mapboxgl.Map({
|
||||
container: "map",
|
||||
style: "mapbox://styles/prakhar/cij2cpsn1004p8ykqqir34jm8",
|
||||
center: [-122.44, 37.77],
|
||||
zoom: 12,
|
||||
});
|
||||
|
||||
ReactDOM.render(<Sidebar map={map} />, document.getElementById("sidebar"));
|
||||
|
||||
function formatHTMLforMarker(props) {
|
||||
var { name, hours, address } = props;
|
||||
var html =
|
||||
'<div class="marker-title">' +
|
||||
name +
|
||||
"</div>" +
|
||||
"<h4>Operating Hours</h4>" +
|
||||
"<span>" +
|
||||
hours +
|
||||
"</span>" +
|
||||
"<h4>Address</h4>" +
|
||||
"<span>" +
|
||||
address +
|
||||
"</span>";
|
||||
return html;
|
||||
}
|
||||
|
||||
// setup popup display on the marker
|
||||
map.on("click", function (e) {
|
||||
var features = map.queryRenderedFeatures(
|
||||
e.point,
|
||||
{ layers: ['trucks', 'trucks-highlight'], radius: 10, includeGeometry: true }
|
||||
);
|
||||
|
||||
if (!features.length) return;
|
||||
|
||||
var feature = features[0];
|
||||
|
||||
new mapboxgl.Popup()
|
||||
.setLngLat(feature.geometry.coordinates)
|
||||
.setHTML(formatHTMLforMarker(feature.properties))
|
||||
.addTo(map);
|
||||
});
|
@ -1,36 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
export default function Intro() {
|
||||
return (
|
||||
<div className="intro">
|
||||
<h3>About</h3>
|
||||
<p>
|
||||
This is a fun application built to accompany the{" "}
|
||||
<a href="http://prakhar.me/docker-curriculum">docker curriculum</a> - a
|
||||
comprehensive tutorial on getting started with Docker targeted
|
||||
especially at beginners.
|
||||
</p>
|
||||
<p>
|
||||
The app is built with Flask on the backend and Elasticsearch is the
|
||||
engine powering the search.
|
||||
</p>
|
||||
<p>
|
||||
The frontend is hand-crafted with React and the beautiful maps are
|
||||
courtesy of Mapbox.
|
||||
</p>
|
||||
<p>
|
||||
If you find the design a bit ostentatious, blame{" "}
|
||||
<a href="http://genius.com/Justin-bieber-baby-lyrics">Genius</a> for
|
||||
giving me the idea of using this color scheme. If you love it, I smugly
|
||||
take all the credit. ⊂(▀¯▀⊂)
|
||||
</p>
|
||||
<p>
|
||||
Lastly, the data for the food trucks is made available in public domain
|
||||
by{" "}
|
||||
<a href="https://data.sfgov.org/Economy-and-Community/Mobile-Food-Facility-Permit/rqzj-sfat">
|
||||
SF Data
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,205 +0,0 @@
|
||||
import React from "react";
|
||||
import request from "superagent";
|
||||
import Intro from "./Intro";
|
||||
import Vendor from "./Vendor";
|
||||
|
||||
class Sidebar extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
results: [],
|
||||
query: "",
|
||||
firstLoad: true,
|
||||
};
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.handleSearch = this.handleSearch.bind(this);
|
||||
this.handleHover = this.handleHover.bind(this);
|
||||
}
|
||||
|
||||
fetchResults() {
|
||||
request.get("/search?q=" + this.state.query).end((err, res) => {
|
||||
if (err) {
|
||||
alert("error in fetching response");
|
||||
} else {
|
||||
this.setState({
|
||||
results: res.body,
|
||||
firstLoad: false,
|
||||
});
|
||||
this.plotOnMap();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
generateGeoJSON(markers) {
|
||||
return {
|
||||
type: "FeatureCollection",
|
||||
features: markers.map((p) => ({
|
||||
type: "Feature",
|
||||
properties: {
|
||||
name: p.name,
|
||||
hours: p.hours,
|
||||
address: p.address,
|
||||
"point-color": "253,237,57,1",
|
||||
},
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: [
|
||||
parseFloat(p.location.longitude),
|
||||
parseFloat(p.location.latitude),
|
||||
],
|
||||
},
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
plotOnMap(vendor) {
|
||||
const map = this.props.map;
|
||||
const results = this.state.results;
|
||||
const markers = [].concat.apply(
|
||||
[],
|
||||
results.trucks.map((t) =>
|
||||
t.branches.map((b) => ({
|
||||
location: b.location,
|
||||
name: t.name,
|
||||
schedule: b.schedule,
|
||||
hours: b.hours,
|
||||
address: b.address,
|
||||
}))
|
||||
)
|
||||
);
|
||||
var highlightMarkers, usualMarkers, usualgeoJSON, highlightgeoJSON;
|
||||
|
||||
if (vendor) {
|
||||
highlightMarkers = markers.filter(
|
||||
(m) => m.name.toLowerCase() === vendor.toLowerCase()
|
||||
);
|
||||
usualMarkers = markers.filter(
|
||||
(m) => m.name.toLowerCase() !== vendor.toLowerCase()
|
||||
);
|
||||
} else {
|
||||
usualMarkers = markers;
|
||||
}
|
||||
|
||||
usualgeoJSON = this.generateGeoJSON(usualMarkers);
|
||||
if (highlightMarkers) {
|
||||
highlightgeoJSON = this.generateGeoJSON(highlightMarkers);
|
||||
}
|
||||
// clearing layers
|
||||
if (map.getLayer("trucks")) {
|
||||
map.removeLayer("trucks");
|
||||
}
|
||||
if (map.getSource("trucks")) {
|
||||
map.removeSource("trucks");
|
||||
}
|
||||
if (map.getLayer("trucks-highlight")) {
|
||||
map.removeLayer("trucks-highlight");
|
||||
}
|
||||
if (map.getSource("trucks-highlight")) {
|
||||
map.removeSource("trucks-highlight");
|
||||
}
|
||||
|
||||
map
|
||||
.addSource("trucks", {
|
||||
type: "geojson",
|
||||
data: usualgeoJSON,
|
||||
})
|
||||
.addLayer({
|
||||
id: "trucks",
|
||||
type: "circle",
|
||||
interactive: true,
|
||||
source: "trucks",
|
||||
paint: {
|
||||
"circle-radius": 8,
|
||||
"circle-color": "rgba(253,237,57,1)",
|
||||
},
|
||||
});
|
||||
|
||||
if (highlightMarkers) {
|
||||
map
|
||||
.addSource("trucks-highlight", {
|
||||
type: "geojson",
|
||||
data: highlightgeoJSON,
|
||||
})
|
||||
.addLayer({
|
||||
id: "trucks-highlight",
|
||||
type: "circle",
|
||||
interactive: true,
|
||||
source: "trucks-highlight",
|
||||
paint: {
|
||||
"circle-radius": 8,
|
||||
"circle-color": "rgba(164,65,99,1)",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleSearch(e) {
|
||||
e.preventDefault();
|
||||
this.fetchResults();
|
||||
}
|
||||
|
||||
onChange(e) {
|
||||
this.setState({ query: e.target.value });
|
||||
}
|
||||
|
||||
handleHover(vendorName) {
|
||||
this.plotOnMap(vendorName);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.firstLoad) {
|
||||
return (
|
||||
<div>
|
||||
<div id="search-area">
|
||||
<form onSubmit={this.handleSearch}>
|
||||
<input
|
||||
type="text"
|
||||
value={this.state.query}
|
||||
onChange={this.onChange}
|
||||
placeholder="Burgers, Tacos or Wraps?"
|
||||
/>
|
||||
<button>Search!</button>
|
||||
</form>
|
||||
</div>
|
||||
<Intro />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const query = this.state.query;
|
||||
const resultsCount = this.state.results.hits || 0;
|
||||
const locationsCount = this.state.results.locations || 0;
|
||||
const results = this.state.results.trucks || [];
|
||||
const renderedResults = results.map((r, i) => (
|
||||
<Vendor key={i} data={r} handleHover={this.handleHover} />
|
||||
));
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div id="search-area">
|
||||
<form onSubmit={this.handleSearch}>
|
||||
<input
|
||||
type="text"
|
||||
value={query}
|
||||
onChange={this.onChange}
|
||||
placeholder="Burgers, Tacos or Wraps?"
|
||||
/>
|
||||
<button>Search!</button>
|
||||
</form>
|
||||
</div>
|
||||
{resultsCount > 0 ? (
|
||||
<div id="results-area">
|
||||
<h5>
|
||||
Found <span className="highlight">{resultsCount}</span> vendors in{" "}
|
||||
<span className="highlight">{locationsCount}</span> different
|
||||
locations
|
||||
</h5>
|
||||
<ul> {renderedResults} </ul>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Sidebar;
|
@ -1,70 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
export default class Vendor extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isExpanded: false,
|
||||
};
|
||||
|
||||
this.toggleExpand = this.toggleExpand.bind(this);
|
||||
}
|
||||
|
||||
formatFoodItems(items) {
|
||||
if (this.state.isExpanded) {
|
||||
return items.join(", ");
|
||||
}
|
||||
const summary = items.join(", ").substr(0, 80);
|
||||
if (summary.length > 70) {
|
||||
const indexOfLastSpace =
|
||||
summary.split("").reverse().join("").indexOf(",") + 1;
|
||||
return summary.substr(0, 80 - indexOfLastSpace) + " & more...";
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
|
||||
toggleExpand() {
|
||||
this.setState({
|
||||
isExpanded: !this.state.isExpanded,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { name, branches, fooditems, drinks } = this.props.data;
|
||||
const servesDrinks = (
|
||||
<div className="row">
|
||||
<div className="icons">
|
||||
{" "}
|
||||
<i className="ion-wineglass"></i>{" "}
|
||||
</div>
|
||||
<div className="content">Serves Cold Drinks</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<li
|
||||
onMouseEnter={this.props.handleHover.bind(null, name)}
|
||||
onClick={this.toggleExpand}
|
||||
>
|
||||
<p className="truck-name">{name}</p>
|
||||
<div className="row">
|
||||
<div className="icons">
|
||||
{" "}
|
||||
<i className="ion-android-pin"></i>{" "}
|
||||
</div>
|
||||
<div className="content"> {branches.length} locations </div>
|
||||
</div>
|
||||
{drinks ? servesDrinks : null}
|
||||
<div className="row">
|
||||
<div className="icons">
|
||||
{" "}
|
||||
<i className="ion-fork"></i> <i className="ion-spoon"></i>
|
||||
</div>
|
||||
<div className="content">
|
||||
Serves {this.formatFoodItems(fooditems)}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,202 +0,0 @@
|
||||
html,
|
||||
body {
|
||||
padding: 0;
|
||||
color: #aaa;
|
||||
box-sizing: border-box;
|
||||
font-family: "Titillium Web", sans-serif;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.github-corner:hover .octo-arm {
|
||||
animation: octocat-wave 560ms ease-in-out;
|
||||
}
|
||||
@keyframes octocat-wave {
|
||||
0%,
|
||||
100% {
|
||||
transform: rotate(0);
|
||||
}
|
||||
20%,
|
||||
60% {
|
||||
transform: rotate(-25deg);
|
||||
}
|
||||
40%,
|
||||
80% {
|
||||
transform: rotate(10deg);
|
||||
}
|
||||
}
|
||||
@media (max-width: 500px) {
|
||||
.github-corner:hover .octo-arm {
|
||||
animation: none;
|
||||
}
|
||||
.github-corner .octo-arm {
|
||||
animation: octocat-wave 560ms ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
i {
|
||||
color: #3a3a3a;
|
||||
}
|
||||
|
||||
div.intro a,
|
||||
div.intro a:visited {
|
||||
color: #fded39;
|
||||
}
|
||||
div.intro {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
div.intro h3 {
|
||||
color: #fded39;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1 i {
|
||||
color: black;
|
||||
text-shadow: none;
|
||||
font-size: 21px;
|
||||
}
|
||||
textarea,
|
||||
input,
|
||||
button {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
height: 90vh;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#map {
|
||||
flex: 3;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
background: #1a1a1a;
|
||||
border-left: 1px solid #444444;
|
||||
width: 320px;
|
||||
flex: 1;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
#sidebar div#results-area {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
div#heading {
|
||||
background: #fded39;
|
||||
margin: 0;
|
||||
height: 10vh;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
div#heading h1 {
|
||||
font-size: 25px;
|
||||
text-transform: uppercase;
|
||||
text-shadow: -2px 2px black;
|
||||
margin: 0;
|
||||
color: #fded39;
|
||||
}
|
||||
|
||||
div#heading p {
|
||||
color: black;
|
||||
font-style: italic;
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
div#search-area {
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #444444;
|
||||
}
|
||||
|
||||
div#search-area input,
|
||||
div#search-area button {
|
||||
padding: 10px 8px;
|
||||
border: none;
|
||||
}
|
||||
div#search-area input {
|
||||
width: 195px;
|
||||
}
|
||||
div#search-area button {
|
||||
background: #fded39;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
div#results-area {
|
||||
padding: 10px;
|
||||
}
|
||||
div#results-area h5 {
|
||||
font-weight: 200;
|
||||
font-style: italic;
|
||||
margin: 0;
|
||||
color: #ddd;
|
||||
}
|
||||
div#results-area h5 span.highlight {
|
||||
color: #fded39;
|
||||
font-weight: 600;
|
||||
}
|
||||
div#results-area ul {
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
div#results-area ul li {
|
||||
border: 1px solid #444;
|
||||
padding: 5px 10px;
|
||||
margin-bottom: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
div#results-area ul li:hover {
|
||||
border: 1px solid #fded39;
|
||||
}
|
||||
|
||||
div#results-area ul li p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
div#results-area ul li p.truck-name {
|
||||
color: #fded39;
|
||||
text-transform: uppercase;
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
div#results-area ul li div.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
div#results-area ul li div.row div.icons {
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
div#results-area ul li div.row div.content {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.mapboxgl-popup-content {
|
||||
background: black;
|
||||
font-family: "Titillium Web", sans-serif;
|
||||
}
|
||||
|
||||
.mapboxgl-popup-anchor-bottom .mapboxgl-popup-tip {
|
||||
border-top-color: black;
|
||||
}
|
||||
.mapboxgl-popup-anchor-top .mapboxgl-popup-tip {
|
||||
border-bottom-color: black;
|
||||
}
|
||||
.mapboxgl-popup-close-button {
|
||||
color: white;
|
||||
}
|
||||
.mapboxgl-popup-content .marker-title {
|
||||
color: #fded39;
|
||||
text-transform: uppercase;
|
||||
font-size: 14px;
|
||||
}
|
||||
.mapboxgl-popup-content h4 {
|
||||
margin: 0;
|
||||
margin-top: 10px;
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset='utf-8' />
|
||||
<title>SF Food Trucks</title>
|
||||
<meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' />
|
||||
<link href='https://fonts.googleapis.com/css?family=Titillium+Web:400,700' rel='stylesheet' type='text/css'>
|
||||
<script src='https://api.mapbox.com/mapbox-gl-js/v1.9.1/mapbox-gl.js'></script>
|
||||
<link href='https://api.mapbox.com/mapbox-gl-js/v1.9.1/mapbox-gl.css' rel='stylesheet' />
|
||||
<link href='/static/styles/main.css' rel='stylesheet' />
|
||||
<link rel="stylesheet" href="http://code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css" />
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="/static/icons/apple-icon-57x57.png">
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="/static/icons/apple-icon-60x60.png">
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="/static/icons/apple-icon-72x72.png">
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="/static/icons/apple-icon-76x76.png">
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="/static/icons/apple-icon-114x114.png">
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="/static/icons/apple-icon-120x120.png">
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="/static/icons/apple-icon-144x144.png">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="/static/icons/apple-icon-152x152.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/static/icons/apple-icon-180x180.png">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="/static/icons//android-icon-192x192.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/static/icons/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="/static/icons/favicon-96x96.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/static/icons/favicon-16x16.png">
|
||||
<meta name="msapplication-TileColor" content="#ffffff">
|
||||
<meta name="msapplication-TileImage" content="/ms-icon-144x144.png">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- awesome svg octocat thanks to http://tholman.com/ -->
|
||||
<a href="https://github.com/prakhar1989/FoodTrucks/" class="github-corner" title="Fork me on Github">
|
||||
<svg width="72" height="72" viewBox="0 0 250 250" style="fill:#000; color:#FDED39; position: absolute; top: 0; border: 0; left: 0; transform: scale(-1, 1);">
|
||||
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
|
||||
<path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path>
|
||||
<path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path>
|
||||
</svg>
|
||||
</a>
|
||||
<div id="heading">
|
||||
<h1>SF F <i class="ion-pizza"></i> <i class="ion-icecream"></i> d Trucks</h1>
|
||||
<p>San Francisco's finger-licking street food now at your fingertips.</p>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div id='map'></div>
|
||||
<div id="sidebar"> </div>
|
||||
</div>
|
||||
<script src='/static/build/main.js'></script>
|
||||
</body>
|
||||
</html>
|
@ -1,19 +0,0 @@
|
||||
module.exports = {
|
||||
cache: true,
|
||||
entry: './static/src/app.js',
|
||||
output: {
|
||||
filename: './static/build/main.js'
|
||||
},
|
||||
devtool: 'source-map',
|
||||
module: {
|
||||
loaders: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader',
|
||||
query: {
|
||||
presets: ['es2015', 'react']
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
};
|
@ -1,46 +0,0 @@
|
||||
import json
|
||||
import requests
|
||||
|
||||
def getData(url):
|
||||
r = requests.get(url)
|
||||
return r.json()
|
||||
|
||||
def convertData(data, msymbol="restaurant", msize="medium"):
|
||||
data_dict = []
|
||||
for d in data:
|
||||
if d.get('longitude') and d.get("latitude"):
|
||||
data_dict.append({
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [float(d["longitude"]),
|
||||
float(d["latitude"])]
|
||||
},
|
||||
"properties": {
|
||||
"name": d.get("applicant", ""),
|
||||
"marker-symbol": msymbol,
|
||||
"marker-size": msize,
|
||||
"marker-color": "#CC0033",
|
||||
"fooditems": d.get('fooditems', ""),
|
||||
"address": d.get("address", "")
|
||||
}
|
||||
})
|
||||
return data_dict
|
||||
|
||||
def writeToFile(data, filename="data.geojson"):
|
||||
template = {
|
||||
"type": "FeatureCollection",
|
||||
"crs": {
|
||||
"type": "name",
|
||||
"properties": {
|
||||
"name": "urn:ogc:def:crs:OGC:1.3:CRS84"
|
||||
},
|
||||
},
|
||||
"features": data }
|
||||
with open(filename, "w") as f:
|
||||
json.dump(template, f, indent=2)
|
||||
print "Geojson generated"
|
||||
|
||||
if __name__ == "__main__":
|
||||
data = getData("http://data.sfgov.org/resource/rqzj-sfat.json")
|
||||
writeToFile(convertData(data[:350]), filename="trucks.geojson")
|