Initial additions
185
README.md
Normal 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. It’s 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>
|
||||
[](https://opensource.org/licenses/MIT)
|
6
api/README.md
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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"
|
||||
}
|
||||
}
|
141
api/nodejs/routes/locationRoutes.js
Normal 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
@ -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
@ -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
@ -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
@ -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 can’t go back!**
|
||||
|
||||
If you aren’t 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 you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t 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
@ -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
36
client/package.json
Normal 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
After Width: | Height: | Size: 15 KiB |
43
client/public/index.html
Normal 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
After Width: | Height: | Size: 8.4 KiB |
BIN
client/public/logo512.png
Normal file
After Width: | Height: | Size: 22 KiB |
25
client/public/manifest.json
Normal 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
@ -0,0 +1,2 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
160
client/src/App.css
Normal 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
@ -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
@ -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);
|
||||
});
|
82
client/src/components/AddEvent.js
Normal 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
|
||||
};
|
189
client/src/components/AddLocation.js
Normal 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
|
||||
};
|
76
client/src/components/AddRestaurantFavorite.js
Normal 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
|
||||
};
|
27
client/src/components/InfoWindowEx.js
Normal 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} />;
|
||||
}
|
||||
}
|
249
client/src/components/MapContainer.js
Normal 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)
|
124
client/src/components/Restaurant.js
Normal 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
|
||||
};
|
124
client/src/components/SportsVenue.js
Normal 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
|
||||
};
|
56
client/src/components/UpdateLastVisit.js
Normal 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
After Width: | Height: | Size: 4.0 KiB |
BIN
client/src/images/restaurant_icon.png
Normal file
After Width: | Height: | Size: 666 B |
BIN
client/src/images/sports_icon.png
Normal file
After Width: | Height: | Size: 839 B |
BIN
client/src/images/star_icon.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
13
client/src/index.css
Normal 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
@ -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
After Width: | Height: | Size: 8.0 KiB |
135
client/src/serviceWorker.js
Normal 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
After Width: | Height: | Size: 125 KiB |
BIN
media/map.png
Normal file
After Width: | Height: | Size: 443 KiB |
BIN
media/npm_start.png
Normal file
After Width: | Height: | Size: 432 KiB |
BIN
media/platform.png
Normal file
After Width: | Height: | Size: 233 KiB |
BIN
media/skysql.png
Normal file
After Width: | Height: | Size: 279 KiB |