diff --git a/.circleci/config.yml b/.circleci/config.yml
index 64726e52f..02ad4d579 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -1,10 +1,47 @@
# JOB DEFINITIONS
-version: 2
+version: 2.1
+orbs:
+ node: circleci/node@5.1.0
+commands:
+ aws-deploy:
+ parameters:
+ build_container_repository:
+ type: env_var_name
+ default: BUILD_CONTAINER_REPOSITORY
+ ecs_cluster:
+ type: string
+ default: ""
+ migration_task:
+ type: string
+ default: ""
+ service_name:
+ type: string
+ default: ""
+ task_family:
+ type: string
+ default: ""
+ steps:
+ - run:
+ name: Deploy
+ command: |
+ aws configure set default.region us-west-2
+ CLEAN_BRANCH=`echo ${CIRCLE_BRANCH} | sed 's/\//-/g'`
+ DOCKER_TAG=${<< parameters.build_container_repository >>}:${CIRCLE_SHA1}
+ CURRENT_TASK=`aws ecs list-task-definitions --status ACTIVE --family-prefix << parameters.task_family >> --sort DESC | jq -r '.taskDefinitionArns[0]'`
+ TASK_JSON=`aws ecs describe-task-definition --task-definition ${CURRENT_TASK} | jq --arg DOCKER_TAG "$DOCKER_TAG" '.taskDefinition.containerDefinitions[0].image = $DOCKER_TAG | {containerDefinitions: .taskDefinition.containerDefinitions, family: .taskDefinition.family}'`
+ aws ecs register-task-definition --cli-input-json "${TASK_JSON}" > /dev/null
+ CURRENT_MIGRATION_TASK=`aws ecs list-task-definitions --status ACTIVE --family-prefix << parameters.migration_task >> --sort DESC | jq -r '.taskDefinitionArns[0]'`
+ MIGRATION_TASK_JSON=`aws ecs describe-task-definition --task-definition ${CURRENT_MIGRATION_TASK} | jq --arg DOCKER_TAG "$DOCKER_TAG" '.taskDefinition.containerDefinitions[0].image = $DOCKER_TAG | {containerDefinitions: .taskDefinition.containerDefinitions, family: .taskDefinition.family}'`
+ aws ecs register-task-definition --cli-input-json "${MIGRATION_TASK_JSON}" > /dev/null
+ aws ecs run-task --cluster << parameters.ecs_cluster >> --task-definition << parameters.migration_task >>
+ NEW_TASK=`aws ecs list-task-definitions --status ACTIVE --family-prefix << parameters.task_family >> --sort DESC | jq -r '.taskDefinitionArns[0]'`
+ aws ecs update-service --service << parameters.service_name >> --cluster << parameters.ecs_cluster >> --task-definition ${NEW_TASK}
+
jobs:
test:
docker:
# image for running tests
- - image: cypress/browsers:node16.14.0-chrome99-ff97
+ - image: cypress/browsers:node18.12.0-chrome103-ff107
environment:
- DB_NAME=lunch_test
- DB_USER=lunch_test
@@ -68,9 +105,9 @@ jobs:
echo 127.0.0.1 integration-test.local.lunch.pink | tee -a /etc/hosts
# build the app
- - run:
- name: build-release
- command: NODE_ENV=test npm run build
+ # - run:
+ # name: build-release
+ # command: NODE_ENV=test npm run build
- run:
name: integration-tests
@@ -89,68 +126,83 @@ jobs:
build:
docker:
- - image: ${BUILD_IMAGE}
+ - image: cimg/aws:2023.01
- working_directory: ~/repo
+ # working_directory: ~/repo
steps:
- checkout
+ - setup_remote_docker:
+ version: 20.10.11
+ docker_layer_caching: true
+
- run:
name: dotenv
command: touch .env && touch .env.prod
- # Download and cache dependencies
- - restore_cache:
- keys:
- - v1-build-dependencies-{{ checksum "package.json" }}
- # fallback to using the latest cache if no exact match is found
- - v1-build-dependencies-
+ # # Download and cache dependencies
+ # - restore_cache:
+ # keys:
+ # - v1-build-dependencies-{{ checksum "package.json" }}
+ # # fallback to using the latest cache if no exact match is found
+ # - v1-build-dependencies-
- # install deps
- - run:
- name: yarn-install
- command: yarn install
+ # # install deps
+ # - run:
+ # name: yarn-install
+ # command: yarn install
- - save_cache:
- paths:
- - node_modules
- key: v1-build-dependencies-{{ checksum "package.json" }}
+ # - save_cache:
+ # paths:
+ # - node_modules
+ # key: v1-build-dependencies-{{ checksum "package.json" }}
+
+ - node/install-packages:
+ pkg-manager: yarn
# build the release
- run:
name: build-release
command: npm run build -- --release --verbose
- - setup_remote_docker
+ # - setup_remote_docker
- # build and push a docker image (script is defined in the custom build image)
+ # build and push a docker image
- run:
name: build-docker-image
- command: ../buildImage.sh
+ command: |
+ CLEAN_BRANCH=`echo $CIRCLE_BRANCH | sed 's/\//-/g'`
+ DOCKER_TAG=${BUILD_CONTAINER_REPOSITORY}:${CIRCLE_SHA1}
+ docker build --tag ${DOCKER_TAG} .
+ aws ecr get-login-password --region us-west-2 | docker login --username AWS --password-stdin ${CONTAINER_REGISTRY}
+ docker push ${DOCKER_TAG}
deploy-staging:
docker:
- - image: ${BUILD_IMAGE}
+ - image: cimg/aws:2023.01
- working_directory: ~/repo
+ # working_directory: ~/repo
steps:
- # update ECS task and service with image build above (script is defined in custom build image)
- - run:
- name: update-service
- command: ../deployStaging.sh
+ - aws-deploy:
+ task_family: lunch-staging
+ ecs_cluster: Lunch-Staging
+ service_name: lunch-staging
+ migration_task: staging_lunch_migrate
deploy-production:
docker:
- - image: ${BUILD_IMAGE}
+ - image: cimg/aws:2023.01
- working_directory: ~/repo
+ # working_directory: ~/repo
steps:
- - run:
- name: update_service
- command: ../deployProduction.sh
+ - aws-deploy:
+ task_family: lunch
+ ecs_cluster: Lunch
+ service_name: lunch
+ migration_task: lunch_migrate
# WORKFLOW DEFINITIONS
workflows:
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 000000000..927f46241
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,5 @@
+Dockerfile
+node_modules
+tmp
+temp
+postgres-data
diff --git a/.editorconfig b/.editorconfig
deleted file mode 100644
index 959db8a94..000000000
--- a/.editorconfig
+++ /dev/null
@@ -1,20 +0,0 @@
-# EditorConfig helps developers define and maintain consistent
-# coding styles between different editors and IDEs
-# http://editorconfig.org
-
-root = true
-
-[*]
-
-# Change these settings to your own preference
-indent_style = space
-indent_size = 2
-
-# We recommend you to keep these unchanged
-end_of_line = lf
-charset = utf-8
-trim_trailing_whitespace = true
-insert_final_newline = true
-
-# editorconfig-tools is unable to ignore longs strings or urls
-max_line_length = null
diff --git a/.env.sample b/.env.sample
index 2d528adad..271166b07 100644
--- a/.env.sample
+++ b/.env.sample
@@ -11,7 +11,7 @@ SUPERUSER_EMAIL=
# Credentials for your Google Developer app
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
-GOOGLE_CLIENT_APIKEY= # optional
+GOOGLE_CLIENT_APIKEY=
GOOGLE_SERVER_APIKEY=
# Google Analytics ID
diff --git a/.eslintrc.js b/.eslintrc.js
index 279053a6e..bc76ede5a 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -10,17 +10,18 @@
// ESLint configuration
// http://eslint.org/docs/user-guide/configuring
module.exports = {
- parser: 'babel-eslint',
+ parser: '@typescript-eslint/parser',
extends: [
'airbnb',
- 'plugin:flowtype/recommended',
'plugin:css-modules/recommended',
+ 'plugin:@typescript-eslint/recommended'
],
plugins: [
- 'flowtype',
'css-modules',
+ '@typescript-eslint',
+ 'import'
],
globals: {
@@ -31,16 +32,18 @@ module.exports = {
browser: true,
},
+ root: true,
+
rules: {
// `js` and `jsx` are common extensions
// `mjs` is for `universal-router` only, for now
'import/extensions': [
'error',
- 'always',
{
js: 'never',
jsx: 'never',
mjs: 'never',
+ ts: 'never',
},
],
@@ -77,6 +80,7 @@ module.exports = {
'key-spacing': 0,
'no-confusing-arrow': 0,
'react/jsx-quotes': 0,
+ 'react/jsx-props-no-spreading': 0,
'max-len': 0,
'jsx-quotes': [
2,
@@ -88,6 +92,13 @@ module.exports = {
'react/forbid-prop-types': 'off',
'react/destructuring-assignment': 'off',
+ 'react/function-component-definition': ['error', {
+ namedComponents: 'arrow-function',
+ unnamedComponents: 'arrow-function'
+ }],
+ 'react/static-property-placement': 'off',
+ 'import/no-relative-packages': 'off',
+ 'import/no-import-module-exports': 'off'
},
settings: {
@@ -95,8 +106,16 @@ module.exports = {
// https://github.com/benmosher/eslint-plugin-import/tree/master/resolvers
'import/resolver': {
node: {
+ extensions: ['.js', '.jsx', '.ts', '.tsx'],
moduleDirectory: ['node_modules', 'src'],
},
+ typescript: {
+ alwaysTryTypes: true, // always try to resolve types under `@types` directory even it doesn't contain any source code, like `@types/unist`
+ extensions: ['.ts', '.tsx'],
+ }
+ },
+ 'import/parsers': {
+ '@typescript-eslint/parser': ['.ts', '.tsx'],
},
},
};
diff --git a/.gitignore b/.gitignore
index b4c6bacfd..82f73ff77 100644
--- a/.gitignore
+++ b/.gitignore
@@ -43,3 +43,7 @@ cypress/videos/
deploy.sh
test-results.xml
screenshots
+Dockerfile.bak
+
+#Docker postgres-data
+postgres-data
\ No newline at end of file
diff --git a/.husky/pre-commit b/.husky/pre-commit
new file mode 100755
index 000000000..d8335613d
--- /dev/null
+++ b/.husky/pre-commit
@@ -0,0 +1,4 @@
+#!/usr/bin/env sh
+. "$(dirname -- "$0")/_/husky.sh"
+
+npm test && npx lint-staged
diff --git a/.mocharc.js b/.mocharc.js
new file mode 100644
index 000000000..9a40fe7e6
--- /dev/null
+++ b/.mocharc.js
@@ -0,0 +1,6 @@
+module.exports = {
+ extension: ['ts'],
+ require: ['./test/setup'],
+ exit: true,
+ file: './test/mocha-setup',
+};
diff --git a/.nvmrc b/.nvmrc
new file mode 100644
index 000000000..6f1f18665
--- /dev/null
+++ b/.nvmrc
@@ -0,0 +1 @@
+v18.14.0
\ No newline at end of file
diff --git a/.stylelintrc.js b/.stylelintrc.js
index 32eef2b23..b94a4ff48 100644
--- a/.stylelintrc.js
+++ b/.stylelintrc.js
@@ -7,13 +7,15 @@
* LICENSE.txt file in the root directory of this source tree.
*/
+const lowerKebabCase = /^[a-z][a-zA-Z0-9]+$/;
+
// stylelint configuration
// https://stylelint.io/user-guide/configuration/
module.exports = {
// The standard config based on a handful of CSS style guides
// https://github.com/stylelint/stylelint-config-standard
- extends: 'stylelint-config-standard',
+ extends: 'stylelint-config-standard-scss',
plugins: [
// stylelint plugin to sort CSS rules content with specified order
@@ -22,13 +24,10 @@ module.exports = {
],
rules: {
- 'at-rule-no-unknown': [
- true,
- {
- ignoreAtRules: ['include', 'mixin'],
- },
- ],
+ 'at-rule-no-unknown': null,
+ 'scss/at-rule-no-unknown': true,
'declaration-empty-line-before': null,
+ 'keyframes-name-pattern': lowerKebabCase,
'number-leading-zero': 'never',
'property-no-unknown': [true, {
ignoreProperties: [
@@ -38,6 +37,7 @@ module.exports = {
'overflow-anchor'
],
}],
+ 'selector-class-pattern': lowerKebabCase,
'selector-pseudo-class-no-unknown': [
true,
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 000000000..3662b3700
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "typescript.tsdk": "node_modules/typescript/lib"
+}
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index 8ac264ff4..57bdba022 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM node:16.17.0
+FROM node:18.14.0
# Set a working directory
WORKDIR /usr/src/app
diff --git a/Dockerfile.local b/Dockerfile.local
new file mode 100644
index 000000000..84d17439b
--- /dev/null
+++ b/Dockerfile.local
@@ -0,0 +1,12 @@
+FROM node:18.14.0
+
+# Set a working directory
+WORKDIR /app
+
+# Install dependencies based on the preferred package manager
+COPY package.json yarn.lock ./
+RUN yarn --frozen-lockfile
+
+COPY . .
+
+CMD [ "npm", "start" ]
diff --git a/babel.config.js b/babel.config.js
index 1fddbc11f..a8301dabc 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -11,11 +11,11 @@
// https://babeljs.io/docs/usage/api/
module.exports = {
plugins: [
- '@babel/plugin-proposal-class-properties',
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-transform-modules-commonjs',
],
presets: [
+ ['@babel/preset-typescript', { allowDeclareFields: true }],
[
'@babel/preset-env',
{
@@ -24,7 +24,6 @@ module.exports = {
},
},
],
- '@babel/preset-flow',
'@babel/preset-react',
],
ignore: ['node_modules', 'build'],
diff --git a/cypress.config.js b/cypress.config.js
index b9d05fd36..c7f0fa577 100644
--- a/cypress.config.js
+++ b/cypress.config.js
@@ -1,11 +1,16 @@
+/* eslint-disable @typescript-eslint/no-var-requires */
+
const { defineConfig } = require('cypress');
+require('dotenv').config({ path: './.env.test' });
module.exports = defineConfig({
port: 4000,
env: {
- subdomain: 'https://integration-test.local.lunch.pink:3000/',
+ subdomain: 'http://integration-test.local.lunch.pink:3000/',
+ superuserEmail: process.env.SUPERUSER_EMAIL,
+ superuserPassword: process.env.SUPERUSER_PASSWORD,
},
e2e: {
- baseUrl: 'https://local.lunch.pink:3000/',
+ baseUrl: 'http://local.lunch.pink:3000/',
},
});
diff --git a/cypress/e2e/lunch_tests/login.cy.js b/cypress/e2e/lunch_tests/login.cy.js
index 447e861bd..b813d6259 100644
--- a/cypress/e2e/lunch_tests/login.cy.js
+++ b/cypress/e2e/lunch_tests/login.cy.js
@@ -1,6 +1,9 @@
/* eslint-disable no-undef */
import * as helpers from '../../support/helpers';
+const superuserEmail = Cypress.env('superuserEmail');
+const superuserPassword = Cypress.env('superuserPassword');
+
describe('login page', () => {
beforeEach(() => {
cy.visit('/login');
@@ -13,8 +16,8 @@ describe('login page', () => {
});
it('logs in and out successfully', () => {
- cy.get('#login-email').type('test@lunch.pink');
- cy.get('#login-password').type('test');
+ cy.get('#login-email').type(superuserEmail);
+ cy.get('#login-password').type(superuserPassword);
cy.get('button[type="submit"]').click();
helpers.deleteTeam();
cy.contains('You’re not currently a part of any teams!');
diff --git a/cypress/support/helpers/addRestaurant.js b/cypress/support/helpers/addRestaurant.js
index 02e12c5f9..b8e3ce813 100644
--- a/cypress/support/helpers/addRestaurant.js
+++ b/cypress/support/helpers/addRestaurant.js
@@ -5,10 +5,10 @@ export default () => {
const lat = 37.7955703;
const lng = -122.39332079999997;
const name = 'Ferry Building Marketplace';
- const place_id = 'ChIJWTGPjmaAhYARxz6l1hOj92w';
+ const placeId = 'ChIJWTGPjmaAhYARxz6l1hOj92w';
cy.request('POST', `${Cypress.env('subdomain')}api/restaurants`, {
name,
- place_id,
+ placeId,
address,
lat,
lng
diff --git a/cypress/support/helpers/deleteRestaurant.js b/cypress/support/helpers/deleteRestaurant.js
index 0afb44214..eddc779a1 100644
--- a/cypress/support/helpers/deleteRestaurant.js
+++ b/cypress/support/helpers/deleteRestaurant.js
@@ -2,7 +2,7 @@
export default () => {
cy.visit('/');
cy.get('button.RestaurantDropdown-toggle').click();
- cy.get('ul.RestaurantDropdown-menu li').contains('Delete').should('be.visible').click();
+ cy.get('.RestaurantDropdown-menu a').contains('Delete').should('be.visible').click();
cy.get('body').should('have.attr', 'class', 'modal-open');
cy.get('.modal-footer .btn-primary').contains('Delete').click();
};
diff --git a/cypress/support/helpers/login.js b/cypress/support/helpers/login.js
index 44295765a..838421748 100644
--- a/cypress/support/helpers/login.js
+++ b/cypress/support/helpers/login.js
@@ -1,7 +1,10 @@
/* eslint-disable no-undef */
+const superuserEmail = Cypress.env('superuserEmail');
+const superuserPassword = Cypress.env('superuserPassword');
+
export default () => {
- const email = 'test@lunch.pink';
- const password = 'test';
+ const email = superuserEmail;
+ const password = superuserPassword;
cy.request('POST', '/login', {
email,
password
diff --git a/db/migrations/20160303171210-AddGoogleParamsToRestaurants.js b/db/migrations/20160303171210-AddGoogleParamsToRestaurants.js
index 3beb92703..1aead39d0 100644
--- a/db/migrations/20160303171210-AddGoogleParamsToRestaurants.js
+++ b/db/migrations/20160303171210-AddGoogleParamsToRestaurants.js
@@ -1,6 +1,4 @@
-const Promise = require('bluebird');
-
-exports.up = (queryInterface, Sequelize) => Promise.all(
+exports.up = (queryInterface, Sequelize) => Promise.all([
queryInterface.addColumn('restaurants', 'place_id', {
type: Sequelize.STRING,
unique: true
@@ -11,10 +9,10 @@ exports.up = (queryInterface, Sequelize) => Promise.all(
queryInterface.addColumn('restaurants', 'lng', {
type: Sequelize.FLOAT
})
-);
+]);
-exports.down = queryInterface => Promise.all(
+exports.down = queryInterface => Promise.all([
queryInterface.removeColumn('restaurants', 'place_id'),
queryInterface.removeColumn('restaurants', 'lat'),
queryInterface.removeColumn('restaurants', 'lng')
-);
+]);
diff --git a/db/migrations/20160308134329-AddTimestampsToRestaurants.js b/db/migrations/20160308134329-AddTimestampsToRestaurants.js
index a33b55b54..e04a53bae 100644
--- a/db/migrations/20160308134329-AddTimestampsToRestaurants.js
+++ b/db/migrations/20160308134329-AddTimestampsToRestaurants.js
@@ -1,6 +1,4 @@
-const Promise = require('bluebird');
-
-exports.up = (queryInterface, Sequelize) => Promise.all(
+exports.up = (queryInterface, Sequelize) => Promise.all([
queryInterface.addColumn('restaurants', 'created_at', {
type: Sequelize.DATE,
allowNull: false
@@ -9,9 +7,9 @@ exports.up = (queryInterface, Sequelize) => Promise.all(
type: Sequelize.DATE,
allowNull: false
})
-);
+]);
-exports.down = queryInterface => Promise.all(
+exports.down = queryInterface => Promise.all([
queryInterface.removeColumn('restaurants', 'created_at'),
queryInterface.removeColumn('restaurants', 'updated_at')
-);
+]);
diff --git a/db/migrations/20160308134819-AddTimestampsToVotes.js b/db/migrations/20160308134819-AddTimestampsToVotes.js
index 9849c7366..28b851580 100644
--- a/db/migrations/20160308134819-AddTimestampsToVotes.js
+++ b/db/migrations/20160308134819-AddTimestampsToVotes.js
@@ -1,6 +1,4 @@
-const Promise = require('bluebird');
-
-exports.up = (queryInterface, Sequelize) => Promise.all(
+exports.up = (queryInterface, Sequelize) => Promise.all([
queryInterface.addColumn('votes', 'created_at', {
type: Sequelize.DATE,
allowNull: false
@@ -9,9 +7,9 @@ exports.up = (queryInterface, Sequelize) => Promise.all(
type: Sequelize.DATE,
allowNull: false
})
-);
+]);
-exports.down = queryInterface => Promise.all(
+exports.down = queryInterface => Promise.all([
queryInterface.removeColumn('votes', 'created_at'),
queryInterface.removeColumn('votes', 'updated_at')
-);
+]);
diff --git a/db/migrations/20160308134835-AddTimestampsToUsers.js b/db/migrations/20160308134835-AddTimestampsToUsers.js
index 8e20e9859..31f2bcf22 100644
--- a/db/migrations/20160308134835-AddTimestampsToUsers.js
+++ b/db/migrations/20160308134835-AddTimestampsToUsers.js
@@ -1,6 +1,4 @@
-const Promise = require('bluebird');
-
-exports.up = (queryInterface, Sequelize) => Promise.all(
+exports.up = (queryInterface, Sequelize) => Promise.all([
queryInterface.addColumn('users', 'created_at', {
type: Sequelize.DATE,
allowNull: false
@@ -9,9 +7,9 @@ exports.up = (queryInterface, Sequelize) => Promise.all(
type: Sequelize.DATE,
allowNull: false
})
-);
+]);
-exports.down = queryInterface => Promise.all(
+exports.down = queryInterface => Promise.all([
queryInterface.removeColumn('users', 'created_at'),
queryInterface.removeColumn('users', 'updated_at')
-);
+]);
diff --git a/db/migrations/20170309225630-CreateTeams.js b/db/migrations/20170309225630-CreateTeams.js
index 65c5d5466..ee0e496c3 100644
--- a/db/migrations/20170309225630-CreateTeams.js
+++ b/db/migrations/20170309225630-CreateTeams.js
@@ -1,7 +1,5 @@
-const db = require('../../src/models/db');
-
exports.up = (queryInterface, Sequelize) => {
- const Team = db.sequelize.define('team', {
+ const Team = queryInterface.sequelize.define('team', {
name: Sequelize.STRING,
}, {
underscored: true
diff --git a/db/migrations/20170309230545-AddTeamIdToRestaurants.js b/db/migrations/20170309230545-AddTeamIdToRestaurants.js
index f762ef2f8..9f2b8d618 100644
--- a/db/migrations/20170309230545-AddTeamIdToRestaurants.js
+++ b/db/migrations/20170309230545-AddTeamIdToRestaurants.js
@@ -1,7 +1,5 @@
-const db = require('../../src/models/db');
-
exports.up = (queryInterface, Sequelize) => {
- const Team = db.sequelize.define('team', {
+ const Team = queryInterface.sequelize.define('team', {
name: Sequelize.STRING,
}, {
underscored: true
diff --git a/db/migrations/20170309232003-AddTeamIdToTags.js b/db/migrations/20170309232003-AddTeamIdToTags.js
index 2620184ac..04ec5a91f 100644
--- a/db/migrations/20170309232003-AddTeamIdToTags.js
+++ b/db/migrations/20170309232003-AddTeamIdToTags.js
@@ -1,7 +1,5 @@
-const db = require('../../src/models/db');
-
exports.up = (queryInterface, Sequelize) => {
- const Team = db.sequelize.define('team', {
+ const Team = queryInterface.sequelize.define('team', {
name: Sequelize.STRING,
}, {
underscored: true
diff --git a/db/migrations/20170309233227-ChangeTagNameUniqueness.js b/db/migrations/20170309233227-ChangeTagNameUniqueness.js
index d0c0322c3..a05075472 100644
--- a/db/migrations/20170309233227-ChangeTagNameUniqueness.js
+++ b/db/migrations/20170309233227-ChangeTagNameUniqueness.js
@@ -1,10 +1,8 @@
-const db = require('../../src/models/db');
-
exports.up = (queryInterface, Sequelize) => queryInterface.changeColumn('tags', 'name', {
type: Sequelize.STRING,
allowNull: false,
unique: false
-}).then(() => db.sequelize.query('ALTER TABLE tags DROP CONSTRAINT IF EXISTS tags_name_key;'));
+}).then(() => queryInterface.sequelize.query('ALTER TABLE tags DROP CONSTRAINT IF EXISTS tags_name_key;'));
exports.down = (queryInterface, Sequelize) => queryInterface.changeColumn('tags', 'name', {
type: Sequelize.STRING,
diff --git a/db/migrations/20170310215129-ChangeUserGoogleIdNull.js b/db/migrations/20170310215129-ChangeUserGoogleIdNull.js
index 8e05e3758..256248ed6 100644
--- a/db/migrations/20170310215129-ChangeUserGoogleIdNull.js
+++ b/db/migrations/20170310215129-ChangeUserGoogleIdNull.js
@@ -1,17 +1,15 @@
-const db = require('../../src/models/db');
-
exports.up = (queryInterface, Sequelize) => queryInterface.changeColumn('users', 'google_id', {
type: Sequelize.STRING
});
exports.down = (queryInterface, Sequelize) => {
- const User = db.sequelize.define('user', {
+ const User = queryInterface.sequelize.define('user', {
google_id: Sequelize.STRING,
}, {
underscored: true
});
- User.destroy({ where: { google_id: null } }).then(() => queryInterface.changeColumn('users', 'google_id', {
+ return User.destroy({ where: { google_id: null } }).then(() => queryInterface.changeColumn('users', 'google_id', {
type: Sequelize.STRING,
allowNull: false
}));
diff --git a/db/migrations/20170310220402-CreateRoles.js b/db/migrations/20170310220402-CreateRoles.js
index 5af7f3e54..b4c01d678 100644
--- a/db/migrations/20170310220402-CreateRoles.js
+++ b/db/migrations/20170310220402-CreateRoles.js
@@ -1,7 +1,5 @@
-const db = require('../../src/models/db');
-
exports.up = (queryInterface, Sequelize) => {
- const User = db.sequelize.define('user', {
+ const User = queryInterface.sequelize.define('user', {
google_id: Sequelize.STRING,
name: Sequelize.STRING,
email: Sequelize.STRING
@@ -9,7 +7,7 @@ exports.up = (queryInterface, Sequelize) => {
underscored: true
});
- const Team = db.sequelize.define('team', {
+ const Team = queryInterface.sequelize.define('team', {
name: Sequelize.STRING,
}, {
underscored: true
@@ -61,7 +59,7 @@ exports.up = (queryInterface, Sequelize) => {
})
.then(() => User.findAll())
.then((users) => Team.findOne({ where: { name: 'Lab Zero' } }).then(team => {
- const Role = db.sequelize.define('role', {
+ const Role = queryInterface.sequelize.define('role', {
type: {
allowNull: false,
type: Sequelize.ENUM('guest', 'member', 'owner'),
@@ -101,4 +99,4 @@ exports.up = (queryInterface, Sequelize) => {
}));
};
-exports.down = queryInterface => queryInterface.dropTable('roles').then(() => db.sequelize.query('DROP TYPE enum_roles_type'));
+exports.down = queryInterface => queryInterface.dropTable('roles').then(() => queryInterface.sequelize.query('DROP TYPE enum_roles_type'));
diff --git a/db/migrations/20170310221346-AddSuperuserToUsers.js b/db/migrations/20170310221346-AddSuperuserToUsers.js
index 0e99ff8cd..03e243243 100644
--- a/db/migrations/20170310221346-AddSuperuserToUsers.js
+++ b/db/migrations/20170310221346-AddSuperuserToUsers.js
@@ -1,11 +1,9 @@
-const db = require('../../src/models/db');
-
exports.up = (queryInterface, Sequelize) => queryInterface.addColumn('users', 'superuser', {
allowNull: false,
type: Sequelize.BOOLEAN,
defaultValue: false
}).then(() => {
- const User = db.sequelize.define('user', {
+ const User = queryInterface.sequelize.define('user', {
google_id: Sequelize.STRING,
name: Sequelize.STRING,
email: Sequelize.STRING,
diff --git a/db/migrations/20170317163119-AddTeamIdToDecisions.js b/db/migrations/20170317163119-AddTeamIdToDecisions.js
index 51d68fd10..5fd76ff2c 100644
--- a/db/migrations/20170317163119-AddTeamIdToDecisions.js
+++ b/db/migrations/20170317163119-AddTeamIdToDecisions.js
@@ -1,7 +1,5 @@
-const db = require('../../src/models/db');
-
exports.up = (queryInterface, Sequelize) => {
- const Team = db.sequelize.define('team', {
+ const Team = queryInterface.sequelize.define('team', {
name: Sequelize.STRING,
slug: Sequelize.STRING(63)
}, {
diff --git a/db/migrations/20170317172934-ChangeTeamSlugAllowNull.js b/db/migrations/20170317172934-ChangeTeamSlugAllowNull.js
index c05a2c5da..aa7be92f5 100644
--- a/db/migrations/20170317172934-ChangeTeamSlugAllowNull.js
+++ b/db/migrations/20170317172934-ChangeTeamSlugAllowNull.js
@@ -1,7 +1,5 @@
-const db = require('../../src/models/db');
-
exports.up = (queryInterface, Sequelize) => {
- const Team = db.sequelize.define('team', {
+ const Team = queryInterface.sequelize.define('team', {
name: Sequelize.STRING,
slug: Sequelize.STRING(63)
}, {
diff --git a/db/migrations/20170328222012-AddLatLngAndAddressToTeams.js b/db/migrations/20170328222012-AddLatLngAndAddressToTeams.js
index 9a299bc61..c7d7b5aa2 100644
--- a/db/migrations/20170328222012-AddLatLngAndAddressToTeams.js
+++ b/db/migrations/20170328222012-AddLatLngAndAddressToTeams.js
@@ -1,6 +1,4 @@
-const Promise = require('bluebird');
-
-exports.up = (queryInterface, Sequelize) => Promise.all(
+exports.up = (queryInterface, Sequelize) => Promise.all([
queryInterface.addColumn('teams', 'lat', {
type: Sequelize.DOUBLE
}),
@@ -10,10 +8,10 @@ exports.up = (queryInterface, Sequelize) => Promise.all(
queryInterface.addColumn('teams', 'address', {
type: Sequelize.STRING
})
-);
+]);
-exports.down = queryInterface => Promise.all(
+exports.down = queryInterface => Promise.all([
queryInterface.removeColumn('teams', 'lat'),
queryInterface.removeColumn('teams', 'lng'),
queryInterface.removeColumn('teams', 'address')
-);
+]);
diff --git a/db/migrations/20170328223830-ChangeLatLngAddressAllowNull.js b/db/migrations/20170328223830-ChangeLatLngAddressAllowNull.js
index a877930b0..5223b38b7 100644
--- a/db/migrations/20170328223830-ChangeLatLngAddressAllowNull.js
+++ b/db/migrations/20170328223830-ChangeLatLngAddressAllowNull.js
@@ -1,8 +1,5 @@
-const Promise = require('bluebird');
-const db = require('../../src/models/db');
-
exports.up = (queryInterface, Sequelize) => {
- const Team = db.sequelize.define('team', {
+ const Team = queryInterface.sequelize.define('team', {
name: Sequelize.STRING,
slug: Sequelize.STRING(63),
lat: Sequelize.DOUBLE,
@@ -18,7 +15,7 @@ exports.up = (queryInterface, Sequelize) => {
lng: -122.399991
}, {
where: { slug: 'labzero' }
- }).then(() => Promise.all(
+ }).then(() => Promise.all([
queryInterface.changeColumn('teams', 'lat', {
type: Sequelize.DOUBLE,
allowNull: false,
@@ -27,10 +24,10 @@ exports.up = (queryInterface, Sequelize) => {
type: Sequelize.DOUBLE,
allowNull: false,
})
- ));
+ ]));
};
-exports.down = (queryInterface, Sequelize) => Promise.all(
+exports.down = (queryInterface, Sequelize) => Promise.all([
queryInterface.changeColumn('teams', 'lat', {
allowNull: true,
type: Sequelize.DOUBLE
@@ -39,4 +36,4 @@ exports.down = (queryInterface, Sequelize) => Promise.all(
allowNull: true,
type: Sequelize.DOUBLE
})
-);
+]);
diff --git a/db/migrations/20170403143257-AddLocalAuthAttributesToUsers.js b/db/migrations/20170403143257-AddLocalAuthAttributesToUsers.js
index 676740f4b..d9b07f2d7 100644
--- a/db/migrations/20170403143257-AddLocalAuthAttributesToUsers.js
+++ b/db/migrations/20170403143257-AddLocalAuthAttributesToUsers.js
@@ -1,6 +1,4 @@
-const Promise = require('bluebird');
-
-exports.up = (queryInterface, Sequelize) => Promise.all(
+exports.up = (queryInterface, Sequelize) => Promise.all([
queryInterface.addColumn('users', 'encrypted_password', {
type: Sequelize.STRING
}),
@@ -21,13 +19,13 @@ exports.up = (queryInterface, Sequelize) => Promise.all(
queryInterface.addColumn('users', 'confirmation_sent_at', {
type: Sequelize.DATE
})
-);
+]);
-exports.down = queryInterface => Promise.all(
+exports.down = queryInterface => Promise.all([
queryInterface.removeColumn('users', 'encrypted_password'),
queryInterface.removeColumn('users', 'reset_password_token'),
queryInterface.removeColumn('users', 'reset_password_sent_at'),
queryInterface.removeColumn('users', 'confirmation_token'),
queryInterface.removeColumn('users', 'confirmed_at'),
queryInterface.removeColumn('users', 'confirmation_sent_at')
-);
+]);
diff --git a/db/migrations/20170403172215-ChangeRestaurantPlaceIdUniqueness.js b/db/migrations/20170403172215-ChangeRestaurantPlaceIdUniqueness.js
index 734a4019e..055e97362 100644
--- a/db/migrations/20170403172215-ChangeRestaurantPlaceIdUniqueness.js
+++ b/db/migrations/20170403172215-ChangeRestaurantPlaceIdUniqueness.js
@@ -1,9 +1,7 @@
-const db = require('../../src/models/db');
-
exports.up = (queryInterface, Sequelize) => queryInterface.changeColumn('restaurants', 'place_id', {
type: Sequelize.STRING,
unique: false
-}).then(() => db.sequelize.query('ALTER TABLE restaurants DROP CONSTRAINT restaurants_place_id_key;'));
+}).then(() => queryInterface.sequelize.query('ALTER TABLE restaurants DROP CONSTRAINT restaurants_place_id_key;'));
exports.down = (queryInterface, Sequelize) => queryInterface.changeColumn('restaurants', 'place_id', {
type: Sequelize.STRING,
diff --git a/db/migrations/20170417043528-ChangeUserEmailUniqueness.js b/db/migrations/20170417043528-ChangeUserEmailUniqueness.js
index c5306d52c..6ee39b54a 100644
--- a/db/migrations/20170417043528-ChangeUserEmailUniqueness.js
+++ b/db/migrations/20170417043528-ChangeUserEmailUniqueness.js
@@ -1,5 +1,3 @@
-const db = require('../../src/models/db');
-
exports.up = (queryInterface, Sequelize) => queryInterface.changeColumn('users', 'email', {
type: Sequelize.STRING,
allowNull: false,
@@ -10,4 +8,4 @@ exports.down = (queryInterface, Sequelize) => queryInterface.changeColumn('users
type: Sequelize.STRING,
allowNull: true,
unique: false
-}).then(() => db.sequelize.query('ALTER TABLE users DROP CONSTRAINT IF EXISTS email_unique_idx;'));
+}).then(() => queryInterface.sequelize.query('ALTER TABLE users DROP CONSTRAINT IF EXISTS email_unique_idx;'));
diff --git a/db/migrations/20180115184107-AddSortDurationToTeams.js b/db/migrations/20180115184107-AddSortDurationToTeams.js
index e087ccac8..ff08c8ed0 100644
--- a/db/migrations/20180115184107-AddSortDurationToTeams.js
+++ b/db/migrations/20180115184107-AddSortDurationToTeams.js
@@ -1,13 +1,9 @@
module.exports = {
- up: (queryInterface, Sequelize) => {
- queryInterface.addColumn('teams', 'sort_duration', {
- type: Sequelize.INTEGER,
- allowNull: false,
- defaultValue: 28
- });
- },
+ up: (queryInterface, Sequelize) => queryInterface.addColumn('teams', 'sort_duration', {
+ type: Sequelize.INTEGER,
+ allowNull: false,
+ defaultValue: 28
+ }),
- down: (queryInterface) => {
- queryInterface.removeColumn('teams', 'sort_duration');
- }
+ down: (queryInterface) => queryInterface.removeColumn('teams', 'sort_duration'),
};
diff --git a/db/migrations/20180503213015-AddIndicesToAppropriateColumns.js b/db/migrations/20180503213015-AddIndicesToAppropriateColumns.js
index 6ac9ad454..d72a7d20f 100644
--- a/db/migrations/20180503213015-AddIndicesToAppropriateColumns.js
+++ b/db/migrations/20180503213015-AddIndicesToAppropriateColumns.js
@@ -1,27 +1,27 @@
module.exports = {
- up: (queryInterface) => {
- queryInterface.addIndex('decisions', ['created_at']);
- queryInterface.addIndex('decisions', ['restaurant_id']);
- queryInterface.addIndex('restaurants_tags', ['restaurant_id']);
- queryInterface.addIndex('restaurants_tags', ['tag_id']);
- queryInterface.addIndex('roles', ['team_id']);
- queryInterface.addIndex('roles', ['user_id']);
- queryInterface.addIndex('teams', ['created_at']);
- queryInterface.addIndex('votes', ['created_at']);
- queryInterface.addIndex('votes', ['restaurant_id']);
- queryInterface.addIndex('votes', ['user_id']);
- },
+ up: (queryInterface) => Promise.all([
+ queryInterface.addIndex('decisions', ['created_at']),
+ queryInterface.addIndex('decisions', ['restaurant_id']),
+ queryInterface.addIndex('restaurants_tags', ['restaurant_id']),
+ queryInterface.addIndex('restaurants_tags', ['tag_id']),
+ queryInterface.addIndex('roles', ['team_id']),
+ queryInterface.addIndex('roles', ['user_id']),
+ queryInterface.addIndex('votes', ['created_at']),
+ queryInterface.addIndex('teams', ['created_at']),
+ queryInterface.addIndex('votes', ['restaurant_id']),
+ queryInterface.addIndex('votes', ['user_id']),
+ ]),
- down: (queryInterface) => {
- queryInterface.removeIndex('decisions', 'decisions_created_at');
- queryInterface.removeIndex('decisions', 'decisions_restaurant_id');
- queryInterface.removeIndex('restaurants_tags', 'restaurants_tags_restaurant_id');
- queryInterface.removeIndex('restaurants_tags', 'restaurants_tags_tag_id');
- queryInterface.removeIndex('roles', 'roles_team_id');
- queryInterface.removeIndex('roles', 'roles_user_id');
- queryInterface.removeIndex('teams', 'teams_created_at');
- queryInterface.removeIndex('votes', 'votes_created_at');
- queryInterface.removeIndex('votes', 'votes_restaurant_id');
- queryInterface.removeIndex('votes', 'votes_user_id');
- }
+ down: (queryInterface) => Promise.all([
+ queryInterface.removeIndex('decisions', 'decisions_created_at'),
+ queryInterface.removeIndex('decisions', 'decisions_restaurant_id'),
+ queryInterface.removeIndex('restaurants_tags', 'restaurants_tags_restaurant_id'),
+ queryInterface.removeIndex('restaurants_tags', 'restaurants_tags_tag_id'),
+ queryInterface.removeIndex('roles', 'roles_team_id'),
+ queryInterface.removeIndex('roles', 'roles_user_id'),
+ queryInterface.removeIndex('teams', 'teams_created_at'),
+ queryInterface.removeIndex('votes', 'votes_created_at'),
+ queryInterface.removeIndex('votes', 'votes_restaurant_id'),
+ queryInterface.removeIndex('votes', 'votes_user_id'),
+ ])
};
diff --git a/db/migrations/20230214171101-ChangeUnderscoresToCamelCase.js b/db/migrations/20230214171101-ChangeUnderscoresToCamelCase.js
new file mode 100644
index 000000000..b3154bfdd
--- /dev/null
+++ b/db/migrations/20230214171101-ChangeUnderscoresToCamelCase.js
@@ -0,0 +1,95 @@
+module.exports = {
+ up: (queryInterface) => queryInterface.sequelize.transaction(transaction => Promise.all([
+ queryInterface.renameTable('restaurants_tags', 'restaurantsTags', { transaction })
+ ]).then(() => Promise.all([
+ queryInterface.renameColumn('decisions', 'restaurant_id', 'restaurantId', { transaction }),
+ queryInterface.renameColumn('decisions', 'team_id', 'teamId', { transaction }),
+ queryInterface.renameColumn('decisions', 'created_at', 'createdAt', { transaction }),
+ queryInterface.renameColumn('decisions', 'updated_at', 'updatedAt', { transaction }),
+ queryInterface.renameColumn('invitations', 'confirmed_at', 'confirmedAt', { transaction }, { transaction }),
+ queryInterface.renameColumn('invitations', 'confirmation_token', 'confirmationToken', { transaction }),
+ queryInterface.renameColumn('invitations', 'confirmation_sent_at', 'confirmationSentAt', { transaction }),
+ queryInterface.renameColumn('invitations', 'created_at', 'createdAt', { transaction }),
+ queryInterface.renameColumn('invitations', 'updated_at', 'updatedAt', { transaction }),
+ queryInterface.renameColumn('restaurants', 'place_id', 'placeId', { transaction }),
+ queryInterface.renameColumn('restaurants', 'team_id', 'teamId', { transaction }),
+ queryInterface.renameColumn('restaurants', 'created_at', 'createdAt', { transaction }),
+ queryInterface.renameColumn('restaurants', 'updated_at', 'updatedAt', { transaction }),
+ queryInterface.renameColumn('restaurantsTags', 'restaurant_id', 'restaurantId', { transaction }),
+ queryInterface.renameColumn('restaurantsTags', 'tag_id', 'tagId', { transaction }),
+ queryInterface.renameColumn('restaurantsTags', 'created_at', 'createdAt', { transaction }),
+ queryInterface.renameColumn('restaurantsTags', 'updated_at', 'updatedAt', { transaction }),
+ queryInterface.renameColumn('roles', 'user_id', 'userId', { transaction }),
+ queryInterface.renameColumn('roles', 'team_id', 'teamId', { transaction }),
+ queryInterface.renameColumn('roles', 'created_at', 'createdAt', { transaction }),
+ queryInterface.renameColumn('roles', 'updated_at', 'updatedAt', { transaction }),
+ queryInterface.renameColumn('tags', 'team_id', 'teamId', { transaction }),
+ queryInterface.renameColumn('tags', 'created_at', 'createdAt', { transaction }),
+ queryInterface.renameColumn('tags', 'updated_at', 'updatedAt', { transaction }),
+ queryInterface.renameColumn('teams', 'default_zoom', 'defaultZoom', { transaction }),
+ queryInterface.renameColumn('teams', 'sort_duration', 'sortDuration', { transaction }),
+ queryInterface.renameColumn('teams', 'created_at', 'createdAt', { transaction }),
+ queryInterface.renameColumn('teams', 'updated_at', 'updatedAt', { transaction }),
+ queryInterface.renameColumn('users', 'google_id', 'googleId', { transaction }),
+ queryInterface.renameColumn('users', 'encrypted_password', 'encryptedPassword', { transaction }),
+ queryInterface.renameColumn('users', 'reset_password_token', 'resetPasswordToken', { transaction }),
+ queryInterface.renameColumn('users', 'reset_password_sent_at', 'resetPasswordSentAt', { transaction }),
+ queryInterface.renameColumn('users', 'confirmation_token', 'confirmationToken', { transaction }),
+ queryInterface.renameColumn('users', 'confirmation_sent_at', 'confirmationSentAt', { transaction }),
+ queryInterface.renameColumn('users', 'confirmed_at', 'confirmedAt', { transaction }),
+ queryInterface.renameColumn('users', 'name_changed', 'nameChanged', { transaction }),
+ queryInterface.renameColumn('users', 'created_at', 'createdAt', { transaction }),
+ queryInterface.renameColumn('users', 'updated_at', 'updatedAt', { transaction }),
+ queryInterface.renameColumn('votes', 'user_id', 'userId', { transaction }),
+ queryInterface.renameColumn('votes', 'restaurant_id', 'restaurantId', { transaction }),
+ queryInterface.renameColumn('votes', 'created_at', 'createdAt', { transaction }),
+ queryInterface.renameColumn('votes', 'updated_at', 'updatedAt', { transaction }),
+ ]))),
+
+ down: (queryInterface) => queryInterface.sequelize.transaction(transaction => Promise.all([
+ queryInterface.renameTable('restaurantsTags', 'restaurants_tags', { transaction })
+ ]).then(() => Promise.all([
+ queryInterface.renameColumn('decisions', 'restaurantId', 'restaurant_id', { transaction }),
+ queryInterface.renameColumn('decisions', 'teamId', 'team_id', { transaction }),
+ queryInterface.renameColumn('decisions', 'createdAt', 'created_at', { transaction }),
+ queryInterface.renameColumn('decisions', 'updatedAt', 'updated_at', { transaction }),
+ queryInterface.renameColumn('invitations', 'confirmedAt', 'confirmed_at', { transaction }),
+ queryInterface.renameColumn('invitations', 'confirmationToken', 'confirmation_token', { transaction }),
+ queryInterface.renameColumn('invitations', 'confirmationSentAt', 'confirmation_sent_at', { transaction }),
+ queryInterface.renameColumn('invitations', 'createdAt', 'created_at', { transaction }),
+ queryInterface.renameColumn('invitations', 'updatedAt', 'updated_at', { transaction }),
+ queryInterface.renameColumn('restaurants', 'placeId', 'place_id', { transaction }),
+ queryInterface.renameColumn('restaurants', 'teamId', 'team_id', { transaction }),
+ queryInterface.renameColumn('restaurants', 'createdAt', 'created_at', { transaction }),
+ queryInterface.renameColumn('restaurants', 'updatedAt', 'updated_at', { transaction }),
+ queryInterface.renameColumn('restaurants_tags', 'restaurantId', 'restaurant_id', { transaction }),
+ queryInterface.renameColumn('restaurants_tags', 'tagId', 'tag_id', { transaction }),
+ queryInterface.renameColumn('restaurants_tags', 'createdAt', 'created_at', { transaction }),
+ queryInterface.renameColumn('restaurants_tags', 'updatedAt', 'updated_at', { transaction }),
+ queryInterface.renameColumn('roles', 'userId', 'user_id', { transaction }),
+ queryInterface.renameColumn('roles', 'teamId', 'team_id', { transaction }),
+ queryInterface.renameColumn('roles', 'createdAt', 'created_at', { transaction }),
+ queryInterface.renameColumn('roles', 'updatedAt', 'updated_at', { transaction }),
+ queryInterface.renameColumn('tags', 'teamId', 'team_id', { transaction }),
+ queryInterface.renameColumn('tags', 'createdAt', 'created_at', { transaction }),
+ queryInterface.renameColumn('tags', 'updatedAt', 'updated_at', { transaction }),
+ queryInterface.renameColumn('teams', 'defaultZoom', 'default_zoom', { transaction }),
+ queryInterface.renameColumn('teams', 'sortDuration', 'sort_duration', { transaction }),
+ queryInterface.renameColumn('teams', 'createdAt', 'created_at', { transaction }),
+ queryInterface.renameColumn('teams', 'updatedAt', 'updated_at', { transaction }),
+ queryInterface.renameColumn('users', 'googleId', 'google_id', { transaction }),
+ queryInterface.renameColumn('users', 'encryptedPassword', 'encrypted_password', { transaction }),
+ queryInterface.renameColumn('users', 'resetPasswordToken', 'reset_password_token', { transaction }),
+ queryInterface.renameColumn('users', 'resetPasswordSentAt', 'reset_password_sent_at', { transaction }),
+ queryInterface.renameColumn('users', 'confirmationToken', 'confirmation_token', { transaction }),
+ queryInterface.renameColumn('users', 'confirmationSentAt', 'confirmation_sent_at', { transaction }),
+ queryInterface.renameColumn('users', 'confirmedAt', 'confirmed_at', { transaction }),
+ queryInterface.renameColumn('users', 'nameChanged', 'name_changed', { transaction }),
+ queryInterface.renameColumn('users', 'createdAt', 'created_at', { transaction }),
+ queryInterface.renameColumn('users', 'updatedAt', 'updated_at', { transaction }),
+ queryInterface.renameColumn('votes', 'userId', 'user_id', { transaction }),
+ queryInterface.renameColumn('votes', 'restaurantId', 'restaurant_id', { transaction }),
+ queryInterface.renameColumn('votes', 'createdAt', 'created_at', { transaction }),
+ queryInterface.renameColumn('votes', 'updatedAt', 'updated_at', { transaction }),
+ ])))
+};
diff --git a/db/seeds/20180108175137-superuser.js b/db/seeds/20180108175137-superuser.js
index 990c6a9a5..3a606e2b7 100644
--- a/db/seeds/20180108175137-superuser.js
+++ b/db/seeds/20180108175137-superuser.js
@@ -11,11 +11,11 @@ module.exports = {
function createUser(encryptedPassword) {
return queryInterface.bulkInsert('users', [{
name,
- encrypted_password: encryptedPassword,
+ encryptedPassword,
superuser: true,
email: process.env.SUPERUSER_EMAIL,
- created_at: now,
- updated_at: now
+ createdAt: now,
+ updatedAt: now
}], {});
}
diff --git a/docker-compose.yml b/docker-compose.yml
index 8de32ca7c..50be97b26 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,8 +1,9 @@
-version: '2'
+version: "2"
services:
web:
build:
context: .
+ dockerfile: Dockerfile.local
depends_on:
- db
links:
@@ -10,10 +11,16 @@ services:
container_name: lunch-node
ports:
- "3000:3000"
- env_file: '.env.prod'
+ - "3010:3010"
db:
- image: postgres:9.5.1
+ image: postgres:14.5
ports:
- "5432:5432"
container_name: lunch-postgres
- env_file: '.env.prod'
\ No newline at end of file
+ env_file: ".env"
+ environment:
+ POSTGRES_USER: lunch
+ POSTGRES_PASSWORD: lunch
+ POSTGRES_DB: lunch
+ volumes:
+ - ./postgres-data:/var/lib/postgresql/data
diff --git a/global.d.ts b/global.d.ts
new file mode 100644
index 000000000..aebce1825
--- /dev/null
+++ b/global.d.ts
@@ -0,0 +1,67 @@
+import { Action } from 'redux';
+import WebSocket from 'ws';
+import { Team, User as UserModel } from './src/models';
+
+declare global {
+ interface Window { App: any; }
+ namespace Express {
+ export interface Request {
+ broadcast: (teamId: number, data: Action) => void;
+ subdomain?: string;
+ team?: Team;
+ user?: UserModel;
+ wss?: Server
+ }
+ }
+}
+
+interface ExtWebSocket extends WebSocket {
+ teamId?: number;
+}
+
+type Dispose = () => void
+type InsertCssItem = () => Dispose
+type GetCSSItem = () => string
+type GetContent = () => string
+
+interface Style {
+ [key: string]: InsertCssItem | GetCSSItem | GetContent | string
+ _insertCss: InsertCssItem
+ _getCss: GetCSSItem
+ _getContent: GetContent
+}
+
+declare module '*.scss' {
+ const style: Style
+ export default style
+}
+
+declare module '*.css' {
+ const style: Style
+ export default style
+}
+
+declare module 'isomorphic-style-loader/useStyles' {
+ function useStyles(...styles: Style[]): void
+ export default useStyles
+}
+
+declare module 'isomorphic-style-loader/StyleContext' {
+ import { Context } from 'react'
+
+ type RemoveGlobalCss = () => void
+ type InsertCSS = (...styles: Style[]) => RemoveGlobalCss | void
+ interface StyleContextValue {
+ insertCss: InsertCSS
+ }
+
+ const StyleContext: Context
+
+ export { StyleContext as default, InsertCSS }
+}
+
+declare module 'express-serve-static-core' {
+ interface Express {
+ hot: __WebpackModuleApi.Hot;
+ }
+}
diff --git a/package.json b/package.json
index 8026ba305..d8e9efd26 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"version": "1.0.0",
"private": true,
"engines": {
- "node": ">=16.13.1",
+ "node": ">=18.12.0",
"npm": ">=5.0"
},
"browserslist": [
@@ -14,96 +14,121 @@
],
"dependencies": {
"@babel/polyfill": "^7.0.0",
- "bcrypt": "^5.0.1",
- "bluebird": "^3.5.1",
+ "@googlemaps/js-api-loader": "^1.15.1",
+ "@honeybadger-io/js": "^5.0.0",
+ "@reduxjs/toolkit": "^1.9.2",
+ "bcrypt": "^5.1.0",
"body-parser": "^1.18.3",
+ "bootstrap": "^5.2.3",
"classnames": "^2.2.6",
"common-password": "^0.1.2",
- "compression": "^1.6.1",
+ "compression": "^1.7.4",
"connect-flash": "^0.1.1",
- "connect-session-sequelize": "^4.1.0",
- "cookie-parser": "^1.4.3",
+ "connect-session-sequelize": "^7.1.5",
+ "cookie-parser": "^1.4.6",
"core-js": "^2.5.4",
"cors": "^2.8.3",
+ "dayjs": "^1.11.7",
"dotenv": "^2.0.0",
"eventemitter3": "^1.2.0",
- "express": "^4.16.3",
- "express-jwt": "^5.3.1",
- "express-session": "^1.15.2",
+ "express": "^4.17.3",
+ "express-jwt": "^8.4.1",
+ "express-session": "^1.17.3",
"express-sslify": "^1.2.0",
- "express-ws": "^3.0.0",
+ "express-ws": "^5.0.2",
"fastclick": "^1.0.6",
- "fbjs": "^0.8.4",
- "fetch-mock": "^5.9.4",
- "google-map-react": "^1.1.2",
- "history": "^4.7.2",
- "honeybadger": "^1.1.3",
- "immutability-helper": "^2.1.2",
- "isomorphic-fetch": "^2.2.1",
- "isomorphic-style-loader": "^4.0.0",
- "jsonwebtoken": "^8.3.0",
- "method-override": "^2.3.8",
- "mocha-junit-reporter": "^1.17.0",
- "moment": "^2.29.2",
- "morgan": "^1.8.1",
- "node-fetch": "^2.6.7",
+ "fbjs": "^3.0.4",
+ "fetch-mock": "^9.11.0",
+ "google-map-react": "^2.2.0",
+ "history": "^5.3.0",
+ "immutability-helper": "^3.1.1",
+ "isomorphic-fetch": "^3.0.0",
+ "isomorphic-style-loader": "^5.3.2",
+ "jsonwebtoken": "^9.0.0",
+ "method-override": "^3.0.0",
+ "mocha-junit-reporter": "^2.2.0",
+ "morgan": "^1.10.0",
+ "node-fetch": "^2.6.9",
"normalizr": "^3.2.2",
- "passport": "^0.4.0",
- "passport-google-oauth20": "^1.0.0",
+ "passport": "^0.6.0",
+ "passport-google-oauth20": "^2.0.0",
"passport-local": "^1.0.0",
- "pg": "^8.8.0",
- "pretty-error": "^2.1.1",
- "prop-types": "^15.6.2",
- "query-string": "^6.1.0",
- "react": "^16.5.2",
- "react-addons-shallow-compare": "^15.4.2",
- "react-autosuggest": "^9.3.1",
- "react-bootstrap": "^0.31.0",
- "react-dom": "^16.5.2",
- "react-flip-move": "^3.0.1",
- "react-geosuggest": "^2.7.0",
- "react-intl": "^2.3.0",
- "react-redux": "^5.0.6",
- "react-scroll": "^1.7.6",
- "react-transition-group": "^1.2.0",
- "redux": "^3.7.2",
- "redux-devtools-extension": "^2.13.2",
- "redux-logger": "^3.0.6",
- "redux-thunk": "^2.2.0",
- "request": "^2.71.0",
+ "pg": "^8.9.0",
+ "pretty-error": "^3.0.4",
+ "prop-types": "^15.8.1",
+ "query-string": "^7.1.3",
+ "react": "^16.14.0",
+ "react-autosuggest": "^10.0.2",
+ "react-bootstrap": "^2.7.0",
+ "react-dom": "^16.14.0",
+ "react-flip-move": "^3.0.5",
+ "react-flip-toolkit": "^7.0.17",
+ "react-geosuggest": "^2.14.1",
+ "react-icons": "^4.7.1",
+ "react-intl": "^6.2.7",
+ "react-redux": "^8.0.5",
+ "react-scroll": "^1.8.9",
+ "react-transition-group": "^4.4.5",
+ "redux": "^4.2.1",
"reselect": "^2.3.0",
"reserved-usernames": "^1.0.3",
- "resolve-url-loader": "^2.0.2",
"robust-websocket": "^0.2.1",
- "rotating-file-stream": "^1.2.1",
+ "rotating-file-stream": "^3.1.0",
"sendgrid": "^5.2.3",
- "sequelize": "^4.38.1",
- "sequelize-cli": "^4.1.0",
- "serialize-javascript": "^1.5.0",
+ "sequelize": "^6.29.0",
+ "sequelize-cli": "^6.6.0",
+ "serialize-javascript": "^6.0.1",
"source-map-support": "^0.5.9",
- "sqlite3": "^5.0.11",
+ "sqlite3": "^5.1.5",
"universal-router": "^8.1.0",
- "uuid": "^3.0.1",
+ "uuid": "^9.0.0",
"whatwg-fetch": "^3.0.0"
},
"devDependencies": {
- "@babel/core": "^7.0.0",
- "@babel/node": "^7.0.0",
- "@babel/plugin-proposal-class-properties": "^7.0.0",
- "@babel/plugin-syntax-dynamic-import": "^7.0.0",
- "@babel/plugin-transform-modules-commonjs": "^7.0.0",
- "@babel/plugin-transform-react-constant-elements": "^7.0.0",
- "@babel/plugin-transform-react-inline-elements": "^7.0.0",
- "@babel/preset-env": "^7.1.0",
- "@babel/preset-flow": "^7.0.0",
- "@babel/preset-react": "^7.0.0",
- "@babel/register": "^7.0.0",
- "assets-webpack-plugin": "^3.5.1",
+ "@babel/core": "^7.20.12",
+ "@babel/eslint-parser": "^7.19.1",
+ "@babel/node": "^7.20.7",
+ "@babel/plugin-proposal-class-properties": "^7.18.6",
+ "@babel/plugin-syntax-dynamic-import": "^7.8.3",
+ "@babel/plugin-transform-modules-commonjs": "^7.20.11",
+ "@babel/plugin-transform-react-constant-elements": "^7.20.2",
+ "@babel/plugin-transform-react-inline-elements": "^7.18.6",
+ "@babel/preset-env": "^7.20.2",
+ "@babel/preset-react": "^7.18.6",
+ "@babel/preset-typescript": "^7.21.0",
+ "@babel/register": "^7.18.9",
+ "@jedmao/redux-mock-store": "^3.0.5",
+ "@pmmmwh/react-refresh-webpack-plugin": "^0.5.10",
+ "@redux-devtools/extension": "^3.2.5",
+ "@types/bcrypt": "^5.0.0",
+ "@types/chai": "^4.3.4",
+ "@types/compression": "^1.7.2",
+ "@types/connect-flash": "^0.0.37",
+ "@types/cookie-parser": "^1.4.3",
+ "@types/express-session": "^1.17.6",
+ "@types/express-sslify": "^1.2.2",
+ "@types/express-ws": "^3.0.1",
+ "@types/fbjs": "^3.0.4",
+ "@types/google-map-react": "^2.1.7",
+ "@types/google.analytics": "^0.0.42",
+ "@types/google.maps": "^3.52.1",
+ "@types/method-override": "^0.0.32",
+ "@types/mocha": "^10.0.1",
+ "@types/morgan": "^1.9.4",
+ "@types/node-fetch": "^2.6.2",
+ "@types/passport": "^1.0.11",
+ "@types/passport-google-oauth20": "^2.0.11",
+ "@types/passport-local": "^1.0.35",
+ "@types/react-dom": "16.9.8",
+ "@types/uuid": "^9.0.0",
+ "@types/webpack-env": "^1.18.0",
+ "@typescript-eslint/eslint-plugin": "^5.59.1",
+ "@typescript-eslint/parser": "^5.59.1",
+ "assets-webpack-plugin": "^7.1.1",
"autoprefixer": "^9.1.5",
"babel-core": "^7.0.0-bridge.0",
- "babel-eslint": "^9.0.0",
- "babel-loader": "^8.0.0",
- "babel-plugin-istanbul": "^4.1.5",
+ "babel-loader": "^9.1.0",
+ "babel-plugin-istanbul": "^6.1.1",
"babel-plugin-react-transform": "^2.0.2",
"babel-plugin-transform-react-remove-prop-types": "^0.4.18",
"babel-preset-env": "^1.5.2",
@@ -112,107 +137,88 @@
"babel-preset-stage-2": "^6.24.1",
"babel-template": "^6.25.0",
"babel-types": "^6.25.0",
- "bootstrap-sass": "^3.3.7",
- "browser-sync": "2.26.7",
- "chai": "4.1.2",
- "chokidar": "^2.0.4",
+ "browser-sync": "2.29.1",
+ "chai": "4.3.7",
+ "chokidar": "^3.5.3",
"cross-env": "^5.0.1",
- "css-loader": "^1.0.0",
+ "css-loader": "^6.7.3",
"custom-event-polyfill": "^0.3.0",
- "cypress": "^10.7.0",
+ "cypress": "^12.5.1",
"del": "^2.2.2",
- "editorconfig-tools": "^0.1.1",
- "enzyme": "^3.6.0",
- "enzyme-adapter-react-16": "^1.0.0",
+ "enzyme": "^3.11.0",
+ "enzyme-adapter-react-16": "^1.15.7",
"es6-promise": "^4.1.0",
- "eslint": "^5.6.0",
- "eslint-config-airbnb": "^17.1.0",
- "eslint-import-resolver-node": "^0.3.2",
- "eslint-loader": "^2.1.1",
- "eslint-plugin-css-modules": "^2.9.1",
- "eslint-plugin-flowtype": "^2.50.1",
- "eslint-plugin-import": "^2.14.0",
- "eslint-plugin-jsx-a11y": "^6.1.1",
- "eslint-plugin-react": "^7.11.1",
- "extend": "^3.0.0",
- "file-loader": "^2.0.0",
+ "eslint": "^8.34.0",
+ "eslint-config-airbnb": "^19.0.4",
+ "eslint-import-resolver-node": "^0.3.7",
+ "eslint-import-resolver-typescript": "^3.5.5",
+ "eslint-plugin-css-modules": "^2.11.0",
+ "eslint-plugin-import": "^2.27.5",
+ "eslint-plugin-jsx-a11y": "^6.7.1",
+ "eslint-plugin-react": "^7.32.2",
+ "file-loader": "^6.2.0",
+ "fork-ts-checker-webpack-plugin": "^7.3.0",
"git-repository": "^0.1.4",
"glob": "^7.1.3",
- "husky": "^1.0.0-rc.15",
+ "husky": "^8.0.3",
"identity-obj-proxy": "^3.0.0",
- "json-loader": "^0.5.4",
- "lint-staged": "^8.1.4",
- "mkdirp": "^0.5.1",
- "mocha": "^5.0.0",
- "node-sass": "7.0.0",
- "npm-run-all": "^4.1.2",
- "null-loader": "^0.1.1",
- "nyc": "^11.4.1",
- "opn-cli": "^3.1.0",
- "pixrem": "^4.0.1",
- "pleeease-filters": "^4.0.0",
- "postcss": "^7.0.36",
- "postcss-calc": "^6.0.1",
- "postcss-color-function": "^4.0.1",
- "postcss-custom-media": "^7.0.3",
- "postcss-custom-properties": "^8.0.5",
- "postcss-custom-selectors": "^5.1.2",
- "postcss-flexbugs-fixes": "^4.1.0",
- "postcss-global-import": "^1.0.0",
- "postcss-import": "^12.0.0",
- "postcss-loader": "^3.0.0",
- "postcss-media-minmax": "^4.0.0",
- "postcss-nested": "^4.1.0",
- "postcss-nesting": "^7.0.0",
- "postcss-pseudoelements": "^5.0.0",
- "postcss-selector-matches": "^4.0.0",
- "postcss-selector-not": "^4.0.0",
+ "json-loader": "^0.5.7",
+ "lint-staged": "^13.2.2",
+ "mkdirp": "^2.1.3",
+ "mocha": "^10.2.0",
+ "npm-run-all": "^4.1.5",
+ "null-loader": "^4.0.1",
+ "nyc": "^15.1.0",
+ "open-cli": "^7.1.0",
+ "postcss": "^8.4.21",
+ "postcss-loader": "^7.0.2",
"proxyquire": "^1.7.11",
"raw-loader": "^0.5.1",
- "react-deep-force-update": "^2.1.3",
- "react-dev-utils": "^5.0.2",
+ "react-dev-utils": "^12.0.1",
"react-error-overlay": "^4.0.1",
- "react-test-renderer": "^16.5.2",
- "redux-mock-store": "^1.5.1",
+ "react-refresh": "^0.14.0",
+ "react-test-renderer": "^16.14.0",
"rimraf": "^2.6.2",
- "sass-loader": "^6.0.6",
+ "sass": "^1.58.0",
+ "sass-loader": "^13.2.0",
"sequelize-mock": "^0.7.0",
"sinon": "^13.0.1",
"style-loader": "^0.13.2",
- "stylelint": "^9.5.0",
- "stylelint-config-standard": "^18.2.0",
- "stylelint-order": "^1.0.0",
- "supertest": "^3.0.0",
- "svg-url-loader": "^2.3.2",
- "url-loader": "^1.1.1",
- "webpack": "^4.19.1",
- "webpack-assets-manifest": "^3.0.2",
- "webpack-bundle-analyzer": "^3.0.2",
- "webpack-dev-middleware": "^3.3.0",
- "webpack-hot-middleware": "^2.24.2",
- "webpack-node-externals": "^1.7.2",
- "workbox-webpack-plugin": "^3.2.0"
+ "stylelint": "^14.16.1",
+ "stylelint-config-standard-scss": "^6.1.0",
+ "stylelint-order": "^6.0.2",
+ "supertest": "^6.3.3",
+ "svg-url-loader": "^8.0.0",
+ "ts-loader": "^9.4.2",
+ "typescript": "^4.9.5",
+ "url-loader": "^4.1.1",
+ "webpack": "^5.76.0",
+ "webpack-assets-manifest": "^5.1.0",
+ "webpack-bundle-analyzer": "^4.8.0",
+ "webpack-cli": "^5.0.1",
+ "webpack-dev-middleware": "^6.0.1",
+ "webpack-hot-middleware": "^2.25.3",
+ "webpack-node-externals": "^3.0.0",
+ "workbox-webpack-plugin": "^6.5.4"
},
"lint-staged": {
- "ignore": [
- "package.json"
+ "*.{js,jsx}": [
+ "eslint --no-ignore --fix",
+ "git add --force"
],
- "linters": {
- "*.{js,jsx}": [
- "eslint --no-ignore --fix",
- "git add --force"
- ],
- "*.{json,md,graphql}": [
- "git add --force"
- ],
- "*.{css,less,styl,scss,sass,sss}": [
- "stylelint --fix",
- "git add --force"
- ]
- }
+ "*.{json,md,graphql}": [
+ "git add --force"
+ ],
+ "*.{css,less,styl,scss,sass,sss}": [
+ "stylelint --fix",
+ "git add --force"
+ ]
+ },
+ "nyc": {
+ "sourceMap": false,
+ "instrument": false
},
"scripts": {
- "precommit": "npm run test && lint-staged",
"lint-js": "eslint --ignore-path .gitignore --ignore-pattern \"!**/.*\" .",
"lint-css": "stylelint \"src/**/*.{css,less,styl,scss,sass,sss}\"",
"lint": "npm run lint-js && npm run lint-css",
@@ -221,14 +227,15 @@
"fix": "npm run fix-js && npm run fix-css",
"test-file": "mocha",
"test-file-ci": "mocha --reporter mocha-junit-reporter",
- "test": "npm run test-file \"./src/**/*.test.js\"",
- "test-ci": "npm run test-file-ci \"./src/**/*.test.js\"",
+ "test": "npm run test-file \"./src/**/*.test.{js,ts}\"",
+ "test-ci": "npm run test-file-ci \"./src/**/*.test.{js,ts}\"",
"test-watch": "npm run test --watch --notify",
"test-cover": "nyc npm run test",
- "coverage": "npm run test-cover && opn coverage/lcov-report/index.html",
+ "coverage": "npm run test-cover && open-cli coverage/lcov-report/index.html",
"db:create": "./node_modules/.bin/sequelize db:create",
"db:seed:all": "./node_modules/.bin/sequelize db:seed:all",
"db:migrate": "./node_modules/.bin/sequelize db:migrate",
+ "db:migrate:undo": "./node_modules/.bin/sequelize db:migrate:undo",
"db:migrate:undo:all": "./node_modules/.bin/sequelize db:migrate:undo:all",
"db:drop": "./node_modules/.bin/sequelize db:drop",
"debug": "npm run start -- --inspect",
@@ -245,10 +252,11 @@
"copy": "babel-node tools/run copy",
"bundle": "babel-node tools/run bundle",
"build": "babel-node tools/run build",
- "build-stats": "npm run build --release --analyse",
+ "build-stats": "npm run build -- --release --analyse",
"deploy": "babel-node tools/run deploy",
"render": "babel-node tools/run render",
"serve": "babel-node tools/run runServer || true",
- "start": "babel-node tools/run start"
+ "start": "babel-node tools/run start",
+ "prepare": "husky install"
}
}
\ No newline at end of file
diff --git a/react-deep-force-update.d.ts b/react-deep-force-update.d.ts
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/actions/decisions.js b/src/actions/decisions.js
deleted file mode 100644
index 0310f6827..000000000
--- a/src/actions/decisions.js
+++ /dev/null
@@ -1,101 +0,0 @@
-import ActionTypes from '../constants/ActionTypes';
-import { processResponse, credentials, jsonHeaders } from '../core/ApiClient';
-
-export function invalidateDecisions() {
- return { type: ActionTypes.INVALIDATE_DECISIONS };
-}
-
-export function requestDecisions() {
- return {
- type: ActionTypes.REQUEST_DECISIONS
- };
-}
-
-export function receiveDecisions(json) {
- return {
- type: ActionTypes.RECEIVE_DECISIONS,
- items: json
- };
-}
-
-export function fetchDecisions() {
- return dispatch => {
- dispatch(requestDecisions());
- return fetch('/api/decisions/', {
- credentials,
- headers: jsonHeaders
- })
- .then(response => processResponse(response, dispatch))
- .then(json => dispatch(receiveDecisions(json)));
- };
-}
-
-function shouldFetchDecisions(state) {
- const { decisions } = state;
- if (decisions.isFetching) {
- return false;
- }
- return decisions.didInvalidate;
-}
-
-export function fetchDecisionsIfNeeded() {
- // Note that the function also receives getState()
- // which lets you choose what to dispatch next.
-
- // This is useful for avoiding a network request if
- // a cached value is already available.
-
- return (dispatch, getState) => {
- if (shouldFetchDecisions(getState())) {
- // Dispatch a thunk from thunk!
- return dispatch(fetchDecisions());
- }
-
- // Let the calling code know there's nothing to wait for.
- return Promise.resolve();
- };
-}
-
-export const postDecision = (restaurantId) => ({
- type: ActionTypes.POST_DECISION,
- restaurantId
-});
-
-export const decisionPosted = (decision, deselected, userId) => ({
- type: ActionTypes.DECISION_POSTED,
- decision,
- deselected,
- userId
-});
-
-export const deleteDecision = () => ({
- type: ActionTypes.DELETE_DECISION,
-});
-
-export const decisionsDeleted = (decisions, userId) => ({
- type: ActionTypes.DECISIONS_DELETED,
- decisions,
- userId
-});
-
-export const decide = (restaurantId, daysAgo) => dispatch => {
- const payload = { daysAgo, restaurant_id: restaurantId };
- dispatch(postDecision(restaurantId));
- return fetch('/api/decisions', {
- credentials,
- headers: jsonHeaders,
- method: 'post',
- body: JSON.stringify(payload)
- })
- .then(response => processResponse(response, dispatch));
-};
-
-export const removeDecision = () => (dispatch) => {
- dispatch(deleteDecision());
- return fetch('/api/decisions/fromToday', {
- credentials,
- headers: jsonHeaders,
- method: 'delete',
- })
- .then(response => processResponse(response, dispatch));
-};
diff --git a/src/actions/decisions.ts b/src/actions/decisions.ts
new file mode 100644
index 000000000..62ad03a3a
--- /dev/null
+++ b/src/actions/decisions.ts
@@ -0,0 +1,76 @@
+import { ThunkAction } from '@reduxjs/toolkit';
+import { processResponse, credentials, jsonHeaders } from '../core/ApiClient';
+import { Action, Decision, State } from '../interfaces';
+
+export function invalidateDecisions() {
+ return { type: "INVALIDATE_DECISIONS" };
+}
+
+export function requestDecisions(): Action {
+ return {
+ type: "REQUEST_DECISIONS"
+ };
+}
+
+export function receiveDecisions(json: Decision[]): Action {
+ return {
+ type: "RECEIVE_DECISIONS",
+ items: json
+ };
+}
+
+export function fetchDecisions(): ThunkAction, State, unknown, Action> {
+ return (dispatch) => {
+ dispatch(requestDecisions());
+ return fetch('/api/decisions/', {
+ credentials,
+ headers: jsonHeaders
+ })
+ .then(response => processResponse(response, dispatch))
+ .then(json => dispatch(receiveDecisions(json)));
+ };
+}
+
+export const postDecision = (restaurantId: number): Action => ({
+ type: "POST_DECISION",
+ restaurantId
+});
+
+export const decisionPosted = (decision: Decision, deselected: Decision[], userId: number): Action => ({
+ type: "DECISION_POSTED",
+ decision,
+ deselected,
+ userId
+});
+
+export const deleteDecision = (): Action => ({
+ type: "DELETE_DECISION",
+});
+
+export const decisionsDeleted = (decisions: Decision[], userId: number): Action => ({
+ type: "DECISIONS_DELETED",
+ decisions,
+ userId
+});
+
+export const decide = (restaurantId: number, daysAgo?: number): ThunkAction, State, unknown, Action> => dispatch => {
+ const payload = { daysAgo, restaurantId };
+ dispatch(postDecision(restaurantId));
+ return fetch('/api/decisions', {
+ credentials,
+ headers: jsonHeaders,
+ method: 'post',
+ body: JSON.stringify(payload)
+ })
+ .then(response => processResponse(response, dispatch));
+};
+
+export const removeDecision = (): ThunkAction, State, unknown, Action> => (dispatch) => {
+ dispatch(deleteDecision());
+ return fetch('/api/decisions/fromToday', {
+ credentials,
+ headers: jsonHeaders,
+ method: 'delete',
+ })
+ .then(response => processResponse(response, dispatch));
+};
diff --git a/src/actions/flash.js b/src/actions/flash.js
deleted file mode 100644
index 40b0944cf..000000000
--- a/src/actions/flash.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import uuidV1 from 'uuid/v1';
-import ActionTypes from '../constants/ActionTypes';
-
-export function flashError(message) {
- return {
- type: ActionTypes.FLASH_ERROR,
- message,
- id: uuidV1()
- };
-}
-
-export function flashSuccess(message) {
- return {
- type: ActionTypes.FLASH_SUCCESS,
- message,
- id: uuidV1()
- };
-}
-
-export function expireFlash(id) {
- return {
- type: ActionTypes.EXPIRE_FLASH,
- id
- };
-}
diff --git a/src/actions/flash.ts b/src/actions/flash.ts
new file mode 100644
index 000000000..d22c535ed
--- /dev/null
+++ b/src/actions/flash.ts
@@ -0,0 +1,25 @@
+import { v1 } from 'uuid';
+import { Action } from '../interfaces';
+
+export function flashError(message: string): Action {
+ return {
+ type: "FLASH_ERROR",
+ message,
+ id: v1()
+ };
+}
+
+export function flashSuccess(message: string): Action {
+ return {
+ type: "FLASH_SUCCESS",
+ message,
+ id: v1()
+ };
+}
+
+export function expireFlash(id: string): Action {
+ return {
+ type: "EXPIRE_FLASH",
+ id
+ };
+}
diff --git a/src/actions/listUi.js b/src/actions/listUi.js
deleted file mode 100644
index ada3ecdae..000000000
--- a/src/actions/listUi.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import ActionTypes from '../constants/ActionTypes';
-
-export function setEditNameFormValue(id, value) {
- return {
- type: ActionTypes.SET_EDIT_NAME_FORM_VALUE,
- id,
- value
- };
-}
-
-export function showEditNameForm(id) {
- return {
- type: ActionTypes.SHOW_EDIT_NAME_FORM,
- id
- };
-}
-
-export function hideEditNameForm(id) {
- return dispatch => {
- dispatch(setEditNameFormValue(id, ''));
- dispatch({
- type: ActionTypes.HIDE_EDIT_NAME_FORM,
- id
- });
- };
-}
-
-export function setFlipMove(val) {
- return {
- type: ActionTypes.SET_FLIP_MOVE,
- val,
- };
-}
diff --git a/src/actions/listUi.ts b/src/actions/listUi.ts
new file mode 100644
index 000000000..2db2cc177
--- /dev/null
+++ b/src/actions/listUi.ts
@@ -0,0 +1,34 @@
+import { ThunkAction } from "@reduxjs/toolkit";
+import { Action, State } from "../interfaces";
+
+export function setEditNameFormValue(id: number, value: string): Action {
+ return {
+ type: "SET_EDIT_NAME_FORM_VALUE",
+ id,
+ value
+ };
+}
+
+export function showEditNameForm(id: number): Action {
+ return {
+ type: "SHOW_EDIT_NAME_FORM",
+ id
+ };
+}
+
+export function hideEditNameForm(id: number): ThunkAction {
+ return dispatch => {
+ dispatch(setEditNameFormValue(id, ''));
+ dispatch({
+ type: "HIDE_EDIT_NAME_FORM",
+ id
+ });
+ };
+}
+
+export function setFlipMove(val: boolean): Action {
+ return {
+ type: "SET_FLIP_MOVE",
+ val,
+ };
+}
diff --git a/src/actions/mapUi.js b/src/actions/mapUi.js
deleted file mode 100644
index 0b5253f86..000000000
--- a/src/actions/mapUi.js
+++ /dev/null
@@ -1,80 +0,0 @@
-import ActionTypes from '../constants/ActionTypes';
-import { getRestaurantById } from '../selectors/restaurants';
-import { scrollToTop } from './pageUi';
-
-export function setCenter(center) {
- return {
- type: ActionTypes.SET_CENTER,
- center
- };
-}
-
-export function clearCenter() {
- return {
- type: ActionTypes.CLEAR_CENTER
- };
-}
-
-export function showGoogleInfoWindow(event) {
- return {
- type: ActionTypes.SHOW_GOOGLE_INFO_WINDOW,
- placeId: event.placeId,
- latLng: {
- lat: event.latLng.lat(),
- lng: event.latLng.lng()
- }
- };
-}
-
-export function showRestaurantInfoWindow(restaurant) {
- return {
- type: ActionTypes.SHOW_RESTAURANT_INFO_WINDOW,
- restaurant
- };
-}
-
-export function hideInfoWindow() {
- return {
- type: ActionTypes.HIDE_INFO_WINDOW
- };
-}
-
-export function createTempMarker(result) {
- return {
- type: ActionTypes.CREATE_TEMP_MARKER,
- result
- };
-}
-
-export function clearTempMarker() {
- return {
- type: ActionTypes.CLEAR_TEMP_MARKER
- };
-}
-
-export function clearNewlyAdded() {
- return {
- type: ActionTypes.CLEAR_MAP_UI_NEWLY_ADDED
- };
-}
-
-export function showMapAndInfoWindow(id) {
- return (dispatch, getState) => {
- dispatch(showRestaurantInfoWindow(getRestaurantById(getState(), id)));
- dispatch(scrollToTop());
- };
-}
-
-export function setShowUnvoted(val) {
- return {
- type: ActionTypes.SET_SHOW_UNVOTED,
- val
- };
-}
-
-export function setShowPOIs(val) {
- return {
- type: ActionTypes.SET_SHOW_POIS,
- val
- };
-}
diff --git a/src/actions/mapUi.ts b/src/actions/mapUi.ts
new file mode 100644
index 000000000..61ca1ab5f
--- /dev/null
+++ b/src/actions/mapUi.ts
@@ -0,0 +1,81 @@
+import { ThunkAction } from '@reduxjs/toolkit';
+import { Action, LatLng, Restaurant, State } from '../interfaces';
+import { getRestaurantById } from '../selectors/restaurants';
+import { scrollToTop } from './pageUi';
+
+export function setCenter(center: LatLng): Action {
+ return {
+ type: "SET_CENTER",
+ center
+ };
+}
+
+export function clearCenter(): Action {
+ return {
+ type: "CLEAR_CENTER"
+ };
+}
+
+export function showGoogleInfoWindow(event: google.maps.IconMouseEvent): Action {
+ return {
+ type: "SHOW_GOOGLE_INFO_WINDOW",
+ placeId: event.placeId!,
+ latLng: {
+ lat: event.latLng!.lat(),
+ lng: event.latLng!.lng()
+ }
+ };
+}
+
+export function showRestaurantInfoWindow(restaurant: Restaurant): Action {
+ return {
+ type: "SHOW_RESTAURANT_INFO_WINDOW",
+ restaurant
+ };
+}
+
+export function hideInfoWindow(): Action {
+ return {
+ type: "HIDE_INFO_WINDOW"
+ };
+}
+
+export function createTempMarker(result: { label: string, latLng: LatLng }): Action {
+ return {
+ type: "CREATE_TEMP_MARKER",
+ result
+ };
+}
+
+export function clearTempMarker(): Action {
+ return {
+ type: "CLEAR_TEMP_MARKER"
+ };
+}
+
+export function clearNewlyAdded(): Action {
+ return {
+ type: "CLEAR_MAP_UI_NEWLY_ADDED"
+ };
+}
+
+export function showMapAndInfoWindow(id: number): ThunkAction {
+ return (dispatch, getState) => {
+ dispatch(showRestaurantInfoWindow(getRestaurantById(getState(), id)));
+ dispatch(scrollToTop());
+ };
+}
+
+export function setShowUnvoted(val: boolean): Action {
+ return {
+ type: "SET_SHOW_UNVOTED",
+ val
+ };
+}
+
+export function setShowPOIs(val: boolean): Action {
+ return {
+ type: "SET_SHOW_POIS",
+ val
+ };
+}
diff --git a/src/actions/modals.js b/src/actions/modals.js
deleted file mode 100644
index 6f1eef5b3..000000000
--- a/src/actions/modals.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import ActionTypes from '../constants/ActionTypes';
-
-export function showModal(name, opts) {
- return {
- type: ActionTypes.SHOW_MODAL,
- name,
- opts
- };
-}
-
-export function hideModal(name) {
- return {
- type: ActionTypes.HIDE_MODAL,
- name
- };
-}
diff --git a/src/actions/modals.ts b/src/actions/modals.ts
new file mode 100644
index 000000000..7e39981f5
--- /dev/null
+++ b/src/actions/modals.ts
@@ -0,0 +1,19 @@
+import { Action, ConfirmOpts, PastDecisionsOpts } from "../interfaces";
+
+export function showModal(name: "pastDecisions", opts: PastDecisionsOpts): Action;
+export function showModal(name: "confirm", opts: ConfirmOpts): Action;
+
+export function showModal(name: unknown, opts: unknown): unknown {
+ return {
+ type: "SHOW_MODAL",
+ name,
+ opts
+ };
+}
+
+export function hideModal(name: string): Action {
+ return {
+ type: "HIDE_MODAL",
+ name
+ };
+}
diff --git a/src/actions/notifications.js b/src/actions/notifications.js
deleted file mode 100644
index 28d0613cd..000000000
--- a/src/actions/notifications.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import ActionTypes from '../constants/ActionTypes';
-
-export function notify(action) {
- return {
- type: ActionTypes.NOTIFY,
- realAction: action
- };
-}
-
-export function expireNotification(id) {
- return {
- type: ActionTypes.EXPIRE_NOTIFICATION,
- id
- };
-}
diff --git a/src/actions/notifications.ts b/src/actions/notifications.ts
new file mode 100644
index 000000000..fb38265ac
--- /dev/null
+++ b/src/actions/notifications.ts
@@ -0,0 +1,15 @@
+import { Action } from "../interfaces";
+
+export function notify(action: Action): Action {
+ return {
+ type: "NOTIFY",
+ realAction: action
+ };
+}
+
+export function expireNotification(id: string): Action {
+ return {
+ type: "EXPIRE_NOTIFICATION",
+ id
+ };
+}
diff --git a/src/actions/pageUi.js b/src/actions/pageUi.js
deleted file mode 100644
index 7fa3f209c..000000000
--- a/src/actions/pageUi.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import ActionTypes from '../constants/ActionTypes';
-
-export function scrollToTop() {
- return {
- type: ActionTypes.SCROLL_TO_TOP
- };
-}
-
-export function scrolledToTop() {
- return {
- type: ActionTypes.SCROLLED_TO_TOP
- };
-}
diff --git a/src/actions/pageUi.ts b/src/actions/pageUi.ts
new file mode 100644
index 000000000..41087ca98
--- /dev/null
+++ b/src/actions/pageUi.ts
@@ -0,0 +1,13 @@
+import { Action } from "../interfaces";
+
+export function scrollToTop(): Action {
+ return {
+ type: "SCROLL_TO_TOP"
+ };
+}
+
+export function scrolledToTop(): Action {
+ return {
+ type: "SCROLLED_TO_TOP"
+ };
+}
diff --git a/src/actions/restaurants.js b/src/actions/restaurants.ts
similarity index 55%
rename from src/actions/restaurants.js
rename to src/actions/restaurants.ts
index 2c358361d..46433fb1b 100644
--- a/src/actions/restaurants.js
+++ b/src/actions/restaurants.ts
@@ -1,14 +1,15 @@
-import ActionTypes from '../constants/ActionTypes';
import { getDecision } from '../selectors/decisions';
import { getNewlyAdded } from '../selectors/listUi';
import { getCurrentUser } from '../selectors/user';
import { processResponse, credentials, jsonHeaders } from '../core/ApiClient';
+import { ThunkAction } from '@reduxjs/toolkit';
+import { Action, Restaurant, State, Tag, Vote } from '../interfaces';
-export function sortRestaurants() {
+export function sortRestaurants(): ThunkAction {
return (dispatch, getState) => {
const state = getState();
return dispatch({
- type: ActionTypes.SORT_RESTAURANTS,
+ type: "SORT_RESTAURANTS",
decision: getDecision(state),
newlyAdded: getNewlyAdded(state),
user: getCurrentUser(state)
@@ -16,160 +17,160 @@ export function sortRestaurants() {
};
}
-export function invalidateRestaurants() {
- return { type: ActionTypes.INVALIDATE_RESTAURANTS };
+export function invalidateRestaurants(): Action {
+ return { type: "INVALIDATE_RESTAURANTS" };
}
-export function postRestaurant(obj) {
+export function postRestaurant(obj: Partial): Action {
return {
- type: ActionTypes.POST_RESTAURANT,
+ type: "POST_RESTAURANT",
restaurant: obj
};
}
-export function restaurantPosted(obj, userId) {
+export function restaurantPosted(obj: Restaurant, userId: number): Action {
return {
- type: ActionTypes.RESTAURANT_POSTED,
+ type: "RESTAURANT_POSTED",
restaurant: obj,
userId
};
}
-export function deleteRestaurant(id) {
+export function deleteRestaurant(id: number): Action {
return {
- type: ActionTypes.DELETE_RESTAURANT,
+ type: "DELETE_RESTAURANT",
id
};
}
-export function restaurantDeleted(id, userId) {
+export function restaurantDeleted(id: number, userId: number): Action {
return {
- type: ActionTypes.RESTAURANT_DELETED,
+ type: "RESTAURANT_DELETED",
id,
userId
};
}
-export function renameRestaurant(id, obj) {
+export function renameRestaurant(id: number, obj: Partial): Action {
return {
- type: ActionTypes.RENAME_RESTAURANT,
+ type: "RENAME_RESTAURANT",
id,
restaurant: obj
};
}
-export function restaurantRenamed(id, obj, userId) {
+export function restaurantRenamed(id: number, obj: Restaurant, userId: number): Action {
return {
- type: ActionTypes.RESTAURANT_RENAMED,
+ type: "RESTAURANT_RENAMED",
id,
fields: obj,
userId
};
}
-export function requestRestaurants() {
+export function requestRestaurants(): Action {
return {
- type: ActionTypes.REQUEST_RESTAURANTS
+ type: "REQUEST_RESTAURANTS"
};
}
-export function receiveRestaurants(json) {
+export function receiveRestaurants(json: Restaurant[]): Action {
return {
- type: ActionTypes.RECEIVE_RESTAURANTS,
+ type: "RECEIVE_RESTAURANTS",
items: json
};
}
-export function postVote(id) {
+export function postVote(id: number): Action {
return {
- type: ActionTypes.POST_VOTE,
+ type: "POST_VOTE",
id
};
}
-export function votePosted(json) {
+export function votePosted(json: Vote): Action {
return {
- type: ActionTypes.VOTE_POSTED,
+ type: "VOTE_POSTED",
vote: json
};
}
-export function deleteVote(restaurantId, id) {
+export function deleteVote(restaurantId: number, id: number): Action {
return {
- type: ActionTypes.DELETE_VOTE,
+ type: "DELETE_VOTE",
restaurantId,
id
};
}
-export function voteDeleted(restaurantId, userId, id) {
+export function voteDeleted(restaurantId: number, userId: number, id: number): Action {
return {
- type: ActionTypes.VOTE_DELETED,
+ type: "VOTE_DELETED",
restaurantId,
userId,
id
};
}
-export function postNewTagToRestaurant(restaurantId, value) {
+export function postNewTagToRestaurant(restaurantId: number, value: string): Action {
return {
- type: ActionTypes.POST_NEW_TAG_TO_RESTAURANT,
+ type: "POST_NEW_TAG_TO_RESTAURANT",
restaurantId,
value
};
}
-export function postedNewTagToRestaurant(restaurantId, tag, userId) {
+export function postedNewTagToRestaurant(restaurantId: number, tag: Tag, userId: number): Action {
return {
- type: ActionTypes.POSTED_NEW_TAG_TO_RESTAURANT,
+ type: "POSTED_NEW_TAG_TO_RESTAURANT",
restaurantId,
tag,
userId
};
}
-export function postTagToRestaurant(restaurantId, id) {
+export function postTagToRestaurant(restaurantId: number, id: number): Action {
return {
- type: ActionTypes.POST_TAG_TO_RESTAURANT,
+ type: "POST_TAG_TO_RESTAURANT",
restaurantId,
id
};
}
-export function postedTagToRestaurant(restaurantId, id, userId) {
+export function postedTagToRestaurant(restaurantId: number, id: number, userId: number): Action {
return {
- type: ActionTypes.POSTED_TAG_TO_RESTAURANT,
+ type: "POSTED_TAG_TO_RESTAURANT",
restaurantId,
id,
userId
};
}
-export function deleteTagFromRestaurant(restaurantId, id) {
+export function deleteTagFromRestaurant(restaurantId: number, id: number): Action {
return {
- type: ActionTypes.DELETE_TAG_FROM_RESTAURANT,
+ type: "DELETE_TAG_FROM_RESTAURANT",
restaurantId,
id
};
}
-export function deletedTagFromRestaurant(restaurantId, id, userId) {
+export function deletedTagFromRestaurant(restaurantId: number, id: number, userId: number): Action {
return {
- type: ActionTypes.DELETED_TAG_FROM_RESTAURANT,
+ type: "DELETED_TAG_FROM_RESTAURANT",
restaurantId,
id,
userId
};
}
-export function setNameFilter(val) {
+export function setNameFilter(val: string): Action {
return {
- type: ActionTypes.SET_NAME_FILTER,
+ type: "SET_NAME_FILTER",
val,
};
}
-export function fetchRestaurants() {
+export function fetchRestaurants(): ThunkAction {
return dispatch => {
dispatch(requestRestaurants());
return fetch('/api/restaurants', {
@@ -181,7 +182,7 @@ export function fetchRestaurants() {
};
}
-function shouldFetchRestaurants(state) {
+function shouldFetchRestaurants(state: State) {
const restaurants = state.restaurants;
if (!restaurants.items) {
return true;
@@ -192,7 +193,7 @@ function shouldFetchRestaurants(state) {
return restaurants.didInvalidate;
}
-export function fetchRestaurantsIfNeeded() {
+export function fetchRestaurantsIfNeeded(): ThunkAction {
// Note that the function also receives getState()
// which lets you choose what to dispatch next.
@@ -210,9 +211,9 @@ export function fetchRestaurantsIfNeeded() {
};
}
-export function addRestaurant(name, placeId, address, lat, lng) {
- const payload = {
- name, place_id: placeId, address, lat, lng
+export function addRestaurant(name: string, placeId: string, address: string, lat: number, lng: number): ThunkAction {
+ const payload: Partial = {
+ name, placeId, address, lat, lng
};
return (dispatch) => {
dispatch(postRestaurant(payload));
@@ -226,7 +227,7 @@ export function addRestaurant(name, placeId, address, lat, lng) {
};
}
-export function removeRestaurant(id) {
+export function removeRestaurant(id: number): ThunkAction {
return (dispatch) => {
dispatch(deleteRestaurant(id));
return fetch(`/api/restaurants/${id}`, {
@@ -237,8 +238,8 @@ export function removeRestaurant(id) {
};
}
-export function changeRestaurantName(id, name) {
- const payload = { name };
+export function changeRestaurantName(id: number, name: string): ThunkAction {
+ const payload: Partial = { name };
return dispatch => {
dispatch(renameRestaurant(id, payload));
return fetch(`/api/restaurants/${id}`, {
@@ -251,7 +252,7 @@ export function changeRestaurantName(id, name) {
};
}
-export function addVote(id) {
+export function addVote(id: number): ThunkAction {
return (dispatch) => {
dispatch(postVote(id));
return fetch(`/api/restaurants/${id}/votes`, {
@@ -262,7 +263,7 @@ export function addVote(id) {
};
}
-export function removeVote(restaurantId, id) {
+export function removeVote(restaurantId: number, id: number): ThunkAction {
return (dispatch) => {
dispatch(deleteVote(restaurantId, id));
return fetch(`/api/restaurants/${restaurantId}/votes/${id}`, {
@@ -273,7 +274,7 @@ export function removeVote(restaurantId, id) {
};
}
-export function addNewTagToRestaurant(restaurantId, name) {
+export function addNewTagToRestaurant(restaurantId: number, name: string): ThunkAction {
return (dispatch) => {
dispatch(postNewTagToRestaurant(restaurantId, name));
return fetch(`/api/restaurants/${restaurantId}/tags`, {
@@ -286,7 +287,7 @@ export function addNewTagToRestaurant(restaurantId, name) {
};
}
-export function addTagToRestaurant(restaurantId, id) {
+export function addTagToRestaurant(restaurantId: number, id: number): ThunkAction {
return (dispatch) => {
dispatch(postTagToRestaurant(restaurantId, id));
return fetch(`/api/restaurants/${restaurantId}/tags`, {
@@ -299,7 +300,7 @@ export function addTagToRestaurant(restaurantId, id) {
};
}
-export function removeTagFromRestaurant(restaurantId, id) {
+export function removeTagFromRestaurant(restaurantId: number, id: number): ThunkAction {
return (dispatch) => {
dispatch(deleteTagFromRestaurant(restaurantId, id));
return fetch(`/api/restaurants/${restaurantId}/tags/${id}`, {
diff --git a/src/actions/tagExclusions.js b/src/actions/tagExclusions.js
deleted file mode 100644
index 142bf2b79..000000000
--- a/src/actions/tagExclusions.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import ActionTypes from '../constants/ActionTypes';
-
-export function addTagExclusion(id) {
- return {
- type: ActionTypes.ADD_TAG_EXCLUSION,
- id
- };
-}
-
-export function clearTagExclusions() {
- return { type: ActionTypes.CLEAR_TAG_EXCLUSIONS };
-}
-
-export function removeTagExclusion(id) {
- return {
- type: ActionTypes.REMOVE_TAG_EXCLUSION,
- id
- };
-}
diff --git a/src/actions/tagExclusions.ts b/src/actions/tagExclusions.ts
new file mode 100644
index 000000000..c6fb36e83
--- /dev/null
+++ b/src/actions/tagExclusions.ts
@@ -0,0 +1,19 @@
+import { Action } from "../interfaces";
+
+export function addTagExclusion(id: number): Action {
+ return {
+ type: "ADD_TAG_EXCLUSION",
+ id
+ };
+}
+
+export function clearTagExclusions(): Action {
+ return { type: "CLEAR_TAG_EXCLUSIONS" };
+}
+
+export function removeTagExclusion(id: number): Action {
+ return {
+ type: "REMOVE_TAG_EXCLUSION",
+ id
+ };
+}
diff --git a/src/actions/tagFilters.js b/src/actions/tagFilters.js
deleted file mode 100644
index a9f4f2cf3..000000000
--- a/src/actions/tagFilters.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import ActionTypes from '../constants/ActionTypes';
-
-export function addTagFilter(id) {
- return {
- type: ActionTypes.ADD_TAG_FILTER,
- id
- };
-}
-
-export function clearTagFilters() {
- return { type: ActionTypes.CLEAR_TAG_FILTERS };
-}
-
-export function removeTagFilter(id) {
- return {
- type: ActionTypes.REMOVE_TAG_FILTER,
- id
- };
-}
diff --git a/src/actions/tagFilters.ts b/src/actions/tagFilters.ts
new file mode 100644
index 000000000..14f5b7815
--- /dev/null
+++ b/src/actions/tagFilters.ts
@@ -0,0 +1,19 @@
+import { Action } from "../interfaces";
+
+export function addTagFilter(id: number): Action {
+ return {
+ type: "ADD_TAG_FILTER",
+ id
+ };
+}
+
+export function clearTagFilters(): Action {
+ return { type: "CLEAR_TAG_FILTERS" };
+}
+
+export function removeTagFilter(id: number): Action {
+ return {
+ type: "REMOVE_TAG_FILTER",
+ id
+ };
+}
diff --git a/src/actions/tags.js b/src/actions/tags.ts
similarity index 63%
rename from src/actions/tags.js
rename to src/actions/tags.ts
index 01fc2d069..ec70a7bcb 100644
--- a/src/actions/tags.js
+++ b/src/actions/tags.ts
@@ -1,24 +1,25 @@
-import ActionTypes from '../constants/ActionTypes';
+import { ThunkAction } from '@reduxjs/toolkit';
import { credentials, jsonHeaders, processResponse } from '../core/ApiClient';
+import { Action, State, Tag } from '../interfaces';
-export function invalidateTags() {
- return { type: ActionTypes.INVALIDATE_TAGS };
+export function invalidateTags(): Action {
+ return { type: "INVALIDATE_TAGS" };
}
-export function requestTags() {
+export function requestTags(): Action {
return {
- type: ActionTypes.REQUEST_TAGS
+ type: "REQUEST_TAGS"
};
}
-export function receiveTags(json) {
+export function receiveTags(json: Tag[]): Action {
return {
- type: ActionTypes.RECEIVE_TAGS,
+ type: "RECEIVE_TAGS",
items: json
};
}
-export function fetchTags() {
+export function fetchTags(): ThunkAction {
return dispatch => {
dispatch(requestTags());
return fetch('/api/tags', {
@@ -30,7 +31,7 @@ export function fetchTags() {
};
}
-function shouldFetchTags(state) {
+function shouldFetchTags(state: State) {
const tags = state.tags;
if (!tags.items) {
return true;
@@ -41,7 +42,7 @@ function shouldFetchTags(state) {
return tags.didInvalidate;
}
-export function fetchTagsIfNeeded() {
+export function fetchTagsIfNeeded(): ThunkAction {
// Note that the function also receives getState()
// which lets you choose what to dispatch next.
@@ -59,22 +60,22 @@ export function fetchTagsIfNeeded() {
};
}
-export function deleteTag(id) {
+export function deleteTag(id: number): Action {
return {
- type: ActionTypes.DELETE_TAG,
+ type: "DELETE_TAG",
id
};
}
-export function tagDeleted(id, userId) {
+export function tagDeleted(id: number, userId: number): Action {
return {
- type: ActionTypes.TAG_DELETED,
+ type: "TAG_DELETED",
id,
userId
};
}
-export function removeTag(id) {
+export function removeTag(id: number): ThunkAction {
return (dispatch, getState) => {
dispatch(deleteTag(id));
return fetch(`/api/tags/${id}`, {
diff --git a/src/actions/team.js b/src/actions/team.ts
similarity index 65%
rename from src/actions/team.js
rename to src/actions/team.ts
index f3d99b351..60afcec56 100644
--- a/src/actions/team.js
+++ b/src/actions/team.ts
@@ -1,19 +1,20 @@
-import ActionTypes from '../constants/ActionTypes';
+import { ThunkAction } from '@reduxjs/toolkit';
import { processResponse, jsonHeaders } from '../core/ApiClient';
+import { Action, State, Team } from '../interfaces';
-export function deleteTeam() {
+export function deleteTeam(): Action {
return {
- type: ActionTypes.DELETE_TEAM
+ type: "DELETE_TEAM"
};
}
-export function teamDeleted() {
+export function teamDeleted(): Action {
return {
- type: ActionTypes.TEAM_DELETED
+ type: "TEAM_DELETED"
};
}
-export function removeTeam() {
+export function removeTeam(): ThunkAction {
return (dispatch, getState) => {
const state = getState();
const teamId = state.team.id;
@@ -29,21 +30,21 @@ export function removeTeam() {
};
}
-export function patchTeam(obj) {
+export function patchTeam(obj: Team): Action {
return {
- type: ActionTypes.PATCH_TEAM,
+ type: "PATCH_TEAM",
team: obj
};
}
-export function teamPatched(json) {
+export function teamPatched(json: Team): Action {
return {
- type: ActionTypes.TEAM_PATCHED,
+ type: "TEAM_PATCHED",
team: json
};
}
-export function updateTeam(payload) {
+export function updateTeam(payload: Team): ThunkAction {
return (dispatch, getState) => {
const state = getState();
const teamId = state.team.id;
diff --git a/src/actions/teams.js b/src/actions/teams.ts
similarity index 58%
rename from src/actions/teams.js
rename to src/actions/teams.ts
index 2bffa98e3..5868564ae 100644
--- a/src/actions/teams.js
+++ b/src/actions/teams.ts
@@ -1,21 +1,22 @@
-import ActionTypes from '../constants/ActionTypes';
+import { ThunkAction } from '@reduxjs/toolkit';
import { processResponse, credentials, jsonHeaders } from '../core/ApiClient';
+import { Action, State, Team } from '../interfaces';
-export function postTeam(obj) {
+export function postTeam(obj: Team): Action {
return {
- type: ActionTypes.POST_TEAM,
+ type: "POST_TEAM",
team: obj
};
}
-export function teamPosted(obj) {
+export function teamPosted(obj: Team): Action {
return {
- type: ActionTypes.TEAM_POSTED,
+ type: "TEAM_POSTED",
team: obj
};
}
-export function createTeam(payload) {
+export function createTeam(payload: Team): ThunkAction {
return (dispatch) => {
dispatch(postTeam(payload));
return fetch('/api/teams', {
diff --git a/src/actions/tests/decisions.test.js b/src/actions/tests/decisions.test.ts
similarity index 80%
rename from src/actions/tests/decisions.test.js
rename to src/actions/tests/decisions.test.ts
index 0c7382962..3bf4e5f79 100644
--- a/src/actions/tests/decisions.test.js
+++ b/src/actions/tests/decisions.test.ts
@@ -1,17 +1,19 @@
/* eslint-env mocha */
/* eslint-disable no-unused-expressions, no-underscore-dangle, import/no-duplicates, arrow-body-style */
+import { ThunkDispatch } from '@reduxjs/toolkit';
import { expect } from 'chai';
-import configureStore from 'redux-mock-store';
+import { configureMockStore, MockStoreEnhanced } from '@jedmao/redux-mock-store';
import fetchMock from 'fetch-mock';
import thunk from 'redux-thunk';
import * as decisions from '../decisions';
+import { Action, State } from '../../interfaces';
const middlewares = [thunk];
-const mockStore = configureStore(middlewares);
+const mockStore = configureMockStore(middlewares);
describe('actions/decisions', () => {
- let store;
+ let store: MockStoreEnhanced>;
beforeEach(() => {
store = mockStore({});
@@ -32,7 +34,7 @@ describe('actions/decisions', () => {
it('fetches all the decisions', () => {
store.dispatch(decisions.fetchDecisions());
- expect(fetchMock.lastCall()[0]).to.eq('/api/decisions/');
+ expect(fetchMock.lastCall()?.[0]).to.eq('/api/decisions/');
});
});
@@ -45,7 +47,7 @@ describe('actions/decisions', () => {
return store.dispatch(decisions.fetchDecisions()).then(() => {
const actions = store.getActions();
expect(actions[1].type).to.eq('RECEIVE_DECISIONS');
- expect(actions[1].items).to.eql([{ foo: 'bar' }]);
+ expect("items" in actions[1] && actions[1].items).to.eql([{ foo: 'bar' }]);
});
});
});
@@ -65,7 +67,7 @@ describe('actions/decisions', () => {
});
describe('decide', () => {
- let restaurantId;
+ let restaurantId: number;
beforeEach(() => {
restaurantId = 1;
@@ -80,15 +82,15 @@ describe('actions/decisions', () => {
return store.dispatch(decisions.decide(restaurantId)).then(() => {
const actions = store.getActions();
expect(actions[0].type).to.eq('POST_DECISION');
- expect(actions[0].restaurantId).to.eq(1);
+ expect("restaurantId" in actions[0] && actions[0].restaurantId).to.eq(1);
});
});
it('fetches decision', () => {
store.dispatch(decisions.decide(restaurantId));
- expect(fetchMock.lastCall()[0]).to.eq('/api/decisions');
- expect(fetchMock.lastCall()[1].body).to.eq(JSON.stringify({ restaurant_id: 1 }));
+ expect(fetchMock.lastCall()?.[0]).to.eq('/api/decisions');
+ expect(fetchMock.lastCall()?.[1]?.body).to.eq(JSON.stringify({ restaurantId: 1 }));
});
});
@@ -122,7 +124,7 @@ describe('actions/decisions', () => {
it('fetches decision', () => {
store.dispatch(decisions.removeDecision());
- expect(fetchMock.lastCall()[0]).to.eq('/api/decisions/fromToday');
+ expect(fetchMock.lastCall()?.[0]).to.eq('/api/decisions/fromToday');
});
});
diff --git a/src/actions/tests/restaurants.test.js b/src/actions/tests/restaurants.test.js
index 2312d44c7..670610f47 100644
--- a/src/actions/tests/restaurants.test.js
+++ b/src/actions/tests/restaurants.test.js
@@ -2,13 +2,13 @@
/* eslint-disable no-unused-expressions, no-underscore-dangle, import/no-duplicates, arrow-body-style */
import { expect } from 'chai';
-import configureStore from 'redux-mock-store';
+import { configureMockStore } from '@jedmao/redux-mock-store';
import fetchMock from 'fetch-mock';
import thunk from 'redux-thunk';
import * as restaurants from '../restaurants';
const middlewares = [thunk];
-const mockStore = configureStore(middlewares);
+const mockStore = configureMockStore(middlewares);
describe('actions/restaurants', () => {
let store;
@@ -90,7 +90,7 @@ describe('actions/restaurants', () => {
expect(actions[0].type).to.eq('POST_RESTAURANT');
expect(actions[0].restaurant).to.eql({
name: 'Lab Zero',
- place_id: '12345',
+ placeId: '12345',
address: '123 Main',
lat: 50,
lng: 100,
@@ -104,7 +104,7 @@ describe('actions/restaurants', () => {
expect(fetchMock.lastCall()[0]).to.eq('/api/restaurants');
expect(fetchMock.lastCall()[1].body).to.eq(JSON.stringify({
name,
- place_id: placeId,
+ placeId,
address,
lat,
lng
diff --git a/src/actions/tests/tags.test.js b/src/actions/tests/tags.test.js
index 0df75ba3a..616928878 100644
--- a/src/actions/tests/tags.test.js
+++ b/src/actions/tests/tags.test.js
@@ -2,13 +2,13 @@
/* eslint-disable no-unused-expressions, no-underscore-dangle, import/no-duplicates, arrow-body-style */
import { expect } from 'chai';
-import configureStore from 'redux-mock-store';
+import { configureMockStore } from '@jedmao/redux-mock-store';
import fetchMock from 'fetch-mock';
import thunk from 'redux-thunk';
import * as tags from '../tags';
const middlewares = [thunk];
-const mockStore = configureStore(middlewares);
+const mockStore = configureMockStore(middlewares);
describe('actions/tags', () => {
let store;
diff --git a/src/actions/tests/teams.test.js b/src/actions/tests/teams.test.js
index 6dc97dbf0..1b9378180 100644
--- a/src/actions/tests/teams.test.js
+++ b/src/actions/tests/teams.test.js
@@ -2,13 +2,13 @@
/* eslint-disable no-unused-expressions, no-underscore-dangle, import/no-duplicates, arrow-body-style */
import { expect } from 'chai';
-import configureStore from 'redux-mock-store';
+import { configureMockStore } from '@jedmao/redux-mock-store';
import fetchMock from 'fetch-mock';
import thunk from 'redux-thunk';
import * as teams from '../teams';
const middlewares = [thunk];
-const mockStore = configureStore(middlewares);
+const mockStore = configureMockStore(middlewares);
describe('actions/teams', () => {
let store;
@@ -51,8 +51,8 @@ describe('actions/teams', () => {
foo: 'bar',
roles: [{
id: 1,
- team_id: 2,
- user_id: 3
+ teamId: 2,
+ userId: 3
}]
};
fetchMock.mock('*', {
diff --git a/src/actions/tests/users.test.js b/src/actions/tests/users.test.js
index 2f43778d7..8d3f0332d 100644
--- a/src/actions/tests/users.test.js
+++ b/src/actions/tests/users.test.js
@@ -2,14 +2,14 @@
/* eslint-disable no-unused-expressions, no-underscore-dangle, import/no-duplicates, arrow-body-style */
import { expect } from 'chai';
-import configureStore from 'redux-mock-store';
+import { configureMockStore } from '@jedmao/redux-mock-store';
import fetchMock from 'fetch-mock';
import proxyquire from 'proxyquire';
import thunk from 'redux-thunk';
import * as users from '../users';
const middlewares = [thunk];
-const mockStore = configureStore(middlewares);
+const mockStore = configureMockStore(middlewares);
describe('actions/users', () => {
let store;
@@ -198,7 +198,7 @@ describe('actions/users', () => {
it('fetches user with full url', () => {
store.dispatch(proxyUsers.removeUser(id, team));
- expect(fetchMock.lastCall()[0]).to.eq(`//${team.slug}.lunch.pink/api/users/${id}`);
+ expect(fetchMock.lastCall()[0]).to.eq(`http://${team.slug}.lunch.pink/api/users/${id}`);
});
});
diff --git a/src/actions/tests/websockets.test.js b/src/actions/tests/websockets.test.js
index 4214f4bef..17fc2666e 100644
--- a/src/actions/tests/websockets.test.js
+++ b/src/actions/tests/websockets.test.js
@@ -2,13 +2,13 @@
import { expect } from 'chai';
import { useFakeTimers } from 'sinon';
-import configureStore from 'redux-mock-store';
+import { configureMockStore } from '@jedmao/redux-mock-store';
import thunk from 'redux-thunk';
import proxyquire from 'proxyquire';
import * as websockets from '../websockets';
const middlewares = [thunk];
-const mockStore = configureStore(middlewares);
+const mockStore = configureMockStore(middlewares);
describe('actions/websockets', () => {
let store;
diff --git a/src/actions/user.js b/src/actions/user.ts
similarity index 55%
rename from src/actions/user.js
rename to src/actions/user.ts
index 79b6b08d7..6b706faae 100644
--- a/src/actions/user.js
+++ b/src/actions/user.ts
@@ -1,21 +1,22 @@
-import ActionTypes from '../constants/ActionTypes';
+import { ThunkAction } from '@reduxjs/toolkit';
import { credentials, jsonHeaders, processResponse } from '../core/ApiClient';
+import { Action, State, User } from '../interfaces';
-export function patchCurrentUser(payload) {
+export function patchCurrentUser(payload: User): Action {
return {
- type: ActionTypes.PATCH_CURRENT_USER,
+ type: "PATCH_CURRENT_USER",
payload
};
}
-export function currentUserPatched(user) {
+export function currentUserPatched(user: User): Action {
return {
- type: ActionTypes.CURRENT_USER_PATCHED,
+ type: "CURRENT_USER_PATCHED",
user
};
}
-export function updateCurrentUser(payload) {
+export function updateCurrentUser(payload: User): ThunkAction {
return (dispatch) => {
dispatch(patchCurrentUser(payload));
return fetch('/api/user', {
diff --git a/src/actions/users.js b/src/actions/users.ts
similarity index 62%
rename from src/actions/users.js
rename to src/actions/users.ts
index 4fdef8860..f4f01de5d 100644
--- a/src/actions/users.js
+++ b/src/actions/users.ts
@@ -1,25 +1,27 @@
-import ActionTypes from '../constants/ActionTypes';
+import { ThunkAction } from '@reduxjs/toolkit';
+import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment';
import { credentials, jsonHeaders, processResponse } from '../core/ApiClient';
+import { Action, RoleType, State, Team, User } from '../interfaces';
import { getCurrentUser } from '../selectors/user';
-export function invalidateUsers() {
- return { type: ActionTypes.INVALIDATE_USERS };
+export function invalidateUsers(): Action {
+ return { type: "INVALIDATE_USERS" };
}
-export function requestUsers() {
+export function requestUsers(): Action {
return {
- type: ActionTypes.REQUEST_USERS
+ type: "REQUEST_USERS"
};
}
-export function receiveUsers(json) {
+export function receiveUsers(json: User[]): Action {
return {
- type: ActionTypes.RECEIVE_USERS,
+ type: "RECEIVE_USERS",
items: json
};
}
-export function fetchUsers() {
+export function fetchUsers(): ThunkAction {
return dispatch => {
dispatch(requestUsers());
return fetch('/api/users', {
@@ -31,7 +33,7 @@ export function fetchUsers() {
};
}
-function shouldFetchUsers(state) {
+function shouldFetchUsers(state: State) {
const users = state.users;
if (!users.items) {
return true;
@@ -42,7 +44,7 @@ function shouldFetchUsers(state) {
return users.didInvalidate;
}
-export function fetchUsersIfNeeded() {
+export function fetchUsersIfNeeded(): ThunkAction {
// Note that the function also receives getState()
// which lets you choose what to dispatch next.
@@ -60,25 +62,25 @@ export function fetchUsersIfNeeded() {
};
}
-export function deleteUser(id, team, isSelf) {
+export function deleteUser(id: number, team: Team, isSelf: boolean): Action {
return {
- type: ActionTypes.DELETE_USER,
+ type: "DELETE_USER",
id,
isSelf,
team
};
}
-export function userDeleted(id, team, isSelf) {
+export function userDeleted(id: number, team: Team, isSelf: boolean): Action {
return {
- type: ActionTypes.USER_DELETED,
+ type: "USER_DELETED",
id,
isSelf,
team
};
}
-export function removeUser(id, team) {
+export function removeUser(id: number, team: Team): ThunkAction {
return (dispatch, getState) => {
const state = getState();
let isSelf = false;
@@ -88,8 +90,12 @@ export function removeUser(id, team) {
dispatch(deleteUser(id, team, isSelf));
let url = `/api/users/${id}`;
const host = state.host;
+ let protocol = 'http:';
+ if (canUseDOM) {
+ protocol = window.location.protocol;
+ }
if (team) {
- url = `//${team.slug}.${host}${url}`;
+ url = `${protocol}//${team.slug}.${host}${url}`;
}
return fetch(url, {
credentials: team ? 'include' : credentials,
@@ -100,21 +106,21 @@ export function removeUser(id, team) {
};
}
-export function postUser(obj) {
+export function postUser(obj: User): Action {
return {
- type: ActionTypes.POST_USER,
+ type: "POST_USER",
user: obj
};
}
-export function userPosted(json) {
+export function userPosted(json: User): Action {
return {
- type: ActionTypes.USER_POSTED,
+ type: "USER_POSTED",
user: json
};
}
-export function addUser(payload) {
+export function addUser(payload: User): ThunkAction {
return (dispatch) => {
dispatch(postUser(payload));
return fetch('/api/users', {
@@ -128,9 +134,9 @@ export function addUser(payload) {
};
}
-export function patchUser(id, roleType, team, isSelf) {
+export function patchUser(id: number, roleType: RoleType, team: Team, isSelf: boolean): Action {
return {
- type: ActionTypes.PATCH_USER,
+ type: "PATCH_USER",
id,
isSelf,
roleType,
@@ -138,9 +144,9 @@ export function patchUser(id, roleType, team, isSelf) {
};
}
-export function userPatched(id, user, team, isSelf) {
+export function userPatched(id: number, user: User, team: Team, isSelf: boolean): Action {
return {
- type: ActionTypes.USER_PATCHED,
+ type: "USER_PATCHED",
id,
isSelf,
team,
@@ -148,7 +154,7 @@ export function userPatched(id, user, team, isSelf) {
};
}
-export function changeUserRole(id, type) {
+export function changeUserRole(id: number, type: RoleType): ThunkAction {
const payload = { id, type };
return (dispatch, getState) => {
const state = getState();
diff --git a/src/actions/websockets.js b/src/actions/websockets.js
deleted file mode 100644
index dc018c469..000000000
--- a/src/actions/websockets.js
+++ /dev/null
@@ -1,64 +0,0 @@
-import ActionTypes from '../constants/ActionTypes';
-import { sortRestaurants } from './restaurants';
-import { notify } from './notifications';
-
-let sortTimeout;
-
-const sort = dispatch => {
- clearTimeout(sortTimeout);
- sortTimeout = setTimeout(() => {
- dispatch(sortRestaurants());
- }, 1000);
-};
-
-const dispatchNotify = data => dispatch => {
- dispatch(notify(data));
- dispatch(data);
-};
-
-const notifyDispatch = data => dispatch => {
- dispatch(notify(data));
- dispatch(data);
-};
-
-const dispatchSortNotify = data => dispatch => {
- dispatch(data);
- sort(dispatch);
- dispatch(notify(data));
-};
-
-const notifyDispatchSort = data => dispatch => {
- dispatch(notify(data));
- dispatch(data);
- sort(dispatch);
-};
-
-const actionMap = {
- [ActionTypes.RESTAURANT_POSTED]: dispatchSortNotify,
- [ActionTypes.RESTAURANT_DELETED]: notifyDispatch,
- [ActionTypes.RESTAURANT_RENAMED]: notifyDispatchSort,
- [ActionTypes.VOTE_POSTED]: notifyDispatchSort,
- [ActionTypes.VOTE_DELETED]: notifyDispatchSort,
- [ActionTypes.POSTED_TAG_TO_RESTAURANT]: dispatchNotify,
- [ActionTypes.POSTED_NEW_TAG_TO_RESTAURANT]: dispatchNotify,
- [ActionTypes.DELETED_TAG_FROM_RESTAURANT]: dispatchNotify,
- [ActionTypes.TAG_DELETED]: notifyDispatch,
- [ActionTypes.DECISION_POSTED]: dispatchSortNotify,
- [ActionTypes.DECISIONS_DELETED]: dispatchSortNotify
-};
-
-export function messageReceived(payload) {
- return dispatch => {
- try {
- const data = JSON.parse(payload);
- const action = actionMap[data.type];
- if (action === undefined) {
- dispatch(data);
- } else {
- dispatch(action(data));
- }
- } catch (SyntaxError) {
- // console.error('Couldn\'t parse message data.');
- }
- };
-}
diff --git a/src/actions/websockets.ts b/src/actions/websockets.ts
new file mode 100644
index 000000000..b422ce45e
--- /dev/null
+++ b/src/actions/websockets.ts
@@ -0,0 +1,65 @@
+import { sortRestaurants } from './restaurants';
+import { notify } from './notifications';
+import { Action, State } from '../interfaces';
+import { ThunkAction, ThunkDispatch } from '@reduxjs/toolkit';
+
+let sortTimeout: NodeJS.Timer;
+
+const sort = (dispatch: ThunkDispatch) => {
+ clearTimeout(sortTimeout);
+ sortTimeout = setTimeout(() => {
+ dispatch(sortRestaurants());
+ }, 1000);
+};
+
+const dispatchNotify: (data: Action) => ThunkAction = data => dispatch => {
+ dispatch(notify(data));
+ dispatch(data);
+};
+
+const notifyDispatch: (data: Action) => ThunkAction = data => dispatch => {
+ dispatch(notify(data));
+ dispatch(data);
+};
+
+const dispatchSortNotify: (data: Action) => ThunkAction = data => dispatch => {
+ dispatch(data);
+ sort(dispatch);
+ dispatch(notify(data));
+};
+
+const notifyDispatchSort: (data: Action) => ThunkAction = data => dispatch => {
+ dispatch(notify(data));
+ dispatch(data);
+ sort(dispatch);
+};
+
+const actionMap: Partial<{[key in Action["type"]]: (data: Action) => ThunkAction}> = {
+ ["RESTAURANT_POSTED"]: dispatchSortNotify,
+ ["RESTAURANT_DELETED"]: notifyDispatch,
+ ["RESTAURANT_RENAMED"]: notifyDispatchSort,
+ ["VOTE_POSTED"]: notifyDispatchSort,
+ ["VOTE_DELETED"]: notifyDispatchSort,
+ ["POSTED_TAG_TO_RESTAURANT"]: dispatchNotify,
+ ["POSTED_NEW_TAG_TO_RESTAURANT"]: dispatchNotify,
+ ["DELETED_TAG_FROM_RESTAURANT"]: dispatchNotify,
+ ["TAG_DELETED"]: notifyDispatch,
+ ["DECISION_POSTED"]: dispatchSortNotify,
+ ["DECISIONS_DELETED"]: dispatchSortNotify
+};
+
+export function messageReceived(payload: string): ThunkAction {
+ return dispatch => {
+ try {
+ const data = JSON.parse(payload) as Action;
+ const action = actionMap[data.type];
+ if (action === undefined) {
+ dispatch(data);
+ } else {
+ dispatch(action(data));
+ }
+ } catch (SyntaxError) {
+ // console.error('Couldn\'t parse message data.');
+ }
+ };
+}
diff --git a/src/api/index.js b/src/api/index.ts
similarity index 73%
rename from src/api/index.js
rename to src/api/index.ts
index a64e20105..00010698c 100644
--- a/src/api/index.js
+++ b/src/api/index.ts
@@ -1,4 +1,4 @@
-import { Router } from 'express';
+import { RequestHandler, Router } from 'express';
import hasRole from '../helpers/hasRole';
import teamApi from './main/teams';
import userApi from './main/user';
@@ -6,10 +6,11 @@ import decisionApi from './team/decisions';
import tagApi from './team/tags';
import usersApi from './team/users';
import restaurantApi from './team/restaurants';
+import { ExtWebSocket } from '../../global';
-export default () => {
- const mainRouter = new Router();
- const teamRouter = new Router();
+export default (): RequestHandler => {
+ const mainRouter = Router();
+ const teamRouter = Router();
mainRouter
.use('/teams', teamApi())
@@ -20,9 +21,9 @@ export default () => {
.use('/restaurants', restaurantApi())
.use('/tags', tagApi())
.use('/users', usersApi())
- .ws('/', async (ws, req) => {
+ .ws('/', async (ws: ExtWebSocket, req) => {
if (hasRole(req.user, req.team)) {
- ws.teamId = req.team.id; // eslint-disable-line no-param-reassign
+ ws.teamId = req.team!.id
} else {
ws.close(1008, 'Not authorized for this team.');
}
diff --git a/src/api/main/teams.js b/src/api/main/teams.js
index b3df9e703..1c9280392 100644
--- a/src/api/main/teams.js
+++ b/src/api/main/teams.js
@@ -46,8 +46,11 @@ export default () => {
} = req.body;
const message409 = 'Could not create new team. It might already exist.';
- if (!req.user.superuser && req.user.roles.length >= TEAM_LIMIT) {
- return res.status(403).json({ error: true, data: { message: `You currently can't join more than ${TEAM_LIMIT} teams.` } });
+ if (!req.user.superuser) {
+ const roles = await req.user.getRoles();
+ if (roles.length >= TEAM_LIMIT) {
+ return res.status(403).json({ error: true, data: { message: `You currently can't join more than ${TEAM_LIMIT} teams.` } });
+ }
}
if (reservedTeamSlugs.indexOf(slug) > -1) {
@@ -66,7 +69,7 @@ export default () => {
name,
slug,
roles: [{
- user_id: req.user.id,
+ userId: req.user.id,
type: 'owner'
}]
}, { include: [Role] });
@@ -107,7 +110,8 @@ export default () => {
const message409 = 'Could not update team. Its new URL might already exist.';
let fieldCount = 0;
- const allowedFields = [{ name: 'default_zoom', type: 'number' }];
+ const allowedFields = [{ name: 'defaultZoom', type: 'number' }];
+
if (hasRole(req.user, req.team, 'owner')) {
allowedFields.push({
name: 'address',
@@ -125,7 +129,7 @@ export default () => {
name: 'slug',
type: 'string'
}, {
- name: 'sort_duration',
+ name: 'sortDuration',
type: 'number'
});
}
@@ -153,8 +157,8 @@ export default () => {
if (filteredPayload.slug && oldSlug !== filteredPayload.slug) {
req.flash('success', 'Team URL has been updated.');
return req.session.save(async () => {
- const teamRoles = await Role.findAll({ where: { team_id: req.team.get('id') } });
- const userIds = teamRoles.map(r => r.get('user_id'));
+ const teamRoles = await Role.findAll({ where: { teamId: req.team.get('id') } });
+ const userIds = teamRoles.map(r => r.get('userId'));
const recipients = await User.findAll({ where: { id: userIds } });
// returns a promise but we're not going to wait to see if it succeeds.
@@ -168,7 +172,7 @@ ${req.user.get('name')} has changed the URL of the ${req.team.get('name')} team
From now on, the team can be accessed at ${generateUrl(req, `${filteredPayload.slug}.${bsHost}`)}. Please update any bookmarks you might have created.
Happy Lunching!`
- }).then(() => {}).catch(() => {});
+ }).then(() => undefined).catch(() => undefined);
return res.status(200).json({ error: false, data: req.team });
});
diff --git a/src/api/main/user.js b/src/api/main/user.js
index f02c14ac9..ef003fcdd 100644
--- a/src/api/main/user.js
+++ b/src/api/main/user.js
@@ -4,7 +4,6 @@ import getUserPasswordUpdates from '../../helpers/getUserPasswordUpdates';
import { User } from '../../models';
import loggedIn from '../helpers/loggedIn';
-
export default () => {
const router = new Router();
@@ -57,7 +56,7 @@ export default () => {
}
if (filteredPayload.name) {
if (req.user.get('name') !== filteredPayload.name) {
- filteredPayload.name_changed = true;
+ filteredPayload.namedChanged = true;
}
}
await req.user.update(filteredPayload);
diff --git a/src/api/team/decisions.js b/src/api/team/decisions.js
index 13067ac1e..69097a171 100644
--- a/src/api/team/decisions.js
+++ b/src/api/team/decisions.js
@@ -1,6 +1,6 @@
import { Router } from 'express';
-import moment from 'moment';
-import { DataTypes } from '../../models/db';
+import dayjs from 'dayjs';
+import { Op } from '../../models/db';
import { Decision } from '../../models';
import checkTeamRole from '../helpers/checkTeamRole';
import loggedIn from '../helpers/loggedIn';
@@ -16,11 +16,11 @@ export default () => {
checkTeamRole(),
async (req, res, next) => {
try {
- const opts = { where: { team_id: req.team.id } };
+ const opts = { where: { teamId: req.team.id } };
const days = parseInt(req.query.days, 10);
if (!Number.isNaN(days)) {
- opts.where.created_at = {
- [DataTypes.Op.gt]: moment().subtract(days, 'days').toDate()
+ opts.where.createdAt = {
+ [Op.gt]: dayjs().subtract(days, 'days').toDate()
};
}
@@ -37,15 +37,15 @@ export default () => {
loggedIn,
checkTeamRole(),
async (req, res, next) => {
- const restaurantId = parseInt(req.body.restaurant_id, 10);
+ const restaurantId = parseInt(req.body.restaurantId, 10);
try {
- const destroyOpts = { where: { team_id: req.team.id } };
+ const destroyOpts = { where: { teamId: req.team.id } };
const daysAgo = parseInt(req.body.daysAgo, 10);
let MaybeScopedDecision = Decision;
if (daysAgo > 0) {
- destroyOpts.where.created_at = {
- [DataTypes.Op.gt]: moment().subtract(daysAgo, 'days').subtract(12, 'hours').toDate(),
- [DataTypes.Op.lt]: moment().subtract(daysAgo, 'days').add(12, 'hours').toDate(),
+ destroyOpts.where.createdAt = {
+ [Op.gt]: dayjs().subtract(daysAgo, 'days').subtract(12, 'hours').toDate(),
+ [Op.lt]: dayjs().subtract(daysAgo, 'days').add(12, 'hours').toDate(),
};
} else {
MaybeScopedDecision = MaybeScopedDecision.scope('fromToday');
@@ -56,16 +56,16 @@ export default () => {
try {
const createOpts = {
- restaurant_id: restaurantId,
- team_id: req.team.id
+ restaurantId,
+ teamId: req.team.id
};
if (daysAgo > 0) {
- createOpts.created_at = moment().subtract(daysAgo, 'days').toDate();
+ createOpts.createdAt = dayjs().subtract(daysAgo, 'days').toDate();
}
const obj = await Decision.create(createOpts);
const json = obj.toJSON();
- req.wss.broadcast(req.team.id, decisionPosted(json, deselected, req.user.id));
+ req.broadcast(req.team.id, decisionPosted(json, deselected, req.user.id));
res.status(201).send({ error: false, data: obj });
} catch (err) {
const error = { message: 'Could not save decision.' };
@@ -82,10 +82,10 @@ export default () => {
checkTeamRole(),
async (req, res, next) => {
try {
- const decisions = await Decision.scope('fromToday').findAll({ where: { team_id: req.team.id } });
- await Decision.scope('fromToday').destroy({ where: { team_id: req.team.id } });
+ const decisions = await Decision.scope('fromToday').findAll({ where: { teamId: req.team.id } });
+ await Decision.scope('fromToday').destroy({ where: { teamId: req.team.id } });
- req.wss.broadcast(req.team.id, decisionsDeleted(decisions, req.user.id));
+ req.broadcast(req.team.id, decisionsDeleted(decisions, req.user.id));
res.status(204).send();
} catch (err) {
next(err);
diff --git a/src/api/team/restaurantTags.js b/src/api/team/restaurantTags.js
index 15c08d243..2f8cfffb2 100644
--- a/src/api/team/restaurantTags.js
+++ b/src/api/team/restaurantTags.js
@@ -17,7 +17,7 @@ export default () => {
loggedIn,
checkTeamRole(),
async (req, res, next) => {
- const restaurantId = parseInt(req.params.restaurant_id, 10);
+ const restaurantId = parseInt(req.params.restaurantId, 10);
const alreadyAddedError = () => {
const error = { message: 'Could not add tag to restaurant. Is it already added?' };
res.status(409).json({ error: true, data: error });
@@ -26,17 +26,17 @@ export default () => {
Tag.findOrCreate({
where: {
name: req.body.name.toLowerCase().trim(),
- team_id: req.team.id
+ teamId: req.team.id
}
- }).spread(async tag => {
+ }).then(async ([tag]) => {
try {
await RestaurantTag.create({
- restaurant_id: restaurantId,
- tag_id: tag.id
+ restaurantId,
+ tagId: tag.id
});
const json = tag.toJSON();
json.restaurant_count = 1;
- req.wss.broadcast(
+ req.broadcast(
req.team.id,
postedNewTagToRestaurant(restaurantId, json, req.user.id)
);
@@ -51,12 +51,12 @@ export default () => {
const id = parseInt(req.body.id, 10);
try {
const obj = await RestaurantTag.create({
- restaurant_id: restaurantId,
- tag_id: id
+ restaurantId,
+ tagId: id
});
const json = obj.toJSON();
- req.wss.broadcast(req.team.id, postedTagToRestaurant(restaurantId, id, req.user.id));
+ req.broadcast(req.team.id, postedTagToRestaurant(restaurantId, id, req.user.id));
res.status(201).send({ error: false, data: json });
} catch (err) {
alreadyAddedError(err);
@@ -72,10 +72,10 @@ export default () => {
checkTeamRole(),
async (req, res, next) => {
const id = parseInt(req.params.id, 10);
- const restaurantId = parseInt(req.params.restaurant_id, 10);
+ const restaurantId = parseInt(req.params.restaurantId, 10);
try {
- await RestaurantTag.destroy({ where: { restaurant_id: restaurantId, tag_id: id } });
- req.wss.broadcast(req.team.id, deletedTagFromRestaurant(restaurantId, id, req.user.id));
+ await RestaurantTag.destroy({ where: { restaurantId, tagId: id } });
+ req.broadcast(req.team.id, deletedTagFromRestaurant(restaurantId, id, req.user.id));
res.status(204).send();
} catch (err) {
next(err);
diff --git a/src/api/team/restaurants.js b/src/api/team/restaurants.js
index 9dc4d75cb..d1dade7e2 100644
--- a/src/api/team/restaurants.js
+++ b/src/api/team/restaurants.js
@@ -1,5 +1,5 @@
import { Router } from 'express';
-import request from 'request';
+import fetch from 'node-fetch';
import { Restaurant, Vote, Tag } from '../../models';
import checkTeamRole from '../helpers/checkTeamRole';
import loggedIn from '../helpers/loggedIn';
@@ -22,7 +22,7 @@ export default () => {
checkTeamRole(),
async (req, res, next) => {
try {
- const all = await Restaurant.findAllWithTagIds({ team_id: req.team.id });
+ const all = await Restaurant.findAllWithTagIds({ teamId: req.team.id });
res.status(200).json({ error: false, data: all });
} catch (err) {
@@ -35,30 +35,28 @@ export default () => {
checkTeamRole(),
async (req, res, next) => {
try {
- const r = await Restaurant.findById(parseInt(req.params.id, 10));
+ const r = await Restaurant.findByPk(parseInt(req.params.id, 10));
- if (r === null || r.team_id !== req.team.id) {
+ if (r === null || r.teamId !== req.team.id) {
notFound(res);
} else {
- request(`https://maps.googleapis.com/maps/api/place/details/json?key=${apikey}&placeid=${r.place_id}`,
- (error, response, body) => {
- if (!error && response.statusCode === 200) {
- const json = JSON.parse(body);
- if (json.status !== 'OK') {
- const newError = {
- message: `Could not get info for restaurant. Google might have
- removed its entry. Try removing it and adding it to Lunch again.`
- };
- res.status(404).json({ error: true, newError });
- } else if (json.result && json.result.url) {
- res.redirect(json.result.url);
- } else {
- res.redirect(`https://www.google.com/maps/place/${r.name}, ${r.address}`);
- }
- } else {
- next(error);
- }
- });
+ const response = await fetch(`https://maps.googleapis.com/maps/api/place/details/json?key=${apikey}&placeid=${r.placeId}`);
+ const json = await response.json();
+ if (response.ok) {
+ if (json.status !== 'OK') {
+ const newError = {
+ message: `Could not get info for restaurant. Google might have
+removed its entry. Try removing it and adding it to Lunch again.`
+ };
+ res.status(404).json({ error: true, newError });
+ } else if (json.result && json.result.url) {
+ res.redirect(json.result.url);
+ } else {
+ res.redirect(`https://www.google.com/maps/place/${r.name}, ${r.address}`);
+ }
+ } else {
+ next(json);
+ }
}
} catch (err) {
next(err);
@@ -71,8 +69,7 @@ export default () => {
checkTeamRole(),
async (req, res, next) => {
const {
- // eslint-disable-next-line camelcase
- name, place_id, lat, lng
+ name, placeId, lat, lng
} = req.body;
let { address } = req.body;
@@ -81,11 +78,11 @@ export default () => {
try {
const obj = await Restaurant.create({
name,
- place_id,
+ placeId,
address,
lat,
lng,
- team_id: req.team.id,
+ teamId: req.team.id,
votes: [],
tags: []
}, { include: [Vote, Tag] });
@@ -93,7 +90,7 @@ export default () => {
const json = obj.toJSON();
json.all_decision_count = 0;
json.all_vote_count = 0;
- req.wss.broadcast(req.team.id, restaurantPosted(json, req.user.id));
+ req.broadcast(req.team.id, restaurantPosted(json, req.user.id));
res.status(201).send({ error: false, data: json });
} catch (err) {
const error = { message: 'Could not save new restaurant. Has it already been added?' };
@@ -111,13 +108,13 @@ export default () => {
Restaurant.update(
{ name },
- { fields: ['name'], where: { id, team_id: req.team.id }, returning: true }
- ).spread((count, rows) => {
+ { fields: ['name'], where: { id, teamId: req.team.id }, returning: true }
+ ).then(([count, rows]) => {
if (count === 0) {
notFound(res);
} else {
const json = { name: rows[0].toJSON().name };
- req.wss.broadcast(req.team.id, restaurantRenamed(id, json, req.user.id));
+ req.broadcast(req.team.id, restaurantRenamed(id, json, req.user.id));
res.status(200).send({ error: false, data: json });
}
}).catch(() => {
@@ -133,11 +130,11 @@ export default () => {
async (req, res, next) => {
const id = parseInt(req.params.id, 10);
try {
- const count = await Restaurant.destroy({ where: { id, team_id: req.team.id } });
+ const count = await Restaurant.destroy({ where: { id, teamId: req.team.id } });
if (count === 0) {
notFound(res);
} else {
- req.wss.broadcast(req.team.id, restaurantDeleted(id, req.user.id));
+ req.broadcast(req.team.id, restaurantDeleted(id, req.user.id));
res.status(204).send();
}
} catch (err) {
@@ -145,6 +142,6 @@ export default () => {
}
}
)
- .use('/:restaurant_id/votes', voteApi())
- .use('/:restaurant_id/tags', restaurantTagApi());
+ .use('/:restaurantId/votes', voteApi())
+ .use('/:restaurantId/tags', restaurantTagApi());
};
diff --git a/src/api/team/tags.js b/src/api/team/tags.js
index 209219bea..196ca775f 100644
--- a/src/api/team/tags.js
+++ b/src/api/team/tags.js
@@ -14,7 +14,7 @@ export default () => {
checkTeamRole(),
async (req, res, next) => {
try {
- const all = await Tag.scope('orderedByRestaurant').findAll({ where: { team_id: req.team.id } });
+ const all = await Tag.scope('orderedByRestaurant').findAll({ distinct: true, where: { teamId: req.team.id } });
res.status(200).send({ error: false, data: all });
} catch (err) {
next(err);
@@ -28,11 +28,11 @@ export default () => {
async (req, res, next) => {
const id = parseInt(req.params.id, 10);
try {
- const count = await Tag.destroy({ where: { id, team_id: req.team.id } });
+ const count = await Tag.destroy({ where: { id, teamId: req.team.id } });
if (count === 0) {
res.status(404).json({ error: true, data: { message: 'Tag not found.' } });
} else {
- req.wss.broadcast(req.team.id, tagDeleted(id, req.user.id, req.team.slug));
+ req.broadcast(req.team.id, tagDeleted(id, req.user.id, req.team.slug));
res.status(204).send();
}
} catch (err) {
diff --git a/src/api/team/users.js b/src/api/team/users.js
index f273a4b17..17faf02b4 100644
--- a/src/api/team/users.js
+++ b/src/api/team/users.js
@@ -20,12 +20,12 @@ export default () => {
if (currentUser.id === targetId) {
return getRole(currentUser, team);
}
- return Role.findOne({ where: { team_id: team.id, user_id: targetId } });
+ return Role.findOne({ where: { teamId: team.id, userId: targetId } });
};
const hasOtherOwners = async (team, id) => {
- const allTeamRoles = await Role.findAll({ where: { team_id: team.id } });
- return allTeamRoles.some(role => role.type === 'owner' && role.user_id !== id);
+ const allTeamRoles = await Role.findAll({ where: { teamId: team.id } });
+ return allTeamRoles.some(role => role.type === 'owner' && role.userId !== id);
};
const getExtraAttributes = (req) => {
@@ -37,7 +37,7 @@ export default () => {
const canChangeUser = async (user, roleToChange, target, team, noOtherOwners) => {
let currentUserRole;
- if (user.id === roleToChange.user_id) {
+ if (user.id === roleToChange.userId) {
currentUserRole = roleToChange;
} else {
currentUserRole = getRole(user, team);
@@ -46,8 +46,8 @@ export default () => {
if (user.superuser) {
allowed = true;
} else if (currentUserRole.type === 'owner') {
- if (user.id === roleToChange.user_id) {
- const otherOwners = await hasOtherOwners(team, roleToChange.user_id);
+ if (user.id === roleToChange.userId) {
+ const otherOwners = await hasOtherOwners(team, roleToChange.userId);
if (otherOwners) {
allowed = true;
} else {
@@ -56,6 +56,8 @@ export default () => {
} else {
allowed = true;
}
+ } else if (target === undefined && user.id === roleToChange.userId) {
+ allowed = true;
} else {
allowed = canChangeRole(currentUserRole.type, roleToChange.type, target);
}
@@ -75,7 +77,7 @@ export default () => {
include: {
attributes: [],
model: Role,
- where: { team_id: req.team.id }
+ where: { teamId: req.team.id }
}
});
@@ -112,7 +114,7 @@ export default () => {
if (hasRole(userToAdd, req.team, undefined, true)) {
return res.status(409).json({ error: true, data: { message: 'User already exists on this team.' } });
}
- await Role.create({ team_id: req.team.id, user_id: userToAdd.id, type });
+ await Role.create({ teamId: req.team.id, userId: userToAdd.id, type });
// returns a promise but we're not going to wait to see if it succeeds.
transporter.sendMail({
@@ -126,7 +128,7 @@ ${req.user.get('name')} invited you to the ${req.team.get('name')} team on Lunch
To get started, simply visit ${generateUrl(req, `${req.team.get('slug')}.${bsHost}`)} and vote away.
Happy Lunching!`
- }).then(() => {}).catch(() => {});
+ }).then(() => undefined).catch(() => undefined);
userToAdd = await UserWithTeamRole.findOne({
where: { email },
@@ -141,16 +143,16 @@ Happy Lunching!`
let newUser = await User.create({
email,
name,
- reset_password_token: resetPasswordToken,
- reset_password_sent_at: new Date(),
+ resetPasswordToken,
+ resetPasswordSentAt: new Date(),
roles: [{
- team_id: req.team.id,
+ teamId: req.team.id,
type
}]
}, { include: [Role] });
// returns a promise but we're not going to wait to see if it succeeds.
- Invitation.destroy({ where: { email } }).then(() => {}).catch(() => {});
+ Invitation.destroy({ where: { email } }).then(() => undefined).catch(() => undefined);
// returns a promise but we're not going to wait to see if it succeeds.
transporter.sendMail({
@@ -167,7 +169,7 @@ If you'd like to log in using a password instead, just follow this URL to genera
${generateUrl(req, bsHost, `/password/edit?token=${resetPasswordToken}`)}
Happy Lunching!`
- }).then(() => {}).catch(() => {});
+ }).then(() => undefined).catch(() => undefined);
// Sequelize can't apply scopes on create, so just get user again.
// Also will exclude hidden fields like password, token, etc.
@@ -192,15 +194,13 @@ Happy Lunching!`
const roleToChange = await getRoleToChange(req.user, id, req.team);
if (roleToChange) {
- const allowed = await canChangeUser(
- req.user, roleToChange, req.body.type, req.team, () => res.status(403).json({
- error: true,
- data: {
- message: `You cannot demote yourself if you are the only owner.
+ const allowed = await canChangeUser(req.user, roleToChange, req.body.type, req.team, () => res.status(403).json({
+ error: true,
+ data: {
+ message: `You cannot demote yourself if you are the only owner.
Grant ownership to another user first.`
- }
- })
- );
+ }
+ }));
// in case of error response within canChangeUser
if (typeof allowed !== 'boolean') {
@@ -233,15 +233,13 @@ Happy Lunching!`
const roleToDelete = await getRoleToChange(req.user, id, req.team);
if (roleToDelete) {
- const allowed = await canChangeUser(
- req.user, roleToDelete, undefined, req.team, () => res.status(403).json({
- error: true,
- data: {
- message: `You cannot remove yourself if you are the only owner.
+ const allowed = await canChangeUser(req.user, roleToDelete, undefined, req.team, () => res.status(403).json({
+ error: true,
+ data: {
+ message: `You cannot remove yourself if you are the only owner.
Transfer ownership to another user first.`
- }
- })
- );
+ }
+ }));
// in case of error response within canChangeUser
if (typeof allowed !== 'boolean') {
diff --git a/src/api/team/votes.js b/src/api/team/votes.js
index 4bcb0b5ac..a0e1f1b33 100644
--- a/src/api/team/votes.js
+++ b/src/api/team/votes.js
@@ -18,15 +18,15 @@ export default () => {
loggedIn,
checkTeamRole(),
async (req, res, next) => {
- const restaurantId = parseInt(req.params.restaurant_id, 10);
+ const restaurantId = parseInt(req.params.restaurantId, 10);
try {
const result = await sequelize.transaction(async (t) => {
const count = await Vote.recentForRestaurantAndUser(restaurantId, req.user.id, t);
if (count === 0) {
return Vote.create({
- restaurant_id: restaurantId,
- user_id: req.user.id
+ restaurantId,
+ userId: req.user.id
}, { transaction: t });
}
return '409';
@@ -36,7 +36,7 @@ export default () => {
} else {
try {
const json = result.toJSON();
- req.wss.broadcast(req.team.id, votePosted(json));
+ req.broadcast(req.team.id, votePosted(json));
res.status(201).send({ error: false, data: result });
} catch (err) {
next(err);
@@ -55,14 +55,14 @@ export default () => {
const id = parseInt(req.params.id, 10);
try {
- const count = await Vote.destroy({ where: { id, user_id: req.user.id } });
+ const count = await Vote.destroy({ where: { id, userId: req.user.id } });
if (count === 0) {
notFound(res);
} else {
- req.wss.broadcast(
+ req.broadcast(
req.team.id,
- voteDeleted(parseInt(req.params.restaurant_id, 10), req.user.id, id)
+ voteDeleted(parseInt(req.params.restaurantId, 10), req.user.id, id)
);
res.status(204).send();
}
diff --git a/src/api/tests/decisions.test.js b/src/api/tests/decisions.test.js
index 0faa8d195..1c4b53a79 100644
--- a/src/api/tests/decisions.test.js
+++ b/src/api/tests/decisions.test.js
@@ -45,11 +45,9 @@ describe('api/team/decisions', () => {
makeApp = deps => {
const decisionsApi = proxyquireStrict('../team/decisions', {
'../../models/db': mockEsmodule({
- DataTypes: {
- Op: {
- lt: 'lt',
- gt: 'gt',
- },
+ Op: {
+ lt: 'lt',
+ gt: 'gt',
},
}),
'../../models': mockEsmodule({
@@ -67,9 +65,7 @@ describe('api/team/decisions', () => {
const server = express();
server.use(bodyParser.json());
server.use((req, res, next) => {
- req.wss = { // eslint-disable-line no-param-reassign
- broadcast: broadcastSpy
- };
+ req.broadcast = broadcastSpy;
next();
});
server.use('/', decisionsApi());
@@ -102,10 +98,10 @@ describe('api/team/decisions', () => {
it('looks for decisions within past 5 days', () => {
expect(findAllSpy.calledWith({
where: {
- created_at: {
+ createdAt: {
gt: match.date,
},
- team_id: 77
+ teamId: 77
}
})).to.be.true;
});
@@ -166,19 +162,19 @@ describe('api/team/decisions', () => {
beforeEach(() => {
destroySpy = spy(DecisionMock, 'destroy');
createSpy = spy(DecisionMock, 'create');
- return request(app).post('/').send({ restaurant_id: 1 });
+ return request(app).post('/').send({ restaurantId: 1 });
});
it('deletes any prior decisions', () => {
expect(destroySpy.calledWith({
- where: { team_id: 77 }
+ where: { teamId: 77 }
})).to.be.true;
});
it('creates new decision', () => {
expect(createSpy.calledWith({
- restaurant_id: 1,
- team_id: 77
+ restaurantId: 1,
+ teamId: 77
})).to.be.true;
});
});
@@ -189,26 +185,26 @@ describe('api/team/decisions', () => {
beforeEach(() => {
destroySpy = spy(DecisionMock, 'destroy');
createSpy = spy(DecisionMock, 'create');
- return request(app).post('/').send({ daysAgo: 1, restaurant_id: 1 });
+ return request(app).post('/').send({ daysAgo: 1, restaurantId: 1 });
});
it('deletes any prior decisions', () => {
expect(destroySpy.calledWith({
where: {
- created_at: {
+ createdAt: {
lt: match.date,
gt: match.date,
},
- team_id: 77,
+ teamId: 77,
},
})).to.be.true;
});
it('creates new decision', () => {
expect(createSpy.calledWith({
- created_at: match.date,
- restaurant_id: 1,
- team_id: 77
+ createdAt: match.date,
+ restaurantId: 1,
+ teamId: 77
})).to.be.true;
});
});
@@ -224,7 +220,7 @@ describe('api/team/decisions', () => {
})
});
- request(app).post('/').send({ restaurant_id: 1 }).then(r => {
+ request(app).post('/').send({ restaurantId: 1 }).then(r => {
response = r;
done();
});
@@ -310,7 +306,7 @@ describe('api/team/decisions', () => {
it('finds decisions', () => {
expect(findAllSpy.calledWith({
- where: { team_id: 77 }
+ where: { teamId: 77 }
})).to.be.true;
});
});
diff --git a/src/api/tests/teams.test.js b/src/api/tests/teams.test.js
index 7bee2dfe2..f0296a438 100644
--- a/src/api/tests/teams.test.js
+++ b/src/api/tests/teams.test.js
@@ -28,11 +28,13 @@ describe('api/main/teams', () => {
TeamMock.findAllForUser = () => Promise.resolve([]);
RoleMock = dbMock.define('role', {});
UserMock = dbMock.define('user', {});
+ UserMock.hasMany(RoleMock);
loggedInSpy = spy((req, res, next) => {
req.user = { // eslint-disable-line no-param-reassign
- get: () => {},
+ get: () => undefined,
id: 231,
+ getRoles: () => [],
roles: []
};
next();
@@ -120,6 +122,7 @@ describe('api/main/teams', () => {
loggedInSpy = spy((req, res, next) => {
req.user = { // eslint-disable-line no-param-reassign
id: 231,
+ getRoles: () => [{}, {}, {}, {}, {}],
roles: [{}, {}, {}, {}, {}]
};
next();
@@ -290,7 +293,7 @@ describe('api/main/teams', () => {
name: 'Lab Zero',
slug: 'labzero',
roles: [{
- user_id: 231,
+ userId: 231,
type: 'owner'
}]
})).to.be.true;
@@ -469,11 +472,11 @@ describe('api/main/teams', () => {
beforeEach(() => {
updateSpy = spy();
stub(TeamMock, 'findOne').callsFake(() => Promise.resolve({
- get: () => {},
+ get: () => undefined,
update: updateSpy
}));
- return request(app).patch('/1').send({ default_zoom: 15, id: 123 });
+ return request(app).patch('/1').send({ defaultZoom: 15, id: 123 });
});
it('updates team', () => {
@@ -496,7 +499,7 @@ describe('api/main/teams', () => {
updateSpy = spy();
stub(TeamMock, 'findOne').callsFake(() => Promise.resolve({
- get: () => {},
+ get: () => undefined,
update: updateSpy
}));
@@ -533,7 +536,7 @@ describe('api/main/teams', () => {
});
stub(TeamMock, 'findOne').callsFake(() => Promise.resolve({
- get: () => {},
+ get: () => undefined,
update: () => {
const e = new Error();
e.name = 'SequelizeUniqueConstraintError';
@@ -560,7 +563,7 @@ describe('api/main/teams', () => {
describe('success', () => {
let response;
beforeEach((done) => {
- request(app).patch('/1').send({ default_zoom: 15 }).then(r => {
+ request(app).patch('/1').send({ defaultZoom: 15 }).then(r => {
response = r;
done();
});
@@ -612,10 +615,10 @@ describe('api/main/teams', () => {
let response;
beforeEach((done) => {
stub(TeamMock, 'findOne').callsFake(() => Promise.resolve({
- get: () => {},
+ get: () => undefined,
update: stub().throws('Oh No')
}));
- request(app).patch('/1').send({ default_zoom: 15 }).then((r) => {
+ request(app).patch('/1').send({ defaultZoom: 15 }).then((r) => {
response = r;
done();
});
diff --git a/src/api/tests/user.test.js b/src/api/tests/user.test.js
index d9e6d69b7..99c124938 100644
--- a/src/api/tests/user.test.js
+++ b/src/api/tests/user.test.js
@@ -134,7 +134,7 @@ describe('api/main/user', () => {
}),
'../../helpers/getUserPasswordUpdates': mockEsmodule({
default: () => Promise.resolve({
- encrypted_password: 'drowssapdoog'
+ encryptedPassword: 'drowssapdoog'
})
})
});
@@ -143,15 +143,15 @@ describe('api/main/user', () => {
});
it('updates with password updates, not password', () => {
- expect(updateSpy.calledWith({ encrypted_password: 'drowssapdoog' })).to.be.true;
+ expect(updateSpy.calledWith({ encryptedPassword: 'drowssapdoog' })).to.be.true;
});
});
describe('with new name', () => {
beforeEach(() => request(app).patch('/').send({ name: 'New Name' }));
- it('sets name_changed', () => {
- expect(updateSpy.calledWith({ name: 'New Name', name_changed: true })).to.be.true;
+ it('sets namedChanged', () => {
+ expect(updateSpy.calledWith({ name: 'New Name', namedChanged: true })).to.be.true;
});
});
diff --git a/src/api/tests/users.test.js b/src/api/tests/users.test.js
index 0d1bc717c..9da8e1edb 100644
--- a/src/api/tests/users.test.js
+++ b/src/api/tests/users.test.js
@@ -75,9 +75,7 @@ describe('api/team/users', () => {
const server = express();
server.use(bodyParser.json());
server.use((req, res, next) => {
- req.wss = { // eslint-disable-line no-param-reassign
- broadcast: broadcastSpy
- };
+ req.broadcast = broadcastSpy;
next();
});
server.use('/', usersApi());
@@ -344,8 +342,8 @@ describe('api/team/users', () => {
it('creates team role for user', () => {
expect(createSpy.calledWith({
- team_id: 77,
- user_id: 2,
+ teamId: 77,
+ userId: 2,
type: 'member'
})).to.be.true;
});
@@ -386,10 +384,10 @@ describe('api/team/users', () => {
expect(createStub.calledWith({
email: 'foo@bar.com',
name: 'Jeffrey',
- reset_password_token: match.string,
- reset_password_sent_at: match.date,
+ resetPasswordToken: match.string,
+ resetPasswordSentAt: match.date,
roles: [{
- team_id: 77,
+ teamId: 77,
type: 'member'
}]
})).to.be.true;
@@ -529,8 +527,8 @@ describe('api/team/users', () => {
beforeEach(() => {
role = {
update: spy(),
- user_id: user.id,
- team_id: team.id,
+ userId: user.id,
+ teamId: team.id,
type: 'owner'
};
path = `/${user.id}`;
@@ -554,7 +552,7 @@ describe('api/team/users', () => {
beforeEach((done) => {
findAllStub = stub(RoleMock, 'findAll').callsFake(() => Promise.resolve([role, {
type: 'member',
- user_id: 2
+ userId: 2
}]));
request(app).patch(path).send(payload).then((r) => {
response = r;
@@ -563,7 +561,7 @@ describe('api/team/users', () => {
});
it('finds all roles', () => {
- expect(findAllStub.calledWith({ where: { team_id: team.id } })).to.be.true;
+ expect(findAllStub.calledWith({ where: { teamId: team.id } })).to.be.true;
});
it('returns 403', () => {
@@ -580,7 +578,7 @@ describe('api/team/users', () => {
beforeEach(() => {
stub(RoleMock, 'findAll').callsFake(() => Promise.resolve([role, {
type: 'owner',
- user_id: 2
+ userId: 2
}]));
return request(app).patch(path).send(payload);
});
@@ -602,8 +600,8 @@ describe('api/team/users', () => {
it('queries role on team', () => {
expect(findOneSpy.calledWith({
where: {
- team_id: team.id,
- user_id: 2
+ teamId: team.id,
+ userId: 2
}
})).to.be.true;
});
@@ -616,14 +614,14 @@ describe('api/team/users', () => {
let role;
beforeEach(() => {
currentUserRole = {
- user_id: user.id,
- team_id: team.id,
+ userId: user.id,
+ teamId: team.id,
type: 'owner'
};
role = {
update: spy(),
- user_id: otherUserId,
- team_id: team.id
+ userId: otherUserId,
+ teamId: team.id
};
otherUserId = 2;
path = `/${otherUserId}`;
@@ -679,14 +677,14 @@ describe('api/team/users', () => {
let role;
beforeEach(() => {
currentUserRole = {
- user_id: user.id,
- team_id: team.id,
+ userId: user.id,
+ teamId: team.id,
type: 'member'
};
role = {
update: spy(),
- user_id: otherUserId,
- team_id: team.id
+ userId: otherUserId,
+ teamId: team.id
};
otherUserId = 2;
path = `/${otherUserId}`;
@@ -791,8 +789,8 @@ describe('api/team/users', () => {
let userToChange;
beforeEach((done) => {
currentUserRole = {
- user_id: user.id,
- team_id: team.id,
+ userId: user.id,
+ teamId: team.id,
type: 'member'
};
userToChange = {
@@ -800,8 +798,8 @@ describe('api/team/users', () => {
};
stub(RoleMock, 'findOne').callsFake(() => Promise.resolve({
update: () => Promise.resolve(),
- user_id: userToChange.id,
- team_id: team.id,
+ userId: userToChange.id,
+ teamId: team.id,
type: 'guest'
}));
stub(UserMock, 'findOne').callsFake(() => Promise.resolve(userToChange));
@@ -872,8 +870,8 @@ describe('api/team/users', () => {
let userToDelete;
beforeEach((done) => {
currentUserRole = {
- user_id: user.id,
- team_id: team.id,
+ userId: user.id,
+ teamId: team.id,
type: 'member'
};
userToDelete = {
@@ -881,8 +879,8 @@ describe('api/team/users', () => {
};
roleToDestroy = {
destroy: spy(() => Promise.resolve()),
- user_id: userToDelete.id,
- team_id: team.id,
+ userId: userToDelete.id,
+ teamId: team.id,
type: 'guest'
};
stub(RoleMock, 'findOne').callsFake(() => Promise.resolve(roleToDestroy));
diff --git a/src/client.js b/src/client.tsx
similarity index 84%
rename from src/client.js
rename to src/client.tsx
index 66ff47645..49f33119a 100644
--- a/src/client.js
+++ b/src/client.tsx
@@ -11,24 +11,20 @@ import 'whatwg-fetch';
import es6Promise from 'es6-promise';
import React from 'react';
import ReactDOM from 'react-dom';
-import deepForceUpdate from 'react-deep-force-update';
import queryString from 'query-string';
-import RobustWebSocket from 'robust-websocket';
-import { createPath } from 'history/PathUtils';
+import { Action, createPath, Location } from 'history';
import App from './components/App';
import createFetch from './createFetch';
import configureStore from './store/configureStore';
import history from './history';
import { updateMeta } from './DOMUtils';
import routerCreator from './router';
-
-/* eslint-disable global-require */
+import { ResolveContext } from 'universal-router';
+import { Style } from '../global';
es6Promise.polyfill();
-window.RobustWebSocket = RobustWebSocket;
-
-let subdomain;
+let subdomain: string | undefined;
// Undo Browsersync mangling of host
let host = window.App.state.host;
@@ -44,7 +40,9 @@ window.App.state.host = host;
if (!subdomain) {
// escape domain periods to not appear as regex wildcards
- const subdomainMatch = window.location.host.match(`^(.*)\\.${host.replace(/\./g, '\\.')}`);
+ const subdomainMatch = window.location.host.match(
+ `^(.*)\\.${host.replace(/\./g, '\\.')}`
+ );
if (subdomainMatch) {
subdomain = subdomainMatch[1];
}
@@ -55,30 +53,32 @@ const store = configureStore(window.App.state, { history });
// Global (context) variables that can be easily accessed from any React component
// https://facebook.github.io/react/docs/context.html
-const context = {
+const context: ResolveContext = {
// Enables critical path CSS rendering
// https://github.com/kriasoft/isomorphic-style-loader
- insertCss: (...styles) => {
+ insertCss: (...styles: Style[]) => {
// eslint-disable-next-line no-underscore-dangle
- const removeCss = styles.map(x => x._insertCss());
+ const removeCss = styles.map((x) => x._insertCss());
return () => {
- removeCss.forEach(f => f());
+ removeCss.forEach((f) => f());
};
},
// Universal HTTP client
fetch: createFetch(fetch, {
baseUrl: window.App.apiUrl,
}),
+ googleApiKey: window.App.googleApiKey,
// Initialize a new Redux store
// http://redux.js.org/docs/basics/UsageWithReact.html
- store
+ store,
+ pathname: '',
+ query: undefined
};
const container = document.getElementById('app');
-let currentLocation = history.location;
-let appInstance;
+let currentLocation = history!.location;
-const scrollPositionsHistory = {};
+const scrollPositionsHistory: {[index: string]: { scrollX: number, scrollY: number }} = {};
let routes;
if (subdomain) {
@@ -90,7 +90,7 @@ if (subdomain) {
const router = routerCreator(routes);
// Re-render the app when window.location changes
-async function onLocationChange(location, action) {
+const onLocationChange = async ({ action, location }: { action?: Action, location: Location }) => {
// Remember the latest scroll position for the previous location
scrollPositionsHistory[currentLocation.key] = {
scrollX: window.pageXOffset,
@@ -124,13 +124,13 @@ async function onLocationChange(location, action) {
if (route.redirect.slice(0, 2) === '//') {
window.location.href = route.redirect;
} else {
- history.replace(route.redirect);
+ history!.replace(route.redirect);
}
return;
}
const renderReactApp = isInitialRender ? ReactDOM.hydrate : ReactDOM.render;
- appInstance = renderReactApp(
+ renderReactApp(
{route.component} ,
container,
() => {
@@ -142,7 +142,7 @@ async function onLocationChange(location, action) {
}
const elem = document.getElementById('css');
- if (elem) elem.parentNode.removeChild(elem);
+ if (elem) elem.parentNode!.removeChild(elem);
return;
}
@@ -182,7 +182,7 @@ async function onLocationChange(location, action) {
if (window.ga) {
window.ga('send', 'pageview', createPath(location));
}
- },
+ }
);
} catch (error) {
if (__DEV__) {
@@ -201,18 +201,13 @@ async function onLocationChange(location, action) {
// Handle client-side navigation by using HTML5 History API
// For more information visit https://github.com/mjackson/history#readme
-history.listen(onLocationChange);
-onLocationChange(currentLocation);
+history!.listen(onLocationChange);
+onLocationChange({ action: Action.Replace, location: currentLocation });
// Enable Hot Module Replacement (HMR)
-if (module.hot) {
+/* if (module.hot) {
const hotUpdate = () => {
- if (appInstance && appInstance.updater.isMounted(appInstance)) {
- // Force-update the whole tree, including components that refuse to update
- deepForceUpdate(appInstance);
- }
-
- onLocationChange(currentLocation);
+ onLocationChange({ action: Action.Replace, location: currentLocation });
};
module.hot.accept('./routes/team', () => {
@@ -224,4 +219,4 @@ if (module.hot) {
routes = require('./routes/main').default; // eslint-disable-line global-require
hotUpdate();
});
-}
+} */
diff --git a/src/components/AddUserForm/AddUserForm.js b/src/components/AddUserForm/AddUserForm.js
index ea88bf106..b4556778b 100644
--- a/src/components/AddUserForm/AddUserForm.js
+++ b/src/components/AddUserForm/AddUserForm.js
@@ -1,13 +1,9 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
-import { intlShape } from 'react-intl';
-import Button from 'react-bootstrap/lib/Button';
-import Col from 'react-bootstrap/lib/Col';
-import ControlLabel from 'react-bootstrap/lib/ControlLabel';
-import FormControl from 'react-bootstrap/lib/FormControl';
-import FormGroup from 'react-bootstrap/lib/FormGroup';
-import HelpBlock from 'react-bootstrap/lib/HelpBlock';
-import Row from 'react-bootstrap/lib/Row';
+import Button from 'react-bootstrap/Button';
+import Col from 'react-bootstrap/Col';
+import Form from 'react-bootstrap/Form';
+import Row from 'react-bootstrap/Row';
import { globalMessageDescriptor as gm } from '../../helpers/generateMessageDescriptor';
class AddUserForm extends Component {
@@ -16,7 +12,7 @@ class AddUserForm extends Component {
hasGuestRole: PropTypes.bool.isRequired,
hasMemberRole: PropTypes.bool.isRequired,
hasOwnerRole: PropTypes.bool.isRequired,
- intl: intlShape.isRequired,
+ intl: PropTypes.shape().isRequired,
};
static defaultState = {
@@ -25,14 +21,18 @@ class AddUserForm extends Component {
type: 'member'
};
- state = Object.assign({}, AddUserForm.defaultState);
+ constructor(props) {
+ super(props);
+
+ this.state = ({ ...AddUserForm.defaultState });
+ }
handleChange = field => event => this.setState({ [field]: event.target.value });
handleSubmit = (event) => {
event.preventDefault();
this.props.addUserToTeam(this.state);
- this.setState(Object.assign({}, AddUserForm.defaultState));
+ this.setState({ ...AddUserForm.defaultState });
};
render() {
@@ -48,27 +48,27 @@ class AddUserForm extends Component {
Add User
);
diff --git a/src/components/AddUserForm/AddUserForm.test.js b/src/components/AddUserForm/AddUserForm.test.js
index 695d0d303..3a209016f 100644
--- a/src/components/AddUserForm/AddUserForm.test.js
+++ b/src/components/AddUserForm/AddUserForm.test.js
@@ -31,6 +31,7 @@ describe('AddUserForm', () => {
};
});
+ /* eslint-disable jsx-a11y/control-has-associated-label */
describe('the options for the User Type form', () => {
it('includes an option for guest if the hasGuestRole prop is true', () => {
props.hasGuestRole = true;
diff --git a/src/components/App.js b/src/components/App.js
index 8649fc6ea..bff2de081 100644
--- a/src/components/App.js
+++ b/src/components/App.js
@@ -7,10 +7,13 @@
* LICENSE.txt file in the root directory of this source tree.
*/
+import StyleContext from 'isomorphic-style-loader/StyleContext';
import PropTypes from 'prop-types';
import React, { Children } from 'react';
import { Provider as ReduxProvider } from 'react-redux';
+import { Loader } from '@googlemaps/js-api-loader';
import IntlProviderContainer from './IntlProvider/IntlProviderContainer';
+import GoogleMapsLoaderContext from './GoogleMapsLoaderContext/GoogleMapsLoaderContext';
const ContextType = {
// Enables critical path CSS rendering
@@ -18,8 +21,10 @@ const ContextType = {
insertCss: PropTypes.func.isRequired,
// Universal HTTP client
fetch: PropTypes.func.isRequired,
+ googleApiKey: PropTypes.string.isRequired,
pathname: PropTypes.string.isRequired,
query: PropTypes.object,
+ store: PropTypes.object.isRequired,
// Integrate Redux
// http://redux.js.org/docs/basics/UsageWithReact.html
...ReduxProvider.childContextTypes,
@@ -55,6 +60,20 @@ class App extends React.PureComponent {
static childContextTypes = ContextType;
+ constructor(props) {
+ super(props);
+
+ this.loaderContextValue = {
+ loader: new Loader({
+ apiKey: this.props.context.googleApiKey,
+ version: 'weekly',
+ libraries: ['places', 'geocoding']
+ })
+ };
+
+ this.styleContextValue = { insertCss: props.context.insertCss };
+ }
+
getChildContext() {
return this.props.context;
}
@@ -63,9 +82,17 @@ class App extends React.PureComponent {
// NOTE: If you need to add or modify header, footer etc. of the app,
// please do that inside the Layout component.
return (
-
- {Children.only(this.props.children)}
-
+
+
+
+
+ {Children.only(this.props.children)}
+
+
+
+
);
}
}
diff --git a/src/components/ChangeTeamURLModal/ChangeTeamURLModal.js b/src/components/ChangeTeamURLModal/ChangeTeamURLModal.js
index b871b1109..a0a1441f8 100644
--- a/src/components/ChangeTeamURLModal/ChangeTeamURLModal.js
+++ b/src/components/ChangeTeamURLModal/ChangeTeamURLModal.js
@@ -1,16 +1,14 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
-import withStyles from 'isomorphic-style-loader/lib/withStyles';
-import Col from 'react-bootstrap/lib/Col';
-import ControlLabel from 'react-bootstrap/lib/ControlLabel';
-import FormControl from 'react-bootstrap/lib/FormControl';
-import FormGroup from 'react-bootstrap/lib/FormGroup';
-import InputGroup from 'react-bootstrap/lib/InputGroup';
-import Modal from 'react-bootstrap/lib/Modal';
-import ModalBody from 'react-bootstrap/lib/ModalBody';
-import ModalFooter from 'react-bootstrap/lib/ModalFooter';
-import Row from 'react-bootstrap/lib/Row';
-import Button from 'react-bootstrap/lib/Button';
+import withStyles from 'isomorphic-style-loader/withStyles';
+import Col from 'react-bootstrap/Col';
+import Form from 'react-bootstrap/Form';
+import InputGroup from 'react-bootstrap/InputGroup';
+import Modal from 'react-bootstrap/Modal';
+import ModalBody from 'react-bootstrap/ModalBody';
+import ModalFooter from 'react-bootstrap/ModalFooter';
+import Row from 'react-bootstrap/Row';
+import Button from 'react-bootstrap/Button';
import { TEAM_SLUG_REGEX } from '../../constants';
import s from './ChangeTeamURLModal.scss';
@@ -20,16 +18,20 @@ class ChangeTeamURLModal extends Component {
team: PropTypes.object.isRequired,
shown: PropTypes.bool.isRequired,
hideModal: PropTypes.func.isRequired,
- updateTeam: PropTypes.func.isRequired
+ updateTeam: PropTypes.func.isRequired,
};
- state = {
- newSlug: '',
- oldSlug: ''
- };
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ newSlug: '',
+ oldSlug: '',
+ };
+ }
- handleChange = field => event => this.setState({
- [field]: event.target.value
+ handleChange = (field) => (event) => this.setState({
+ [field]: event.target.value,
});
handleSubmit = () => {
@@ -39,7 +41,7 @@ class ChangeTeamURLModal extends Component {
updateTeam({ slug: newSlug }).then(() => {
window.location.href = `//${newSlug}.${host}/team`;
});
- }
+ };
render() {
const { team, shown, hideModal } = this.props;
@@ -51,18 +53,22 @@ class ChangeTeamURLModal extends Component {
Be forewarned:
{' '}
-Changing the team URL frees up the old URL to be used
- by other teams. This means that any bookmarks your team members have created for this
- team will no longer work. We’ll send out an email notification to all users on
- the team that this change has taken place.
+ Changing the team URL frees up the
+ old URL to be used by other teams. This means that any bookmarks
+ your team members have created for this team will no longer work.
+ We’ll send out an email notification to all users on the team
+ that this change has taken place.
+
+
+ To confirm, please write the current URL of the team in the field
+ below.
- To confirm, please write the current URL of the team in the field below.
-
- Current team URL
+
+ Current team URL
-
- .lunch.pink
+ .lunch.pink
-
-
- New team URL
+
+
+ New team URL
-
- .lunch.pink
+ .lunch.pink
-
+
- Cancel
+
+ Cancel
+
- Cancel
-
+ Cancel
+
{actionLabel}
diff --git a/src/components/ConfirmModal/ConfirmModal.test.js b/src/components/ConfirmModal/ConfirmModal.test.js
index 9fc35fdf4..51730c7c8 100644
--- a/src/components/ConfirmModal/ConfirmModal.test.js
+++ b/src/components/ConfirmModal/ConfirmModal.test.js
@@ -1,7 +1,7 @@
/* eslint-env mocha */
/* eslint-disable padded-blocks, no-unused-expressions */
-import Modal from 'react-bootstrap/lib/Modal';
+import Modal from 'react-bootstrap/Modal';
import { expect } from 'chai';
import React from 'react';
import sinon from 'sinon';
diff --git a/src/components/ConfirmModal/ConfirmModalContainer.js b/src/components/ConfirmModal/ConfirmModalContainer.js
index e242dc480..28a188eb2 100644
--- a/src/components/ConfirmModal/ConfirmModalContainer.js
+++ b/src/components/ConfirmModal/ConfirmModalContainer.js
@@ -7,17 +7,20 @@ const modalName = 'confirm';
const mapStateToProps = state => ({
actionLabel: state.modals[modalName].actionLabel,
body: state.modals[modalName].body,
- internalHandleSubmit: state.modals[modalName].handleSubmit,
+ action: state.modals[modalName].action,
shown: !!state.modals[modalName].shown
});
const mapDispatchToProps = dispatch => ({
+ dispatch,
hideModal: () => dispatch(hideModal('confirm')),
});
-const mergeProps = (stateProps, dispatchProps) => Object.assign({}, stateProps, dispatchProps, {
+const mergeProps = (stateProps, dispatchProps) => ({
+ ...stateProps,
+ ...dispatchProps,
handleSubmit: () => {
- stateProps.internalHandleSubmit();
+ dispatchProps.dispatch(stateProps.action);
dispatchProps.hideModal();
}
});
diff --git a/src/components/DeleteTeamModal/DeleteTeamModal.js b/src/components/DeleteTeamModal/DeleteTeamModal.js
index a5b5c0560..724b5bf2b 100644
--- a/src/components/DeleteTeamModal/DeleteTeamModal.js
+++ b/src/components/DeleteTeamModal/DeleteTeamModal.js
@@ -1,16 +1,14 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
-import withStyles from 'isomorphic-style-loader/lib/withStyles';
-import Col from 'react-bootstrap/lib/Col';
-import ControlLabel from 'react-bootstrap/lib/ControlLabel';
-import FormControl from 'react-bootstrap/lib/FormControl';
-import FormGroup from 'react-bootstrap/lib/FormGroup';
-import InputGroup from 'react-bootstrap/lib/InputGroup';
-import Modal from 'react-bootstrap/lib/Modal';
-import ModalBody from 'react-bootstrap/lib/ModalBody';
-import ModalFooter from 'react-bootstrap/lib/ModalFooter';
-import Row from 'react-bootstrap/lib/Row';
-import Button from 'react-bootstrap/lib/Button';
+import withStyles from 'isomorphic-style-loader/withStyles';
+import Col from 'react-bootstrap/Col';
+import Form from 'react-bootstrap/Form';
+import InputGroup from 'react-bootstrap/InputGroup';
+import Modal from 'react-bootstrap/Modal';
+import ModalBody from 'react-bootstrap/ModalBody';
+import ModalFooter from 'react-bootstrap/ModalFooter';
+import Row from 'react-bootstrap/Row';
+import Button from 'react-bootstrap/Button';
import { TEAM_SLUG_REGEX } from '../../constants';
import s from './DeleteTeamModal.scss';
@@ -20,18 +18,22 @@ class DeleteTeamModal extends Component {
team: PropTypes.object.isRequired,
shown: PropTypes.bool.isRequired,
hideModal: PropTypes.func.isRequired,
- deleteTeam: PropTypes.func.isRequired
+ deleteTeam: PropTypes.func.isRequired,
};
- state = {
- confirmSlug: ''
- };
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ confirmSlug: '',
+ };
+ }
handleChange = (event) => {
this.setState({
- confirmSlug: event.target.value
+ confirmSlug: event.target.value,
});
- }
+ };
handleSubmit = () => {
const { deleteTeam, host } = this.props;
@@ -39,7 +41,7 @@ class DeleteTeamModal extends Component {
deleteTeam().then(() => {
window.location.href = `//${host}/teams`;
});
- }
+ };
render() {
const { team, shown, hideModal } = this.props;
@@ -53,20 +55,22 @@ class DeleteTeamModal extends Component {
{' '}
{team.name}
{' '}
-team?
+ team?
{' '}
This is irreversible.
{' '}
- All restaurants and tags will be deleted,
- and all users will be unassigned from the team.
+ All restaurants and tags will
+ be deleted, and all users will be unassigned from the team.
+
+
+ To confirm, please write the URL of the team in the field below.
- To confirm, please write the URL of the team in the field below.
-
- Team URL
+
+ Team URL
-
- .lunch.pink
+ .lunch.pink
-
+
- Cancel
+
+ Cancel
+
({
dispatch
});
-const mergeProps = (stateProps, dispatchProps) => Object.assign({}, stateProps, dispatchProps, {
- deleteTeam: () => dispatchProps.dispatch(removeTeam())
-});
+const mergeProps = (stateProps, dispatchProps) => ({ ...stateProps, ...dispatchProps, deleteTeam: () => dispatchProps.dispatch(removeTeam()) });
export default connect(
mapStateToProps,
diff --git a/src/components/ErrorPage/ErrorPage.js b/src/components/ErrorPage/ErrorPage.js
index 0ea869b5b..913202a62 100644
--- a/src/components/ErrorPage/ErrorPage.js
+++ b/src/components/ErrorPage/ErrorPage.js
@@ -9,10 +9,10 @@
import PropTypes from 'prop-types';
import React from 'react';
-import withStyles from 'isomorphic-style-loader/lib/withStyles';
+import withStyles from 'isomorphic-style-loader/withStyles';
import s from './ErrorPage.scss';
-export function ErrorPage({ error }) {
+export const ErrorPage = ({ error }) => {
let title = 'Error';
let content = 'Sorry, a critical error occurred on this page.';
let errorMessage = null;
@@ -31,7 +31,7 @@ export function ErrorPage({ error }) {
{errorMessage}
);
-}
+};
ErrorPage.propTypes = { error: PropTypes.object.isRequired };
diff --git a/src/components/ErrorPage/ErrorPage.scss b/src/components/ErrorPage/ErrorPage.scss
index 7728c5bbc..aa043b5e0 100644
--- a/src/components/ErrorPage/ErrorPage.scss
+++ b/src/components/ErrorPage/ErrorPage.scss
@@ -7,7 +7,7 @@
* LICENSE.txt file in the root directory of this source tree.
*/
-@import '../../styles/_variables.scss';
+@import '../../styles/variables';
* {
margin: 0;
diff --git a/src/components/Flash/Flash.js b/src/components/Flash/Flash.js
index 8aaaa8bae..d71269ff3 100644
--- a/src/components/Flash/Flash.js
+++ b/src/components/Flash/Flash.js
@@ -1,6 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
-import withStyles from 'isomorphic-style-loader/lib/withStyles';
+import withStyles from 'isomorphic-style-loader/withStyles';
// eslint-disable-next-line css-modules/no-unused-class
import s from './Flash.scss';
@@ -8,7 +8,7 @@ class Flash extends Component {
static propTypes = {
expireFlash: PropTypes.func.isRequired,
message: PropTypes.string.isRequired,
- type: PropTypes.string.isRequired
+ type: PropTypes.string.isRequired,
};
componentDidMount() {
@@ -17,7 +17,9 @@ class Flash extends Component {
render() {
return (
- {this.props.message}
+
+ {this.props.message}
+
);
}
}
diff --git a/src/components/Flash/Flash.scss b/src/components/Flash/Flash.scss
index b4e36696f..917924bcc 100644
--- a/src/components/Flash/Flash.scss
+++ b/src/components/Flash/Flash.scss
@@ -1,4 +1,4 @@
-@import '../../styles/_variables.scss';
+@import '../../styles/variables';
.root {
padding: .5em;
diff --git a/src/components/Footer/Footer.js b/src/components/Footer/Footer.js
index 9d951347f..8aabb1efa 100644
--- a/src/components/Footer/Footer.js
+++ b/src/components/Footer/Footer.js
@@ -9,16 +9,13 @@
import PropTypes from 'prop-types';
import React from 'react';
-import withStyles from 'isomorphic-style-loader/lib/withStyles';
+import withStyles from 'isomorphic-style-loader/withStyles';
import s from './Footer.scss';
const Footer = ({ host }) => (
-
+
About / Privacy
@@ -38,7 +35,7 @@ const Footer = ({ host }) => (
);
Footer.propTypes = {
- host: PropTypes.string.isRequired
+ host: PropTypes.string.isRequired,
};
export default withStyles(s)(Footer);
diff --git a/src/components/Footer/Footer.scss b/src/components/Footer/Footer.scss
index eab2fd458..b1f7d6efa 100644
--- a/src/components/Footer/Footer.scss
+++ b/src/components/Footer/Footer.scss
@@ -7,8 +7,8 @@
* LICENSE.txt file in the root directory of this source tree.
*/
-@import '../../styles/_mixins.scss';
-@import '../../styles/_variables.scss';
+@import '../../styles/mixins';
+@import '../../styles/variables';
.root {
background: url('./footer-bg.jpg');
diff --git a/src/components/GoogleInfoWindow/GoogleInfoWindow.js b/src/components/GoogleInfoWindow/GoogleInfoWindow.js
index 4954e702b..6896d4fd0 100644
--- a/src/components/GoogleInfoWindow/GoogleInfoWindow.js
+++ b/src/components/GoogleInfoWindow/GoogleInfoWindow.js
@@ -1,15 +1,15 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment';
-import Button from 'react-bootstrap/lib/Button';
-import withStyles from 'isomorphic-style-loader/lib/withStyles';
+import Button from 'react-bootstrap/Button';
+import withStyles from 'isomorphic-style-loader/withStyles';
import s from './GoogleInfoWindow.scss';
let google = {
maps: {
Marker: { MAX_ZINDEX: 1000000 },
- places: { PlacesService: () => {}, PlacesServiceStatus: {} }
- }
+ places: { PlacesService: () => {}, PlacesServiceStatus: {} },
+ },
};
if (canUseDOM) {
google = window.google || google;
@@ -19,7 +19,7 @@ class GoogleInfoWindow extends Component {
static propTypes = {
addRestaurant: PropTypes.func.isRequired,
map: PropTypes.any.isRequired,
- placeId: PropTypes.string.isRequired
+ placeId: PropTypes.string.isRequired,
};
constructor(props) {
@@ -45,12 +45,8 @@ class GoogleInfoWindow extends Component {
style={{ zIndex: google.maps.Marker.MAX_ZINDEX * 2 }}
>
-
-Add to Lunch
+
+ Add to Lunch
diff --git a/src/components/GoogleInfoWindow/GoogleInfoWindow.scss b/src/components/GoogleInfoWindow/GoogleInfoWindow.scss
index 2ecd2d70f..4235278b6 100644
--- a/src/components/GoogleInfoWindow/GoogleInfoWindow.scss
+++ b/src/components/GoogleInfoWindow/GoogleInfoWindow.scss
@@ -1,5 +1,5 @@
-@import '../../styles/_mixins.scss';
-@import '../../styles/_variables.scss';
+@import '../../styles/mixins';
+@import '../../styles/variables';
.root {
@include info-window;
diff --git a/src/components/GoogleMapsLoaderContext/GoogleMapsLoaderContext.js b/src/components/GoogleMapsLoaderContext/GoogleMapsLoaderContext.js
new file mode 100644
index 000000000..1e8d57271
--- /dev/null
+++ b/src/components/GoogleMapsLoaderContext/GoogleMapsLoaderContext.js
@@ -0,0 +1,5 @@
+import { createContext } from 'react';
+
+const GoogleMapsLoaderContext = createContext();
+
+export default GoogleMapsLoaderContext;
diff --git a/src/components/Header/Header.js b/src/components/Header/Header.js
index 9ebf1d49f..e59e03753 100644
--- a/src/components/Header/Header.js
+++ b/src/components/Header/Header.js
@@ -9,7 +9,7 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
-import withStyles from 'isomorphic-style-loader/lib/withStyles';
+import withStyles from 'isomorphic-style-loader/withStyles';
import HeaderLoginContainer from '../HeaderLogin/HeaderLoginContainer';
import FlashContainer from '../Flash/FlashContainer';
import MenuContainer from '../Menu/MenuContainer';
@@ -22,33 +22,37 @@ class Header extends Component {
flashes: PropTypes.array.isRequired,
loggedIn: PropTypes.bool.isRequired,
// eslint-disable-next-line react/no-unused-prop-types
- path: PropTypes.string
+ path: PropTypes.string,
};
static defaultProps = {
- path: PropTypes.string
+ path: PropTypes.string,
};
static getDerivedStateFromProps(nextProps, state) {
if (nextProps.path !== state.prevPath) {
return {
menuOpen: false,
- prevPath: nextProps.path
+ prevPath: nextProps.path,
};
}
return null;
}
- state = {
- menuOpen: false,
- // eslint-disable-next-line react/no-unused-state
- prevPath: null
- };
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ menuOpen: false,
+ // eslint-disable-next-line react/no-unused-state
+ prevPath: null,
+ };
+ }
flashContainers = () => {
const { flashes } = this.props;
- return flashes.map(flash => (
+ return flashes.map((flash) => (
));
- }
+ };
closeMenu = () => {
this.setState({
- menuOpen: false
+ menuOpen: false,
});
- }
+ };
toggleMenu = () => {
- this.setState(prevState => ({
- menuOpen: !prevState.menuOpen
+ this.setState((prevState) => ({
+ menuOpen: !prevState.menuOpen,
}));
- }
+ };
render() {
const { loggedIn } = this.props;
@@ -78,9 +82,7 @@ class Header extends Component {
-
- {this.flashContainers()}
-
+
{this.flashContainers()}
@@ -90,18 +92,28 @@ class Header extends Component {
- {loggedIn
- ? (
-
-
- Menu
-
- {menuOpen && }
-
-
- )
- :
- }
+ {loggedIn ? (
+
+
+ Menu
+
+ {menuOpen && (
+
+ )}
+
+
+ ) : (
+
+ )}
);
}
diff --git a/src/components/Header/Header.scss b/src/components/Header/Header.scss
index a28949942..4e1b7d272 100644
--- a/src/components/Header/Header.scss
+++ b/src/components/Header/Header.scss
@@ -7,8 +7,8 @@
* LICENSE.txt file in the root directory of this source tree.
*/
-@import '../../styles/_mixins.scss';
-@import '../../styles/_variables.scss';
+@import '../../styles/mixins';
+@import '../../styles/variables';
@keyframes animatedBackground {
from {
@@ -21,7 +21,7 @@
}
@keyframes squashStretch {
- from {
+ 0% {
transform: scaleX(1);
}
@@ -29,7 +29,7 @@
transform: scaleX(1.5);
}
- to {
+ 100% {
transform: scaleX(1);
}
}
@@ -138,7 +138,7 @@
.menuBackground {
@include plain-button;
- background: rgba(0, 0, 0, .5);
+ background: rgb(0 0 0 / 50%);
height: 100%;
left: 0;
position: fixed;
diff --git a/src/components/HeaderLogin/HeaderLogin.js b/src/components/HeaderLogin/HeaderLogin.js
index d8145d0df..5e509c7ba 100644
--- a/src/components/HeaderLogin/HeaderLogin.js
+++ b/src/components/HeaderLogin/HeaderLogin.js
@@ -9,8 +9,8 @@
import PropTypes from 'prop-types';
import React from 'react';
-import withStyles from 'isomorphic-style-loader/lib/withStyles';
-import Button from 'react-bootstrap/lib/Button';
+import withStyles from 'isomorphic-style-loader/withStyles';
+import Button from 'react-bootstrap/Button';
import s from './HeaderLogin.scss';
const HeaderLogin = ({ user }) => {
@@ -18,7 +18,7 @@ const HeaderLogin = ({ user }) => {
if (user.id === undefined) {
content = (
-
+
Log in
@@ -29,7 +29,7 @@ const HeaderLogin = ({ user }) => {
};
HeaderLogin.propTypes = {
- user: PropTypes.object.isRequired
+ user: PropTypes.object.isRequired,
};
export default withStyles(s)(HeaderLogin);
diff --git a/src/components/HereMarker/HereMarker.js b/src/components/HereMarker/HereMarker.js
index 1fa988603..cc09a9c2d 100644
--- a/src/components/HereMarker/HereMarker.js
+++ b/src/components/HereMarker/HereMarker.js
@@ -1,5 +1,5 @@
import React from 'react';
-import withStyles from 'isomorphic-style-loader/lib/withStyles';
+import withStyles from 'isomorphic-style-loader/withStyles';
import s from './HereMarker.scss';
const HereMarker = () =>
;
diff --git a/src/components/Html.js b/src/components/Html.js
index 4e29e67bc..020441c87 100644
--- a/src/components/Html.js
+++ b/src/components/Html.js
@@ -16,7 +16,6 @@ import config from '../config';
class Html extends Component {
static propTypes = {
- apikey: PropTypes.string,
app: PropTypes.object, // eslint-disable-line
title: PropTypes.string.isRequired,
ogTitle: PropTypes.string.isRequired,
@@ -31,7 +30,6 @@ class Html extends Component {
};
static defaultProps = {
- apikey: '',
styles: [],
scripts: [],
root: ''
@@ -39,7 +37,6 @@ class Html extends Component {
render() {
const {
- apikey,
app,
title,
ogTitle,
@@ -55,7 +52,7 @@ class Html extends Component {
{config.analytics.googleTrackingId
&& (
-
+ <>
-
- )
- }
+ >
+ )}
{title}
@@ -100,7 +96,6 @@ class Html extends Component {
{!module.hot &&