diff --git a/.eslintignore b/.eslintignore index 2952f5022..c4d7d9993 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,3 @@ src/js/vendor/*.js -src/js/mock-data/ \ No newline at end of file +src/js/mock-data/ +src/js/dispatcher/Dispatcher.js diff --git a/.eslintrc b/.eslintrc index 8f524243f..491e37665 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,5 +1,8 @@ { "parser": "babel-eslint", + "parserOptions": { + "sourceType": "module" + }, "env": { "browser": true, @@ -7,206 +10,71 @@ "es6": true }, - "ecmaFeatures": { - "arrowFunctions": true, - "binaryLiterals": true, - "blockBindings": true, - "classes": false, - "defaultParams": true, - "destructuring": true, - "forOf": true, - "generators": true, - "modules": true, - "objectLiteralComputedProperties": true, - "objectLiteralDuplicateProperties": true, - "objectLiteralShorthandMethods": true, - "objectLiteralShorthandProperties": true, - "octalLiterals": true, - "regexUFlag": true, - "regexYFlag": true, - "spread": true, - "superInFunctions": false, - "templateStrings": true, - "unicodeCodePointEscapes": true, - "globalReturn": true, - "jsx": true - }, + "extends": "airbnb", "rules": { - "block-scoped-var": [0], - "brace-style": [2, "1tbs", {"allowSingleLine": true}], - "camelcase": [0], - "comma-dangle": [0], - "comma-spacing": [1], - "comma-style": [2, "last"], - "complexity": [0, 11], - "consistent-return": [1], - "consistent-this": [0, "that"], - "curly": [0, "multi-line"], - "default-case": [1], - "dot-notation": [2, {"allowKeywords": true}], - "eol-last": [1], - "eqeqeq": [1], - "func-names": [0], - "func-style": [0, "declaration"], - "generator-star-spacing": [2, "after"], - "guard-for-in": [0], - "handle-callback-err": [0], - "key-spacing": [1, {"beforeColon": false, "afterColon": true}], - "quotes": [1, "double", "avoid-escape"], - "max-depth": [0, 4], + "array-bracket-spacing": [1, "never"], + "camelcase": [1], + "class-methods-use-this": 0, // We want to allow helper functions that don't use 'this' in classes + "jsx-a11y/alt-text": 0, + "jsx-a11y/anchor-has-content": 1, + "jsx-a11y/anchor-is-valid": 1, + "jsx-a11y/click-events-have-key-events": 0, + "jsx-a11y/heading-has-content": 1, + "jsx-a11y/iframe-has-title": 1, + "jsx-a11y/label-has-associated-control": 1, + "jsx-a11y/label-has-for": 0, // Deprecated in favor of 'label-has-associated-control' + "jsx-a11y/mouse-events-have-key-events": 1, + "jsx-a11y/no-autofocus": 1, + "jsx-a11y/no-noninteractive-element-interactions": 1, + "jsx-a11y/no-static-element-interactions": 0, + "jsx-quotes": 2, "max-len": [0, 80, 4], - "max-nested-callbacks": [0, 2], - "max-params": [0, 3], - "max-statements": [0, 10], - "new-parens": [2], - "new-cap": [0], - "newline-after-var": [0], - "no-alert": [2], - "no-array-constructor": [2], - "no-bitwise": [0], - "no-caller": [2], - "no-catch-shadow": [2], - "no-cond-assign": [2], "no-console": [0], - "no-constant-condition": [1], - "no-continue": [2], - "no-control-regex": [2], - "no-debugger": [2], - "no-delete-var": [2], - "no-div-regex": [0], - "no-dupe-args": [2], - "no-dupe-keys": [2], - "no-duplicate-case": [2], "no-else-return": [0], - "no-empty": [2], - "no-empty-character-class": [2], - "no-empty-label": [2], - "no-eq-null": [0], - "no-eval": [2], - "no-ex-assign": [2], - "no-extend-native": [1], - "no-extra-bind": [1], - "no-extra-boolean-cast": [2], - "no-extra-semi": [1], - "no-fallthrough": [2], - "no-floating-decimal": [2], - "no-func-assign": [2], - "no-implied-eval": [2], - "no-inline-comments": [0], - "no-inner-declarations": [2, "functions"], - "no-invalid-regexp": [2], - "no-irregular-whitespace": [2], - "no-iterator": [2], - "no-label-var": [2], - "no-labels": [2], - "no-lone-blocks": [2], - "no-lonely-if": [1], - "no-loop-func": [1], - "no-mixed-requires": [0, false], - "no-mixed-spaces-and-tabs": [2, false], - "no-multi-spaces": [1], - "no-multi-str": [2], - "no-multiple-empty-lines": [2, {"max": 2}], - "no-native-reassign": [1], - "no-negated-in-lhs": [2], - "no-nested-ternary": [0], - "no-new": [2], - "no-new-func": [2], - "no-new-object": [2], - "no-new-require": [0], - "no-new-wrappers": [2], - "no-obj-calls": [2], - "no-octal": [2], - "no-octal-escape": [2], - "no-param-reassign": [1], - "no-path-concat": [0], + "no-multi-spaces": [0], + "no-multiple-empty-lines": [0], // allow indented comments (like this one) "no-plusplus": [0], - "no-process-env": [0], - "no-process-exit": [2], - "no-proto": [2], - "no-redeclare": [1], - "no-regex-spaces": [2], - "no-reserved-keys": [0], - "no-restricted-modules": [0], - "no-return-assign": [2], - "no-script-url": [2], - "no-self-compare": [0], - "no-sequences": [1], - "no-shadow": [1], - "no-shadow-restricted-names": [2], - "no-spaced-func": [1], - "no-sparse-arrays": [2], - "no-sync": [0], - "no-ternary": [0], - "no-throw-literal": [2], - "no-trailing-spaces": [1], - "no-undef": [1], - "no-undef-init": [2], - "no-undefined": [0], "no-underscore-dangle": [0], - "no-unreachable": [2], - "no-unused-expressions": [1], - "no-unused-vars": [1], - "no-use-before-define": [1], - "no-void": [0], - "no-warning-comments": [0, {"terms": ["todo", "fixme", "xxx"], "location": "start"}], - "no-with": [2], - "no-extra-parens": [1], - "one-var": [1, "never"], - "operator-assignment": [0, "always"], - "operator-linebreak": [1, "after"], - "padded-blocks": [0], - "quote-props": [0], - "radix": [0], - "semi": [1], - "semi-spacing": [2, {"before": false, "after": true}], - "sort-vars": [0], - "space-after-keywords": [2, "always"], - "space-before-function-paren": [1, {"anonymous": "always", "named": "always"}], - "space-before-blocks": [0, "always"], - "space-in-brackets": [ - 0, "never", { - "singleValue": true, - "arraysInArrays": false, + "object-curly-newline": 0, + "object-curly-spacing": [1, + "always", { "arraysInObjects": false, - "objectsInArrays": true, - "objectsInObjects": true, - "propertyName": false + "objectsInObjects": true } ], - "space-in-parens": [0], - "space-infix-ops": [1], - "space-return-throw-case": [2], - "space-unary-ops": [0, {"words": true, "nonwords": false}], - "spaced-line-comment": [0, "always"], - "strict": [0, "never"], - "use-isnan": [2], - "valid-jsdoc": [0], - "valid-typeof": [2], - "vars-on-top": [0], - "wrap-iife": [2], - "wrap-regex": [1], - "yoda": [2, "never", {"exceptRange": true}], - "react/jsx-boolean-value": 2, - "react/jsx-no-undef": 2, - "react/jsx-sort-props": 0, - "react/jsx-sort-prop-types": 0, - "react/jsx-uses-react": 2, - "react/jsx-uses-vars": 2, + "operator-linebreak": [1, "after"], + "padded-blocks": [1], + "prefer-destructuring": 1, + "quotes": [1, "single", "avoid-escape"], + "radix": 0, // Dec 2018: We always use base 10, so specifying it each time seems excessive + "react/button-has-type": 1, + "react/destructuring-assignment": 0, // Dec 2018: We should do this! But right now we have 3990 warnings/errors if enabled. + "react/forbid-prop-types": 0, // Dec 2018: Should consider someday + "react/indent-prop": 0, + "react/jsx-first-prop-new-line": 0, + "react/jsx-indent-props": 0, + "react/jsx-no-bind": 1, // Dec 2018: Should these be errors? + "react/no-access-state-in-setstate": 1, + "react/no-array-index-key": 1, + "react/no-children-prop": 1, "react/no-did-mount-set-state": 0, - "react/no-did-update-set-state": 2, - "react/no-multi-comp": 2, - "react/no-unknown-property": 1, + "react/no-did-update-set-state": 0, + "react/no-string-refs": 1, + "react/no-unused-prop-types": 1, + "react/no-unused-state": 1, + "react/prefer-stateless-function": 0, "react/prop-types": 1, - "react/react-in-jsx-scope": 2, - "react/self-closing-comp": 2, - "jsx-quotes": 2, + "react/require-default-props": 0, // Dec 2018: Might have value someday + "react/sort-comp": 1, + "react-hooks/rules-of-hooks": "error", // Checks rules of Hooks + "react-hooks/exhaustive-deps": "warn", // Checks effect dependencies + "space-before-function-paren": [1, {"anonymous": "always", "named": "always"}], + "space-in-parens": [1], + "template-curly-spacing": ["warn", "never"] }, "plugins": [ - "react" - ], - "global": { - "React": true - } + "react", + "react-hooks" + ] } diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 000000000..8418c8d11 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,3 @@ +### Please describe the issue (What happens? What do you expect?) + +### Steps to reproduce the problem (1, 2, 3...), including links diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..6a192ff63 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,3 @@ +### What github.com/wevote/WebApp/issues does this fix? + +### Changes included this pull request? diff --git a/.gitignore b/.gitignore index 734bedcbd..3e7ee8a2c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ # Ignore build files build +build-live +package-lock.json # TernJS ################### @@ -24,10 +26,6 @@ pids ################### lib-cov -# Coverage directory used by tools like istanbul -################### -coverage - # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) ################### .grunt @@ -60,13 +58,8 @@ venv *.class *.dll *.exe -*.o *.so - - - - # PyInstaller # # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. @@ -109,11 +102,8 @@ target/ # git has its own built in compression methods *.7z *.dmg -*.gz *.iso -*.jar *.rar -*.tar *.zip # OS generated files # @@ -152,8 +142,6 @@ sftp-config.json # Vim / emacs # ############### -*~ -*# *.swp *.swo @@ -161,15 +149,32 @@ sftp-config.json ################## .sass-cache +# Heroku Related # +################## +staticfiles + # Other # ######### /geo/data/ /**/migrations/*.py !/**/migrations/__init__.py -config/environment_variables.json +src/js/config.js +src/js/config-qa.js +src/js/config-www.js +tests/browserstack/browserstack.config.js +src/javascript/google-tag-manager.js +src/javascript/google-tag-manager-qa.js +src/javascript/google-tag-manager-www.js +src/javascript/google-analytics.js +src/javascript/google-analytics-qa.js +src/javascript/google-analytics-www.js web_app/build/* +*.crt +*.key +.vscode +.gitattributes +/yarn.lock +server.csr +tests/browserstack/wdio.conf.js +tests/browserstack/wdio.conf.template -# Heroku Related # -################## -*.pyc -staticfiles diff --git a/.jscsrc b/.jscsrc index e4b04b4dc..b6d5f679c 100644 --- a/.jscsrc +++ b/.jscsrc @@ -1,5 +1,11 @@ { "preset": "airbnb", "validateQuoteMarks": null, - "excludeFiles": ["build/**", "node_modules"] + "excludeFiles": ["build/**", "node_modules"], + "maximumLineLength": null, + "maxErrors": 200, + "disallowSpacesInFunctionDeclaration": null, + "requirePaddingNewLinesBeforeLineComments": null, + "requirePaddingNewLinesAfterBlocks": null, + "disallowMultipleLineBreaks": null } diff --git a/.snyk b/.snyk new file mode 100644 index 000000000..43752f637 --- /dev/null +++ b/.snyk @@ -0,0 +1,91 @@ +# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. +version: v1.19.0 +ignore: {} +# patches apply the minimum changes required to fix a vulnerability +patch: + SNYK-JS-LODASH-450202: + - lodash: + patched: '2019-07-04T03:07:14.083Z' + - node-sass > sass-graph > lodash: + patched: '2019-07-04T03:07:14.083Z' + - styled-components > babel-plugin-styled-components > lodash: + patched: '2019-07-04T03:07:14.083Z' + - node-sass > gaze > globule > lodash: + patched: '2019-07-04T03:07:14.083Z' + - styled-components > @babel/helper-module-imports > @babel/types > lodash: + patched: '2019-07-04T03:07:14.083Z' + - styled-components > babel-plugin-styled-components > @babel/helper-annotate-as-pure > @babel/types > lodash: + patched: '2019-07-04T03:07:14.083Z' + SNYK-JS-HTTPSPROXYAGENT-469131: + - snyk > proxy-agent > https-proxy-agent: + patched: '2019-10-04T03:05:20.920Z' + - snyk > proxy-agent > pac-proxy-agent > https-proxy-agent: + patched: '2019-10-04T03:05:20.920Z' + SNYK-JS-TREEKILL-536781: + - snyk > snyk-sbt-plugin > tree-kill: + patched: '2019-12-12T03:06:33.992Z' + SNYK-JS-LODASH-567746: + - snyk > lodash: + patched: '2020-05-01T03:05:39.212Z' + - react-phone-number-input > react-responsive-ui > lodash: + patched: '2020-05-01T03:05:39.212Z' + - snyk > @snyk/dep-graph > lodash: + patched: '2020-05-01T03:05:39.212Z' + - snyk > inquirer > lodash: + patched: '2020-05-01T03:05:39.212Z' + - snyk > snyk-config > lodash: + patched: '2020-05-01T03:05:39.212Z' + - snyk > snyk-mvn-plugin > lodash: + patched: '2020-05-01T03:05:39.212Z' + - snyk > snyk-nodejs-lockfile-parser > lodash: + patched: '2020-05-01T03:05:39.212Z' + - snyk > snyk-nuget-plugin > lodash: + patched: '2020-05-01T03:05:39.212Z' + - styled-components > @babel/traverse > lodash: + patched: '2020-05-01T03:05:39.212Z' + - styled-components > babel-plugin-styled-components > lodash: + patched: '2020-05-01T03:05:39.212Z' + - snyk > @snyk/dep-graph > graphlib > lodash: + patched: '2020-05-01T03:05:39.212Z' + - snyk > snyk-go-plugin > graphlib > lodash: + patched: '2020-05-01T03:05:39.212Z' + - snyk > snyk-nodejs-lockfile-parser > graphlib > lodash: + patched: '2020-05-01T03:05:39.212Z' + - snyk > @snyk/snyk-cocoapods-plugin > @snyk/dep-graph > lodash: + patched: '2020-05-01T03:05:39.212Z' + - snyk > snyk-nuget-plugin > dotnet-deps-parser > lodash: + patched: '2020-05-01T03:05:39.212Z' + - snyk > snyk-php-plugin > @snyk/composer-lockfile-parser > lodash: + patched: '2020-05-01T03:05:39.212Z' + - styled-components > @babel/traverse > @babel/generator > lodash: + patched: '2020-05-01T03:05:39.212Z' + - snyk > @snyk/snyk-cocoapods-plugin > @snyk/dep-graph > graphlib > lodash: + patched: '2020-05-01T03:05:39.212Z' + - snyk > @snyk/snyk-cocoapods-plugin > @snyk/cocoapods-lockfile-parser > @snyk/ruby-semver > lodash: + patched: '2020-05-01T03:05:39.212Z' + - styled-components > babel-plugin-styled-components > @babel/helper-annotate-as-pure > @babel/types > lodash: + patched: '2020-05-01T03:05:39.212Z' + - styled-components > @babel/traverse > @babel/helper-split-export-declaration > @babel/types > lodash: + patched: '2020-05-01T03:05:39.212Z' + - snyk > @snyk/snyk-cocoapods-plugin > @snyk/cocoapods-lockfile-parser > @snyk/dep-graph > graphlib > lodash: + patched: '2020-05-01T03:05:39.212Z' + - styled-components > @babel/traverse > @babel/helper-function-name > @babel/template > @babel/types > lodash: + patched: '2020-05-01T03:05:39.212Z' + - wdio > selenium-standalone > lodash: + patched: '2020-08-22T21:58:11.586Z' + - wdio > selenium-standalone > async > lodash: + patched: '2020-08-22T21:58:11.586Z' + - wdio > webdriverio > archiver > lodash: + patched: '2020-08-22T21:58:11.586Z' + - wdio > webdriverio > inquirer > lodash: + patched: '2020-08-22T21:58:11.586Z' + - wdio > webdriverio > archiver > async > lodash: + patched: '2020-08-22T21:58:11.586Z' + - wdio > webdriverio > archiver > archiver-utils > lodash: + patched: '2020-08-22T21:58:11.586Z' + - wdio > webdriverio > archiver > zip-stream > lodash: + patched: '2020-08-22T21:58:11.586Z' + - wdio > webdriverio > gaze > globule > lodash: + patched: '2020-08-22T21:58:11.586Z' + - wdio > webdriverio > archiver > zip-stream > archiver-utils > lodash: + patched: '2020-08-22T21:58:11.586Z' diff --git a/.stylelintrc b/.stylelintrc new file mode 100644 index 000000000..a3cf679d6 --- /dev/null +++ b/.stylelintrc @@ -0,0 +1,43 @@ +{ + "extends": "stylelint-config-standard", + "plugins": [ + "stylelint-declaration-use-variable" + ], + "rules": { + "at-rule-empty-line-before": null, + "at-rule-no-unknown": [true, { + "ignoreAtRules": ["content", "each", "else", "error", "extend", "for", "function", "if", "include", "mixin", "return", "warn", "while"] + }], + "block-closing-brace-newline-after": ["always-multi-line", { + "ignoreAtRules": ["if", "else"] + }], + "block-opening-brace-space-before": "always-multi-line", + "color-hex-case": "lower", + "declaration-empty-line-before": null, + "font-family-no-missing-generic-family-keyword": null, + "max-empty-lines": 3, + "no-descending-specificity": null, + "number-leading-zero": "never", + "rule-empty-line-before": null, + "selector-max-id": 0, + "selector-max-type": [0, { + "ignoreTypes": ["a"] + }], + "shorthand-property-no-redundant-values": null, + "sh-waqar/declaration-use-variable": [[ + "border-radius", + "/color/", + "/margin/", + "/padding/" + ]], + "string-quotes": "single" + }, + "ignoreFiles": [ + "**/index.html", + "**/vendor/**/*.scss", + "**/_normalize.scss", + "**/overrides/_print.scss", + "**/components/_bootstrap3LeftBehind.scss", + "**/components/_nps-input.scss" + ] +} diff --git a/.travis.yml b/.travis.yml index 6361fa79b..29920310e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,22 @@ language: node_js node_js: - - "4.1" - - "4.0" + - "10.12.0" sudo: false +before_script: + - "cp src/js/config-template.js src/js/config.js" + - "npm install -g se-interpreter" script: - npm run lint + - "npm run autoTest" + - "se-interpreter tests/selenium/interpreter_config.json" +env: + global: + - SAUCE_USERNAME=WeVote + - SAUCE_ACCESS_KEY=eb6e0454-e844-4a1a-a695-2d5830897566 + - secure: "xPe0mHTsisrniOOce/bgc6G9dmClm0sT3uxfw6nknfV6PdL6FglVX2+ji9RdttHzvg4aLk4QURJLQjzuZsNhxSNeHNU1qtSuoFVwvFuQ7iFNvWFF6U06vbQayK488piEDm4AxXcgAuj4jtEvAmRNSrCPO31HGiBo5wGAA3Jgn/8y0F0VXWYcEk7qr57hT6l4+qZXMVIwp+6LksptLg1obAmYmF1Gprnlixv5VBvLJqABr/HbnUUg31RIGz459sKvdOG+R6eJPMNSp73pMg5NxGpxAwh21HYQI+R4qs8aQYgCM/2UAv3ujIleqzkG2J/9uSDT0sc6RRnFfAzX6DmYZKD6Sf9pWFgk4zS4jqnVIZpry7vrMUkZK0qeQpphB7otvCJB0cPbYwoiwdWOKxkRZXFR1BPOm5FdM9thhUsdMqSfGHnGVhq9NRPl4bQJOIltn8lYHj1z84jkEyVGmpU4XTDfrlSVbRCBV5WNn9w6bFtsPgoupX99xhsjDkJ4lSOlQj6Ul5ncAFLe9KCXdsGPq+y0BK6DQwY4xoBTsxTqDl5VvrN63wtzJ2Br5REz25j+Y9aagDgsRQGX4ue/RCxdf3PHj879cskz7cE+GP1GBRca/NKts2VVzgTKsinQDTN3zyFaZQR8ggShkvPvcvW+7xJf3UKkrN+KTMU5WjeMNlw=" notifications: slack: wevote:Ngu6uKzt4qRAvhrCPPoTIIVM + email: + - SauceLabs@WeVote.US +addons: + jwt: + secure: "eb6e0454-e844-4a1a-a695-2d5830897566" diff --git a/AppleSilicon.md b/AppleSilicon.md new file mode 100644 index 000000000..f442f0021 --- /dev/null +++ b/AppleSilicon.md @@ -0,0 +1,20 @@ +# Apple Silicon on macOS Big Sur 11 beta, October 9, 2020 + +Will remove these notes once Big Sur is released, and Apple Silicon (iOS app running on the desktop) is stable. + +1. Safari on Big Sur can not currently inspect "My Mac" targets, so the weak xCode console is all we have for now. +See Apple [Universal App Quick Start] private forum [662281](https://developer.apple.com/forums/thread/662281?login=true). Also +an Apple Beta Feedback Assistant issue, for Big Sur beta 8, has been lodged (FB8755308) titled "No Inspectable Applications" in Safari Debug Menu for targets with "My Mac" -- No response yet from Apple. +1. node-sass is tied to macOS versions, so it is not available for Big Sur yet. Hopefully we will design it out +of the "We Vote Web App" this year. In the meantime, we need to build main.css on an intel Mac, and copy it to the +Apple Silicon (ARM64) mac in order to have all the legacy styles work. +Manual changes have to be made to package.json and webpack.config.js on the ARM64 Mac to remove +node-sass from the build. +1. Firebase is not ready for Big Sur, so it has to be manually removed from WeVoteCordova, so we lose notifications +in iOS until it is ready. Remove "cordova-plugin-firebase-analytics" and "cordova-plugin-firebase-messaging" to stop compile errors on the ARM64 DTK. +1. cordova.plugins.diagnostic has been added to detect which processor the app is running on. +1. A forked version of cordova-plugin-device is needed, to return the native code c++ variable `isIOSAppOnMac` so we can determine +`isIOSAppOnMac()`. It is a small change, and I'll submit a PR to Apache, to see if they will adopt it. +1. The Sign in with Twitter and Facebook are currently unavailable since there is a problem where opening a "tab" with corodova-plugin-inappbrowser +opens an empty modal dialog, and then opens the actual URL you send it in a tab in the Safari desktop browser, breaking oAuth +flow. See [Universal App Quick Start] private forum issue [662697](https://developer.apple.com/forums/thread/662697) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a1abf24e..2422d565d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ## Contents - [WeVoteUSA Web App Change Log](#wevoteusa-web-app-change-log) + - [1.0.0](#100) + - [0.8.9](#089) - [0.8.0](#080) - [0.5.0](#050) @@ -11,6 +13,83 @@ #WeVoteUSA Web App Change Log All notable changes to this project will be documented here. +##1.0.0 +
  • view commit • Rename facebookactioncreators - Lisa Cho
  • +
  • view commit • Refactor - Lisa Cho
  • +
  • view commit • Iterated on Navigation elements and changed some language per design discussions. - Dale John McGrew
  • +
  • view commit • remove duplicated jquery loading - Lisa Cho
  • +
  • view commit • Fix for Issue #233 "Two voter_device_id cookies created" - Dale John McGrew
  • +
  • view commit • Fix link - Lisa Cho
  • +
  • view commit • Turn back on Facebook login and add logout, remove message box when there is no ballot caveat - Lisa Cho
  • +
  • view commit • Fixed some Javascript errors around undefined variables. Fixed one more "npm test" warning. Made javascript modifications per Lisa's suggestions. Added measure_subtitle. - Dale John McGrew
  • +
  • view commit • removing unnecessary dependency - Nick Fiorini
  • +
  • view commit • Update package.json - Nick Fiorini
  • +
  • view commit • updating minification process to reduce the bundle size down to 1.3mb - nick fiorini
  • +
  • view commit • adding uglification proccess to production build, reducing the bundle output by ~%50, 5.5mb -> 2mb - nick fiorini
  • +
  • view commit • Fixed some Javascript errors around undefined variables. Fixed one more "npm test" warning. Made javascript modifications per Lisa's suggestions. Added measure_subtitle. - Dale John McGrew
  • +
  • view commit • Fixed some Javascript errors around undefined variables. Fixed one more "npm test" warning. - Dale John McGrew
  • +
  • view commit • Fix candidate reload bug, organize widget files - Lisa Cho
  • +
  • view commit • Add hover to button, bug fixes - Lisa Cho
  • +
  • view commit • Added twitter_description to candidate card and candidate detail page. Moved functions "numberWithCommas" and "removeTwitterNameFromDescription" to utils/textFormat.js. Fixed "toString" error Lisa found. - Dale John McGrew
  • +
  • view commit • Fixed some eslint errors from running "npm test". Added Twitter Description to organization display. - Dale John McGrew
  • +
  • view commit • Connect support toggle button on organization page - Lisa Cho
  • +
  • view commit • Added 0.8.9 to changelog - Rob Simpson
  • + + +##0.8.9 +
  • view commit • Update PositionItem.jsx - Dale John McGrew
  • +
  • view commit • Header icons stay in place in desktop mode - Rob Simpson
  • +
  • view commit • Button change from Following to Unfollow and change color - Rob Simpson
  • +
  • view commit • Updated components/Ballot/PositionItem.jsx to support individual voter opinions. - Dale John McGrew
  • +
  • view commit • Fixed glitch with Ballot filtering introduced, relating to missing address. Updated the project About page to give credit based on time volunteered. - Dale John McGrew
  • +
  • view commit • Fix links - Lisa Cho
  • +
  • view commit • Make submenu slidedown - Lisa Cho
  • +
  • view commit • Added political party to Candidate card on Ballot list. Changed some site text, including navigation text to be consistent. Fixed some Javascript errors on Ballot route. Updated site title. - Dale John McGrew
  • +
  • view commit • Clean up linting errors for CI build - Rob Simpson
  • +
  • view commit • fix android footer keyboard issue - Rob Simpson
  • +
  • view commit • The queries for the change in sizes are added so that the app will fit inside of smaller devices - Rob Simpson
  • +
  • view commit • At this time there will need to be refactoring of the ButtonToolbar in order for all buttons to be consistent accross the app. Until that can happen, the larger display will continue to have the smaller buttons on this view. - Rob Simpson
  • +
  • view commit • At Status - Lisa Cho
  • +
  • view commit • Prevent ballot items from immediately disappearing from filtered ballots, add titles - Lisa Cho
  • +
  • view commit • Add ballot caveat and properties to ballot - Lisa Cho
  • +
  • view commit • Fix hard refresh issues - Lisa Cho
  • +
  • view commit • Replace multiple API calls with single starAllStatusRetrieve - Lisa Cho
  • +
  • view commit • Fix bug - Lisa Cho
  • +
  • view commit • Add Choices remaining ballot - Lisa Cho
  • +
  • view commit • Add support ballot - Lisa Cho
  • +
  • view commit • Use new API endpoints for fetching multiple support/oppose count and voter support/oppose statuses - Lisa Cho
  • +
  • view commit • Note that this is static at this time until there is a better idea of what the other site is that will be created. If this is good enough, close this ticket and open another, or move this into another milestone when that site will be created. - Rob Simpson
  • +
  • view commit • #116 - Reduce vertical display of voter guide boxes - Rob Simpson
  • +
  • view commit • Fixed Link: limited scope so the "Follow"/"Ignore" buttons aren't affected. - Dale John McGrew
  • +
  • view commit • Added links to among voter guides, candidates and orgs so you can navigate easily. Changed (removed) some site text based on voter testing. Fixed some broken README links. - Dale John McGrew
  • +
  • view commit • Add organization page - Lisa Cho
  • +
  • view commit • Forgot one change - Lisa Cho
  • +
  • view commit • Fixing broken image links, bootstrap-map-css warning, address not found on page reload - Lisa Cho
  • +
  • view commit • Worked on issues #8 & #133. Added political party display on Candidate page. Modified Address edit field. Deleted deprecated Home.jsx file. Added "Find Opinions" button at the bottom of the candidate page. - Dale John McGrew
  • +
  • view commit • Add search box to IntroOpinions and Opinions - Lisa Cho
  • +
  • view commit • Removing deprecated files - Lisa Cho
  • +
  • view commit • add ternjs for repo ignoring - Rob Simpson
  • +
  • view commit • Fix IntroOpinions page - Lisa Cho
  • +
  • view commit • adding a production task, `npm run prod` that avoids browsersync and watching files for changes. also added a timestamp to the browsersync prefix and updated the server task to a module that takes production flag as input. - nick fiorini
  • +
  • view commit • update changelog with 0.8.0 commits - Rob Simpson
  • +
  • view commit • Restructured our documentation so it is easier for new developers to get started, and easier for current developers to zero-in on the specific information they need. I tried *really* hard to make sure I didn't delete any existing documentation. My apologies in advance if I removed any documentation (without moving it to a new place). There is still more clean-up of this documentation to be done, but I think it can be done incrementally, and by multiple people. PS. Can someone help create the table of contents for these pages with doctoc? - Dale John McGrew
  • +
  • view commit • Refactor voter store and initial loading of data, redirect user when ballot is empty, remove cookies, preload opinions - Lisa Cho
  • +
  • view commit • Handle rapid clicks on support/oppose items - Lisa Cho
  • +
  • view commit • Serious Karma-points checkin. :) Fixed code so we have nearly 200 fewer eslint errors when you run ‘npm test’. - Dale John McGrew
  • +
  • view commit • Made the area a voter can touch much bigger for Links in org Positions on Candidate page, and navigation items. Removed some code we won't use for several versions. Commented out sign in, but left code because several of us may need to use the code soon for testing. Added link to Candidate-specific "More Opinions" page, and built out OpinionsAboutItem.jsx page. (Debugging help needed from Lisa.) Made edits to Location page where we edit address, including adding Focus to the form box on componentDidMount. Removed old "More menu" page. Did some ESLint clean up. - Dale John McGrew
  • +
  • view commit • Added search box to More Opinions to Follow page -- ready to get working. Changed the way we react to missing data. Removed test address from config.js file. - Dale John McGrew
  • +
  • view commit • Update .travis.yml - Rob Simpson
  • +
  • view commit • Fixed browser sync to sync remote devices - Rob Simpson
  • +
  • view commit • Fix let vs var and refactor actions format - Lisa Cho
  • +
  • view commit • Link colors inline with the new style guide - Rob Simpson
  • +
  • view commit • Container for candidates more padding for icons - Rob Simpson
  • +
  • view commit • Margin and padding give space to container - Rob Simpson
  • +
  • view commit • Update README.md - Rob Simpson
  • +
  • view commit • Update README.md - Rob Simpson
  • +
  • view commit • Update CONTRIBUTING.md - Rob Simpson
  • +
  • view commit • Update BallotActions.js - Dale John McGrew
  • +
  • view commit • Now that we can return actual ballots reliably, turned off the "use_test_election" that had been built into voterBallotItemsRetrieve calls. Other assorted visual updates. - Dale John McGrew
  • + ##0.8.0
  • view commit • Contributing license agreement for those who contribute - Rob Simpson
  • view commit • My opinions followed stop following and ignore buttons work and refactored voterguidestore - Lisa Cho
  • @@ -124,7 +203,7 @@ All notable changes to this project will be documented here.
  • view commit • removing console log and fixing test errors
  • view commit • Leaving commented out code in place since we will be turning back on soon.
  • view commit • Turn off Account Settings page when NOT signed in.
  • -
  • view commit • Made the entire CandidateItem div a link (per original designs). Changed "support/oppose" language in cases where we are using ratings. Added "source" under Vote Smart ratings. Changed "My Ballot" to "My Voter Guide", and added "demo version" per Jenifer's requests. Added "My Voter Guide" link to left menu. Added indicator in both ItemActionBar's for when the voter supports or opposes something. Did some ES lint clean up. Added _position stylesheet. Added text offering test address.
  • +
  • view commit • Made the entire CandidateItem div a link (per original designs). Changed "support/oppose" language in cases where we are using ratings. Added "source" under Vote Smart ratings. Changed "My Ballot" to "My Voter Guide", and added "demo version". Added "My Voter Guide" link to left menu. Added indicator in both ItemActionBar's for when the voter supports or opposes something. Did some ES lint clean up. Added _position stylesheet. Added text offering test address.
  • view commit • Iteration on "demo version" text. Cleaned up more ES lint warnings.
  • view commit • Added text to README as test
  • diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 581fa2227..fecb0d3cd 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -10,9 +10,9 @@ -#WeVoteUSA Code of Conduct +# WeVoteUSA Code of Conduct --- -##Conduct +## Conduct * We are committed to providing a friendly, safe and welcoming environment for all, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, nationality, or other similar characteristic. * On public communication channels, please avoid using overtly sexual nicknames or other nicknames that might detract from a friendly, safe and welcoming environment for all. @@ -26,7 +26,7 @@ Respect that people have differences of opinion and that every design or impleme --- -##Moderation +## Moderation These are the policies for upholding our community's standards of conduct. If you feel that a thread needs moderation, please contact the WeVoteUSA moderation team. @@ -45,4 +45,4 @@ And if someone takes issue with something you said or did, resist the urge to be The enforcement policies listed above apply to all official WeVoteUSA venues; including official [Slack channel](http://wevote.slack.com) and GitHub repositories under WeVoteUSA. For other projects adopting the WeVoteUSA Code of Conduct, please contact the maintainers of those projects for enforcement. If you wish to use this code of conduct for your own project, consider explicitly mentioning your moderation policy or making a copy with your own moderation policy so as to avoid confusion. -Adapted from the [Rust CoC](http://www.rust-lang.org/conduct.html), [Node.js Policy on Trolling](http://blog.izs.me/post/30036893703/policy-on-trolling) as well as the [Contributor Covenant v1.3.0](http://contributor-covenant.org/version/1/3/0/) \ No newline at end of file +Adapted from the [Rust CoC](http://www.rust-lang.org/conduct.html), [Node.js Policy on Trolling](http://blog.izs.me/post/30036893703/policy-on-trolling) as well as the [Contributor Covenant v1.3.0](http://contributor-covenant.org/version/1/3/0/) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4c97df097..1de1d893c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,18 +1,3 @@ - - -## Contents - -- [Contributing to wevote/WebApp](#contributing-to-wevotewebapp) - - [Pull Requests](#pull-requests) - - [Setting up your repository for work](#setting-up-your-repository-for-work) - - [Some useful tips and tricks](#some-useful-tips-and-tricks) - - [Git completion](#git-completion) - - [Prompt](#prompt) - - [How to get a change from someone’s repository into your repo before it’s pushed to master](#how-to-get-a-change-from-someone%E2%80%99s-repository-into-your-repo-before-it%E2%80%99s-pushed-to-master) - - [How to get rid of garbage files that shouldn’t be in git](#how-to-get-rid-of-garbage-files-that-shouldn%E2%80%99t-be-in-git) - - - # Contributing to wevote/WebApp Thank you for your interest in the We Vote WebApp project. Please let us know if we can help you get started. @@ -20,152 +5,3 @@ Thank you for your interest in the We Vote WebApp project. Please let us know if To get started, [sign the Contributor License Agreement](https://www.clahub.com/agreements/wevote/WebApp) -This README outlines the proper ways to contribute to the project. -(Feb 13, 2016 Update: This process is still a work-in-progress.) - -## Pull Requests - -**Things to never ever do (or at least try to avoid)** - -Especially if you have commit access to an Angular repository - -1.don't make changes to master, always start a new branch. -2.don’t merge. It messes up the commit history. -3.don’t pull upstream without -a rebase (see above). git fetch and then rebase - instead (or equivalently, `git pull upstream master --rebase`). -4.don’t use `git commit -a`. You could silently commit something regrettable. Use -p instead. - -### Setting up your repository for work - -1.Install and configure git on your local machine. -2.Create a fork of wevote/WebApp.git -3.Clone your fork -`git clone https://github.com/username/WebApp.git` -4.In your local repository, set up a remote for upstream: -`$ git remote add upstream git@github.com:wevote/WebApp.git` -5.Create ssh keys: `ssh-keygen -t rsa -C "youremail@somedomain.com"` -6.`ssh-add ~/.ssh/id_rsa` -7.`pbcopy < ~/.ssh/id_rsa.pub` -8.Go paste your keys into Github, under SSH Keys for your account. -9.Set up a git client where origin is a fork of the repository (e.g. - pertrai1/WebApp), and upstream is the real deal (e.g. wevote/WebApp) -10.Before creating a branch to work in, first make sure you’re on your local - master branch `git checkout master` -11.Next, make sure that master is in sync with the upstream source of truth: - `git fetch upstream` and then `git rebase upstream/master` Or, if you prefer - `git pull upstream master --rebase` - -Note: if there’s a conflicting commit in the history of your master branch, you -can destroy your branch and replace it with a fresh copy using the command `git -checkout -B master upstream/master`. - -12. Now create a new branch `git checkout -b doc-script-changes` -13. On the new branch, make edits to the files. - -Note: Time passes, stuff changes in the upstream repo.... -Commit your changes with `git commit -p`, or git commit and individually add -files with `git add` - -To sync your changes with what's upstream, `git fetch`. - -To make sure your commit goes in at the top of everything else on the upstream -repo, rebase: `git rebase upstream/master` - -If there are conflicts, open the file and look for the diff markers, resolve, and continue. - -Send your changes to your forked copy of the repo in the appropriate branch: -`git push -f origin doc-script-changes`. - -In the web client, go to your fork of the repo, and initiate a pull request by pushing the Pull Request button. Submit the pull request! - -While the pull request is out for consideration: -Any new changes unrelated to this one should be on a brand new branch (`git -checkout -b some-new-thing`). Don't forget to check out the master branch first, otherwise you'll branch off of the current PR branch - -If you want to make changes to your earlier commit in response to comments on -the pull request, you change back to the branch that you submitted it from (`git -checkout doc-script-changes`), make any changes, then commit and push them (steps 6-9). These get automatically added to your pull request since they're in the same branch. - -If your changes are small fixes, they should not be a new commit. Instead, use -git add and then `git commit --amend` to fix up your original commit. - -If you decide to abandon a pull request, you can CLOSE the issue it created and ignore it. - -If the pull request is good, there's nothing else for you to do, besides wait for someone to accept it. - -Once the pull request is accepted (or closed) you can delete your branch from the client. Or, you can wait until you have collected a bunch of them and delete all of the obsolete ones in one go. - -Also keep in mind that `git branch -D my-branch` deletes branches only locally, to delete them from the remote repo you have to do `git push origin :my-branch` - -Moving a change between branches -Sometimes you make a change on the wrong branch. You can move it to the right branch with git stash. From the branch where you made the changes: -`git stash` -`git checkout branch-you-want-it-on` -`git stash pop` - - -### Some useful tips and tricks - -Modify your github client to pull in all the PRs and work on them -You can set up your github client to make it easy to work with submitted pull requests. - -Edit your .git/config and add the two fetch lines shown below under remote “upstream”: -[remote "upstream"] - url = git@github.com:wevote/WebApp.git - fetch = +refs/heads/*:refs/remotes/upstream/* - fetch = +refs/pull/*/head:refs/remotes/upstream/pr/* - -Now, when you fetch upstream, you’ll get references to a bunch of PRs. -check one out with `git checkout upstream/pr/3328` for example -from detached head mode, create a branch with `git branch BRANCHNAME` -fetch upstream, rebase against master, test things out. -push to your branch to verify that CI tests are green for these changes. - -when everything is green and looks legit: -`git push upstream BRANCHNAME:master` -Pull in a specific PR for testing - -Drop commits from a PR -In the branch where you created the commits you want to drop: -`$ git rebase -i upstream/master` - -This opens an editor. Delete the lines you don’t want. Then: - $ git push -f origin branchname -Pretty colors and branch name in your command line prompt -Add something like this to your .bashrc -source ~/.bash_colors - -### Git completion -`source $PATH_TO_GIT_CORE/git-completion.bash` -`source $PATH_TO_GIT_CORE/git-prompt.sh` - -### Prompt -`export -PS1="\[$Green\]\t\[$Red\]:\[$Yellow\]\W\[\033[m\]\[$Blue\]\$(__git_ps1)\[$White\]\$ -"` -And create a file .bash_colors. - -#### How to get a change from someone’s repository into your repo before it’s pushed to master -Define a remote for their github repo, e.g. -`git remote add pertrai1 https://github.com/pertrai1/WebApp.git` - -Now fetch their changes and rebase on top of the branch they have that change in: -`git fetch pertrai1` -`git rebase pertrai1/pertrai1_branchname` - -If there are collisions, these files are removed from your git commit. Hunt them down by searching for seven > characters ‘>>>>>>>’ in your project. Resolve any conflicts in your editor. git status will also show the affected files (in red, since they’re not part of a commit) - -Add the files back into tracking with `git add .` - -Carry on with the rebase: `git rebase --continue` - -#### How to get rid of garbage files that shouldn’t be in git - -Sometimes my Mac makes .DS_Store files in my git directories and I want to get rid of them: - -`$ git status` - check that you don’t have anything important that should be added first! -`$ git clean . -f` - -Caution -- if you have any new files that aren’t under git control, this will remove all of them. - diff --git a/Gulpfile.js b/Gulpfile.js index ef839bfc7..572d9b436 100644 --- a/Gulpfile.js +++ b/Gulpfile.js @@ -1,34 +1,106 @@ +/* eslint-disable */ // dependencies const gulp = require("gulp"); const sass = require("gulp-sass"); +const autoprefixer = require("gulp-autoprefixer"); +const uglify = require("gulp-uglify"); +const sourcemaps = require("gulp-sourcemaps"); +const watchify = require("watchify"); const browserSync = require("browser-sync").create(); const browserify = require("browserify"); const babelify = require("babelify"); const source = require("vinyl-source-stream"); +const buffer = require("vinyl-buffer"); +const notifier = require('node-notifier'); +// const cssmin = require("gulp-cssnano"); +// Dec 2019: "gulp-cssnano" has not been updated for two years and is lightly used compared to gulp-clean-css -- not worth migrating since +// gulp is not used for production bundles, so it doesn't matter if css is minified. const del = require("del"); const server = require("./server"); const PRODUCTION = process.env.NODE_ENV === "production"; gulp.task("browserify", function () { - return browserify({ + const ops = { + debug: !PRODUCTION, entries: "js/index.js", + cache: {}, + packageCache: {}, extensions: [".js", ".jsx"], basedir: "./src", - transform: [babelify] - }) - .bundle() - .on("error", function (err) { - console.error(err.toString()); + transform: [babelify], + }; + // const ops = { + // debug: !PRODUCTION, + // entries: "js/startReactReadyApp.js", + // cache: {}, + // packageCache: {}, + // extensions: [".js", ".jsx"], + // basedir: "./src", + // transform: [babelify], + // }; + + // 2017-04-05 Watchify is causing too many problems, so we are turning it off until we can resolve Issue 757 + // var opsWatchify = assign({ cache: {}, packageCache: {} }, watchify.args, ops); + // var browserifyWithWatchify = watchify(browserify(opsWatchify)); + + function err (e) { + console.error(e.stack); + notifier.notify({ title: "Compile Error", message: e.stack }); this.emit("end"); - }) - .pipe(source("bundle.js")) - .pipe(gulp.dest("./build/js")) - .pipe(browserSync.stream()); + } + + const bundler = browserify(ops); + + const bundle = function () { + return PRODUCTION ? + + // production build with minification + bundler + .transform('uglifyify', { global: true }) + .bundle() + .on("error", err) + .pipe(source("bundle.js")) + .pipe(buffer()) + .pipe(uglify({ preserveComments: false, mangle: false })) + .pipe(gulp.dest("./build/js")) : + + // development build + bundler + .plugin('watchify', { + verbose: true, + }) + .bundle() + .on("error", err) + .on("update", bundle) + .pipe(source("bundle.js")) + .pipe(gulp.dest("./build/js")) + .pipe(browserSync.stream()); + + // Compressed development build - didn't work + // bundler + // .plugin('watchify', { + // verbose: true, + // }) + // .transform('uglifyify', { global: true }) + // .bundle() + // .on("error", err) + // .on("update", bundle) + // .pipe(source("bundle.js")) + // .pipe(buffer()) + // .pipe(uglify({ preserveComments: false, mangle: false })) + // .pipe(gulp.dest("./build/js")) + // .pipe(browserSync.stream()); + + + } + return bundle(); }); +// Run server gulp.task("server", PRODUCTION ? () => server(PRODUCTION) : function () { server(); + // only start browserSync when this is development browserSync.init({ proxy: "localhost:3003", @@ -36,54 +108,152 @@ gulp.task("server", PRODUCTION ? () => server(PRODUCTION) : function () { ghostMode: { clicks: true, forms: true, - scroll: true + scroll: true, }, - logPrefix: `${new Date().toString().split(" ")[4]} - We Vote USA` + logPrefix: `${new Date().toString().split(" ")[4]} - We Vote USA`, }); }); +// October 2018: bootstrap-sccs is styling for bootstrap 3, so we no longer use it. +// Pull all bootstrap styling from the node_modules package bootstrap (v4) +gulp.task("compile-bootstrap", function () { + return gulp.src("./node_modules/bootstrap/scss/bootstrap.scss") + .pipe(sourcemaps.init()) + .on("error", function (err) { console.error(err); }) + .pipe(sass({ style: "expanded", + includePaths: [ + "./node_modules/bootstrap/sccs", + "./node_modules/bootstrap/sccs/utilities", + "./node_modules/bootstrap/sccs/mixins", + ], })) + .pipe(autoprefixer("last 2 version")) + // .pipe(cssmin()) + .pipe(sourcemaps.write(".")) // --> working directory is /build/css + .pipe(gulp.dest("./build/css")) + .pipe(browserSync.stream()); +}); + +// Compile main and loading-screen, then copy them to the /build/css directory +// 2020-06 Deprecated by Dale: "./src/sass/loading-screen.scss", gulp.task("sass", function () { - return gulp.src("./src/sass/main.scss") - .on("error", function (err) { console.error(err); }) - .pipe(sass()) - .pipe(gulp.dest("./build/css")) - .pipe(browserSync.stream()); + return gulp.src(["./src/sass/main.scss", + ]) + .pipe(sourcemaps.init()) + .on("error", function (err) { console.error(err); }) + .pipe(sass({ style: "expanded" })) + .pipe(autoprefixer("last 2 version")) + // .pipe(cssmin()) + .pipe(sourcemaps.write(".")) // --> working directory is /build/css + .pipe(gulp.dest("./build/css")) + .pipe(browserSync.stream()); +}); + +gulp.task("lint-css", function () { + const gulpStylelint = require("gulp-stylelint"); + return gulp + .src("./src/sass/**/*.scss") + .pipe(gulpStylelint({ + failAfterError: false, + reporters: [ + { formatter: "string", console: true }, + ], + })); }); -gulp.task("clean:build", function () { - return del.sync(["./build/**"]); +// Clean out Build directory +gulp.task("clean:build", function (done) { + return del(["./build/**"], done); }); -gulp.task("copy-fonts", function () { +// Copy font files to Build directory +gulp.task("copy-fonts", function (done) { gulp.src("./src/sass/base/fonts/**") .pipe(gulp.dest("./build/fonts")) .pipe(browserSync.stream()); + done(); }); -gulp.task("copy-index", function () { - gulp.src("./src/index.html") +// Copy Index page to Build directory +gulp.task("copy-index", function (done) { + gulp.src("./src/*.html") .pipe(gulp.dest("./build")) .pipe(browserSync.stream()); + done(); }); +// Copy CSS files to Build directory gulp.task("copy-css", function () { - return gulp.src("./src/css/**/*.css") + return gulp.src("./src/css/**/*.scss") .pipe(gulp.dest("./build/css")) .pipe(browserSync.stream()); }); -gulp.task("build", ["copy-fonts", "copy-index", "copy-css", "browserify", "sass"]); +// Copy image files to Build directory +gulp.task("copy-img", function () { + return gulp.src("./src/img/**/*") + .pipe(gulp.dest("./build/img")) + .pipe(browserSync.stream()); +}); + +// Copy javascript files to Build directory +gulp.task("copy-javascript", function () { + return gulp.src("./src/javascript/*") + .pipe(gulp.dest("./build/javascript")) + .pipe(browserSync.stream()); +}); + +// Build tasks +gulp.task("build", gulp.series("copy-fonts", "copy-index", "compile-bootstrap", "copy-css", "copy-img", "copy-javascript", "browserify", "sass")); + +// Watch tasks +gulp.task("watch", PRODUCTION ? ()=> {} : function (done) { + gulp.watch("./src/index.html", gulp.parallel("copy-index")).on("change", function (path) { + console.log("Watcher: " + path + " was changed."); + done(); + }); + + gulp.watch("./src/sass/base/base/fonts/**", gulp.parallel("copy-fonts")).on("change", function (path) { + console.log("Watcher: " + path + " was changed."); + done(); + }); + + gulp.watch("./src/sass/bootstrap/**", gulp.parallel("compile-bootstrap")).on("change", function (path) { + console.log("Watcher: " + path + " was changed."); + done(); + }); + + gulp.watch("./src/css/**/*.scss", gulp.parallel("copy-css", "lint-css")).on("change", function (path) { + console.log("Watcher: " + path + " was changed."); + done(); + }); + + gulp.watch("./src/img/**/*", gulp.parallel("copy-img")).on("change", function (path) { + console.log("Watcher: " + path + " was changed."); + done(); + }); + + gulp.watch("./src/sass/**/*.scss", gulp.parallel("sass")).on("change", function (path) { + console.log("Watcher: " + path + " was changed."); + done(); + }); + + gulp.watch("./src/javascript/*.js", gulp.parallel("copy-javascript")).on("change", function (path) { + console.log("Watcher: " + path + " was changed."); + done(); + }); + + gulp.watch("./src/js/**/*.js?(x)", gulp.parallel("browserify")).on("change", function (path) { + console.log("Watcher: " + path + " was changed."); + done(); + }); -gulp.task("watch", ["build"], PRODUCTION ? ()=>{} : function () { - gulp.watch(["./src/index.html"], ["copy-index"]); - gulp.watch(["./src/sass/base/base/fonts/**"], ["copy-fonts"]); - gulp.watch(["./src/css/**/*.css"], ["copy-css"]); - gulp.watch(["./src/sass/**/*.scss"], ["sass"]); - gulp.watch(["./src/js/**/*.js?(x)"], ["browserify"]); + done(); }); -gulp.task("default", [ +// Default +gulp.task("default", gulp.series( "clean:build", + "build", "watch", "server" -]); +)); diff --git a/README.md b/README.md index 2096005c7..4fbd22eb7 100644 --- a/README.md +++ b/README.md @@ -1,57 +1,53 @@ - - -## Contents +# We Vote WebApp -- [We Vote WebApp - README Home](#we-vote-webapp---readme-home) -- [Installing WebApp](#installing-webapp) -- [Working with WebApp](#working-with-webapp) -- [Contributing to the Project](#contributing-to-the-project) -- [How to Submit Code / Pull Requests](#how-to-submit-code--pull-requests) - - - -# We Vote WebApp - README Home - -[![Build Status](https://travis-ci.org/wevote/WebApp.svg?branch=develop)](https://travis-ci.org/wevote/WebApp) [![Coverage Status](https://coveralls.io/repos/github/wevote/WebApp/badge.svg?branch=master)](https://coveralls.io/github/wevote/WebApp?branch=master) [![Build status](https://ci.appveyor.com/api/projects/status/g7dvmiax01d9ydjr?svg=true)](https://ci.appveyor.com/project/pertrai1/webapp) - -![We Vote USA](unclesamewevote.jpg) +[![Build Status](https://travis-ci.org/wevote/WebApp.svg?branch=develop)](https://travis-ci.org/wevote/WebApp) +[![Sauce Test Status](https://saucelabs.com/buildstatus/WeVote)](https://saucelabs.com/u/WeVote) This WebApp repository contains a Node/React/Flux Javascript application. Using data from Google Civic API, Vote Smart, MapLight, TheUnitedStates.io and the Voting Information Project, we give voters a social way to interact with ballot data. -Interested in volunteering? [Starting presentation here](https://prezi.com/5v4drd74pt6n/we-vote-introduction-strategic-landscape/). Please also [read about our values](https://wevote.hackpad.com/Community-Rules-C0sn7DhZhDt) and [see our Code of Conduct](CODE_OF_CONDUCT.md) +Interested in [volunteering or applying for an internship](https://www.idealist.org/en/nonprofit/f917ce3db61a46cb8ad2b0d4e335f0af-we-vote-oakland#opportunities)? [Starting presentation here](https://prezi.com/5v4drd74pt6n/we-vote-introduction-strategic-landscape/). +Please also [read about our values](https://docs.google.com/document/d/12qBXevI3mVKUsGmXL8mrDMPnWJ1SYw9zX9LGW5cozgg/edit) and +[see our Code of Conduct](CODE_OF_CONDUCT.md) +To join us, please [review our openings here](https://www.idealist.org/en/nonprofit/f917ce3db61a46cb8ad2b0d4e335f0af-we-vote-oakland#opportunities), and express your interest by emailing JoinUs@WeVote.US -You can see our current wireframe mockup for a San Francisco ballot here: -http://start.wevoteusa.org/ +Our current Beta version is here [https://WeVote.US](https://WeVote.US) and we are working on version 2 now! -And finally, our current live version is here: https://wevote.me +## Installing WebApp +Our installation process is built to allow engineers all over America to contribute to We Vote. +It may seem complicated, but it allows anyone to be in a position to make suggestions, and get involved. -# Installing WebApp -1. [Overview](docs/installing/README_INSTALLING.md) +Manual Installation +1. [Preparing the Environment on Your Machine](docs/installing/ENVIRONMENT.md) -2. [Preparing the Environment on Your Machine](docs/installing/ENVIRONMENT.md) +2. [Bringing Code to Your Machine](docs/installing/CLONING_CODE.md) -3. [Bringing Code to Your Machine](docs/installing/CLONING_CODE.md) +3. [Running WebApp for the First Time](docs/installing/RUNNING_FIRST_TIME.md) -4. [Running WebApp for the First Time](docs/installing/RUNNING_FIRST_TIME.md) +Automated Installation +1. [Run automated scripts](docs/installing/AUTOMATED_INSTALLATION.md) -# Working with WebApp +## Working with WebApp 1. [Working with WebApp Day-to-Day](docs/working/README_WORKING_WITH_WEB_APP.md) 2. [Debugging Tools and Tips](docs/working/DEBUGGING_TOOLS.md) 3. [Issues and Reporting Bugs](docs/working/ISSUES.md) -# Contributing to the Project -Please read the following before you start contributing to the project. Thank you! +4. [Styling Guidelines](docs/working/STYLING.md) + +5. [Want to sign in with Facebook or Twitter on localhost?](docs/working/SECURE_CERTIFICATE.md) -1. [Overview](docs/contributing/index.md) +Thanks to BrowserStack for helping us with automated testing! -2. [Our Culture and Philosophy](docs/contributing/CONTRIBUTING_PHILOSOPHY.md) + -3. [Coding Standards and Best Practices](docs/contributing/CONTRIBUTING_STANDARDS.md) +## Contributing to the Project +Please read the following before you start contributing to the project. Thank you! + +[Coding Standards and Best Practices](docs/contributing/CONTRIBUTING_STANDARDS.md) -# How to Submit Code / Pull Requests +## How to Submit Code / Pull Requests 1. [What the Heck is a Pull Request?](docs/contributing/PULL_REQUEST_BACKGROUND.md) 2. [Before Your First Pull Request](docs/contributing/PULL_REQUEST_SETUP.md) @@ -64,4 +60,12 @@ Please read the following before you start contributing to the project. Thank yo 6. [Approving Pull Requests](docs/contributing/APPROVING_PULL_REQUESTS.md) +7. [Progressive Web App Feature](docs/working/PROGRESSIVE_WEB_APP.MD) + +## Testing WebApp + +1. [Introduction to WebApp testing](docs/testing/README_TESTING.md) + +2. [Explanation of various files](docs/testing/EXPLAIN_FILES.md) + Welcome aboard!! diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 209178257..000000000 --- a/appveyor.yml +++ /dev/null @@ -1,43 +0,0 @@ -# Thanks for Grunt for template of this file! - -# http://www.appveyor.com/docs/appveyor-yml - -# Fix line endings in Windows. (runs before repo cloning) -init: - - git config --global core.autocrlf input - -# Test against these versions of Node.js. -environment: - matrix: - - nodejs_version: "0.10" - - nodejs_version: "0.12" - - nodejs_version: "5" - - nodejs_version: "4" - -# Allow failing jobs for bleeding-edge Node.js versions. -matrix: - allow_failures: - - nodejs_version: "0.10" - - nodejs_version: "5" - -# Install scripts. (runs after repo cloning) -install: - # Get the latest stable version of Node 0.STABLE.latest - - ps: Install-Product node $env:nodejs_version - # Output useful info for debugging. - - node --version - - npm --version - - git --version - - svn --version - # Install all dependencies - - npm install - -# Post-install test scripts. -test_script: - - cmd: npm test - -# Don't actually build. -build: off - -# Set build version format here instead of in the admin panel. -version: "{build}" \ No newline at end of file diff --git a/buildDateFile.js b/buildDateFile.js new file mode 100644 index 000000000..a41d99218 --- /dev/null +++ b/buildDateFile.js @@ -0,0 +1,6 @@ +const fs = require('fs'); + +// Creates the compileDate.js file that contains the compile date as a string, so it can be displayed where wanted in the app. +const d = new Date(); +const fileContent = `module.exports = ['${d.toLocaleString()}'];\n`; +fs.writeFileSync('./src/js/compileDate.js', fileContent, { encoding: 'utf8', flag: 'w' }); diff --git a/docs/contributing/APPROVING_PULL_REQUESTS.md b/docs/contributing/APPROVING_PULL_REQUESTS.md index a6dab1ab2..4b966ea6c 100644 --- a/docs/contributing/APPROVING_PULL_REQUESTS.md +++ b/docs/contributing/APPROVING_PULL_REQUESTS.md @@ -21,16 +21,23 @@ Add the contributor's repository as a remote repo. Here are the core contributor git remote add pertrai1 https://github.com/pertrai1/WebApp git fetch pertrai1 - git remote add pertrai1 https://github.com/nf071590/WebApp + git remote add nf071590 https://github.com/nf071590/WebApp git fetch nf071590 - git remote add pertrai1 https://github.com/lisamcho/WebApp + git remote add lisamcho https://github.com/lisamcho/WebApp git fetch lisamcho - git remote add pertrai1 https://github.com/dalemcgrew/WebApp + git remote add dalemcgrew https://github.com/dalemcgrew/WebApp git fetch dalemcgrew Find the branch that the person is submitting: git branch -a -TODO: Figure out how to pull a remote branch to your local + +If you are wanting to test a PR on your local machine, make sure you have already done a `fetch` on the +users repo. If you want to see if you have done so, you can do so by `git branch pertrai1 -a`. This will +show you all of the branches for that user (from the example above for pertrai1). Now you can find the +branch that is being worked on from the user and create a local branch based on that remote branch. Now +you can do a `git pull ` and it will put you into a local version of that +users branch. Running `gulp` at this point will have you seeing what has been done for this PR and allow +you to look at the code. --- diff --git a/docs/contributing/CONTRIBUTING_STANDARDS.md b/docs/contributing/CONTRIBUTING_STANDARDS.md index c5af451f3..808c673d5 100644 --- a/docs/contributing/CONTRIBUTING_STANDARDS.md +++ b/docs/contributing/CONTRIBUTING_STANDARDS.md @@ -21,21 +21,115 @@ Please use descriptive full word variable names. current features as painless as possible. We will have many engineers working with this code, and we want to be welcoming to engineers who are new to the project. * Short variable names can often create confusion, where a new engineer needs to spend time -figuring out what a short variable name actually means. (Ex/ “per” or “p” instead of “person”.) +figuring out what a short variable name actually means. (Ex/ Please use “person” instead of “per” or “p”.) For this project please use descriptive full word variable names. * Fellow engineers should be able to zoom around the code and not get stopped with riddles created by short names. +## Variable Naming Standards -## If there's no issue, please create one +* Please use full words instead of abbreviations (Ex/ Please use “Person” instead of “per” or “p”.) +* Please use lower case camel case ("camelCaseLettering") +* Please be aware that many variables are coming from our API server lower case separated by an underscore. +(e.g., "google_civic_election_id"), -## Let us Know you're working on the issue +## Art Asset Naming + +1. Asset names should be all lower case (ex/ "lower.png") + +2. If a name has multiple words, use "-" between words in the name (ex/ "lower-case.png") + +3. If sequential, put order word + number at start of name (ex/ "slide1-") + +4. For fixed size images, include width and height at the end of the name (ex/ "-200x150"). +Does not apply to svg files. + +5. If an image is still a "first draft", lets add "-draft" to the name + version number (ex/ +"slide3-connect-list-with-photos-draft1-300x370.png") + +6. If adding images that are meant to be used in one limited area of the App, +please put them in their own folder within "src/img/global". + +Examples of good names: + + slide1-ballot-simple.svg + + slide3-connect-list-with-photos-draft1-300x370.png + +Examples of names we avoid: + + Image1.svg + + +# Code Style and eslint + +There is no perfect code style. If you have worked at a few different companies or on a few different open source projects, you +will have used a few different styles. -If you're actively working on an issue, please comment in the issue thread stating that you're working on a fix, or (if you're an official contributor) assign it to yourself. +As of December 2018, the We Vote WebApp src directory contained 1073 files, that have been created and edited by dozens of volunteer +software engineers. And we had dozens of different code styles in different files. + +Inconsistently formatted code is less reliable code, so we decided to (mostly) put aside the +issue of "what is the best code style", and to adopt the (very widely adopted) styles +that Airbnb came up with for React and JavaScript. Airbnb's styles have been source controlled +as an NPM project containing eslint configuration rules for Javascript, and a few related projects for React, and those +rules have been incorporated in our WebApp. + +If you use a IDE like [WebStorm](https://www.jetbrains.com/webstorm/), (sadly WebStorm is not free), all the lint [warnings will show up in amber, and lint +errors will show up in red.](https://www.themarketingtechnologist.co/eslint-with-airbnb-javascript-style-guide-in-webstorm/) Errors will block a git commit and must be fixed. If there is just +no reasonable way to fix the lint error, or fixing the code is too risky for some reason, it is ok +to suppress the error on the line where it occurs by adding a comment like this... + +```const element = findDOMNode( // eslint-disable-line react/no-find-dom-node``` + +If your editor does not show lint warnings, running `npm test` from the command line will provide a list of the lint output. +The list might be quite long, so you may want to redirect it into a file that you can edit `npm test > testOutput.txt` + +# Code Craftsmanship + +We value Software Craftsmanship for the We Vote project since it makes working on the We Vote code base … +* Fun since the product is composed of clean self documenting components that + * Do not repeat themselves -- each functional block has a component, or collection of components that can be widely used, or is completely unique. + * Are easy to rearrange on panels and pages, with less (or no) modifying of the components themselves for new uses or new UI approaches.. +* Satisfying since changes have fewer unintentioned effects +* Easier to work on since, the architectural layout is clear, with properly named components and variables, and just enough comments to explain the area that are hard to understand. +* Reliable, since well defined components are partially self tested by being used in many different ways for many purposes. + + + +# Code Quality Guidelines: +* Do no harm: Stuff happens, but if you are not sure, ask before you commit. +* Software Craftsmenship: Leave every file they touch in better shape than they found it. +* In React JSX files: commit files that (as much as possible) comply with the “airbnb” preset for jslint (with our few modifications). If you understand the warnings in the files you work on, make small changes to clear the warnings in the areas you are modifying. +* In /WebApp/src/sass files: [BEM Naming Conventions](../working/STYLING.md) should be followed -- BEM Naming makes it straightforward to organize where to group styles together in an object-oriented fashion. (Ex/ We can use a style named `ballot__header__title` and find where it is defined.) +* In Python: Clear all PEP8 warnings before committing. +* Do not copy classes and make small changes, instead examine existing classes and components to see how they modified for the new use case and then reused. If you absolutely have to copy, be sure to remove, or temporarily comment out, code that is not immediately in use. +* Commented out code should be rare and should include the date of commenting, and why it remains in place. +* After you have some experience with this project, carefully perform incremental refactoring to break apart monolithic components into smaller reusable components. +* After you have some experience, carefully perform incremental refactoring to rename classes, components and variables to match their current use. +* Don't commit refactoring changes that you are unable to test -- if you can't test it, or haven't tested it, don't check it in. +* Delete code that you made redundant. Cautiously comment out, or delete code that has not been in use for months. If unsure, add a comment with a proposed deletion date, a few months in the future. +* If while searching globally for a phrase, you find multiple matches, this might be a indication that a refactor is needed. +* In code reviews: + * Do not allow new slightly duplicated code into our codebase (unless there is a good reason for it). + * If variable or class names are will be meaningless to others, flag them. + * Overly complex code should be commented or simplified. + * Flag code style changes or variable conventions, that are unlike the rest of our code. +* Be on the lookout for abandoned, or duplicative styles. Clean them out when you find them. Before creating new styles, first look for styles that make sense to reuse. +* Simple is better than clever code, but if "clever" really adds value, add a note to help out the next engineer who will be looking at the code. +* Most React classes should be less than 300 lines long, at 500 lines consider breaking the class up into sub-components. If the the component’s render method contains multiple static blocks of React components and markup, consider moving them into a new React component. +* Ask before including new open source NPM projects. Our strong preference is to include projects that NPM rates as high in popularity, quality and maintenance. Avoid projects that haven’t been updated in months or years, as they can pose security risks. +* If you need to fix the open source projects we rely on, please make a pull request against that project. + + +# If there's no issue in github.com, please create one + +## Let us Know you're working on the issue -This way, others will know they shouldn't try to work on a fix at the same time. +If you're actively working on an issue, please comment in the issue thread stating that you're working on a fix, or (if you're an official contributor) assign it to yourself. You can also keep the team updated on the work you are doing by attending a team stand-up held on the Daily Hangouts, or post a note in the #agile-stand-up Slack channel. This way, others will know they shouldn't try to work on a fix at the same time. +# Version numbering We use [SemVer](http://semver.org/) for version numbers. More info: [Versions: Release Names vs Version Numbers](versions/index.md) diff --git a/docs/contributing/CREATING_PULL_REQUEST.md b/docs/contributing/CREATING_PULL_REQUEST.md index 4effec351..b13f248dc 100644 --- a/docs/contributing/CREATING_PULL_REQUEST.md +++ b/docs/contributing/CREATING_PULL_REQUEST.md @@ -23,8 +23,11 @@ # Creating a Pull Request If you have never created a pull request, please read “[What the Heck is a Pull Request?](PULL_REQUEST_BACKGROUND.md) +We also created [this handy chart](https://docs.google.com/drawings/d/1ED4X3Gpy_UruGDSiO8FjjxQeGOmQqIApguodHDo6-ok/edit) +to show the various steps working with pull requests. ## Get Your Command Line Ready + `cd /Users/DaleMcGrew/NodeEnvironments/WebAppEnv/` # Activate your node environment `. bin/activate` @@ -32,6 +35,7 @@ If you have never created a pull request, please read “[What the Heck is a Pul `cd /Users/DaleMcGrew/PythonProjects/PersonalGitForks/WebApp` # Change to directory where you checked out your Personal Fork from github ## Preparing to Create a Branch + `git branch -a` # See what branch you are currently set to `git checkout develop` # If you aren’t set to the develop branch, switch to that @@ -45,7 +49,8 @@ You can see changes here: https://github.com/DaleMcGrew/WebApp ## How to Create a Branch in Your Personal Fork We want to set up a branch on your local computer. -`git checkout -b dale_work_feb28` # The “-b” creates the new branch +`git checkout -b ` # The “-b” creates the new branch. +Replace "" with your branch name. In PyCharm: Right click WebApp > Git > Repository > Branches > New Branch @@ -61,10 +66,24 @@ In PyCharm: Right click WebApp > Git > Repository > Branches > New Branch `git push origin develop` # Push this latest version of develop up to your Personal Fork on the github servers -`git checkout dale_work_feb28` +`git checkout ` # Replace "" with your branch name +Now you need to merge locally the latest code from "develop" with your branch name. Dale does this merging with +the PyCharm IDE. How you do this depends on the development environment you use. TODO: Add instructions for merging with develop via command line +### Merging via the command line + +To merge your local develop branch with : + +`$ git checkout develop` # Switch to the develop branch + +`$ git merge ` # Merge your branch with develop + +You can than delete you branch if you are done making changes + +`$ git branch -d ` + ## Test Before Creating Pull Request We use a tool that verifies our Javascript meets the eslint spec. @@ -74,44 +93,38 @@ You may get warnings or errors. Please minimally fix the errors, and try to fix ## Commit: -* Make sure you comply with the [.editorconfig](http://editorconfig.org/) +* Make sure you comply with the [.editorconfig](http://editorconfig.org/) +Reference the issue number in your commit message e.g.: ``` git commit -m '[Issue #] ' ``` -### Commit Hints - -Reference the issue number in your commit message e.g.: +or ``` -$ git commit -m '[#5] Make sure to follow the PR process for contributions' +git commit -a ``` -### For large changes spanning many commits / Pull Requests +or -* Create a meta-issue with a bullet list using the `* [ ] item` markdown syntax. -* Create issues for each bullet point -* Link to the meta-issue from each bullet point issue -* Check off the bullet list as items get completed - -Linking from the bullet point issues to the meta issue will create a list of issues with status indicators in the issue comments stream, which will give us a quick visual reference to see what's done and what still needs doing. +`git commit -p` # Commit all of your changes +or -### PR Merge Exception +`git commit FILENAME` and then individually add files with `git add` -* Minor documentation grammar/spelling fixes (code example changes should be reviewed) +#### Windows machines only -## How to Put Your Changes on your Personal Fork on the github servers -`git branch -a` # Make sure you are looking at the branch you want to push +You need to commit files via the command line so git doesn't throw an error (based on the line endings) -`git pull upstream develop` # Make sure your personal fork on your local machine has the latest code from wevote/WebApp +1. Stage your changes using `git add .` or using your IDE. -`git commit -p` # Commit all of your changes +2. Run `$ git commit -n -m 'Your commit message'` in the terminal -or +## How to Put Your Changes on your Personal Fork on the github servers -`git commit FILENAME` and then individually add files with `git add` +`git branch -a` # Make sure you are looking at the branch you want to push `git push origin ` # Push your changes to your Personal Fork on the github servers @@ -124,10 +137,27 @@ You can go to the github web page for your Personal Fork and make sure it shows * Please don't merge your own changes. Create a pull request so others can review the changes. * **Wait for the reviewer to approve and merge the request** -This guide walks through the process of sending a hypothetical pull request and using the various code review and management tools to take the change to completion. +This guide walks through the process of sending a hypothetical pull request and using the various code review +and management tools to take the change to completion. https://help.github.com/articles/using-pull-requests/ +### For large changes spanning many commits / Pull Requests + +* Create a meta-issue with a bullet list using the `* [ ] item` markdown syntax. +* Create issues for each bullet point +* Link to the meta-issue from each bullet point issue +* Check off the bullet list as items get completed + +Linking from the bullet point issues to the meta issue will create a list of issues with status indicators +in the issue comments stream, which will give us a quick visual reference to see what's done and what still needs doing. + + +### PR Merge Exception + +* Minor documentation grammar/spelling fixes (code example changes should be reviewed) + + ## SemVer We follow [SemVer](http://semver.org/) for our releases. Please read if you plan to tag for any releases. @@ -136,15 +166,15 @@ We follow [SemVer](http://semver.org/) for our releases. Please read if you plan ## I Have Submitted a Pull Request, Now What? Sometimes pull requests can take a day or two to be approved. How do you keep working? TODO discuss this. - -While the pull request is out for consideration: +While your pull request is being considered by the We Vote admins: Any new changes unrelated to this one should be on a brand new branch (`git -checkout -b some-new-thing`). Don't forget to check out the master branch first, otherwise you'll branch off of the current PR branch - +checkout -b `). Don't forget to check out the develop branch first, otherwise you'll +branch off of the current PR branch If you want to make changes to your earlier commit in response to comments on the pull request, you change back to the branch that you submitted it from (`git -checkout doc-script-changes`), make any changes, then commit and push them (steps 6-9). These get automatically added to your pull request since they're in the same branch. +checkout `), make any changes, then commit and push them (steps 6-9). +These get automatically added to your pull request since they're in the same branch. If your changes are small fixes, they should not be a new commit. Instead, use git add and then `git commit --amend` to fix up your original commit. @@ -153,16 +183,33 @@ If you decide to abandon a pull request, you can CLOSE the issue it created and If the pull request is good, there's nothing else for you to do, besides wait for someone to accept it. -Once the pull request is accepted (or closed) you can delete your branch from the client. Or, you can wait until you have collected a bunch of them and delete all of the obsolete ones in one go. - -Also keep in mind that `git branch -D my-branch` deletes branches only locally, to delete them from the remote repo you have to do `git push origin :my-branch` - Moving a change between branches -Sometimes you make a change on the wrong branch. You can move it to the right branch with git stash. From the branch where you made the changes: +Sometimes you make a change on the wrong branch. You can move it to the right branch with git stash. +From the branch where you made the changes: + `git stash` + `git checkout branch-you-want-it-on` + `git stash pop` +## My Pull Request Was Approved + +Once the pull request is accepted (or closed) you can delete your branch from the client. +Or, you can wait until you have collected a bunch of them and delete all of the obsolete ones in one go. +For example with the branch "dale_doc_updates_mar24": + +`git branch -D ` + +Also keep in mind that `git branch -D my-branch` deletes branches only locally, +to delete them from the remote repo you have to do `git push origin :my-branch` + +or + +`git push origin --delete ` + +`git remote prune origin` + --- For more advanced tips about using Pull Requests, see [Pull Request Tips & Tricks](PULL_REQUEST_ADVANCED.md) diff --git a/docs/contributing/PULL_REQUEST_ADVANCED.md b/docs/contributing/PULL_REQUEST_ADVANCED.md index b9a9d2015..5297493f5 100644 --- a/docs/contributing/PULL_REQUEST_ADVANCED.md +++ b/docs/contributing/PULL_REQUEST_ADVANCED.md @@ -101,8 +101,9 @@ Define a remote for their github repo, e.g. `git remote add pertrai1 https://github.com/pertrai1/WebApp.git` Now fetch their changes and rebase on top of the branch they have that change in: -`git fetch pertrai1` -`git rebase pertrai1/pertrai1_branchname` + + git fetch pertrai1 + git rebase pertrai1/pertrai1_branchname If there are collisions, these files are removed from your git commit. Hunt them down by searching for seven > characters ‘>>>>>>>’ in your project. Resolve any conflicts in your editor. git status will also show the affected files (in red, since they’re not part of a commit) diff --git a/docs/contributing/PULL_REQUEST_BACKGROUND.md b/docs/contributing/PULL_REQUEST_BACKGROUND.md index e9fd7b23e..267c90185 100644 --- a/docs/contributing/PULL_REQUEST_BACKGROUND.md +++ b/docs/contributing/PULL_REQUEST_BACKGROUND.md @@ -18,6 +18,9 @@ http://nvie.com/posts/a-successful-git-branching-model/ Collaborating on projects using pull requests: https://help.github.com/categories/collaborating-on-projects-using-pull-requests/ +We also created [this handy chart](https://docs.google.com/drawings/d/1ED4X3Gpy_UruGDSiO8FjjxQeGOmQqIApguodHDo6-ok/edit) +to show the various steps working with pull requests. + ## Code staging areas: 1) wevote/WebApp, master branch (github servers): Used for working releases diff --git a/docs/contributing/PULL_REQUEST_SETUP.md b/docs/contributing/PULL_REQUEST_SETUP.md index 4e7c507c7..248635f87 100644 --- a/docs/contributing/PULL_REQUEST_SETUP.md +++ b/docs/contributing/PULL_REQUEST_SETUP.md @@ -37,11 +37,12 @@ Next, make sure that master is in sync with the upstream source of truth: ## Connect Your Personal Fork To wevote/WebApp -Make sure your are connecting this way: -`vi .git/config` + `cd PersonalGitForks/WebApp` + `vi .git/config` Replace the “url” connection string under [remote “origin”] with: -`url = git@github.com:YOUR_GITHUB_ACCOUNT/WebApp.git` + + `url = git@github.com:YOUR_GITHUB_ACCOUNT/WebApp.git` Make sure your `[remote “upstream”]` lines look like this: @@ -50,7 +51,8 @@ Make sure your `[remote “upstream”]` lines look like this: fetch = +refs/heads/*:refs/remotes/upstream/* Run this command to confirm your setup: -`git remote -v` + + `git remote -v` You should see: diff --git a/docs/contributing/index.md b/docs/contributing/index.md index aca4d8fa7..3d7e2761f 100644 --- a/docs/contributing/index.md +++ b/docs/contributing/index.md @@ -3,11 +3,18 @@ ## Contents -- [](#) - +Start here: +1. [Our Culture and Philosophy](CONTRIBUTING_PHILOSOPHY.md) +1. [Coding Standards](CONTRIBUTING_STANDARDS.md) +1. `[Optional]` [What the Heck is a Pull Request?](PULL_REQUEST_BACKGROUND.md) +1. [Before Your First Pull Request](PULL_REQUEST_SETUP.md) +1. [Creating a Pull Request](CREATING_PULL_REQUEST.md) +1. [Pull Request Tips & Tricks](PULL_REQUEST_ADVANCED.md) +1. [Troubleshooting Pull Request Problems](PULL_REQUEST_TROUBLESHOOTING.md) +1. [Approving Pull Requests](APPROVING_PULL_REQUESTS.md) diff --git a/docs/images/ChromeDebuggerApplicationTab.png b/docs/images/ChromeDebuggerApplicationTab.png new file mode 100644 index 000000000..4f7f5a67b Binary files /dev/null and b/docs/images/ChromeDebuggerApplicationTab.png differ diff --git a/docs/images/ChromeDebuggerNetworkOnline.png b/docs/images/ChromeDebuggerNetworkOnline.png new file mode 100644 index 000000000..2572a876b Binary files /dev/null and b/docs/images/ChromeDebuggerNetworkOnline.png differ diff --git a/docs/images/ReactDevTools.png b/docs/images/ReactDevTools.png new file mode 100644 index 000000000..046aadadd Binary files /dev/null and b/docs/images/ReactDevTools.png differ diff --git a/docs/images/SafariServiceWorkerDebug.png b/docs/images/SafariServiceWorkerDebug.png new file mode 100644 index 000000000..795cf3c31 Binary files /dev/null and b/docs/images/SafariServiceWorkerDebug.png differ diff --git a/docs/images/WebpackBundleAnalyzer.png b/docs/images/WebpackBundleAnalyzer.png new file mode 100644 index 000000000..c036ac083 Binary files /dev/null and b/docs/images/WebpackBundleAnalyzer.png differ diff --git a/docs/images/WorkboxConsoleOutput.png b/docs/images/WorkboxConsoleOutput.png new file mode 100644 index 000000000..749a7bd30 Binary files /dev/null and b/docs/images/WorkboxConsoleOutput.png differ diff --git a/docs/installing/AUTOMATED_INSTALLATION.md b/docs/installing/AUTOMATED_INSTALLATION.md new file mode 100644 index 000000000..f687cc701 --- /dev/null +++ b/docs/installing/AUTOMATED_INSTALLATION.md @@ -0,0 +1,18 @@ +# Automated Installation +[Go back to Readme Home](../../README.md) + +## For Mac Users + +Copy the contents of [installmac.sh](https://github.com/wevote/WebApp/blob/develop/installmac.sh) to a file and run it (Note: this script requires the root password) + + $ [text editor] installmac.sh (Copy contents of file to text editor) + $ chmod +x installmac.sh + $ ./installmac.sh + +## For Ubuntu Users + +Copy the contents of [installUbuntu.sh](https://github.com/wevote/WebApp/blob/develop/installUbuntu.sh) to a file and run it (Note: this script requires the root password) + + $ [text editor] installmac.sh (Copy contents of file to text editor) + $ chmod +x installUbuntu.sh + $ ./installUbuntu.sh diff --git a/docs/installing/CHANGE_PORT.md b/docs/installing/CHANGE_PORT.md new file mode 100644 index 000000000..3a9ac76ae --- /dev/null +++ b/docs/installing/CHANGE_PORT.md @@ -0,0 +1,32 @@ +# How to run WebApp on a different port + + +## December 2019: We no longer use gulp, so these instructions are obsolete, and need to be updated + + +In src/js/config.js, change ```WE_VOTE_HOSTNAME```'s port: + +``` +module.exports = { + WE_VOTE_URL_PROTOCOL: "http://", + WE_VOTE_HOSTNAME: "localhost:8080", + SECURE_CERTIFICATE_INSTALLED: false, +``` + +In server.js, change ```port```: + +``` +module.exports = function (PROD) { + const port = 8080; + const opts = { redirect: true }; +``` + +In Gulpfile.js, add the ```port``` option to the initialization parameters for browserSync: + +``` +browserSync.init({ + proxy: "localhost:3003", + open: false, + port: 8080, + ... +``` diff --git a/docs/installing/CLONING_CODE.md b/docs/installing/CLONING_CODE.md index 3cbb03b12..d0590dae4 100644 --- a/docs/installing/CLONING_CODE.md +++ b/docs/installing/CLONING_CODE.md @@ -1,49 +1,64 @@ - - -## Contents +# Bringing Code to Your Machine +[Go back to Readme Home](../../README.md) -- [Bringing Code to Your Machine](#bringing-code-to-your-machine) - - [Setting up your repository for work](#setting-up-your-repository-for-work) - - [Clone the repo](#clone-the-repo) - - [](#) +## Set up your environment - +Make sure you have created a place to put all of the code from Github, for example: -# Bringing Code to Your Machine + $ mkdir /Users//MyProjects/ ## Setting up your repository for work -1.Install and configure git on your local machine. +1. Install and configure git on your local machine if it is not already installed. See instructions [here](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git). + +1. Create a fork of wevote/WebApp.git repository. You can do this from https://github.com/wevote/WebApp with the "Fork" button (upper right of screen) + +1. Using your terminal program, change directory into the local folder on your computer where you want the WebApp repository to be downloaded (replacing "" with your login name, and with your github username) and clone your fork: + + `cd /Users//MyProjects/` + + `git clone https://github.com//WebApp.git` + +1. Change into your local WebApp repository folder, and set up a remote for upstream: + + `cd /Users//MyProjects/WebApp` -2.Create a fork of wevote/WebApp.git + `$ git remote add upstream git@github.com:wevote/WebApp.git` -3.Clone your fork +#### Windows machines only -`git clone https://github.com/username/WebApp.git` +5. Set the line endings to LF in git so ESLint doesn't flag CRLF line endings as errors -4.In your local repository, set up a remote for upstream: -`$ git remote add upstream git@github.com:wevote/WebApp.git` + $ git config --global core.autocrlf false -5.Create ssh keys: `ssh-keygen -t rsa -C "youremail@somedomain.com"` +Next, set `* text=auto` in your `.gitattributes` file (you may need to create this file in your root WebApp folder) -6.`ssh-add ~/.ssh/id_rsa` + * text=auto -7.`pbcopy < ~/.ssh/id_rsa.pub` +Finally, set the core.eol to LF -8.Go paste your keys into Github, under SSH Keys for your account. + $ git config --global core.eol lf -9.Set up a git client where origin is a fork of the repository (e.g. - pertrai1/WebApp), and upstream is the real deal (e.g. wevote/WebApp) -### Clone the repo +### Create and set up SSH keys. (For Windows machines, refer to [these instructions instead](https://help.github.com/articles/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent/#platform-windows)). -* Click the GitHub fork button to create your own fork -* Clone your fork of the repo to your dev system + 1. Create SSH key: + + `ssh-keygen -t rsa -C "youremail@somedomain.com"` + + 2. Add SSH private keys into the SSH authentication agent: + + `ssh-add ~/.ssh/id_rsa` -``` -git clone git@github.com:pertrai1/wevote.git -``` + 3. Copy the contents of the *public* key file into your computer's clipboard: + + `pbcopy < ~/.ssh/id_rsa.pub` + 4. Go to your "Settings" page in GitHub (click on your avatar on the top right). In the left navigation, choose "SSH and GPG keys". + + 5. Click the "New SSH key" button on the top right. + + 6. Paste the contents of the "~/.ssh/id_rsa.pub" key file (which you just copied in step 3) into the "Key" text area, and give it any Title you would like. --- diff --git a/docs/installing/ENVIRONMENT.md b/docs/installing/ENVIRONMENT.md index d0584805a..c64526714 100644 --- a/docs/installing/ENVIRONMENT.md +++ b/docs/installing/ENVIRONMENT.md @@ -1,25 +1,69 @@ - - -## Contents +# Preparing the Environment on Your Machine +[Go back to Readme Home](../../README.md) -- [Preparing the Environment on Your Machine](#preparing-the-environment-on-your-machine) - - [Install nodeenv ("Node Env")](#install-nodeenv-node-env) - - [Set up your environment](#set-up-your-environment) - - [](#) +## Explanation - +In this section we are going to install or update three package managers -- software that downloads libraries and/or executable programs that we rely on to build +the WebApp (npm, Homebrew, and pip). We are also going install or update two language interpreters (Node and Python): +* [Node](https://nodejs.org/en/): "Node.js is a JavaScript runtime built on Chrome's V8 JavaScript engine." Node allows you to run JavaScript outside of a browser, +at the command line, or on a server. Many of our build processes run on Node. +* [npm](https://www.npmjs.com/): NPM is the Node package manager (installer of software modules), and is automatically installed with Node. It can be updated separately and occasionally +this might be necessary. NPM is used extensively in WebApp to update most of the opensource JavaScript libraries that we leverage. +* [Python](https://www.python.org/): Python is a popular interpreted language that is used most often as a server side language for handling APIs or simple web applications. The [WeVoteServer](https://github.com/wevote/WeVoteServer), our API server, is written in Python. +* [Homebrew](https://brew.sh/): Homebrew, or simply `brew` at the command line, is "the missing package manager for MacOS." It is the package manager that is often the +first one installed, and can be used to install other package managers and libraries. +* [pip](https://pip.pypa.io/en/stable/installing/): "pip is [Python's] preferred installer program. Starting with Python 3.4, it is included by default with the Python binary installers." +The Mac's built-in Python distibution does not include pip, so we need to install it manually. +* [nodeenv](https://github.com/ekalinin/nodeenv): "nodeenv (Node.js virtual environment) is a tool to create isolated Node.js environments. +It creates an environment that has its own installation directories, that doesn't share libraries with other Node.js virtual environments." +* [node-sass](https://github.com/sass/node-sass): "Node-sass is a library that provides binding for Node.js to LibSass, the C version of the popular stylesheet preprocessor, Sass." We write styles in Sass (scss files), and the node-sass pre-processor compiles them to css on the fly, before the excution of the WebApp begins. +* [Ruby](https://www.ruby-lang.org/en/): "A dynamic, open source programming language with a focus on simplicity and productivity." Ruby is yet another interpreted language, that comes pre-installed on the Mac. We use it to install Homebrew. -# Preparing the Environment on Your Machine +## Install nodeenv ("Node Env") - Macintosh (see below for Windows) + +Install [Homebrew](https://brew.sh/), and then install Python: -## Install nodeenv ("Node Env") + $ cd ~ + $ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)" + $ brew install python -Install nodeenv globally. For instructions installing it locally, see: https://github.com/ekalinin/nodeenv + + +Next use Python to install pip: + + $ curl https://bootstrap.pypa.io/get-pip.py | sudo python -Create a place for your WebApp virtual environment to live on your hard drive. We recommend installing it -away from the WebApp source code: +Now install nodeenv with pip. Install nodeenv globally. (For instructions installing it locally, see: https://github.com/ekalinin/nodeenv): + + $ sudo -H pip install nodeenv + +If you are already using Node and npm, confirm that your installation is at least at these minimum +versions: + + $ node -v + v10.12.0 + + $ npm -v + 6.4.1 + +If you find that your Mac, does not have Node installed, install it with brew. (If you want to have +a fresh install of Node you can `brew unlink node` first.) A fresh or initial install of Node, +will automatically install the latest version of npm. + + $ brew install node + $ node -v + $ npm -v + +Create a place for your WebApp virtual environment to live on your hard drive. We recommend installing it away from the WebApp source code: $ mkdir /Users//NodeEnvironments/ $ cd /Users//NodeEnvironments/ @@ -33,16 +77,54 @@ Now activate this new virtual environment: $ cd /Users//NodeEnvironments/WebAppEnv/ $ . bin/activate -Confirm the versions of your main packages are >= to these versions: +**Note** that you will need to do the step above every time before you start local development. Once you have activated this virtual Node environment, it will persist in the current Terminal tab even as you navigate to different folders (e.g., the We Vote WebApp folder). You'll see that each line on the Terminal starts with `(WebAppEnv)` while the virtual environment is in effect. You can read more about nodeenv [here](https://github.com/ekalinin/nodeenv). + +Just to be safe, rebuild node-sass: + + (WebAppEnv) $ npm rebuild node-sass + (WebAppEnv) $ /usr/local/bin/node -v + v9.10.1 + +Export your path to the local environment (append the `/usr/local/bin` path segment to the path that is used when in the WebAppEnv): + + (WebAppEnv) $ export PATH="/usr/local/bin:$PATH" - (WebAppEnv) $ node -v - v5.3.0 +**Mac users are now done with this page,** go on to the next section: [Bringing Code to Your Machine](CLONING_CODE.md) - (WebAppEnv) $ npm -v - 3.3.12 + +## Install Node.js - Windows + +The following instructions were modified from this [blog post](http://blog.teamtreehouse.com/install-node-js-npm-windows). + +1. Download the Windows installer from the Node.js web site (typically the .msi file): + + https://nodejs.org/en/download/ + +2. Run the installer (the .msi file you downloaded in the previous step). + +3. Follow the prompts in the Node installer (Accept the license agreement, click the NEXT button a bunch of times and accept the default installation settings). + +4. Restart your computer. You won’t be able to run Node.js® until you restart your computer. + +5. Test the installation (instructions below). + +Make sure you have Node and npm installed by running simple commands to see what version of each is installed and to run a simple test program: + +*Test Node:* To see if Node is installed, open the Windows Command Prompt, Powershell or a similar command line tool, and type `node -v`. This should print a version number, so you’ll see something like this v0.10.35. + +*Test NPM:* To see if NPM is installed, type `npm -v` in Terminal. This should print NPM’s version number so you’ll see something like this 1.4.28 + +Create a test file and run it. A simple way to test that Node.js works is to create a JavaScript file. For example, name a file `hello.js`, and just add the code `console.log('Node is installed!');`. To run the code simply open your command line program, navigate to the folder where you saved the file, and type `node hello.js`. This will start Node and run the code in the `hello.js` file. You should see the output `Node is installed!`. + + +Make sure to run `npm install -g gulp-cli` + +If you are still getting errors with gulp this is a [helpful link](https://stackoverflow.com/questions/24027551/gulp-command-not-found-error-after-installing-gulp) ## Set up your environment +If you are running Windows, we recommend installing [Git Bash](https://git-scm.com/downloads). + Create a place to put all of the code from Github: $ mkdir /Users//MyProjects/ diff --git a/docs/installing/README_INSTALLING.md b/docs/installing/README_INSTALLING.md deleted file mode 100644 index fcc484775..000000000 --- a/docs/installing/README_INSTALLING.md +++ /dev/null @@ -1,21 +0,0 @@ - - -## Contents - -- [Installing WebApp - Overview of Process](#installing-webapp---overview-of-process) - - [](#) - - - -# Installing WebApp - Overview of Process - -Our installation process is built to allow engineers all over America to contribute to We Vote. - -It may seem complicated, but it allows anyone to be in a position to make suggestions, and get involved. - ---- - -Next: [Preparing the Environment on Your Machine](ENVIRONMENT.md) - -[Go back to Readme Home](../../README.md) - diff --git a/docs/installing/RUNNING_FIRST_TIME.md b/docs/installing/RUNNING_FIRST_TIME.md index e10bdb130..cea923266 100644 --- a/docs/installing/RUNNING_FIRST_TIME.md +++ b/docs/installing/RUNNING_FIRST_TIME.md @@ -1,15 +1,5 @@ - - -## Contents - -- [Running WebApp for the First Time](#running-webapp-for-the-first-time) - - [Install and start web application](#install-and-start-web-application) - - [Using We Vote API server Locally](#using-we-vote-api-server-locally) - - [](#) - - - # Running WebApp for the First Time +[Go back to Readme Home](../../README.md) Please make sure you have read: @@ -17,25 +7,102 @@ Please make sure you have read: * [Bringing Code to Your Machine](CLONING_CODE.md) +Note that for the following steps, you must have activated the `WebAppEnv` Node virtual environment that you set up when preparing your environment. As a reminder, to activate the environment, you can run: + + $ cd /Users//NodeEnvironments/WebAppEnv/ + $ . bin/activate + +## Local config.js file + +Every developer needs to maintain their own `WebApp/src/js/config.js` file, which can be copied from `WebApp/src/js/config-template.js`. The default configuration, copied from `config-template.js`, should work as-is for new developers. + +Copy `WebApp/src/js/config-template.js` into `WebApp/src/js/config.js`: + + (WebAppEnv) $ cd /Users//MyProjects/WebApp + (WebAppEnv) $ cp src/js/config-template.js src/js/config.js + (WebAppEnv) $ cp tests/browserstack/browserstack.config-template.js tests/browserstack/browserstack.config.js + +## Mac users will need to install fsevents ("Native access to OS X FSEvents in Node.js"): + + (WebAppEnv) $ cd /Users//MyProjects/WebApp + (WebAppEnv) $ npm install fsevents + ## Install and start web application (WebAppEnv) $ cd /Users//MyProjects/WebApp - (WebAppEnv) $ npm -g install gulp-cli // try sudo if it does not work - (WebAppEnv) $ npm install - (WebAppEnv) $ gulp - (WebAppEnv) $ npm rebuild node-sass + (WebAppEnv) $ npm install // try sudo if it does not work + (WebAppEnv) $ npm start You should be able to visit WebApp here: http://localhost:3000 +(If you would like to run the WebApp on a different port follow [these instructions](CHANGE_PORT.md)) + +## Start the WebApp in HTTPS + +You will need to install [OpenSSL](https://www.openssl.org/) in order to make an ssl (https) connection to the WebApp on +your local PC/Mac. An https connection will be required for oauth logins +via Twitter, Facebook, or Apple connect. It is also required for donation with Stripe. + +To install OpenSSL for Mac OS X, type in your terminal: + +1) `brew install openssl` + +1) After installation, check the version: `openssl version` + +1) If it's not showing the most recent version, then you need to symlink to the updated openssl version like so: + + ln -s /usr/local/Cellar/openssl/1.0.2h_1/bin/openssl /usr/local/bin/openssl + +Now create a self signed certificate, starting from the WebApp folder. + + openssl genrsa -des3 -passout pass:xxxx -out server.pass.key 2048 + openssl rsa -passin pass:xxxx -in server.pass.key -out server.key + rm server.pass.key + openssl req -new -key server.key -out server.csr + +At this point you will be asked for certificate information, like this: + + Country Name (2 letter code) []:US + State or Province Name (full name) []:California + Locality Name (eg, city) []:Oakland + Organization Name (eg, company) []:We Vote + Organizational Unit Name (eg, section) []: + Common Name (eg, fully qualified host name) []:localhost + Email Address []: + + Please enter the following 'extra' attributes + to be sent with your certificate request + A challenge password []: + +Finally run this: + + openssl x509 -req -sha256 -days 365 -in server.csr -signkey server.key -out server.crt + mv server.key ./src/cert/ + mv server.crt ./src/cert/ + +Enter this into your Chrome address bar: + + chrome://flags/#allow-insecure-localhost + +And then enable that setting. You will have to restart your browser. + +Then you should be able to start the WebApp using SSL + + (WebAppEnv) $ npm run start-https + +You should be able to visit WebApp via HTTPS here: + + https://localhost:3000 + -## Using We Vote API server Locally +## Using We Vote API server Locally: OPTIONAL -The default configuration connections to our live API server at: https://api.wevoteusa.org +The default configuration connections to our live API server at: https://api.wevoteusa.org, so this step is optional. -IFF you would like to install the We Vote API server locally, start by reading the instructions -[install WeVoteServer](https://github.com/wevote/WeVoteServer/blob/master/README_API_INSTALL.md) +If you would like to install the We Vote API server locally, start by reading the instructions to +[install WeVoteServer](https://github.com/wevote/WeVoteServer/blob/master/README_API_INSTALL.md). --- diff --git a/docs/testing/EXPLAIN_FILES.md b/docs/testing/EXPLAIN_FILES.md new file mode 100644 index 000000000..7b60b1634 --- /dev/null +++ b/docs/testing/EXPLAIN_FILES.md @@ -0,0 +1,81 @@ +# Understanding important files in the WebApp/tests/browserstack directory + +-rw-r--r-- 1 rpite rpite 6122 Jul 5 14:00 add_devices.py
    +-rw-r--r-- 1 rpite rpite 3929 Jun 20 17:22 automate.py
    +-rw-r--r-- 1 rpite rpite 476 Jul 27 13:13 browserstack.config.js
    +-rw-r--r-- 1 rpite rpite 337 May 15 12:33 browserstack.config-template.js
    +-rw-r--r-- 1 rpite rpite 5358 Jul 29 08:25 config.py
    +-rw-r--r-- 1 rpite rpite 6603 Jul 15 14:16 devices_to_test.json
    +-rw-r--r-- 1 rpite rpite 112 Apr 27 05:15 .eslintrc
    +-rw-r--r-- 1 rpite rpite 4744 Jul 29 08:26 parallel.wdio.config.js
    +-rw-r--r-- 1 rpite rpite 117 May 21 18:51 README.md
    +-rw-r--r-- 1 rpite rpite 1551 Jul 13 10:33 remove_devices.py
    +-rw-r--r-- 1 rpite rpite 1852 Jul 27 13:30 repl
    +-rw-r--r-- 1 rpite rpite 78 Jul 29 10:42 requirements.txt
    +drwxr-xr-x 2 rpite rpite 4096 Jul 29 10:25 specs
    +-rwxr-xr-x 1 rpite rpite 1607 Jul 15 05:52 testScript
    +-rw-r--r-- 1 rpite rpite 7979 Jul 27 13:29 utils.js
    +-rw-r--r-- 1 rpite rpite 4169 Jul 29 08:21 wdio.conf.js
    +-rw-r--r-- 1 rpite rpite 4564 Jul 29 07:55 wdio.conf.template
    + +## Python Scripts + +Requires python3+ + +To install python3 modules do: + + $ pip3 install -r requirements.txt + +### add_devices.py + +Adds device json to the devices_to_test.json file. Use -h for help. + +### automate.py + +Automatically inserts code into test script file. Requires [i3-wm](https://i3wm.org/) and xclip. Use -h for help. + +### remove_devices.py + +Removes device json from the devices_to_test.json file. Use -h for help. + +### config.py + +Uses the wdio.config.template file to generate the wdio.config.js file, which is used for running the test. Use -h for help. + +## Javascript files + +### browserstack.config.js + +Configuration file for test scripts. + +### parallel.wdio.config.js + +Template file for creating wdio.config.template. + +### utils.js + +Defines javascript functions used in test scripts. + +### wdio.conf.js + +Configuration for test run by webdriverio. + +## Bash scripts + +Requires bash + +### testScript + +Generates the template file named wdio.config.template which acts as a template for creating wdio.config.js as well as creates the script using the script name parameter if it does not already exist. + +## Miscellaneous files + +### repl + +Copied and pasted into the [wdio repl](https://webdriver.io/docs/repl.html) to use the functions defined in utils.js + +### wdio.conf.template + +Template file for creating wdio.conf.js + + diff --git a/docs/testing/README_TESTING.md b/docs/testing/README_TESTING.md new file mode 100644 index 000000000..cf3dd16e2 --- /dev/null +++ b/docs/testing/README_TESTING.md @@ -0,0 +1,89 @@ +# Testing WebApp - Overview of Process + +## Minimum Browsers + +[Click here to see the minimum browser versions](https://docs.google.com/spreadsheets/d/1FlUMCvg1pNIO0IzJm0jQyvUW1YC_KHh-LO4l-OVIcog/edit#gid=1774503729) +that we support. + +## How to test Wevote WebApp with BrowserStack + +If you haven't updated your dependencies in a while, run `npm install` from your terminal to install WebdriverIO (this is a framework that lets us test both the browser app and Cordova mobile apps with a single script). + +### Manual installation only + +Copy `WebApp/tests/browserstack/browserstack.config-template.js` into `WebApp/tests/browserstack/browserstack.config.js`: + + (WebAppEnv) $ cd WebApp + (WebAppEnv) $ cp tests/browserstack/browserstack.config-template.js tests/browserstack/browserstack.config.js + +### Automated installation start here + +You'll need to add your credentials to `browserstack.config.js`. Sign into Browserstack and navigate to the [BrowserStack Automate dashboard](https://automate.browserstack.com/). Press the down arrow next to where it says "Access Key" in the header. You should see your username ("YOUR-USERNAME" below) and access key ("ACCESS-KEY-HERE" below). You will need both of these values to upload the compiled App. + +You will also need the URL for the android app .apk file. You can get this by asking someone else or by uploading the file with Browserstack's REST API as described [here](https://www.browserstack.com/app-automate/rest-api?framework=appium). +Visit this page when you are signed into Browserstack, and they will customize the command that you need to run from your terminal window: + + curl -u "YOUR-USERNAME:ACCESS-KEY-HERE" -X POST https://api-cloud.browserstack.com/app-automate/upload -F "file=@/path/to/app/file/Application-debug.apk" -F 'data={"custom_id": "MyApp"}' + +You can find the latest We Vote APK (for Android) and IPA (for iOS) in [this Google Drive folder](https://drive.google.com/drive/u/0/folders/10tK7oqY7FKWhe0ilHDcli-DWpT9ldTFs). +Please download it to your Download folder. For example, to find this path on a Mac: + + (WebAppEnv) $ cd ~/Downloads + (WebAppEnv) $ pwd + /Users/dalemcgrew/Downloads + +In this example, the Android APK downloaded file is `app-debug-5-29-19.apk`. The full path to this downloaded file is now: + + /Users/dalemcgrew/Downloads/app-debug-5-29-19.apk + +So the terminal command to upload the file would look like this: + + curl -u "YOUR-USERNAME:ACCESS-KEY-HERE" -X POST https://api-cloud.browserstack.com/app-automate/upload -F "file=@/Users/dalemcgrew/Downloads/app-debug-5-29-19.apk" -F 'data={"custom_id": "MyApp"}' + +It will typically take 30-60 seconds to upload (without any feedback), and then return a path like this: + + {"app_url":"bs://ANOTHER-GENERATED-STRING-HERE","custom_id":"MyApp","shareable_id":"dalemcgrew1/MyApp"} + +Copy the path `bs://ANOTHER-GENERATED-STRING-HERE` into your `WebApp/tests/browserstack/browserstack.config.js` file, +and put it into the `BROWSERSTACK_APK_URL` value field like this: + + BROWSERSTACK_APK_URL: 'bs://ANOTHER-GENERATED-STRING-HERE', + +With this `BROWSERSTACK_APK_URL` variable set now, we can run tests on the android mobile application. + +There are three scripts for running tests: config.py, testscript, and wdio.config.js. testScript generates the template file named wdio.config.template which acts as a template for creating wdio.config.js as well as creates the script using the script name parameter if it does not already exist. config.py uses the wdio.config.template file to generate the wdio.config.js file, which is used for running the test. Note that testScript requires bash and config.py requires python3. + + (WebAppEnv) $ ./testscript -s - - - +
    +
    +
    +
    Your election is loading...
    +
    +
    +
    + + + + + + diff --git a/src/javascript/google-analytics-template.js b/src/javascript/google-analytics-template.js new file mode 100644 index 000000000..8bea45afc --- /dev/null +++ b/src/javascript/google-analytics-template.js @@ -0,0 +1,6 @@ +(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ + (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), + m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) + })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); +ga('create', 'UA-key-here', 'auto'); +ga('send', 'pageview'); diff --git a/src/javascript/google-tag-manager-template.js b/src/javascript/google-tag-manager-template.js new file mode 100644 index 000000000..7961a6277 --- /dev/null +++ b/src/javascript/google-tag-manager-template.js @@ -0,0 +1,5 @@ +(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': +new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], +j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= +'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); +})(window,document,'script','dataLayer','GTM-key-here'); diff --git a/src/js/Application.jsx b/src/js/Application.jsx index 10c8484ae..5b5ceba16 100644 --- a/src/js/Application.jsx +++ b/src/js/Application.jsx @@ -1,77 +1,487 @@ -import React, { Component, PropTypes } from "react"; -import Navigator from "./components/Navigator"; -import MoreMenu from "./components/MoreMenu"; -import Header from "./components/Header"; -import SubHeader from "./components/SubHeader"; -import VoterStore from "./stores/VoterStore"; -import StarActions from "./actions/StarActions"; -import VoterActions from "./actions/VoterActions"; -import LoadingWheel from "./components/LoadingWheel"; -import Headroom from "react-headroom"; - -export default class Application extends Component { - static propTypes = { - children: PropTypes.element, - route: PropTypes.object, - location: PropTypes.object - }; +import React, { Component } from 'react'; +import * as PropTypes from 'prop-types'; +import { ToastContainer } from 'react-toastify'; +import styled from 'styled-components'; +import AppActions from './actions/AppActions'; +import AppStore from './stores/AppStore'; +import { getApplicationViewBooleans, setZenDeskHelpVisibility } from './utils/applicationUtils'; +import cookies from './utils/cookies'; +import { getToastClass, historyPush, isIOSAppOnMac, isCordova, isWebApp } from './utils/cordovaUtils'; +import { cordovaContainerMainOverride, cordovaVoterGuideTopPadding } from './utils/cordovaOffsets'; +import cordovaScrollablePaneTopPadding from './utils/cordovaScrollablePaneTopPadding'; +import DelayedLoad from './components/Widgets/DelayedLoad'; +import displayFriendsTabs from './utils/displayFriendsTabs'; +import ElectionActions from './actions/ElectionActions'; +import FooterBar from './components/Navigation/FooterBar'; +import FriendActions from './actions/FriendActions'; +import Header from './components/Navigation/Header'; +import OrganizationActions from './actions/OrganizationActions'; +import { renderLog, routingLog } from './utils/logging'; +import ShareButtonFooter from './components/Share/ShareButtonFooter'; +import signInModalGlobalState from './components/Widgets/signInModalGlobalState'; +import SnackNotifier from './components/Widgets/SnackNotifier'; +import { stringContains } from './utils/textFormat'; +import { initializationForCordova, removeCordovaSpecificListeners } from './startCordova'; +import VoterActions from './actions/VoterActions'; +import VoterStore from './stores/VoterStore'; +import webAppConfig from './config'; + +class Application extends Component { constructor (props) { super(props); - this.state = {}; + this.state = { + // Do not define voter here. We rely on it being undefined + voter_initial_retrieve_needed: true, + }; + // 2021-1-3: This is a workaround for the difficulty of nesting components in react-router V5, it should not be necessary + global.weVoteGlobalHistory.listen((location, action) => { + if (location.pathname !== this.state.priorPath) { + // Re-render the Application if the path changed (Needed for React-router V5) + this.setState({ priorPath: window.locationPathname }); + } + if (webAppConfig.LOG_ROUTING) { + console.log(`Application: The current URL is ${location.pathname}${location.search}${location.hash}`); + console.log(`Application: The last navigation action was ${action}`, JSON.stringify(global.weVoteGlobalHistory, null, 2)); + } + }); } componentDidMount () { - let voter_device_id = VoterStore.voterDeviceId(); - VoterActions.retrieveVoter(voter_device_id); - StarActions.retrieveAll(); - this.token = VoterStore.addListener(this._onChange.bind(this)); + const voterDeviceId = VoterStore.voterDeviceId(); + VoterActions.voterRetrieve(); + // console.log('===== VoterRetrieve from Application, voterDeviceId:', voterDeviceId); + + let { hostname } = window.location; + hostname = hostname || ''; + AppActions.siteConfigurationRetrieve(hostname); + console.log('React Application --------------- componentDidMount () hostname: ', hostname); + this.initializeFacebookSdkForJavascript(); + if (isCordova()) { + initializationForCordova(); + } + + // console.log('Application, componentDidMount, voterDeviceId:', voterDeviceId); + if (voterDeviceId) { + this.onVoterStoreChange(); + } + + ElectionActions.electionsRetrieve(); + + this.appStoreListener = AppStore.addListener(this.onAppStoreChange.bind(this)); + this.voterStoreListener = VoterStore.addListener(this.onVoterStoreChange.bind(this)); + window.addEventListener('scroll', this.handleWindowScroll); + // dumpCookies(); + } + + // See https://reactjs.org/docs/error-boundaries.html + static getDerivedStateFromError (error) { // eslint-disable-line no-unused-vars + // Update state so the next render will show the fallback UI, We should have a "Oh snap" page + return { hasError: true }; + } + + // eslint-disable-next-line no-unused-vars + componentDidUpdate (prevProps, prevState, nextContent) { + // console.log('Application componentDidUpdate'); + const { location: { pathname } } = window; + const { lastZenDeskVisibilityPathName } = this.state; + if (stringContains('/ballot', pathname.toLowerCase().slice(0, 7)) || + stringContains('/ready', pathname.toLowerCase().slice(0, 7))) { + if (!AppStore.showEditAddressButton()) { + AppActions.setShowEditAddressButton(true); + } + } else if (AppStore.showEditAddressButton()) { + AppActions.setShowEditAddressButton(false); + } + + if (isWebApp() && String(lastZenDeskVisibilityPathName) !== String(pathname)) { + // console.log('lastZenDeskVisibilityPathName:', lastZenDeskVisibilityPathName, ', pathname:', pathname); + setZenDeskHelpVisibility(pathname); + this.setState({ + lastZenDeskVisibilityPathName: String(pathname), + }); + } + } + + componentDidCatch (error, info) { + // We should get this information to Splunk! + console.error('Application caught error: ', `${error} with info: `, info); } componentWillUnmount () { - this.token.remove(); + this.appStoreListener.remove(); + this.voterStoreListener.remove(); + window.removeEventListener('scroll', this.handleWindowScroll); + if (isCordova()) { + removeCordovaSpecificListeners(); + } } - _onChange () { - this.setState({ - voter: VoterStore.voter(), - location: VoterStore.getAddress() - }); + initializeFacebookSdkForJavascript () { // eslint-disable-line + if (webAppConfig.ENABLE_FACEBOOK) { + window.fbAsyncInit = function () { // eslint-disable-line func-names + const { FB } = window; + FB.init({ + appId: webAppConfig.FACEBOOK_APP_ID, + autoLogAppEvents: true, + xfbml: true, + version: 'v7.0', // Facebook JavaScript SDK - Facebook Version + status: true, // set this status to true, this will fix the popup blocker issue + }); + }; + + (function (d, s, id) { // eslint-disable-line + let js; + const fjs = d.getElementsByTagName(s)[0]; + if (d.getElementById(id)) { + return; + } + + js = d.createElement(s); // eslint-disable-line prefer-const + js.id = id; + js.src = 'https://connect.facebook.net/en_US/sdk.js'; + fjs.parentNode.insertBefore(js, fjs); + }(document, 'script', 'facebook-jssdk')); + } } - render () { - var { location: { pathname }} = this.props; - var { voter, location } = this.state; - var ballotItemWeVoteId = ""; /* TODO Dale: Store the ballot item that is "on stage" in Ballot store? (wv02cand3) */ + onAppStoreChange () { + // console.log('Application, onAppStoreChange'); + let signInStartFullUrl = cookies.getItem('sign_in_start_full_url'); + // console.log('Application onAppStoreChange, current signInStartFullUrl: ', signInStartFullUrl); + // Do not let sign_in_start_full_url be set again. Different logic while we figure out how to call AppActions.unsetStoreSignInStartFullUrl() + if (AppStore.storeSignInStartFullUrl() && !signInStartFullUrl) { + const { origin, pathname } = window.location; + // console.log('window.location:', window.location); + const oneDayExpires = 86400; + signInStartFullUrl = `${origin}${pathname}`; + // console.log('Application onAppStoreChange, new origin: ', origin, ', pathname: ', pathname, ', NEW signInStartFullUrl: ', signInStartFullUrl); + if (stringContains('facebook_sign_in', signInStartFullUrl)) { + // console.log('Do NOT set signInStartFullUrl:', signInStartFullUrl); + } else if (origin && stringContains('wevote.us', origin)) { + cookies.setItem('sign_in_start_full_url', signInStartFullUrl, oneDayExpires, '/', 'wevote.us'); + } else { + cookies.setItem('sign_in_start_full_url', signInStartFullUrl, oneDayExpires, '/'); + } + // AppActions.unsetStoreSignInStartFullUrl(); // Throws this error: Cannot dispatch in the middle of a dispatch. + } + } - if (voter === undefined || location === undefined ) { - return LoadingWheel; + onVoterStoreChange () { + if (!signInModalGlobalState.get('textOrEmailSignInInProcess')) { + // console.log('Application, onVoterStoreChange'); + const voterDeviceId = VoterStore.voterDeviceId(); + if (voterDeviceId && voterDeviceId !== '') { + if (this.state.voter_initial_retrieve_needed) { + VoterActions.voterEmailAddressRetrieve(); + VoterActions.voterSMSPhoneNumberRetrieve(); + FriendActions.friendInvitationsSentToMe(); + this.incomingVariableManagement(); + this.setState({ + voter: VoterStore.getVoter(), + voter_initial_retrieve_needed: false, + }); + } else { + this.setState({ + voter: VoterStore.getVoter(), + }); + } + } + // console.log('Application onVoterStoreChange voter: ', VoterStore.getVoter()); + // console.log('SignedIn Voter in Application onVoterStoreChange voter: ', VoterStore.getVoter().full_name); } + } - return
    - -
    -
    -
    + getAppBaseClass = () => { + // console.log('Determine the headroom space pathname:' + pathname); + let appBaseClass = 'app-base'; + if (isWebApp() || isIOSAppOnMac()) { + appBaseClass += ' headroom-webapp'; + } else { + appBaseClass += ' cordova-base'; + } + return appBaseClass; + }; + + handleWindowScroll = (evt) => { + const { scrollTop } = evt.target.scrollingElement; + if (scrollTop > 60 && !AppStore.getScrolledDown()) { + AppActions.setScrolled(true); + } + if (scrollTop < 60 && AppStore.getScrolledDown()) { + AppActions.setScrolled(false); + } + }; + + incomingVariableManagement () { + // console.log('Application, incomingVariableManagement, this.props.location.query: ', this.props.location.query); + const { location: { pathname, query } } = window; + if (query) { + // Cookie needs to expire in One day i.e. 24*60*60 = 86400 + let atLeastOneQueryVariableFound = false; + const { hide_intro_modal: hideIntroModal } = this.props.location.query; + const hideIntroModalFromUrl = this.props.location.query ? hideIntroModal : 0; + const hideIntroModalFromUrlTrue = hideIntroModalFromUrl === 1 || hideIntroModalFromUrl === '1' || hideIntroModalFromUrl === 'true'; + if (hideIntroModalFromUrl) { + // console.log('hideIntroModalFromUrl: ', hideIntroModalFromUrl); + atLeastOneQueryVariableFound = true; + } + + const hideIntroModalFromCookie = cookies.getItem('hide_intro_modal'); + const hideIntroModalFromCookieTrue = hideIntroModalFromCookie === 1 || hideIntroModalFromCookie === '1' || hideIntroModalFromCookie === 'true'; + if (hideIntroModalFromUrlTrue && !hideIntroModalFromCookieTrue) { + const oneDayExpires = 86400; + cookies.setItem('hide_intro_modal', hideIntroModalFromUrl, oneDayExpires, '/'); + } + + // Support the incoming "id=" url variable. This is the client id referred to as external_voter_id in https://api.wevoteusa.org/apis/v1/docs/organizationAnalyticsByVoter/ + const { id: externalVoterId } = this.props.location.query; + if (externalVoterId) { + // console.log('externalVoterId: ', externalVoterId); + VoterActions.setExternalVoterId(externalVoterId); + atLeastOneQueryVariableFound = true; + } + + let autoFollowListFromUrl = ''; + if (this.props.location.query) { + // console.log('this.props.location.query: ', this.props.location.query); + const { + af, auto_follow: autoFollow, + voter_address: voterAddress, + } = this.props.location.query; + if (this.props.location.query.af) { + autoFollowListFromUrl = af; + atLeastOneQueryVariableFound = true; + } else if (autoFollow) { + atLeastOneQueryVariableFound = true; + autoFollowListFromUrl = autoFollow; + } + + const autoFollowList = autoFollowListFromUrl ? autoFollowListFromUrl.split(',') : []; + autoFollowList.forEach((organizationTwitterHandle) => { + OrganizationActions.organizationFollow('', organizationTwitterHandle); + }); + + if (voterAddress) { + // console.log('this.props.location.query.voter_address: ', this.props.location.query.voter_address); + atLeastOneQueryVariableFound = true; + if (voterAddress && voterAddress !== '') { + // Do not save a blank voterAddress -- we don't want to over-ride an existing address with a blank + VoterActions.voterAddressSave(voterAddress); + } + } + + if (atLeastOneQueryVariableFound && pathname) { + console.log('atLeastOneQueryVariableFound push: ', atLeastOneQueryVariableFound); + console.log('pathname: ', pathname); + historyPush(pathname); + } else { + console.log('atLeastOneQueryVariableFound NO NO NO push no query vars found: '); + } + } + } + } + + render () { + renderLog('Application'); // Set LOG_RENDER_EVENTS to log all renders + const { location: { pathname } } = window; + const { params } = this.props; + const { StripeCheckout } = window; + const waitForStripe = (String(pathname) === '/more/donate' && StripeCheckout === undefined); + // console.log('Application render, pathname:', pathname); + + if (this.state.voter === undefined || this.props.location === undefined || waitForStripe) { + if (waitForStripe) { + console.log('Waiting for stripe to load, on an initial direct URL to DonationForm'); + } + return ( + +
    +

    More election data loading...

    + { isCordova() && ( + +

    Does your phone have access to the internet?

    +
    + )} +
    + + ); + } + + routingLog(pathname); + + const { + inTheaterMode, contentFullWidthMode, extensionPageMode, settingsMode, sharedItemLandingPage, + showFooterBar, showShareButtonFooter, twitterSignInMode, voterGuideCreatorMode, + voterGuideMode, + } = getApplicationViewBooleans(pathname); + // console.log('showShareButtonFooter:', showShareButtonFooter); + // dumpObjProps('Application params', params); + // const nextReleaseFeaturesEnabled = webAppConfig.ENABLE_NEXT_RELEASE_FEATURES === undefined ? false : webAppConfig.ENABLE_NEXT_RELEASE_FEATURES; + + if (extensionPageMode || sharedItemLandingPage || twitterSignInMode) { + return ( +
    + { this.props.children }
    -
    -
    - -
    -
    - -
    -
    -
    - { voter.signed_in_personal ? : } -
    -
    - { this.props.children } -
    + ); + } else if (inTheaterMode) { + // console.log('inTheaterMode', inTheaterMode); + return ( +
    + +
    +
    +
    +
    + { this.props.children } +
    +
    +
    +
    +
    +
    + ); + } else if (voterGuideMode || voterGuideCreatorMode) { + // console.log('voterGuideMode', voterGuideMode); + return ( +
    + +
    + + +
    +
    + { this.props.children } +
    +
    +
    + {showFooterBar && ( +
    + +
    + )} + {showShareButtonFooter && ( + + )}
    + ); + } else if (settingsMode) { + // console.log('settingsMode', settingsMode); + return ( +
    + +
    + + +
    +
    + { this.props.children } +
    +
    +
    + {showFooterBar && ( +
    + +
    + )} + {showShareButtonFooter && ( + + )} +
    + ); + } + // This handles other pages, like Welcome and the Ballot display + // console.log('Application, another mode'); + return ( +
    + +
    + + { typeof pathname !== 'undefined' && pathname && + (String(pathname) === '/for-campaigns' || + String(pathname) === '/for-organizations' || + String(pathname).startsWith('/how') || + String(pathname) === '/more/about' || + String(pathname) === '/more/credits' || + String(pathname).startsWith('/more/donate') || + String(pathname).startsWith('/more/pricing') || + String(pathname) === '/welcome' || + !contentFullWidthMode || displayFriendsTabs()) ? + ( +
    + { this.props.children } +
    + ) : + ( + +
    +
    +
    + { this.props.children } +
    +
    +
    +
    + )} + {showFooterBar && ( +
    + +
    + )} + {showShareButtonFooter && ( + + )}
    - -
    ; + ); } } +Application.propTypes = { + children: PropTypes.object, + location: PropTypes.object, + params: PropTypes.object, +}; + +const Wrapper = styled.div` + padding-top: ${({ padTop }) => padTop}; +`; + +const LoadingScreen = styled.div` + position: 'fixed', + height: '100vh', + width: '100vw', + display: 'flex', + top: 0, + left: 0, + background-color: '#2E3C5D', + justify-content: 'center', + align-items: 'center', + font-size: '30px', + color: '#fff', + flex-direction: 'column', + @media print{ + color: '#2E3C5D'; + } +`; + +export default Application; diff --git a/src/js/ApplicationForReady.jsx b/src/js/ApplicationForReady.jsx new file mode 100644 index 000000000..0504f8f58 --- /dev/null +++ b/src/js/ApplicationForReady.jsx @@ -0,0 +1,188 @@ +import React, { Component } from 'react'; +import * as PropTypes from 'prop-types'; +import { ToastContainer } from 'react-toastify'; +import styled from 'styled-components'; +import { getToastClass, isCordova, isWebApp } from './utils/cordovaUtils'; +// import { cordovaContainerMainOverride, cordovaScrollablePaneTopPadding } from './utils/cordovaOffsets'; +// import FooterBar from './components/Navigation/FooterBar'; +// import Header from './components/Navigation/Header'; +import { renderLog } from './utils/logging'; +// import ShareButtonFooter from './components/Share/ShareButtonFooter'; +import SnackNotifier from './components/Widgets/SnackNotifier'; + +class ApplicationForReady extends Component { + constructor (props) { + super(props); + this.state = { + // Do not define voter here. We rely on it being undefined + // voter_initial_retrieve_needed: true, + }; + } + + componentDidMount () { + let { hostname } = window.location; + hostname = hostname || ''; + // AppActions.siteConfigurationRetrieve(hostname); + console.log('ApplicationForReady --------------- componentDidMount () hostname: ', hostname); + // polyfillFixes(); + // this.initializeFacebookSdkForJavascript(); + // if (isCordova()) { + // initializationForCordova(); + // } + + // const voterDeviceId = VoterStore.voterDeviceId(); + // VoterActions.voterRetrieve(); + // + // // console.log('Application, componentDidMount, voterDeviceId:', voterDeviceId); + // if (voterDeviceId) { + // this.onVoterStoreChange(); + // } + // + // ElectionActions.electionsRetrieve(); + // + // this.appStoreListener = AppStore.addListener(this.onAppStoreChange.bind(this)); + // this.voterStoreListener = VoterStore.addListener(this.onVoterStoreChange.bind(this)); + // window.addEventListener('scroll', this.handleWindowScroll); + } + + // See https://reactjs.org/docs/error-boundaries.html + static getDerivedStateFromError (error) { // eslint-disable-line no-unused-vars + // Update state so the next render will show the fallback UI, We should have a "Oh snap" page + return { hasError: true }; + } + + componentDidCatch (error, info) { + // We should get this information to Splunk! + console.error('Application caught error: ', `${error} with info: `, info); + } + + componentWillUnmount () { + // this.appStoreListener.remove(); + // this.voterStoreListener.remove(); + // window.removeEventListener('scroll', this.handleWindowScroll); + // if (isCordova()) { + // removeCordovaSpecificListeners(); + // } + } + + getAppBaseClass = () => { + // console.log('Determine the headroom space pathname:' + pathname); + let appBaseClass = 'app-base'; + if (isWebApp()) { + appBaseClass += ' headroom-webapp'; + } else { + appBaseClass += ' cordova-base'; + } + return appBaseClass; + }; + + render () { + renderLog('ApplicationForReady'); // Set LOG_RENDER_EVENTS to log all renders + // const { location: { pathname } } = this.props; + // console.log('ApplicationForReady render, pathname:', pathname); + + if (this.props.location === undefined) { + return ( + +
    +

    More election data loading...

    + { isCordova() && +

    Does your phone have access to the internet?

    } +
    +
    + + ); + } + + // routingLog(pathname); + + // const { + // inTheaterMode, contentFullWidthMode, extensionPageMode, settingsMode, sharedItemLandingPage, + // showFooterBar, showShareButtonFooter, twitterSignInMode, voterGuideCreatorMode, + // voterGuideMode, + // } = getApplicationViewBooleans(pathname); + // const contentFullWidthMode = true; + // const readyMode = true; + // const showFooterBar = true; + // const showShareButtonFooter = false; + // console.log('showShareButtonFooter:', showShareButtonFooter); + // const nextReleaseFeaturesEnabled = webAppConfig.ENABLE_NEXT_RELEASE_FEATURES === undefined ? false : webAppConfig.ENABLE_NEXT_RELEASE_FEATURES; + + // This handles other pages, like Welcome and the Ballot display + // console.log('ApplicationForReady, another mode'); + return ( +
    + + {/*
    */} + + {/* padTop={cordovaScrollablePaneTopPadding()} */} + +
    +
    + {/* style={{ paddingTop: `${cordovaContainerMainOverride()}` }} */} +
    + { this.props.children } +
    +
    +
    +
    + {/* {showFooterBar && ( */} + {/*
    */} + {/* */} + {/*
    */} + {/* )} */} + {/* {showShareButtonFooter && ( */} + {/* */} + {/* )} */} +
    + ); + } +} +ApplicationForReady.propTypes = { + children: PropTypes.element, + location: PropTypes.object, + // match: PropTypes.object, +}; + +const Wrapper = styled.div` + padding-top: ${({ padTop }) => padTop}; +`; + +const LoadingScreen = styled.div` + position: 'fixed', + height: '100vh', + width: '100vw', + display: 'flex', + top: 0, + left: 0, + background-color: '#2E3C5D', + justify-content: 'center', + align-items: 'center', + font-size: '30px', + color: '#fff', + flex-direction: 'column', + @media print{ + color: '#2E3C5D'; + } +`; + +export default ApplicationForReady; diff --git a/src/js/Root.jsx b/src/js/Root.jsx index dfac18654..c6c07c22f 100644 --- a/src/js/Root.jsx +++ b/src/js/Root.jsx @@ -1,107 +1,398 @@ -import React from "react"; -import { Route, IndexRoute, IndexRedirect } from "react-router"; -import cookies from "./utils/cookies"; - -// main Application -import Application from "./Application"; - -/****************************** ROUTE-COMPONENTS ******************************/ -/* Intro */ -import Intro from "./routes/Intro/Intro"; -import IntroContests from "./routes/Intro/IntroContests"; -import IntroOpinions from "./routes/Intro/IntroOpinions"; - -/* Settings */ -import SettingsDashboard from "./routes/Settings/SettingsDashboard"; -import Settings from "./routes/Settings/Settings"; -import Location from "./routes/Settings/Location"; - -/* Pages that use Ballot Navigation */ -import BallotIndex from "./routes/Ballot/BallotIndex"; -import Ballot from "./routes/Ballot/Ballot"; -import Candidate from "./routes/Ballot/Candidate"; -import EmptyBallot from "./routes/Ballot/EmptyBallot"; - -/* Ballot Off-shoot Pages */ -import Opinions from "./routes/Opinions"; // More opinions about anything on the ballot -import OpinionsAboutItem from "./routes/Ballot/OpinionsAboutItem"; // More opinions about one particular ballot item (candidate or measure) -import GuidePositionList from "./routes/Guide/PositionList"; // A list of all positions from one guide - -/* More */ -import About from "./routes/More/About"; -import OpinionsFollowed from "./routes/More/OpinionsFollowed"; -import SignIn from "./routes/More/SignIn"; -import EmailBallot from "./routes/More/EmailBallot"; -import Privacy from "./routes/More/Privacy"; - -import Requests from "./routes/Requests"; -import Connect from "./routes/Connect"; -import Activity from "./routes/Activity"; -import NotFound from "./routes/NotFound"; -import AddFriends from "./routes/AddFriends"; - - -const firstVisit = !cookies.getItem("voter_device_id"); - -const routes = () => - - - { firstVisit ? - : - } - - - - - - - {/* Settings go in this structure... */} - - - /* Complete path on one line for searching */ - - - {/* Ballot Off-shoot Pages */} - - - - - - - {/* More Menu Pages */} - - - - - - - {/* Voter Guide Pages */} - - - - - - - - - - {/* - - - - - - - */} - - - - - - - // Any route that is not found -> @return NotFound component - - ; +import React, { Suspense } from 'react'; +import { Redirect, Route, Switch } from 'react-router-dom'; // Route, +import Application from './Application'; +import News from './routes/Activity/News'; +import AddCandidateForExtension from './routes/Ballot/AddCandidateForExtension'; +import Ballot from './routes/Ballot/Ballot'; +// import BallotIndex from './routes/Ballot/BallotIndex'; +import Candidate from './routes/Ballot/Candidate'; +import CandidateForExtension from './routes/Ballot/CandidateForExtension'; +import Measure from './routes/Ballot/Measure'; +import Office from './routes/Ballot/Office'; +import FacebookInvitableFriends from './routes/FacebookInvitableFriends'; +import Friends from './routes/Friends/Friends'; +import HowItWorks from './routes/HowItWorks'; +import FriendInvitationOnboarding from './routes/Intro/FriendInvitationOnboarding'; +import GetStarted from './routes/Intro/GetStarted'; +import Intro from './routes/Intro/Intro'; +import IntroNetwork from './routes/Intro/IntroNetwork'; +import SampleBallot from './routes/Intro/SampleBallot'; +import About from './routes/More/About'; +import AbsenteeBallot from './routes/More/AbsenteeBallot'; +import Attributions from './routes/More/Attributions'; +import Credits from './routes/More/Credits'; +import Donate from './routes/More/Donate'; +import DonateThankYou from './routes/More/DonateThankYou'; +import ElectionReminder from './routes/More/ElectionReminder'; +import Elections from './routes/More/Elections'; +import ExtensionSignIn from './routes/More/ExtensionSignIn'; +import FacebookRedirectToWeVote from './routes/More/FacebookRedirectToWeVote'; +import FAQ from './routes/More/FAQ'; +import Pricing from './routes/More/Pricing'; +import Privacy from './routes/More/Privacy'; +import ProcessingDonation from './routes/More/ProcessingDonation'; +import RegisterToVote from './routes/More/RegisterToVote'; +// import ScratchPad from './routes/ScratchPad'; +import SearchPage from './routes/More/SearchPage'; +import StripeElementsTest from './routes/More/StripeElementsTest'; +import TermsOfService from './routes/More/TermsOfService'; +import VerifyRegistration from './routes/More/VerifyRegistration'; +import WeVoteBallotEmbed from './routes/More/WeVoteBallotEmbed'; +import Opinions2020 from './routes/Opinions2020'; +import OpinionsFollowed from './routes/OpinionsFollowed'; +import OpinionsIgnored from './routes/OpinionsIgnored'; +import PageNotFound from './routes/PageNotFound'; +import AppleSignInProcess from './routes/Process/AppleSignInProcess'; +import FacebookLandingProcess from './routes/Process/FacebookLandingProcess'; +import FriendInvitationByEmailVerifyProcess from './routes/Process/FriendInvitationByEmailVerifyProcess'; +import SignInEmailProcess from './routes/Process/SignInEmailProcess'; +import SignInJumpProcess from './routes/Process/SignInJumpProcess'; +import TwitterSignInProcess from './routes/Process/TwitterSignInProcess'; +import VerifyEmailProcess from './routes/Process/VerifyEmailProcess'; +// import GetReady from './routes/GetReady'; +import Ready from './routes/Ready'; +import ReadyRedirect from './routes/ReadyRedirect'; +import Register from './routes/Register'; +import ClaimYourPage from './routes/Settings/ClaimYourPage'; +import HamburgerMenu from './routes/Settings/HamburgerMenu'; +import Location from './routes/Settings/Location'; +import SettingsDashboard from './routes/Settings/SettingsDashboard'; +import SettingsMenuMobile from './routes/Settings/SettingsMenuMobile'; +import VoterGuideListDashboard from './routes/Settings/VoterGuideListDashboard'; +import VoterGuideSettingsDashboard from './routes/Settings/VoterGuideSettingsDashboard'; +import VoterGuideSettingsMenuMobile from './routes/Settings/VoterGuideSettingsMenuMobile'; +import VoterGuidesMenuMobile from './routes/Settings/VoterGuidesMenuMobile'; +import SharedItemLanding from './routes/SharedItemLanding'; +import TwitterHandleLanding from './routes/TwitterHandleLanding'; +import Values from './routes/Values'; +import ValuesList from './routes/Values/ValuesList'; +import VoterGuidesUnderOneValue from './routes/Values/VoterGuidesUnderOneValue'; +import Vote from './routes/Vote'; +import OrganizationVoterGuide from './routes/VoterGuide/OrganizationVoterGuide'; +import OrganizationVoterGuideCandidate from './routes/VoterGuide/OrganizationVoterGuideCandidate'; +import OrganizationVoterGuideMeasure from './routes/VoterGuide/OrganizationVoterGuideMeasure'; +import OrganizationVoterGuideMobileDetails from './routes/VoterGuide/OrganizationVoterGuideMobileDetails'; +import OrganizationVoterGuideOffice from './routes/VoterGuide/OrganizationVoterGuideOffice'; +import VerifyThisIsMe from './routes/VoterGuide/VerifyThisIsMe'; +import WelcomeForCampaigns from './routes/WelcomeForCampaigns'; +import WelcomeForOrganizations from './routes/WelcomeForOrganizations'; +import WelcomeForVoters from './routes/WelcomeForVoters'; +import YourPage from './routes/YourPage'; +import componentLoader from './utils/componentLoader'; +import cookies from './utils/cookies'; +import { isWebApp, polyfillFixes } from './utils/cordovaUtils'; +import RouterV5SendMatch from './utils/RouterV5SendMatch'; +polyfillFixes('Root.jsx'); + +// See /js/components/Navigation/HeaderBar.jsx for show_full_navigation cookie +// const ballotHasBeenVisited = cookies.getItem('ballot_has_been_visited'); +const firstVisit = !cookies.getItem('voter_device_id'); +// const { history } = window; +// const history = useHistory(); +// const history = global.weVoteGlobalHistory; +let { hostname } = window.location; +hostname = hostname || ''; +const weVoteSites = ['wevote.us', 'quality.wevote.us', 'localhost', 'silicon', '']; // localhost on Cordova is a '' +const isWeVoteMarketingSite = weVoteSites.includes(String(hostname)); +const isNotWeVoteMarketingSite = !isWeVoteMarketingSite; + +/* eslint-disable react/jsx-props-no-spreading */ +const routes = () => { + // console.log('window.innerWidth:', window.innerWidth); + console.log('Root.jsx routes immediately prior to instantiation'); + return ( + <> + + {(function redirect () { + if (isWebApp()) { + return ; + } else { + return firstVisit ? : ; + } + }())} + + {/* Loading...
    }> */} + {/* */} + {/* */} + Loading...
    }> + {/* */} + + {/* { */} + + {/* */} + } /> + + + + + + ()} /> + ()} /> + + + {/* Subpages BallotIndex ?????????????? */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* Additional Ballot Paths */} + + + + + + + + + + + + + + + + } /> + } /> + + + + + + + + {/* Your Settings go in this structure... */} + {/* Complete path on one line for searching */} + + + + + + + + {/* settings/:edit_mode includes "/settings/account", "/settings/address", "/settings/domain", "/settings/election", + "/settings/issues_linked", "/settings/issues_to_link", "/settings/issues", "/settings/notifications", + "/settings/profile", "/settings/text", "/settings/tools" */} + ()} /> + ()} /> + ()} /> + + {/* Ballot Off-shoot Pages */} + + + + + + + {/* Friend related Pages */} + {/* /friends/current */} + + + + + + {/* More Menu Pages */} + + + + + + + + + + + + + + + + + + + {/* Redirecting old URLs to new components */} + + + + + + + + + + + + + + + + {/* Voter Guide Pages - By Organization */} + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + } /> + } /> + } /> + + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + } /> + } /> + } /> + ()} /> + ()} /> + + {/* Voter Guide Settings go in this structure... "/vg/wvYYvgYY/settings/positions", "/vg/wvYYvgYY/settings/addpositions" */} + + + + + + + + + + + + {/* Confirming that person owns twitter handle */} + + + + + {/* Custom link. "/-/" is controlled by customer and tied to hostname, "/-" is generated by software */} + + + + {/* Temporary scratchpad for component testing */} + + + + + + + + + {/* view_mode not taken in yet */} + + {/* Any route that is not found -> @return TwitterHandleLanding component */} + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + {/* Next line handles: ":twitter_handle/btcand/:back_to_cand_we_vote_id/b/:back_to_variable" */} + + + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + } /> + } /> + } /> + + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + } /> + } /> + } /> + + + + + + {/* /> */} + + + + ); +}; export default routes; diff --git a/src/js/RootForReady.jsx b/src/js/RootForReady.jsx new file mode 100644 index 000000000..3f00c2f92 --- /dev/null +++ b/src/js/RootForReady.jsx @@ -0,0 +1,34 @@ +import React, { Suspense, lazy } from 'react'; +// import Loadable from 'react-loadable'; +// import { IndexRedirect, Route } from 'react-router-dom'; +import { Router, Route, Switch } from 'react-router-dom'; +// import ApplicationForReady from './ApplicationForReady'; +// import PageNotFound from './routes/PageNotFound'; +import GetReady from './routes/ReadyNoApi'; +// import Ballot from './routes/Ballot/Ballot'; +// const Ballot = React.lazy(() => import(/* webpackPrefetch: true, webpackChunkName: "Ballot" */ './routes/Ballot/Ballot')); +const Ballot = lazy(() => import(/* webpackPrefetch: true, webpackChunkName: "Ballot" */ './routes/Ballot/Ballot')); +// const AsyncBallotComponent = loadable( { +// loader: () => import( './home.component' ), +// loading: LoadingComponent +// } ); +// const Ballot = Loadable({ +// loader: () => import(/* webpackPrefetch: true, webpackChunkName: "Ballot" */'./routes/Ballot/Ballot'), +// loading: () =>
    Loading...
    , +// }); + +const routes = () => { // eslint-disable-line arrow-body-style + return ( + + + Loading...
    }> + + + + + {/* */} + + ); +}; + +export default routes; diff --git a/src/js/WeVoteRouter.jsx b/src/js/WeVoteRouter.jsx new file mode 100644 index 000000000..77e82dff9 --- /dev/null +++ b/src/js/WeVoteRouter.jsx @@ -0,0 +1,41 @@ +import { BrowserRouter } from 'react-router-dom'; +import webAppConfig from './config'; + +// https://stackoverflow.com/questions/34093913/how-to-debug-react-router +// When a history.push is called correctly for the v5 react-router, the incoming +// component receives a props.location like ... +// location: +// hash: "" +// key: "qcccs4" +// pathname: "/ballot" +// search: "" +// state: undefined +// And a props.history like... +// history: +// action: "PUSH" +// block: ƒ block(prompt) +// createHref: ƒ createHref(location) +// go: ƒ go(n) +// goBack: ƒ goBack() +// goForward: ƒ goForward() +// length: 39 +// listen: ƒ listen(listener) +// location: {pathname: "/ballot", search: "", hash: "", state: undefined, key: "qcccs4"} +// push: ƒ push(path, state) +// replace: ƒ replace(path, state) + +// Possible: https://stackoverflow.com/questions/59402649/how-can-i-use-history-pushpath-in-react-router-5-1-2-in-stateful-component +// Possible: https://stackoverflow.com/questions/63400050/refactoring-react-class-to-hooks-entity-update-component +export default class WeVoteRouter extends BrowserRouter { + constructor (props) { + super(props); + global.weVoteGlobalHistory = this.history; + if (webAppConfig.LOG_ROUTING) { + console.log('Router: initial history is: ', JSON.stringify(this.history, null, 2)); + this.history.listen((location, action) => { + console.log(`Router: The current URL is ${location.pathname}${location.search}${location.hash}`); + console.log(`Router: The last navigation action was ${action}`, JSON.stringify(this.history, null, 2)); + }); + } + } +} diff --git a/src/js/actions/ActivityActions.js b/src/js/actions/ActivityActions.js new file mode 100644 index 000000000..a33986da1 --- /dev/null +++ b/src/js/actions/ActivityActions.js @@ -0,0 +1,46 @@ +import Dispatcher from '../dispatcher/Dispatcher'; + +// Keep this comment as a cheat-sheet for the constants used API server +// Kind of Seeds +// NOTICE_FRIEND_ENDORSEMENTS_SEED +// +// Kind of Notices +// NOTICE_FRIEND_ENDORSEMENTS + +export default { + activityCommentSave (activityCommentWeVoteId = '', parentWeVoteId = '', statementText = null, visibilitySetting = 'FRIENDS_ONLY', parentCommentWeVoteId = '') { + // console.log('activityNoticeListRetrieve'); + Dispatcher.loadEndpoint('activityCommentSave', + { + activity_comment_we_vote_id: activityCommentWeVoteId, + parent_we_vote_id: parentWeVoteId, + parent_comment_we_vote_id: parentCommentWeVoteId, + statement_text: statementText, + visibility_setting: visibilitySetting, + }); + }, + activityListRetrieve (activityTidbitWeVoteIdList = []) { + // console.log('activityNoticeListRetrieve'); + Dispatcher.loadEndpoint('activityListRetrieve', + { + activity_tidbit_we_vote_id_list: activityTidbitWeVoteIdList, + }); + }, + activityNoticeListRetrieve (activityNoticeIdListClicked = [], activityNoticeIdListSeen = []) { + // console.log('activityNoticeListRetrieve'); + Dispatcher.loadEndpoint('activityNoticeListRetrieve', + { + activity_notice_id_list_clicked: activityNoticeIdListClicked, + activity_notice_id_list_seen: activityNoticeIdListSeen, + }); + }, + activityPostSave (activityPostWeVoteId = '', statementText = null, visibilitySetting = 'FRIENDS_ONLY') { + // console.log('activityNoticeListRetrieve'); + Dispatcher.loadEndpoint('activityPostSave', + { + activity_post_we_vote_id: activityPostWeVoteId, + statement_text: statementText, + visibility_setting: visibilitySetting, + }); + }, +}; diff --git a/src/js/actions/AnalyticsActions.js b/src/js/actions/AnalyticsActions.js new file mode 100644 index 000000000..e20ad8ea9 --- /dev/null +++ b/src/js/actions/AnalyticsActions.js @@ -0,0 +1,361 @@ +import Dispatcher from '../dispatcher/Dispatcher'; +import AppStore from '../stores/AppStore'; // eslint-disable-line import/no-cycle + +// Dec 2018: Keep this comment as a cheat-sheet for the enumerated values sent by the API +// ACTION_VOTER_GUIDE_VISIT = 1; +// ACTION_VOTER_GUIDE_ENTRY = 2; +// ACTION_ORGANIZATION_FOLLOW = 3; +// ACTION_ORGANIZATION_AUTO_FOLLOW = 4; +// ACTION_ISSUE_FOLLOW = 5; +// ACTION_BALLOT_VISIT = 6; +// ACTION_POSITION_TAKEN = 7; +// ACTION_VOTER_TWITTER_AUTH = 8; +// ACTION_VOTER_FACEBOOK_AUTH = 9; +// ACTION_WELCOME_ENTRY = 10; +// ACTION_FRIEND_ENTRY = 11; +// ACTION_WELCOME_VISIT = 12; +// ACTION_ORGANIZATION_FOLLOW_IGNORE = 13 +// ACTION_ORGANIZATION_STOP_FOLLOWING = 14 +// ACTION_ISSUE_FOLLOW_IGNORE = 15 +// ACTION_ISSUE_STOP_FOLLOWING = 16 +// ACTION_MODAL_ISSUES = 17 +// ACTION_MODAL_ORGANIZATIONS = 18 +// ACTION_MODAL_POSITIONS = 19 +// ACTION_MODAL_FRIENDS = 20 +// ACTION_MODAL_SHARE = 21 +// ACTION_MODAL_VOTE = 22 +// ACTION_NETWORK = 23 +// ACTION_FACEBOOK_INVITABLE_FRIENDS = 24 +// ACTION_DONATE_VISIT = 25 +// ACTION_ACCOUNT_PAGE = 26 +// ACTION_INVITE_BY_EMAIL = 27 +// ACTION_ABOUT_GETTING_STARTED = 28 +// ACTION_ABOUT_VISION = 29 +// ACTION_ABOUT_ORGANIZATION = 30 +// ACTION_ABOUT_TEAM = 31 +// ACTION_ABOUT_MOBILE = 32 +// ACTION_OFFICE = 33 +// ACTION_CANDIDATE = 34 +// ACTION_FACEBOOK_AUTHENTICATION_EXISTS = 36 +// ACTION_GOOGLE_AUTHENTICATION_EXISTS = 37 +// ACTION_TWITTER_AUTHENTICATION_EXISTS = 38 +// ACTION_EMAIL_AUTHENTICATION_EXISTS = 39 +// ACTION_ELECTIONS = 40 +// ACTION_ORGANIZATION_STOP_IGNORING = 41 +// ACTION_MODAL_VOTER_PLAN = 42 +// ACTION_READY_VISIT = 43 +// ACTION_SELECT_BALLOT_MODAL = 44 +// ACTION_SHARE_BUTTON_COPY = 45 +// ACTION_SHARE_BUTTON_EMAIL = 46 +// ACTION_SHARE_BUTTON_FACEBOOK = 47 +// ACTION_SHARE_BUTTON_FRIENDS = 48 +// ACTION_SHARE_BUTTON_TWITTER = 49 +// ACTION_SHARE_BALLOT = 50 +// ACTION_SHARE_BALLOT_ALL_OPINIONS = 51 +// ACTION_SHARE_CANDIDATE = 52 +// ACTION_SHARE_CANDIDATE_ALL_OPINIONS = 53 +// ACTION_SHARE_MEASURE = 54 +// ACTION_SHARE_MEASURE_ALL_OPINIONS = 55 +// ACTION_SHARE_OFFICE = 56 +// ACTION_SHARE_OFFICE_ALL_OPINIONS = 57 +// ACTION_SHARE_READY = 58 +// ACTION_SHARE_READY_ALL_OPINIONS = 59 +// ACTION_VIEW_SHARED_BALLOT = 60 +// ACTION_VIEW_SHARED_BALLOT_ALL_OPINIONS = 61 +// ACTION_VIEW_SHARED_CANDIDATE = 62 +// ACTION_VIEW_SHARED_CANDIDATE_ALL_OPINIONS = 63 +// ACTION_VIEW_SHARED_MEASURE = 64 +// ACTION_VIEW_SHARED_MEASURE_ALL_OPINIONS = 65 +// ACTION_VIEW_SHARED_OFFICE = 66 +// ACTION_VIEW_SHARED_OFFICE_ALL_OPINIONS = 67 +// ACTION_VIEW_SHARED_READY = 68 +// ACTION_VIEW_SHARED_READY_ALL_OPINIONS = 69 +// ACTION_SEARCH_OPINIONS = 70 +// ACTION_UNSUBSCRIBE_EMAIL_PAGE = 71 +// ACTION_UNSUBSCRIBE_SMS_PAGE = 72 +// ACTION_MEASURE = 73 +// ACTION_NEWS = 74 +// ACTION_SHARE_ORGANIZATION = 75 +// ACTION_SHARE_ORGANIZATION_ALL_OPINIONS = 76 +// ACTION_VIEW_SHARED_ORGANIZATION = 77 +// ACTION_VIEW_SHARED_ORGANIZATION_ALL_OPINIONS = 78 + +export default { + + saveActionWrapper (actionConstant, googleCivicElectionId = '') { + Dispatcher.loadEndpoint('saveAnalyticsAction', + { + action_constant: actionConstant, + google_civic_election_id: googleCivicElectionId, + }); + }, + + saveActionWrapperWithOrganization (actionConstant, googleCivicElectionId, organizationWeVoteId) { + Dispatcher.loadEndpoint('saveAnalyticsAction', + { + action_constant: actionConstant, + google_civic_election_id: googleCivicElectionId, + organization_we_vote_id: organizationWeVoteId, + }); + }, + + saveActionWrapperWithBallotItem (actionConstant, googleCivicElectionId, ballotItemWeVoteId) { + Dispatcher.loadEndpoint('saveAnalyticsAction', + { + action_constant: actionConstant, + google_civic_election_id: googleCivicElectionId, + ballot_item_we_vote_id: ballotItemWeVoteId, + }); + }, + + saveActionAboutGettingStarted (googleCivicElectionId) { + const actionConstant = 28; // ACTION_ABOUT_GETTING_STARTED + this.saveActionWrapper(actionConstant, googleCivicElectionId); + }, + + saveActionAboutVision (googleCivicElectionId) { + const actionConstant = 29; // ACTION_ABOUT_VISION + this.saveActionWrapper(actionConstant, googleCivicElectionId); + }, + + saveActionAboutOrganization (googleCivicElectionId) { + const actionConstant = 30; // ACTION_ABOUT_ORGANIZATION + this.saveActionWrapper(actionConstant, googleCivicElectionId); + }, + + saveActionAboutTeam (googleCivicElectionId) { + const actionConstant = 31; // ACTION_ABOUT_TEAM + this.saveActionWrapper(actionConstant, googleCivicElectionId); + }, + + saveActionAboutMobile (googleCivicElectionId) { + const actionConstant = 32; // ACTION_ABOUT_MOBILE + this.saveActionWrapper(actionConstant, googleCivicElectionId); + }, + + saveActionAccountPage (googleCivicElectionId) { + const actionConstant = 26; // ACTION_ACCOUNT_PAGE + this.saveActionWrapper(actionConstant, googleCivicElectionId); + }, + + saveActionBallotVisit (googleCivicElectionId) { + const actionConstant = 6; // ACTION_BALLOT_VISIT + const siteOwnerOrganizationWeVoteId = AppStore.getSiteOwnerOrganizationWeVoteId(); + if (siteOwnerOrganizationWeVoteId) { + this.saveActionWrapperWithOrganization(actionConstant, googleCivicElectionId, siteOwnerOrganizationWeVoteId); + } else { + this.saveActionWrapper(actionConstant, googleCivicElectionId); + } + }, + + saveActionCandidate (googleCivicElectionId, ballotItemWeVoteId) { + const actionConstant = 34; // ACTION_CANDIDATE + this.saveActionWrapperWithBallotItem(actionConstant, googleCivicElectionId, ballotItemWeVoteId); + }, + + saveActionDonateVisit (googleCivicElectionId) { + const actionConstant = 25; // ACTION_DONATE_VISIT + this.saveActionWrapper(actionConstant, googleCivicElectionId); + }, + + saveActionElections (googleCivicElectionId) { + const actionConstant = 40; // ACTION_ELECTIONS + this.saveActionWrapper(actionConstant, googleCivicElectionId); + }, + + saveActionFacebookInvitableFriends (googleCivicElectionId) { + const actionConstant = 24; // ACTION_FACEBOOK_INVITABLE_FRIENDS + this.saveActionWrapper(actionConstant, googleCivicElectionId); + }, + + saveActionInviteByEmail (googleCivicElectionId) { + const actionConstant = 27; // ACTION_INVITE_BY_EMAIL + this.saveActionWrapper(actionConstant, googleCivicElectionId); + }, + + saveActionMeasure (googleCivicElectionId, ballotItemWeVoteId) { + const actionConstant = 73; // ACTION_MEASURE + this.saveActionWrapperWithBallotItem(actionConstant, googleCivicElectionId, ballotItemWeVoteId); + }, + + saveActionModalIssues (googleCivicElectionId) { + const actionConstant = 17; // ACTION_MODAL_ISSUES + this.saveActionWrapper(actionConstant, googleCivicElectionId); + }, + + saveActionModalOrganizations (googleCivicElectionId) { + const actionConstant = 18; // ACTION_MODAL_ORGANIZATIONS + this.saveActionWrapper(actionConstant, googleCivicElectionId); + }, + + saveActionModalPositions (googleCivicElectionId) { + const actionConstant = 19; // ACTION_MODAL_POSITIONS + this.saveActionWrapper(actionConstant, googleCivicElectionId); + }, + + saveActionModalFriends (googleCivicElectionId) { + const actionConstant = 20; // ACTION_MODAL_FRIENDS + this.saveActionWrapper(actionConstant, googleCivicElectionId); + }, + + saveActionModalShare (googleCivicElectionId) { + const actionConstant = 21; // ACTION_MODAL_SHARE + this.saveActionWrapper(actionConstant, googleCivicElectionId); + }, + + saveActionModalVote (googleCivicElectionId) { + const actionConstant = 22; // ACTION_MODAL_VOTE + this.saveActionWrapper(actionConstant, googleCivicElectionId); + }, + + saveActionModalVoterPlan (googleCivicElectionId) { + const actionConstant = 42; // ACTION_MODAL_VOTER_PLAN + this.saveActionWrapper(actionConstant, googleCivicElectionId); + }, + + saveActionNetwork (googleCivicElectionId) { + const actionConstant = 23; // ACTION_NETWORK + this.saveActionWrapper(actionConstant, googleCivicElectionId); + }, + + saveActionNews (googleCivicElectionId) { + const actionConstant = 74; // ACTION_NEWS + this.saveActionWrapper(actionConstant, googleCivicElectionId); + }, + + saveActionOffice (googleCivicElectionId, ballotItemWeVoteId) { + const actionConstant = 33; // ACTION_OFFICE + this.saveActionWrapperWithBallotItem(actionConstant, googleCivicElectionId, ballotItemWeVoteId); + }, + + saveActionReadyVisit (googleCivicElectionId) { + const actionConstant = 43; // ACTION_READY_VISIT + this.saveActionWrapper(actionConstant, googleCivicElectionId); + }, + + saveActionSearchOpinions (googleCivicElectionId = '') { + const actionConstant = 70; // ACTION_SEARCH_OPINIONS + this.saveActionWrapper(actionConstant, googleCivicElectionId); + }, + + saveActionSelectBallotModal (googleCivicElectionId) { + const actionConstant = 44; // ACTION_SELECT_BALLOT_MODAL + this.saveActionWrapper(actionConstant, googleCivicElectionId); + }, + + saveActionShareBallot (googleCivicElectionId) { + const actionConstant = 50; // ACTION_SHARE_BALLOT + this.saveActionWrapper(actionConstant, googleCivicElectionId); + }, + + saveActionShareBallotAllOpinions (googleCivicElectionId) { + const actionConstant = 51; // ACTION_SHARE_BALLOT_ALL_OPINIONS + this.saveActionWrapper(actionConstant, googleCivicElectionId); + }, + + saveActionShareButtonCopy (googleCivicElectionId) { + const actionConstant = 45; // ACTION_SHARE_BUTTON_COPY + this.saveActionWrapper(actionConstant, googleCivicElectionId); + }, + + saveActionShareButtonEmail (googleCivicElectionId) { + const actionConstant = 46; // ACTION_SHARE_BUTTON_EMAIL + this.saveActionWrapper(actionConstant, googleCivicElectionId); + }, + + saveActionShareButtonFacebook (googleCivicElectionId) { + const actionConstant = 47; // ACTION_SHARE_BUTTON_FACEBOOK + this.saveActionWrapper(actionConstant, googleCivicElectionId); + }, + + saveActionShareButtonFriends (googleCivicElectionId) { + const actionConstant = 48; // ACTION_SHARE_BUTTON_FRIENDS + this.saveActionWrapper(actionConstant, googleCivicElectionId); + }, + + saveActionShareButtonTwitter (googleCivicElectionId) { + const actionConstant = 49; // ACTION_SHARE_BUTTON_TWITTER + this.saveActionWrapper(actionConstant, googleCivicElectionId); + }, + + saveActionShareCandidate (googleCivicElectionId) { + const actionConstant = 52; // ACTION_SHARE_CANDIDATE + this.saveActionWrapper(actionConstant, googleCivicElectionId); + }, + + saveActionShareCandidateAllOpinions (googleCivicElectionId) { + const actionConstant = 53; // ACTION_SHARE_CANDIDATE_ALL_OPINIONS + this.saveActionWrapper(actionConstant, googleCivicElectionId); + }, + + saveActionShareMeasure (googleCivicElectionId) { + const actionConstant = 54; // ACTION_SHARE_MEASURE + this.saveActionWrapper(actionConstant, googleCivicElectionId); + }, + + saveActionShareMeasureAllOpinions (googleCivicElectionId) { + const actionConstant = 55; // ACTION_SHARE_MEASURE_ALL_OPINIONS + this.saveActionWrapper(actionConstant, googleCivicElectionId); + }, + + saveActionShareOffice (googleCivicElectionId) { + const actionConstant = 56; // ACTION_SHARE_OFFICE + this.saveActionWrapper(actionConstant, googleCivicElectionId); + }, + + saveActionShareOfficeAllOpinions (googleCivicElectionId) { + const actionConstant = 57; // ACTION_SHARE_OFFICE_ALL_OPINIONS + this.saveActionWrapper(actionConstant, googleCivicElectionId); + }, + + saveActionShareOrganization (googleCivicElectionId) { + const actionConstant = 75; // ACTION_SHARE_ORGANIZATION + this.saveActionWrapper(actionConstant, googleCivicElectionId); + }, + + saveActionShareOrganizationAllOpinions (googleCivicElectionId) { + const actionConstant = 76; // ACTION_SHARE_ORGANIZATION_ALL_OPINIONS + this.saveActionWrapper(actionConstant, googleCivicElectionId); + }, + + saveActionShareReady (googleCivicElectionId) { + const actionConstant = 58; // ACTION_SHARE_READY + this.saveActionWrapper(actionConstant, googleCivicElectionId); + }, + + saveActionShareReadyAllOpinions (googleCivicElectionId) { + const actionConstant = 59; // ACTION_SHARE_READY_ALL_OPINIONS + this.saveActionWrapper(actionConstant, googleCivicElectionId); + }, + + saveActionUnsubscribeEmailPage (googleCivicElectionId) { + const actionConstant = 71; // ACTION_UNSUBSCRIBE_EMAIL_PAGE + this.saveActionWrapper(actionConstant, googleCivicElectionId); + }, + + saveActionUnsubscribeSmsPage (googleCivicElectionId) { + const actionConstant = 72; // ACTION_UNSUBSCRIBE_SMS_PAGE + this.saveActionWrapper(actionConstant, googleCivicElectionId); + }, + + saveActionVoterGuideAutoFollow (organizationWeVoteId, googleCivicElectionId) { + const actionConstant = 4; // ACTION_ORGANIZATION_AUTO_FOLLOW + this.saveActionWrapperWithOrganization(actionConstant, googleCivicElectionId, organizationWeVoteId); + }, + + saveActionVoterGuideGetStarted (organizationWeVoteId, googleCivicElectionId) { + const actionConstant = 35; // ACTION_VOTER_GUIDE_GET_STARTED + this.saveActionWrapper(actionConstant, googleCivicElectionId); + }, + + saveActionVoterGuideVisit (organizationWeVoteId, googleCivicElectionId) { + const actionConstant = 1; // ACTION_VOTER_GUIDE_VISIT + this.saveActionWrapperWithOrganization(actionConstant, googleCivicElectionId, organizationWeVoteId); + }, + + saveActionWelcomeVisit (googleCivicElectionId) { + const actionConstant = 12; // ACTION_WELCOME_VISIT + this.saveActionWrapper(actionConstant, googleCivicElectionId); + }, + +}; diff --git a/src/js/actions/AppActions.js b/src/js/actions/AppActions.js new file mode 100644 index 000000000..012b475c8 --- /dev/null +++ b/src/js/actions/AppActions.js @@ -0,0 +1,126 @@ +import Dispatcher from '../dispatcher/AppDispatcher'; + +export default { + setActivityTidbitWeVoteIdForDrawer (activityTidbitWeVoteId) { + Dispatcher.dispatch({ type: 'activityTidbitWeVoteIdForDrawer', payload: activityTidbitWeVoteId }); + }, + + setActivityTidbitWeVoteIdForDrawerAndOpen (activityTidbitWeVoteId) { + Dispatcher.dispatch({ type: 'activityTidbitWeVoteIdForDrawerAndOpen', payload: activityTidbitWeVoteId }); + }, + + setGetStartedMode (getStartedMode) { + Dispatcher.dispatch({ type: 'getStartedMode', payload: getStartedMode }); + }, + + setOrganizationModalBallotItemWeVoteId (ballotItemWeVoteId) { + Dispatcher.dispatch({ type: 'organizationModalBallotItemWeVoteId', payload: ballotItemWeVoteId }); + }, + + setVoterGuideSettingsDashboardEditMode (getVoterGuideSettingsDashboardEditMode) { + Dispatcher.dispatch({ type: 'getVoterGuideSettingsDashboardEditMode', payload: getVoterGuideSettingsDashboardEditMode }); + }, + + setScrolled (scrolledDown) { + Dispatcher.dispatch({ type: 'scrolledDown', payload: scrolledDown }); + }, + + setShareModalStep (step) { + // console.log('setShareModalStep, step:', step); + Dispatcher.dispatch({ type: 'shareModalStep', payload: step }); + }, + + setShowActivityTidbitDrawer (show) { + Dispatcher.dispatch({ type: 'showActivityTidbitDrawer', payload: show }); + }, + + setShowAdviserIntroModal (show) { + Dispatcher.dispatch({ type: 'showAdviserIntroModal', payload: show }); + }, + + setShowEditAddressButton (show) { + Dispatcher.dispatch({ type: 'showEditAddressButton', payload: show }); + }, + + setShowFirstPositionIntroModal (show) { + Dispatcher.dispatch({ type: 'showFirstPositionIntroModal', payload: show }); + }, + + setShowHowItWorksModal (show) { + // The chosenPaidAccount values are: free, professional, enterprise + Dispatcher.dispatch({ type: 'showHowItWorksModal', payload: show }); + }, + + setShowVoterPlanModal (show) { + // The chosenPaidAccount values are: free, professional, enterprise + Dispatcher.dispatch({ type: 'showVoterPlanModal', payload: show }); + }, + + setShowNewVoterGuideModal (show) { + Dispatcher.dispatch({ type: 'showNewVoterGuideModal', payload: show }); + }, + + setShowElectionsWithOrganizationVoterGuidesModal (show) { + Dispatcher.dispatch({ type: 'showElectionsWithOrganizationVoterGuidesModal', payload: show }); + }, + + setShowPaidAccountUpgradeModal (chosenPaidAccount) { + // The chosenPaidAccount values are: free, professional, enterprise + Dispatcher.dispatch({ type: 'showPaidAccountUpgradeModal', payload: chosenPaidAccount }); + }, + + setShowPersonalizedScoreIntroModal (show) { + Dispatcher.dispatch({ type: 'showPersonalizedScoreIntroModal', payload: show }); + }, + + setShowSelectBallotModal (showSelectBallotModal, showSelectBallotModalHideAddress = false, showSelectBallotModalHideElections = false) { + Dispatcher.dispatch({ type: 'showSelectBallotModal', showSelectBallotModal, showSelectBallotModalHideAddress, showSelectBallotModalHideElections }); + }, + + setShowShareModal (show) { + // The chosenPaidAccount values are: free, professional, enterprise + Dispatcher.dispatch({ type: 'showShareModal', payload: show }); + }, + + setShowSharedItemModal (sharedItemCode) { + Dispatcher.dispatch({ type: 'showSharedItemModal', payload: sharedItemCode }); + }, + + setShowSignInModal (show) { + Dispatcher.dispatch({ type: 'showSignInModal', payload: show }); + }, + + setShowOrganizationModal (show) { + // console.log("Setting organizationModal to ", show); + Dispatcher.dispatch({ type: 'showOrganizationModal', payload: show }); + }, + + setShowValuesIntroModal (show) { + Dispatcher.dispatch({ type: 'showValuesIntroModal', payload: show }); + }, + + setShowImageUploadModal (show) { + console.log('Setting image upload modal to open!'); + Dispatcher.dispatch({ type: 'showImageUploadModal', payload: show }); + }, + + setViewingOrganizationVoterGuide (isViewing) { + Dispatcher.dispatch({ type: 'viewingOrganizationVoterGuide', payload: isViewing }); + }, + + siteConfigurationRetrieve (hostname, refresh_string = '') { + Dispatcher.loadEndpoint('siteConfigurationRetrieve', + { + hostname, + refresh_string, + }); + }, + + storeSignInStartFullUrl () { + Dispatcher.dispatch({ type: 'storeSignInStartFullUrl', payload: true }); + }, + + unsetStoreSignInStartFullUrl () { + Dispatcher.dispatch({ type: 'unsetStoreSignInStartFullUrl', payload: false }); + }, +}; diff --git a/src/js/actions/BallotActions.js b/src/js/actions/BallotActions.js index 4f28afd4d..acbaacf83 100644 --- a/src/js/actions/BallotActions.js +++ b/src/js/actions/BallotActions.js @@ -1,9 +1,75 @@ -import Dispatcher from "../dispatcher/Dispatcher"; +import Dispatcher from '../dispatcher/Dispatcher'; -module.exports = { +export default { + allBallotItemsRetrieve (googleCivicElectionId, stateCode = '') { + // This API is always retrieved from our CDN per: WebApp/src/js/utils/service.js + Dispatcher.loadEndpoint('allBallotItemsRetrieve', { + google_civic_election_id: googleCivicElectionId, + state_code: stateCode, + }); + }, - retrieve: function () { - Dispatcher.loadEndpoint("voterBallotItemsRetrieve", { use_test_election: false }); - } + allBallotItemsRetrieveCalled () { + Dispatcher.dispatch({ type: 'allBallotItemsRetrieveCalled', payload: true }); + }, + ballotItemOptionsClear () { + Dispatcher.dispatch({ + type: 'ballotItemOptionsClear', + res: { + success: true, + }, + }); + }, + + ballotItemOptionsRetrieve (googleCivicElectionId = 0, search_string = '', state_code = '') { + Dispatcher.loadEndpoint('ballotItemOptionsRetrieve', { + google_civic_election_id: googleCivicElectionId, + search_string, + state_code, + }); + }, + + completionLevelFilterTypeSave (completionLevelFilterType = '') { + Dispatcher.dispatch({ + type: 'completionLevelFilterTypeSave', + res: { + completion_level_filter_type_saved: completionLevelFilterType, + success: true, + }, + }); + }, + + raceLevelFilterTypeSave (raceLevelFilterType = '') { + Dispatcher.dispatch({ + type: 'raceLevelFilterTypeSave', + res: { + race_level_filter_type_saved: raceLevelFilterType, + success: true, + }, + }); + }, + + voterBallotItemsRetrieve (googleCivicElectionId = 0, ballot_returned_we_vote_id = '', ballot_location_shortcut = '') { + Dispatcher.loadEndpoint('voterBallotItemsRetrieve', { + use_test_election: false, + google_civic_election_id: googleCivicElectionId, + ballot_returned_we_vote_id, + ballot_location_shortcut, + }); + }, + + voterBallotListRetrieve () { + Dispatcher.loadEndpoint('voterBallotListRetrieve'); + }, + + voterBallotItemOpenOrClosedSave: (ballotItemUnfurledTracker) => { + Dispatcher.dispatch({ + type: 'voterBallotItemOpenOrClosedSave', + res: { + ballot_item_unfurled_tracker: ballotItemUnfurledTracker, + success: true, + }, + }); + }, }; diff --git a/src/js/actions/CandidateActions.js b/src/js/actions/CandidateActions.js index 2acd563a1..80e2cdec1 100644 --- a/src/js/actions/CandidateActions.js +++ b/src/js/actions/CandidateActions.js @@ -1,8 +1,44 @@ -import Dispatcher from "../dispatcher/Dispatcher"; +import Dispatcher from '../dispatcher/Dispatcher'; -module.exports = { - retrieve: function (we_vote_id) { - Dispatcher.loadEndpoint("candidateRetrieve", { candidate_we_vote_id: we_vote_id} ); - Dispatcher.loadEndpoint("positionListForBallotItem", { ballot_item_we_vote_id: we_vote_id, kind_of_ballot_item: "CANDIDATE"} ); - } +export default { + candidateRetrieve (candidateWeVoteId) { + Dispatcher.loadEndpoint('candidateRetrieve', + { + candidate_we_vote_id: candidateWeVoteId, + }); + }, + + candidatesRetrieve (officeWeVoteId) { + Dispatcher.loadEndpoint('candidatesRetrieve', + { + office_we_vote_id: officeWeVoteId, + }); + }, + + positionListForBallotItemPublic (ballotItemWeVoteId) { + // This API is always retrieved from our CDN per: WebApp/src/js/utils/service.js + Dispatcher.loadEndpoint('positionListForBallotItem', + { + ballot_item_we_vote_id: ballotItemWeVoteId, + kind_of_ballot_item: 'CANDIDATE', + }); + }, + + positionListForBallotItemPrivateIndividualsOnly (ballotItemWeVoteId) { + // This API is always retrieved from our CDN per: WebApp/src/js/utils/service.js + Dispatcher.loadEndpoint('positionListForBallotItem', + { + ballot_item_we_vote_id: ballotItemWeVoteId, + kind_of_ballot_item: 'CANDIDATE', + private_citizens_only: true, + }); + }, + + positionListForBallotItemFromFriends (ballotItemWeVoteId) { + Dispatcher.loadEndpoint('positionListForBallotItemFromFriends', + { + ballot_item_we_vote_id: ballotItemWeVoteId, + kind_of_ballot_item: 'CANDIDATE', + }); + }, }; diff --git a/src/js/actions/DonateActions.js b/src/js/actions/DonateActions.js new file mode 100644 index 000000000..af57f74de --- /dev/null +++ b/src/js/actions/DonateActions.js @@ -0,0 +1,60 @@ +import Dispatcher from '../dispatcher/Dispatcher'; + + +export default { + couponSummaryRetrieve (couponCode) { + Dispatcher.loadEndpoint('couponSummaryRetrieve', { + coupon_code: couponCode, + }); + }, + + defaultPricing () { + Dispatcher.loadEndpoint('defaultPricing', {}); + }, + + donationCancelSubscriptionAction (subscriptionId, planTypeEnum = '') { + Dispatcher.loadEndpoint('donationCancelSubscription', + { + plan_type_enum: planTypeEnum, + subscription_id: subscriptionId, + }); + }, + + donationRefund (charge) { + Dispatcher.loadEndpoint('donationRefund', { charge }); + }, + + donationRefreshDonationList () { + Dispatcher.loadEndpoint('donationHistory', + { + }); + }, + + donationWithStripe (token, email, donationAmount, monthlyDonation, isOrganizationPlan, planType, couponCode) { + Dispatcher.loadEndpoint('donationWithStripe', { + token, + email, + donation_amount: donationAmount, + monthly_donation: monthlyDonation, + is_organization_plan: isOrganizationPlan, + plan_type_enum: planType, + coupon_code: couponCode, + }); + }, + + setLatestCouponViewed (latestCouponViewed) { + Dispatcher.dispatch({ type: 'latestCouponViewed', payload: latestCouponViewed }); + }, + + doesOrgHavePaidPlan () { + // DALE 2019-09-19 Migrate away from this -- donationHistory provides what we need + Dispatcher.loadEndpoint('doesOrgHavePaidPlan', {}); + }, + + validateCoupon (planType, couponCode) { + Dispatcher.loadEndpoint('validateCoupon', { + plan_type_enum: planType, + coupon_code: couponCode, + }); + }, +}; diff --git a/src/js/actions/ElectionActions.js b/src/js/actions/ElectionActions.js new file mode 100644 index 000000000..013b29bf0 --- /dev/null +++ b/src/js/actions/ElectionActions.js @@ -0,0 +1,8 @@ +import Dispatcher from '../dispatcher/Dispatcher'; + +export default { + + electionsRetrieve () { + Dispatcher.loadEndpoint('electionsRetrieve', {}); + }, +}; diff --git a/src/js/actions/FacebookActionCreators.js b/src/js/actions/FacebookActionCreators.js deleted file mode 100644 index 972e4364b..000000000 --- a/src/js/actions/FacebookActionCreators.js +++ /dev/null @@ -1,94 +0,0 @@ -const web_app_config = require("../config"); -import FacebookDispatcher from "../dispatcher/FacebookDispatcher"; -import FacebookConstants from "../constants/FacebookConstants"; - -const FacebookActionCreators = { - initFacebook: function () { - window.fbAsyncInit = function () { - FB.init({ - appId: web_app_config.FACEBOOK_APP_ID, - xfbml: true, - version: "v2.5" - }); - - // after initialization, get the login status - FacebookActionCreators.getLoginStatus(); - }, - (function (d, s, id){ - var js, fjs = d.getElementsByTagName(s)[0]; - if (d.getElementById(id)) {return;} - js = d.createElement(s); js.id = id; - js.src = "//connect.facebook.net/en_US/sdk.js"; - fjs.parentNode.insertBefore(js, fjs); - }(document, "script", "facebook-jssdk")); - }, - - getLoginStatus: function () { - window.FB.getLoginStatus((response) => { - FacebookDispatcher.dispatch({ - actionType: FacebookConstants.FACEBOOK_INITIALIZED, - data: response - }); - }); - }, - - login: () => { - try { - window.FB.login((response) => { - if (response.status === "connected") { - // Logged into We Vote and Facebook - FacebookDispatcher.dispatch({ - actionType: FacebookConstants.FACEBOOK_LOGGED_IN, - data: response - }); - } else if (response.status === "not_authorized") { - // The person is logged into Facebook, but not We Vote - } else { - // The person is not logged into Facebook - } - }); - } catch (e) { - // If FB already logged in, carry on - } - }, - - logout: () => { - window.FB.logout((response) => { - FacebookDispatcher.dispatch({ - actionType: FacebookConstants.FACEBOOK_LOGGED_OUT, - data: response - }); - }); - }, - // Dale considering the need for this here - //connectWithFacebook: () => { - // // Add connection between We Vote and Facebook - // FacebookDispatcher.dispatch({ - // actionType: FacebookConstants.FACEBOOK_SIGN_IN_CONNECT, - // data: true - // }); - //}, - - disconnectFromFacebook: () => { - // Removing connection between We Vote and Facebook - FacebookDispatcher.dispatch({ - actionType: FacebookConstants.FACEBOOK_SIGN_IN_DISCONNECT, - data: true - }); - }, - - getFacebookProfilePicture: (userId) => { - FacebookDispatcher.dispatch({ - actionType: FacebookConstants.FACEBOOK_GETTING_PICTURE, - data: null - }); - window.FB.api(`/${userId}/picture?type=large`, (response) => { - FacebookDispatcher.dispatch({ - actionType: FacebookConstants.FACEBOOK_RECEIVED_PICTURE, - data: response - }); - }); - } -}; - -module.exports = FacebookActionCreators; diff --git a/src/js/actions/FacebookActions.js b/src/js/actions/FacebookActions.js new file mode 100644 index 000000000..3021fcab5 --- /dev/null +++ b/src/js/actions/FacebookActions.js @@ -0,0 +1,346 @@ +import { isWebApp } from '../utils/cordovaUtils'; // eslint-disable-line import/no-cycle +import Dispatcher from '../dispatcher/Dispatcher'; +import FacebookConstants from '../constants/FacebookConstants'; +import FriendActions from './FriendActions'; // eslint-disable-line import/no-cycle +import { oAuthLog } from '../utils/logging'; +import signInModalGlobalState from '../components/Widgets/signInModalGlobalState'; +import VoterActions from './VoterActions'; // eslint-disable-line import/no-cycle +import VoterSessionActions from './VoterSessionActions'; +import webAppConfig from '../config'; + +// Including FacebookStore causes problems in the WebApp, and again in the Native App + +/* +For the WebApp, see initFacebook() in Application.jsx +For Cordova we rely on the FacebookConnectPlugin4 + from https://www.npmjs.com/package/cordova-plugin-facebook4 +this is the "Jeduan" fork from https://github.com/jeduan/cordova-plugin-facebook4 +The "Jeduan" fork is forked from the VERY OUT OF DATE https://github.com/Wizcorp/phonegap-facebook-plugin +As of May 2018, the "Wizcorp" fork has not been maintained for 3 years, even though it +displays the (WRONG) note "This is the official plugin for Facebook in Apache Cordova/PhoneGap!" + */ + +export default { + facebookApi () { + return isWebApp() ? window.FB : window.facebookConnectPlugin; // eslint-disable-line no-undef + }, + + appLogout () { + signInModalGlobalState.set('waitingForFacebookApiCompletion', false); + VoterSessionActions.voterSignOut(); // This deletes the device_id cookie + VoterActions.voterRetrieve(); + VoterActions.voterEmailAddressRetrieve(); + VoterActions.voterSMSPhoneNumberRetrieve(); + }, + + disconnectFromFacebook () { + // Removing connection between We Vote and Facebook + Dispatcher.dispatch({ + type: FacebookConstants.FACEBOOK_SIGN_IN_DISCONNECT, + data: true, + }); + }, + + facebookDisconnect () { + Dispatcher.loadEndpoint('facebookDisconnect'); + }, + + // Sept 2017, We now use the Facebook "games" api "invitable_friends" data on the fly from the webapp for the "Choose Friends" feature. + // We use the more limited "friends" api call from the server to find Facebook profiles of friends already using We Vote. + facebookFriendsAction () { + Dispatcher.loadEndpoint('facebookFriendsAction', {}); + FriendActions.suggestedFriendList(); + }, + + // https://developers.facebook.com/docs/graph-api/reference/v2.6/user + getFacebookData () { + if (!webAppConfig.ENABLE_FACEBOOK) { + console.log('FacebookActions.getFacebookData was not invoked, see ENABLE_FACEBOOK in config.js'); + return; + } + // console.log('FacebookActions.getFacebookData invocation'); + if (this.facebookApi()) { + this.facebookApi().api( + '/me?fields=id,email,first_name,middle_name,last_name,cover', (response) => { + // console.log('FacebookActions.getFacebookData response ', response); + oAuthLog('getFacebookData response', response); + Dispatcher.dispatch({ + type: FacebookConstants.FACEBOOK_RECEIVED_DATA, + data: response, + }); + }, + ); + } else { + console.log('FacebookActions.getFacebookProfilePicture was not invoked, this.facebookApi() undefined'); + } + }, + + // Save incoming data from Facebook + // For offsets, see https://developers.facebook.com/docs/graph-api/reference/cover-photo/ + voterFacebookSignInData (data) { + /** + * Save incoming data from Facebook + * For offsets, see https://developers.facebook.com/docs/graph-api/reference/cover-photo/ + * @param data + * @param data.cover.offset_x + * @param data.cover.offset_y + */ + // console.log("FacebookActions voterFacebookSignInData, data:", data); + let background = false; + let offsetX = false; + let offsetY = false; + if (data.cover && data.cover.source) { + background = data.cover.source; + offsetX = data.cover.offset_x; // zero is a valid value so can't use the short-circuit operation " || false" + offsetY = data.cover.offset_y; // zero is a valid value so can't use the short-circuit operation " || false" + } + + Dispatcher.loadEndpoint('voterFacebookSignInSave', { + facebook_user_id: data.id || false, + facebook_email: data.email || false, + facebook_first_name: data.first_name || false, + facebook_middle_name: data.middle_name || false, + facebook_last_name: data.last_name || false, + facebook_profile_image_url_https: data.url || false, + facebook_background_image_url_https: background, + facebook_background_image_offset_x: offsetX, + facebook_background_image_offset_y: offsetY, + save_auth_data: false, + save_profile_data: true, + }); + }, + + getPicture () { + this.facebookApi().api( + '/me?fields=picture.type(large)&redirect=false', ['public_profile', 'email'], + (response) => { + oAuthLog('getFacebookProfilePicture response', response); + Dispatcher.dispatch({ + type: FacebookConstants.FACEBOOK_RECEIVED_PICTURE, + data: response, + }); + }, + ); + }, + + getFacebookProfilePicture () { + if (!webAppConfig.ENABLE_FACEBOOK) { + console.log('FacebookActions.getFacebookProfilePicture was not invoked, see ENABLE_FACEBOOK in config.js'); + return; + } + oAuthLog('getFacebookProfilePicture before fields request'); + + // Our "signed_in_facebook" field in the postgres database does not mean the user is actually signed in to facebook, it + // means that at some point in the past, the voter has logged into facebook and they *might* still be logged into facebook, + // but regardless of whether they are actually logged into facebook at this moment, we consider them "logged in to WeVote" + // having using facebook auth in the past. That is ok for our authentication methodology, but if you assume you are really + // logged into facebook in Cordova, and your're not, you get a distracting login dialog that comes from the facebook native + // package everytime we refresh the avatar in the header in this function -- so first check if the voter is really logged in. + if (this.facebookApi()) { + this.getPicture(); + } else { + console.log('FacebookActions.getFacebookProfilePicture was not invoked, this.facebookApi() undefined'); + } + }, + + getFacebookInvitableFriendsList (pictureWidth, pictureHeight) { + const pictureWidthVerified = pictureWidth || 50; + const pictureHeightVerified = pictureHeight || 50; + if (!webAppConfig.ENABLE_FACEBOOK) { + console.log('FacebookActions.getFacebookInvitableFriendsList was not invoked, see ENABLE_FACEBOOK in config.js'); + return; + } + + if (this.facebookApi()) { + const fbApiForInvitableFriends = `/me?fields=invitable_friends.limit(1000){name,id,picture.width(${pictureWidthVerified}).height(${pictureHeightVerified})`; + this.facebookApi().api( + fbApiForInvitableFriends, + (response) => { + oAuthLog('getFacebookInvitableFriendsList', response); + Dispatcher.dispatch({ + type: FacebookConstants.FACEBOOK_RECEIVED_INVITABLE_FRIENDS, + data: response, + }); + }, + ); + } else { + console.log('FacebookActions.getFacebookInvitableFriendsList was not invoked, this.facebookApi() undefined'); + } + }, + + readFacebookAppRequests () { + if (!webAppConfig.ENABLE_FACEBOOK) { + console.log('FacebookActions.readFacebookAppRequests was not invoked, see ENABLE_FACEBOOK in config.js'); + return; + } + + if (this.facebookApi()) { + const fbApiForReadingAppRequests = 'me?fields=apprequests.limit(10){from,to,created_time,id}'; + this.facebookApi().api( + fbApiForReadingAppRequests, + (response) => { + oAuthLog('readFacebookAppRequests', response); + Dispatcher.dispatch({ + type: FacebookConstants.FACEBOOK_READ_APP_REQUESTS, + data: response, + }); + }, + ); + } else { + console.log('FacebookActions.readFacebookAppRequests was not invoked, this.facebookApi() undefined'); + } + }, + + deleteFacebookAppRequest (requestId) { + if (!webAppConfig.ENABLE_FACEBOOK) { + console.log('FacebookActions.deleteFacebookAppRequest was not invoked, see ENABLE_FACEBOOK in config.js'); + return; + } + + if (this.facebookApi()) { + console.log('deleteFacebookAppRequest requestId: ', requestId); + this.facebookApi().api( + requestId, + 'delete', + (response) => { + oAuthLog('deleteFacebookAppRequest response', response); + Dispatcher.dispatch({ + type: FacebookConstants.FACEBOOK_DELETE_APP_REQUEST, + data: response, + }); + }, + ); + } else { + console.log('FacebookActions.deleteFacebookAppRequest was not invoked, this.facebookApi() undefined'); + } + }, + + logout () { + if (!webAppConfig.ENABLE_FACEBOOK) { + console.log('FacebookActions.logout was not invoked, see ENABLE_FACEBOOK in config.js'); + return; + } + + if (this.facebookApi()) { + this.facebookApi().logout( + (response) => { + oAuthLog('FacebookActions logout response: ', response); + Dispatcher.dispatch({ + type: FacebookConstants.FACEBOOK_LOGGED_OUT, + data: response, + }); + }, + ); + } else { + console.log('FacebookActions.logout was not invoked, this.facebookApi() undefined'); + } + }, + + loginSuccess (successResponse) { + signInModalGlobalState.set('waitingForFacebookApiCompletion', false); + if (successResponse.authResponse) { + oAuthLog('FacebookActions loginSuccess userData: ', successResponse); + Dispatcher.dispatch({ + type: FacebookConstants.FACEBOOK_LOGGED_IN, + data: successResponse, + }); + } else { + // Check if successResponse.authResponse is null indicating cancelled login attempt + oAuthLog('FacebookActions null authResponse indicating cancelled login attempt: ', successResponse); + } + }, + + loginFailure (errorResponse) { + signInModalGlobalState.set('waitingForFacebookApiCompletion', false); + oAuthLog('FacebookActions loginFailure error response: ', errorResponse); + }, + + getPermissions () { + if (isWebApp()) { + return { + scope: 'public_profile, email', // was 'public_profile, email, user_friends', prior to Oct 2020 + }; + } else { + return ['public_profile', 'email']; // was ['public_profile', 'email', 'user_friends']; prior to Oct 2020 + } + }, + + login () { + if (!webAppConfig.FACEBOOK_APP_ID) { + console.log('ERROR: Missing FACEBOOK_APP_ID from src/js/config.js'); // DO NOT REMOVE THIS! + } + + if (!webAppConfig.ENABLE_FACEBOOK) { + console.log('FacebookActions.login was not invoked, see ENABLE_FACEBOOK in config.js'); // DO NOT REMOVE THIS! + return; + } + + // FB.getLoginStatus does an ajax call and when you call FB.login on it's response, the popup that would open + // as a result of this call is blocked. A solution to this problem would be to to specify status: true in the + // options object of FB.init and you need to be confident that login status has already loaded. + oAuthLog('FacebookActions this.facebookApi().login'); + + if (this.facebookApi()) { + const innerThis = this; + this.facebookApi().getLoginStatus( + (response) => { + oAuthLog('FacebookActions this.facebookApi().getLoginStatus response: ', response); + // dumpObjProps('facebookApi().getLoginStatus()', response); + if (response.status === 'connected') { + Dispatcher.dispatch({ + type: FacebookConstants.FACEBOOK_LOGGED_IN, + data: response, + }); + } else { + if (isWebApp()) { // eslint-disable-line no-lonely-if + window.FB.login(innerThis.loginSuccess, innerThis.loginFailure, innerThis.getPermissions()); + } else { + window.facebookConnectPlugin.login(innerThis.getPermissions(), innerThis.loginSuccess, innerThis.loginFailure); + } + } + }, + ); + } else { + console.log('FacebookActions.login was not invoked, this.facebookApi() undefined'); + } + }, + + // July 2017: Not called from anywhere + savePhoto (url) { + Dispatcher.loadEndpoint('voterPhotoSave', { facebook_profile_image_url_https: url }); + }, + + // Save incoming auth data from Facebook + saveFacebookSignInAuth (data) { + // console.log('saveFacebookSignInAuth (result of incoming data from the FB API) kicking off an api server voterFacebookSignInSave'); + Dispatcher.loadEndpoint('voterFacebookSignInSave', { + facebook_access_token: data.accessToken || false, + facebook_user_id: data.userID || false, + facebook_expires_in: data.expiresIn || false, + facebook_signed_request: data.signedRequest || false, + save_auth_data: true, + save_profile_data: false, + }); + }, + + voterFacebookSignInPhoto (facebookUserId, data) { + // console.log("FacebookActions voterFacebookSignInPhoto, data:", data); + if (data) { + Dispatcher.loadEndpoint('voterFacebookSignInSave', { + facebook_user_id: facebookUserId || false, + facebook_profile_image_url_https: data.url || false, + save_photo_data: true, + }); + } + }, + + voterFacebookSignInRetrieve () { + Dispatcher.loadEndpoint('voterFacebookSignInRetrieve', { + }); + }, + + voterFacebookSignInConfirm () { + Dispatcher.loadEndpoint('voterFacebookSignInRetrieve', { + }); + }, +}; diff --git a/src/js/actions/FriendActions.js b/src/js/actions/FriendActions.js new file mode 100644 index 000000000..c7a8e8dd3 --- /dev/null +++ b/src/js/actions/FriendActions.js @@ -0,0 +1,177 @@ +import Dispatcher from '../dispatcher/Dispatcher'; +import AppStore from '../stores/AppStore'; // eslint-disable-line import/no-cycle + +export default { + acceptFriendInvite (otherVoterWeVoteId) { + Dispatcher.loadEndpoint('friendInviteResponse', { + voter_we_vote_id: otherVoterWeVoteId, + kind_of_invite_response: 'ACCEPT_INVITATION', + hostname: AppStore.getHostname(), + }); + }, + + clearErrorMessageToShowVoter () { + Dispatcher.dispatch({ type: 'clearErrorMessageToShowVoter', payload: true }); + }, + + currentFriends () { + Dispatcher.loadEndpoint('friendList', + { + kind_of_list: 'CURRENT_FRIENDS', + }); + }, + + cancelFriendInviteVoter (otherVoterWeVoteId) { + Dispatcher.loadEndpoint('friendInviteResponse', { + voter_we_vote_id: otherVoterWeVoteId, + kind_of_invite_response: 'DELETE_INVITATION_VOTER_SENT_BY_ME', + hostname: AppStore.getHostname(), + }); + }, + + cancelFriendInviteEmail (otherVoterEmailAddress) { + Dispatcher.loadEndpoint('friendInviteResponse', { + recipient_voter_email: otherVoterEmailAddress, + kind_of_invite_response: 'DELETE_INVITATION_EMAIL_SENT_BY_ME', + hostname: AppStore.getHostname(), + }); + }, + + emailBallotData (emailAddressArray, firstNameArray, lastNameArray, emailAddresses, + invitationMessage, ballotLink, senderEmailAddress, verificationEmailSent, deviceType) { + Dispatcher.loadEndpoint('emailBallotData', + { + email_address_array: emailAddressArray, + first_name_array: firstNameArray, + last_name_array: lastNameArray, + email_addresses_raw: emailAddresses, + invitation_message: invitationMessage, + ballot_link: ballotLink, + sender_email_address: senderEmailAddress, + verification_email_sent: verificationEmailSent, + device_type: deviceType, + hostname: AppStore.getHostname(), + }); + }, + + friendInvitationByEmailSend (emailAddressArray, firstNameArray, lastNameArray, emailAddresses, + invitationMessage, senderEmailAddress) { + Dispatcher.loadEndpoint('friendInvitationByEmailSend', + { + email_address_array: emailAddressArray, + first_name_array: firstNameArray, + last_name_array: lastNameArray, + email_addresses_raw: emailAddresses, + invitation_message: invitationMessage, + sender_email_address: senderEmailAddress, + hostname: AppStore.getHostname(), + }); + }, + + friendInvitationByFacebookSend (data) { + console.log('FacebookActions friendInvitationByFacebookSend', data); + Dispatcher.loadEndpoint('friendInvitationByFacebookSend', { + facebook_request_id: data.request_id || false, + recipients_facebook_id_array: data.recipients_facebook_id_array || false, + recipients_facebook_name_array: data.recipients_facebook_name_array || false, + }); + }, + + friendInvitationByWeVoteIdSend (otherVoterWeVoteId) { + Dispatcher.loadEndpoint('friendInvitationByWeVoteIdSend', + { + other_voter_we_vote_id: otherVoterWeVoteId, + hostname: AppStore.getHostname(), + }); + }, + + // TODO DALE To be built on API server + friendInvitationByTwitterHandleSend (twitterHandles, invitationMessage) { + Dispatcher.loadEndpoint('friendInvitationByTwitterHandleSend', + { + twitter_handles_raw: twitterHandles, + invitation_message: invitationMessage, + }); + }, + + friendInvitationsProcessed () { + Dispatcher.loadEndpoint('friendList', + { + kind_of_list: 'FRIEND_INVITATIONS_PROCESSED', + }); + }, + + friendInvitationsWaitingForVerification () { + Dispatcher.loadEndpoint('friendList', + { + kind_of_list: 'FRIEND_INVITATIONS_WAITING_FOR_VERIFICATION', + }); + }, + + friendInvitationsSentByMe () { + Dispatcher.loadEndpoint('friendList', + { + kind_of_list: 'FRIEND_INVITATIONS_SENT_BY_ME', + }); + }, + + friendInvitationsSentToMe () { + Dispatcher.loadEndpoint('friendList', + { + kind_of_list: 'FRIEND_INVITATIONS_SENT_TO_ME', + }); + }, + + friendInvitationInformation (invitationSecretKey) { + Dispatcher.loadEndpoint('friendInvitationInformation', { + invitation_secret_key: invitationSecretKey, + }); + }, + + friendInvitationByEmailVerify (invitationSecretKey) { + Dispatcher.loadEndpoint('friendInvitationByEmailVerify', { + invitation_secret_key: invitationSecretKey, + hostname: AppStore.getHostname(), + }); + }, + + friendInvitationByFacebookVerify (facebookRequestId, recipientFacebookId, senderFacebookId) { + console.log('friendInvitationByFacebookVerify', facebookRequestId); + Dispatcher.loadEndpoint('friendInvitationByFacebookVerify', { + facebook_request_id: facebookRequestId, + recipient_facebook_id: recipientFacebookId, + sender_facebook_id: senderFacebookId, + }); + }, + + ignoreFriendInvite (otherVoterWeVoteId) { + Dispatcher.loadEndpoint('friendInviteResponse', { + voter_we_vote_id: otherVoterWeVoteId, + kind_of_invite_response: 'IGNORE_INVITATION', + hostname: AppStore.getHostname(), + }); + }, + + ignoreSuggestedFriend (otherVoterWeVoteId) { + Dispatcher.loadEndpoint('friendInviteResponse', { + voter_we_vote_id: otherVoterWeVoteId, + kind_of_invite_response: 'IGNORE_SUGGESTION', + hostname: AppStore.getHostname(), + }); + }, + + suggestedFriendList () { + Dispatcher.loadEndpoint('friendList', + { + kind_of_list: 'SUGGESTED_FRIEND_LIST', + }); + }, + + unFriend (otherVoterWeVoteId) { + Dispatcher.loadEndpoint('friendInviteResponse', { + voter_we_vote_id: otherVoterWeVoteId, + kind_of_invite_response: 'UNFRIEND_CURRENT_FRIEND', + hostname: AppStore.getHostname(), + }); + }, +}; diff --git a/src/js/actions/GuideActions.js b/src/js/actions/GuideActions.js deleted file mode 100644 index 109bd2f46..000000000 --- a/src/js/actions/GuideActions.js +++ /dev/null @@ -1,30 +0,0 @@ -import Dispatcher from "../dispatcher/Dispatcher"; - -module.exports = { - ignore: function (we_vote_id) { - Dispatcher.loadEndpoint("organizationFollowIgnore", { organization_we_vote_id: we_vote_id} ); - }, - - follow: function (we_vote_id) { - Dispatcher.loadEndpoint("organizationFollow", { organization_we_vote_id: we_vote_id} ); - }, - - stopFollowing: function (we_vote_id) { - Dispatcher.loadEndpoint("organizationStopFollowing", { organization_we_vote_id: we_vote_id} ); - }, - - retrieveGuidesToFollow: function (election_id, str) { - Dispatcher.loadEndpoint("voterGuidesToFollowRetrieve", { google_civic_election_id: election_id, - maximum_number_to_retrieve: 15, search_string: str || "" }); - }, - - retrieveGuidesToFollowByBallotItem: function (ballot_item_we_vote_id, kind_of_ballot_item) { - Dispatcher.loadEndpoint("voterGuidesToFollowRetrieve", { - ballot_item_we_vote_id: ballot_item_we_vote_id, kind_of_ballot_item: kind_of_ballot_item - }); - }, - - retrieveGuidesFollowed: function () { - Dispatcher.loadEndpoint("voterGuidesFollowedRetrieve"); - } -}; diff --git a/src/js/actions/IssueActions.js b/src/js/actions/IssueActions.js new file mode 100644 index 000000000..6f727314b --- /dev/null +++ b/src/js/actions/IssueActions.js @@ -0,0 +1,87 @@ +import Dispatcher from '../dispatcher/Dispatcher'; + +export default { + issueDescriptionsRetrieve () { + Dispatcher.loadEndpoint('issueDescriptionsRetrieve', {}); + }, + + issueDescriptionsRetrieveCalled () { + Dispatcher.dispatch({ type: 'issueDescriptionsRetrieveCalled', payload: true }); + }, + + issuesUnderBallotItemsRetrieveCalled (googleCivicElectionId) { + Dispatcher.dispatch({ type: 'issuesUnderBallotItemsRetrieveCalled', payload: googleCivicElectionId }); + }, + + issuesFollowedRetrieve () { + Dispatcher.loadEndpoint('issuesFollowedRetrieve', {}); + }, + + issuesUnderBallotItemsRetrieve (googleCivicElectionId, ballot_location_shortcut = '', ballot_returned_we_vote_id = '') { + Dispatcher.loadEndpoint('issuesUnderBallotItemsRetrieve', { + ballot_location_shortcut, + ballot_returned_we_vote_id, + google_civic_election_id: googleCivicElectionId, + }); + }, + + issueFollow (issueWeVoteId, googleCivicElectionId = 0) { + Dispatcher.loadEndpoint('issueFollow', { + issue_we_vote_id: issueWeVoteId, + google_civic_election_id: googleCivicElectionId, + follow: true, + ignore: false, + }); + }, + + issueStopFollowing (issueWeVoteId, googleCivicElectionId = 0) { + Dispatcher.loadEndpoint('issueFollow', { + issue_we_vote_id: issueWeVoteId, + google_civic_election_id: googleCivicElectionId, + follow: false, + ignore: false, + }); + }, + + issueLinkForOrganization (organizationWeVoteId, issueWeVoteId) { + Dispatcher.loadEndpoint('organizationLinkToIssue', + { + organization_we_vote_id: organizationWeVoteId, + issue_we_vote_id: issueWeVoteId, + organization_linked_to_issue: true, + }); + }, + + issueUnLinkForOrganization (organizationWeVoteId, issueWeVoteId) { + Dispatcher.loadEndpoint('organizationLinkToIssue', + { + organization_we_vote_id: organizationWeVoteId, + issue_we_vote_id: issueWeVoteId, + organization_linked_to_issue: false, + }); + }, + + removeBallotItemIssueScoreFromCache: (ballotItemWeVoteId) => { + Dispatcher.dispatch({ + type: 'removeBallotItemIssueScoreFromCache', + res: { + ballot_item_we_vote_id: ballotItemWeVoteId, + success: true, + }, + }); + }, + + retrieveIssuesToLinkForOrganization (organizationWeVoteId) { + Dispatcher.loadEndpoint('issuesToLinkToForOrganization', + { + organization_we_vote_id: organizationWeVoteId, + }); + }, + + retrieveIssuesLinkedForOrganization (organizationWeVoteId) { + Dispatcher.loadEndpoint('issuesLinkedToOrganization', + { + organization_we_vote_id: organizationWeVoteId, + }); + }, +}; diff --git a/src/js/actions/MeasureActions.js b/src/js/actions/MeasureActions.js index 122edb398..9ad1a0531 100644 --- a/src/js/actions/MeasureActions.js +++ b/src/js/actions/MeasureActions.js @@ -1,17 +1,37 @@ -import MeasureConstants from "../constants/MeasureConstants"; -const AppDispatcher = require("../dispatcher/AppDispatcher"); +import Dispatcher from '../dispatcher/Dispatcher'; +export default { + measureRetrieve (measureWeVoteId) { + Dispatcher.loadEndpoint('measureRetrieve', + { + measure_we_vote_id: measureWeVoteId, + }); + }, -const MeasureActions = { - /** - * @param {String} id we_vote_id of ballot item - * @param {Object} item ballot item as javascript object - */ - addItemById: function (id, item) { - AppDispatcher.dispatch({ - actionType: MeasureConstants.MEASURE_ADDED, id, item - }); - } -}; + positionListForBallotItemPublic (ballotItemWeVoteId) { + // This API is always retrieved from our CDN per: WebApp/src/js/utils/service.js + Dispatcher.loadEndpoint('positionListForBallotItem', + { + ballot_item_we_vote_id: ballotItemWeVoteId, + kind_of_ballot_item: 'MEASURE', + }); + }, + + positionListForBallotItemPrivateIndividualsOnly (ballotItemWeVoteId) { + // This API is always retrieved from our CDN per: WebApp/src/js/utils/service.js + Dispatcher.loadEndpoint('positionListForBallotItem', + { + ballot_item_we_vote_id: ballotItemWeVoteId, + kind_of_ballot_item: 'MEASURE', + private_citizens_only: true, + }); + }, -export default MeasureActions; + positionListForBallotItemFromFriends (ballotItemWeVoteId) { + Dispatcher.loadEndpoint('positionListForBallotItemFromFriends', + { + ballot_item_we_vote_id: ballotItemWeVoteId, + kind_of_ballot_item: 'MEASURE', + }); + }, +}; diff --git a/src/js/actions/OfficeActions.js b/src/js/actions/OfficeActions.js index cacb5d0a3..612adb6c2 100644 --- a/src/js/actions/OfficeActions.js +++ b/src/js/actions/OfficeActions.js @@ -1,7 +1,37 @@ -import Dispatcher from "../dispatcher/Dispatcher"; +import Dispatcher from '../dispatcher/Dispatcher'; -module.exports = { - retrieve: function (we_vote_id) { - Dispatcher.loadEndpoint("officeRetrieve", { office_we_vote_id: we_vote_id } ); - } +export default { + officeRetrieve (officeWeVoteId) { + Dispatcher.loadEndpoint('officeRetrieve', + { + office_we_vote_id: officeWeVoteId, + }); + }, + + positionListForBallotItemPublic (ballotItemWeVoteId) { + // This API is always retrieved from our CDN per: WebApp/src/js/utils/service.js + Dispatcher.loadEndpoint('positionListForBallotItem', + { + ballot_item_we_vote_id: ballotItemWeVoteId, + kind_of_ballot_item: 'OFFICE', + }); + }, + + positionListForBallotItemPrivateIndividualsOnly (ballotItemWeVoteId) { + // This API is always retrieved from our CDN per: WebApp/src/js/utils/service.js + Dispatcher.loadEndpoint('positionListForBallotItem', + { + ballot_item_we_vote_id: ballotItemWeVoteId, + kind_of_ballot_item: 'OFFICE', + private_citizens_only: true, + }); + }, + + positionListForBallotItemFromFriends (ballotItemWeVoteId) { + Dispatcher.loadEndpoint('positionListForBallotItemFromFriends', + { + ballot_item_we_vote_id: ballotItemWeVoteId, + kind_of_ballot_item: 'OFFICE', + }); + }, }; diff --git a/src/js/actions/OrganizationActions.js b/src/js/actions/OrganizationActions.js index 0968f1041..2c2fc9cc5 100644 --- a/src/js/actions/OrganizationActions.js +++ b/src/js/actions/OrganizationActions.js @@ -1,12 +1,239 @@ -import Dispatcher from "../dispatcher/Dispatcher"; +import Dispatcher from '../dispatcher/Dispatcher'; -module.exports = { - retrieve: function (we_vote_id) { - Dispatcher.loadEndpoint("organizationRetrieve", { organization_we_vote_id: we_vote_id }); +export default { + organizationFollow (organizationWeVoteId, organization_twitter_handle = '', organization_follow_based_on_issue = false) { + // console.log('OrganizationActions.organizationFollow, organizationWeVoteId: ', organizationWeVoteId); + Dispatcher.loadEndpoint('organizationFollow', { + organization_we_vote_id: organizationWeVoteId, + organization_twitter_handle, + organization_follow_based_on_issue, + }); }, - retrievePositions: function (we_vote_id) { - Dispatcher.loadEndpoint("positionListForOpinionMaker", { opinion_maker_we_vote_id: we_vote_id, kind_of_opinion_maker: "ORGANIZATION" }); + organizationFollowIgnore (organizationWeVoteId) { + Dispatcher.loadEndpoint('organizationFollowIgnore', { organization_we_vote_id: organizationWeVoteId }); }, + organizationStopFollowing (organizationWeVoteId) { + Dispatcher.loadEndpoint('organizationStopFollowing', { organization_we_vote_id: organizationWeVoteId }); + }, + + organizationStopIgnoring (organizationWeVoteId) { + Dispatcher.loadEndpoint('organizationStopIgnoring', { organization_we_vote_id: organizationWeVoteId }); + }, + + organizationsFollowedRetrieve (autoFollowedFromTwitterSuggestion) { + Dispatcher.loadEndpoint('organizationsFollowedRetrieve', { auto_followed_from_twitter_suggestion: autoFollowedFromTwitterSuggestion }); + }, + + organizationRetrieve (weVoteId) { + Dispatcher.loadEndpoint('organizationRetrieve', + { + organization_we_vote_id: weVoteId, + }); + }, + + organizationDescriptionSave (organizationWeVoteId, organizationDescription) { + Dispatcher.loadEndpoint('organizationSave', + { + organization_description: organizationDescription, + organization_we_vote_id: organizationWeVoteId, + }); + }, + + organizationGetStartedSave (organizationWeVoteId, organizationName, organizationWebsite) { + Dispatcher.loadEndpoint('organizationSave', + { + organization_name: organizationName, + organization_we_vote_id: organizationWeVoteId, + organization_website: organizationWebsite, + }); + }, + + organizationChosenGoogleAnalyticsTrackerSave (organizationWeVoteId, organizationChosenGoogleAnalyticsTracker) { + Dispatcher.loadEndpoint('organizationSave', + { + chosen_google_analytics_account_number: organizationChosenGoogleAnalyticsTracker, + organization_we_vote_id: organizationWeVoteId, + }); + }, + + organizationChosenFaviconDelete (organizationWeVoteId) { + Dispatcher.loadEndpoint('organizationPhotosSave', + { + delete_chosen_favicon: true, + organization_we_vote_id: organizationWeVoteId, + }); + }, + + organizationChosenFaviconSave (organizationWeVoteId, chosenFaviconFromFileReader) { + Dispatcher.loadEndpoint('organizationPhotosSave', + { + chosen_favicon_from_file_reader: chosenFaviconFromFileReader, + organization_we_vote_id: organizationWeVoteId, + }); + }, + + organizationChosenLogoDelete (organizationWeVoteId) { + Dispatcher.loadEndpoint('organizationPhotosSave', + { + delete_chosen_logo: true, + organization_we_vote_id: organizationWeVoteId, + }); + }, + + organizationChosenLogoSave (organizationWeVoteId, chosenLogoFromFileReader) { + Dispatcher.loadEndpoint('organizationPhotosSave', + { + chosen_logo_from_file_reader: chosenLogoFromFileReader, + organization_we_vote_id: organizationWeVoteId, + }); + }, + + organizationChosenReadyIntroductionSave (organizationWeVoteId, organizationReadyIntroductionTitle, organizationReadyIntroductionText) { + // console.log('OrganizationActions, organizationChosenReadyIntroductionSave, organizationReadyIntroductionText:', organizationReadyIntroductionText, ', organizationReadyIntroductionTitle:', organizationReadyIntroductionTitle); + Dispatcher.loadEndpoint('organizationSave', + { + chosen_ready_introduction_text: organizationReadyIntroductionText, + chosen_ready_introduction_title: organizationReadyIntroductionTitle, + organization_we_vote_id: organizationWeVoteId, + }); + }, + + organizationChosenSocialShareMasterImageDelete (organizationWeVoteId) { + Dispatcher.loadEndpoint('organizationPhotosSave', + { + delete_chosen_social_share_master_image: true, + organization_we_vote_id: organizationWeVoteId, + }); + }, + + organizationChosenSocialShareMasterImageSave (organizationWeVoteId, chosenSocialShareMasterImageFromFileReader) { + Dispatcher.loadEndpoint('organizationPhotosSave', + { + chosen_social_share_master_image_from_file_reader: chosenSocialShareMasterImageFromFileReader, + organization_we_vote_id: organizationWeVoteId, + }); + }, + + organizationChosenHtmlVerificationSave (organizationWeVoteId, organizationChosenHtmlVerificationString) { + Dispatcher.loadEndpoint('organizationSave', + { + chosen_html_verification_string: organizationChosenHtmlVerificationString, + organization_we_vote_id: organizationWeVoteId, + }); + }, + + organizationChosenSocialShareDescriptionSave (organizationWeVoteId, organizationChosenSocialShareDescription) { + Dispatcher.loadEndpoint('organizationSave', + { + chosen_social_share_description: organizationChosenSocialShareDescription, + organization_we_vote_id: organizationWeVoteId, + }); + }, + + organizationChosenSubdomainSave (organizationWeVoteId, organizationChosenSubdomainName) { + Dispatcher.loadEndpoint('organizationSave', + { + chosen_subdomain_string: organizationChosenSubdomainName, + organization_we_vote_id: organizationWeVoteId, + }); + }, + + organizationChosenDomainNameSave (organizationWeVoteId, organizationChosenDomainName) { + Dispatcher.loadEndpoint('organizationSave', + { + chosen_domain_string: organizationChosenDomainName, + organization_we_vote_id: organizationWeVoteId, + }); + }, + + organizationChosenHideWeVoteLogoSave (organizationWeVoteId, organizationChosenHideWeVoteLogo) { + Dispatcher.loadEndpoint('organizationSave', + { + chosen_hide_we_vote_logo: organizationChosenHideWeVoteLogo, + organization_we_vote_id: organizationWeVoteId, + }); + }, + + organizationPreventSharingOpinions (organizationWeVoteId, organizationPreventSharingOpinions) { + Dispatcher.loadEndpoint('organizationSave', + { + chosen_prevent_sharing_opinions: organizationPreventSharingOpinions, + organization_we_vote_id: organizationWeVoteId, + }); + }, + + organizationNameSave (organizationWeVoteId, organizationName) { + Dispatcher.loadEndpoint('organizationSave', + { + organization_name: organizationName, + organization_we_vote_id: organizationWeVoteId, + }); + }, + + organizationSearch (organizationSearchTerm, organization_twitter_handle = '', exact_match = false) { + // console.log('OrganizationActions.organizationSearch, organizationSearchTerm: ', organizationSearchTerm); + Dispatcher.loadEndpoint('organizationSearch', { + exact_match, + organization_search_term: organizationSearchTerm, + organization_twitter_handle, + }); + }, + + organizationTypeSave (organizationWeVoteId, organizationType) { + Dispatcher.loadEndpoint('organizationSave', + { + organization_type: organizationType, + organization_we_vote_id: organizationWeVoteId, + }); + }, + + organizationWebsiteSave (organizationWeVoteId, organizationWebsite) { + Dispatcher.loadEndpoint('organizationSave', + { + organization_we_vote_id: organizationWeVoteId, + organization_website: organizationWebsite, + }); + }, + + positionListForOpinionMaker (organizationWeVoteId, filterForVoter, filterOutVoter, google_civic_election_id = 0) { // Calls positionListForOpinionMaker endpoint + Dispatcher.loadEndpoint('positionListForOpinionMaker', + { + opinion_maker_we_vote_id: organizationWeVoteId, + filter_for_voter: filterForVoter, + filter_out_voter: filterOutVoter, + google_civic_election_id, + kind_of_opinion_maker: 'ORGANIZATION', + }); + }, + + positionListForOpinionMakerForFriends (weVoteId, filterForVoter, filterOutVoter) { // Calls positionListForOpinionMaker endpoint + Dispatcher.loadEndpoint('positionListForOpinionMaker', + { + opinion_maker_we_vote_id: weVoteId, + filter_for_voter: filterForVoter, + filter_out_voter: filterOutVoter, + friends_vs_public: 'FRIENDS_ONLY', + kind_of_opinion_maker: 'ORGANIZATION', + }); + }, + + saveFromFacebook (facebookId, facebookEmail, facebookProfileImageUrlHttps, organizationName) { + Dispatcher.loadEndpoint('organizationSave', + { + facebook_id: facebookId, + facebook_email: facebookEmail, + facebook_profile_image_url_https: facebookProfileImageUrlHttps, + organization_name: organizationName, + }); + }, + + saveFromTwitter (twitterHandle) { + Dispatcher.loadEndpoint('organizationSave', + { + organization_twitter_handle: twitterHandle, + refresh_from_twitter: 1, + }); + }, }; diff --git a/src/js/actions/ReactionActions.js b/src/js/actions/ReactionActions.js new file mode 100644 index 000000000..845f59dda --- /dev/null +++ b/src/js/actions/ReactionActions.js @@ -0,0 +1,26 @@ +import Dispatcher from '../dispatcher/Dispatcher'; + +export default { + reactionLikeStatusRetrieve (likedItemWeVoteIdList) { + // console.log('reactionLikeStatusRetrieve'); + Dispatcher.loadEndpoint('reactionLikeStatusRetrieve', + { + liked_item_we_vote_id_list: likedItemWeVoteIdList, + }); + }, + voterReactionLikeOffSave (likedItemWeVoteId = '') { + // console.log('voterReactionLikeOffSave'); + Dispatcher.loadEndpoint('voterReactionLikeOffSave', + { + liked_item_we_vote_id: likedItemWeVoteId, + }); + }, + voterReactionLikeOnSave (likedItemWeVoteId = '', activityTidbitWeVoteId = '') { + // console.log('voterReactionLikeOnSave'); + Dispatcher.loadEndpoint('voterReactionLikeOnSave', + { + activity_tidbit_we_vote_id: activityTidbitWeVoteId, + liked_item_we_vote_id: likedItemWeVoteId, + }); + }, +}; diff --git a/src/js/actions/ReadyActions.js b/src/js/actions/ReadyActions.js new file mode 100644 index 000000000..a804c1a50 --- /dev/null +++ b/src/js/actions/ReadyActions.js @@ -0,0 +1,31 @@ +import Dispatcher from '../dispatcher/Dispatcher'; + +export default { + voterPlansForVoterRetrieve (year = 0, month = 0, googleCivicElectionId = 0, stateCode = '') { + // Retrieve the click statistics for all of the items you have shared + return Dispatcher.loadEndpoint('voterPlansForVoterRetrieve', { + google_civic_election_id: googleCivicElectionId, + month, + state_code: stateCode, + year, + }); + }, + + voterPlanListRetrieve (googleCivicElectionId = 0, stateCode = '') { + return Dispatcher.loadEndpoint('voterPlanListRetrieve', { + google_civic_election_id: googleCivicElectionId, + state_code: stateCode, + }); + }, + + voterPlanSave (googleCivicElectionId = 0, showToPublic = '', stateCode = '', voterPlanDataSerialized = '', voterPlanText = '') { + // Look up siteOwnerOrganizationWeVoteId + return Dispatcher.loadEndpoint('voterPlanSave', { + google_civic_election_id: googleCivicElectionId, + show_to_public: showToPublic, + state_code: stateCode, + voter_plan_data_serialized: voterPlanDataSerialized, + voter_plan_text: voterPlanText, + }); + }, +}; diff --git a/src/js/actions/SearchAllActions.js b/src/js/actions/SearchAllActions.js new file mode 100644 index 000000000..6e8604d6f --- /dev/null +++ b/src/js/actions/SearchAllActions.js @@ -0,0 +1,29 @@ +import Dispatcher from '../dispatcher/Dispatcher'; + +export default { + searchAll (textFromSearchField) { + Dispatcher.loadEndpoint('searchAll', + { + text_from_search_field: textFromSearchField, + }); + }, + + exitSearch () { + // setTimeout, as some components attempt to close the search + // while it is already being closed + this.timer = setTimeout(() => Dispatcher.dispatch('exitSearch'), 0); + }, + + retrieveRecentSearches () { + // Dispatcher.loadEndpoint("retrieveRecentSearches", + // { + // }); + }, + + retrieveRelatedSearches () { + // Dispatcher.loadEndpoint("retrieveRelatedSearches", + // { + // }); + }, + +}; diff --git a/src/js/actions/ShareActions.js b/src/js/actions/ShareActions.js new file mode 100644 index 000000000..a95d8f9e0 --- /dev/null +++ b/src/js/actions/ShareActions.js @@ -0,0 +1,43 @@ +import Dispatcher from '../dispatcher/Dispatcher'; + +export default { + sharedItemListRetrieve (year = 0, month = 0, googleCivicElectionId = 0, stateCode = '') { + // Retrieve the click statistics for all of the items you have shared + return Dispatcher.loadEndpoint('sharedItemListRetrieve', { + google_civic_election_id: googleCivicElectionId, + month, + state_code: stateCode, + year, + }); + }, + + sharedItemRetrieveByCode (sharedItemCode) { + return Dispatcher.loadEndpoint('sharedItemRetrieve', { + shared_item_clicked: true, + shared_item_code: sharedItemCode, + }); + }, + + sharedItemRetrieveByFullUrl (destinationFullUrl) { + return Dispatcher.loadEndpoint('sharedItemRetrieve', { + shared_item_clicked: false, + destination_full_url: destinationFullUrl, + }); + }, + + sharedItemSave (destinationFullUrl, kindOfShare = 'BALLOT', ballotItemWeVoteId = '', googleCivicElectionId = 0, organizationWeVoteId = '') { + // Look up siteOwnerOrganizationWeVoteId + return Dispatcher.loadEndpoint('sharedItemSave', { + ballot_item_we_vote_id: ballotItemWeVoteId, + destination_full_url: destinationFullUrl, + google_civic_election_id: googleCivicElectionId, + is_ballot_share: (kindOfShare === 'BALLOT'), + is_candidate_share: (kindOfShare === 'CANDIDATE'), + is_measure_share: (kindOfShare === 'MEASURE'), + is_office_share: (kindOfShare === 'OFFICE'), + is_organization_share: (kindOfShare === 'ORGANIZATION'), + is_ready_share: (kindOfShare === 'READY'), + organization_we_vote_id: organizationWeVoteId, + }); + }, +}; diff --git a/src/js/actions/StarActions.js b/src/js/actions/StarActions.js deleted file mode 100644 index 0ce30c828..000000000 --- a/src/js/actions/StarActions.js +++ /dev/null @@ -1,16 +0,0 @@ -import Dispatcher from "../dispatcher/Dispatcher"; - -module.exports = { - - retrieveAll: function (){ - Dispatcher.loadEndpoint("voterAllStarsStatusRetrieve"); - }, - - voterStarOnSave: function (we_vote_id, type) { - Dispatcher.loadEndpoint("voterStarOnSave", { ballot_item_we_vote_id: we_vote_id, kind_of_ballot_item: type }); - }, - - voterStarOffSave: function (we_vote_id, type) { - Dispatcher.loadEndpoint("voterStarOffSave", { ballot_item_we_vote_id: we_vote_id, kind_of_ballot_item: type }); - } -}; diff --git a/src/js/actions/SupportActions.js b/src/js/actions/SupportActions.js index 4de83f98e..52761e60e 100644 --- a/src/js/actions/SupportActions.js +++ b/src/js/actions/SupportActions.js @@ -1,28 +1,39 @@ -import Dispatcher from "../dispatcher/Dispatcher"; +import Dispatcher from '../dispatcher/Dispatcher'; -module.exports = { +export default { + voterAllPositionsRetrieve () { + Dispatcher.loadEndpoint('voterAllPositionsRetrieve'); + }, - retrieveAll: function (){ - Dispatcher.loadEndpoint("voterAllPositionsRetrieve"); + voterOpposingSave (weVoteId, type) { + Dispatcher.loadEndpoint('voterOpposingSave', { ballot_item_we_vote_id: weVoteId, kind_of_ballot_item: type }); }, - retrieveAllCounts: function (election_id){ - Dispatcher.loadEndpoint("positionsCountForAllBallotItems", {google_civic_election_id: election_id}); + voterStopOpposingSave (weVoteId, type) { + Dispatcher.loadEndpoint('voterStopOpposingSave', { ballot_item_we_vote_id: weVoteId, kind_of_ballot_item: type }); }, - voterOpposingSave: function (we_vote_id, type) { - Dispatcher.loadEndpoint("voterOpposingSave", {ballot_item_we_vote_id: we_vote_id, kind_of_ballot_item: type}); + voterSupportingSave (weVoteId, type) { + Dispatcher.loadEndpoint('voterSupportingSave', { ballot_item_we_vote_id: weVoteId, kind_of_ballot_item: type }); }, - voterStopOpposingSave: function (we_vote_id, type) { - Dispatcher.loadEndpoint("voterStopOpposingSave", {ballot_item_we_vote_id: we_vote_id, kind_of_ballot_item: type}); + voterStopSupportingSave (weVoteId, type) { + Dispatcher.loadEndpoint('voterStopSupportingSave', { ballot_item_we_vote_id: weVoteId, kind_of_ballot_item: type }); }, - voterSupportingSave: function (we_vote_id, type) { - Dispatcher.loadEndpoint("voterSupportingSave", {ballot_item_we_vote_id: we_vote_id, kind_of_ballot_item: type}); + voterPositionCommentSave (weVoteId, type, statementText) { + Dispatcher.loadEndpoint('voterPositionCommentSave', { + ballot_item_we_vote_id: weVoteId, + kind_of_ballot_item: type, + statement_text: statementText, + }); }, - voterStopSupportingSave: function (we_vote_id, type) { - Dispatcher.loadEndpoint("voterStopSupportingSave", {ballot_item_we_vote_id: we_vote_id, kind_of_ballot_item: type}); - } + voterPositionVisibilitySave (weVoteId, type, visibilitySetting) { + Dispatcher.loadEndpoint('voterPositionVisibilitySave', { + ballot_item_we_vote_id: weVoteId, + kind_of_ballot_item: type, + visibility_setting: visibilitySetting, + }); + }, }; diff --git a/src/js/actions/TwitterActions.js b/src/js/actions/TwitterActions.js new file mode 100644 index 000000000..d11ff2f8b --- /dev/null +++ b/src/js/actions/TwitterActions.js @@ -0,0 +1,43 @@ +import Dispatcher from '../dispatcher/Dispatcher'; +import VoterActions from './VoterActions'; +import VoterSessionActions from './VoterSessionActions'; + +export default { + // TODO Convert this to sign out of just Twitter + appLogout () { + VoterSessionActions.voterSignOut(); + VoterActions.voterRetrieve(); + }, + + resetTwitterHandleLanding () { + Dispatcher.dispatch({ type: 'resetTwitterHandleLanding', payload: true }); + }, + + twitterIdentityRetrieve (newTwitterHandle) { + Dispatcher.loadEndpoint('twitterIdentityRetrieve', + { + twitter_handle: newTwitterHandle, + }); + }, + + twitterNativeSignInSave (twitterAccessToken, twitterAccessTokenSecret) { + Dispatcher.loadEndpoint('twitterNativeSignInSave', + { + twitter_access_token: twitterAccessToken, + twitter_access_token_secret: twitterAccessTokenSecret, + }); + }, + + twitterSignInRetrieve () { + Dispatcher.loadEndpoint('twitterSignInRetrieve', { + }); + }, + + twitterSignInStart (returnUrl) { + Dispatcher.loadEndpoint('twitterSignInStart', + { + return_url: returnUrl, + }); + }, + +}; diff --git a/src/js/actions/VoterActions.js b/src/js/actions/VoterActions.js index 5f36874b5..cf48807ce 100644 --- a/src/js/actions/VoterActions.js +++ b/src/js/actions/VoterActions.js @@ -1,16 +1,346 @@ -import Dispatcher from "../dispatcher/Dispatcher"; +import Dispatcher from '../dispatcher/Dispatcher'; +import { isCordova } from '../utils/cordovaUtils'; // eslint-disable-line import/no-cycle +import AppStore from '../stores/AppStore'; // eslint-disable-line import/no-cycle -module.exports = { +export default { + clearEmailAddressStatus () { + Dispatcher.dispatch({ type: 'clearEmailAddressStatus', payload: true }); + }, + + clearSecretCodeVerificationStatus () { + Dispatcher.dispatch({ type: 'clearSecretCodeVerificationStatus', payload: true }); + }, + + clearSMSPhoneNumberStatus () { + Dispatcher.dispatch({ type: 'clearSMSPhoneNumberStatus', payload: true }); + }, + + organizationSuggestionTasks (kindOfSuggestionTask, kindOfFollowTask) { + Dispatcher.loadEndpoint('organizationSuggestionTasks', + { + kind_of_suggestion_task: kindOfSuggestionTask, + kind_of_follow_task: kindOfFollowTask, + }); + }, + + positionListForVoter (showOnlyThisElection, showAllOtherElections) { + Dispatcher.loadEndpoint('positionListForVoter', + { + show_only_this_election: showOnlyThisElection, + show_all_other_elections: showAllOtherElections, + }); + }, + + removeVoterEmailAddress (emailWeVoteId) { + Dispatcher.loadEndpoint('voterEmailAddressSave', { + email_we_vote_id: emailWeVoteId, + delete_email: true, + }); + }, + + removeVoterSMSPhoneNumber (smsWeVoteId) { + Dispatcher.loadEndpoint('voterSMSPhoneNumberSave', { + sms_we_vote_id: smsWeVoteId, + delete_sms: true, + hostname: AppStore.getHostname(), + }); + }, + + // Send the sign in link to their email address + // We keep this in place for historical purposes, since people who haven't + // updated their apps are still using this function + sendSignInLinkEmail (voterEmailAddress) { + Dispatcher.loadEndpoint('voterEmailAddressSave', { + text_for_email_address: voterEmailAddress, + send_link_to_sign_in: true, + make_primary_email: true, + hostname: AppStore.getHostname(), + }); + }, + + // This is for sending a 6 digit code that the voter enters in the same + // interface where the code is requested + sendSignInCodeEmail (voterEmailAddress) { + Dispatcher.loadEndpoint('voterEmailAddressSave', { + text_for_email_address: voterEmailAddress, + send_sign_in_code_email: true, + make_primary_email: true, + hostname: AppStore.getHostname(), + }); + }, + + // This is for sending a 6 digit code that the voter enters in the same + // interface where the code is requested + sendSignInCodeSMS (voterSMSPhoneNumber) { + Dispatcher.loadEndpoint('voterSMSPhoneNumberSave', { + sms_phone_number: voterSMSPhoneNumber, + send_sign_in_code_sms: true, + make_primary_sms_phone_number: true, + hostname: AppStore.getHostname(), + }); + }, + + sendVerificationEmail (voterEmailWeVoteId) { + Dispatcher.loadEndpoint('voterEmailAddressSave', { + email_we_vote_id: voterEmailWeVoteId, + resend_verification_email: true, + hostname: AppStore.getHostname(), + }); + }, + + setAsPrimaryEmailAddress (emailWeVoteId) { + Dispatcher.loadEndpoint('voterEmailAddressSave', { + email_we_vote_id: emailWeVoteId, + make_primary_email: true, + }); + }, + + setAsPrimarySMSPhoneNumber (smsWeVoteId) { + Dispatcher.loadEndpoint('voterSMSPhoneNumberSave', { + sms_we_vote_id: smsWeVoteId, + make_primary_sms_phone_number: true, + hostname: AppStore.getHostname(), + }); + }, + + setExternalVoterId (externalVoterId) { + Dispatcher.dispatch({ type: 'setExternalVoterId', payload: externalVoterId }); + }, + + twitterRetrieveIdsIfollow () { + Dispatcher.loadEndpoint('twitterRetrieveIdsIFollow', {}); + }, + + voterAddressRetrieve (id) { + // console.log("VoterActions, voterAddressRetrieve"); + Dispatcher.loadEndpoint('voterAddressRetrieve', { voter_device_id: id }); + }, + + voterAddressSave (text, simple_save = false, google_civic_election_id = 0) { + Dispatcher.loadEndpoint('voterAddressSave', { text_for_map_search: text, simple_save, google_civic_election_id }); + }, + + voterAnalysisForJumpProcess (incomingVoterDeviceId) { + Dispatcher.loadEndpoint('voterAnalysisForJumpProcess', { + incoming_voter_device_id: incomingVoterDeviceId, + }); + }, + + voterEmailAddressRetrieve () { + Dispatcher.loadEndpoint('voterEmailAddressRetrieve', {}); + }, + + voterEmailAddressSave (voterEmailAddress, send_link_to_sign_in = false) { + Dispatcher.loadEndpoint('voterEmailAddressSave', { + text_for_email_address: voterEmailAddress, + send_link_to_sign_in, + make_primary_email: true, + is_cordova: isCordova(), + hostname: AppStore.getHostname(), + }); + }, + + voterEmailAddressSignIn (emailSecretKey) { + Dispatcher.loadEndpoint('voterEmailAddressSignIn', { + email_secret_key: emailSecretKey, + }); + }, + + voterEmailAddressSignInConfirm (emailSecretKey) { + Dispatcher.loadEndpoint('voterEmailAddressSignIn', { + email_secret_key: emailSecretKey, + yes_please_merge_accounts: true, + }); + }, + + voterEmailAddressVerify (emailSecretKey) { + Dispatcher.loadEndpoint('voterEmailAddressVerify', { + email_secret_key: emailSecretKey, + }); + }, + + voterExternalIdSave (externalVoterId, membershipOrganizationWeVoteId) { + Dispatcher.loadEndpoint('voterUpdate', + { + external_voter_id: externalVoterId, + membership_organization_we_vote_id: membershipOrganizationWeVoteId, + }); + }, + + voterFacebookSaveToCurrentAccount () { + Dispatcher.loadEndpoint('voterFacebookSaveToCurrentAccount', { + }); + }, + + // Tell the server to only save this name if a name does not currently exist + voterFullNameSoftSave (firstName, lastName, full_name = '') { + Dispatcher.loadEndpoint('voterUpdate', + { + first_name: firstName, + last_name: lastName, + full_name, + name_save_only_if_no_existing_names: true, + }); + }, + + voterMergeTwoAccountsByEmailKey (emailSecretKey) { + Dispatcher.loadEndpoint('voterMergeTwoAccounts', + { + email_secret_key: emailSecretKey, + facebook_secret_key: '', + incoming_voter_device_id: '', + invitation_secret_key: '', + twitter_secret_key: '', + hostname: AppStore.getHostname(), + }); + }, + + voterMergeTwoAccountsByFacebookKey (facebookSecretKey) { + // console.log("VoterActions, voterMergeTwoAccountsByFacebookKey"); + Dispatcher.loadEndpoint('voterMergeTwoAccounts', + { + email_secret_key: '', + facebook_secret_key: facebookSecretKey, + incoming_voter_device_id: '', + invitation_secret_key: '', + twitter_secret_key: '', + hostname: AppStore.getHostname(), + }); + }, + + voterMergeTwoAccountsByInvitationKey (invitationSecretKey) { + Dispatcher.loadEndpoint('voterMergeTwoAccounts', + { + email_secret_key: '', + facebook_secret_key: '', + incoming_voter_device_id: '', + invitation_secret_key: invitationSecretKey, + twitter_secret_key: '', + hostname: AppStore.getHostname(), + }); + }, + + voterMergeTwoAccountsByJumpProcess (incomingVoterDeviceId) { + // TODO DALE 2018-01-10 voterMergeTwoAccounts doesn't support incomingVoterDeviceId yet + Dispatcher.loadEndpoint('voterMergeTwoAccounts', + { + email_secret_key: '', + facebook_secret_key: '', + incoming_voter_device_id: incomingVoterDeviceId, + invitation_secret_key: '', + twitter_secret_key: '', + hostname: AppStore.getHostname(), + }); + }, + + voterMergeTwoAccountsByTwitterKey (twitterSecretKey) { + Dispatcher.loadEndpoint('voterMergeTwoAccounts', + { + email_secret_key: '', + facebook_secret_key: '', + incoming_voter_device_id: '', + invitation_secret_key: '', + twitter_secret_key: twitterSecretKey, + hostname: AppStore.getHostname(), + }); + }, + + voterNameSave (firstName, lastName) { + Dispatcher.loadEndpoint('voterUpdate', + { + first_name: firstName, + last_name: lastName, + }); + }, + + voterRetrieve () { + Dispatcher.loadEndpoint('voterRetrieve'); + }, + + voterSMSPhoneNumberRetrieve () { + Dispatcher.loadEndpoint('voterSMSPhoneNumberRetrieve', {}); + }, + + voterSMSPhoneNumberSave (smsPhoneNumber) { + Dispatcher.loadEndpoint('voterSMSPhoneNumberSave', { + sms_phone_number: smsPhoneNumber, + make_primary_sms_phone_number: true, + hostname: AppStore.getHostname(), + }); + }, + + voterSplitIntoTwoAccounts () { + Dispatcher.loadEndpoint('voterSplitIntoTwoAccounts', + { + split_off_twitter: true, + }); + }, + + voterTwitterSaveToCurrentAccount () { + Dispatcher.loadEndpoint('voterTwitterSaveToCurrentAccount', { + }); + }, + + voterUpdateInterfaceStatusFlags (flagIntegerToSet) { + Dispatcher.loadEndpoint('voterUpdate', + { + flag_integer_to_set: flagIntegerToSet, + }); + }, + + voterUpdateNotificationSettingsFlags (flagIntegerToSet, flagIntegerToUnset = '') { + Dispatcher.loadEndpoint('voterUpdate', + { + notification_flag_integer_to_set: flagIntegerToSet, + notification_flag_integer_to_unset: flagIntegerToUnset, + }); + }, + + voterNotificationSettingsUpdateFromSecretKey (emailSubscriptionSecretKey = '', smsSubscriptionSecretKey = '', flagIntegerToSet = 0, flagIntegerToUnset = 0) { + Dispatcher.loadEndpoint('voterNotificationSettingsUpdate', + { + email_subscription_secret_key: emailSubscriptionSecretKey, + sms_subscription_secret_key: smsSubscriptionSecretKey, + notification_flag_integer_to_set: flagIntegerToSet, + notification_flag_integer_to_unset: flagIntegerToUnset, + }); + }, + + voterUpdateRefresh () { + // Just make sure we have the latest voter data + Dispatcher.loadEndpoint('voterUpdate', + { + }); + }, + + voterVerifySecretCode (secretCode, codeSentToSMSPhoneNumber) { + // console.log('VoterActions, voterVerifySecretCode codeSentToSMSPhoneNumber:', codeSentToSMSPhoneNumber); + Dispatcher.loadEndpoint('voterVerifySecretCode', { + secret_code: secretCode, + code_sent_to_sms_phone_number: codeSentToSMSPhoneNumber, + }); + }, - retrieveVoter: function () { - Dispatcher.loadEndpoint("voterRetrieve"); + voterAppleSignInSave (email, givenName, middleName, familyName, user, identityToken) { + // eslint-disable-next-line camelcase + const { device: { platform: apple_platform, version: apple_os_version, model: apple_model } } = window; + Dispatcher.loadEndpoint('appleSignInSave', { + email, + first_name: givenName, + middle_name: middleName, + last_name: familyName, + user_code: user, + apple_platform, + apple_os_version, + apple_model, + identity_token: identityToken, + }); }, - retrieveAddress: function (id){ - Dispatcher.loadEndpoint("voterAddressRetrieve", { voter_device_id: id}); + deviceStoreFirebaseCloudMessagingToken (firebaseFCMToken) { + Dispatcher.loadEndpoint('deviceStoreFirebaseCloudMessagingToken', { + firebase_fcm_token: firebaseFCMToken, + }); }, - saveAddress: function (text){ - Dispatcher.loadEndpoint("voterAddressSave", { text_for_map_search: text }); - } }; diff --git a/src/js/actions/VoterGuideActions.js b/src/js/actions/VoterGuideActions.js new file mode 100644 index 000000000..31b2f5f77 --- /dev/null +++ b/src/js/actions/VoterGuideActions.js @@ -0,0 +1,111 @@ +import Dispatcher from '../dispatcher/Dispatcher'; + +export default { + + pledgeToVoteWithVoterGuide (voterGuideWeVoteId, delete_pledge = false) { + Dispatcher.loadEndpoint('pledgeToVoteWithVoterGuide', { + voter_guide_we_vote_id: voterGuideWeVoteId, + delete_pledge, + }); + }, + + voterGuidesRetrieve (organizationWeVoteId) { + Dispatcher.loadEndpoint('voterGuidesRetrieve', { + organization_we_vote_id: organizationWeVoteId, + }); + }, + + voterGuidesToFollowRetrieve (electionId, searchString, addVoterGuidesNotFromElection, startRetrieveAtThisNumber = 0) { + // We have migrated to a newer API call that we cache by CDN: voterGuidesUpcomingRetrieve + const maximumNumberToRetrieve = 50; + return Dispatcher.loadEndpoint('voterGuidesToFollowRetrieve', { + google_civic_election_id: electionId, + start_retrieve_at_this_number: startRetrieveAtThisNumber, + maximum_number_to_retrieve: maximumNumberToRetrieve, + search_string: searchString || '', + add_voter_guides_not_from_election: addVoterGuidesNotFromElection || false, + }); + }, + + voterGuidesToFollowRetrieveByBallotItem (ballotItemWeVoteId, kindOfBallotItem) { + Dispatcher.loadEndpoint('voterGuidesToFollowRetrieve', { + ballot_item_we_vote_id: ballotItemWeVoteId, + kind_of_ballot_item: kindOfBallotItem, + }); + }, + + voterGuidesToFollowRetrieveByIssuesFollowed () { + // DALE 2019-12-26 Testing without this + Dispatcher.loadEndpoint('voterGuidesToFollowRetrieve', { + filter_voter_guides_by_issue: true, + }); + }, + + voterFollowAllOrganizationsFollowedByOrganization (organizationWeVoteId) { + Dispatcher.loadEndpoint('voterFollowAllOrganizationsFollowedByOrganization', { + organization_we_vote_id: organizationWeVoteId, + }); + }, + + voterGuidesFollowedRetrieve () { + Dispatcher.loadEndpoint('voterGuidesFollowedRetrieve', { + maximum_number_to_retrieve: 0, + }); + }, + + voterGuidesFollowedByOrganizationRetrieve (organizationWeVoteId) { + Dispatcher.loadEndpoint('voterGuidesFollowedByOrganizationRetrieve', { + organization_we_vote_id: organizationWeVoteId, + }); + }, + + voterGuidesRecommendedByOrganizationRetrieve (organizationWeVoteId, googleCivicElectionId) { + Dispatcher.loadEndpoint('voterGuidesFollowedByOrganizationRetrieve', { + organization_we_vote_id: organizationWeVoteId, + filter_by_this_google_civic_election_id: googleCivicElectionId, + }); + }, + + voterGuideFollowersRetrieve (organizationWeVoteId) { + Dispatcher.loadEndpoint('voterGuideFollowersRetrieve', { + organization_we_vote_id: organizationWeVoteId, + maximum_number_to_retrieve: 200, + }); + }, + + voterGuidesIgnoredRetrieve () { + // We do not currently limit the maximumNumberToRetrieve + Dispatcher.loadEndpoint('voterGuidesIgnoredRetrieve'); + }, + + voterGuideSave (googleCivicElectionId, voterGuideWeVoteId) { + Dispatcher.loadEndpoint('voterGuideSave', { + google_civic_election_id: googleCivicElectionId, + voter_guide_we_vote_id: voterGuideWeVoteId, + }); + }, + + voterGuidesUpcomingRetrieve (googleCivicElectionId = 0) { + // This API is always retrieved from our CDN per: WebApp/src/js/utils/service.js + + // let maximumNumberToRetrieve = 500; + // For now, just pass one googleCivicElectionId into list. If we want multiple, we will need to dispatch + // with multiple "google_civic_election_id_list" entries + Dispatcher.loadEndpoint('voterGuidesUpcomingRetrieve', { + google_civic_election_id_list: [googleCivicElectionId], + // maximum_number_to_retrieve: maximumNumberToRetrieve, + }); + }, + + voterGuidesFromFriendsUpcomingRetrieve (googleCivicElectionId = 0) { + // This API is always retrieved from our CDN per: WebApp/src/js/utils/service.js + + // let maximumNumberToRetrieve = 500; + // For now, just pass one googleCivicElectionId into list. If we want multiple, we will need to dispatch + // with multiple "google_civic_election_id_list" entries + Dispatcher.loadEndpoint('voterGuidesFromFriendsUpcomingRetrieve', { + google_civic_election_id_list: [googleCivicElectionId], + // maximum_number_to_retrieve: maximumNumberToRetrieve, + }); + }, +}; diff --git a/src/js/actions/VoterGuidePossibilityActions.js b/src/js/actions/VoterGuidePossibilityActions.js new file mode 100644 index 000000000..4e7b43860 --- /dev/null +++ b/src/js/actions/VoterGuidePossibilityActions.js @@ -0,0 +1,48 @@ +import Dispatcher from '../dispatcher/Dispatcher'; + +export default { + /** + * Retrieve a Voter Guide Position Possibility + * @param voterGuidePossibilityId, id of the Guide possibility + * @param voterGuidePossibilityPositionId, id of the Position possibility, an integer + * @returns {*} + */ + voterGuidePossibilityPositionsRetrieve (voterGuidePossibilityId, voterGuidePossibilityPositionId = 0) { + // We have migrated to a newer API call that we cache by CDN: voterGuidesUpcomingRetrieve + return Dispatcher.loadEndpoint('voterGuidePossibilityPositionsRetrieve', { + voter_guide_possibility_id: voterGuidePossibilityId, + voter_guide_possibility_position_id: voterGuidePossibilityPositionId, + }); + }, + + /** + * Save a Voter Guide Position Possibility + * @param voterGuidePossibilityId, id of the Guide possibility + * @param voterGuidePossibilityPositionId, id of the Position possibility. Untyped. If zero, create a new position. Otherwise a number or ''. + * @param dictionaryToSave, dictionary of the data to be saved + */ + voterGuidePossibilityPositionSave (voterGuidePossibilityId, voterGuidePossibilityPositionId, dictionaryToSave = {}) { + let dispatchDictionary = { + voter_guide_possibility_id: voterGuidePossibilityId, + voter_guide_possibility_position_id: voterGuidePossibilityPositionId, + }; + dispatchDictionary = { ...dispatchDictionary, ...dictionaryToSave }; + // console.log('voterGuidePossibilityPositionSave dispatchDictionary:', dispatchDictionary); + Dispatcher.loadEndpoint('voterGuidePossibilityPositionSave', dispatchDictionary); + }, + + voterGuidePossibilityRetrieve (urlToScan = '', voterGuidePossibilityId = '') { + Dispatcher.loadEndpoint('voterGuidePossibilityRetrieve', { + url_to_scan: urlToScan, + voter_guide_possibility_id: voterGuidePossibilityId, + }); + }, + + voterGuidePossibilitySave (voterGuidePossibilityId, dictionaryToSave = {}) { + let dispatchDictionary = { + voter_guide_possibility_id: voterGuidePossibilityId, + }; + dispatchDictionary = { ...dispatchDictionary, ...dictionaryToSave }; + Dispatcher.loadEndpoint('voterGuidePossibilitySave', dispatchDictionary); + }, +}; diff --git a/src/js/actions/VoterSessionActions.js b/src/js/actions/VoterSessionActions.js new file mode 100644 index 000000000..80c698e7e --- /dev/null +++ b/src/js/actions/VoterSessionActions.js @@ -0,0 +1,36 @@ +import Dispatcher from '../dispatcher/Dispatcher'; +import cookies from '../utils/cookies'; +import AppActions from './AppActions'; +import { stringContains } from '../utils/textFormat'; + +export default { + voterSignOut () { + AppActions.setShowSignInModal(false); + AppActions.unsetStoreSignInStartFullUrl(); + Dispatcher.loadEndpoint('voterSignOut', { sign_out_all_devices: false }); + cookies.removeItem('voter_device_id'); + cookies.removeItem('voter_device_id', '/'); + cookies.removeItem('voter_device_id', '/', 'wevote.us'); + cookies.removeItem('ballot_has_been_visited'); + cookies.removeItem('ballot_has_been_visited', '/'); + cookies.removeItem('location_guess_closed'); + cookies.removeItem('location_guess_closed', '/'); + cookies.removeItem('location_guess_closed', '/', 'wevote.us'); + cookies.removeItem('show_full_navigation'); + cookies.removeItem('show_full_navigation', '/'); + cookies.removeItem('sign_in_start_full_url', '/'); + cookies.removeItem('sign_in_start_full_url', '/', 'wevote.us'); + }, + + setVoterDeviceIdCookie (id) { + let { hostname } = window.location; + hostname = hostname || ''; + console.log('VoterSessionActions setVoterDeviceIdCookie hostname:', hostname); + if (hostname && stringContains('wevote.us', hostname)) { + // If hanging off We Vote subdomain, store the cookie with top level domain + cookies.setItem('voter_device_id', id, Infinity, '/', 'wevote.us'); + } else { + cookies.setItem('voter_device_id', id, Infinity, '/'); + } + }, +}; diff --git a/src/js/attributions.js b/src/js/attributions.js new file mode 100644 index 000000000..54a59279d --- /dev/null +++ b/src/js/attributions.js @@ -0,0 +1,197 @@ +module.exports = [ + 'We Vote is open source under the MIT License. The text of the MIT License (MIT):\n' + + 'Permission is hereby granted, free of charge, to any person obtaining a copy ' + + 'of this software and associated documentation files (the "Software"), to deal ' + + 'in the Software without restriction, including without limitation the rights ' + + 'to use, copy, modify, merge, publish, distribute, sublicense, and/or sell ' + + 'copies of the Software, and to permit persons to whom the Software is ' + + 'furnished to do so, subject to the following conditions:\n' + + 'The above copyright notice and this permission notice shall be included in ' + + 'all copies or substantial portions of the Software.\n' + + 'THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR ' + + 'IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, ' + + 'FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE ' + + 'AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER ' + + 'LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, ' + + 'OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN ' + + 'THE SOFTWARE.', + // Jan 2021, https://github.com/babel/babel/blob/main/LICENSE + 'babel/babel is licensed under the MIT License (MIT)\n' + + '\n' + + 'Copyright (c) 2014-present Sebastian McKenzie and other contributors.\n', + // Jan 2021, https://github.com/mui-org/material-ui/blob/next/LICENSE + 'mui-org/material-ui is licensed under the MIT License (MIT)\n' + + '\n' + + 'Copyright (c) 2014 Call-Em-All\n', + // Jan 2021, https://github.com/mui-org/material-ui/tree/master/packages/material-ui-icons + // No license cited + // Jan 2019, https://github.com/twbs/bootstrap/blob/v4-dev/LICENSE + 'twbs/bootstrap is licensed under the MIT License (MIT)\n' + + '\n' + + 'Copyright (c) 2011-2020 Twitter, Inc.\n' + + 'Copyright (c) 2011-2020 The Bootstrap Authors\n', + // Jan 2019, https://github.com/JedWatson/classnames/blob/master/LICENSE + 'JedWatson/classnames is licensed under the MIT License (MIT)\n' + + 'Copyright (c) 2018 Jed Watson\n', + // Jan 20201, https://github.com/lukeed/clsx/blob/master/license + 'lukeed/clsx is licensed under the MIT License (MIT)\n' + + 'Copyright (c) Luke Edwards (lukeed.com)\n', + // Jan 2021, https://github.com/d3/d3-geo/blob/master/LICENSE + 'd3/d3-geo is licensed under the BSD-3-Clause\n' + + 'Copyright (c) 2008-2012, Charles Karney\n', + // Jan 2021, https://github.com/d3/d3-selection/blob/master/LICENSE + 'd3/d3-selection is licensed under the BSD-3-Clause\n' + + 'Copyright (c) 2010-2018, Michael Bostock\n', + // Jan 2021, https://github.com/d3/d3-zoom/blob/master/LICENSE + 'd3/d3-zoom is licensed under the BSD-3-Clause\n' + + 'Copyright (c) 2010-2016, Michael Bostock\n', + // Jan 2019, + 'stefanpenner/es6-promise is licensed under the MIT License (MIT)\n' + + 'Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors\n', + // Jan 2019, https://github.com/facebook/flux/blob/master/LICENSE + 'BSD License for Flux software\n' + + 'Copyright (c) 2014-present, Facebook, Inc. All rights reserved.\n' + + 'Redistribution and use in source and binary forms, with or without modification, ' + + 'are permitted provided that the following conditions are met:\n' + + ' * Redistributions of source code must retain the above copyright notice, this ' + + 'list of conditions and the following disclaimer.\n' + + ' * Redistributions in binary form must reproduce the above copyright notice, ' + + 'this list of conditions and the following disclaimer in the ' + + 'documentation and/or other materials provided with the distribution.\n' + + ' * Neither the name Facebook nor the names of its contributors may be used to ' + + 'endorse or promote products derived from this software without specific ' + + 'prior written permission.', + // Jan 2019, https://github.com/FortAwesome/Font-Awesome + 'Font Awesome Free License\n' + + 'Font Awesome Free is free, open source, and GPL friendly. You can use it for ' + + 'commercial projects, open source projects, or really almost whatever you want.\n' + + 'Full Font Awesome Free license: https://fontawesome.com/license/free.\n' + + '\n' + + '# Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/)\n' + + 'In the Font Awesome Free download, the CC BY 4.0 license applies to all icons ' + + 'packaged as SVG and JS file types.\n' + + '\n' + + '# Fonts: SIL OFL 1.1 License (https://scripts.sil.org/OFL)\n' + + 'In the Font Awesome Free download, the SIL OFL license applies to all icons ' + + 'packaged as web and desktop font files.\n' + + '\n' + + '# Code: MIT License (https://opensource.org/licenses/MIT)\n' + + 'In the Font Awesome Free download, the MIT license applies to all non-font and ' + + 'non-icon files.\n' + + '\n' + + '# Attribution\n' + + 'Attribution is required by MIT, SIL OFL, and CC BY licenses. Downloaded Font ' + + 'Awesome Free files already contain embedded comments with sufficient ' + + "attribution, so you shouldn't need to do anything additional when using these " + + 'files normally.', + // Jan 2019, https://github.com/davidjbradshaw/iframe-resizer/blob/master/LICENSE + 'davidjbradshaw/iframe-resizer is licensed under the MIT License (MIT)\n' + + '\n' + + 'Copyright (c) 2013-2018 David J. Bradshaw', + // Jan 2019, https://github.com/jquery/jquery/blob/master/LICENSE.txt + 'jquery/jquery is licensed under the MIT License (MIT)\n' + + 'Copyright JS Foundation and other contributors, https://js.foundation/\n', + // Jan 2021, keymirror is unlicensed, https://www.npmjs.com/package/keymirror + // Jan 2019, https://github.com/lodash/lodash/blob/master/LICENSE + 'lodash/lodash is licensed under the MIT License (MIT)\n' + + '\n' + + 'Copyright JS Foundation and other contributors \n' + + '\n' + + 'Based on Underscore.js, copyright Jeremy Ashkenas, ' + + 'DocumentCloud and Investigative Reporters & Editors \n', + // Jan 2021, https://github.com/lodash/lodash/blob/master/LICENSE + 'lodash/lodash (and lodash-assign) is licensed under the MIT License (MIT)\n' + + '\n' + + 'Copyright JS Foundation and other contributors \n' + + '\n' + + 'Based on Underscore.js, copyright Jeremy Ashkenas, ' + + 'DocumentCloud and Investigative Reporters & Editors \n', + // Jan 2019, https://github.com/moment/moment/blob/develop/LICENSE + 'moment/moment (and moment/moment-timezone) is licensed under the MIT License (MIT)\n' + + 'Copyright (c) JS Foundation and other contributors\n', + // Jan 2019, https://github.com/sindresorhus/object-assign/blob/master/license + 'sindresorhus/object-assign is licensed under the MIT License (MIT)\n' + + '\n' + + 'Copyright (c) Sindre Sorhus (sindresorhus.com)\n', + // Jan 2019, https://github.com/FezVrasta/popper.js/blob/master/LICENSE.md + 'FezVrasta/popper.js is licensed under the MIT License (MIT)\n' + + 'Copyright (c) 2016 Federico Zivolo and contributors\n', + // Jan 2019, https://github.com/facebook/prop-types/blob/master/LICENSE + ' facebook/prop-types is licensed under the MIT License (MIT)\n' + + '\n' + + 'Copyright (c) 2013-present, Facebook, Inc.\n', + // Jan 2019, https://github.com/facebook/react/blob/master/LICENSE + 'facebook/react (and facebook/react-dom) is licensed under the MIT License (MIT)\n' + + 'Copyright (c) Facebook, Inc. and its affiliates.\n', + // Jan 2019, https://github.com/react-bootstrap/react-bootstrap/blob/master/LICENSE + 'react-bootstrap/react-bootstrap is licensed under the MIT License (MIT)\n' + + 'Copyright (c) 2014-present Stephen J. Collings, Matthew Honnibal, Pieter Vanderwerff\n', + // Jan 2019, https://github.com/nkbt/react-copy-to-clipboard/blob/master/LICENSE + 'nkbt/react-copy-to-clipboard is licensed under the MIT License (MIT)\n' + + '\n' + + 'Copyright (c) 2016 Nik Butenko\n', + // Jan 2021, https://github.com/arnthor3/react-delay-render/blob/master/LICENCE + 'arnthor3/react-delay-render is licensed under MIT License (MIT)\n' + + 'Copyright (c) 2015 Chris Shiplet\n', + // Jan 2019, https://github.com/nfl/react-helmet/blob/master/LICENSE + 'nfl/react-helmet is licensed under the MIT License (MIT)\n' + + 'Copyright (c) 2015 NFL\n', + // https://github.com/zeroasterisk/react-iframe-resizer-super/blob/master/LICENSE + 'zeroasterisk/react-iframe-resizer-super is licensed under the MIT License (MIT)\n' + + 'Copyright (c) 2016 Your Name. ', + // Jan 2021, https://gitlab.com/catamphetamine/react-phone-number-input/-/blob/master/LICENSE + 'catamphetamine/react-phone-number-input is licensed under the MIT License (MIT)\n' + + 'Copyright (c) 2016 @catamphetamine \n', + // Jan 2019, https://github.com/CookPete/react-player/blob/master/LICENSE.md + 'CookPete/react-player is licensed under the MIT License (MIT)\n' + + 'Copyright (c) 2017 Pete Cook http://cookpete.com\n', + // Jan 2019, https://github.com/ReactTraining/react-router/blob/master/LICENSE + 'ReactTraining/react-router (and react-router-dom) is licensed under the MIT License (MIT)\n' + + 'Copyright (c) React Training 2016-2018\n', + // Jan 2021, https://github.com/nygardk/react-share/blob/master/LICENSE + 'nygardk/react-share is licensed under the MIT License (MIT)\n' + + 'Copyright (c) 2015 Klaus Nygård\n', + // https://github.com/akiran/react-slick + 'akiran/react-slick is licensed under the The MIT License (MIT)\n' + + 'Copyright (c) 2014 Kiran Abburi\n', + // https://github.com/stripe/react-stripe-elements/blob/master/LICENSE + 'stripe/react-stripe-elements is licensed under the MIT License (MIT)\n' + + 'Copyright (c) 2017 Stripe\n', + // Jan 2019, https://github.com/tanem/react-svg/blob/master/LICENSE + 'tanem/react-svg is licensed under the MIT License (MIT)\n' + + 'Copyright (c) 2014-present Tane Morgan \n', + // Jan 2021, https://github.com/ShinyChang/React-Text-Truncate/blob/master/LICENSE + 'ShinyChang/React-Text-Truncate s licensed under the MIT License (MIT)\n' + + 'Copyright (c) 2014-present Tane Morgan \n', + // Jan 2019, https://github.com/fkhadra/react-toastify/blob/master/LICENSE + 'fkhadra/react-toastify is licensed under the MIT License (MIT)\n' + + 'Copyright (c) 2017 Fadi Khadra\n', + // Jan 2019, https://github.com/aaronshaf/react-toggle/blob/master/LICENSE + 'aaronshaf/react-toggle is licensed under the MIT License (MIT)\n' + + 'Copyright (c) 2015 instructure-react\n', + // Jan 2019, https://github.com/styled-components/styled-components/blob/master/LICENSE + 'styled-components/styled-components is licensed under the MIT License (MIT)\n' + + 'Copyright (c) 2016-present Glen Maddern and Maximilian Stoiber\n', + // Jan 2019, https://github.com/visionmedia/superagent/blob/master/LICENSE + 'visionmedia/superagent is licensed under the MIT License (MIT)\n' + + 'Copyright (c) 2014-2016 TJ Holowaychuk \n', + // Jan 2021, + 'topojson/topojson-client is licensed under the ISC License\n' + + 'Copyright 2012-2019 Michael Bostock\n', + // Jan 2019, https://dev.maxmind.com/geoip/geoip2/geolite2/ (In Python server, but requests attribution in app) + 'This product includes GeoLite2 data created by MaxMind\n' + + 'Available from https://www.maxmind.com', + // July 2019, https://developer.zendesk.com/embeddables/docs/widget/legal + 'The Zendesk embeddable contains third-party, open source software and/or libraries. \n' + + 'To view them and their license terms, go to http://goto.zendesk.com/embeddable-legal-notices', + + // Commented out in code + /* + // Jan 2019, https://github.com/taion/react-router-scroll/blob/master/LICENSE + 'taion/react-router-scroll is licensed under the MIT License (MIT)\n' + + 'Copyright (c) 2016 Jimmy Jia\n', + // Jan 2019, https://github.com/andreypopp/react-textarea-autosize/blob/master/LICENSE + 'andreypopp/react-textarea-autosize is licensed under the MIT License (MIT)\n' + + 'Copyright (c) 2013 Andrey Popp\n', + */ +]; diff --git a/src/js/components/Activity/ActivityCommentAdd.jsx b/src/js/components/Activity/ActivityCommentAdd.jsx new file mode 100644 index 000000000..3ffad8127 --- /dev/null +++ b/src/js/components/Activity/ActivityCommentAdd.jsx @@ -0,0 +1,234 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import styled from 'styled-components'; +import { withStyles, withTheme } from '@material-ui/core/styles'; +import { + FormControl, + IconButton, + InputAdornment, + TextField, +} from '@material-ui/core'; +import { AccountCircle, Send } from '@material-ui/icons'; +import ActivityActions from '../../actions/ActivityActions'; +import ActivityStore from '../../stores/ActivityStore'; +import AppActions from '../../actions/AppActions'; +import { renderLog } from '../../utils/logging'; +import VoterStore from '../../stores/VoterStore'; + + +class ActivityCommentAdd extends Component { + constructor (props) { + super(props); + this.state = { + activityCommentCount: 0, + statementText: '', + }; + } + + componentDidMount () { + this.activityStoreListener = ActivityStore.addListener(this.onActivityStoreChange.bind(this)); + this.voterStoreListener = VoterStore.addListener(this.onVoterStoreChange.bind(this)); + this.onActivityStoreChange(); + this.onVoterStoreChange(); + } + + componentWillUnmount () { + this.activityStoreListener.remove(); + this.voterStoreListener.remove(); + } + + onActivityStoreChange () { + const { activityCommentWeVoteId, activityTidbitWeVoteId } = this.props; + if (activityCommentWeVoteId) { + const activityComment = ActivityStore.getActivityCommentByWeVoteId(activityCommentWeVoteId); + const { + statement_text: statementText, + } = activityComment; + this.setState({ + statementText, + }); + // console.log('ActivityCommentAdd onActivityStoreChange, activityCommentWeVoteId:', activityCommentWeVoteId, ', statementText:', statementText); + } + const activityCommentCount = ActivityStore.getActivityCommentParentCountByTidbitWeVoteId(activityTidbitWeVoteId); + // console.log('activityTidbitWeVoteId activityCommentCount:', activityCommentCount); + this.setState({ + activityCommentCount, + }); + } + + onVoterStoreChange () { + const voter = VoterStore.getVoter(); + const { full_name: voterFullName, voter_photo_url_tiny: voterPhotoUrlTiny } = voter; + this.setState({ + voterFullName, + voterPhotoUrlTiny, + }); + } + + saveActivityComment = () => { + const { activityCommentWeVoteId, activityTidbitWeVoteId, parentCommentWeVoteId } = this.props; + const { visibilityIsPublic, statementText } = this.state; + // console.log('ActivityPostModal activityTidbitWeVoteId:', activityTidbitWeVoteId, 'statementText: ', statementText, 'visibilityIsPublic: ', visibilityIsPublic); + const visibilitySetting = visibilityIsPublic ? 'SHOW_PUBLIC' : 'FRIENDS_ONLY'; + ActivityActions.activityCommentSave(activityCommentWeVoteId, activityTidbitWeVoteId, statementText, visibilitySetting, parentCommentWeVoteId); + this.setState({ + statementText: '', + }); + if (this.props.addChildSavedFunction && parentCommentWeVoteId) { + this.props.addChildSavedFunction(parentCommentWeVoteId); + } + if (this.props.commentEditSavedFunction) { + this.props.commentEditSavedFunction(); + } + } + + updateStatementTextToBeSaved = (e) => { + this.setState({ + statementText: e.target.value, + }); + } + + onClickShowActivityTidbitDrawer = () => { + const { activityTidbitWeVoteId } = this.props; + // console.log('onClickShowActivityTidbitDrawer activityTidbitWeVoteId:', activityTidbitWeVoteId); + AppActions.setActivityTidbitWeVoteIdForDrawer(activityTidbitWeVoteId); + AppActions.setShowActivityTidbitDrawer(true); + } + + render () { + renderLog('ActivityCommentAdd'); // Set LOG_RENDER_EVENTS to log all renders + const { activityCommentWeVoteId, activityTidbitWeVoteId, classes, hidePhotoFromTextField, inEditMode } = this.props; + const { statementText, activityCommentCount, voterFullName, voterPhotoUrlTiny } = this.state; + if (!activityTidbitWeVoteId) { + return null; + } + const showSendButton = inEditMode || statementText; + // console.log('activityCommentCount:', activityCommentCount); + return ( + 0)}> + + + {/* NOT WORKING in classes: , multiline: classes.textFieldMultilineClasses */} + + {(voterPhotoUrlTiny) ? ( + + + + ) : ( + + + + )} + + ), + }} + /> + + + {showSendButton && ( + + + + + + )} + + ); + } +} +ActivityCommentAdd.propTypes = { + activityTidbitWeVoteId: PropTypes.string.isRequired, + activityCommentWeVoteId: PropTypes.string, + addChildSavedFunction: PropTypes.func, + classes: PropTypes.object, + commentEditSavedFunction: PropTypes.func, + hidePhotoFromTextField: PropTypes.bool, + inEditMode: PropTypes.bool, + parentCommentWeVoteId: PropTypes.string, // Signifies that this is a response to a comment +}; + + +const styles = () => ({ + accountCircle: { + color: 'rgba(17, 17, 17, .4)', + '&:hover': { + backgroundColor: 'transparent', + }, + }, + buttonOutlinedPrimary: { + background: 'white', + }, + formControl: { + margin: 0, + width: '100%', + }, + saveComment: { + color: 'rgba(17, 17, 17, .4)', + '&:hover': { + backgroundColor: 'transparent', + }, + padding: 6, + }, + saveCommentActive: { + color: '#2e3c5d', + '&:hover': { + backgroundColor: 'transparent', + }, + padding: 6, + }, + textFieldClasses: { + margin: 0, + width: '100%', + }, +}); + +const AddReplyTextWrapper = styled.div` + width: 100%; +`; + +const ActivityImage = styled.img` + border-radius: 4px; + width: 24px; + height: 24px; + margin-top: 3px; +`; + +const SendButtonWrapper = styled.div` + width: 22px; +`; + +const SpeakerAvatar = styled.div` + background: transparent; + display: flex; + justify-content: center; + position: relative; +`; + +const Wrapper = styled.div` + align-items: center; + display: flex; + font-size: 14px; + justify-content: space-between; + ${({ commentsExist }) => ((commentsExist) ? 'margin-top: 6px !important;' : 'margin-top: 4px !important;')} + padding: 0px !important; +`; + +export default withTheme(withStyles(styles)(ActivityCommentAdd)); diff --git a/src/js/components/Activity/ActivityPositionList.jsx b/src/js/components/Activity/ActivityPositionList.jsx new file mode 100644 index 000000000..a521d23cc --- /dev/null +++ b/src/js/components/Activity/ActivityPositionList.jsx @@ -0,0 +1,143 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import styled from 'styled-components'; +import { withStyles } from '@material-ui/core/styles'; +import { CircularProgress } from '@material-ui/core'; +import { renderLog } from '../../utils/logging'; +import VoterGuidePositionItem from '../VoterGuide/VoterGuidePositionItem'; +import ShowMoreItems from '../Widgets/ShowMoreItems'; + + +const STARTING_NUMBER_OF_POSITIONS_TO_DISPLAY = 6; + +class ActivityPositionList extends Component { + constructor (props) { + super(props); + this.state = { + loadingMoreItems: false, + numberOfPositionItemsToDisplay: STARTING_NUMBER_OF_POSITIONS_TO_DISPLAY, + }; + } + + componentDidMount () { + // console.log('ActivityPositionList componentDidMount'); + if (this.props.startingNumberOfPositionsToDisplay && this.props.startingNumberOfPositionsToDisplay > 0) { + this.setState({ + numberOfPositionItemsToDisplay: this.props.startingNumberOfPositionsToDisplay, + }); + } + } + + componentWillUnmount () { + if (this.positionItemTimer) { + clearTimeout(this.positionItemTimer); + this.positionItemTimer = null; + } + } + + increaseNumberOfPositionItemsToDisplay = () => { + let { numberOfPositionItemsToDisplay } = this.state; + // console.log('Number of position items before increment: ', numberOfPositionItemsToDisplay); + + numberOfPositionItemsToDisplay += 2; + // console.log('Number of position items after increment: ', numberOfPositionItemsToDisplay); + + this.positionItemTimer = setTimeout(() => { + this.setState({ + numberOfPositionItemsToDisplay, + }); + }, 500); + } + + render () { + const { incomingPositionList, organizationWeVoteId } = this.props; + renderLog('ActivityPositionList'); // Set LOG_RENDER_EVENTS to log all renders + // console.log('ActivityPositionList render'); + if (!incomingPositionList) { + // console.log('ActivityPositionList Loading...'); + return null; + } + const { loadingMoreItems, numberOfPositionItemsToDisplay } = this.state; + let positionsExist = false; + let count; + for (count = 0; count < incomingPositionList.length; count++) { + positionsExist = true; + } + if (!positionsExist) { + return null; + } + + let numberOfPositionItemsDisplayed = 0; + return ( +
    +
      + {incomingPositionList.map((onePosition) => { + // console.log('onePosition:', onePosition); + // console.log('numberOfPositionItemsDisplayed:', numberOfPositionItemsDisplayed); + if (numberOfPositionItemsDisplayed >= numberOfPositionItemsToDisplay) { + return null; + } + numberOfPositionItemsDisplayed += 1; + // console.log('numberOfBallotItemsDisplayed: ', numberOfBallotItemsDisplayed); + return ( +
      + +
      + ); + })} +
    + + {!!(incomingPositionList && incomingPositionList.length > 1) && ( + + )} + + + {(loadingMoreItems) && ( + + )} + +
    + ); + } +} +ActivityPositionList.propTypes = { + incomingPositionList: PropTypes.array.isRequired, + organizationWeVoteId: PropTypes.string.isRequired, + startingNumberOfPositionsToDisplay: PropTypes.number, +}; + +const styles = () => ({ + iconButton: { + padding: 8, + }, +}); + +const LoadingItemsWheel = styled.div` + width: 100%; + display: flex; + align-items: center; + justify-content: center; +`; + +const ShowMoreItemsWrapper = styled.div` + margin-bottom: 0px; + padding-left: 16px; + padding-right: 26px; + @media (max-width: ${({ theme }) => theme.breakpoints.sm}) { + padding-right: 16px; + } + @media print{ + display: none; + } +`; + +export default withStyles(styles)(ActivityPositionList); diff --git a/src/js/components/Activity/ActivityPostAdd.jsx b/src/js/components/Activity/ActivityPostAdd.jsx new file mode 100644 index 000000000..93324a749 --- /dev/null +++ b/src/js/components/Activity/ActivityPostAdd.jsx @@ -0,0 +1,247 @@ +import React, { Component } from 'react'; +import styled from 'styled-components'; +import { withStyles, withTheme } from '@material-ui/core/styles'; +import PropTypes from 'prop-types'; +import { InputBase, Card } from '@material-ui/core'; +import ActivityPostModal from './ActivityPostModal'; +import VoterStore from '../../stores/VoterStore'; +import stockAvatar from '../../../img/global/icons/avatar-generic.png'; +import { cordovaNewsPaddingTop } from '../../utils/cordovaOffsets'; +import { cordovaDot, isCordova } from '../../utils/cordovaUtils'; +import { renderLog } from '../../utils/logging'; + + +class ActivityPostAdd extends Component { + constructor (props) { + super(props); + this.state = { + showActivityPostModal: false, + statementText: '', + }; + this.updateStatementTextToBeSaved = this.updateStatementTextToBeSaved.bind(this); + } + + componentDidMount () { + const voter = VoterStore.getVoter(); + const { voter_photo_url_medium: voterPhotoUrlMedium } = voter; + this.setState({ + // voter, + // voterIsSignedIn, + voterPhotoUrlMedium, + }); + this.voterStoreListener = VoterStore.addListener(this.onVoterStoreChange.bind(this)); + } + + componentDidCatch (error, info) { + // We should get this information to Splunk! + console.error('ActivityPostAdd caught error: ', `${error} with info: `, info); + } + + componentWillUnmount () { + this.voterStoreListener.remove(); + } + + // See https://reactjs.org/docs/error-boundaries.html + static getDerivedStateFromError (error) { // eslint-disable-line no-unused-vars + // Update state so the next render will show the fallback UI, We should have a "Oh snap" page + return { hasError: true }; + } + + handleFocus (e) { + e.target.blur(); + } + + onVoterStoreChange () { + const voter = VoterStore.getVoter(); + const { voter_photo_url_medium: voterPhotoUrlMedium } = voter; + this.setState({ + // voter, + // voterIsSignedIn, + voterPhotoUrlMedium, + }); + } + + toggleActivityPostModal = () => { + const { showActivityPostModal } = this.state; + // console.log('toggleActivityPostModal showActivityPostModal:', showActivityPostModal); + this.setState({ + showActivityPostModal: !showActivityPostModal, + }); + } + + updateStatementTextToBeSaved (e) { + this.setState({ + statementText: e.target.value, + }); + } + + render () { + renderLog('ActivityPostAdd'); // Set LOG_RENDER_EVENTS to log all renders + const { classes, externalUniqueId } = this.props; + const { + showActivityPostModal, + voterPhotoUrlMedium, statementText, + } = this.state; + + // console.log('inModal: ', inModal); + + // const horizontalEllipsis = '\u2026'; + const statementPlaceholderText = 'What\'s on your mind?'; + + // let videoUrl = ''; + // let statementTextNoUrl = null; + // let youTubeUrl; + // let vimeoUrl; + // + // if (statementText) { + // youTubeUrl = statementText.match(youTubeRegX); + // vimeoUrl = statementText.match(vimeoRegX); + // } + // + // if (youTubeUrl) { + // [videoUrl] = youTubeUrl; + // statementTextNoUrl = statementText.replace(videoUrl, ''); + // } + // + // if (vimeoUrl) { + // [videoUrl] = vimeoUrl; + // statementTextNoUrl = statementText.replace(videoUrl, ''); + // } + + // console.log('ActivityPostAdd'); + // console.log('editMode:', editMode); + + const unsetSideMarginsIfCordova = isCordova() ? { margin: 0 } : {}; + const adjustMarginsIfCordova = isCordova() ? { + margin: 0, + paddingTop: cordovaNewsPaddingTop(), + } : {}; + + return ( + + + Create Post + + + + + { this.textarea = tag; }} + multiline + name="statementText" + onClick={this.toggleActivityPostModal} + onFocus={this.handleFocus} + placeholder={statementPlaceholderText} + rows="1" + /> + + {showActivityPostModal && ( + + )} + + + ); + } +} +ActivityPostAdd.propTypes = { + externalUniqueId: PropTypes.string, + classes: PropTypes.object, +}; + +const styles = (theme) => ({ + root: { + boxShadow: 'none', + border: '1px solid #333', + padding: '8px', + [theme.breakpoints.down('xs')]: { + height: 'auto', + }, + }, + rootWhite: { + boxShadow: 'none', + border: 'none', + padding: '8px', + [theme.breakpoints.down('xs')]: { + height: 'auto', + }, + }, + textInput: { + flex: '1 1 0', + fontSize: 20, + height: '100%', + [theme.breakpoints.down('sm')]: { + fontSize: 18, + }, + }, + disabled: { + background: '#eee', + border: 'none', + }, + disabledWhite: { + background: '#fff', + border: 'none', + }, + disabledInput: { + color: '#313131', + }, + buttonOutlinedPrimary: { + padding: '4px 8px', + fontWeight: 600, + background: 'white', + color: '#313131', + [theme.breakpoints.down('md')]: { + fontWeight: 500, + height: '100%', + fontSize: 12, + }, + [theme.breakpoints.down('sm')]: { + padding: '2px 4px', + fontWeight: 600, + height: '100%', + fontSize: 10, + }, + }, +}); + +const AddTidbitTitle = styled.div` + background: #2e3c5d; + border-bottom: 1px solid #ddd; + color: #fff; + font-size: 14px; + font-weight: 700; + // margin: 0 -16px 0 0; + padding: 4px 16px; + @media (max-width: ${({ theme }) => theme.breakpoints.sm}) { + font-size: 16px; + // margin: 0 15px; + } +`; + +const CardNewsWrapper = styled.div` + margin: 0 0 8px 0; + padding: 8px 16px 8px 16px; + @media (max-width: ${({ theme }) => theme.breakpoints.sm}) { + // margin: 0 15px; + } +`; + +const InnerFlexWrapper = styled.div` + align-items: center; + display: flex; + justify-content: flex-start; + width: 100%; +`; + +export default withTheme(withStyles(styles)(ActivityPostAdd)); diff --git a/src/js/components/Activity/ActivityPostModal.jsx b/src/js/components/Activity/ActivityPostModal.jsx new file mode 100644 index 000000000..69b461965 --- /dev/null +++ b/src/js/components/Activity/ActivityPostModal.jsx @@ -0,0 +1,314 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import styled from 'styled-components'; +import { Close } from '@material-ui/icons'; +import { withStyles, withTheme } from '@material-ui/core/styles'; +import { + Button, + Dialog, + DialogTitle, + IconButton, + DialogContent, + InputBase, +} from '@material-ui/core'; +import ActivityActions from '../../actions/ActivityActions'; +import ActivityStore from '../../stores/ActivityStore'; +import ActivityPostPublicToggle from './ActivityPostPublicToggle'; +import { renderLog } from '../../utils/logging'; +import { + cordovaDot, + hasIPhoneNotch, prepareForCordovaKeyboard, + restoreStylesAfterCordovaKeyboard, +} from '../../utils/cordovaUtils'; +import stockAvatar from '../../../img/global/icons/avatar-generic.png'; +import VoterStore from '../../stores/VoterStore'; + +class ActivityPostModal extends Component { + constructor (props) { + super(props); + this.state = { + visibilityIsPublic: false, + voterPhotoUrlMedium: '', + }; + } + + componentDidMount () { + this.activityStoreListener = ActivityStore.addListener(this.onActivityStoreChange.bind(this)); + this.voterStoreListener = VoterStore.addListener(this.onVoterStoreChange.bind(this)); + this.onActivityStoreChange(); + this.onVoterStoreChange(); + } + + componentDidUpdate () { + const { initialFocusSet } = this.state; + if (this.activityPostInput) { + // Set the initial focus at the end of any existing text + if (!initialFocusSet) { + const { activityPostInput } = this; + const { length } = activityPostInput.value; + activityPostInput.focus(); + activityPostInput.setSelectionRange(length, length); + this.setState({ + initialFocusSet: true, + }); + } + } + } + + componentWillUnmount () { + this.activityStoreListener.remove(); + this.voterStoreListener.remove(); + } + + onActivityStoreChange () { + const { activityTidbitWeVoteId } = this.props; + const activityPost = ActivityStore.getActivityTidbitByWeVoteId(activityTidbitWeVoteId); + // console.log('onActivityStoreChange activityTidbitWeVoteId:', activityTidbitWeVoteId, ', activityPost:', activityPost); + if (activityPost) { + const { + statement_text: statementText, + visibility_is_public: visibilityIsPublic, + } = activityPost; + this.setState({ + visibilityIsPublic, + statementText, + }); + } + } + + onVoterStoreChange () { + const voter = VoterStore.getVoter(); + const { voter_photo_url_medium: voterPhotoUrlMedium } = voter; + this.setState({ + voterPhotoUrlMedium, + }); + } + + onBlurInput = () => { + restoreStylesAfterCordovaKeyboard(ActivityPostModal); + }; + + onFocusInput = () => { + prepareForCordovaKeyboard('ItemPositionStatementActionBar'); + }; + + onPublicToggleChange = (visibilityIsPublic) => { + this.setState({ + visibilityIsPublic, + }); + } + + saveActivityPost = (e) => { + e.preventDefault(); + const { activityTidbitWeVoteId } = this.props; + const { visibilityIsPublic, statementText } = this.state; + // console.log('ActivityPostModal activityTidbitWeVoteId:', activityTidbitWeVoteId, 'statementText: ', statementText, 'visibilityIsPublic: ', visibilityIsPublic); + const visibilitySetting = visibilityIsPublic ? 'SHOW_PUBLIC' : 'FRIENDS_ONLY'; + ActivityActions.activityPostSave(activityTidbitWeVoteId, statementText, visibilitySetting); + this.props.toggleActivityPostModal(); + } + + updateStatementTextToBeSaved = (e) => { + this.setState({ + statementText: e.target.value, + }); + } + + render () { + renderLog('ActivityPostModal'); // Set LOG_RENDER_EVENTS to log all renders + const { activityTidbitWeVoteId } = this.props; + const { + classes, externalUniqueId, + } = this.props; + const { + visibilityIsPublic, + voterPhotoUrlMedium, + statementText, + } = this.state; + + // const horizontalEllipsis = '\u2026'; + const statementPlaceholderText = 'What\'s on your mind?'; + + const rowsToShow = 6; + + // console.log('ActivityPostModal render, voter_address_object: ', voter_address_object); + return ( + { this.props.toggleActivityPostModal(); }} + > + + + {activityTidbitWeVoteId === '' ? 'Create Post' : 'Edit Post'} + + { this.props.toggleActivityPostModal(); }} + id="closeActivityPostModal" + > + + + + + +
    +
    + + { this.activityPostInput = input; }} + multiline + name="statementText" + onChange={this.updateStatementTextToBeSaved} + placeholder={statementPlaceholderText} + rows={rowsToShow} + value={statementText || ''} + /> +
    + + + + + +
    +
    +
    + ); + } +} +ActivityPostModal.propTypes = { + activityTidbitWeVoteId: PropTypes.string, + classes: PropTypes.object, + externalUniqueId: PropTypes.string, + show: PropTypes.bool, + toggleActivityPostModal: PropTypes.func.isRequired, +}; + +const styles = (theme) => ({ + dialogTitle: { + padding: 16, + }, + dialogPaper: { + marginTop: hasIPhoneNotch() ? '107px !important' : '48px !important', + minHeight: '200px', + maxHeight: '350px', + height: '80%', + width: '90%', + maxWidth: '600px', + top: '0px', + transform: 'translate(0%, -20%)', + [theme.breakpoints.down('xs')]: { + minWidth: '95%', + maxWidth: '95%', + width: '95%', + minHeight: '200px', + maxHeight: '330px', + height: '70%', + margin: '0 auto', + transform: 'translate(0%, -30%)', + }, + }, + dialogContent: { + padding: '0px 16px', + display: 'flex', + flexDirection: 'column', + justifyContent: 'space-between', + height: '100%', + }, + closeButton: { + position: 'absolute', + right: `${theme.spacing(1)}px`, + top: `${theme.spacing(1)}px`, + }, + saveButtonRoot: { + width: '100%', + }, + formStyles: { + width: '100%', + }, + formControl: { + width: '100%', + marginTop: 16, + }, + inputMultiline: { + fontSize: 20, + height: '100%', + width: '100%', + [theme.breakpoints.down('sm')]: { + fontSize: 18, + }, + }, + inputStyles: { + flex: '1 1 0', + fontSize: 18, + height: '100%', + width: '100%', + [theme.breakpoints.down('sm')]: { + fontSize: 16, + }, + }, + select: { + padding: '12px 12px', + margin: '0px 1px', + }, +}); + +const PostSaveButton = styled.div` + width: 100%; +`; + +const TextFieldWrapper = styled.div` + height: 100%; + display: flex; + flex-direction: column; + justify-content: space-between; +`; + +const Title = styled.div` + font-size: 16px; + font-weight: bold; + margin: 0; + margin-top: 2px; + text-align: left; +`; + +export default withTheme(withStyles(styles)(ActivityPostModal)); diff --git a/src/js/components/Activity/ActivityPostPublicToggle.jsx b/src/js/components/Activity/ActivityPostPublicToggle.jsx new file mode 100644 index 000000000..e0e8578b0 --- /dev/null +++ b/src/js/components/Activity/ActivityPostPublicToggle.jsx @@ -0,0 +1,364 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import styled from 'styled-components'; +import { Dialog, DialogContent, DialogTitle, IconButton, Typography, Radio, FormControlLabel, FormControl } from '@material-ui/core'; +import { Close } from '@material-ui/icons'; +import { withStyles } from '@material-ui/core/styles'; +import { renderLog } from '../../utils/logging'; +import { isCordova, isWebApp } from '../../utils/cordovaUtils'; +import SettingsAccount from '../Settings/SettingsAccount'; +import ActivityStore from '../../stores/ActivityStore'; +import VoterStore from '../../stores/VoterStore'; +import { openSnackbar } from '../Widgets/SnackNotifier'; + +class ActivityPostPublicToggle extends Component { + constructor (props) { + super(props); + this.state = { + visibilityIsPublic: false, + inTestMode: false, + isSignedIn: null, + showActivityPostIsPublicHelpModal: false, + voterWeVoteId: '', + }; + } + + componentDidMount () { + this.onVoterStoreChange(); + this.activityStoreListener = ActivityStore.addListener(this.onActivityStoreChange.bind(this)); + this.voterStoreListener = VoterStore.addListener(this.onVoterStoreChange.bind(this)); + const { initialVisibilityIsPublic: visibilityIsPublic, inTestMode } = this.props; + const visibilityIsPublicBoolean = !!(visibilityIsPublic); + this.setState({ + visibilityIsPublic: visibilityIsPublicBoolean, + inTestMode, + }); + } + + componentWillUnmount () { + this.activityStoreListener.remove(); + this.voterStoreListener.remove(); + } + + onActivityStoreChange () { + const { activityTidbitWeVoteId } = this.props; + if (activityTidbitWeVoteId) { + const activityTidbit = ActivityStore.getActivityTidbitByWeVoteId(activityTidbitWeVoteId); + if (activityTidbit) { + const { + visibility_is_public: visibilityIsPublic, + } = activityTidbit; + + this.setState({ + visibilityIsPublic, + }); + } + } + } + + onVoterStoreChange () { + const voter = VoterStore.getVoter(); + const { is_signed_in: isSignedIn, we_vote_id: voterWeVoteId } = voter; + this.setState({ + isSignedIn, + voterWeVoteId, + }); + } + + handleRadioButtonChange = (evt) => { + const { value } = evt.target; + if (value === 'Public') { + this.showItemToPublic(); + } else { + this.showItemToFriendsOnly(); + } + }; + + showItemToFriendsOnly () { + this.setState({ + visibilityIsPublic: false, + }); + this.props.onToggleChange(false); + openSnackbar({ message: 'Post now visible to We Vote friends only!' }); + } + + showItemToPublic () { + const { inTestMode, isSignedIn } = this.state; + + // console.log('ActivityPostPublicToggle-showItemToPublic'); + if (inTestMode) { + this.setState({ + visibilityIsPublic: true, + }); + openSnackbar({ message: 'Post now visible to anyone!' }); + } else if (isSignedIn) { + this.setState({ + visibilityIsPublic: true, + }); + this.props.onToggleChange(true); + const ActivityPostPublicToggleModalHasBeenShown = false; // Always show + if (!ActivityPostPublicToggleModalHasBeenShown) { + this.toggleActivityPostIsPublicHelpModal(); + // TODO DALE: VoterActions.voterUpdateInterfaceStatusFlags(VoterConstants.POSITION_PUBLIC_MODAL_SHOWN); + } else { + openSnackbar({ message: 'Post now visible to anyone!' }); + } + } else { + this.toggleActivityPostIsPublicHelpModal(); + } + } + + toggleActivityPostIsPublicHelpModal () { + const { showActivityPostIsPublicHelpModal } = this.state; + this.setState({ + showActivityPostIsPublicHelpModal: !showActivityPostIsPublicHelpModal, + }); + } + + render () { + renderLog('ActivityPostPublicToggle'); // Set LOG_RENDER_EVENTS to log all renders + const { classes, externalUniqueId, preventStackedButtons } = this.props; + const { inTestMode, isSignedIn, showActivityPostIsPublicHelpModal, voterWeVoteId } = this.state; + let { visibilityIsPublic } = this.state; + if (!voterWeVoteId) { + return
    ; + } + + let onChangeByKeypress; + const _this = this; + if (visibilityIsPublic) { + onChangeByKeypress = () => { + visibilityIsPublic = false; + + // TODO Somehow cause the tooltip to update if inTestMode + if (!inTestMode) { + _this.showItemToFriendsOnly(); + } + }; + } else { + onChangeByKeypress = () => { + visibilityIsPublic = true; + + // TODO Somehow cause the tooltip to update if inTestMode + if (!inTestMode) { + _this.showItemToPublic(); + } + }; + } + + // this onKeyDown function is for accessibility: the parent div of the toggle + // has a tab index so that users can use tab key to select the toggle, and then + // press either space or enter (key codes 32 and 13, respectively) to toggle + const onKeyDown = (e) => { + const enterAndSpaceKeyCodes = [13, 32]; + if (enterAndSpaceKeyCodes.includes(e.keyCode)) { + onChangeByKeypress(); + } + }; + + // This modal is shown when the user clicks on public toggle either when not signed in + // or for the first time after being signed in. + const ActivityPostIsPublicHelpModal = ( + { this.toggleActivityPostIsPublicHelpModal(); }} + > + + + {isSignedIn ? 'Public' : 'Show to Public'} + + { this.toggleActivityPostIsPublicHelpModal(); }} + id="profileCloseActivityPostPublicToggle" + > + + + + + {isSignedIn ? ( +
    +
    Your post will be visible to anyone who is following you.
    +
    +
    Click the "Friends Only" toggle to show to We Vote friends only.
    +
    + ) : ( +
    + +
    + )} +
    +
    +
    +
    + ); + + return ( + + {showActivityPostIsPublicHelpModal ? ActivityPostIsPublicHelpModal : null} + + + + + + ) + } + /> + + + + ) + } + /> + + + + + + ); + } +} +ActivityPostPublicToggle.propTypes = { + activityTidbitWeVoteId: PropTypes.string, + classes: PropTypes.object, + className: PropTypes.string, + externalUniqueId: PropTypes.string, + initialVisibilityIsPublic: PropTypes.bool, + inTestMode: PropTypes.bool, + preventStackedButtons: PropTypes.bool, + onToggleChange: PropTypes.func.isRequired, +}; + +const styles = (theme) => ({ + dialogRoot: isCordova() ? { + height: '100%', + position: 'absolute !important', + top: '-15%', + left: '0% !important', + right: 'unset !important', + bottom: 'unset !important', + width: '100%', + } : {}, + dialogPaper: isWebApp() ? { + [theme.breakpoints.down('sm')]: { + minWidth: '95%', + maxWidth: '95%', + width: '95%', + maxHeight: '90%', + margin: '0 auto', + }, + } : { + margin: '0 !important', + width: '95%', + height: 'unset', + maxHeight: '90%', + offsetHeight: 'unset !important', + top: '50%', + left: '50%', + right: 'unset !important', + bottom: 'unset !important', + position: 'absolute', + transform: 'translate(-50%, -25%)', + }, + dialogContent: { + [theme.breakpoints.down('md')]: { + padding: '0 8px 8px', + }, + }, + radioPrimary: { + padding: '.1rem', + margin: '.1rem .1rem .6rem .6rem', + [theme.breakpoints.down('md')]: { + marginLeft: 0, + }, + }, + radioLabel: { + fontSize: '14px', + bottom: '4px', + position: 'relative', + [theme.breakpoints.down('sm')]: { + fontSize: '11px', + }, + }, + formControl: { + width: '100%', + }, + closeButton: { + position: 'absolute', + right: `${theme.spacing(1)}px`, + top: `${theme.spacing(1)}px`, + }, +}); + +const Wrapper = styled.div` + margin-left: auto; + width: fit-content; +`; + +const PublicToggle = styled.div` + padding-left: 15px; + @media (max-width: ${({ theme }) => theme.breakpoints.md}) { + padding-top: 4px; + margin-bottom: 10px; + } +`; + +const RadioItemStackedStyles = ` + @media (max-width: ${({ theme }) => theme.breakpoints.xs}) { + width: 100% !important; + min-width: 100% !important; + margin-bottom: -6px; + } +`; + +const RadioItem = styled.div` + ${({ preventStackedButtons }) => ((preventStackedButtons) ? '' : RadioItemStackedStyles)} +`; + +const RadioGroup = styled.div` + display: flex; + flex-flow: row nowrap; + width: 100%; + @media (max-width: ${({ theme }) => theme.breakpoints.md}) { + margin-bottom: -10px; + } + @media (max-width: ${({ theme }) => theme.breakpoints.xs}) { + ${({ preventStackedButtons }) => ((preventStackedButtons) ? '' : 'flex-flow: row wrap;')} + margin-bottom: 0; + } +`; + + +export default withStyles(styles)(ActivityPostPublicToggle); diff --git a/src/js/components/Activity/ActivitySpeakerCard.jsx b/src/js/components/Activity/ActivitySpeakerCard.jsx new file mode 100755 index 000000000..f73c377ab --- /dev/null +++ b/src/js/components/Activity/ActivitySpeakerCard.jsx @@ -0,0 +1,254 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import styled from 'styled-components'; +import ActivityStore from '../../stores/ActivityStore'; +import avatarGenericIcon from '../../../img/global/svg-icons/avatar-generic.svg'; +import { cordovaDot } from '../../utils/cordovaUtils'; +import FriendsOnlyIndicator from '../Widgets/FriendsOnlyIndicator'; +import LoadingWheel from '../LoadingWheel'; +import { renderLog } from '../../utils/logging'; +import OpenExternalWebSite from '../Widgets/OpenExternalWebSite'; +import OrganizationPopoverCard from '../Organization/OrganizationPopoverCard'; +import ReadMore from '../Widgets/ReadMore'; +import StickyPopover from '../Ballot/StickyPopover'; +import { createDescriptionOfFriendPosts } from '../../utils/activityUtils'; +import { timeFromDate } from '../../utils/dateFormat'; +import { numberWithCommas } from '../../utils/textFormat'; +import VoterStore from '../../stores/VoterStore'; + +class ActivitySpeakerCard extends Component { + constructor (props) { + super(props); + this.state = { + actionDescription: null, + activityTimeFromDate: '', + isActivityNoticeSeed: false, + isActivityPost: false, + speakerName: '', + speakerOrganizationWeVoteId: '', + speakerProfileImageUrlMedium: '', + speakerTwitterHandle: '', + speakerTwitterFollowersCount: 0, + speakerIsVoter: false, + }; + } + + componentDidMount () { + this.onActivityStoreChange(); + this.activityStoreListener = ActivityStore.addListener(this.onActivityStoreChange.bind(this)); + } + + componentWillUnmount () { + this.activityStoreListener.remove(); + } + + onActivityStoreChange () { + const { activityTidbitWeVoteId } = this.props; + const activityTidbit = ActivityStore.getActivityTidbitByWeVoteId(activityTidbitWeVoteId); + const { + date_created: dateOfNotice, + kind_of_activity: kindOfActivity, + position_name_list: positionNameList, + speaker_name: speakerName, + speaker_organization_we_vote_id: speakerOrganizationWeVoteId, + speaker_profile_image_url_medium: speakerProfileImageUrlMedium, + speaker_twitter_handle: speakerTwitterHandle, + speaker_twitter_followers_count: speakerTwitterFollowersCount, + speaker_voter_we_vote_id: speakerVoterWeVoteId, + visibility_is_public: visibilityIsPublic, + } = activityTidbit; + const voter = VoterStore.getVoter(); + const speakerIsVoter = (voter.we_vote_id === speakerVoterWeVoteId); + let isActivityNoticeSeed = false; + let isActivityPost = false; + if (kindOfActivity === 'ACTIVITY_NOTICE_SEED') { + isActivityNoticeSeed = true; + } else if (kindOfActivity === 'ACTIVITY_POST') { + isActivityPost = true; + } + const activityTimeFromDate = timeFromDate(dateOfNotice); + const actionDescription = createDescriptionOfFriendPosts(positionNameList); + // const actionDescription = added a new opinion.; + this.setState({ + actionDescription, + activityTimeFromDate, + isActivityNoticeSeed, + isActivityPost, + speakerName, + speakerOrganizationWeVoteId, + speakerProfileImageUrlMedium, + speakerTwitterHandle, + speakerTwitterFollowersCount, + speakerIsVoter, + visibilityIsPublic, + }); + } + + render () { + renderLog('ActivitySpeakerCard'); // Set LOG_RENDER_EVENTS to log all renders + const { activityTidbitWeVoteId, showTwitterInformation } = this.props; + const { + actionDescription, activityTimeFromDate, + isActivityNoticeSeed, isActivityPost, speakerIsVoter, + speakerName, speakerOrganizationWeVoteId, + speakerProfileImageUrlMedium, speakerTwitterFollowersCount, speakerTwitterHandle, + visibilityIsPublic, + } = this.state; + if (!speakerName && !speakerIsVoter) { + return
    {LoadingWheel}
    ; + } + const organizationPopoverCard = (); + + // const voterGuideLink = speakerTwitterHandle ? `/${speakerTwitterHandle}` : `/voterguide/${speakerOrganizationWeVoteId}`; + + return ( + + + {(speakerProfileImageUrlMedium) ? ( + + + + ) : ( + + + + )} + + + + + + {speakerIsVoter ? 'You' : speakerName} + + + {(actionDescription && isActivityNoticeSeed) && ( + + + + )} + + + + {(activityTimeFromDate) && ( + + {activityTimeFromDate} + + )} + {(isActivityPost) && ( + + )} + + {(speakerTwitterHandle && showTwitterInformation) && ( + + + @ + {speakerTwitterHandle} + + { !!(speakerTwitterFollowersCount && String(speakerTwitterFollowersCount) !== '0') && ( + + + {numberWithCommas(speakerTwitterFollowersCount)} + + )} + + )} + /> + )} + + + + ); + } +} +ActivitySpeakerCard.propTypes = { + activityTidbitWeVoteId: PropTypes.string.isRequired, + showTwitterInformation: PropTypes.bool, +}; + +const ActionDescriptionWrapper = styled.div` + font-size: 14px; + margin-left: 3px; + margin-top: 4px; + padding: 0px !important; +`; + +const ActivityImage = styled.img` + border-radius: 4px; + width: 50px; +`; + +const ActivityTime = styled.div` + color: #999; + font-size: 11px; + font-weight: 400; + margin-right: 6px; +`; + +const SpeakerAvatar = styled.div` + background: transparent; + display: flex; + justify-content: center; + min-width: 50px; + position: relative; +`; + +const SecondLineWrapper = styled.div` +`; + +const SpeakerActionTimeWrapper = styled.div` + margin-left: 6px; +`; + +const SpeakerAndActionWrapper = styled.div` + align-items: flex-start; + display: flex; + justify-content: start; +`; + +const SpeakerNameWrapper = styled.div` + font-size: 18px; + font-weight: 700; + padding: 0px !important; + margin-right: 3px; +`; + +const TimeAndFriendsOnlyWrapper = styled.div` + align-items: center; + display: flex; + justify-content: start; +`; + +const TwitterHandleWrapper = styled.span` + margin-right: 10px; +`; + +const TwitterName = styled.span` +`; + +const Wrapper = styled.div` + align-items: flex-start; + display: flex; + font-size: 14px; + justify-content: flex-start; + padding: 0px !important; +`; + +export default ActivitySpeakerCard; diff --git a/src/js/components/Activity/ActivityTidbitAddReaction.jsx b/src/js/components/Activity/ActivityTidbitAddReaction.jsx new file mode 100644 index 000000000..1f30b5660 --- /dev/null +++ b/src/js/components/Activity/ActivityTidbitAddReaction.jsx @@ -0,0 +1,229 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import styled from 'styled-components'; +import { withStyles, withTheme } from '@material-ui/core/styles'; +import { IconButton } from '@material-ui/core'; +import { Message, ThumbUp } from '@material-ui/icons'; // Reply, +import AppActions from '../../actions/AppActions'; +import ReactionActions from '../../actions/ReactionActions'; +import ReactionStore from '../../stores/ReactionStore'; +import { renderLog } from '../../utils/logging'; + + +class ActivityTidbitAddReaction extends Component { + constructor (props) { + super(props); + this.state = { + voterLikesThisItem: false, + }; + } + + componentDidMount () { + this.reactionStoreListener = ReactionStore.addListener(this.onReactionStoreChange.bind(this)); + this.onReactionStoreChange(); + } + + componentWillUnmount () { + this.reactionStoreListener.remove(); + } + + onReactionStoreChange () { + const { activityTidbitWeVoteId } = this.props; + // console.log('ActivityTidbitAddReaction onReactionStoreChange, activityTidbitWeVoteId:', activityTidbitWeVoteId); + const voterLikesThisItem = ReactionStore.voterLikesThisItem(activityTidbitWeVoteId); + this.setState({ + voterLikesThisItem, + }); + } + + onClickReactionLikeToggle = () => { + const { activityTidbitWeVoteId } = this.props; + // console.log('onClickShowActivityTidbitDrawer activityTidbitWeVoteId:', activityTidbitWeVoteId); + if (ReactionStore.voterLikesThisItem(activityTidbitWeVoteId)) { + ReactionActions.voterReactionLikeOffSave(activityTidbitWeVoteId); + this.setState({ + voterLikesThisItem: false, + }); + } else { + ReactionActions.voterReactionLikeOnSave(activityTidbitWeVoteId); + this.setState({ + voterLikesThisItem: true, + }); + } + } + + onClickShowActivityTidbitDrawer = () => { + const { activityTidbitWeVoteId } = this.props; + // console.log('onClickShowActivityTidbitDrawer activityTidbitWeVoteId:', activityTidbitWeVoteId); + AppActions.setActivityTidbitWeVoteIdForDrawer(activityTidbitWeVoteId); + AppActions.setShowActivityTidbitDrawer(true); + } + + render () { + renderLog('ActivityTidbitAddReaction'); // Set LOG_RENDER_EVENTS to log all renders + const { activityTidbitWeVoteId, classes } = this.props; + const { voterLikesThisItem } = this.state; + if (!activityTidbitWeVoteId) { + return null; + } + return ( + + + + + + + Like + + + + + {/* */} + {/* */} + + + + + + Comment + + + + {/* */} + {/* */} + {/* */} + {/* */} + {/* Share */} + {/* */} + {/* */} + {/* */} + + + ); + } +} +ActivityTidbitAddReaction.propTypes = { + activityTidbitWeVoteId: PropTypes.string.isRequired, + classes: PropTypes.object, +}; + +const styles = () => ({ + commentsButton: { + color: 'rgba(17, 17, 17, .4)', + '&:hover': { + backgroundColor: 'transparent', + }, + padding: 6, + }, + commentsIcon: { + color: 'rgba(17, 17, 17, .4)', + '&:hover': { + backgroundColor: 'transparent', + }, + width: 19, + }, + likeButton: { + color: 'rgba(17, 17, 17, .4)', + '&:hover': { + backgroundColor: 'transparent', + }, + padding: 6, + }, + likeButtonSelected: { + color: '#2e3c5d', + '&:hover': { + backgroundColor: 'transparent', + }, + padding: 6, + }, + likeIcon: { + color: 'rgba(17, 17, 17, .4)', + '&:hover': { + backgroundColor: 'transparent', + }, + width: 19, + }, + likeIconSelected: { + color: '#2e3c5d', + '&:hover': { + backgroundColor: 'transparent', + }, + width: 19, + }, + sendReply: { + color: 'rgba(17, 17, 17, .4)', + '&:hover': { + backgroundColor: 'transparent', + }, + padding: 6, + }, + shareIcon: { + transform: 'scaleX(-1)', + position: 'relative', + top: -1, + }, +}); + +const CommentWrapper = styled.div` +`; + +const CommentTextWrapper = styled.div` + font-size: 14px; + padding-left: 4px; +`; + +const LeftColumnWrapper = styled.div` + align-items: center; + display: flex; + justify-content: flex-start; + width: 100%; +`; + +// const CenterColumnWrapper = styled.div` +// align-items: center; +// display: flex; +// justify-content: center; +// width: 100%; +// `; + +const RightColumnWrapper = styled.div` + align-items: center; + display: flex; + justify-content: flex-end; + width: 100%; +`; + +const LikeTextWrapper = styled.div` + font-size: 14px; + padding-left: 4px; +`; + +const LikeWrapper = styled.div` +`; + +// const ShareWrapper = styled.div` +// `; + +const Wrapper = styled.div` + align-items: center; + display: flex; + font-size: 14px; + justify-content: space-between; + padding: 0px !important; +`; + +export default withTheme(withStyles(styles)(ActivityTidbitAddReaction)); diff --git a/src/js/components/Activity/ActivityTidbitComments.jsx b/src/js/components/Activity/ActivityTidbitComments.jsx new file mode 100644 index 000000000..bc15b8551 --- /dev/null +++ b/src/js/components/Activity/ActivityTidbitComments.jsx @@ -0,0 +1,519 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import styled from 'styled-components'; +import { withStyles, withTheme } from '@material-ui/core/styles'; +import { IconButton } from '@material-ui/core'; +import { AccountCircle, MoreHoriz } from '@material-ui/icons'; +import ActivityCommentAdd from './ActivityCommentAdd'; +import ActivityStore from '../../stores/ActivityStore'; +import AppActions from '../../actions/AppActions'; +import ChildCommentList from './ChildCommentList'; +import ReactionStore from '../../stores/ReactionStore'; +import ReactionActions from '../../actions/ReactionActions'; +import { renderLog } from '../../utils/logging'; +import { timeFromDate } from '../../utils/dateFormat'; +import VoterStore from '../../stores/VoterStore'; + + +const STARTING_NUMBER_OF_PARENT_COMMENTS_TO_DISPLAY = 1; + +class ActivityTidbitComments extends Component { + constructor (props) { + super(props); + this.state = { + addChildCommentOpenByParentCommentWeVoteId: {}, + commentWeVoteIdBeingEditedNow: '', + commentsInTree: [], + numberOfParentCommentsToDisplay: STARTING_NUMBER_OF_PARENT_COMMENTS_TO_DISPLAY, + totalNumberOfParentComments: 0, + voterLikesThisItemByWeVoteId: {}, + voterWeVoteId: '', + }; + } + + componentDidMount () { + // console.log('ActivityTidbitComments componentDidMount'); + this.activityStoreListener = ActivityStore.addListener(this.onActivityStoreChange.bind(this)); + this.reactionStoreListener = ReactionStore.addListener(this.onReactionStoreChange.bind(this)); + this.voterStoreListener = VoterStore.addListener(this.onVoterStoreChange.bind(this)); + this.onActivityStoreChange(); + this.onReactionStoreChange(); + this.onVoterStoreChange(); + } + + componentWillUnmount () { + this.activityStoreListener.remove(); + this.reactionStoreListener.remove(); + this.voterStoreListener.remove(); + } + + onActivityStoreChange () { + const { activityTidbitWeVoteId } = this.props; + const commentsInTree = ActivityStore.getActivityCommentsInTreeByTidbitWeVoteId(activityTidbitWeVoteId); + const totalNumberOfParentComments = commentsInTree.length || 0; + this.setState({ + commentsInTree, + totalNumberOfParentComments, + }); + } + + onReactionStoreChange () { + const { activityTidbitWeVoteId } = this.props; + const { voterLikesThisItemByWeVoteId } = this.state; + // console.log('ActivityTidbitReactionsSummary onReactionStoreChange, activityTidbitWeVoteId:', activityTidbitWeVoteId); + const voterWeVoteId = VoterStore.getVoterWeVoteId(); + const reactionLikesListUnderActivityTidbitWeVoteId = ReactionStore.getReactionLikesByParentActivityTidbitWeVoteId(activityTidbitWeVoteId); + // For interaction speed we work off voterLikesThisItemByWeVoteId, so when new data comes in modify that + // console.log('onReactionStoreChange reactionLikesListUnderActivityTidbitWeVoteId:', reactionLikesListUnderActivityTidbitWeVoteId); + reactionLikesListUnderActivityTidbitWeVoteId.forEach((reactionLike) => { + if (reactionLike.voter_we_vote_id === voterWeVoteId) { + if (ReactionStore.voterLikesThisItem(reactionLike.liked_item_we_vote_id)) { + voterLikesThisItemByWeVoteId[reactionLike.liked_item_we_vote_id] = true; + } + } + }); + this.setState({ + voterLikesThisItemByWeVoteId, + }); + } + + onVoterStoreChange () { + const voterWeVoteId = VoterStore.getVoterWeVoteId(); + this.setState({ + voterWeVoteId, + }); + } + + addChildSavedFunction = (parentCommentWeVoteId) => { + const { addChildCommentOpenByParentCommentWeVoteId } = this.state; + if (parentCommentWeVoteId) { + addChildCommentOpenByParentCommentWeVoteId[parentCommentWeVoteId] = false; + this.setState({ + addChildCommentOpenByParentCommentWeVoteId, + }); + } + // console.log('addChildSavedFunction addChildCommentOpenByParentCommentWeVoteId:', addChildCommentOpenByParentCommentWeVoteId); + } + + commentEditSavedFunction = () => { + // console.log('commentEditSavedFunction'); + this.setState({ + commentWeVoteIdBeingEditedNow: '', + }); + } + + onClickEditComment = (commentWeVoteId) => { + // const { commentWeVoteIdBeingEditedNow } = this.state; + // console.log('onClickEditComment, commentWeVoteId:', commentWeVoteId); + this.setState({ + commentWeVoteIdBeingEditedNow: commentWeVoteId, + }); + } + + onClickEditCommentCancel = () => { + // console.log('onClickEditCommentCancel'); + this.setState({ + commentWeVoteIdBeingEditedNow: '', + }); + } + + onClickReactionLikeToggle = (likedItemWeVoteId) => { + // console.log('onClickReactionLikeToggle likedItemWeVoteId:', likedItemWeVoteId); + const { activityTidbitWeVoteId } = this.props; + const { voterLikesThisItemByWeVoteId } = this.state; + if (ReactionStore.voterLikesThisItem(likedItemWeVoteId)) { + ReactionActions.voterReactionLikeOffSave(likedItemWeVoteId); + voterLikesThisItemByWeVoteId[likedItemWeVoteId] = false; + } else { + ReactionActions.voterReactionLikeOnSave(likedItemWeVoteId, activityTidbitWeVoteId); + voterLikesThisItemByWeVoteId[likedItemWeVoteId] = true; + } + this.setState({ + voterLikesThisItemByWeVoteId, + }); + } + + onClickToggleReplyToComment = (parentCommentWeVoteId) => { + const { addChildCommentOpenByParentCommentWeVoteId } = this.state; + // console.log('onClickToggleReplyToComment addChildCommentOpenByParentCommentWeVoteId', addChildCommentOpenByParentCommentWeVoteId); + if (parentCommentWeVoteId) { + // Set it the first time to false + if (!addChildCommentOpenByParentCommentWeVoteId[parentCommentWeVoteId]) { + addChildCommentOpenByParentCommentWeVoteId[parentCommentWeVoteId] = false; + } + if (addChildCommentOpenByParentCommentWeVoteId[parentCommentWeVoteId]) { + addChildCommentOpenByParentCommentWeVoteId[parentCommentWeVoteId] = false; + } else { + addChildCommentOpenByParentCommentWeVoteId[parentCommentWeVoteId] = true; + } + this.setState({ + addChildCommentOpenByParentCommentWeVoteId, + }); + } + // console.log('onClickReactionLikeToggle parentCommentWeVoteId:', parentCommentWeVoteId, ', addChildCommentOpenByParentCommentWeVoteId[parentCommentWeVoteId]', addChildCommentOpenByParentCommentWeVoteId[parentCommentWeVoteId]); + } + + onClickShowActivityTidbitDrawer = () => { + const { activityTidbitWeVoteId } = this.props; + // console.log('onClickShowActivityTidbitDrawer activityTidbitWeVoteId:', activityTidbitWeVoteId); + AppActions.setActivityTidbitWeVoteIdForDrawer(activityTidbitWeVoteId); + AppActions.setShowActivityTidbitDrawer(true); + } + + increaseNumberOfActivityTidbitsToDisplay = () => { + // console.log('increaseNumberOfActivityTidbitsToDisplay'); + let { numberOfParentCommentsToDisplay } = this.state; + numberOfParentCommentsToDisplay += 2; + this.positionItemTimer = setTimeout(() => { + this.setState({ + numberOfParentCommentsToDisplay, + }); + }, 500); + } + + render () { + renderLog('ActivityTidbitComments'); // Set LOG_RENDER_EVENTS to log all renders + // console.log('ActivityTidbitComments render'); + const { activityTidbitWeVoteId, classes, editingTurnedOff, showAllParentComments } = this.props; + const { + addChildCommentOpenByParentCommentWeVoteId, commentWeVoteIdBeingEditedNow, commentsInTree, + numberOfParentCommentsToDisplay, totalNumberOfParentComments, + voterLikesThisItemByWeVoteId, voterWeVoteId, + } = this.state; + if (!commentsInTree || commentsInTree.length === 0) { + return null; + } + const hideParentCommentBottomLinks = !showAllParentComments; + let likeButtonSelected = false; + let numberOfCommentsDisplayed = 0; + let commenterIsVoter = false; + let showMoreHasBeenShown = false; + // console.log('voterLikesThisItemByWeVoteId:', voterLikesThisItemByWeVoteId); + return ( + + {commentsInTree.map((parentComment) => { + // console.log('oneActivityTidbit:', oneActivityTidbit); + // console.log('numberOfCommentsDisplayed:', numberOfCommentsDisplayed); + if (!parentComment || !parentComment.commenter_name) { + // console.log('Missing oneActivityTidbit.commenter_name:', parentComment); + return null; + } + if (showAllParentComments) { + // Don't stop + } else if (numberOfCommentsDisplayed >= numberOfParentCommentsToDisplay) { + if (showMoreHasBeenShown) { + return null; + } else { + showMoreHasBeenShown = true; + return ( + + + show all + {' '} + {totalNumberOfParentComments} + {' '} + comments + + + ); + } + } + numberOfCommentsDisplayed += 1; + likeButtonSelected = !!(voterLikesThisItemByWeVoteId[parentComment.we_vote_id]); + commenterIsVoter = (voterWeVoteId === parentComment.commenter_voter_we_vote_id); + // console.log('parentComment.comment_list:', parentComment.comment_list); + return ( + + + {(parentComment.commenter_profile_image_url_tiny) ? ( + + + + ) : ( + + + + )} + + + {commentWeVoteIdBeingEditedNow === parentComment.we_vote_id ? ( + + ) : ( + + {hideParentCommentBottomLinks ? ( + + {parentComment.statement_text} + + ) : ( + + {parentComment.statement_text} + + )} + {(commenterIsVoter && !editingTurnedOff) && ( + this.onClickEditComment(parentComment.we_vote_id)} + > + + + )} + + )} + {(!hideParentCommentBottomLinks) && ( + + {(parentComment.date_created) && ( + + {timeFromDate(parentComment.date_created)} + + )} + + this.onClickReactionLikeToggle(parentComment.we_vote_id)} + > + + Like + + + + + this.onClickToggleReplyToComment(parentComment.we_vote_id)} + > + + Reply + + + + {(commentWeVoteIdBeingEditedNow === parentComment.we_vote_id) && ( + + this.onClickEditCommentCancel(parentComment.we_vote_id)} + > + + Cancel Edit + + + + )} + + )} + {(parentComment.comment_list && parentComment.comment_list.length > 0) && ( + + + + )} + {(totalNumberOfParentComments === 1) && ( + + )} + {(addChildCommentOpenByParentCommentWeVoteId && addChildCommentOpenByParentCommentWeVoteId[parentComment.we_vote_id]) && ( + + )} + + + ); + })} + + ); + } +} +ActivityTidbitComments.propTypes = { + activityTidbitWeVoteId: PropTypes.string.isRequired, + classes: PropTypes.object, + editingTurnedOff: PropTypes.bool, + showAllParentComments: PropTypes.bool, +}; + +const styles = () => ({ + accountCircle: { + color: 'rgba(17, 17, 17, .4)', + '&:hover': { + backgroundColor: 'transparent', + }, + width: 32, + height: 32, + }, + cancelButton: { + color: 'rgba(17, 17, 17, .4)', + '&:hover': { + backgroundColor: 'transparent', + }, + padding: '4px 6px 6px 6px', + }, + likeButton: { + color: 'rgba(17, 17, 17, .4)', + '&:hover': { + backgroundColor: 'transparent', + }, + padding: '4px 6px 6px 6px', + }, + likeButtonSelected: { + color: '#2e3c5d', + fontWeight: 600, + '&:hover': { + backgroundColor: 'transparent', + }, + padding: '4px 6px 6px 6px', + }, + numberOfLikesButton: { + padding: 6, + }, + numberOfCommentsButton: { + color: 'rgba(17, 17, 17, .4)', + '&:hover': { + backgroundColor: 'transparent', + }, + padding: 6, + }, + replyButton: { + color: 'rgba(17, 17, 17, .4)', + '&:hover': { + backgroundColor: 'transparent', + }, + padding: '4px 6px 6px 6px', + }, +}); + +const ActivityCommentEditWrapper = styled.div` +`; + +const ActivityImage = styled.img` + border-radius: 4px; + width: 32px; +`; + +const CancelTextWrapper = styled.div` + color: #999; + font-size: 11px; + padding-left: 4px; +`; + +const CancelWrapper = styled.div` +`; + +const ChildCommentListWrapper = styled.div` +`; + +const CommentActivityTime = styled.div` + color: #999; + font-size: 11px; + font-weight: 400; + margin-right: 6px; +`; + +const CommentWrapper = styled.div` + align-items: flex-start; + display: flex; + justify-content: space-between; + margin-bottom: 3px; +`; + +const LikeTextWrapper = styled.div` + ${({ likeButtonSelected }) => (likeButtonSelected ? 'color: #2e3c5d;' : 'color: #999;')} + font-size: 11px; + ${({ likeButtonSelected }) => (likeButtonSelected ? 'font-weight: 600;' : '')} + padding-left: 4px; +`; + +const LikeWrapper = styled.div` +`; + +const ParentCommentBottomLinks = styled.div` + align-items: center; + display: flex; + justify-content: start; + margin-bottom: 3px; + margin-left: 12px; +`; + +const ParentCommentStatementText = styled.div` + width: 100%; +`; + +const ParentCommentStatementTextOutsideWrapper = styled.div` + align-items: flex-start; + display: flex; + justify-content: space-between; + border-radius: 32px; + background: rgb(224, 224, 224); + color: #2e3c5d; + font-size: 16px; + font-weight: 500; + padding: 4px 12px; + margin-top: 0px; +`; + +const ParentCommentWrapper = styled.div` + width: 100%; +`; + +const ParentPhotoDiv = styled.div` +`; + +const ReplyTextWrapper = styled.div` + color: #999; + font-size: 11px; + padding-left: 4px; +`; + +const ReplyWrapper = styled.div` +`; + +const ShowMoreLink = styled.div` + color: rgba(17, 17, 17, .4); +`; + +const ShowMoreWrapper = styled.div` + display: flex; + justify-content: center; + margin-top: -5px; + width: 100%; +`; + +const SpacerBelowComments = styled.div` + height: 4px; +`; + +const SpeakerAvatar = styled.div` + background: transparent; + display: flex; + justify-content: center; + margin-right: 4px; + position: relative; +`; + +const Wrapper = styled.div` + margin-bottom: 6px !important; + padding: 0px !important; +`; + +export default withTheme(withStyles(styles)(ActivityTidbitComments)); diff --git a/src/js/components/Activity/ActivityTidbitDrawer.jsx b/src/js/components/Activity/ActivityTidbitDrawer.jsx new file mode 100644 index 000000000..e00acd52d --- /dev/null +++ b/src/js/components/Activity/ActivityTidbitDrawer.jsx @@ -0,0 +1,163 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import styled from 'styled-components'; +import { withStyles, withTheme } from '@material-ui/core/styles'; +import { Drawer, IconButton } from '@material-ui/core'; +import { cordovaDrawerTopMargin } from '../../utils/cordovaOffsets'; +import ActivityCommentAdd from './ActivityCommentAdd'; +import ActivityStore from '../../stores/ActivityStore'; +import ActivityTidbitAddReaction from './ActivityTidbitAddReaction'; +import ActivityTidbitComments from './ActivityTidbitComments'; +import ActivityTidbitItem from './ActivityTidbitItem'; +import ActivityTidbitReactionsSummary from './ActivityTidbitReactionsSummary'; +import { hideZenDeskHelpVisibility, showZenDeskHelpVisibility } from '../../utils/applicationUtils'; +import { historyPush, isCordova } from '../../utils/cordovaUtils'; +import DelayedLoad from '../Widgets/DelayedLoad'; +import { renderLog } from '../../utils/logging'; +import { startsWith } from '../../utils/textFormat'; + + +class ActivityTidbitDrawer extends Component { + constructor (props) { + super(props); + this.state = { + modalOpen: this.props.modalOpen, + }; + } + + componentDidMount () { + this.onActivityStoreChange(); + this.activityStoreListener = ActivityStore.addListener(this.onActivityStoreChange.bind(this)); + hideZenDeskHelpVisibility(); + } + + componentWillUnmount () { + this.activityStoreListener.remove(); + showZenDeskHelpVisibility(); + } + + onActivityStoreChange () { + const { activityTidbitWeVoteId } = this.props; + const activityTidbit = ActivityStore.getActivityTidbitByWeVoteId(activityTidbitWeVoteId); + const { + speaker_voter_we_vote_id: speakerVoterWeVoteId, + } = activityTidbit; + // console.log('ActivityTidbitItem onActivityStoreChange, activityTidbitWeVoteId:', activityTidbitWeVoteId, ', statementText:', statementText); + this.setState({ + speakerVoterWeVoteId, + }); + } + + closeActivityTidbitDrawer = () => { + const { activityTidbitWeVoteId } = this.props; + this.setState({ modalOpen: false }); + setTimeout(() => { + this.props.toggleFunction(); + }, 500); + const { pathname: pathnameRaw, href: hrefRaw } = window.location; + let pathname = pathnameRaw; + if (isCordova()) { + pathname = hrefRaw.replace(/file:\/\/.*?Vote.app\/www\/index.html#\//, ''); + } + if (typeof pathname !== 'undefined' && pathname && startsWith('/news/a/', pathname)) { + historyPush(`/news#${activityTidbitWeVoteId}`); + } + } + + render () { + // console.log(this.props.candidate_we_vote_id); + renderLog('ActivityTidbitDrawer'); // Set LOG_RENDER_EVENTS to log all renders + const { activityTidbitWeVoteId, classes } = this.props; + const { modalOpen, speakerVoterWeVoteId } = this.state; + + return ( + <> + + + + + + {(activityTidbitWeVoteId && speakerVoterWeVoteId) ? ( + <> + + + + + + + + + ) : ( + +
    + That discussion item could not be found, or you are not allowed to see it. +
    +
    + Please make sure you are signed in so you can see all of your friend's amazing thoughts! +
    +
    + )} +
    +
    + + ); + } +} +ActivityTidbitDrawer.propTypes = { + activityTidbitWeVoteId: PropTypes.string.isRequired, + classes: PropTypes.object, + modalOpen: PropTypes.bool, + toggleFunction: PropTypes.func.isRequired, +}; + +const styles = () => ({ + drawerClasses: { + marginTop: cordovaDrawerTopMargin(), + maxWidth: '550px !important', + '& *': { + maxWidth: '550px !important', + }, + '@media(max-width: 576px)': { + maxWidth: '360px !important', + '& *': { + maxWidth: '360px !important', + }, + }, + }, + closeButton: { + marginRight: 'auto', + }, +}); + +const ActivityTidbitItemWrapper = styled.div` +`; + +const ActivityTidbitDrawerInnerWrapper = styled.div` + margin-left: 12px; + margin-right: 12px; +`; +export default withTheme(withStyles(styles)(ActivityTidbitDrawer)); diff --git a/src/js/components/Activity/ActivityTidbitItem.jsx b/src/js/components/Activity/ActivityTidbitItem.jsx new file mode 100644 index 000000000..0d1fdbc11 --- /dev/null +++ b/src/js/components/Activity/ActivityTidbitItem.jsx @@ -0,0 +1,229 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import styled from 'styled-components'; +import Alert from 'react-bootstrap/Alert'; +import { Link } from 'react-router-dom'; +import { withStyles, withTheme } from '@material-ui/core/styles'; +import { MoreHoriz } from '@material-ui/icons'; +import ActivityPositionList from './ActivityPositionList'; +import ActivityPostModal from './ActivityPostModal'; +import ActivitySpeakerCard from './ActivitySpeakerCard'; +import ActivityStore from '../../stores/ActivityStore'; +import AppActions from '../../actions/AppActions'; +import DelayedLoad from '../Widgets/DelayedLoad'; +import OrganizationStore from '../../stores/OrganizationStore'; +import { renderLog } from '../../utils/logging'; +import VoterStore from '../../stores/VoterStore'; + + +class ActivityTidbitItem extends Component { + constructor (props) { + super(props); + this.state = { + isActivityPost: false, + showActivityPostModal: false, + speakerIsVoter: false, + speakerOrganizationWeVoteId: '', + statementText: '', + }; + } + + componentDidMount () { + this.onActivityStoreChange(); + this.activityStoreListener = ActivityStore.addListener(this.onActivityStoreChange.bind(this)); + this.organizationStoreListener = OrganizationStore.addListener(this.onOrganizationStoreChange.bind(this)); + } + + componentWillUnmount () { + this.activityStoreListener.remove(); + this.organizationStoreListener.remove(); + } + + onActivityStoreChange () { + const { activityTidbitWeVoteId } = this.props; + const activityTidbit = ActivityStore.getActivityTidbitByWeVoteId(activityTidbitWeVoteId); + const { + kind_of_activity: kindOfActivity, + position_we_vote_id_list: positionWeVoteIdList, + speaker_name: speakerName, + speaker_organization_we_vote_id: speakerOrganizationWeVoteId, + speaker_voter_we_vote_id: speakerVoterWeVoteId, + statement_text: statementText, + } = activityTidbit; + // console.log('ActivityTidbitItem onActivityStoreChange, activityTidbitWeVoteId:', activityTidbitWeVoteId, ', statementText:', statementText); + const voter = VoterStore.getVoter(); + const speakerIsVoter = (voter.we_vote_id === speakerVoterWeVoteId); + let isActivityNoticeSeed = false; + let isActivityPost = false; + if (kindOfActivity === 'ACTIVITY_NOTICE_SEED') { + isActivityNoticeSeed = true; + } else if (kindOfActivity === 'ACTIVITY_POST') { + isActivityPost = true; + } + if (isActivityNoticeSeed) { + this.updatePositionsEnteredState(positionWeVoteIdList); + } + this.setState({ + isActivityPost, + speakerIsVoter, + speakerName, + speakerOrganizationWeVoteId, + statementText, + }); + } + + onOrganizationStoreChange () { + const { activityTidbitWeVoteId } = this.props; + const activityTidbit = ActivityStore.getActivityTidbitByWeVoteId(activityTidbitWeVoteId); + const { + position_we_vote_id_list: positionWeVoteIdList, + } = activityTidbit; + this.updatePositionsEnteredState(positionWeVoteIdList); + } + + onClickShowActivityTidbitDrawer = () => { + const { activityTidbitWeVoteId } = this.props; + // console.log('onClickShowActivityTidbitDrawer activityTidbitWeVoteId:', activityTidbitWeVoteId); + AppActions.setActivityTidbitWeVoteIdForDrawer(activityTidbitWeVoteId); + AppActions.setShowActivityTidbitDrawer(true); + } + + updatePositionsEnteredState = (positionWeVoteIdList) => { + const newPositionsEntered = []; + let onePosition = {}; + let positionWeVoteId = ''; + // console.log('positionWeVoteIdList:', positionWeVoteIdList); + for (let count = 0; count < positionWeVoteIdList.length; count++) { + positionWeVoteId = positionWeVoteIdList[count]; + if (positionWeVoteId) { + onePosition = OrganizationStore.getPositionByPositionWeVoteId(positionWeVoteId); + // console.log('onePosition:', onePosition); + if (onePosition && onePosition.position_we_vote_id) { + newPositionsEntered.push(onePosition); + } + } + } + // console.log('newPositionsEntered:', newPositionsEntered); + this.setState({ + newPositionsEntered, + }); + } + + toggleActivityPostModal = () => { + const { showActivityPostModal } = this.state; + // console.log('toggleActivityPostModal showActivityPostModal:', showActivityPostModal); + this.setState({ + showActivityPostModal: !showActivityPostModal, + }); + } + + render () { + renderLog('ActivityTidbitItem'); // Set LOG_RENDER_EVENTS to log all renders + const { activityTidbitWeVoteId, startingNumberOfPositionsToDisplay } = this.props; + const { + externalUniqueId, isActivityPost, newPositionsEntered, + showActivityPostModal, speakerIsVoter, speakerName, speakerOrganizationWeVoteId, statementText, + } = this.state; + if (!activityTidbitWeVoteId) { + return null; + } + const startingNumberOfPositionsToDisplayLocal = startingNumberOfPositionsToDisplay || 1; + return ( + + + + {(isActivityPost && speakerIsVoter) && ( + + + + )} + + {(!speakerName && speakerIsVoter) && ( + + + Please add your name so we can show this post. + {' '} + + add name + + + + )} + + {(newPositionsEntered && newPositionsEntered.length) ? ( + + + + + + ) : ( + + )} + {isActivityPost && ( + + {/* onClick={this.onClickShowActivityTidbitDrawer} */} + {statementText} + + )} + {showActivityPostModal && ( + + )} + + ); + } +} +ActivityTidbitItem.propTypes = { + activityTidbitWeVoteId: PropTypes.string.isRequired, + startingNumberOfPositionsToDisplay: PropTypes.number, +}; + +const styles = () => ({ + buttonOutlinedPrimary: { + background: 'white', + }, +}); + +const ActivityPositionListWrapper = styled.div` + margin-top: 12px; +`; + +const ActivityPositionListMissingWrapper = styled.div` + margin-bottom: 8px; +`; + +const ActivityPostEditWrapper = styled.div` +`; + +const ActivityPostWrapper = styled.div` + font-size: 18px; +`; + +const ActivitySpeakerCardWrapper = styled.div` + align-items: flex-start; + display: flex; + justify-content: space-between; + width: 100%; +`; + +const MissingNameAlertWrapper = styled.div` + margin-top: 3px; +`; + +const Wrapper = styled.div` +`; + +export default withTheme(withStyles(styles)(ActivityTidbitItem)); diff --git a/src/js/components/Activity/ActivityTidbitReactionsSummary.jsx b/src/js/components/Activity/ActivityTidbitReactionsSummary.jsx new file mode 100644 index 000000000..cd4c6eaa7 --- /dev/null +++ b/src/js/components/Activity/ActivityTidbitReactionsSummary.jsx @@ -0,0 +1,239 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import styled from 'styled-components'; +import { withStyles, withTheme } from '@material-ui/core/styles'; +import { IconButton } from '@material-ui/core'; +import { ThumbUp } from '@material-ui/icons'; +import ActivityStore from '../../stores/ActivityStore'; +import AppActions from '../../actions/AppActions'; +import { renderLog } from '../../utils/logging'; +import ReactionStore from '../../stores/ReactionStore'; +import StickyPopover from '../Ballot/StickyPopover'; + + +class ActivityTidbitReactionsSummary extends Component { + constructor (props) { + super(props); + this.state = { + numberOfComments: 0, + numberOfLikes: 0, + // namesOfLikesList: [], + }; + } + + componentDidMount () { + this.activityStoreListener = ActivityStore.addListener(this.onActivityStoreChange.bind(this)); + this.reactionStoreListener = ReactionStore.addListener(this.onReactionStoreChange.bind(this)); + this.onActivityStoreChange(); + this.onReactionStoreChange(); + } + + componentWillUnmount () { + this.activityStoreListener.remove(); + this.reactionStoreListener.remove(); + } + + onActivityStoreChange () { + const { activityTidbitWeVoteId } = this.props; + const numberOfComments = ActivityStore.getActivityCommentAllCountByTidbitWeVoteId(activityTidbitWeVoteId); + this.setState({ + numberOfComments, + }); + } + + onReactionStoreChange () { + const { activityTidbitWeVoteId } = this.props; + // console.log('ActivityTidbitReactionsSummary onReactionStoreChange, activityTidbitWeVoteId:', activityTidbitWeVoteId); + const voterWeVoteIdsWhoLikedThisActivityTidbit = ReactionStore.getVoterWeVoteIdListByLikedItemWeVoteId(activityTidbitWeVoteId); + const reactionLikesList = ReactionStore.getReactionLikesByLikedItemWeVoteId(activityTidbitWeVoteId); + const numberOfLikes = voterWeVoteIdsWhoLikedThisActivityTidbit.length; + this.setState({ + reactionLikesList, + numberOfLikes, + }); + } + + onClickShowActivityTidbitDrawer = () => { + const { activityTidbitWeVoteId } = this.props; + // console.log('onClickShowActivityTidbitDrawer activityTidbitWeVoteId:', activityTidbitWeVoteId); + AppActions.setActivityTidbitWeVoteIdForDrawer(activityTidbitWeVoteId); + AppActions.setShowActivityTidbitDrawer(true); + } + + render () { + renderLog('ActivityTidbitReactionsSummary'); // Set LOG_RENDER_EVENTS to log all renders + const { activityTidbitWeVoteId, classes } = this.props; + const { numberOfComments, numberOfLikes, reactionLikesList } = this.state; + if (!activityTidbitWeVoteId || numberOfLikes === 0) { + return null; + } + const likesSummaryPopover = ( + + + + Who Liked + {' '} + + + + {reactionLikesList.map((reactionLike) => ( +
    {reactionLike.voter_display_name}
    + ))} +
    +
    + ); + + return ( + + + {(numberOfLikes) && ( + + + + + + {numberOfLikes} + + + + + )} + + + {!!(numberOfComments) && ( + + + + {numberOfComments} + {' '} + Comment + {numberOfComments === 1 ? '' : 's'} + + + + )} + + + ); + } +} +ActivityTidbitReactionsSummary.propTypes = { + activityTidbitWeVoteId: PropTypes.string.isRequired, + classes: PropTypes.object, +}; + +const styles = () => ({ + likeIcon: { + color: '#fff', + width: 12, + height: 12, + }, + numberOfLikesButton: { + backgroundColor: 'rgb(200, 200, 200)', + borderRadius: 12, + marginBottom: 2, + padding: '2px 5px', + '&:hover': { + backgroundColor: '#2e3c5d', + }, + }, + numberOfCommentsButton: { + color: 'rgba(17, 17, 17, .4)', + '&:hover': { + backgroundColor: 'transparent', + }, + padding: 6, + }, +}); + +const LeftColumnWrapper = styled.div` + align-items: center; + display: flex; + justify-content: flex-start; + width: 100%; +`; + +const RightColumnWrapper = styled.div` + align-items: center; + display: flex; + justify-content: flex-end; + width: 100%; +`; + +const CommentWrapper = styled.div` +`; + +const CommentTextWrapper = styled.div` + font-size: 10px; + padding-left: 4px; +`; + +const LikeTextWrapper = styled.div` + color: #fff; // #1fc06f + font-size: 10px; + font-weight: 500; + padding-left: 4px; +`; + +const LikeWrapper = styled.div` +`; + +const PopoverWrapper = styled.div` + width: 100%; + height: 100%; +`; + +const PopoverHeader = styled.div` + background: ${({ theme }) => theme.colors.brandBlue}; + padding: 4px 8px; + min-height: 35px; + color: white; + display: flex; + justify-content: flex-start; + align-items: center; + border-radius: 5px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +`; + +const PopoverTitleText = styled.div` + font-size: 14px; + font-weight: bold; + margin-right: 20px; +`; + +const PopoverBody = styled.div` + padding: 8px; + border-left: .5px solid #ddd; + border-right: .5px solid #ddd; + border-bottom: .5px solid #ddd; + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; +`; + +const Wrapper = styled.div` + align-items: center; + border-bottom: 1px solid #e8e8e8; + display: flex; + justify-content: space-between; + margin-bottom: 0px !important; + padding: 0px !important; +`; + +export default withTheme(withStyles(styles)(ActivityTidbitReactionsSummary)); diff --git a/src/js/components/Activity/ChildCommentList.jsx b/src/js/components/Activity/ChildCommentList.jsx new file mode 100644 index 000000000..fc519399d --- /dev/null +++ b/src/js/components/Activity/ChildCommentList.jsx @@ -0,0 +1,469 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import styled from 'styled-components'; +import { withStyles, withTheme } from '@material-ui/core/styles'; +import { IconButton } from '@material-ui/core'; +import { AccountCircle, MoreHoriz } from '@material-ui/icons'; +import ActivityCommentAdd from './ActivityCommentAdd'; +import ActivityStore from '../../stores/ActivityStore'; +import AppActions from '../../actions/AppActions'; +import ReactionStore from '../../stores/ReactionStore'; +import ReactionActions from '../../actions/ReactionActions'; +import { renderLog } from '../../utils/logging'; +import { timeFromDate } from '../../utils/dateFormat'; +import VoterStore from '../../stores/VoterStore'; + + +const STARTING_NUMBER_OF_PARENT_COMMENTS_TO_DISPLAY = 1; + +class ChildCommentList extends Component { + constructor (props) { + super(props); + this.state = { + commentWeVoteIdBeingEditedNow: '', + childCommentsList: [], + numberOfChildCommentsToDisplay: STARTING_NUMBER_OF_PARENT_COMMENTS_TO_DISPLAY, + voterLikesThisItemByWeVoteId: {}, + voterWeVoteId: '', + }; + } + + componentDidMount () { + // console.log('ChildCommentList componentDidMount'); + this.activityStoreListener = ActivityStore.addListener(this.onActivityStoreChange.bind(this)); + this.reactionStoreListener = ReactionStore.addListener(this.onReactionStoreChange.bind(this)); + this.voterStoreListener = VoterStore.addListener(this.onVoterStoreChange.bind(this)); + this.onActivityStoreChange(); + this.onReactionStoreChange(); + this.onVoterStoreChange(); + } + + componentWillUnmount () { + this.activityStoreListener.remove(); + this.reactionStoreListener.remove(); + this.voterStoreListener.remove(); + } + + onActivityStoreChange () { + // console.log('ChildCommentList onActivityStoreChange'); + const { parentCommentWeVoteId } = this.props; + const childCommentsList = ActivityStore.getChildCommentsByParentCommentWeVoteId(parentCommentWeVoteId); + this.setState({ + childCommentsList, + }); + } + + onReactionStoreChange () { + // console.log('ChildCommentList onReactionStoreChange'); + const { activityTidbitWeVoteId } = this.props; + const { voterLikesThisItemByWeVoteId } = this.state; + // console.log('ActivityTidbitReactionsSummary onReactionStoreChange, activityTidbitWeVoteId:', activityTidbitWeVoteId); + const voterWeVoteId = VoterStore.getVoterWeVoteId(); + const reactionLikesListUnderActivityTidbitWeVoteId = ReactionStore.getReactionLikesByParentActivityTidbitWeVoteId(activityTidbitWeVoteId); + // For interaction speed we work off voterLikesThisItemByWeVoteId, so when new data comes in modify that + // console.log('onReactionStoreChange reactionLikesListUnderActivityTidbitWeVoteId:', reactionLikesListUnderActivityTidbitWeVoteId); + reactionLikesListUnderActivityTidbitWeVoteId.forEach((reactionLike) => { + if (reactionLike.voter_we_vote_id === voterWeVoteId) { + if (ReactionStore.voterLikesThisItem(reactionLike.liked_item_we_vote_id)) { + voterLikesThisItemByWeVoteId[reactionLike.liked_item_we_vote_id] = true; + } + } + }); + this.setState({ + voterLikesThisItemByWeVoteId, + }); + } + + onVoterStoreChange () { + // console.log('ChildCommentList onVoterStoreChange'); + const voterWeVoteId = VoterStore.getVoterWeVoteId(); + this.setState({ + voterWeVoteId, + }); + } + + commentEditSavedFunction = () => { + // console.log('commentEditSavedFunction'); + this.setState({ + commentWeVoteIdBeingEditedNow: '', + }); + } + + onClickEditComment = (commentWeVoteId) => { + // console.log('onClickEditComment'); + // const { commentWeVoteIdBeingEditedNow } = this.state; + this.setState({ + commentWeVoteIdBeingEditedNow: commentWeVoteId, + }); + } + + onClickEditCommentCancel = () => { + // console.log('onClickEditCommentCancel'); + this.setState({ + commentWeVoteIdBeingEditedNow: '', + }); + } + + onClickReactionLikeToggle = (likedItemWeVoteId) => { + // console.log('onClickReactionLikeToggle likedItemWeVoteId:', likedItemWeVoteId); + const { activityTidbitWeVoteId } = this.props; + const { voterLikesThisItemByWeVoteId } = this.state; + if (ReactionStore.voterLikesThisItem(likedItemWeVoteId)) { + ReactionActions.voterReactionLikeOffSave(likedItemWeVoteId); + voterLikesThisItemByWeVoteId[likedItemWeVoteId] = false; + } else { + ReactionActions.voterReactionLikeOnSave(likedItemWeVoteId, activityTidbitWeVoteId); + voterLikesThisItemByWeVoteId[likedItemWeVoteId] = true; + } + this.setState({ + voterLikesThisItemByWeVoteId, + }); + } + + onClickShowActivityTidbitDrawer = () => { + const { activityTidbitWeVoteId } = this.props; + // console.log('onClickShowActivityTidbitDrawer activityTidbitWeVoteId:', activityTidbitWeVoteId); + AppActions.setActivityTidbitWeVoteIdForDrawer(activityTidbitWeVoteId); + AppActions.setShowActivityTidbitDrawer(true); + } + + onClickToggleReplyToCommentLocal = () => { + const { parentCommentWeVoteId } = this.props; + // console.log('onClickToggleReplyToCommentLocal parentCommentWeVoteId:', parentCommentWeVoteId); + if (this.props.onClickToggleReplyToComment && parentCommentWeVoteId) { + this.props.onClickToggleReplyToComment(parentCommentWeVoteId); + } + } + + increaseNumberOfActivityTidbitsToDisplay = () => { + // console.log('increaseNumberOfActivityTidbitsToDisplay'); + let { numberOfChildCommentsToDisplay } = this.state; + numberOfChildCommentsToDisplay += 2; + this.positionItemTimer = setTimeout(() => { + this.setState({ + numberOfChildCommentsToDisplay, + }); + }, 500); + } + + render () { + renderLog('ChildCommentList'); // Set LOG_RENDER_EVENTS to log all renders + const { + activityTidbitWeVoteId, classes, editingTurnedOff, hideChildCommentBottomLinks, + parentCommentWeVoteId, showAllChildComments, + } = this.props; + const { + commentWeVoteIdBeingEditedNow, childCommentsList, + numberOfChildCommentsToDisplay, + voterLikesThisItemByWeVoteId, voterWeVoteId, + } = this.state; + // console.log('ChildCommentList render, childCommentsList:', childCommentsList); + if (!childCommentsList || childCommentsList.length === 0) { + return null; + } + let likeButtonSelected = false; + let numberOfCommentsDisplayed = 0; + let commenterIsVoter = false; + let showMoreHasBeenShown = false; + // console.log('voterLikesThisItemByWeVoteId:', voterLikesThisItemByWeVoteId); + return ( + + {childCommentsList.map((childComment) => { + // console.log('oneActivityTidbit:', oneActivityTidbit); + // console.log('numberOfCommentsDisplayed:', numberOfCommentsDisplayed); + if (!childComment || !childComment.commenter_name) { + // console.log('Missing oneActivityTidbit.commenter_name:', childComment); + return null; + } + if (showAllChildComments) { + // Don't stop + } else if (numberOfCommentsDisplayed >= numberOfChildCommentsToDisplay) { + if (showMoreHasBeenShown) { + return null; + } else { + showMoreHasBeenShown = true; + return ( + + + show more comments + + + ); + } + } + numberOfCommentsDisplayed += 1; + likeButtonSelected = !!(voterLikesThisItemByWeVoteId[childComment.we_vote_id]); + commenterIsVoter = (voterWeVoteId === childComment.commenter_voter_we_vote_id); + // console.log('likeButtonSelected:', likeButtonSelected); + return ( + + + {(childComment.commenter_profile_image_url_tiny) ? ( + + + + ) : ( + + + + )} + + + {commentWeVoteIdBeingEditedNow === childComment.we_vote_id ? ( + + ) : ( + + {hideChildCommentBottomLinks ? ( + + {childComment.statement_text} + + ) : ( + + {childComment.statement_text} + + )} + {(commenterIsVoter && !editingTurnedOff) && ( + this.onClickEditComment(childComment.we_vote_id)} + > + + + )} + + )} + {(!hideChildCommentBottomLinks) && ( + + {(childComment.date_created) && ( + + {timeFromDate(childComment.date_created)} + + )} + + this.onClickReactionLikeToggle(childComment.we_vote_id)} + > + + Like + + + + + + + Reply + + + + {(commentWeVoteIdBeingEditedNow === childComment.we_vote_id) && ( + + this.onClickEditCommentCancel(childComment.we_vote_id)} + > + + Cancel Edit + + + + )} + + )} + + + ); + })} + + ); + } +} +ChildCommentList.propTypes = { + activityTidbitWeVoteId: PropTypes.string.isRequired, + classes: PropTypes.object, + editingTurnedOff: PropTypes.bool, + hideChildCommentBottomLinks: PropTypes.bool, + onClickToggleReplyToComment: PropTypes.func, + parentCommentWeVoteId: PropTypes.string.isRequired, + showAllChildComments: PropTypes.bool, +}; + +const styles = () => ({ + accountCircle: { + color: 'rgba(17, 17, 17, .4)', + '&:hover': { + backgroundColor: 'transparent', + }, + width: 24, + height: 24, + }, + cancelButton: { + color: 'rgba(17, 17, 17, .4)', + '&:hover': { + backgroundColor: 'transparent', + }, + padding: '4px 6px 6px 6px', + }, + likeButton: { + color: 'rgba(17, 17, 17, .4)', + '&:hover': { + backgroundColor: 'transparent', + }, + padding: '4px 6px 6px 6px', + }, + likeButtonSelected: { + color: '#2e3c5d', + fontWeight: 600, + '&:hover': { + backgroundColor: 'transparent', + }, + padding: '4px 6px 6px 6px', + }, + numberOfLikesButton: { + padding: 6, + }, + numberOfCommentsButton: { + color: 'rgba(17, 17, 17, .4)', + '&:hover': { + backgroundColor: 'transparent', + }, + padding: 6, + }, + replyButton: { + color: 'rgba(17, 17, 17, .4)', + '&:hover': { + backgroundColor: 'transparent', + }, + padding: '4px 6px 6px 6px', + }, +}); + +const ActivityCommentEditWrapper = styled.div` +`; + +const ActivityImage = styled.img` + border-radius: 4px; + width: 24px; + height: 24px; +`; + +const CancelTextWrapper = styled.div` + color: #999; + font-size: 11px; + padding-left: 4px; +`; + +const CancelWrapper = styled.div` +`; + +const ChildCommentBottomLinks = styled.div` + align-items: center; + display: flex; + justify-content: start; + margin-bottom: 3px; + margin-left: 12px; +`; + +const ChildCommentStatementText = styled.div` + width: 100%; +`; + +const ChildCommentText = styled.div` + align-items: center; + display: flex; + justify-content: space-between; + border-radius: 32px; + background: rgb(224, 224, 224); + color: #2e3c5d; + font-size: 14px; + font-weight: 500; + padding: 2px 12px; + margin-top: 0px; +`; + +const ChildCommentPhotoDiv = styled.div` +`; + +const ChildCommentWrapper = styled.div` + width: 100%; +`; + +const CommentActivityTime = styled.div` + color: #999; + font-size: 11px; + font-weight: 400; + margin-right: 6px; +`; + +const CommentWrapper = styled.div` + align-items: flex-start; + display: flex; + justify-content: space-between; + margin-top: 3px; + margin-bottom: 3px; +`; + +const LikeTextWrapper = styled.div` + ${({ likeButtonSelected }) => (likeButtonSelected ? 'color: #2e3c5d;' : 'color: #999;')} + font-size: 11px; + ${({ likeButtonSelected }) => (likeButtonSelected ? 'font-weight: 600;' : '')} + padding-left: 4px; +`; + +const LikeWrapper = styled.div` +`; + +const ReplyTextWrapper = styled.div` + color: #999; + font-size: 11px; + padding-left: 4px; +`; + +const ReplyWrapper = styled.div` +`; + +const ShowMoreLink = styled.div` + color: rgba(17, 17, 17, .4); +`; + +const ShowMoreWrapper = styled.div` + display: flex; + justify-content: center; + margin-top: -5px; + width: 100%; +`; + +const SpeakerAvatar = styled.div` + background: transparent; + display: flex; + justify-content: center; + margin-right: 4px; + position: relative; +`; + +const Wrapper = styled.div` + margin-bottom: 6px !important; + padding: 0px !important; +`; + +export default withTheme(withStyles(styles)(ChildCommentList)); diff --git a/src/js/components/AddressBox.jsx b/src/js/components/AddressBox.jsx index 7274b5b33..8c6e38d60 100644 --- a/src/js/components/AddressBox.jsx +++ b/src/js/components/AddressBox.jsx @@ -1,88 +1,309 @@ -import React, { Component, PropTypes } from "react"; -import { Button, ButtonToolbar } from "react-bootstrap"; -import VoterStore from "../stores/VoterStore"; -import LoadingWheel from "../components/LoadingWheel"; -import VoterActions from "../actions/VoterActions"; - -export default class AddressBox extends Component { - static propTypes = { - history: PropTypes.object, - saveUrl: PropTypes.string.isRequired - }; +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { EditLocation } from '@material-ui/icons'; +import { withStyles } from '@material-ui/core/styles'; +import { Paper, InputBase, Button } from '@material-ui/core'; +import BallotActions from '../actions/BallotActions'; +import BallotStore from '../stores/BallotStore'; +import cookies from '../utils/cookies'; +import { historyPush, isCordova, isWebApp, prepareForCordovaKeyboard, + restoreStylesAfterCordovaKeyboard } from '../utils/cordovaUtils'; +import LoadingWheel from './LoadingWheel'; +import { renderLog } from '../utils/logging'; +import VoterActions from '../actions/VoterActions'; +import VoterStore from '../stores/VoterStore'; +class AddressBox extends Component { constructor (props) { - super(props); - this.state = { loading: false }; + super(props); + this.state = { + loading: false, + textForMapSearch: '', + ballotCaveat: '', + voterSavedAddress: false, + }; + + // this.autocomplete = React.createRef(); + + this.updateVoterAddress = this.updateVoterAddress.bind(this); + this.voterAddressSaveLocal = this.voterAddressSaveLocal.bind(this); + this.voterAddressSaveSubmit = this.voterAddressSaveSubmit.bind(this); + this.handleKeyPress = this.handleKeyPress.bind(this); + } + + // eslint-disable-next-line camelcase,react/sort-comp + UNSAFE_componentWillMount () { + prepareForCordovaKeyboard('AddressBox'); } componentDidMount () { - this.setState({ location: VoterStore.getAddress() }); - this.listener = VoterStore.addListener(this._onChange.bind(this)); + this.setState({ + textForMapSearch: VoterStore.getTextForMapSearch(), + ballotCaveat: BallotStore.getBallotCaveat(), + }); + this.voterStoreListener = VoterStore.addListener(this.onVoterStoreChange.bind(this)); + this.ballotStoreListener = BallotStore.addListener(this.onBallotStoreChange.bind(this)); + const { google } = window; // Cordova purposefully does not load the google maps API at this time + if (google !== undefined) { + const addressAutocomplete = new google.maps.places.Autocomplete(this.autoComplete); + addressAutocomplete.setComponentRestrictions({ country: 'us' }); + this.googleAutocompleteListener = addressAutocomplete.addListener('place_changed', this._placeChanged.bind(this, addressAutocomplete)); + } else if (isWebApp()) { + console.log('ERROR: Google Maps API IS NOT LOADED'); + } } - componentWillUnmount (){ - this.listener.remove(); + shouldComponentUpdate (nextProps, nextState) { + if (this.state.loading !== nextState.loading) { + return true; + } + if (this.state.voterSavedAddress !== nextState.voterSavedAddress) { + return true; + } + if (this.state.textForMapSearch !== nextState.textForMapSearch) { + return true; + } + if (this.state.ballotCaveat !== nextState.ballotCaveat) { + return true; + } + return false; } - _onChange () { - if (this.state.location){ - this.props.history.push(this.props.saveUrl); + componentDidUpdate () { + // If we're in the slide with this component, autofocus the address box, otherwise defocus. + if (this.props.manualFocus !== undefined) { + const addressBox = this.autoComplete; + if (addressBox) { + if (this.props.manualFocus) { + addressBox.focus(); + } else { + addressBox.blur(); + } + } + } + if (this.googleAutocompleteListener === undefined) { + const { google } = window; + if (google !== undefined) { + const addressAutocomplete = new google.maps.places.Autocomplete(this.autoComplete); + addressAutocomplete.setComponentRestrictions({ country: 'us' }); + this.googleAutocompleteListener = addressAutocomplete.addListener('place_changed', this._placeChanged.bind(this, addressAutocomplete)); + } + } + } + + componentDidCatch (error, info) { + // We should get this information to Splunk! + console.error('AddressBox caught error: ', `${error} with info: `, info); + } + + componentWillUnmount () { + this.voterStoreListener.remove(); + this.ballotStoreListener.remove(); + if (this.googleAutocompleteListener !== undefined) { // Temporary fix until google maps key is fixed for Cordova + this.googleAutocompleteListener.remove(); + } + restoreStylesAfterCordovaKeyboard('AddressBox'); + } + + // See https://reactjs.org/docs/error-boundaries.html + static getDerivedStateFromError (error) { // eslint-disable-line no-unused-vars + // Update state so the next render will show the fallback UI, We should have a "Oh snap" page + console.log('AddressBox error', error); + return { hasError: true }; + } + + handleKeyPress (event) { + // console.log('AddressBox, handleKeyPress, event: ', event); + const enterAndSpaceKeyCodes = [13]; // We actually don't want to use the space character to save, 32 + if (enterAndSpaceKeyCodes.includes(event.keyCode)) { + event.preventDefault(); + this.voterAddressSaveLocal(event); + } + } + + onVoterStoreChange () { + // console.log('AddressBox, onVoterStoreChange, this.state:', this.state); + const { textForMapSearch, voterSavedAddress } = this.state; + + if (textForMapSearch && voterSavedAddress) { + this.incomingToggleSelectAddressModal(); + historyPush(this.props.saveUrl); + } else { + this.setState({ + loading: false, + textForMapSearch: VoterStore.getTextForMapSearch(), + voterSavedAddress, + }); + } + } + + onBallotStoreChange () { + // console.log('AddressBox, onBallotStoreChange, this.state:', this.state); + this.setState({ + ballotCaveat: BallotStore.getBallotCaveat(), + }); + } + + incomingToggleSelectAddressModal = () => { + if (this.props.toggleSelectAddressModal) { + this.props.toggleSelectAddressModal(); + } + } + + _placeChanged (addressAutocomplete) { + const place = addressAutocomplete.getPlace(); + if (place.formatted_address) { + this.setState({ + textForMapSearch: place.formatted_address, + }); } else { - this.setState({ location: VoterStore.getAddress(), loading: false }); + this.setState({ + textForMapSearch: place.name, + }); } } - _ballotLoaded (){ - console.log("ballotLoaded"); - this.props.history.push(this.props.saveUrl); + // saveAddressFromOnBlur (event) { + // // console.log('saveAddressFromOnBlur CALLING-VoterActions.voterAddressSave event.target.value: ', event.target.value); + // VoterActions.voterAddressSave(event.target.value); + // } + + updateVoterAddress (event) { + this.setState({ textForMapSearch: event.target.value }); } - updateLocation (e) { + voterAddressSaveLocal (event) { + // console.log('CALLING-VoterActions.voterAddressSave, event.target.value:', event.target.value); + event.preventDefault(); + VoterActions.voterAddressSave(event.target.value); + BallotActions.completionLevelFilterTypeSave('filterAllBallotItems'); + const oneMonthExpires = 86400 * 31; + cookies.setItem('location_guess_closed', '1', oneMonthExpires, '/'); this.setState({ - location: e.target.value + loading: true, + textForMapSearch: event.target.value, + voterSavedAddress: true, }); } - saveLocation (e) { - e.preventDefault(); - var { location } = this.state; - console.log("Saving location", location); - VoterActions.saveAddress(location); - this.setState({loading: true}); + voterAddressSaveSubmit () { + const { textForMapSearch } = this.state; + VoterActions.voterAddressSave(textForMapSearch); + BallotActions.completionLevelFilterTypeSave('filterAllBallotItems'); + const oneMonthExpires = 86400 * 31; + cookies.setItem('location_guess_closed', '1', oneMonthExpires, '/'); + this.setState({ + loading: true, + voterSavedAddress: true, + }); } render () { - var { loading, location } = this.state; - if (loading){ - return LoadingWheel; + renderLog('AddressBox'); // Set LOG_RENDER_EVENTS to log all renders + // console.log('AddressBox render'); + let { waitingMessage } = this.props; + const { classes, disableAutoFocus, externalUniqueId, showCancelEditAddressButton } = this.props; + const { ballotCaveat, loading, textForMapSearch } = this.state; + if (loading) { + if (!waitingMessage) waitingMessage = 'Please wait a moment while we find your ballot...'; + + return ( +
    +

    {waitingMessage}

    + {LoadingWheel} +
    + ); } - var floatRight = { - float: "right" - }; + return ( -
    -
    - +
    + + + + { this.autoComplete = autocomplete; }} + inputProps={{ + autoFocus: (!isCordova() && !disableAutoFocus), + // onBlur: this.saveAddressFromOnBlur, + onChange: this.updateVoterAddress, + onKeyDown: this.handleKeyPress, + }} + id={externalUniqueId ? `addressBoxText-${externalUniqueId}` : 'addressBoxText'} + /> + + {showCancelEditAddressButton ? ( + + ) : null} +
    + - -
    - - - - - -
    -
    ); +

    +

    {ballotCaveat}

    +
    + ); } } +AddressBox.propTypes = { + externalUniqueId: PropTypes.string, + showCancelEditAddressButton: PropTypes.bool, + disableAutoFocus: PropTypes.bool, + manualFocus: PropTypes.bool, + toggleEditingAddress: PropTypes.func, + toggleSelectAddressModal: PropTypes.func, + saveUrl: PropTypes.string.isRequired, + waitingMessage: PropTypes.string, + classes: PropTypes.object, +}; + +const styles = { + root: { + padding: '2px .7rem', + display: 'flex', + alignItems: 'center', + width: '100%', + marginBottom: '1rem', + // marginRight: '1rem', + }, + saveButton: { + // marginRight: '.3rem', + height: 'fit-content', + width: 'calc(50% - 8px)', + left: 16, + }, + fullWidthSaveButton: { + // marginRight: '.3rem', + height: 'fit-content', + margin: 0, + }, + cancelButton: { + // marginRight: '.3rem', + width: 'calc(50% - 8px)', + }, + input: { + marginLeft: 8, + flex: 1, + }, +}; + +export default withStyles(styles)(AddressBox); diff --git a/src/js/components/Apple/AppleSignIn.jsx b/src/js/components/Apple/AppleSignIn.jsx new file mode 100644 index 000000000..08a253518 --- /dev/null +++ b/src/js/components/Apple/AppleSignIn.jsx @@ -0,0 +1,284 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import styled from 'styled-components'; +import cookies from '../../utils/cookies'; +import VoterActions from '../../actions/VoterActions'; +import webAppConfig from '../../config'; +import { isAndroid, isIOS, isWebApp } from '../../utils/cordovaUtils'; +import { openSnackbar } from '../Widgets/SnackNotifier'; +import { oAuthLog, renderLog } from '../../utils/logging'; + +class AppleSignIn extends Component { + constructor (props) { + super(props); + this.signInToAppleIOS = this.signInToAppleIOS.bind(this); + this.signInToAppleWebApp = this.signInToAppleWebApp.bind(this); + this.signInClicked = this.signInClicked.bind(this); + this.onSignInSuccess = this.onSignInSuccess.bind(this); + this.onSignInFailure = this.onSignInFailure.bind(this); + } + + componentDidMount () { + if (isWebApp()) { + const { AppleID } = window; + const state = JSON.stringify({ + voter_device_id: cookies.getItem('voter_device_id'), + return_url: window.location.href, + }); + if (AppleID) { + console.log('voter_device_id from cookie', cookies.getItem('voter_device_id')); + AppleID.auth.init({ + clientId: 'us.wevote.webapp', + scope: 'name email', + redirectURI: `${webAppConfig.WE_VOTE_SERVER_API_ROOT_URL}appleSignInOauthRedirectDestination`, + state, + popup: true, + }); + } else { + console.log('ERROR in AppleSignIn, the Sign In with Apple client did not load'); + } + document.addEventListener('AppleIDSignInOnSuccess', (data) => { + console.log('AppleIDSignInOnSuccessListener data:', data); + }); + document.addEventListener('AppleIDSignInOnFailure', (error) => { + console.log('AppleIDSignInOnFailureListener ERROR:', error); + }); + } + } + + componentWillUnmount () { + if (isWebApp()) { + // document.removeEventListener('AppleIDSignInOnSuccess', this.onSignInSuccess()); + // document.removeEventListener('AppleIDSignInOnFailure', this.onSignInFailure()); + } + } + + onSignInSuccess (data) { + console.log('onSignInSuccess data:', data); + } + + onSignInFailure (data) { + console.log('onSignInFailure data:', data); + } + + signInToAppleIOS () { + console.log('SignInWithApple signInToAppleIOS: Button clicked'); + const { SignInWithApple: { signin } } = window.cordova.plugins; + + signin( + { requestedScopes: [0, 1]}, + (response) => { + console.log(`SignInWithApple: ${JSON.stringify(response)}`); + const { user, email, identityToken, fullName: { givenName, middleName, familyName } } = response; + console.log('AppleSignInSave called with email:', email); + // In August 2020, Apple stopped returning the name and address on first signin, and email on subsequent signins + // So it seems that we have no way to determine if they use an alias email, found notes that dropbox and others have + // trouble with alias emails (probably since they match siwa sign ins with previous signins by email). This will be trouble for us + // unless it is a short term bug on the Apple API servers. + // if (!email || email.length === 0) { + // openSnackbar({ + // message: 'We Vote does not support "Hide My Email" at this time.', + // duration: 7000, + // }); + // oAuthLog('We Vote does not support "Hide My Email" at this time.'); + // } else { + VoterActions.voterAppleSignInSave(email, givenName, middleName, familyName, user, identityToken); + oAuthLog('Sign in with Apple successful signin for: ', email); + // } + if (this.props.closeSignInModal) { + this.props.closeSignInModal(); + } + }, + (err) => { + // console.error(err); + console.log(`SignInWithApple: ${JSON.stringify(err)}`); + oAuthLog(`SignInWithApple: ${JSON.stringify(err)}`); + if (err.code === '1000') { + // SignInWithApple: {"code":"1000","localizedFailureReason":"","error":"ASAUTHORIZATION_ERROR","localizedDescription":"The operation couldn’t be completed. (com.apple.AuthenticationServices.AuthorizationError error 1000.)"} + // iOS takes over and the voter will be taking a break to go to settings to setup AppleID or login to iCloud for the first time on this device. + // Super edge case outside of testing situations + openSnackbar({ + message: `You may need to open Settings, and login to iCloud before proceeding. (code: ${err.code})`, + duration: 7000, + }); + if (this.props.closeSignInModal) { + this.props.closeSignInModal(); + } + } + }, + ); + } + + signInToAppleWebApp () { // https://i.stack.imgur.com/Le6Jf.png https://stackoverflow.com/questions/61071848/sign-in-with-apple-js-returns-invalid-request-in + oAuthLog('AppleSignIn signInToAppleWebApp button pressed'); + try { + const { auth } = window.AppleID; + auth.signIn(); + } catch (error) { + oAuthLog('signInToAppleWebApp exception ERROR:', error); + } + } + + // eslint-disable-next-line consistent-return + signInClicked (enabled) { + if (!enabled) { + return null; + } + if (isWebApp()) { + this.signInToAppleWebApp(); + } else { + this.signInToAppleIOS(); + } + } + + render () { + renderLog('AppleSignIn'); // Set LOG_RENDER_EVENTS to log all renders + const isWeb = isWebApp(); + const { signedIn } = this.props; + let enabled = true; + const tinyScreen = isWeb && window.innerWidth < 300; // Galaxy fold, folded + + if (isAndroid()) { + // console.log('Sign in with Apple is not available on Android'); + return null; + } else if (isIOS()) { + const { device: { version } } = window; + const floatVersion = parseFloat(version); + if (floatVersion < 13.0) { + console.log('Sign in with Apple is not available on iOS < 13, this phone is running: ', floatVersion); + enabled = false; + } + } + + if (signedIn) { + return ( + + + + ); + } else { + return ( + + this.signInClicked(enabled)} + > + + + {enabled ? 'Sign in with Apple' : '(REQUIRES iOS 13)'} + + + + ); + } + } +} +AppleSignIn.propTypes = { + closeSignInModal: PropTypes.func, + signedIn: PropTypes.bool, +}; + +export default AppleSignIn; + +export function AppleLogo (parameters) { + return ( + + ); +} + +/* +Note, May 21, 2020: Before making changes to these styles, be sure you are compliant with +https://developer.apple.com/design/resources/ or we risk getting rejected by Apple +*/ +const AppleLogoSvg = styled.svg` + position: absolute; + left: ${({ signedIn }) => (signedIn ? '29%' : '5%')}; + top: 11px; + height: 20px; + color: ${({ enabled }) => (enabled ? '#fff' : 'grey')}; +`; + +/* +Note, May 21, 2020: Before making changes to these styles, be sure you are compliant with +https://developer.apple.com/design/resources/ or we risk getting rejected by Apple +*/ +const AppleSignInText = styled.span` + font-size: 18px; + padding: 0; + border: none; + color: ${({ enabled }) => (enabled ? '#fff' : 'grey')}; +`; + +/* +Note, May 21, 2020: Before making changes to these styles, be sure you are compliant with +https://developer.apple.com/design/resources/ or we risk getting rejected by Apple +*/ +const AppleSignInButton = styled.button` + margin-top: ${({ isWeb }) => (isWeb ? '8px' : '10px')}; + border: none; + padding-left: ${({ tinyScreen }) => (tinyScreen ? '20px' : '40px')}; + background-color: #000; + color: #fff; +`; + +/* +Note, May 21, 2020: Before making changes to these styles, be sure you are compliant with +https://developer.apple.com/design/resources/ or we risk getting rejected by Apple +*/ +const AppleSignInContainer = styled.div` + font-family: -apple-system, BlinkMacSystemFont, sans-serif; + background-color: #000; + border-color: #000; + color: ${({ enabled }) => (enabled ? '#fff' : 'grey')}; + display: block; + margin: 0 auto 11px; + height: 46px; + border-radius: 4px; + overflow: hidden; + padding: 0 40px; + position: relative; + text-overflow: ellipsis; + white-space: nowrap; + width: 100%; +`; + +/* +Note, May 21, 2020: Before making changes to these styles, be sure you are compliant with +https://developer.apple.com/design/resources/ or we risk getting rejected by Apple +*/ +const AppleSignedInContainer = styled.div` + background-color: #000; + border-color: #000; + color: #fff; + margin: 0 auto 11px; + height: 46px; + border-radius: 4px; + max-width: 408px; + overflow: hidden; + position: relative; + width: 46px; +`; diff --git a/src/js/components/Ballot/BallotElectionList.jsx b/src/js/components/Ballot/BallotElectionList.jsx new file mode 100644 index 000000000..5a26409f9 --- /dev/null +++ b/src/js/components/Ballot/BallotElectionList.jsx @@ -0,0 +1,484 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import moment from 'moment'; +import styled from 'styled-components'; +import { Button } from '@material-ui/core'; +import { convertStateCodeToStateText } from '../../utils/addressFunctions'; +import BallotActions from '../../actions/BallotActions'; +import BallotStore from '../../stores/BallotStore'; +import { historyPush } from '../../utils/cordovaUtils'; +import { renderLog } from '../../utils/logging'; +import LoadingWheel from '../LoadingWheel'; +import OrganizationActions from '../../actions/OrganizationActions'; +import VoterActions from '../../actions/VoterActions'; +import VoterStore from '../../stores/VoterStore'; +import { cleanArray } from '../../utils/textFormat'; + +const MAXIMUM_NUMBER_OF_CHARACTERS_TO_SHOW = 36; +const MAXIMUM_NUMBER_OF_CHARACTERS_TO_SHOW_DESKTOP = 36; + +// DEPRECATED - MIGRATE AWAY FROM THIS - DEPRECATED - DEPRECATED - DEPRECATED +// New file is BallotElectionListWithFilters +export default class BallotElectionList extends Component { + constructor (props) { + super(props); + let priorElectionId = ''; + if (BallotStore.ballotProperties) { + priorElectionId = BallotStore.ballotProperties.google_civic_election_id; + } else if (VoterStore.electionId()) { + priorElectionId = VoterStore.electionId(); + } + const stateCode = VoterStore.getStateCodeFromIPAddress(); + + this.state = { + loadingNewBallotItems: false, + priorElectionId, + showMoreUpcomingElections: false, + showMorePriorElections: false, + showPriorElectionsList: false, + stateName: convertStateCodeToStateText(stateCode), + updatedElectionId: '', + }; + + this.ballotStoreListener = BallotStore.addListener(this.onBallotStoreChange.bind(this)); + this.voterStoreListener = VoterStore.addListener(this.onVoterStoreChange.bind(this)); + } + + componentWillUnmount () { + this.ballotStoreListener.remove(); + this.voterStoreListener.remove(); + } + + onBallotStoreChange () { + // console.log("BallotElectionList.jsx onBallotStoreChange, priorElectionId: ", this.state.priorElectionId, ", updatedElectionId: ", this.state.updatedElectionId); + // console.log("BallotStore.ballotProperties: ", BallotStore.ballotProperties); + if (BallotStore.ballotProperties && BallotStore.ballotProperties.ballot_found && BallotStore.ballot && BallotStore.ballot.length === 0) { + // Ballot is found but ballot is empty. We want to stay put. + // console.log("onBallotStoreChange: ballot_with_all_items is empty"); + } + if (this.state.priorElectionId !== this.state.updatedElectionId && this.state.loadingNewBallotItems && this.props.toggleFunction) { + // console.log("onBallotStoreChange--------- loadingNewBallotItems:", this.state.loadingNewBallotItems); + this.setState({ + loadingNewBallotItems: false, + updatedElectionId: BallotStore.ballotProperties.google_civic_election_id, + }); + // console.log("onBallotStoreChange--------- this.props.toggleFunction()"); + this.props.toggleFunction(this.state.destinationUrlForHistoryPush); + } + } + + onVoterStoreChange () { + // console.log("BallotElectionList.jsx onVoterStoreChange, VoterStore.electionId(): ", VoterStore.electionId(), ", priorElectionId: ", this.state.priorElectionId, ", updatedElectionId: ", this.state.updatedElectionId); + // if (BallotStore.ballotProperties && BallotStore.ballotProperties.ballot_found && BallotStore.ballot && BallotStore.ballot.length !== 0) { + if (VoterStore.electionId() && VoterStore.electionId() !== this.state.priorElectionId) { + if (this.state.loadingNewBallotItems && this.props.toggleFunction) { + // console.log("onVoterStoreChange--------- loadingNewBallotItems:", this.state.loadingNewBallotItems); + const stateCode = VoterStore.getStateCodeFromIPAddress(); + this.setState({ + loadingNewBallotItems: false, + stateName: convertStateCodeToStateText(stateCode), + updatedElectionId: VoterStore.electionId(), + }); + // console.log("onVoterStoreChange--------- this.props.toggleFunction()"); + this.props.toggleFunction(this.state.destinationUrlForHistoryPush); + } + } + } + + goToDifferentElection = (ballotLocationShortcut, ballotReturnedWeVoteId, googleCivicElectionId, originalTextForMapSearch = '') => { + const ballotBaseurl = this.props.ballotBaseUrl || '/ballot'; + const { organization_we_vote_id: organizationWeVoteId } = this.props; + let destinationUrlForHistoryPush = ''; + if (ballotLocationShortcut && ballotLocationShortcut !== '' && ballotLocationShortcut !== 'none') { + // console.log("goToDifferentElection, ballotLocationShortcut: ", ballotLocationShortcut); + BallotActions.voterBallotItemsRetrieve(0, '', ballotLocationShortcut); + destinationUrlForHistoryPush = `${ballotBaseurl}/${ballotLocationShortcut}`; // Used with historyPush once modal is closed + } else if (ballotReturnedWeVoteId && ballotReturnedWeVoteId !== '' && ballotReturnedWeVoteId !== 'none') { + // console.log("goToDifferentElection, ballotReturnedWeVoteId: ", ballotReturnedWeVoteId); + BallotActions.voterBallotItemsRetrieve(0, ballotReturnedWeVoteId, ''); + destinationUrlForHistoryPush = `${ballotBaseurl}/id/${ballotReturnedWeVoteId}`; // Used with historyPush once modal is closed + } else if (originalTextForMapSearch && originalTextForMapSearch !== '') { + // Do we still want to be updating addresses? Maybe instead just update google_civic_election_id? + // console.log("goToDifferentElection, originalTextForMapSearch: ", originalTextForMapSearch); + const simpleSave = false; + VoterActions.voterAddressSave(originalTextForMapSearch, simpleSave, googleCivicElectionId); + destinationUrlForHistoryPush = ballotBaseurl; // Used with historyPush once modal is closed + } else if (googleCivicElectionId && googleCivicElectionId !== 0) { + BallotActions.voterBallotItemsRetrieve(googleCivicElectionId, '', ''); + // console.log("goToDifferentElection, googleCivicElectionId: ", googleCivicElectionId); + destinationUrlForHistoryPush = `${ballotBaseurl}/election/${googleCivicElectionId}`; // Used with historyPush once modal is closed + } + + // Request positions for the different election + if (organizationWeVoteId && organizationWeVoteId !== '') { + // console.log("BallotElectionList calling positionListForOpinionMaker, organizationWeVoteId: ", organizationWeVoteId, ", googleCivicElectionId:", googleCivicElectionId); + // if (!OrganizationStore.positionListForOpinionMakerHasBeenRetrievedOnce(googleCivicElectionId, organizationWeVoteId)) { + OrganizationActions.positionListForOpinionMaker(organizationWeVoteId, true, false, googleCivicElectionId); + // } + } + + if (this.props.toggleFunction) { + // console.log("goToDifferentElection, loadingNewBallotItems: ", this.state.loadingNewBallotItems); + // console.log("goToDifferentElection, priorElectionId: ", this.state.priorElectionId, ", updatedElectionId: ", this.state.updatedElectionId); + this.setState({ + destinationUrlForHistoryPush, + loadingNewBallotItems: true, + priorElectionId: BallotStore.ballotProperties.google_civic_election_id || VoterStore.electionId() || 0, + updatedElectionId: 0, + }); + } else { + historyPush(destinationUrlForHistoryPush); + } + } + + filterElectionsInState (electionList) { + return electionList.filter((election) => this.isElectionInState(election)); + } + + // filterElectionsOutsideState (electionList) { + // return electionList.filter(election => !this.isElectionInState(election)); + // } + + isElectionInState (election) { + const electionName = election.election_description_text; + if (this.state.stateName.length && electionName.includes(this.state.stateName)) { + return true; + } + // show all national elections regardless of state + // return election.is_national; + return electionName.includes('U.S.') || + electionName.includes('US') || + electionName.includes('United States'); + } + + + toggleShowMoreUpcomingElections () { + this.setState((prevState) => ({ showMoreUpcomingElections: !prevState.showMoreUpcomingElections })); + } + + toggleShowMorePriorElections () { + this.setState((prevState) => ({ showMorePriorElections: !prevState.showMorePriorElections })); + } + + toggleShowPriorElectionsList () { + this.setState((prevState) => ({ showPriorElectionsList: !prevState.showPriorElectionsList })); + } + + renderUpcomingElectionList (list, currentDate) { + const renderedList = list.map((item) => { + const electionDateTomorrowMoment = moment(item.election_day_text, 'YYYY-MM-DD').add(1, 'days'); + const electionDateTomorrow = electionDateTomorrowMoment.format('YYYY-MM-DD'); + return electionDateTomorrow > currentDate ? ( +
    +
    + +
    +
    + ) : + null; + }); + return cleanArray(renderedList); + } + + renderPriorElectionList (list, currentDate) { + const renderedList = list.map((item) => { + const electionDateTomorrowMoment = moment(item.election_day_text, 'YYYY-MM-DD').add(1, 'days'); + const electionDateTomorrow = electionDateTomorrowMoment.format('YYYY-MM-DD'); + return electionDateTomorrow > currentDate ? + null : ( +
    +
    + +
    +
    + ); + }); + return cleanArray(renderedList); + } + + render () { + renderLog('BallotElectionList'); // Set LOG_RENDER_EVENTS to log all renders + if (this.state.loadingNewBallotItems) { + return ( +
    +

    Switching ballot data now...

    +
    + {LoadingWheel} +
    + ); + } + + const currentDate = moment().format('YYYY-MM-DD'); + + const ballotElectionListUpcomingSorted = this.props.ballotElectionList.concat(); + // We want to sort ascending so the next upcoming election is first + ballotElectionListUpcomingSorted.sort((a, b) => { + const electionDayTextA = a.election_day_text.toLowerCase(); + const electionDayTextB = b.election_day_text.toLowerCase(); + if (electionDayTextA < electionDayTextB) { // sort string ascending + return -1; + } + if (electionDayTextA > electionDayTextB) return 1; + return 0; // default return value (no sorting) + }); + + const ballotElectionListPastSorted = this.props.ballotElectionList.concat(); + // We want to sort descending so the most recent election is first + ballotElectionListPastSorted.sort((a, b) => { + const electionDayTextA = a.election_day_text.toLowerCase(); + const electionDayTextB = b.election_day_text.toLowerCase(); + if (electionDayTextA < electionDayTextB) { // sort string descending + return 1; + } + if (electionDayTextA > electionDayTextB) return -1; + return 0; // default return value (no sorting) + }); + + const upcomingElectionList = this.renderUpcomingElectionList(ballotElectionListUpcomingSorted, currentDate); + const priorElectionList = this.renderPriorElectionList(ballotElectionListPastSorted, currentDate); + + if (this.props.showRelevantElections) { + const upcomingBallotElectionListInState = this.filterElectionsInState(ballotElectionListUpcomingSorted); + const priorBallotElectionListInState = this.filterElectionsInState(ballotElectionListPastSorted); + + const upcomingElectionListInState = this.renderUpcomingElectionList(upcomingBallotElectionListInState, currentDate); + const priorElectionListInState = this.renderPriorElectionList(priorBallotElectionListInState, currentDate); + + const upcomingElectionListOutsideCount = upcomingElectionList.length - upcomingElectionListInState.length; + const priorElectionListOutsideCount = priorElectionList.length - priorElectionListInState.length; + + // If there are no upcoming elections and no prior elections (anywhere in the country), return empty div + if (!upcomingElectionList.length && !priorElectionList.length) { + return ( +
    + ); + } + + // December 2018, these nested ternary expression should get fixed at some point + + return ( +
    +
    +

    + Upcoming Election + { (upcomingElectionListInState && upcomingElectionListInState.length !== 1 && !this.state.showMoreUpcomingElections) || + (upcomingElectionList && upcomingElectionList.length !== 1 && this.state.showMoreUpcomingElections) ? 's' : null} + { this.state.stateName && this.state.stateName.length && !this.state.showMoreUpcomingElections ? + ` in ${this.state.stateName}` : + null} +

    + { this.state.showMoreUpcomingElections ? // eslint-disable-line no-nested-ternary + upcomingElectionList && upcomingElectionList.length ? + upcomingElectionList : + 'There are no upcoming elections at this time.' : + upcomingElectionListInState && upcomingElectionListInState.length ? + upcomingElectionListInState : + 'There are no upcoming elections in the state you are in at this time.'} + { upcomingElectionListOutsideCount ? // eslint-disable-line no-nested-ternary + this.state.showMoreUpcomingElections ? ( +
    +
    + { this.state.stateName && this.state.stateName.length ? + `Only show elections in ${this.state.stateName}` : + 'Hide state elections'} +
    +
    + ) : ( +
    +
    + Show all states - + {' '} + { upcomingElectionListOutsideCount } + {' '} + more election + { upcomingElectionListOutsideCount !== 1 ? 's' : null } +
    +
    + ) : + null} +
    + + { this.state.showPriorElectionsList ? ( +
    + { priorElectionListInState && priorElectionListInState.length ? ( +

    + Prior Election + { (priorElectionListInState.length > 1 || + (priorElectionList && priorElectionList.length > 1)) ? + 's' : + null} + { this.state.stateName && this.state.stateName.length && !this.state.showMorePriorElections ? + ` in ${this.state.stateName}` : + null} +

    + ) : null} + { this.state.showMorePriorElections ? // eslint-disable-line no-nested-ternary + priorElectionList && priorElectionList.length ? + priorElectionList : + null : + priorElectionListInState && priorElectionListInState.length ? + priorElectionListInState : + null} + { priorElectionListOutsideCount ? // eslint-disable-line no-nested-ternary + this.state.showMorePriorElections ? ( +
    + { this.state.stateName && this.state.stateName.length ? + `Only show elections in ${this.state.stateName}` : + 'Hide state elections'} +
    + ) : ( +
    + Show all states - + {' '} + { priorElectionListOutsideCount } + {' '} + more election + { priorElectionListOutsideCount !== 1 ? 's' : null } +
    + ) : null} +
    + ) : ( +
    +
    + Show prior elections +
    +
    + )} +
    + ); + } else { + return ( +
    +
    + { upcomingElectionList && upcomingElectionList.length ? ( +

    + Upcoming Election + { upcomingElectionList.length > 1 ? 's' : null } +

    + ) : + null} + { upcomingElectionList && upcomingElectionList.length ? upcomingElectionList : null } +
    + +
    + { priorElectionList && priorElectionList.length ? ( +

    + Prior Election + { priorElectionList.length > 1 ? 's' : null } +

    + ) : + null} + { priorElectionList && priorElectionList.length ? priorElectionList : null } +
    +
    + ); + } + } +} +BallotElectionList.propTypes = { + ballotElectionList: PropTypes.array.isRequired, + ballotBaseUrl: PropTypes.string, + organization_we_vote_id: PropTypes.string, // If looking at voter guide, we pass in the parent organization_we_vote_id + showRelevantElections: PropTypes.bool, + toggleFunction: PropTypes.func, +}; + +const ButtonContentsWrapper = styled.div` + display: flex; + flex-flow: column; +`; + +const ElectionName = styled.div` +`; + +const ElectionDate = styled.div` + font-size: 12px; + font-weight: 100; + @media (min-width: ${({ theme }) => theme.breakpoints.md}) { + font-size: 12px; + } +`; diff --git a/src/js/components/Ballot/BallotElectionListWithFilters.jsx b/src/js/components/Ballot/BallotElectionListWithFilters.jsx new file mode 100644 index 000000000..0b247ee0f --- /dev/null +++ b/src/js/components/Ballot/BallotElectionListWithFilters.jsx @@ -0,0 +1,707 @@ +/* eslint-disable no-nested-ternary */ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import moment from 'moment'; +import styled from 'styled-components'; +import { Button } from '@material-ui/core'; +import BallotActions from '../../actions/BallotActions'; +import BallotStore from '../../stores/BallotStore'; +import DelayedLoad from '../Widgets/DelayedLoad'; +import { historyPush } from '../../utils/cordovaUtils'; +import { renderLog } from '../../utils/logging'; +import LoadingWheel from '../LoadingWheel'; +import OrganizationActions from '../../actions/OrganizationActions'; +import VoterActions from '../../actions/VoterActions'; +import VoterStore from '../../stores/VoterStore'; +import { cleanArray } from '../../utils/textFormat'; +import VoterGuideStore from '../../stores/VoterGuideStore'; +import VoterGuideActions from '../../actions/VoterGuideActions'; + + +export default class BallotElectionListWithFilters extends Component { + constructor (props) { + super(props); + + this.state = { + ballotElectionList: [], + ballotElectionListCount: 0, + loadingNewBallotItems: false, + updatedElectionId: '', + voterBallotListHasBeenRetrievedOnce: false, + voterGuideHasBeenRetrievedOnce: {}, + }; + this.executeDifferentElection = this.executeDifferentElection.bind(this); + this.goToBallotForDifferentElection = this.goToBallotForDifferentElection.bind(this); + } + + componentDidMount () { + this.ballotStoreListener = BallotStore.addListener(this.onBallotStoreChange.bind(this)); + this.voterGuideStoreListener = VoterGuideStore.addListener(this.onVoterGuideStoreChange.bind(this)); + this.voterStoreListener = VoterStore.addListener(this.onVoterStoreChange.bind(this)); + let priorElectionId = ''; + if (BallotStore.ballotProperties) { + priorElectionId = BallotStore.ballotProperties.google_civic_election_id; + } else if (VoterStore.electionId()) { + priorElectionId = VoterStore.electionId(); + } + const { voterBallotListHasBeenRetrievedOnce, voterGuideHasBeenRetrievedOnce } = this.state; + let ballotElectionList; + let ballotElectionListCount = 0; + const { displayElectionsForOrganizationVoterGuidesMode, organizationWeVoteId } = this.props; + if (displayElectionsForOrganizationVoterGuidesMode) { + ballotElectionList = VoterGuideStore.getVoterGuideElectionList(organizationWeVoteId); + ballotElectionListCount = ballotElectionList.length; + if (ballotElectionListCount === 0 && !this.localVoterGuideHasBeenRetrievedOnce(organizationWeVoteId)) { + VoterGuideActions.voterGuidesRetrieve(organizationWeVoteId); + voterGuideHasBeenRetrievedOnce[organizationWeVoteId] = true; + this.setState({ + voterGuideHasBeenRetrievedOnce, + }); + } + } else { + ballotElectionList = BallotStore.ballotElectionList(); + ballotElectionListCount = ballotElectionList.length; + if (ballotElectionListCount === 0 && !voterBallotListHasBeenRetrievedOnce) { + BallotActions.voterBallotListRetrieve(); + this.setState({ + voterBallotListHasBeenRetrievedOnce: true, + }); + } + } + // console.log('componentDidMount displayElectionsForOrganizationVoterGuidesMode:', displayElectionsForOrganizationVoterGuidesMode, ', organizationWeVoteId:', organizationWeVoteId); + this.setState({ + ballotElectionList, + ballotElectionListCount, + priorElectionId, + }); + } + + shouldComponentUpdate (nextProps, nextState) { + if (this.state.ballotElectionListCount !== nextState.ballotElectionListCount) { + // console.log('this.state.ballotElectionListCount', this.state.ballotElectionListCount, ', nextState.ballotElectionListCount', nextState.ballotElectionListCount); + return true; + } + if (this.props.stateToShow !== nextProps.stateToShow) { + return true; + } + if (this.props.hideUpcomingElectionsList !== nextProps.hideUpcomingElectionsList) return true; + if (this.props.showPriorElectionsList !== nextProps.showPriorElectionsList) return true; + if (this.state.destinationUrlForHistoryPush !== nextState.destinationUrlForHistoryPush) { + // console.log('this.state.destinationUrlForHistoryPush', this.state.destinationUrlForHistoryPush, ', nextState.destinationUrlForHistoryPush', nextState.destinationUrlForHistoryPush); + return true; + } + if (this.state.loadingNewBallotItems !== nextState.loadingNewBallotItems) { + // console.log('this.state.loadingNewBallotItems', this.state.loadingNewBallotItems, ', nextState.loadingNewBallotItems', nextState.loadingNewBallotItems); + return true; + } + if (this.state.priorElectionId !== nextState.priorElectionId) { + // console.log('this.state.priorElectionId', this.state.priorElectionId, ', nextState.priorElectionId', nextState.priorElectionId); + return true; + } + if (this.state.updatedElectionId !== nextState.updatedElectionId) { + // console.log('this.state.updatedElectionId', this.state.updatedElectionId, ', nextState.updatedElectionId', nextState.updatedElectionId); + return true; + } + // ////////////////// Props //////////////////// + if (this.props.ballotBaseUrl !== nextProps.ballotBaseUrl) { + // console.log('this.props.ballotBaseUrl', this.props.ballotBaseUrl, ', nextProps.ballotBaseUrl', nextProps.ballotBaseUrl); + return true; + } + if (this.props.organizationWeVoteId !== nextProps.organizationWeVoteId) { + // console.log('this.props.organizationWeVoteId', this.props.organizationWeVoteId, ', nextProps.organizationWeVoteId', nextProps.organizationWeVoteId); + return true; + } + // console.log('shouldComponentUpdate false'); + return false; + } + + componentWillUnmount () { + this.ballotStoreListener.remove(); + this.voterStoreListener.remove(); + this.voterGuideStoreListener.remove(); + } + + onBallotStoreChange () { + // console.log('BallotElectionListWithFilters.jsx onBallotStoreChange, priorElectionId: ', this.state.priorElectionId, ', updatedElectionId: ', this.state.updatedElectionId); + // console.log('BallotStore.ballotProperties: ', BallotStore.ballotProperties); + if (BallotStore.ballotProperties && BallotStore.ballotProperties.ballot_found && BallotStore.ballot && BallotStore.ballot.length === 0) { + // Ballot is found but ballot is empty. We want to stay put. + // console.log('onBallotStoreChange: ballot_with_all_items is empty'); + } + if (this.state.priorElectionId !== this.state.updatedElectionId && this.state.loadingNewBallotItems) { + // console.log('onBallotStoreChange--------- loadingNewBallotItems:', this.state.loadingNewBallotItems); + this.setState({ + loadingNewBallotItems: false, + updatedElectionId: BallotStore.ballotProperties.google_civic_election_id, + }); + // console.log('onBallotStoreChange--------- this.props.toggleFunction()'); + this.incomingToggleFunction(this.state.destinationUrlForHistoryPush); + } + let ballotElectionList; + let ballotElectionListCount; + const { displayElectionsForOrganizationVoterGuidesMode, organizationWeVoteId } = this.props; + const { voterBallotListHasBeenRetrievedOnce, voterGuideHasBeenRetrievedOnce } = this.state; + if (displayElectionsForOrganizationVoterGuidesMode) { + ballotElectionList = VoterGuideStore.getVoterGuideElectionList(organizationWeVoteId); + ballotElectionListCount = ballotElectionList.length; + if (ballotElectionListCount === 0 && !this.localVoterGuideHasBeenRetrievedOnce(organizationWeVoteId)) { + VoterGuideActions.voterGuidesRetrieve(organizationWeVoteId); + voterGuideHasBeenRetrievedOnce[organizationWeVoteId] = true; + this.setState({ + voterGuideHasBeenRetrievedOnce, + }); + } + } else { + ballotElectionList = BallotStore.ballotElectionList(); + ballotElectionListCount = ballotElectionList.length; + if (ballotElectionListCount === 0 && !voterBallotListHasBeenRetrievedOnce) { + BallotActions.voterBallotListRetrieve(); + this.setState({ + voterBallotListHasBeenRetrievedOnce: true, + }); + } + } + this.setState({ + ballotElectionList, + ballotElectionListCount, + }); + } + + onVoterGuideStoreChange () { + const { displayElectionsForOrganizationVoterGuidesMode, organizationWeVoteId } = this.props; + // console.log('onVoterGuideStoreChange displayElectionsForOrganizationVoterGuidesMode:', displayElectionsForOrganizationVoterGuidesMode, ', organizationWeVoteId:', organizationWeVoteId); + if (displayElectionsForOrganizationVoterGuidesMode) { + const ballotElectionList = VoterGuideStore.getVoterGuideElectionList(organizationWeVoteId); + const ballotElectionListCount = ballotElectionList.length; + this.setState({ + ballotElectionList, + ballotElectionListCount, + }); + } + const voter = VoterStore.getVoter(); + const voterGuideSaveResults = VoterGuideStore.getVoterGuideSaveResults(); + if (voterGuideSaveResults && voter && voterGuideSaveResults.organization_we_vote_id === voter.linked_organization_we_vote_id) { + this.goToVoterGuideForDifferentElection(voterGuideSaveResults.we_vote_id); + } + } + + onVoterStoreChange () { + // console.log('BallotElectionListWithFilters.jsx onVoterStoreChange, VoterStore.electionId(): ', VoterStore.electionId(), ', priorElectionId: ', this.state.priorElectionId, ', updatedElectionId: ', this.state.updatedElectionId); + // if (BallotStore.ballotProperties && BallotStore.ballotProperties.ballot_found && BallotStore.ballot && BallotStore.ballot.length !== 0) { + if (VoterStore.electionId() && VoterStore.electionId() !== this.state.priorElectionId) { + if (this.state.loadingNewBallotItems) { + // console.log('onVoterStoreChange--------- loadingNewBallotItems:', this.state.loadingNewBallotItems); + this.setState({ + loadingNewBallotItems: false, + updatedElectionId: VoterStore.electionId(), + }); + // console.log('onVoterStoreChange--------- this.props.toggleFunction()'); + this.incomingToggleFunction(this.state.destinationUrlForHistoryPush); + } + } + } + + incomingToggleFunction = (destinationUrlForHistoryPush) => { + if (this.props.toggleFunction) { + this.props.toggleFunction(destinationUrlForHistoryPush); + } + } + + localVoterGuideHasBeenRetrievedOnce = (organizationWeVoteId) => { + if (organizationWeVoteId) { + const { voterGuideHasBeenRetrievedOnce } = this.state; + return voterGuideHasBeenRetrievedOnce[organizationWeVoteId] || false; + } + return false; + } + + saveVoterGuideForElection = (googleCivicElectionId) => { + BallotActions.voterBallotItemsRetrieve(googleCivicElectionId, '', ''); + VoterGuideActions.voterGuideSave(googleCivicElectionId, ''); + // When the result comes back from voterGuideSave, onVoterGuideStoreChange triggers a call to goToVoterGuideForDifferentElection + } + + switchElectionBehindTheScenes = (googleCivicElectionId) => { + // Load new election + const { organizationWeVoteId } = this.props; + // console.log('switchElectionBehindTheScenes, googleCivicElectionId:', googleCivicElectionId); + BallotActions.voterBallotItemsRetrieve(googleCivicElectionId, '', ''); + OrganizationActions.positionListForOpinionMaker(organizationWeVoteId, true, false, googleCivicElectionId); + if (this.props.toggleFunction) { + this.props.toggleFunction(); + } + } + + goToVoterGuideForDifferentElection = (voterGuideWeVoteId) => { + const voterGuideBallotItems = `/vg/${voterGuideWeVoteId}/settings/positions`; + historyPush(voterGuideBallotItems); + if (this.props.toggleFunction) { + this.props.toggleFunction(); + } + } + + goToBallotForDifferentElection (originalTextForMapSearch, googleCivicElectionId, ballotLocationShortcut = '', ballotReturnedWeVoteId = '') { + // console.log('BallotElectionListWithFilters, goToBallotForDifferentElection'); + const ballotBaseUrlClean = this.props.ballotBaseUrl || '/ballot'; + const { organizationWeVoteId } = this.props; + let destinationUrlForHistoryPush = ''; + if (ballotLocationShortcut && ballotLocationShortcut !== '' && ballotLocationShortcut !== 'none') { + // console.log('goToBallotForDifferentElection, ballotLocationShortcut: ', ballotLocationShortcut); + BallotActions.voterBallotItemsRetrieve(0, '', ballotLocationShortcut); + destinationUrlForHistoryPush = `${ballotBaseUrlClean}/${ballotLocationShortcut}`; // Used with historyPush once modal is closed + } else if (ballotReturnedWeVoteId && ballotReturnedWeVoteId !== '' && ballotReturnedWeVoteId !== 'none') { + // console.log('goToBallotForDifferentElection, ballotReturnedWeVoteId: ', ballotReturnedWeVoteId); + BallotActions.voterBallotItemsRetrieve(0, ballotReturnedWeVoteId, ''); + destinationUrlForHistoryPush = `${ballotBaseUrlClean}/id/${ballotReturnedWeVoteId}`; // Used with historyPush once modal is closed + } else if (googleCivicElectionId && googleCivicElectionId !== 0) { + BallotActions.voterBallotItemsRetrieve(googleCivicElectionId, '', ''); + // console.log('goToBallotForDifferentElection, googleCivicElectionId: ', googleCivicElectionId); + destinationUrlForHistoryPush = `${ballotBaseUrlClean}/election/${googleCivicElectionId}`; // Used with historyPush once modal is closed + } else if (originalTextForMapSearch && originalTextForMapSearch !== '') { + // Do we still want to be updating addresses? Maybe instead just update google_civic_election_id? + // console.log('goToBallotForDifferentElection, originalTextForMapSearch: ', originalTextForMapSearch); + const simpleSave = false; + VoterActions.voterAddressSave(originalTextForMapSearch, simpleSave, googleCivicElectionId); + destinationUrlForHistoryPush = ballotBaseUrlClean; // Used with historyPush once modal is closed + } + // Request positions for the different election + if (organizationWeVoteId && organizationWeVoteId !== '') { + // console.log('BallotElectionListWithFilters calling positionListForOpinionMaker, this.props.organizationWeVoteId: ', this.props.organizationWeVoteId, ', googleCivicElectionId:', googleCivicElectionId); + // if (!OrganizationStore.positionListForOpinionMakerHasBeenRetrievedOnce(googleCivicElectionId, organizationWeVoteId)) { + OrganizationActions.positionListForOpinionMaker(organizationWeVoteId, true, false, googleCivicElectionId); + // } + } + + if (this.props.toggleFunction) { + // console.log('goToBallotForDifferentElection, loadingNewBallotItems: ', this.state.loadingNewBallotItems); + // console.log('goToBallotForDifferentElection, priorElectionId: ', this.state.priorElectionId, ', updatedElectionId: ', this.state.updatedElectionId, ', destinationUrlForHistoryPush: ', destinationUrlForHistoryPush, ', BallotStore.ballotProperties.google_civic_election_id: ', BallotStore.ballotProperties.google_civic_election_id, ', VoterStore.electionId():', VoterStore.electionId()); + let ballotPropertiesGoogleCivicElectionId = 0; + if (BallotStore.ballotProperties) { + ballotPropertiesGoogleCivicElectionId = BallotStore.ballotProperties.google_civic_election_id; + } + this.setState({ + destinationUrlForHistoryPush, + loadingNewBallotItems: true, + priorElectionId: ballotPropertiesGoogleCivicElectionId || VoterStore.electionId() || 0, + updatedElectionId: 0, + }); + } else { + // console.log('destinationUrlForHistoryPush: ', destinationUrlForHistoryPush); + historyPush(destinationUrlForHistoryPush); + } + } + + executeDifferentElection (election) { + if (election) { + const { ballotBaseUrl, displayElectionsForOrganizationVoterGuidesMode } = this.props; + // console.log('executeDifferentElection ballotBaseUrl:', ballotBaseUrl, ', displayElectionsForOrganizationVoterGuidesMode:', displayElectionsForOrganizationVoterGuidesMode); + if (displayElectionsForOrganizationVoterGuidesMode) { + this.switchElectionBehindTheScenes(election.google_civic_election_id); + } else if (ballotBaseUrl) { + this.goToBallotForDifferentElection(election.original_text_for_map_search, election.google_civic_election_id); // Removing for now: , election.ballot_location_shortcut, election.ballot_returned_we_vote_id + } else { + this.saveVoterGuideForElection(election.google_civic_election_id); + } + } + } + + renderUpcomingElectionList (list, currentDate) { + if (!list || !Array.isArray(list)) { + return null; + } + const renderedList = list.map((election) => { + // console.log('election: ', election); + if (!election.election_description_text || election.election_description_text === '') return null; + const electionDateTomorrowMoment = moment(election.election_day_text, 'YYYY-MM-DD').add(1, 'days'); + const electionDateTomorrow = electionDateTomorrowMoment.format('YYYY-MM-DD'); + let electionStateCodeList = []; + if (election && election.state_code_list) { + electionStateCodeList = election.state_code_list || []; + } + const electionId = election.google_civic_election_id || 0; + return (electionDateTomorrow > currentDate) && ( +
    +
    + this.executeDifferentElection(election)} + variant="contained" + > + + + {moment(election.election_day_text).format('MMM Do, YYYY')} + {' - '} + {election.election_description_text.split(' in')[0]} + + + {electionStateCodeList.map((stateAbbrev, index) => { + if (index < 5) { + return ( + {stateAbbrev} + ); + } else if (index === 6) { + return ( + {`+${electionStateCodeList.length - 6}`} + ); + } else { + return null; + } + })} + + + +
    +
    + ); + }); + return cleanArray(renderedList); + } + + renderUpcomingElectionListByState (list, currentDate) { + if (!list || !Array.isArray(list)) { + return null; + } + const renderedList = list.filter((filterItem) => filterItem.state_code_list && filterItem.state_code_list.includes(this.props.stateToShow)).map((election) => { + // console.log('election.election_description_text: ', election.election_description_text, 'election.election_day_text: ', election.election_day_text); + if (!election.election_description_text || election.election_description_text === '') return null; + const electionDateTomorrowMoment = moment(election.election_day_text, 'YYYY-MM-DD').add(1, 'days'); + const electionDateTomorrow = electionDateTomorrowMoment.format('YYYY-MM-DD'); + let electionStateCodeList = []; + if (election && election.state_code_list) { + electionStateCodeList = election.state_code_list || []; + } + const electionId = election.google_civic_election_id || 0; + return electionDateTomorrow > currentDate ? ( +
    +
    + this.executeDifferentElection(election)} + variant="contained" + > + + + {moment(election.election_day_text).format('MMM Do, YYYY')} + {' - '} + {election.election_description_text.split(' in')[0]} + + + {electionStateCodeList.map((stateAbbrev, index) => { + if (index < 5) { + return ( + {stateAbbrev} + ); + } else if (index === 6) { + return ( + {`+${electionStateCodeList.length - 6}`} + ); + } else { + return null; + } + })} + + + +
    +
    + ) : + null; + }); + return cleanArray(renderedList); + } + + renderPriorElectionList (list, currentDate) { + if (!list || !Array.isArray(list)) { + return null; + } + const renderedList = list.map((election) => { + const electionDateTomorrowMoment = moment(election.election_day_text, 'YYYY-MM-DD').add(1, 'days'); + const electionDateTomorrow = electionDateTomorrowMoment.format('YYYY-MM-DD'); + let electionStateCodeList = []; + if (election && election.state_code_list) { + electionStateCodeList = election.state_code_list || []; + } + const electionId = election.google_civic_election_id || 0; + return electionDateTomorrow > currentDate ? + null : ( +
    +
    + this.executeDifferentElection(election)} + variant="contained" + > + + + {moment(election.election_day_text).format('MMM Do, YYYY')} + {' - '} + {election.election_description_text.split(' in')[0]} + + + {electionStateCodeList.map((stateAbbrev, index) => { + if (index < 5) { + return ( + {stateAbbrev} + ); + } else if (index === 6) { + return ( + {`+${electionStateCodeList.length - 6}`} + ); + } else { + return null; + } + })} + + + +
    +
    + ); + }); + return cleanArray(renderedList); + } + + renderPriorElectionListByState (list, currentDate) { + if (!list || !Array.isArray(list)) { + return null; + } + const renderedList = list.filter((filterItem) => filterItem.state_code_list && filterItem.state_code_list.includes(this.props.stateToShow)).map((election) => { + const electionDateTomorrowMoment = moment(election.election_day_text, 'YYYY-MM-DD').add(1, 'days'); + const electionDateTomorrow = electionDateTomorrowMoment.format('YYYY-MM-DD'); + let electionStateCodeList = []; + if (election && election.state_code_list) { + electionStateCodeList = election.state_code_list || []; + } + const electionId = election.google_civic_election_id || 0; + return electionDateTomorrow > currentDate ? + null : ( +
    +
    + this.executeDifferentElection(election)} + variant="contained" + > + {' '} + + + {moment(election.election_day_text).format('MMM Do, YYYY')} + {' - '} + {election.election_description_text.split(' in')[0]} + + + {electionStateCodeList.map((stateAbbrev, index) => { + if (index < 5) { + return ( + {stateAbbrev} + ); + } else if (index === 6) { + return ( + {`+${electionStateCodeList.length - 6}`} + ); + } else { + return null; + } + })} + + + +
    +
    + ); + }); + return cleanArray(renderedList); + } + + render () { + renderLog('BallotElectionListWithFilters'); // Set LOG_RENDER_EVENTS to log all renders + if (this.state.loadingNewBallotItems) { + return ( +
    +

    Switching ballot data now...

    +
    + {LoadingWheel} +
    + ); + } + const currentDate = moment().format('YYYY-MM-DD'); + const { hideUpcomingElectionTitle } = this.props; + let { showPriorElectionsList, hideUpcomingElectionsList } = this.props; + // console.log('this.state.ballotElectionList:', this.state.ballotElectionList); + + const ballotElectionListUpcomingSorted = this.state.ballotElectionList.concat(); + // We want to sort ascending so the next upcoming election is first + ballotElectionListUpcomingSorted.sort((a, b) => { + const electionDayTextA = a.election_day_text.toLowerCase(); + const electionDayTextB = b.election_day_text.toLowerCase(); + if (electionDayTextA < electionDayTextB) { // sort string ascending + return -1; + } + if (electionDayTextA > electionDayTextB) return 1; + return 0; // default return value (no sorting) + }); + const upcomingElectionList = this.renderUpcomingElectionList(ballotElectionListUpcomingSorted, currentDate); + + const upcomingElectionListByState = this.renderUpcomingElectionListByState(ballotElectionListUpcomingSorted, currentDate); + + let priorElectionList = []; + const ballotElectionListPastSorted = this.state.ballotElectionList.concat(); + if (showPriorElectionsList) { + // We want to sort descending so the most recent election is first + ballotElectionListPastSorted.sort((a, b) => { + const electionDayTextA = a.election_day_text.toLowerCase(); + const electionDayTextB = b.election_day_text.toLowerCase(); + if (electionDayTextA < electionDayTextB) { // sort string descending + return 1; + } + if (electionDayTextA > electionDayTextB) return -1; + return 0; // default return value (no sorting) + }); + priorElectionList = this.renderPriorElectionList(ballotElectionListPastSorted, currentDate); + } + + const priorElectionListByState = this.renderPriorElectionListByState(ballotElectionListPastSorted, currentDate); + + + if (priorElectionList && !priorElectionList.length) { + showPriorElectionsList = false; // Override to hide + } + if ((upcomingElectionList && !upcomingElectionList.length) && (priorElectionList && priorElectionList.length)) { + // If there aren't any upcoming elections, but there are prior elections, hide the whole upcoming elections block + hideUpcomingElectionsList = true; // Override to hide + } + + // console.log('hideUpcomingElectionsList: ', hideUpcomingElectionsList, ', showPriorElectionsList: ', showPriorElectionsList); + + return ( +
    + { !hideUpcomingElectionsList && ( +
    + {!hideUpcomingElectionTitle && ( + + +

    + Upcoming Election + {(upcomingElectionList && upcomingElectionList.length !== 1) ? 's' : null } +

    +
    +
    + )} + { upcomingElectionList && upcomingElectionList.length ? + ( + <> + {this.props.stateToShow === 'all' ? upcomingElectionList : + upcomingElectionListByState.length > 0 ? upcomingElectionListByState : + 'There are no upcoming elections for this state.'} + + ) : ( + +
    + {this.props.stateToShow !== 'all' ? 'There are no upcoming elections at this time for this state.' : 'There are no upcoming elections at this time.'} +
    +
    + )} +
    + )} + {(!hideUpcomingElectionsList && showPriorElectionsList) && ( + +   + + )} + { showPriorElectionsList && ( +
    + { priorElectionList && priorElectionList.length ? + ( + +

    Prior Elections

    + {this.props.stateToShow === 'all' ? priorElectionList : + priorElectionListByState.length > 0 ? priorElectionListByState : + 'There are no prior elections for this state.'} +
    + ) : ( + +
    + {this.props.stateToShow !== 'all' ? 'There are no prior elections at this time for this state.' : 'There are no prior elections at this time.'} +
    +
    + )} +
    + )} +
    + ); + } +} +BallotElectionListWithFilters.propTypes = { + ballotBaseUrl: PropTypes.string, + displayElectionsForOrganizationVoterGuidesMode: PropTypes.bool, + hideUpcomingElectionsList: PropTypes.bool, + hideUpcomingElectionTitle: PropTypes.bool, + organizationWeVoteId: PropTypes.string, // If looking at voter guide, we pass in the parent organizationWeVoteId + showPriorElectionsList: PropTypes.bool, + stateToShow: PropTypes.string, + toggleFunction: PropTypes.func, +}; + +const ElectionButton = styled(Button)` + background: white !important; + border: 1px solid #555 !important; + box-shadow: none !important; + color:#2e3c5d !important; + text-transform: none !important; + margin: 8px 0 !important; + padding: 10px !important; +`; + +const ElectionTitle = styled.h3` + margin: 0 !important; + font-size: 15px; + font-weight: 600; + text-align: left !important; + margin-bottom: 6px !important; +`; + +const ButtonContentsWrapper = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + width: 100%; + align-items: flex-start; +`; + +const ElectionStates = styled.div` + display: flex; + align-items: center; + justify-content: flex-start; + margin-top: 4px; +`; + +const ElectionState = styled.div` + border-radius: 50px; + height: 20px; + width: fit-content; + padding: 2px 8px; + background: #e8e8e8; + display: flex; + align-items: center; + margin: 0 2px; +`; + +const PriorOrUpcomingElectionsWrapper = styled.div` + margin-top: 20px; +`; + +const SpaceBetweenElections = styled.div` + margin-bottom: 20px; +`; diff --git a/src/js/components/Ballot/BallotItem.jsx b/src/js/components/Ballot/BallotItem.jsx deleted file mode 100644 index 8809503a1..000000000 --- a/src/js/components/Ballot/BallotItem.jsx +++ /dev/null @@ -1,38 +0,0 @@ -import React, { Component, PropTypes } from "react"; -import CandidateList from "../../components/Ballot/CandidateList"; -import Measure from "../../components/Ballot/Measure"; -import StarAction from "../../components/StarAction"; - -const TYPES = require("keymirror")({ - OFFICE: null, - MEASURE: null -}); - -export default class BallotItem extends Component { - static propTypes = { - kind_of_ballot_item: PropTypes.string.isRequired, - we_vote_id: PropTypes.string.isRequired, - ballot_item_display_name: PropTypes.string.isRequired, - candidate_list: PropTypes.array - }; - - isMeasure () { - return this.props.kind_of_ballot_item === TYPES.MEASURE; - } - - render () { - return
    - - - { this.props.ballot_item_display_name } - - - - - { this.isMeasure() ? : } - -
    ; - } -} diff --git a/src/js/components/Ballot/BallotItemCompressed.jsx b/src/js/components/Ballot/BallotItemCompressed.jsx new file mode 100644 index 000000000..8a5c533bf --- /dev/null +++ b/src/js/components/Ballot/BallotItemCompressed.jsx @@ -0,0 +1,35 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import { renderLog } from '../../utils/logging'; +import MeasureItemCompressed from './MeasureItemCompressed'; +import OfficeItemCompressed from './OfficeItemCompressed'; + +export default class BallotItemCompressed extends PureComponent { + render () { + renderLog('BallotItemCompressed'); // Set LOG_RENDER_EVENTS to log all renders + const { isMeasure, weVoteId, ballotItemDisplayName, candidateList, candidatesToShowForSearchResults } = this.props; + return ( +
    + { isMeasure ? ( + + ) : ( + + )} +
    + ); + } +} +BallotItemCompressed.propTypes = { + ballotItemDisplayName: PropTypes.string.isRequired, + candidateList: PropTypes.array, + candidatesToShowForSearchResults: PropTypes.array, + weVoteId: PropTypes.string.isRequired, + isMeasure: PropTypes.bool, +}; diff --git a/src/js/components/Ballot/BallotItemSearchResult.jsx b/src/js/components/Ballot/BallotItemSearchResult.jsx new file mode 100644 index 000000000..f3c41c255 --- /dev/null +++ b/src/js/components/Ballot/BallotItemSearchResult.jsx @@ -0,0 +1,44 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { renderLog } from '../../utils/logging'; +import MeasureItemCompressed from './MeasureItemCompressed'; +import CandidateItemCompressed from './CandidateItemCompressed'; + +const TYPES = require('keymirror')({ + OFFICE: null, + MEASURE: null, +}); + +export default class BallotItemSearchResult extends Component { + isMeasure () { + const { kindOfBallotItem } = this.props; + return kindOfBallotItem === TYPES.MEASURE; + } + + render () { + renderLog('BallotItemSearchResult'); // Set LOG_RENDER_EVENTS to log all renders + const { ballotItemWeVoteId, organization } = this.props; + return ( +
    + { this.isMeasure() ? ( + + ) : ( + + )} +
    + ); + } +} +BallotItemSearchResult.propTypes = { + // allBallotItemsCount: PropTypes.number, + kindOfBallotItem: PropTypes.string.isRequired, + organization: PropTypes.object, + ballotItemWeVoteId: PropTypes.string.isRequired, + // updateOfficeDisplayUnfurledTracker: PropTypes.func, +}; diff --git a/src/js/components/Ballot/BallotSearchResults.jsx b/src/js/components/Ballot/BallotSearchResults.jsx new file mode 100644 index 000000000..19c116ae5 --- /dev/null +++ b/src/js/components/Ballot/BallotSearchResults.jsx @@ -0,0 +1,182 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import BallotActions from '../../actions/BallotActions'; +import BallotItemSearchResult from './BallotItemSearchResult'; +import BallotStore from '../../stores/BallotStore'; +import { cordovaDot } from '../../utils/cordovaUtils'; +import { renderLog } from '../../utils/logging'; +import OrganizationActions from '../../actions/OrganizationActions'; +import SearchBar from '../Search/SearchBar'; +import thumbUpIcon from '../../../img/global/svg-icons/thumbs-up-icon.svg'; +import thumbDownIcon from '../../../img/global/svg-icons/thumbs-down-icon.svg'; + + +export default class BallotSearchResults extends Component { + constructor (props) { + super(props); + this.state = { + ballotItemSearchResultsList: [], + clearSearchTextNow: false, + searchString: '', + }; + this.searchFunction = this.searchFunction.bind(this); + this.clearFunction = this.clearFunction.bind(this); + } + + componentDidMount () { + // console.log("BallotSearchResults componentDidMount, this.props.clearSearchTextNow:", this.props.clearSearchTextNow); + this.setState({ + ballotItemSearchResultsList: BallotStore.ballotItemSearchResultsList(), + clearSearchTextNow: this.props.clearSearchTextNow, + }); + this.ballotStoreListener = BallotStore.addListener(this.onBallotStoreChange.bind(this)); + // this.voterGuideStoreListener = VoterGuideStore.addListener(this.onVoterGuideStoreChange.bind(this)); + } + + // eslint-disable-next-line camelcase,react/sort-comp + UNSAFE_componentWillReceiveProps (nextProps) { + // console.log("BallotSearchResults componentWillReceiveProps, nextProps.clearSearchTextNow:", nextProps.clearSearchTextNow); + this.setState({ + clearSearchTextNow: nextProps.clearSearchTextNow, + }); + } + + componentWillUnmount () { + // console.log("BallotSearchResults componentWillUnmount"); + this.ballotStoreListener.remove(); + // this.voterGuideStoreListener.remove(); + // Cannot call in componentWillUnmount: BallotActions.ballotItemOptionsClear(); + } + + onBallotStoreChange () { + // console.log("BallotSearchResults onBallotStoreChange, BallotStore.ballotProperties: ", BallotStore.ballotProperties); + this.setState({ + ballotItemSearchResultsList: BallotStore.ballotItemSearchResultsList(), + }); + } + + onVoterGuideStoreChange () { // eslint-disable-line + // console.log("BallotSearchResults onVoterGuideStoreChange"); + } + + searchFunction (searchString) { + if (searchString && searchString !== '') { + BallotActions.ballotItemOptionsRetrieve(this.props.googleCivicElectionId, searchString); + if (this.props.searchUnderwayFunction) { + this.props.searchUnderwayFunction(true); + } + } else { + BallotActions.ballotItemOptionsClear(); + } + this.setState({ + clearSearchTextNow: false, + searchString, + }); + } + + clearFunction () { + OrganizationActions.positionListForOpinionMaker(this.props.organizationWeVoteId, true, false, this.props.googleCivicElectionId); + BallotActions.ballotItemOptionsClear(); + if (this.props.searchUnderwayFunction) { + this.props.searchUnderwayFunction(false); + } + this.setState({ + clearSearchTextNow: false, + searchString: '', + }); + } + + render () { + renderLog('BallotSearchResults.jsx'); // Set LOG_RENDER_EVENTS to log all renders + const { ballotItemSearchResultsList, searchString, clearSearchTextNow } = this.state; + if (!ballotItemSearchResultsList) { + return null; + } + + const iconSize = 18; + const iconColor = '#999'; + const noSearchResultsPossibility = searchString && searchString !== '' ? +
    No search results found.
    : null; + + const actionDescription = ( +
    + Click + {' '} + + + thumbs up + + {' '} + Support + + {' '} + or  + + + thumbs down + + {' '} + Oppose + + {' '} + to add an item to your ballot. +
    + ); + + // Jan 2019, Steve: What sets the state.ballotItemSearchResultsList? (I think nothing sets it) + const searchResults = ballotItemSearchResultsList.map((ballotItem) => ( + + )); + + const featureTurnedOff = true; + return ( +
    +
    + {featureTurnedOff ? null : ( +
    + +
    + )} +
    + {ballotItemSearchResultsList && ballotItemSearchResultsList.length ? ( +
    + {actionDescription} + {searchResults} +
    + ) : + { noSearchResultsPossibility }} +
    +
    +
    + ); + } +} +BallotSearchResults.propTypes = { + clearSearchTextNow: PropTypes.bool, + googleCivicElectionId: PropTypes.number, + organizationWeVoteId: PropTypes.string, + searchUnderwayFunction: PropTypes.func, +}; diff --git a/src/js/components/Ballot/BallotStatusMessage.jsx b/src/js/components/Ballot/BallotStatusMessage.jsx new file mode 100644 index 000000000..f40097c70 --- /dev/null +++ b/src/js/components/Ballot/BallotStatusMessage.jsx @@ -0,0 +1,269 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import moment from 'moment'; +import { Snackbar } from '@material-ui/core'; +import { withStyles } from '@material-ui/core/styles'; +import BallotStore from '../../stores/BallotStore'; +import cookies from '../../utils/cookies'; +import ElectionStore from '../../stores/ElectionStore'; +import { renderLog } from '../../utils/logging'; +import VoterStore from '../../stores/VoterStore'; + +const styles = (theme) => ({ + anchorOriginBottomCenter: { + bottom: 54, + [theme.breakpoints.up('md')]: { + bottom: 20, + }, + }, +}); + +class BallotStatusMessage extends Component { + constructor (props) { + super(props); + this.state = { + ballotLocationChosen: false, + ballotLocationDisplayName: '', + componentDidMountFinished: false, + electionDayText: '', + electionIsUpcoming: false, + electionsWithBallotStatusMessageClosed: [], + googleCivicElectionId: 0, + showBallotStatus: true, + substitutedAddressNearby: '', + voterEnteredAddress: false, + voterSpecificBallotFromGoogleCivic: false, + open: true, + }; + + this.handleMessageClose = this.handleMessageClose.bind(this); + } + + componentDidMount () { + // console.log("In BallotStatusMessage componentDidMount"); + const electionsWithBallotStatusMessageClosedValueFromCookie = cookies.getItem('elections_with_ballot_status_message_closed'); + let electionsWithBallotStatusMessageClosed = []; + if (electionsWithBallotStatusMessageClosedValueFromCookie) { + electionsWithBallotStatusMessageClosed = JSON.parse(electionsWithBallotStatusMessageClosedValueFromCookie) || []; + } + + this.ballotStoreListener = BallotStore.addListener(this.onBallotStoreChange.bind(this)); + this.electionStoreListener = ElectionStore.addListener(this.onBallotStoreChange.bind(this)); + this.voterStoreListener = VoterStore.addListener(this.onBallotStoreChange.bind(this)); + + this.setState({ + ballotLocationChosen: this.props.ballotLocationChosen, + componentDidMountFinished: true, + googleCivicElectionId: this.props.googleCivicElectionId, + showBallotStatus: true, + electionsWithBallotStatusMessageClosed, + }); + } + + // eslint-disable-next-line camelcase,react/sort-comp + UNSAFE_componentWillReceiveProps (nextProps) { + // console.log("BallotStatusMessage componentWillReceiveProps"); + this.setState({ + ballotLocationChosen: nextProps.ballotLocationChosen, + googleCivicElectionId: this.props.googleCivicElectionId, + showBallotStatus: true, + }); + } + + shouldComponentUpdate (nextProps, nextState) { + // This lifecycle method tells the component to NOT render if componentWillReceiveProps didn't see any changes + if (this.state.componentDidMountFinished === false) { + // console.log("shouldComponentUpdate: componentDidMountFinished === false"); + return true; + } + if (this.state.ballotLocationChosen !== nextState.ballotLocationChosen) { + // console.log("shouldComponentUpdate: changed, this.state.ballotLocationChosen: ", this.state.ballotLocationChosen, ", nextState.ballotLocationChosen", nextState.ballotLocationChosen); + return true; + } + if (this.state.ballotLocationDisplayName !== nextState.ballotLocationDisplayName) { + // console.log("shouldComponentUpdate: changed, this.state.ballotLocationDisplayName: ", this.state.ballotLocationDisplayName, ", nextState.ballotLocationDisplayName", nextState.ballotLocationDisplayName); + return true; + } + if (this.state.electionDayText !== nextState.electionDayText) { + // console.log("shouldComponentUpdate: changed, this.state.electionDayText: ", this.state.electionDayText, ", nextState.electionDayText", nextState.electionDayText); + return true; + } + if (this.state.electionIsUpcoming !== nextState.electionIsUpcoming) { + // console.log("shouldComponentUpdate: changed, this.state.electionIsUpcoming: ", this.state.electionIsUpcoming, ", nextState.electionIsUpcoming", nextState.electionIsUpcoming); + return true; + } + if (this.state.substitutedAddressNearby !== nextState.substitutedAddressNearby) { + // console.log("shouldComponentUpdate: changed, this.state.substitutedAddressNearby: ", this.state.substitutedAddressNearby, ", nextState.substitutedAddressNearby", nextState.substitutedAddressNearby); + return true; + } + if (this.state.voterEnteredAddress !== nextState.voterEnteredAddress) { + // console.log("shouldComponentUpdate: changed, this.state.voterEnteredAddress: ", this.state.voterEnteredAddress, ", nextState.voterEnteredAddress", nextState.voterEnteredAddress); + return true; + } + if (this.state.voterSpecificBallotFromGoogleCivic !== nextState.voterSpecificBallotFromGoogleCivic) { + // console.log("shouldComponentUpdate: changed, this.state.voterSpecificBallotFromGoogleCivic: ", this.state.voterSpecificBallotFromGoogleCivic, ", nextState.voterSpecificBallotFromGoogleCivic", nextState.voterSpecificBallotFromGoogleCivic); + return true; + } + if (this.state.open !== nextState.open) { + return true; + } + return false; + } + + componentWillUnmount () { + // console.log("Ballot componentWillUnmount"); + this.ballotStoreListener.remove(); + this.electionStoreListener.remove(); + this.voterStoreListener.remove(); + } + + handleMessageClose () { + // setting cookie to track the elections where user has closed the warning messages for them + if (this.props.googleCivicElectionId) { + const { electionsWithBallotStatusMessageClosed } = this.state; + const electionsWithBallotStatusMessageClosedUpdated = [...electionsWithBallotStatusMessageClosed, this.props.googleCivicElectionId]; + const electionsWithBallotStatusMessageClosedForCookie = JSON.stringify(electionsWithBallotStatusMessageClosedUpdated); + cookies.setItem('elections_with_ballot_status_message_closed', electionsWithBallotStatusMessageClosedForCookie, Infinity, '/'); + this.setState({ + electionsWithBallotStatusMessageClosed: electionsWithBallotStatusMessageClosedUpdated, + open: false, + }); + } + } + + onBallotStoreChange () { + let ballotLocationDisplayName = ''; + const { googleCivicElectionId } = this.state; + const electionDayText = ElectionStore.getElectionDayText(googleCivicElectionId); + const electionIsUpcoming = ElectionStore.isElectionUpcoming(googleCivicElectionId); + let substitutedAddressNearby = ''; + const voterBallotLocation = VoterStore.getBallotLocationForVoter(); + let voterEnteredAddress = false; + let voterSpecificBallotFromGoogleCivic = false; + + if (voterBallotLocation && voterBallotLocation.voter_entered_address) { + voterEnteredAddress = true; + } + + if (voterBallotLocation && voterBallotLocation.voter_specific_ballot_from_google_civic) { + voterSpecificBallotFromGoogleCivic = true; + } + + if (BallotStore.ballotProperties && BallotStore.ballotProperties.ballot_location_display_name) { + // console.log("BallotStore.ballotProperties:", BallotStore.ballotProperties); + ballotLocationDisplayName = BallotStore.ballotProperties.ballot_location_display_name; + } else if (voterBallotLocation && voterBallotLocation.ballot_location_display_name) { + // Get the location name from the VoterStore address object + // console.log("voterBallotLocation:", voterBallotLocation); + ballotLocationDisplayName = voterBallotLocation.ballot_location_display_name; + } + + if (BallotStore.ballotProperties && BallotStore.ballotProperties.substituted_address_nearby) { + if (BallotStore.ballotProperties.substituted_address_city && BallotStore.ballotProperties.substituted_address_state && BallotStore.ballotProperties.substituted_address_zip) { + substitutedAddressNearby = `${BallotStore.ballotProperties.substituted_address_city}, `; + substitutedAddressNearby += `${BallotStore.ballotProperties.substituted_address_state} `; + substitutedAddressNearby += BallotStore.ballotProperties.substituted_address_zip; + } else { + substitutedAddressNearby = BallotStore.ballotProperties.substituted_address_nearby; + } + } else if (voterBallotLocation && voterBallotLocation.text_for_map_search) { + // Get the location from the VoterStore address object + substitutedAddressNearby = voterBallotLocation.text_for_map_search; + } + // console.log("BallotStatusMessage, onBallotStoreChange, electionDayText: ", electionDayText, "electionIsUpcoming: ", electionIsUpcoming, "substitutedAddressNearby: ", substitutedAddressNearby); + this.setState({ + ballotLocationDisplayName, + electionDayText, + electionIsUpcoming, + substitutedAddressNearby, + voterEnteredAddress, + voterSpecificBallotFromGoogleCivic, + }); + } + + render () { + renderLog('BallotStatusMessage'); // Set LOG_RENDER_EVENTS to log all renders + const { classes } = this.props; + let messageString = ''; + const today = moment(new Date()); + const isVotingDay = today.isSame(this.state.electionDayText, 'day'); + + if (isVotingDay) { + messageString = `It is Voting Day, ${ + moment(this.state.electionDayText).format('MMM Do, YYYY') + }. If you haven't already voted, please go vote!`; + // I don't think this is necessary on election day. + // messageString += !this.state.voterSpecificBallotFromGoogleCivic && this.state.ballotLocationChosen && this.state.ballotLocationDisplayName ? + // " Some items shown below may not have been on your official ballot." : " Some items below may not have been on your official ballot."; + } else if (this.state.electionIsUpcoming) { + if (this.state.voterSpecificBallotFromGoogleCivic) { + // We do not have an equivalent flag when we retrieve a ballot from Ballotpedia + messageString += ''; // No additional text + } else if (this.state.ballotLocationChosen && this.state.substitutedAddressNearby) { + messageString += `This is a ballot for ${this.state.substitutedAddressNearby}.`; + // This does not make sense when using Ballotpedia, since we don't know if voter entered a full address: Enter your full address to see your official ballot. + } else if (this.state.voterEnteredAddress) { + messageString += "This is our best guess for what's on your ballot."; + // I'm not sure we need to introduce doubt, expecially since sometime this appears after someone enters their full address. + // messageString += "Some items below may not be on your official ballot."; + } + } else { + let messageInPastString; + if (this.state.electionDayText) { + messageInPastString = `This election was held on ${moment(this.state.electionDayText).format('MMM Do, YYYY')}.`; + } else { + messageInPastString = ''; // Was "This election has passed." but it showed up inaccurately. + } + + if (this.state.voterSpecificBallotFromGoogleCivic) { + messageString += messageInPastString; // No additional text + } else if (this.state.ballotLocationChosen && this.state.ballotLocationDisplayName) { + messageString += messageInPastString; + // Not sure the benefit of adding this doubt. messageString += " Some items shown below may not have been on your official ballot."; + } else { + messageString += messageInPastString; + // Not sure the benefit of adding this doubt. + " Some items below may not have been on your official ballot."; + } + } + + let messageStringLength = 0; + if (messageString) { + messageStringLength = messageString.length; + } + + let electionBallotStatusMessageShouldBeClosed = false; + if (this.props.googleCivicElectionId) { + electionBallotStatusMessageShouldBeClosed = this.state.electionsWithBallotStatusMessageClosed.includes(this.props.googleCivicElectionId); + } + + if (electionBallotStatusMessageShouldBeClosed) { + return null; + } else if (this.state.showBallotStatus && messageStringLength > 0) { + return ( + 0} + autoHideDuration={5000} + onClose={this.handleMessageClose} + ContentProps={{ + 'aria-describedby': 'message-id', + }} + message={{messageString}} + /> + ); + } else { + return <>; + } + } +} +BallotStatusMessage.propTypes = { + ballotLocationChosen: PropTypes.bool.isRequired, + googleCivicElectionId: PropTypes.number, + classes: PropTypes.object, +}; + +export default withStyles(styles)(BallotStatusMessage); diff --git a/src/js/components/Ballot/CandidateItem.jsx b/src/js/components/Ballot/CandidateItem.jsx index 49e85c197..99eb2bb71 100644 --- a/src/js/components/Ballot/CandidateItem.jsx +++ b/src/js/components/Ballot/CandidateItem.jsx @@ -1,65 +1,691 @@ -import React, { Component, PropTypes } from "react"; -import { Link } from "react-router"; - -import StarAction from "../../components/StarAction"; -import ItemActionBar from "../../components/ItemActionbar"; -import ItemSupportOpposeCounts from "../../components/ItemSupportOpposeCounts"; - -export default class Candidate extends Component { - static propTypes = { - ballot_item_display_name: PropTypes.string.isRequired, - candidate_photo_url: PropTypes.string.isRequired, - party: PropTypes.string, - we_vote_id: PropTypes.string.isRequired, +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import TextTruncate from 'react-text-truncate'; +import styled from 'styled-components'; +import { withStyles, withTheme } from '@material-ui/core/styles'; +import { Info } from '@material-ui/icons'; +import AppActions from '../../actions/AppActions'; +import BallotItemSupportOpposeComment from '../Widgets/BallotItemSupportOpposeComment'; +import BallotItemSupportOpposeCountDisplay from '../Widgets/BallotItemSupportOpposeCountDisplay'; +import CandidateStore from '../../stores/CandidateStore'; +import { historyPush } from '../../utils/cordovaUtils'; +import ImageHandler from '../ImageHandler'; +import isMobileAndTabletScreenSize from '../../utils/isMobileAndTabletScreenSize'; +import IssuesByBallotItemDisplayList from '../Values/IssuesByBallotItemDisplayList'; +import IssueStore from '../../stores/IssueStore'; +import ItemActionBar from '../Widgets/ItemActionBar/ItemActionBar'; +import { renderLog } from '../../utils/logging'; +import OfficeNameText from '../Widgets/OfficeNameText'; +import OpenExternalWebSite from '../Widgets/OpenExternalWebSite'; +import ReadMore from '../Widgets/ReadMore'; +import ShowMoreFooter from '../Navigation/ShowMoreFooter'; +import SupportStore from '../../stores/SupportStore'; +import TopCommentByBallotItem from '../Widgets/TopCommentByBallotItem'; +import VoterGuideStore from '../../stores/VoterGuideStore'; +import { + abbreviateNumber, + numberWithCommas, + stripHtmlFromString, +} from '../../utils/textFormat'; + +// This is related to /js/components/VoterGuide/OrganizationVoterGuideCandidateItem.jsx +class CandidateItem extends Component { + constructor (props) { + super(props); + this.state = { + allCachedPositionsForThisCandidateLength: 0, + ballotItemDisplayName: '', + // ballotpediaCandidateUrl: '', + candidatePhotoUrl: '', + candidateUrl: '', + contestOfficeName: '', + candidateWeVoteId: '', + issuesUnderThisBallotItemVoterIsFollowingLength: 0, + issuesUnderThisBallotItemVoterIsNotFollowingLength: 0, + largeAreaHoverColorOnNow: null, + largeAreaHoverLinkOnNow: false, + officeWeVoteId: '', + politicalParty: '', + twitterFollowersCount: '', + voterOpposesBallotItem: false, + voterSupportsBallotItem: false, + voterTextStatement: '', + }; + this.getCandidateLink = this.getCandidateLink.bind(this); + this.getOfficeLink = this.getOfficeLink.bind(this); + this.goToCandidateLink = this.goToCandidateLink.bind(this); + this.goToOfficeLink = this.goToOfficeLink.bind(this); + } + + componentDidMount () { + // console.log('CandidateItem componentDidMount'); + this.candidateStoreListener = CandidateStore.addListener(this.onCandidateStoreChange.bind(this)); + this.onVoterGuideStoreChange(); + this.issueStoreListener = IssueStore.addListener(this.onIssueStoreChange.bind(this)); + this.voterGuideStoreListener = VoterGuideStore.addListener(this.onVoterGuideStoreChange.bind(this)); + this.supportStoreListener = SupportStore.addListener(this.onSupportStoreChange.bind(this)); + // console.log('CandidateItem, this.props:', this.props); + const { candidateWeVoteId, showLargeImage } = this.props; + if (candidateWeVoteId) { + // If here we want to get the candidate so we can get the officeWeVoteId + const candidate = CandidateStore.getCandidate(candidateWeVoteId); + // console.log('CandidateItem, componentDidMount, candidate:', candidate); + + let candidatePhotoUrl; + if (showLargeImage && candidate.candidate_photo_url_large) { + candidatePhotoUrl = candidate.candidate_photo_url_large; + } else if (candidate.candidate_photo_url_medium) { + candidatePhotoUrl = candidate.candidate_photo_url_medium; + } else if (candidate.candidate_photo_url_tiny) { + candidatePhotoUrl = candidate.candidate_photo_url_tiny; + } + const candidateUrl = candidate.candidate_url; + const twitterDescription = candidate.twitter_description; + const twitterDescriptionText = twitterDescription && twitterDescription.length ? `${twitterDescription} ` : ''; + const ballotpediaCandidateSummary = candidate.ballotpedia_candidate_summary; + let ballotpediaCandidateSummaryText = ballotpediaCandidateSummary && ballotpediaCandidateSummary.length ? ballotpediaCandidateSummary : ''; + ballotpediaCandidateSummaryText = stripHtmlFromString(ballotpediaCandidateSummaryText); + const candidateText = twitterDescriptionText + ballotpediaCandidateSummaryText; + const voterOpposesBallotItem = SupportStore.getVoterOpposesByBallotItemWeVoteId(candidateWeVoteId); + const voterSupportsBallotItem = SupportStore.getVoterSupportsByBallotItemWeVoteId(candidateWeVoteId); + const voterTextStatement = SupportStore.getVoterTextStatementByBallotItemWeVoteId(candidateWeVoteId); + this.setState({ + ballotItemDisplayName: candidate.ballot_item_display_name, + // ballotpediaCandidateUrl: candidate.ballotpedia_candidate_url, + candidatePhotoUrl, + candidateText, + candidateUrl, + candidateWeVoteId, // Move into state to signal that all state data is ready + contestOfficeName: candidate.contest_office_name, + officeWeVoteId: candidate.contest_office_we_vote_id, + politicalParty: candidate.party, + twitterFollowersCount: candidate.twitter_followers_count, + voterOpposesBallotItem, + voterSupportsBallotItem, + voterTextStatement, + }); + } + } + + shouldComponentUpdate (nextProps, nextState) { + if (this.state.allCachedPositionsForThisCandidateLength !== nextState.allCachedPositionsForThisCandidateLength) { + return true; + } + if (this.state.ballotItemDisplayName !== nextState.ballotItemDisplayName) { + return true; + } + if (this.state.ballotItemWeVoteId !== nextState.ballotItemWeVoteId) { + return true; + } + if (this.state.candidatePhotoUrl !== nextState.candidatePhotoUrl) { + return true; + } + if (this.state.candidateText !== nextState.candidateText) { + return true; + } + if (this.state.candidateUrl !== nextState.candidateUrl) { + return true; + } + if (this.state.candidateWeVoteId !== nextState.candidateWeVoteId) { + return true; + } + if (this.props.closeSupportOpposeCountDisplayModal !== nextProps.closeSupportOpposeCountDisplayModal) { + return true; + } + if (this.state.issuesUnderThisBallotItemVoterIsFollowingLength !== nextState.issuesUnderThisBallotItemVoterIsFollowingLength) { + return true; + } + if (this.state.issuesUnderThisBallotItemVoterIsNotFollowingLength !== nextState.issuesUnderThisBallotItemVoterIsNotFollowingLength) { + return true; + } + if (this.state.largeAreaHoverColorOnNow !== nextState.largeAreaHoverColorOnNow) { + return true; + } + if (this.props.openAdviserMaterialUIPopover !== nextProps.openAdviserMaterialUIPopover) { + return true; + } + if (this.props.openSupportOpposeCountDisplayModal !== nextProps.openSupportOpposeCountDisplayModal) { + return true; + } + if (this.props.organizationWeVoteId !== nextProps.organizationWeVoteId) { + return true; + } + if (this.props.showPositionStatementActionBar !== nextProps.showPositionStatementActionBar) { + return true; + } + if (this.props.supportOpposeCountDisplayModalTutorialOn !== nextProps.supportOpposeCountDisplayModalTutorialOn) { + return true; + } + if (this.props.supportOpposeCountDisplayModalTutorialText !== nextProps.supportOpposeCountDisplayModalTutorialText) { + return true; + } + if (this.props.showDownArrow !== nextProps.showDownArrow) { + return true; + } + if (this.props.showUpArrow !== nextProps.showUpArrow) { + return true; + } + if (this.state.voterOpposesBallotItem !== nextState.voterOpposesBallotItem) { + return true; + } + if (this.state.voterSupportsBallotItem !== nextState.voterSupportsBallotItem) { + return true; + } + if (this.state.voterTextStatement !== nextState.voterTextStatement) { + return true; + } + // console.log('CandidateItem shouldComponentUpdate FALSE'); + return false; + } + + componentWillUnmount () { + this.candidateStoreListener.remove(); + this.issueStoreListener.remove(); + this.voterGuideStoreListener.remove(); + this.supportStoreListener.remove(); + } + + onCandidateStoreChange () { + const { candidateWeVoteId, showLargeImage } = this.props; + // console.log('CandidateItem onCandidateStoreChange, candidateWeVoteId:', candidateWeVoteId); + if (candidateWeVoteId) { + const candidate = CandidateStore.getCandidate(candidateWeVoteId); + // console.log('CandidateItem onCandidateStoreChange, candidate:', candidate); + let candidatePhotoUrl; + if (showLargeImage && candidate.candidate_photo_url_large) { + candidatePhotoUrl = candidate.candidate_photo_url_large; + } else if (candidate.candidate_photo_url_medium) { + candidatePhotoUrl = candidate.candidate_photo_url_medium; + } else if (candidate.candidate_photo_url_tiny) { + candidatePhotoUrl = candidate.candidate_photo_url_tiny; + } + const candidateUrl = candidate.candidate_url; + const twitterDescription = candidate.twitter_description; + const twitterDescriptionText = twitterDescription && twitterDescription.length ? `${twitterDescription} ` : ''; + const ballotpediaCandidateSummary = candidate.ballotpedia_candidate_summary; + let ballotpediaCandidateSummaryText = ballotpediaCandidateSummary && ballotpediaCandidateSummary.length ? ballotpediaCandidateSummary : ''; + ballotpediaCandidateSummaryText = stripHtmlFromString(ballotpediaCandidateSummaryText); + const candidateText = twitterDescriptionText + ballotpediaCandidateSummaryText; + const allCachedPositionsForThisCandidate = CandidateStore.getAllCachedPositionsByCandidateWeVoteId(candidateWeVoteId); + const allCachedPositionsForThisCandidateLength = allCachedPositionsForThisCandidate.length || 0; + this.setState({ + allCachedPositionsForThisCandidateLength, + ballotItemDisplayName: candidate.ballot_item_display_name, + // ballotpediaCandidateUrl: candidate.ballotpedia_candidate_url, + candidatePhotoUrl, + candidateText, + candidateUrl, + candidateWeVoteId, // Move into state to signal that all state data is ready + contestOfficeName: candidate.contest_office_name, + officeWeVoteId: candidate.contest_office_we_vote_id, + politicalParty: candidate.party, + twitterFollowersCount: candidate.twitter_followers_count, + }); + } + } + + onIssueStoreChange () { + const { candidateWeVoteId } = this.props; + // console.log('CandidateItem onIssueStoreChange candidateWeVoteId:', candidateWeVoteId); + if (candidateWeVoteId) { + const issuesUnderThisBallotItemVoterIsFollowing = IssueStore.getIssuesUnderThisBallotItemVoterIsFollowing(candidateWeVoteId) || []; + const issuesUnderThisBallotItemVoterIsNotFollowing = IssueStore.getIssuesUnderThisBallotItemVoterNotFollowing(candidateWeVoteId) || []; + const issuesUnderThisBallotItemVoterIsFollowingLength = issuesUnderThisBallotItemVoterIsFollowing.length; + const issuesUnderThisBallotItemVoterIsNotFollowingLength = issuesUnderThisBallotItemVoterIsNotFollowing.length; + this.setState({ + issuesUnderThisBallotItemVoterIsFollowingLength, + issuesUnderThisBallotItemVoterIsNotFollowingLength, + }); + } + } + + onVoterGuideStoreChange () { + // We just want to trigger a re-render + this.setState(); + } + + onSupportStoreChange () { + const { candidateWeVoteId } = this.props; + if (candidateWeVoteId) { + const voterOpposesBallotItem = SupportStore.getVoterOpposesByBallotItemWeVoteId(candidateWeVoteId); + const voterSupportsBallotItem = SupportStore.getVoterSupportsByBallotItemWeVoteId(candidateWeVoteId); + this.setState({ + voterOpposesBallotItem, + voterSupportsBallotItem, + }); + } + } + + getCandidateLink () { + // If here, we assume the voter is on the Office page + const { candidateWeVoteId, organizationWeVoteId } = this.props; + if (candidateWeVoteId) { + if (organizationWeVoteId) { + return `/candidate/${candidateWeVoteId}/bto/${organizationWeVoteId}`; // back-to-office + } else { + return `/candidate/${candidateWeVoteId}/b/btdo`; // back-to-default-office + } + } + return ''; + } + + getOfficeLink () { + const { organizationWeVoteId } = this.props; + const { officeWeVoteId } = this.state; + if (organizationWeVoteId && organizationWeVoteId !== '') { + return `/office/${officeWeVoteId}/btvg/${organizationWeVoteId}`; // back-to-voter-guide + } else if (officeWeVoteId) { + return `/office/${officeWeVoteId}/b/btdb`; // back-to-default-ballot + } else return ''; + } + + handleEnter = () => { + // console.log('Handle largeAreaHoverColorOnNow', e.target); + if (this.props.showHover) { + this.setState({ largeAreaHoverColorOnNow: true, largeAreaHoverLinkOnNow: true }); + } }; - render () { - let { - ballot_item_display_name, - candidate_photo_url, - party, - we_vote_id, + handleLeave = () => { + // console.log('Handle leave', e.target); + if (this.props.showHover) { + this.setState({ largeAreaHoverColorOnNow: false, largeAreaHoverLinkOnNow: false }); + } + }; + + candidateRenderBlock = (candidateWeVoteId, useLinkToCandidatePage = false, forDesktop = false, openSupportOpposeCountDisplayModal = false) => { + const { + controlAdviserMaterialUIPopoverFromProp, closeSupportOpposeCountDisplayModal, + hideCandidateUrl, linkToBallotItemPage, linkToOfficePage, + openAdviserMaterialUIPopover, + supportOpposeCountDisplayModalTutorialOn, supportOpposeCountDisplayModalTutorialText, + showDownArrow, showUpArrow, showHover, showOfficeName, } = this.props; + const { + ballotItemDisplayName, + candidatePhotoUrl, + candidateUrl, + contestOfficeName, + largeAreaHoverColorOnNow, + politicalParty, + twitterFollowersCount, + } = this.state; + // console.log('CandidateItem candidateRenderBlock, candidateWeVoteId:', candidateWeVoteId, ', useLinkToCandidatePage:', useLinkToCandidatePage, ', forDesktop:', forDesktop, ', linkToBallotItemPage:', linkToBallotItemPage); + // console.log('candidateRenderBlock candidatePhotoUrl: ', candidatePhotoUrl); + return ( +
    + + this.goToCandidateLink() : null} + > +
    + +
    + +
    + {ballotItemDisplayName} +
    + {!!(twitterFollowersCount && forDesktop) && ( + + + {abbreviateNumber(twitterFollowersCount)} + + )} + {(!hideCandidateUrl && candidateUrl && forDesktop) && ( + + + candidate website + {' '} +