Initial additions

This commit is contained in:
Rob Hedgpeth
2020-04-01 17:16:52 -05:00
parent d578ef3e01
commit 721742f575
47 changed files with 16369 additions and 0 deletions

185
README.md Normal file
View File

@ -0,0 +1,185 @@
# Places
**Places** is a web application backed by the power of the power, performance, and simplicity of [MariaDB platform](https://mariadb.com/products/mariadb-platform/), allows you to record all of your favorite locations!
<p align="center">
<kbd>
<img src="media/map.png" />
</kbd>
</p>
The following will walk you through the steps for getting this application up and running within minutes! This application is completely open source. Please feel free to use it and the source code as you see fit.
# Table of Contents
2. [Overview](#overview)
1. [MariaDB Platform](#platform)
2. [MariaDB SkySQL](#skysql)
3. [Using JSON in a relational database](#json-relational)
3. [Requirements](#requirements)
4. [Getting started](#getting-started)
1. [Get the code](#code)
2. [Create the schema](#schema)
3. [Anatomy of the app](#app)
4. [Build and run the app](#build-run)
5. [JSON Data Models](#data-models)
6. [Support and Contribution](#support-contribution)
7. [License](#license)
## Overview <a name="overview"></a>
### MariaDB Platform <a name="platform"></a>
[MariaDB Platform](https://mariadb.com/products/mariadb-platform/) integrates the former [MariaDB TX (transactions)](https://mariadb.com/products/mariadb-platform-transactional/) and [MariaDB AX (analytics)](https://mariadb.com/products/mariadb-platform-analytical/) products so developers can build modern applications by enriching transactions with real-time analytics and historical data, creating insightful experiences and compelling opportunities for customers and for businesses, endless ways to monetize data. Its the only enterprise open source database built for modern applications running in the cloud.
To download and deploy MariaDB check out the instructions [here](https://mariadb.com/docs/deploy/installation/). You can also make use of the [MariaDB image available on Docker Hub](https://hub.docker.com/_/mariadb).
### MariaDB SkySQL <a name="skysql">
[SkySQL](https://mariadb.com/products/skysql/) is the first and only database-as-a-service (DBaaS) to bring the full power of MariaDB Platform to the cloud, including its support for transactional, analytical and hybrid workloads. Built on Kubernetes, and optimized for cloud infrastructure and services, SkySQL combines ease of use and self-service with enterprise reliability and world-class support everything needed to safely run mission-critical databases in the cloud, and with enterprise governance.
[Get started with SkySQL!](https://mariadb.com/products/skysql/#get-started)
<p align="center" spacing="10">
<kbd>
<img src="media/skysql.png" />
</kbd>
</p>
### Using JSON in a relational database <a name="json-relational"></a>
[JSON](https://www.json.org) is fast becoming the standard format for data interchange and for unstructured data, and MariaDB Platform (in fact, all MariaDB versions 10.2 and later) include a range of [JSON supporting functions](https://mariadb.com/topic/json/).
The Places application uses only a **single table** for all location, and uses JSON to store more specific information based on the location type.
For more information on how JSON can be used within MariaDB please check out this [blog post](https://mariadb.com/resources/blog/json-with-mariadb-10-2/)!
## Getting started <a name="getting-started"></a>
In order to run the Places application you will need to have a MariaDB instance to connect to. For more information please check out "[Get Started with MariaDB](https://mariadb.com/get-started-with-mariadb/)".
### Get the code <a name="code"></a>
Download this code directly or use [git](git-scm.org) (through CLI or a client) to retrieve the code using `git clone`:
```
$ git clone https://github.com/mariadb-corporation/dev-example-orders.git
```
### Create the schema <a name="schema"></a>
[Connect to the database](https://mariadb.com/kb/en/connecting-to-mariadb/) using CLI or a client and execute the following:
```sql
CREATE TABLE `Locations` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL DEFAULT '',
`description` varchar(500) DEFAULT '',
`type` char(1) NOT NULL DEFAULT '',
`latitude` decimal(9,6) NOT NULL,
`longitude` decimal(9,6) NOT NULL,
`attr` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`attr`)),
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=19 DEFAULT CHARSET=utf8mb4;
```
### Anatomy of the app <a name="app"></a>
This application is made of two parts:
* Client
- communicates with the API.
- is a React.js project located in the [client](client) folder.
* API
- uses a MariaDB Connector to connect to MariaDB.
- contains multiple projects, located in the [api](api) folder.
- [Node.js](api/nodejs)
- Python (coming soon!)
See the README's in [client](client/README.md) and [api](api/README.md) for more information on how to get started!
### Build and run the app <a name="build-run"></a>
1. Nagivate to the [client](client) folder and execute the following CLI command before proceeding:
```
$ npm install
```
2. Add the a Google Maps API KEY to [MapContainer.js](client/src/components/MapContainer.js#L248):
```javascript
export default GoogleApiWrapper({
apiKey: ("ENTER_GOOGLE_API_KEY")
})(MapContainer)
```
3. Pick an [API](api) project and follow the instructions of the README within the API project root.
## JSON Data Models <a name="data-models"></a>
Below are samples of the data model per Location Type.
**Attraction**
```json
{
"category":"Landmark",
"lastVisitDate":"11/5/2019"
}
```
**Location**
```json
{
"details":{
"foodType":"Pizza",
"menu":"www.giodanos.com/menu"
},
"favorites":[
{
"description":"Classic Chicago",
"price":24.99
},
{
"description":"Salad",
"price":9.99
}
]
}
```
**Sports Venue**
```json
{
"details":{
"yearOpened":1994,
"capacity":23500
},
"events":[
{
"date":"10/18/2019",
"description":"Bulls vs Celtics"
},
{
"date":"10/21/2019",
"description":"Bulls vs Lakers"
},
{
"date":"11/5/2019",
"description":"Bulls vs Bucks"
},
{
"date":"11/5/2019",
"description":"Blackhawks vs Blues"
}
]
}
```
## Support and Contribution <a name="support-contribution"></a>
Thanks so much for taking a look at the Places app! As this is a very simple example, there's plenty of potential for customization. Please feel free to submit PR's to the project to include your modifications!
If you have any questions, comments, or would like to contribute to this or future projects like this please reach out to us directly at developers@mariadb.com or on [Twitter](https://twitter.com/mariadb).
## License <a name="license"></a>
[![License](https://img.shields.io/badge/License-MIT-blue.svg?style=plastic)](https://opensource.org/licenses/MIT)

6
api/README.md Normal file
View File

@ -0,0 +1,6 @@
# Places - API
The API project is responsible for exposing endpoints to the client and integrating (using a MariaDB Connector) a MariaDB database. This application contains multiple API projects that can be used by the [client](../client).
Continue into one of the folders for more instructions...

61
api/nodejs/.gitignore vendored Normal file
View File

@ -0,0 +1,61 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# next.js build output
.next

14
api/nodejs/Dockerfile Normal file
View File

@ -0,0 +1,14 @@
FROM node:8
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
COPY package*.json ./
RUN npm install
# Copy app source code
COPY . .
#Expose port and start application
EXPOSE 80
CMD [ "npm", "start" ]

109
api/nodejs/README.md Normal file
View File

@ -0,0 +1,109 @@
# Places - Node.js API
1. [Environment and compatibility](#compatibility)
2. [Getting started with the app](#getting-started)
1. [Configure the code](#configure-code)
2. [Build the code](#build-code)
3. [Run the app](#run-app)
## Environment and compatibility <a name="compatibility"></a>
This sample was created using the following techologies:
* [Node.js (v.12.x)](https://nodejs.org/docs/latest-v12.x/api/index.html)
* [NPM (v.6.11.3)](https://docs.npmjs.com/)
## Getting started with the app <a name="getting-started"></a>
### Configure the code <a name="configure-code"></a>
Configure the MariaDB connection by [adding an .env file to the Node.js project](https://github.com/mariadb-corporation/mariadb-connector-nodejs/blob/master/documentation/promise-api.md#security-consideration).
Example implementation:
```
DB_HOST=<host_address>
DB_PORT=<port_number>
DB_USER=<username>
DB_PASS=<password>
DB_NAME=<database>
```
**Configuring db.js**
The environmental variables from `.env` are used within the [db.js](src/db.js) for the MariaDB Node.js Connector configuration pool settings:
```javascript
var mariadb = require('mariadb');
require('dotenv').config();
const pool = mariadb.createPool({
host: process.env.DB_HOST_1,
user: process.env.DB_USER_1,
password: process.env.DB_PASS_1,
port: process.env.DB_PORT_1,
multipleStatements: true,
connectionLimit: 5
});
```
**Configuring db.js for MariaDB SkySQL**
MariaDB SkySQL uses requires SSL additions to connection. It's as easy as 1-2-3 (steps below).
```javascript
var mariadb = require('mariadb');
require('dotenv').config();
// 1.) Access the Node File System package
const fs = require("fs");
// 2.) Retrieve the Certificate Authority chain file (wherever you placed it - notice it's just in the Node project root here)
const serverCert = [fs.readFileSync("skysql_chain_t.pem", "utf8")];
var pools = [
mariadb.createPool({
host: process.env.DB_HOST_1,
user: process.env.DB_USER_1,
password: process.env.DB_PASS_1,
port: process.env.DB_PORT_1,
database: process.env.DB_NAME_1,
multipleStatements: true,
connectionLimit: 5,
// 3.) Add an "ssl" property to the connection pool configuration, using the serverCert const defined above
ssl: {
ca: serverCert
}
})
];
```
### Build the code <a name="build-code"></a>
Once you have retrieved a copy of the code you're ready to build and run the project! However, before running the code it's important to point out that the application uses several Node Packages.
Executing the CLI command
```
$ npm install
```
Doing this targets relative `package.json` file and [install all dependencies](https://docs.npmjs.com/downloading-and-installing-packages-locally).
**IMPORTANT**: Be sure that the Node modules are installed for the [client](../../client). This can be done manually executing the following CLI command for [client](../../client):
```
$ npm install
```
### Run the app <a name="run-app"></a>
Once you've pulled down the code and have verified that all of the required Node packages are installed you're ready to run the application!
1. Execute the following CLI command
```bash
$ npm start
```
2. Open a browser window and navigate to http://localhost:3000.

35
api/nodejs/db.js Normal file
View File

@ -0,0 +1,35 @@
var mariadb = require('mariadb');
require('dotenv').config();
// SSL (e.g. SkySQL) connections
// * Remember to change the location of "skysql_chain.pem" to wherever you placed it!
// * To use just uncomment the two lines below and the 'ssl' property (and value) within the connection pool configuration
//const fs = require("fs");
//const serverCert = [fs.readFileSync("skysql_chain.pem", "utf8")];
const pool = mariadb.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS,
port: process.env.DB_PORT,
database: process.env.DB_NAME,
multipleStatements: true,
connectionLimit: 5,
/*
,ssl: {
ca: serverCert
}*/
});
module.exports={
getConnection: function(){
return new Promise(function(resolve,reject){
pool.getConnection().then(function(connection){
resolve(connection);
}).catch(function(error){
reject(error);
});
});
}
}

16
api/nodejs/deployment.yml Normal file
View File

@ -0,0 +1,16 @@
apiVersion: apps/v1beta1
kind: Deployment
metadata:
name: places-api-deployment
spec:
replicas: 2
template:
metadata:
labels: # labels to select/identify the deployment
app: places-api
spec: # pod spec
containers:
- name: places-api
image: gcr.io/mariadb-technical-marketing/places-api-image:v1 # image we pushed
ports:
- containerPort: 80

818
api/nodejs/package-lock.json generated Normal file
View File

@ -0,0 +1,818 @@
{
"name": "places",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@types/geojson": {
"version": "7946.0.7",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.7.tgz",
"integrity": "sha512-wE2v81i4C4Ol09RtsWFAqg3BUitWbHSpSlIo+bNdsCJijO9sjme+zm+73ZMCa/qMC8UEERxzGbvmr1cffo2SiQ=="
},
"@types/node": {
"version": "13.9.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.9.5.tgz",
"integrity": "sha512-hkzMMD3xu6BrJpGVLeQ3htQQNAcOrJjX7WFmtK8zWQpz2UJf13LCFF2ALA7c9OVdvc2vQJeDdjfR35M0sBCxvw=="
},
"accepts": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
"integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
"requires": {
"mime-types": "~2.1.24",
"negotiator": "0.6.2"
}
},
"ansi-regex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg=="
},
"ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"requires": {
"color-convert": "^1.9.0"
}
},
"array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
},
"body-parser": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
"integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
"requires": {
"bytes": "3.1.0",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "~1.1.2",
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"on-finished": "~2.3.0",
"qs": "6.7.0",
"raw-body": "2.4.0",
"type-is": "~1.6.17"
}
},
"bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
},
"camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
},
"chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"requires": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
},
"dependencies": {
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"requires": {
"has-flag": "^3.0.0"
}
}
}
},
"cliui": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
"integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
"requires": {
"string-width": "^3.1.0",
"strip-ansi": "^5.2.0",
"wrap-ansi": "^5.1.0"
}
},
"color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"requires": {
"color-name": "1.1.3"
}
},
"color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
},
"concurrently": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-5.1.0.tgz",
"integrity": "sha512-9ViZMu3OOCID3rBgU31mjBftro2chOop0G2u1olq1OuwRBVRw/GxHTg80TVJBUTJfoswMmEUeuOg1g1yu1X2dA==",
"requires": {
"chalk": "^2.4.2",
"date-fns": "^2.0.1",
"lodash": "^4.17.15",
"read-pkg": "^4.0.1",
"rxjs": "^6.5.2",
"spawn-command": "^0.0.2-1",
"supports-color": "^6.1.0",
"tree-kill": "^1.2.2",
"yargs": "^13.3.0"
}
},
"content-disposition": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
"integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==",
"requires": {
"safe-buffer": "5.1.2"
}
},
"content-type": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
"integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
},
"cookie": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
"integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg=="
},
"cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
},
"date-fns": {
"version": "2.11.1",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.11.1.tgz",
"integrity": "sha512-3RdUoinZ43URd2MJcquzBbDQo+J87cSzB8NkXdZiN5ia1UNyep0oCyitfiL88+R7clGTeq/RniXAc16gWyAu1w=="
},
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
},
"decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
},
"denque": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz",
"integrity": "sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ=="
},
"depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
},
"destroy": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
},
"dotenv": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz",
"integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw=="
},
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
},
"emoji-regex": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
"integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA=="
},
"encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
},
"error-ex": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
"integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
"requires": {
"is-arrayish": "^0.2.1"
}
},
"escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
},
"escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
},
"etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
},
"express": {
"version": "4.17.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
"integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==",
"requires": {
"accepts": "~1.3.7",
"array-flatten": "1.1.1",
"body-parser": "1.19.0",
"content-disposition": "0.5.3",
"content-type": "~1.0.4",
"cookie": "0.4.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~1.1.2",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "~1.1.2",
"fresh": "0.5.2",
"merge-descriptors": "1.0.1",
"methods": "~1.1.2",
"on-finished": "~2.3.0",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.5",
"qs": "6.7.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.1.2",
"send": "0.17.1",
"serve-static": "1.14.1",
"setprototypeof": "1.1.1",
"statuses": "~1.5.0",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
}
},
"finalhandler": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
"integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
"requires": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"on-finished": "~2.3.0",
"parseurl": "~1.3.3",
"statuses": "~1.5.0",
"unpipe": "~1.0.0"
}
},
"find-up": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
"integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
"requires": {
"locate-path": "^3.0.0"
}
},
"forwarded": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
"integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
},
"fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
},
"get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="
},
"has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
},
"hosted-git-info": {
"version": "2.8.8",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
"integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg=="
},
"http-errors": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
"integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
"requires": {
"depd": "~1.1.2",
"inherits": "2.0.3",
"setprototypeof": "1.1.1",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.0"
}
},
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
},
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"ipaddr.js": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz",
"integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA=="
},
"is-arrayish": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
"integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0="
},
"is-fullwidth-code-point": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8="
},
"json-parse-better-errors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
"integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw=="
},
"locate-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
"integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
"requires": {
"p-locate": "^3.0.0",
"path-exists": "^3.0.0"
}
},
"lodash": {
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
},
"long": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
"integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="
},
"mariadb": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/mariadb/-/mariadb-2.3.1.tgz",
"integrity": "sha512-suv+ygoiS+tQSKmxgzJsGV9R+USN8g6Ql+GuMo9k7alD6FxOT/lwebLHy63/7yPZfVtlyAitK1tPd7ZoFhN/Sg==",
"requires": {
"@types/geojson": "^7946.0.7",
"@types/node": ">=8.0.0",
"denque": "^1.4.1",
"iconv-lite": "^0.5.1",
"long": "^4.0.0",
"moment-timezone": "^0.5.27"
},
"dependencies": {
"iconv-lite": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.5.1.tgz",
"integrity": "sha512-ONHr16SQvKZNSqjQT9gy5z24Jw+uqfO02/ngBSBoqChZ+W8qXX7GPRa1RoUnzGADw8K63R1BXUMzarCVQBpY8Q==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
}
}
},
"media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
},
"merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
},
"methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
},
"mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
},
"mime-db": {
"version": "1.40.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz",
"integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA=="
},
"mime-types": {
"version": "2.1.24",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz",
"integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==",
"requires": {
"mime-db": "1.40.0"
}
},
"moment": {
"version": "2.24.0",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
"integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg=="
},
"moment-timezone": {
"version": "0.5.28",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.28.tgz",
"integrity": "sha512-TDJkZvAyKIVWg5EtVqRzU97w0Rb0YVbfpqyjgu6GwXCAohVRqwZjf4fOzDE6p1Ch98Sro/8hQQi65WDXW5STPw==",
"requires": {
"moment": ">= 2.9.0"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"negotiator": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
},
"normalize-package-data": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
"integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
"requires": {
"hosted-git-info": "^2.1.4",
"resolve": "^1.10.0",
"semver": "2 || 3 || 4 || 5",
"validate-npm-package-license": "^3.0.1"
}
},
"on-finished": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
"requires": {
"ee-first": "1.1.1"
}
},
"p-limit": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz",
"integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==",
"requires": {
"p-try": "^2.0.0"
}
},
"p-locate": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
"integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
"requires": {
"p-limit": "^2.0.0"
}
},
"p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="
},
"parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
"integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=",
"requires": {
"error-ex": "^1.3.1",
"json-parse-better-errors": "^1.0.1"
}
},
"parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
},
"path-exists": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
"integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU="
},
"path-parse": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw=="
},
"path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
},
"pify": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY="
},
"proxy-addr": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz",
"integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==",
"requires": {
"forwarded": "~0.1.2",
"ipaddr.js": "1.9.0"
}
},
"qs": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
},
"range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
},
"raw-body": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
"integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
"requires": {
"bytes": "3.1.0",
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
}
},
"read-pkg": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-4.0.1.tgz",
"integrity": "sha1-ljYlN48+HE1IyFhytabsfV0JMjc=",
"requires": {
"normalize-package-data": "^2.3.2",
"parse-json": "^4.0.0",
"pify": "^3.0.0"
}
},
"require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I="
},
"require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
},
"resolve": {
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz",
"integrity": "sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==",
"requires": {
"path-parse": "^1.0.6"
}
},
"rxjs": {
"version": "6.5.4",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.4.tgz",
"integrity": "sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q==",
"requires": {
"tslib": "^1.9.0"
}
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
},
"send": {
"version": "0.17.1",
"resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
"integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==",
"requires": {
"debug": "2.6.9",
"depd": "~1.1.2",
"destroy": "~1.0.4",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "~1.7.2",
"mime": "1.6.0",
"ms": "2.1.1",
"on-finished": "~2.3.0",
"range-parser": "~1.2.1",
"statuses": "~1.5.0"
},
"dependencies": {
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
}
}
},
"serve-static": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz",
"integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==",
"requires": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.17.1"
}
},
"set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
},
"setprototypeof": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
},
"spawn-command": {
"version": "0.0.2-1",
"resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz",
"integrity": "sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A="
},
"spdx-correct": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz",
"integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==",
"requires": {
"spdx-expression-parse": "^3.0.0",
"spdx-license-ids": "^3.0.0"
}
},
"spdx-exceptions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz",
"integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA=="
},
"spdx-expression-parse": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz",
"integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==",
"requires": {
"spdx-exceptions": "^2.1.0",
"spdx-license-ids": "^3.0.0"
}
},
"spdx-license-ids": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz",
"integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q=="
},
"statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
},
"string-width": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
"integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
"requires": {
"emoji-regex": "^7.0.1",
"is-fullwidth-code-point": "^2.0.0",
"strip-ansi": "^5.1.0"
}
},
"strip-ansi": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
"requires": {
"ansi-regex": "^4.1.0"
}
},
"supports-color": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz",
"integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==",
"requires": {
"has-flag": "^3.0.0"
}
},
"toidentifier": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
},
"tree-kill": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="
},
"tslib": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz",
"integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA=="
},
"type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"requires": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
}
},
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
},
"utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
},
"validate-npm-package-license": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
"integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
"requires": {
"spdx-correct": "^3.0.0",
"spdx-expression-parse": "^3.0.0"
}
},
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
},
"which-module": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
"integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho="
},
"wrap-ansi": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
"integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
"requires": {
"ansi-styles": "^3.2.0",
"string-width": "^3.0.0",
"strip-ansi": "^5.0.0"
}
},
"y18n": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
"integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w=="
},
"yargs": {
"version": "13.3.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
"integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==",
"requires": {
"cliui": "^5.0.0",
"find-up": "^3.0.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^3.0.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^13.1.2"
}
},
"yargs-parser": {
"version": "13.1.2",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
"integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
"requires": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
}
}
}
}

20
api/nodejs/package.json Normal file
View File

@ -0,0 +1,20 @@
{
"name": "places",
"version": "1.0.0",
"description": "",
"main": "server.js",
"scripts": {
"start": "concurrently \"npm run server\" \"npm run client\"",
"server": "node server.js",
"client": "npm start --prefix ../../client"
},
"author": "",
"license": "ISC",
"dependencies": {
"body-parser": "^1.19.0",
"concurrently": "^5.1.0",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"mariadb": "^2.3.1"
}
}

View File

@ -0,0 +1,141 @@
"use strict";
let express = require("express"),
router = express.Router(),
pool = require('../db');
// GET all locations
router.get("/", async (req, res, next) => {
let conn;
try {
conn = await pool.getConnection();
var query = "select id, name, type, longitude, latitude, " +
"case when type = 'R' then concat((case when json_length(attr, '$.favorites') " +
"is not null then json_length(attr, '$.favorites') else 0 end), ' favorite meals') " +
"when type = 'A' then (case when json_value(attr, '$.lastVisitDate') is not null " +
"then json_value(attr, '$.lastVisitDate') else 'N/A' end) " +
"when type = 'S' then concat((case when json_length(attr, '$.events') is not null " +
"then json_length(attr, '$.events') else 0 end), ' events') end as description " +
"from Locations";
var rows = await conn.query(query);
res.send(rows);
} catch (err) {
throw err;
} finally {
if (conn) return conn.release();
}
});
// POST new location
router.post("/", async (req, res, next) => {
let location = req.body;
let conn;
try {
conn = await pool.getConnection();
var query = "insert into Locations (name, description, type, latitude, longitude, attr) values (?, ?, ?, ?, ?, json_compact(?))";
var result = await conn.query(query, [location.name, location.description, location.type, location.latitude, location.longitude, JSON.stringify(location.attr)]);
res.send(result);
} catch (err) {
throw err;
} finally {
if (conn) return conn.release();
}
});
// GET restaurant by id
router.get("/restaurant", async (req, res, next) => {
let conn;
try {
conn = await pool.getConnection();
var id = req.query.id;
var query = "select " +
"name, " +
"json_value(attr,'$.details.foodType') as foodType, " +
"json_value(attr,'$.details.menu') as menu, " +
"json_query(attr,'$.favorites') as favorites " +
"from Locations " +
"where id = ?";
var rows = await conn.query(query, [id]);
res.send(rows[0]);
} catch (err) {
throw err;
} finally {
if (conn) return conn.release();
}
});
// POST new restaurant favorite
router.post("/restaurant/favorites", async (req, res, next) => {
let favorite = req.body;
let details = favorite.details;
let conn;
try {
conn = await pool.getConnection();
var query = "update Locations set attr = json_array_append(attr, '$.favorites', json_compact(?)) where id = ?"
var result = await conn.query(query, [JSON.stringify(details), favorite.locationid]);
res.send(result);
} catch (err) {
throw err;
} finally {
if (conn) return conn.release();
}
});
// GET sports venue by id
router.get("/sportsvenue", async (req, res, next) => {
let conn;
try {
conn = await pool.getConnection();
var id = req.query.id;
var query = "select " +
"name, " +
"json_value(attr,'$.details.yearOpened') as yearOpened, " +
"json_value(attr,'$.details.capacity') as capacity, " +
"json_query(attr,'$.events') as events " +
"from Locations " +
"where id = ?";
var rows = await conn.query(query, [id]);
res.send(rows[0]);
} catch (err) {
throw err;
} finally {
if (conn) return conn.release();
}
});
// POST new sports venue event
router.post("/sportsvenue/event", async (req, res, next) => {
let event = req.body;
let conn;
try {
conn = await pool.getConnection();
var query = "update Locations set attr = json_array_append(attr, '$.events', json_compact(?)) where id = ?";
var result = await conn.query(query, [JSON.stringify(event.details), event.locationid]);
res.send(result);
} catch (err) {
throw err;
} finally {
if (conn) return conn.release();
}
});
// PUT last visited an attraction
router.put("/attractions", async (req, res, next) => {
let locationId = req.query.id;
let lastVisitDate = req.query.dt;
let conn;
try {
conn = await pool.getConnection();
var query = "update Locations set attr = json_set(attr,'$.lastVisitDate', ?) where id = ?";
var result = await conn.query(query, [lastVisitDate, locationId]);
res.send(result);
} catch (err) {
throw err;
} finally {
if (conn) return conn.release();
}
});
module.exports = router;

28
api/nodejs/server.js Normal file
View File

@ -0,0 +1,28 @@
const express = require('express');
const app = express();
const port = 8080;
const path = require('path');
const bodyParser = require("body-parser");
const locationRoutes = require("./routes/locationRoutes");
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
if (process.env.NODE_ENV === 'production') {
app.use(express.static('client/build'));
}
app.use("/api/locations", locationRoutes);
app.get("/*", (req, res) => {
res.sendFile(path.join(__dirname, "/client/build/index.html"));
});
app.use((err, req, res, next) => {
res.status(422).send({ error: err._message });
});
// console.log that your server is up and running
app.listen(port, () => console.log(`Listening on port ${port}`));

23
client/.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

15
client/Dockerfile Normal file
View File

@ -0,0 +1,15 @@
# build environment
FROM node:12.2.0-alpine as build
WORKDIR /app
ENV PATH /app/node_modules/.bin:$PATH
COPY package.json /app/package.json
RUN npm install --silent
RUN npm install react-scripts@3.0.1 -g --silent
COPY . /app
RUN npm run build
# production environment
FROM nginx:1.16.0-alpine
COPY --from=build /app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

72
client/README.md Normal file
View File

@ -0,0 +1,72 @@
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm install`
Installs all the dependencies indicated within `package.json`.
### `npm start`
Runs the app in the development mode.<br />
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.<br />
You will also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.<br />
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.<br />
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.<br />
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
### Analyzing the Bundle Size
This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
### Making a Progressive Web App
This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
### Advanced Configuration
This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
### Deployment
This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
### `npm run build` fails to minify
This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify

16
client/deployment.yml Normal file
View File

@ -0,0 +1,16 @@
apiVersion: apps/v1beta1
kind: Deployment
metadata:
name: places-ui-deployment
spec:
replicas: 2
template:
metadata:
labels: # labels to select/identify the deployment
app: places-ui
spec: # pod spec
containers:
- name: places-ui
image: gcr.io/mariadb-technical-marketing/places-ui-image:v1 # image we pushed
ports:
- containerPort: 80

13438
client/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

36
client/package.json Normal file
View File

@ -0,0 +1,36 @@
{
"name": "client",
"version": "0.1.0",
"private": true,
"dependencies": {
"google-maps-react": "^2.0.2",
"prop-types": "^15.7.2",
"react": "^16.13.1",
"react-datepicker": "^2.14.1",
"react-dom": "^16.13.1",
"react-modal": "^3.11.2",
"react-scripts": "3.2.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"proxy": "http://localhost:8080"
}

BIN
client/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

43
client/public/index.html Normal file
View File

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Places</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

BIN
client/public/logo192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

BIN
client/public/logo512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

2
client/public/robots.txt Normal file
View File

@ -0,0 +1,2 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *

160
client/src/App.css Normal file
View File

@ -0,0 +1,160 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #09d3ac;
}
.centered {
text-align: center;
}
.actionable {
color: blue;
cursor: pointer;
}
.add-location-actionable {
position: absolute;
top: 13px;
right: 60px;
}
button {
height: 35px;
border-radius: 5px;
background: #96DDCF;
border-color: #E5E1E5;
color: #ffffff;
font-weight: bold;
}
button:hover {
background: #ffffff;
border-color: #E5E1E5;
color: #96DDCF;
cursor: pointer;
}
.modal {
position: fixed;
top: 50%;
left: 50%;
background-color: #ffffff;
border-radius: 8px;
border-color: #2F99A3;
transform: translate(-50%,-50%);
outline: none;
}
.overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.6);
}
.modal-close {
position: absolute;
top: -15px;
right: -15px;
}
.modal-title {
background-color: #0E6488;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
padding: 15px;
}
.modal-title h3 {
color: #ffffff;
margin: 0px;
}
.form-main {
padding: 15px;
min-width: 300px;
}
.form-main button {
width: 125px;
margin-left: auto;
margin-right: auto;
display: block;
margin-top: 15px;
}
.form-main table {
font-size: 14px;
}
.info-table {
border-spacing: 10px;
border-collapse: separate;
font-size: 12px;
}
.info-table td:first-child {
font-weight: bold;
}
.list-table {
margin-top: 15px;
text-align: center;
width: 100%;
border-collapse: collapse;
border-spacing: 0;
}
.list-table td {
padding: 5px;
}
.list-table tr:first-child {
background: #96DDCF;
color: #ffffff;
}
.list-table td {
border-bottom-style: dotted;
border-bottom-color: #c0c0c0;
border-bottom-width: 1px;
}
.small-text-title {
font-weight: bold;
font-size: 11px;
margin: 0px;
}
.centered button {
display: inline-block;
}
button.negative {
background: #ff6961;
}
button.negative:hover {
color: #ff6961;
background: #ffffff;
}

9
client/src/App.js Normal file
View File

@ -0,0 +1,9 @@
import React from 'react';
import './App.css';
import MapContainer from './components/MapContainer';
function App() {
return (<MapContainer />);
}
export default App;

9
client/src/App.test.js Normal file
View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<App />, div);
ReactDOM.unmountComponentAtNode(div);
});

View File

@ -0,0 +1,82 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
export default class AddEvent extends Component {
constructor(props) {
super(props);
this.state = {
eventDate: new Date()
}
this.handleChange = this.handleChange.bind(this);
this.saveEvent = this.saveEvent.bind(this);
}
handleChange(date) {
this.setState({
eventDate: date
});
}
handleDescriptionChange = event => {
this.setState({ description: event.target.value });
};
async saveEvent() {
var event = this.getEvent();
var res = await fetch('/api/locations/sportsvenue/event',{
method: 'POST',
body: JSON.stringify(event),
headers: {"Content-Type": "application/json"}
});
if (res.status === 200) {
this.props.onSave();
}
}
getEvent() {
var event = {
locationid: this.props.id,
details: {
date: this.state.eventDate.toLocaleDateString(),
description: this.state.description
}
};
return event;
}
render() {
return (
<div className="form-main">
<table className="info-table">
<tr>
<td>Date:</td>
<td><DatePicker selected={this.state.eventDate} onChange={this.handleChange} /></td>
</tr>
<tr>
<td>Description:</td>
<td><input value={this.state.description} onChange={this.handleDescriptionChange} /></td>
</tr>
</table>
<div className="centered">
<button onClick={this.saveEvent}>Save</button>
<button className="negative" onClick={this.props.onCancel}>Cancel</button>
</div>
</div>
);
}
}
AddEvent.propTypes = {
onSave: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
id: PropTypes.number,
name: PropTypes.string
};

View File

@ -0,0 +1,189 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
export default class AddLocation extends Component {
constructor(props) {
super(props);
this.state = {
type: 'A'
}
this.onSelectChange = this.onSelectChange.bind(this);
this.saveLocation = this.saveLocation.bind(this);
}
onSelectChange(event) {
this.setState({ type: event.target.value });
this.forceUpdate();
}
handleNameChange = event => {
this.setState({ name: event.target.value });
};
handleDescriptionChange = event => {
this.setState({ desciption: event.target.value });
};
handleLatitudeChange = event => {
this.setState({ latitude: event.target.value });
};
handleLongitudeChange = event => {
this.setState({ longitude: event.target.value });
};
handleFoodTypeChange = event => {
this.setState({ foodType: event.target.value });
};
handleMenuChange = event => {
this.setState({ menu: event.target.value });
};
handleCategoryChange = event => {
this.setState({ category: event.target.value });
};
handleYearOpenedChange = event => {
this.setState({ yearOpened: event.target.value });
};
handleCapacityChange = event => {
this.setState({ capacity: event.target.value });
};
async saveLocation() {
var location = this.getLocation();
var res = await fetch('/api/locations',{
method: 'POST',
body: JSON.stringify(location),
headers: {"Content-Type": "application/json"}
});
if (res.status === 200) {
this.props.onSave();
}
}
getLocation() {
var location = {
name: this.state.name,
description: (this.state.description === undefined ? null : this.state.description),
type: this.state.type,
latitude: this.state.latitude,
longitude: this.state.longitude
};
if (this.state.type === 'A') {
location.attr = {
category: this.state.category
}
}
else if (this.state.type === 'R') {
location.attr = {
details: {
foodType: this.state.foodType,
menu: this.state.menu
},
favorites: []
};
}
else if (this.state.type === 'S') {
location.attr = {
details: {
yearOpened: this.state.yearOpened,
capacity: this.state.capacity
},
events: []
};
}
return location;
}
renderLocationTypeOptions() {
if (this.state.type === 'A'){
return(
<tr>
<td>Category:</td>
<td><input value={this.state.category} onChange={this.handleCategoryChange} /></td>
</tr>
);
}
else if (this.state.type === 'R'){
return(
<tbody>
<tr>
<td>Food Type:</td>
<td><input value={this.state.foodType} onChange={this.handleFoodTypeChange} /></td>
</tr>
<tr>
<td>Menu:</td>
<td><input value={this.state.menu} onChange={this.handleMenuChange} /></td>
</tr>
</tbody>
);
}
else if (this.state.type === 'S'){
return(
<tbody>
<tr>
<td>Year Opened:</td>
<td><input value={this.state.yearOpened} onChange={this.handleYearOpenedChange} /></td>
</tr>
<tr>
<td>Max Capacity:</td>
<td><input value={this.state.capacity} onChange={this.handleCapacityChange} /></td>
</tr>
</tbody>
);
}
}
render() {
return (
<div className="form-main">
<table className="info-table">
<tr>
<td>Type:</td>
<td>
<select onChange={this.onSelectChange}>
<option value="A">Attraction</option>
<option value="R">Restaurant</option>
<option value="S">Sports Venue</option>
</select>
</td>
</tr>
<tr>
<td>Name:</td>
<td><input value={this.state.name} onChange={this.handleNameChange} /></td>
</tr>
<tr>
<td>Description:</td>
<td><input value={this.state.description} onChange={this.handleDescriptionChange} /></td>
</tr>
<tr>
<td>Latitude:</td>
<td><input value={this.state.latitude} onChange={this.handleLatitudeChange} /></td>
</tr>
<tr>
<td>Longitude:</td>
<td><input value={this.state.longitude} onChange={this.handleLongitudeChange} /></td>
</tr>
{this.renderLocationTypeOptions()}
</table>
<button onClick={this.saveLocation}>Save</button>
</div>
);
}
}
AddLocation.propTypes = {
onSave: PropTypes.func.isRequired
};

View File

@ -0,0 +1,76 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import "react-datepicker/dist/react-datepicker.css";
export default class AddRestaurantVisit extends Component {
constructor(props) {
super(props);
this.state = {
visitDate: new Date()
}
this.saveFavorite = this.saveFavorite.bind(this);
}
handleDescriptionChange = event => {
this.setState({ description: event.target.value });
};
handlePriceChange = event => {
this.setState({ price: event.target.value });
};
async saveFavorite() {
var favorite = this.getFavorite();
var res = await fetch('/api/locations/restaurant/favorites',{
method: 'POST',
body: JSON.stringify(favorite),
headers: {"Content-Type": "application/json"}
});
if (res.status === 200) {
this.props.onSave();
}
}
getFavorite() {
var favorite = {
locationid: this.props.id,
details: {
description: this.state.description,
price: this.state.price
}
};
return favorite;
}
render() {
return (
<div className="form-main">
<table className="info-table " cellSpacing="10px">
<tr>
<td>Description:</td>
<td><input value={this.state.description} onChange={this.handleDescriptionChange} /></td>
</tr>
<tr>
<td>Price:</td>
<td><input value={this.state.price} onChange={this.handlePriceChange} /></td>
</tr>
</table>
<div className="centered">
<button onClick={this.saveFavorite}>Add</button>
<button className="negative" onClick={this.props.onCancel}>Cancel</button>
</div>
</div>
);
}
}
AddRestaurantVisit.propTypes = {
onSave: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
id: PropTypes.number
};

View File

@ -0,0 +1,27 @@
import React, {Component} from 'react';
import ReactDOM from 'react-dom'
import {InfoWindow} from 'google-maps-react';
export default class InfoWindowEx extends Component {
constructor(props) {
super(props);
this.infoWindowRef = React.createRef();
this.contentElement = document.createElement(`div`);
}
componentDidUpdate(prevProps) {
if (this.props.children !== prevProps.children) {
ReactDOM.render(
React.Children.only(this.props.children),
this.contentElement
);
this.infoWindowRef.current.infowindow.setContent(this.contentElement);
}
}
render() {
return <InfoWindow ref={this.infoWindowRef} {...this.props} />;
}
}

View File

@ -0,0 +1,249 @@
import React, {Component} from 'react';
import {Map, Marker, GoogleApiWrapper} from 'google-maps-react';
import Modal from 'react-modal';
import close from './../images/close.png';
import restaurant_icon from './../images/restaurant_icon.png';
import sports_icon from './../images/sports_icon.png';
import star_icon from './../images/star_icon.png';
import InfoWindowEx from './InfoWindowEx'
import AddLocation from './AddLocation';
import Restaurant from './Restaurant';
import SportsVenue from './SportsVenue';
import UpdateLastVisit from './UpdateLastVisit';
export class MapContainer extends Component {
constructor(props) {
super(props);
this.state = {
showingInfoWindow: false,
activeMarker: {},
selectedPlace: {},
locations: []
}
this.onMarkerClick = this.onMarkerClick.bind(this);
this.onMapClicked = this.onMapClicked.bind(this);
this.getIcon = this.getIcon.bind(this);
this.toggleViewLocationModal = this.toggleViewLocationModal.bind(this);
this.toggleAddLocationModal = this.toggleAddLocationModal.bind(this);
this.toggleUpdateLastVisitModal = this.toggleUpdateLastVisitModal.bind(this);
this.loadLocations = this.loadLocations.bind(this);
}
componentDidMount() {
this.loadLocations();
}
async loadLocations() {
await this.getLocations()
.then(res => this.setState({ locations: res }))
.catch(err => console.log(err));
}
async getLocations() {
const response = await fetch('/api/locations');
const body = await response.json();
if (response.status !== 200) {
throw Error(body.message)
}
return body;
};
onMarkerClick(props, marker, e) {
this.setState({
selectedPlace: props,
activeMarker: marker,
showingInfoWindow: true
});
}
onMapClicked(props) {
if (this.state.showingInfoWindow) {
this.setState({
showingInfoWindow: false,
activeMarker: null
})
}
}
getIcon(type) {
if (type === 'R') {
return restaurant_icon;
}
else if (type === 'S') {
return sports_icon;
}
else {
return star_icon;
}
}
openModal(location,target) {
this.setState({
locationid: location.id,
locationtype: location.type,
locationname: location.name
});
target();
}
toggleViewLocationModal() {
this.setState({
isViewLocationOpen: !this.state.isViewLocationOpen
});
}
toggleAddLocationModal(reload) {
this.setState({
isAddLocationOpen: !this.state.isAddLocationOpen
})
if (reload) {
this.loadLocations();
}
}
toggleUpdateLastVisitModal(reload) {
this.setState({
isUpdateLastVisitOpen: !this.state.isUpdateLastVisitOpen
})
if (reload) {
this.loadLocations();
}
}
viewLocation() {
if (this.state.locationtype === 'R') {
return(<Restaurant id={this.state.locationid} onUpdate={this.loadLocations}></Restaurant>);
}
else if (this.state.locationtype === 'S') {
return(<SportsVenue id={this.state.locationid} onUpdate={this.loadLocations}></SportsVenue>);
}
}
getInfoWindowContents(selectedPlace) {
if (selectedPlace !== undefined &&
selectedPlace.type !== undefined ) {
var location = this.state.locations.filter(function(location) { return location.id === selectedPlace.id })[0];
if (selectedPlace.type === 'R') {
return(
<div>
<p className="actionable" onClick={() => this.openModal(location, this.toggleViewLocationModal)}>View Details</p>
<p className="small-text-title">{location.description}</p>
</div>
);
}
else if (selectedPlace.type === 'S') {
return(
<div>
<p className="actionable" onClick={() => this.openModal(location, this.toggleViewLocationModal)}>View Details</p>
<p className="small-text-title">{location.description}</p>
</div>
);
}
else {
return(
<div>
<div>
<p className="small-text-title">Last visited</p>
{location.description}
</div>
<p className="actionable" onClick={() => this.openModal(location, this.toggleUpdateLastVisitModal)}>Update Last Visit</p>
</div>
);
}
}
}
renderLocations() {
return this.state.locations.map(location => (
<Marker onClick={this.onMarkerClick}
id={location.id}
name={location.name}
type={location.type}
description={location.description}
position={{ lat: location.latitude, lng: location.longitude }}
icon={{ url: this.getIcon(location.type) }}/>
))
}
render() {
return (
<div>
<Map google={this.props.google}
zoom={14}
onClick={this.onMapClicked}
initialCenter={{
lat: 41.8781,
lng: -87.6298
}}>
{this.renderLocations()}
<InfoWindowEx
marker={this.state.activeMarker}
visible={this.state.showingInfoWindow}>
<div className="centered location-details">
<h3>{this.state.selectedPlace.name}</h3>
{this.getInfoWindowContents(this.state.selectedPlace)}
</div>
</InfoWindowEx>
</Map>
<button className="add-location-actionable" onClick={this.toggleAddLocationModal}>Add Location</button>
<Modal
isOpen={this.state.isViewLocationOpen}
onRequestClose={this.toggleViewLocationModal}
className="modal"
overlayClassName="overlay">
<div>
<img src={close} className="modal-close" alt="close" onClick={this.toggleViewLocationModal} />
<div className="modal-title">
<h3>{this.state.locationname}</h3>
</div>
{this.viewLocation()}
</div>
</Modal>
<Modal
isOpen={this.state.isAddLocationOpen}
onRequestClose={() => this.toggleAddLocationModal(false)}
className="modal"
overlayClassName="overlay">
<div>
<img src={close} className="modal-close" alt="close" onClick={() => this.toggleAddLocationModal(false)} />
<div className="modal-title">
<h3>Add Location</h3>
</div>
<AddLocation onSave={() => this.toggleAddLocationModal(true)} />
</div>
</Modal>
<Modal
isOpen={this.state.isUpdateLastVisitOpen}
onRequestClose={this.toggleUpdateLastVisitModal}
className="modal"
overlayClassName="overlay">
<div>
<img src={close} className="modal-close" alt="close" onClick={() => this.toggleUpdateLastVisitModal(false)} />
<div className="modal-title">
<h3>Update Last Visit</h3>
</div>
<UpdateLastVisit id={this.state.locationid} name={this.state.locationname} onSave={() => this.toggleUpdateLastVisitModal(true)} />
</div>
</Modal>
</div>
);
}
}
export default GoogleApiWrapper({
apiKey: ("ENTER_GOOGLE_API_KEY")
})(MapContainer)

View File

@ -0,0 +1,124 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import AddRestaurantFavorite from './AddRestaurantFavorite';
export default class Restaurant extends Component {
constructor(props) {
super(props);
this.state = {
addFavorite: false,
restaurant: {}
}
this.toggleAddFavorite = this.toggleAddFavorite.bind(this);
this.favoriteAdded = this.favoriteAdded.bind(this);
}
componentDidMount() {
if (this.props !== undefined &&
this.props.id !== undefined &&
this.props.id !== null) {
this.loadRestaurant();
}
}
loadRestaurant() {
this.getRestaurant(this.props.id)
.then(res => this.setState({ restaurant: res }))
.catch(err => console.log(err));
}
getRestaurant = async (id) => {
const response = await fetch('/api/locations/restaurant?id=' + id);
const body = await response.json();
if (response.status !== 200) {
throw Error(body.message)
}
return body;
};
toggleAddFavorite() {
this.setState({
addFavorite: !this.state.addFavorite
})
}
favoriteAdded() {
this.toggleAddFavorite();
this.loadRestaurant();
this.props.onUpdate();
}
renderAddFavorite() {
if (this.state.addFavorite) {
return (
<div>
<AddRestaurantFavorite id={this.props.id} onSave={this.favoriteAdded} onCancel={this.toggleAddFavorite} />
</div>
);
}
else {
return(<button onClick={this.toggleAddFavorite}>Add Favorite Meal</button>);
}
}
renderFavoriteItems(favorites) {
return favorites.map(favorite => (
<tr>
<td>{favorite.description}</td>
<td>{favorite.price}</td>
</tr>
));
}
renderFavorites() {
if (this.state.restaurant !== undefined &&
this.state.restaurant.favorites !== undefined &&
this.state.restaurant.favorites !== null) {
var favorites = JSON.parse(this.state.restaurant.favorites);
if (favorites.length > 0) {
return (
<div>
<h4>Favorite Meals</h4>
<table className="list-table">
<tr>
<th>Description</th>
<th>Price</th>
</tr>
{this.renderFavoriteItems(favorites)}
</table>
</div>
);
}
}
}
render() {
return (
<div className="form-main">
<table className="info-table">
<tr>
<td>Type:</td>
<td>{this.state.restaurant.foodType}</td>
</tr>
<tr>
<td>Menu:</td>
<td><a target="_blank" rel="noopener noreferrer" href={this.state.restaurant.menu}>{this.state.restaurant.menu}</a></td>
</tr>
</table>
{this.renderFavorites()}
{this.renderAddFavorite()}
</div>
);
}
}
Restaurant.propTypes = {
onUpdate: PropTypes.func.isRequired,
id: PropTypes.number
};

View File

@ -0,0 +1,124 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import AddEvent from './AddEvent';
export default class SportsVenue extends Component {
constructor(props) {
super(props);
this.state = {
addEvent: false,
venue: {}
}
this.toggleAddEvent = this.toggleAddEvent.bind(this);
this.eventAdded = this.eventAdded.bind(this);
}
componentDidMount() {
if (this.props !== undefined &&
this.props.id !== undefined &&
this.props.id !== null) {
this.loadVenue();
}
}
loadVenue() {
this.getVenue(this.props.id)
.then(res => this.setState({ venue: res }))
.catch(err => console.log(err));
}
getVenue = async (id) => {
const response = await fetch('/api/locations/sportsvenue?id=' + id);
const body = await response.json();
if (response.status !== 200) {
throw Error(body.message)
}
return body;
};
toggleAddEvent() {
this.setState({
addEvent: !this.state.addEvent
})
}
eventAdded() {
this.toggleAddEvent();
this.loadVenue();
this.props.onUpdate();
}
renderAddEvent() {
if (this.state.addEvent) {
return (
<div>
<AddEvent id={this.props.id} onSave={this.eventAdded} onCancel={this.toggleAddEvent} />
</div>
);
}
else {
return(<button onClick={this.toggleAddEvent}>Add Event</button>);
}
}
renderEventItems(events) {
return events.map(event => (
<tr>
<td>{event.date}</td>
<td>{event.description}</td>
</tr>
));
}
renderEvents() {
if (this.state.venue !== undefined &&
this.state.venue.events !== undefined &&
this.state.venue.events !== null) {
var events = JSON.parse(this.state.venue.events);
if (events.length > 0) {
return (
<div>
<h4>Events</h4>
<table className="list-table">
<tr>
<th>Date</th>
<th>Description</th>
</tr>
{this.renderEventItems(events)}
</table>
</div>
);
}
}
}
render() {
return (
<div class="form-main">
<table className="info-table">
<tr>
<td>Year Opened:</td>
<td>{this.state.venue.yearOpened}</td>
</tr>
<tr>
<td>Max Capacity:</td>
<td>{this.state.venue.capacity}</td>
</tr>
</table>
{this.renderEvents()}
{this.renderAddEvent()}
</div>
);
}
}
SportsVenue.propTypes = {
onUpdate: PropTypes.func.isRequired,
id: PropTypes.object
};

View File

@ -0,0 +1,56 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
export default class UpdateLastVisit extends Component {
constructor(props) {
super(props);
this.state = {
visitDate: new Date()
}
this.handleChange = this.handleChange.bind(this);
this.saveLastVisitedDate = this.saveLastVisitedDate.bind(this);
}
handleChange(date) {
this.setState({
visitDate: date
});
}
async saveLastVisitedDate() {
const response = await fetch('/api/locations/attractions?id=' + this.props.id + '&dt=' + this.state.visitDate.toLocaleDateString(), { method: 'PUT'});
if (response.status === 200) {
this.props.onSave();
}
}
render() {
return (
<div className="form-main">
<table className="info-table">
<tr>
<td>Attraction:</td>
<td>{this.props.name}</td>
</tr>
<tr>
<td>Date:</td>
<td><DatePicker selected={this.state.visitDate} onChange={this.handleChange} /></td>
</tr>
</table>
<button onClick={this.saveLastVisitedDate}>Save</button>
</div>
);
}
}
UpdateLastVisit.propTypes = {
onSave: PropTypes.func.isRequired,
id: PropTypes.number,
name: PropTypes.string
};

BIN
client/src/images/close.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 666 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 839 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

13
client/src/index.css Normal file
View File

@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}

12
client/src/index.js Normal file
View File

@ -0,0 +1,12 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
ReactDOM.render(<App />, document.getElementById('root'));
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

1
client/src/logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.0 KiB

135
client/src/serviceWorker.js Normal file
View File

@ -0,0 +1,135 @@
// This optional code is used to register a service worker.
// register() is not called by default.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.
// To learn more about the benefits of this model and instructions on how to
// opt-in, read https://bit.ly/CRA-PWA
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export function register(config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://bit.ly/CRA-PWA'
);
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
}
});
}
}
function registerValidSW(swUrl, config) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
);
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl, config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type');
if (
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
}

BIN
media/cli_root.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

BIN
media/map.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 443 KiB

BIN
media/npm_start.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 KiB

BIN
media/platform.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 KiB

BIN
media/skysql.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB