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

- - + + Name - + - - - - + + + Email - + - - - - + + + Type - + - {f(gm('guestRole'))}} {hasMemberRole && } {hasOwnerRole && } - + - + Members can add new users and remove guests. {hasOwnerRole - && ' Owners can manage all user roles and manage overall team information.' - } - - + && ' Owners can manage all user roles and manage overall team information.'} + + - + Please tell the user you are inviting to check their spam folder if they don’t receive anything shortly. - +
); 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 - + - + - + 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 - + - + 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 - ? ( -
- - {menuOpen &&
- ) - : - } + {loggedIn ? ( +
+ + {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 = (
-
@@ -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 && ( - + <>