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")
|
|