├── .gitignore
├── src
├── favicon.ico
├── components
│ ├── create-topic
│ │ ├── create-topic-style.scss
│ │ └── create-topic.jsx
│ ├── currency-icon
│ │ ├── currency-icon-style.scss
│ │ └── currency-icon.jsx
│ ├── dev-container
│ │ ├── dev-container-style.scss
│ │ └── dev-container.jsx
│ ├── influence
│ │ ├── influence-style.scss
│ │ └── influence.jsx
│ ├── dropdown
│ │ ├── dropdown-style.scss
│ │ └── dropdown.jsx
│ ├── votes
│ │ ├── votes-style.scss
│ │ └── votes.jsx
│ ├── wrapper
│ │ ├── wrapper-style.scss
│ │ └── wrapper.jsx
│ ├── account
│ │ ├── account-style.scss
│ │ └── account.jsx
│ └── topic
│ │ ├── topic-style.scss
│ │ └── topic.jsx
├── utils
│ ├── scss
│ │ ├── _functions.scss
│ │ ├── _mixins.scss
│ │ └── _vars.scss
│ └── js
│ │ ├── api.js
│ │ └── api.dev.js
└── app.jsx
├── .postcssrc.js
├── .babelrc
├── .editorconfig
├── .eslintrc
├── README.md
├── webpack.dist.babel.js
├── webpack.dev.babel.js
├── CHANGELOG.md
├── package.json
├── webpack.common.babel.js
└── dist
├── style.min.css
└── vote.bundle.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webpack/voting-app/master/src/favicon.ico
--------------------------------------------------------------------------------
/.postcssrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [
3 | require('autoprefixer')
4 | ]
5 | }
--------------------------------------------------------------------------------
/src/components/create-topic/create-topic-style.scss:
--------------------------------------------------------------------------------
1 | .create-topic {
2 | text-align: left;
3 | }
4 |
--------------------------------------------------------------------------------
/src/utils/scss/_functions.scss:
--------------------------------------------------------------------------------
1 | @import 'vars';
2 |
3 | @function getColor($name) {
4 | @return map-get($colors, $name);
5 | }
6 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "es2015",
4 | "react"
5 | ],
6 | "plugins": [
7 | "transform-object-rest-spread"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/src/utils/scss/_mixins.scss:
--------------------------------------------------------------------------------
1 | @import 'vars';
2 |
3 | @mixin break ($size: medium) {
4 | @media (min-width: map-get($screens, $size)) {
5 | @content;
6 | }
7 | }
--------------------------------------------------------------------------------
/src/components/currency-icon/currency-icon-style.scss:
--------------------------------------------------------------------------------
1 | @import 'functions';
2 |
3 | .currency-icon {
4 | display: inline-block;
5 |
6 | &--influence { fill: getColor(denim); }
7 | &--goldenInfluence { fill: #f9bf3b; }
8 | &--support { fill: green; }
9 | }
--------------------------------------------------------------------------------
/src/components/dev-container/dev-container-style.scss:
--------------------------------------------------------------------------------
1 | @import 'vars';
2 | @import 'functions';
3 |
4 | .dev-container {
5 | max-width: map-get($screens, large);
6 | margin: 0 auto;
7 | font-family: 'Helvetica', sans-serif;
8 | color: getColor(emperor);
9 | }
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Top-most EditorConfig file
2 | root = true
3 |
4 | # 4 space indentation
5 | [*.{js,jsx,scss}]
6 | charset = utf-8
7 | indent_style = space
8 | indent_size = 4
9 |
10 | # Format Config
11 | [{package.json,.editorconfig,.babelrc,.eslintrc}]
12 | indent_style = space
13 | indent_size = 2
14 |
--------------------------------------------------------------------------------
/src/app.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import DevContainer from 'Components/dev-container/dev-container';
4 | import Wrapper from 'Components/wrapper/wrapper';
5 |
6 | ReactDOM.render((
7 |
8 |
9 |
10 | ), document.getElementById('root'));
11 |
--------------------------------------------------------------------------------
/src/components/dev-container/dev-container.jsx:
--------------------------------------------------------------------------------
1 | // WARNING: This component is only used in development and will not be exported in dist
2 | // It simply provides some base styling for the site
3 |
4 | import React from 'react';
5 | import './dev-container-style';
6 |
7 | export default props => (
8 |
9 | { props.children }
10 |
11 | );
--------------------------------------------------------------------------------
/src/utils/scss/_vars.scss:
--------------------------------------------------------------------------------
1 | $colors: (
2 | malibu: #8DD6F9,
3 | denim: #1D78C1,
4 | fiord: #465E69,
5 | elephant: #2B3A42,
6 | white: #ffffff,
7 | concrete: #f2f2f2,
8 | alto: #dedede,
9 | dusty-grey: #999999,
10 | dove-grey: #666666,
11 | emperor: #535353,
12 | mine-shaft: #333333
13 | );
14 |
15 | $screens: (
16 | xlarge: 1525px,
17 | large: 1024px,
18 | medium: 768px
19 | );
20 |
--------------------------------------------------------------------------------
/src/components/influence/influence-style.scss:
--------------------------------------------------------------------------------
1 | @import 'mixins';
2 | @import 'functions';
3 |
4 | .influence {
5 | flex: 1 1 50%;
6 |
7 | &:first-child {
8 | margin-right: 1em;
9 | }
10 |
11 | &__header {
12 | font-size: 1.5em;
13 | margin-bottom: 0.25em;
14 | }
15 |
16 | &__description {
17 | line-height: 1.5;
18 |
19 | em { font-weight: bolder; }
20 | i { font-style: italic; }
21 | }
22 | }
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "extends": "eslint:recommended",
4 | "parser": "babel-eslint",
5 |
6 | "env": {
7 | "browser": true,
8 | "es6": true,
9 | "node": true
10 | },
11 |
12 | "plugins": [
13 | "react"
14 | ],
15 |
16 | "rules": {
17 | "no-undef": 2,
18 | "no-unreachable": 2,
19 | "no-unused-vars": 0,
20 | "no-console": 0,
21 | "semi": [ "error", "always" ],
22 | "quotes": [ "error", "single" ]
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Voting Application
2 |
3 | This repository contains all the source for the voting application found at [webpack.js.org/vote][1].
4 |
5 |
6 | ## Development
7 |
8 | All development of this application should happen here. Upon release, this package is deployed as an
9 | npm package and then picked up by the other repository.
10 |
11 | - `npm start` to start the `webpack-dev-server`
12 | - `npm run build` to generate the `/dist` directory
13 |
14 |
15 | [1]: https://webpack.js.org/vote
--------------------------------------------------------------------------------
/src/components/currency-icon/currency-icon.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './currency-icon-style';
3 |
4 | // Specify BEM block name
5 | const block = 'currency-icon';
6 |
7 | // ...
8 | const sizes = {
9 | small: 10,
10 | large: 16,
11 | huge: 19
12 | };
13 |
14 | export default ({
15 | type,
16 | size = 'small',
17 | ...props
18 | }) => (
19 |
24 |
25 |
26 |
27 |
28 | );
--------------------------------------------------------------------------------
/webpack.dist.babel.js:
--------------------------------------------------------------------------------
1 | import Merge from 'webpack-merge'
2 | import cssnano from 'cssnano'
3 | import OptimizeCssAssetsPlugin from 'optimize-css-assets-webpack-plugin'
4 |
5 | import CommonConfig from './webpack.common.babel.js'
6 |
7 | export default env => Merge(CommonConfig(env), {
8 | entry: './components/wrapper/wrapper.jsx',
9 |
10 | externals: {
11 | react: {
12 | root: 'React',
13 | commonjs2: 'react',
14 | commonjs: 'react',
15 | amd: 'react'
16 | }
17 | },
18 |
19 | plugins: [
20 | new OptimizeCssAssetsPlugin({
21 | assetNameRegExp: /\.min\.css$/g,
22 | cssProcessor: cssnano
23 | })
24 | ]
25 | })
--------------------------------------------------------------------------------
/webpack.dev.babel.js:
--------------------------------------------------------------------------------
1 | import Path from 'path'
2 | import Webpack from 'webpack'
3 | import HTMLPlugin from 'html-webpack-plugin'
4 | import HTMLTemplate from 'html-webpack-template'
5 | import Merge from 'webpack-merge'
6 |
7 | import CommonConfig from './webpack.common.babel.js'
8 |
9 | export default env => Merge(CommonConfig(env), {
10 | devtool: 'source-map',
11 | entry: './app.jsx',
12 |
13 | plugins: [
14 | new HTMLPlugin({
15 | inject: false,
16 | template: HTMLTemplate,
17 |
18 | title: 'webpack | vote (dev-mode)',
19 | appMountId: 'root',
20 | mobile: true,
21 | favicon: './favicon.ico'
22 | }),
23 |
24 | new Webpack.HotModuleReplacementPlugin()
25 | ],
26 |
27 | devServer: {
28 | hot: true,
29 | port: 3030,
30 | inline: true,
31 | compress: true,
32 | historyApiFallback: true,
33 | contentBase: Path.resolve(__dirname, './dist')
34 | }
35 | })
--------------------------------------------------------------------------------
/src/components/create-topic/create-topic.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Topic from 'Components/topic/topic';
3 | import './create-topic-style';
4 |
5 | const block = 'create-topic';
6 |
7 | export default class CreateTopic extends React.Component {
8 | render() {
9 | return (
10 |
16 | );
17 | }
18 |
19 | /**
20 | * Submit new topic and clear fields
21 | *
22 | * @param {object} e - React synthetic event
23 | */
24 | _create(id, topic) {
25 | let { title, description } = topic;
26 |
27 | if ( title.length && description.length ) {
28 | this.props.onCreate(this.props.id, title, description);
29 |
30 | } else alert('Please enter a valid title and description.');
31 |
32 | return Promise.resolve(false);
33 | }
34 | }
--------------------------------------------------------------------------------
/src/components/dropdown/dropdown-style.scss:
--------------------------------------------------------------------------------
1 | @import 'functions';
2 |
3 | .dropdown {
4 | position: relative;
5 |
6 | &__menu {
7 | position: absolute;
8 | top: calc(100% + 12px);
9 | right: 0;
10 | box-shadow: 0 1px 2px getColor(grey);
11 | border-radius: 3px;
12 | background: getColor(concrete);
13 | }
14 |
15 | &__tip {
16 | position: absolute;
17 | top: -8px;
18 | right: 4px;
19 | margin: 0;
20 | padding: 0;
21 | border-left: 8px solid transparent;
22 | border-bottom: 8px solid getColor(concrete);
23 | border-right: 8px solid transparent;
24 | }
25 |
26 | &__option {
27 | display: block;
28 | width: 100%;
29 | font-size: 12.8px;
30 | padding: 0.25em 1em;
31 | border: none;
32 | outline: none;
33 | text-align: right;
34 | background: transparent;
35 | transition: background 250ms;
36 |
37 | &:not(:last-of-type) {
38 | border-bottom: 1px solid getColor(alto);
39 | }
40 |
41 | &:hover {
42 | background: getColor(alto);
43 | }
44 | }
45 | }
--------------------------------------------------------------------------------
/src/components/votes/votes-style.scss:
--------------------------------------------------------------------------------
1 | @import 'vars';
2 | @import 'mixins';
3 | @import 'functions';
4 |
5 | .votes {
6 | display: flex;
7 | align-items: center;
8 | line-height: 1;
9 |
10 | &__currency {
11 | display: inline-flex;
12 | flex-direction: column;
13 | margin-right: 12px;
14 | }
15 |
16 | &__up,
17 | &__down {
18 | display: flex;
19 | justify-content: center;
20 | margin: 0 auto;
21 | width: 0;
22 | padding: 6px;
23 | &::after {
24 | content: ' ';
25 | border-left: 6px solid transparent;
26 | border-right: 6px solid transparent;
27 | }
28 | cursor: pointer;
29 | transition: border-color 250ms;
30 | }
31 |
32 | &__up {
33 | &::after {
34 | border-bottom: 6px solid getColor(malibu);
35 | margin-bottom: 3px;
36 | }
37 | &:hover::after {
38 | border-bottom: 6px solid getColor(denim);
39 | }
40 | }
41 |
42 | &__down {
43 | &::after {
44 | border-top: 6px solid getColor(dusty-grey);
45 | margin-top: 3px;
46 | }
47 |
48 | &:hover::after {
49 | border-top: 6px solid getColor(emperor);
50 | }
51 | }
52 |
53 | &__score {
54 | }
55 |
56 | &__multiplier {
57 | margin-left: 6px;
58 | font-size: 0.75em;
59 | color: getColor(dusty-grey);
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/components/wrapper/wrapper-style.scss:
--------------------------------------------------------------------------------
1 | @import 'vars';
2 | @import 'mixins';
3 | @import 'functions';
4 |
5 | .wrapper {
6 | margin: 1.5em;
7 |
8 | &__top {
9 | display: flex;
10 | align-items: center;
11 | }
12 |
13 | &__title {
14 | flex: 1 1 auto;
15 | margin: 0;
16 | font-size: 2em;
17 | }
18 |
19 | &__description {
20 | margin: 1em 0;
21 | line-height: 1.5;
22 | }
23 |
24 | &__influences {
25 | display: flex;
26 | margin-bottom: 1em;
27 | }
28 |
29 | &__topics {
30 | padding: 0;
31 | list-style: none;
32 | }
33 |
34 | &__new {
35 | text-align: center;
36 | }
37 |
38 | &__add {
39 | font-size: 14px;
40 | font-weight: 600;
41 | color: getColor(denim);
42 | cursor: pointer;
43 | transition: color 250ms;
44 |
45 | &:hover {
46 | color: getColor(fiord);
47 | }
48 | }
49 |
50 | @media (max-width: 720px) {
51 | & [class*="__top"] {
52 | flex-direction: column;
53 | align-items: flex-start;
54 |
55 | & .account__info {
56 | order: 2;
57 | text-align: left;
58 | }
59 | }
60 |
61 | & [class*="__influences"] {
62 | flex-direction: column;
63 |
64 | & .influence {
65 | margin-bottom: 1em;
66 | }
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/components/influence/influence.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import CurrencyIcon from 'Components/currency-icon/currency-icon';
3 | import './influence-style';
4 |
5 | // Specify BEM block name
6 | const block = 'influence';
7 |
8 | export default props => {
9 | return props.type === 'normal' ? (
10 |
11 |
12 | Influence
13 |
14 |
15 |
16 | Influence is a unit of measure based on time you have been a member on GitHub. However,
17 | from 2017 on you will receive one influence per day.
18 |
19 |
20 | ) : (
21 |
22 |
23 | Golden Influence
24 |
25 |
26 |
27 | Golden Influence is equal to 100 normal influence . Golden Influence is obtained
28 | by being a backer or sponsor on our
29 |
30 | Open Collective page
31 | .
32 |
33 |
34 | );
35 | };
36 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4 |
5 |
6 | # [0.3.0](https://github.com/webpack-contrib/voting-app/compare/v0.1.3...v0.3.0) (2020-03-14)
7 |
8 |
9 | ### Features
10 |
11 | * include vote keys in tab order ([#19](https://github.com/webpack-contrib/voting-app/issues/19)) ([95e524f](https://github.com/webpack-contrib/voting-app/commit/95e524f))
12 |
13 |
14 |
15 |
16 | # [0.2.0](https://github.com/webpack-contrib/voting-app/compare/v0.1.3...v0.2.0) (2020-03-11)
17 |
18 |
19 | ### Features
20 |
21 | * include vote keys in tab order ([#19](https://github.com/webpack-contrib/voting-app/issues/19)) ([95e524f](https://github.com/webpack-contrib/voting-app/commit/95e524f))
22 |
23 |
24 |
25 |
26 | ## [0.1.2](https://github.com/webpack-contrib/voting-app/compare/v0.1.1...v0.1.2) (2017-12-28)
27 |
28 |
29 |
30 |
31 | ## [0.1.1](https://github.com/webpack-contrib/voting-app/compare/v0.1.0...v0.1.1) (2017-12-27)
32 |
33 |
34 | ### Bug Fixes
35 |
36 | * **login:** unwrap callbackUrl object and query includes off of href ([a83a6ee](https://github.com/webpack-contrib/voting-app/commit/a83a6ee))
37 |
38 |
39 |
40 |
41 | # 0.1.0 (2017-12-20)
42 |
43 |
44 | ### Bug Fixes
45 |
46 | * **config:** fix webpack configuration to properly export the top-level component ([003b395](https://github.com/webpack-contrib/voting-app/commit/003b395))
47 | * fix issue with `fetch` replaced by an object ([c8d901a](https://github.com/webpack-contrib/voting-app/commit/c8d901a))
48 |
49 |
50 | ### Features
51 |
52 | * update styling to display the site well on mobile devices ([2610742](https://github.com/webpack-contrib/voting-app/commit/2610742))
53 |
--------------------------------------------------------------------------------
/src/components/account/account-style.scss:
--------------------------------------------------------------------------------
1 | @import 'functions';
2 |
3 | .account {
4 | flex: 0 0 auto;
5 | position: relative;
6 |
7 | &__login {
8 | display: flex;
9 | border: none;
10 | outline: none;
11 | color: getColor(white);
12 | background: getColor(elephant);
13 | padding: 5px 10px 5px 10px;
14 | border-radius: 5px;
15 | font-size: 13px;
16 | cursor: pointer;
17 | align-items: center;
18 |
19 | &:hover {
20 | background: black;
21 | }
22 |
23 | &:active {
24 | background: getColor(elephant)
25 | }
26 |
27 | svg {
28 | padding-left: 5px;
29 | }
30 | }
31 |
32 | &__inner {
33 | display: flex;
34 | }
35 |
36 | &__info {
37 | flex: 1 1 auto;
38 | display: flex;
39 | flex-direction: column;
40 | font-size: 12.8px;
41 | margin: 0 0.5em;
42 | justify-content: space-around;
43 | text-align: right;
44 | }
45 |
46 | &__title {
47 | font-weight: 600;
48 | }
49 |
50 | &__separator {
51 | margin: 0 0.5em;
52 | color: getColor(alto);
53 | }
54 |
55 | &__currency {
56 | cursor: help;
57 |
58 | &:last-child {
59 | margin-left: 1em;
60 | }
61 |
62 | & :last-child {
63 | margin-left: 0.5em;
64 | }
65 | }
66 |
67 | &__avatar {
68 | display: inline-block;
69 | width: 35px;
70 | height: 35px;
71 | border-radius: 100%;
72 | font-size: 12.8px;
73 | line-height: 2.75;
74 | text-align: center;
75 | text-overflow: ellipsis;
76 | cursor: pointer;
77 | background: whitesmoke;
78 | overflow: hidden;
79 | transition: background 250ms;
80 |
81 | &:hover {
82 | background: getColor(alto);
83 | }
84 | }
85 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "webpack.vote",
3 | "version": "0.3.0",
4 | "description": "An application for casting votes on new webpack features and fixes.",
5 | "main": "dist/vote.bundle.js",
6 | "scripts": {
7 | "start": "webpack-dev-server --config webpack.dev.babel.js --env.dev",
8 | "watch": "webpack --config webpack.dist.babel.js --watch",
9 | "build": "webpack --config webpack.dist.babel.js",
10 | "test": "echo \"Error: no test specified\" && exit 1",
11 | "release": "standard-version"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "git+https://github.com/webpack-contrib/voting-app.git"
16 | },
17 | "keywords": [
18 | "webpack",
19 | "vote",
20 | "voting"
21 | ],
22 | "author": "Greg Venech",
23 | "license": "ISC",
24 | "bugs": {
25 | "url": "https://github.com/webpack-contrib/voting-app/issues"
26 | },
27 | "homepage": "https://github.com/webpack-contrib/voting-app#readme",
28 | "devDependencies": {
29 | "autoprefixer": "^7.1.6",
30 | "babel-core": "^6.24.1",
31 | "babel-eslint": "^8.0.2",
32 | "babel-loader": "^7.1.2",
33 | "babel-plugin-transform-object-rest-spread": "^6.23.0",
34 | "babel-preset-es2015": "^6.24.1",
35 | "babel-preset-react": "^6.24.1",
36 | "css-loader": "^0.28.0",
37 | "cssnano": "^4.1.10",
38 | "eslint": "^4.19.1",
39 | "eslint-loader": "^2.2.1",
40 | "eslint-plugin-react": "^7.5.1",
41 | "file-loader": "^1.1.11",
42 | "html-webpack-plugin": "^3.2.0",
43 | "html-webpack-template": "^6.0.1",
44 | "mini-css-extract-plugin": "^0.9.0",
45 | "node-sass": "^4.13.1",
46 | "optimize-css-assets-webpack-plugin": "^5.0.3",
47 | "postcss-loader": "^2.1.6",
48 | "react": "^16.13.0",
49 | "react-dom": "^16.13.0",
50 | "sass-loader": "^6.0.3",
51 | "standard-version": "^4.2.0",
52 | "style-loader": "^1.1.3",
53 | "webpack": "^4.42.0",
54 | "webpack-cli": "^3.3.11",
55 | "webpack-dev-server": "^3.10.3",
56 | "webpack-merge": "^4.1.0"
57 | },
58 | "dependencies": {
59 | "react-textarea-autosize": "^5.2.1",
60 | "uuid": "^3.1.0"
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/webpack.common.babel.js:
--------------------------------------------------------------------------------
1 | import Path from 'path'
2 | import MiniCssExtractPlugin from 'mini-css-extract-plugin'
3 |
4 | export default (env = {}) => ({
5 | context: Path.resolve(__dirname, './src'),
6 |
7 | mode: env.dev ? 'development' : 'production',
8 |
9 | resolve: {
10 | symlinks: false,
11 | extensions: ['.js', '.jsx', '.scss'],
12 | alias: {
13 | Components: Path.resolve(__dirname, './src/components'),
14 | Utils: Path.resolve(__dirname, './src/utils')
15 | }
16 | },
17 |
18 | module: {
19 | rules: [
20 | {
21 | test: /\.jsx?$/,
22 | exclude: /node_modules/,
23 | use: [
24 | 'babel-loader',
25 | {
26 | loader: 'eslint-loader',
27 | options: {
28 | fix: true,
29 | configFile: Path.resolve(__dirname, './.eslintrc')
30 | }
31 | }
32 | ]
33 | },
34 | {
35 | test: /\.s?css$/,
36 | use: [
37 | MiniCssExtractPlugin.loader,
38 | 'css-loader',
39 | {
40 | loader: 'postcss-loader',
41 | options: {
42 | plugins: () => [
43 | require('autoprefixer')
44 | ],
45 | }
46 | },
47 | {
48 | loader: 'sass-loader',
49 | options: {
50 | includePaths: [Path.resolve(__dirname, './src/utils/scss')]
51 | }
52 | }
53 | ]
54 | },
55 | {
56 | test: /\.(jpg|png|svg)$/,
57 | use: 'file-loader'
58 | }
59 | ]
60 | },
61 |
62 | plugins: [
63 | new MiniCssExtractPlugin({
64 | filename: 'style.min.css'
65 | })
66 | ],
67 |
68 | output: {
69 | path: Path.resolve(__dirname, './dist'),
70 | publicPath: '/',
71 | filename: 'vote.bundle.js',
72 | library: 'vote',
73 | libraryTarget: 'umd'
74 | },
75 |
76 | stats: {
77 | children: false
78 | }
79 | })
80 |
--------------------------------------------------------------------------------
/src/components/dropdown/dropdown.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './dropdown-style';
3 |
4 | // Specify BEM block name
5 | const block = 'dropdown';
6 |
7 | export default class Dropdown extends React.Component {
8 | constructor(props) {
9 | super(props);
10 |
11 | this._handleAllClicks = this._handleAllClicks.bind(this);
12 | this.state = {
13 | open: false
14 | };
15 | }
16 |
17 | render() {
18 | let { className = '', options, onChange } = this.props,
19 | { width = 150, tipOffset = 6 } = this.props;
20 |
21 | return (
22 | this._container = ref }
24 | className={ `${block} ${className}` }>
25 |
28 | { this.props.children }
29 |
30 |
31 | { this.state.open ? (
32 |
35 | { options.map(option => (
36 |
40 | { option.label }
41 |
42 | ))}
43 |
44 |
47 |
48 | ) : null }
49 |
50 | );
51 | }
52 |
53 | componentDidMount() {
54 | if ( document ) {
55 | document.addEventListener(
56 | 'click',
57 | this._handleAllClicks
58 | );
59 | }
60 | }
61 |
62 | componentWillUnmount() {
63 | if ( document ) {
64 | document.removeEventListener(
65 | 'click',
66 | this._handleAllClicks
67 | );
68 | }
69 | }
70 |
71 | /**
72 | * Display and hide the menu
73 | *
74 | */
75 | _toggle() {
76 | this.setState({
77 | open: !this.state.open
78 | });
79 | }
80 |
81 | /**
82 | * Handle any clicks throughout the page
83 | *
84 | * @param {object} e - Native click event
85 | */
86 | _handleAllClicks(e) {
87 | if ( !this._container.contains(e.target) ) {
88 | this.setState({
89 | open: false
90 | });
91 | }
92 | }
93 | }
--------------------------------------------------------------------------------
/src/components/topic/topic-style.scss:
--------------------------------------------------------------------------------
1 | @import 'mixins';
2 | @import 'functions';
3 |
4 | .topic {
5 | display: flex;
6 | margin-bottom: 24px;
7 | font-size: 12.8px;
8 |
9 | &__content,
10 | &__vote {
11 | display: flex;
12 | flex-direction: column;
13 | }
14 |
15 | &__content {
16 | flex: 1 1 auto;
17 |
18 | & [class*="__inner"] {
19 | border-width: 0 0 2px 2px;
20 | }
21 | }
22 |
23 | input,
24 | textarea {
25 | padding: 0;
26 | outline: none;
27 | border: none;
28 | box-shadow: none;
29 | font-family: inherit;
30 | font-size: inherit;
31 | font-weight: inherit;
32 | color: inherit;
33 | background: transparent;
34 | }
35 |
36 | &__title {
37 | flex: 0 0 auto;
38 | font-weight: 600;
39 | padding: 0.55em 1em 0.5em;
40 | text-transform: uppercase;
41 | background: transparentize(getColor(fiord), 0.75);
42 |
43 | &--vote {
44 | background: getColor(fiord);
45 | text-align: right;
46 | font-weight: 400;
47 | color: getColor(white);
48 | }
49 |
50 | input {
51 | width: 90%;
52 | }
53 | }
54 |
55 | &__settings {
56 | float: right;
57 | max-height: 15px;
58 | cursor: pointer;
59 |
60 | path {
61 | fill: getColor(denim);
62 | transition: fill 250ms;
63 | }
64 |
65 | &:hover path {
66 | fill: getColor(fiord);
67 | }
68 | }
69 |
70 | &__inner {
71 | flex: 1 1 auto;
72 | border: 2px solid transparentize(getColor(fiord), 0.75);
73 | border-width: 0 2px 2px 2px;
74 | }
75 |
76 | &__content [class*="__inner"] {
77 | display: flex;
78 | flex-direction: column;
79 | }
80 |
81 | &__description {
82 | flex: 1 1 auto;
83 | padding: 1em;
84 | line-height: 1.5;
85 |
86 | textarea {
87 | width: 100% !important;
88 | line-height: 1.5;
89 | resize: none;
90 | }
91 | }
92 |
93 | &__save {
94 | flex: 0 0 auto;
95 | margin-left: 1em;
96 | }
97 |
98 | &__sponsors {
99 | flex: 0 0 auto;
100 | padding: 1em;
101 | }
102 |
103 | &__people {
104 | margin-top: 0.25em;
105 | }
106 |
107 | &__vote {
108 | flex: 0 0 250px;
109 | }
110 |
111 | &__vote [class*="__inner"] {
112 | display: flex;
113 | flex-direction: column;
114 | justify-content: flex-end;
115 | padding-top: 1em;
116 | }
117 |
118 | &__field,
119 | &__total {
120 | font-size: 24px;
121 | }
122 |
123 | &__field {
124 | justify-content: flex-end;
125 | padding: 0 16px;
126 | margin-bottom: 0.5em;
127 |
128 | &:first-child:after {
129 | content: '+';
130 | margin-left: 12px;
131 | }
132 | }
133 |
134 | &__total {
135 | padding: 0.25em 16px;
136 | text-align: right;
137 | border-top: 2px solid transparentize(getColor(fiord), 0.75);
138 | }
139 |
140 | @media (max-width: 720px) {
141 | flex-direction: column;
142 | font-size: 1rem;
143 | border: 2px solid transparentize(getColor(fiord), 0.75);
144 |
145 | & [class*="__sponsors"],
146 | & [class*="__title--vote"] {
147 | display: none;
148 | }
149 |
150 | & [class*="__inner"] {
151 | border: none;
152 | }
153 |
154 | & [class*="__vote"] {
155 | flex: 0 0 auto;
156 | margin-top: 1em;
157 | }
158 |
159 | & [class*="__field"] {
160 | justify-content: flex-start;
161 | }
162 |
163 | & [class*="__total"] {
164 | text-align: left;
165 | }
166 | }
167 | }
--------------------------------------------------------------------------------
/src/utils/js/api.js:
--------------------------------------------------------------------------------
1 | const API_URL = 'https://oswils44oj.execute-api.us-east-1.amazonaws.com/production/';
2 | const GITHUB_CLIENT_ID = '4d355e2799cb8926c665';
3 |
4 | function checkResult(result) {
5 | if ( !result ) throw new Error('No result received');
6 | if ( result.errorMessage ) throw new Error(result.errorMessage);
7 |
8 | return result;
9 | }
10 |
11 | export function isLoginActive() {
12 | return /^\?code=([^&]*)&state=([^&]*)/.test(window.location.search);
13 | }
14 |
15 | export function startLogin(url = '') {
16 | let state = '' + Math.random();
17 |
18 | if ( url.includes('webpack.js.org') ) {
19 | window.localStorage.githubState = state;
20 | window.location = 'https://github.com/login/oauth/authorize?client_id=' + GITHUB_CLIENT_ID + '&scope=user:email&state=' + state + '&allow_signup=false&redirect_uri=' + encodeURIComponent(url);
21 |
22 | } else alert(
23 | 'You can\'t login with GitHub OAuth on localhost. Please pass the ' +
24 | '`development` prop to the `Wrapper` in order to use `api.dev`.'
25 | );
26 |
27 | return Promise.resolve();
28 | }
29 |
30 | export function continueLogin() {
31 | const match = /^\?code=([^&]*)&state=([^&]*)/.exec(window.location.search);
32 |
33 | if ( match ) {
34 | return login(match[1], match[2]).then(result => {
35 | setTimeout(() => {
36 | let href = window.location.href;
37 | window.location = href.substr(0, href.length - window.location.search.length);
38 | }, 100);
39 |
40 | return result;
41 | });
42 | }
43 |
44 | return Promise.resolve();
45 | }
46 |
47 | function login(code, state) {
48 | if ( state !== window.localStorage.githubState ) {
49 | return Promise.reject(new Error('Request state doesn\'t match (Login was triggered by 3rd party)'));
50 |
51 | } else {
52 | delete window.localStorage.githubState;
53 |
54 | return fetch(API_URL + '/login', {
55 | headers: {
56 | 'Content-Type': 'application/json'
57 | },
58 | method: 'POST',
59 | body: JSON.stringify({
60 | code,
61 | state
62 | })
63 | })
64 | .then((res) => res.json())
65 | .then(checkResult).then(result => {
66 | if (!result.token) throw new Error('No token received from API');
67 |
68 | return result.token;
69 | });
70 | }
71 | }
72 |
73 | export function getSelf(token) {
74 | return fetch(`${API_URL}/self?token=${token}`, {
75 | mode: 'cors'
76 | })
77 | .then((res) => res.json())
78 | .then(checkResult);
79 | }
80 |
81 | export function getList(token, name = 'todo') {
82 | return fetch(`${API_URL}/list/${name}` + (token ? `?token=${token}` : ''), {
83 | mode: 'cors'
84 | })
85 | .then((res) => res.json())
86 | .then(checkResult);
87 | }
88 |
89 | export function createItem(token, list = 'todo', title, description) {
90 | return fetch(`${API_URL}/list/${list}?token=${token}`, {
91 | method: 'POST',
92 | headers: {
93 | 'Content-Type': 'application/json'
94 | },
95 | body: JSON.stringify({
96 | title,
97 | description
98 | })
99 | })
100 | .then((res) => res.json())
101 | .then(checkResult);
102 | }
103 |
104 | export function vote(token, itemId, voteName, value) {
105 | return fetch(`${API_URL}/vote/${itemId}/${voteName}?token=${token}`, {
106 | method: 'POST',
107 | headers: {
108 | 'Content-Type': 'application/json'
109 | },
110 | body: JSON.stringify({
111 | count: value
112 | })
113 | })
114 | .then((res) => res.json())
115 | .then(checkResult)
116 | .then(result => true);
117 | }
118 |
119 | export function configItem(token, itemId, config) {
120 | return fetch(`${API_URL}/config/${itemId}?token=${token}`, {
121 | method: 'POST',
122 | headers: {
123 | 'Content-Type': 'application/json'
124 | },
125 | body: JSON.stringify({
126 | config: config
127 | })
128 | })
129 | .then((res) => res.json())
130 | .then(checkResult).then(result => true);
131 | }
132 |
--------------------------------------------------------------------------------
/src/components/votes/votes.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import CurrencyIcon from 'Components/currency-icon/currency-icon';
3 | import './votes-style';
4 |
5 | // Specify BEM block name
6 | const block = 'votes';
7 |
8 | export default class Votes extends React.Component {
9 | render() {
10 | let { className = '', currency, current, locked, user } = this.props;
11 |
12 | return (
13 |
14 |
15 | { !locked && user.currency && (
16 | this._vote(1) }
20 | onMouseDown={ () => this._startCounter(true) }
21 | onMouseUp={ () => this._stopCounter() }
22 | onMouseOut={ () => this._stopCounter() }
23 | onTouchStart={ () => this._startCounter(true) }
24 | onTouchEnd={ () => this._stopCounter() }
25 | onTouchCancel={ () => this._stopCounter() } />
26 | )}
27 |
28 |
31 |
32 | { !locked && user.currency && (
33 | this._vote(-1) }
37 | onMouseDown={ () => this._startCounter(false) }
38 | onMouseUp={ () => this._stopCounter() }
39 | onMouseOut={ () => this._stopCounter() }
40 | onTouchStart={ () => this._startCounter(false) }
41 | onTouchEnd={ () => this._stopCounter() }
42 | onTouchCancel={ () => this._stopCounter() } />
43 | )}
44 |
45 |
46 | { current.votes }
47 |
48 |
49 | x { currency.score }
50 |
51 |
52 |
53 | );
54 | }
55 |
56 | /**
57 | * Computes the maximum amount of votes that can be used
58 | *
59 | * @return {number} - The maximum number of votes allowed
60 | */
61 | get _maximum() {
62 | let { user, currency } = this.props,
63 | { maximum = 1000 } = currency;
64 |
65 | if ( user.currency && (user.currency.remaining + user.votes.votes) < maximum ) {
66 | return user.currency.remaining + user.votes.votes;
67 |
68 | } else return maximum;
69 | }
70 |
71 | /**
72 | * Trigger a new `number` of votes to be added
73 | *
74 | * @param {number} number - The number of votes to use
75 | */
76 | _vote(number) {
77 | let { user } = this.props,
78 | { votes } = user.votes,
79 | limit = this._maximum - votes;
80 |
81 | this.props.onVote(
82 | Math.min(
83 | limit,
84 | Math.max(number, -votes)
85 | )
86 | );
87 | }
88 |
89 | /**
90 | * Continually increase or decrease the vote with a dynamic change
91 | * based on how the long the button has been held
92 | *
93 | * @param {boolean} increase - Indicates whether to increase or decrease
94 | */
95 | _startCounter(increase) {
96 | let current = 0;
97 | let change = 0;
98 |
99 | if (this._interval) {
100 | clearInterval(this._interval);
101 | }
102 |
103 | this._interval = setInterval(() => {
104 | if ( current <= 5 ) {
105 | current++;
106 | change = 1;
107 |
108 | } else if ( current <= 10 ) {
109 | current += 2;
110 | change = 2;
111 |
112 | } else if ( current <= 40 ) {
113 | current += 5;
114 | change = 5;
115 |
116 | } else if ( current <= 70 ) {
117 | current += 10;
118 | change = 10;
119 |
120 | } else {
121 | current += 15;
122 | change = 15;
123 | }
124 |
125 | if ( !increase ) {
126 | change *= -1;
127 | }
128 |
129 | this._vote(change);
130 | }, 200);
131 | }
132 |
133 | /**
134 | * Stop the continual increase or decrease interval
135 | *
136 | */
137 | _stopCounter() {
138 | if (this._interval) {
139 | clearInterval(this._interval);
140 | }
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/src/components/account/account.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import CurrencyIcon from 'Components/currency-icon/currency-icon';
3 | import Dropdown from 'Components/dropdown/dropdown';
4 | import './account-style';
5 |
6 | // Specify BEM block name
7 | const block = 'account';
8 |
9 | export default class Account extends React.Component {
10 | render() {
11 | let { userData, possibleVotes = [], loading } = this.props,
12 | { currencies = [] } = userData || {};
13 |
14 | return (
15 |
16 | { !userData && loading ? (
17 |
Loading user info...
18 |
19 | ) : !userData ? (
20 |
23 | Login with GitHub
24 |
25 |
26 |
27 |
28 |
29 | ) : (
30 |
31 |
32 |
33 | { userData.name }
34 | //
35 | { userData.login }
36 |
37 |
38 | {
39 | possibleVotes
40 | .map(settings => currencies.find(obj => obj.name === settings.currency))
41 | .map(({ name, displayName, remaining, used, value }) => (
42 |
46 | { remaining }
47 |
48 |
49 | ))
50 | }
51 |
52 |
53 |
54 |
61 |
65 |
66 |
67 | )}
68 |
69 | );
70 | }
71 |
72 | /**
73 | * Initiate GitHub login process
74 | *
75 | */
76 | _login() {
77 | let { location = {} } = window,
78 | { href = '' } = location;
79 |
80 | this.props.startLogin(href);
81 | }
82 |
83 | /**
84 | * Log the user out by removing their token
85 | *
86 | */
87 | _logout() {
88 | delete window.localStorage.voteAppToken;
89 | window.location.reload();
90 | }
91 | }
--------------------------------------------------------------------------------
/dist/style.min.css:
--------------------------------------------------------------------------------
1 | .currency-icon{display:inline-block}.currency-icon--influence{fill:#1d78c1}.currency-icon--goldenInfluence{fill:#f9bf3b}.currency-icon--support{fill:green}.influence{-webkit-box-flex:1;-ms-flex:1 1 50%;flex:1 1 50%}.influence:first-child{margin-right:1em}.influence__header{font-size:1.5em;margin-bottom:.25em}.influence__description{line-height:1.5}.influence__description em{font-weight:bolder}.influence__description i{font-style:italic}.dropdown{position:relative}.dropdown__menu{position:absolute;top:calc(100% + 12px);right:0;-webkit-box-shadow:0 1px 2px;box-shadow:0 1px 2px;border-radius:3px;background:#f2f2f2}.dropdown__tip{position:absolute;top:-8px;right:4px;margin:0;padding:0;border-left:8px solid transparent;border-bottom:8px solid #f2f2f2;border-right:8px solid transparent}.dropdown__option{display:block;width:100%;font-size:12.8px;padding:.25em 1em;border:none;outline:none;text-align:right;background:transparent;-webkit-transition:background .25s;transition:background .25s}.dropdown__option:not(:last-of-type){border-bottom:1px solid #dedede}.dropdown__option:hover{background:#dedede}.account{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;position:relative}.account__login{display:-webkit-box;display:-ms-flexbox;display:flex;border:none;outline:none;color:#fff;background:#2b3a42;padding:5px 10px;border-radius:5px;font-size:13px;cursor:pointer;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.account__login:hover{background:#000}.account__login:active{background:#2b3a42}.account__login svg{padding-left:5px}.account__info,.account__inner{display:-webkit-box;display:-ms-flexbox;display:flex}.account__info{-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;font-size:12.8px;margin:0 .5em;-ms-flex-pack:distribute;justify-content:space-around;text-align:right}.account__title{font-weight:600}.account__separator{margin:0 .5em;color:#dedede}.account__currency{cursor:help}.account__currency:last-child{margin-left:1em}.account__currency :last-child{margin-left:.5em}.account__avatar{display:inline-block;width:35px;height:35px;border-radius:100%;font-size:12.8px;line-height:2.75;text-align:center;text-overflow:ellipsis;cursor:pointer;background:#f5f5f5;overflow:hidden;-webkit-transition:background .25s;transition:background .25s}.account__avatar:hover{background:#dedede}.votes{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;line-height:1}.votes__currency{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;margin-right:12px}.votes__down,.votes__up{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;margin:0 auto;width:0;padding:6px;cursor:pointer;-webkit-transition:border-color .25s;transition:border-color .25s}.votes__down:after,.votes__up:after{content:" ";border-left:6px solid transparent;border-right:6px solid transparent}.votes__up:after{border-bottom:6px solid #8dd6f9;margin-bottom:3px}.votes__up:hover:after{border-bottom:6px solid #1d78c1}.votes__down:after{border-top:6px solid #999;margin-top:3px}.votes__down:hover:after{border-top:6px solid #535353}.votes__multiplier{margin-left:6px;font-size:.75em;color:#999}.topic{margin-bottom:24px;font-size:12.8px}.topic,.topic__content,.topic__vote{display:-webkit-box;display:-ms-flexbox;display:flex}.topic__content,.topic__vote{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.topic__content{-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto}.topic__content [class*=__inner]{border-width:0 0 2px 2px}.topic input,.topic textarea{padding:0;outline:none;border:none;-webkit-box-shadow:none;box-shadow:none;font-family:inherit;font-size:inherit;font-weight:inherit;color:inherit;background:transparent}.topic__title{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;font-weight:600;padding:.55em 1em .5em;text-transform:uppercase;background:rgba(70,94,105,.25)}.topic__title--vote{background:#465e69;text-align:right;font-weight:400;color:#fff}.topic__title input{width:90%}.topic__settings{float:right;max-height:15px;cursor:pointer}.topic__settings path{fill:#1d78c1;-webkit-transition:fill .25s;transition:fill .25s}.topic__settings:hover path{fill:#465e69}.topic__inner{-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;border:2px solid rgba(70,94,105,.25);border-top:0 solid rgba(70,94,105,.25)}.topic__content [class*=__inner]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.topic__description{-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;padding:1em;line-height:1.5}.topic__description textarea{width:100%!important;line-height:1.5;resize:none}.topic__save{margin-left:1em}.topic__save,.topic__sponsors{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto}.topic__sponsors{padding:1em}.topic__people{margin-top:.25em}.topic__vote{-webkit-box-flex:0;-ms-flex:0 0 250px;flex:0 0 250px}.topic__vote [class*=__inner]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end;padding-top:1em}.topic__field,.topic__total{font-size:24px}.topic__field{-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end;padding:0 16px;margin-bottom:.5em}.topic__field:first-child:after{content:"+";margin-left:12px}.topic__total{padding:.25em 16px;text-align:right;border-top:2px solid rgba(70,94,105,.25)}@media (max-width:720px){.topic{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;font-size:1rem;border:2px solid rgba(70,94,105,.25)}.topic [class*=__sponsors],.topic [class*=__title--vote]{display:none}.topic [class*=__inner]{border:none}.topic [class*=__vote]{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;margin-top:1em}.topic [class*=__field]{-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.topic [class*=__total]{text-align:left}}.create-topic{text-align:left}.wrapper{margin:1.5em}.wrapper__top{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.wrapper__title{-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;margin:0;font-size:2em}.wrapper__description{margin:1em 0;line-height:1.5}.wrapper__influences{display:-webkit-box;display:-ms-flexbox;display:flex;margin-bottom:1em}.wrapper__topics{padding:0;list-style:none}.wrapper__new{text-align:center}.wrapper__add{font-size:14px;font-weight:600;color:#1d78c1;cursor:pointer;-webkit-transition:color .25s;transition:color .25s}.wrapper__add:hover{color:#465e69}@media (max-width:720px){.wrapper [class*=__top]{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start}.wrapper [class*=__top] .account__info{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2;text-align:left}.wrapper [class*=__influences]{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.wrapper [class*=__influences] .influence{margin-bottom:1em}}
--------------------------------------------------------------------------------
/src/components/topic/topic.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Votes from 'Components/votes/votes';
3 | import Dropdown from 'Components/dropdown/dropdown';
4 | import Textarea from 'react-textarea-autosize';
5 | import './topic-style';
6 |
7 | // Specify BEM block name
8 | const block = 'topic';
9 |
10 | export default class Topic extends React.Component {
11 | constructor(props) {
12 | super(props);
13 |
14 | this.state = {
15 | editing: props.editing || false,
16 | title: props.topic.title,
17 | description: props.topic.description
18 | };
19 | }
20 |
21 | render() {
22 | let { className = '', user, admin, topic, votes, token } = this.props,
23 | { editing, title, description } = this.state;
24 |
25 | return (
26 |
27 |
28 |
29 | { !editing ? title : (
30 |
34 | )}
35 |
36 | { admin ? (
37 |
47 |
48 |
49 |
50 |
51 | ) : null }
52 |
53 |
54 |
55 | { !editing ? description : (
56 |
59 | )}
60 |
61 |
62 | { editing ? (
63 |
66 | Done Editing
67 |
68 | ) : null }
69 |
70 |
71 |
Sponsors
72 |
Coming soon...
73 |
74 |
75 |
76 |
77 |
78 |
79 | {topic.locked ? 'Voting is not allowed' : 'Place your vote'}
80 |
81 |
82 | { votes.map((currency, i) => (
83 |
obj.name === currency.name) }
88 | locked={ topic.locked }
89 | user={{
90 | votes: topic.userVotes && topic.userVotes.find(obj => obj.name === currency.name),
91 | currency: user && user.currencies.find(obj => obj.name === currency.currency)
92 | }}
93 | onVote={ this._vote.bind(this, currency) } />
94 | ))}
95 |
96 | { topic.score }
97 |
98 |
99 |
100 |
101 | );
102 | }
103 |
104 | /**
105 | * Trigger voting process
106 | *
107 | * @param {object} settings - Data describing the type of vote
108 | * @param {number} difference - Amount to change influence
109 | */
110 | _vote(settings = {}, difference) {
111 | let { topic } = this.props;
112 |
113 | this.props.onVote(
114 | topic.id,
115 | settings.name,
116 | difference,
117 | settings.currency,
118 | settings.score
119 | );
120 | }
121 |
122 | /**
123 | * Enable topic editing
124 | *
125 | * @param {object} e - React synthetic event
126 | */
127 | _edit(e) {
128 | this.setState({
129 | editing: true
130 | });
131 | }
132 |
133 | /**
134 | * Update the topic's title
135 | *
136 | * @param {object} e - React synthetic event
137 | */
138 | _changeTitle(e) {
139 | this.setState({
140 | title: e.target.value
141 | });
142 | }
143 |
144 | /**
145 | * Update the topic's description
146 | *
147 | * @param {object} e - React synthetic event
148 | */
149 | _changeDescription(e) {
150 | this.setState({
151 | description: e.target.value
152 | });
153 | }
154 |
155 | /**
156 | * Update topic settings
157 | *
158 | * @param {object} option - The dropdown object (a label and settings)
159 | */
160 | _changeSettings({ label, ...settings}) {
161 | let { topic } = this.props;
162 |
163 | this.props.onChangeSettings(
164 | topic.id,
165 | settings
166 | );
167 | }
168 |
169 | /**
170 | * Pass changes up via this.props.save
171 | *
172 | * @param {object} e - React synthetic event
173 | */
174 | _saveChanges(e) {
175 | let { topic } = this.props,
176 | { title, description } = this.state;
177 |
178 | this.props
179 | .onChangeSettings(topic.id, { title, description })
180 | .then(success => {
181 | if (success) {
182 | this.setState({
183 | editing: false
184 | });
185 | }
186 | });
187 | }
188 | }
189 |
--------------------------------------------------------------------------------
/src/utils/js/api.dev.js:
--------------------------------------------------------------------------------
1 | let usedCurrencies = {
2 | influence: 100,
3 | goldenInfluence: 100
4 | };
5 |
6 | let totalCurrencies = {
7 | influence: 1000,
8 | goldenInfluence: 300
9 | };
10 |
11 | let lists = {
12 | todo: {
13 | possibleVotes: [
14 | {
15 | name: 'influence',
16 | currency: 'influence',
17 | score: 1,
18 | color: 'blue'
19 | },
20 | {
21 | name: 'golden',
22 | currency: 'goldenInfluence',
23 | score: 100,
24 | color: '#bfa203'
25 | }
26 | ],
27 | items: [
28 | {
29 | id: '1234',
30 | list: 'todo',
31 | title: 'Some Feature',
32 | description: 'Mauris et sem a risus pharetra suscipit. Fusce gravida purus non nisi pulvinar, non lobortis tortor vehicula. Mauris at dui a ex vestibulum condimentum id sit amet nisl. Nam finibus ornare laoreet. Duis ultrices sollicitudin quam eu vulputate. Sed ac ante odio. Mauris fermentum vel tortor sit amet iaculis.',
33 | influence: 15
34 | },
35 | {
36 | id: '2345',
37 | list: 'todo',
38 | title: 'Review Stuff',
39 | description: 'Cras libero libero, elementum eu laoreet nec, convallis sit amet sem. Donec ut diam dignissim, hendrerit ante id, congue felis. In hac habitasse platea dictumst. Donec sit amet tellus et neque auctor consequat. Cras porta finibus turpis, id auctor lectus.',
40 | golden: 20
41 | },
42 | {
43 | id: '3456',
44 | list: 'todo',
45 | title: 'Another Feature',
46 | description: 'Curabitur pharetra facilisis mauris. Integer interdum venenatis metus quis dictum. Cras aliquet erat ut risus vestibulum, sed tincidunt enim maximus. Cras tincidunt vulputate ante vitae tincidunt. Cras quis erat eu augue aliquam pretium nec sit amet magna. Etiam nisi nunc, blandit vel hendrerit et, suscipit finibus nunc.',
47 | golden: 20
48 | }
49 | ]
50 | }
51 | };
52 |
53 | let allItems = {
54 | '1234': lists.todo.items[0],
55 | '2345': lists.todo.items[1],
56 | '3456': lists.todo.items[2]
57 | };
58 |
59 | function delay(time) {
60 | return new Promise(function (fulfill) {
61 | setTimeout(fulfill, time);
62 | });
63 | }
64 |
65 | function clone(json) {
66 | return JSON.parse(
67 | JSON.stringify(json)
68 | );
69 | }
70 |
71 | export function isLoginActive() {
72 | return /^\?login=/.test(window.location.search);
73 | }
74 |
75 | export function startLogin(callbackUrl) {
76 | window.location.search = '?login=' + encodeURIComponent(callbackUrl);
77 | return Promise.resolve();
78 | }
79 |
80 | export function continueLogin() {
81 | if ( /^\?login=/.test(window.location.search) ) {
82 | return delay(2000).then(() => {
83 | setTimeout(() => window.location = decodeURIComponent(window.location.search.substr(7), 100));
84 | return 'developer';
85 | });
86 | }
87 |
88 | return Promise.resolve();
89 | }
90 |
91 | export function getSelf(token) {
92 | if (token !== 'developer') {
93 | return Promise.reject(new Error('Not logged in as developer'));
94 |
95 | } else {
96 | return delay(500).then(() => ({
97 | login: 'dev',
98 | name: 'Developer',
99 | avatar: 'https://github.com/webpack.png',
100 | currencies: [
101 | {
102 | name: 'influence',
103 | displayName: 'Influence',
104 | description: 'Some **description**',
105 | value: totalCurrencies.influence,
106 | used: usedCurrencies.influence,
107 | remaining: totalCurrencies.influence - usedCurrencies.influence
108 | },
109 | {
110 | name: 'goldenInfluence',
111 | displayName: 'Golden Influence',
112 | description: 'Some **description**',
113 | value: totalCurrencies.goldenInfluence,
114 | used: usedCurrencies.goldenInfluence,
115 | remaining: totalCurrencies.goldenInfluence - usedCurrencies.goldenInfluence
116 | }
117 | ]
118 | }));
119 | }
120 | }
121 |
122 | export function getList(token, name = 'todo') {
123 | const loggedIn = token === 'developer';
124 | const listData = lists[name];
125 |
126 | return delay(500).then(() => ({
127 | name: name,
128 | displayName: 'Development Mode: ' + name,
129 | description: 'These items are simply for testing functionality...',
130 | lockable: true,
131 | deletable: true,
132 | archivable: true,
133 | isAdmin: true,
134 | possibleVotes: listData.possibleVotes,
135 | items: lists[name].items.map(item => {
136 | const votes = listData.possibleVotes.map(pv => ({
137 | name: pv.name,
138 | votes: (item[pv.name] || 0) + Math.floor(Math.random() * 100)
139 | }));
140 |
141 | const score = listData.possibleVotes.map((pv, i) => {
142 | return pv.score * votes[i].votes;
143 | }).reduce((a, b) => a + b, 0);
144 |
145 | return {
146 | id: item.id,
147 | list: item.list,
148 | title: item.title,
149 | locked: item.locked || false,
150 | archived: item.archived || false,
151 | description: item.description,
152 | votes,
153 | userVotes: loggedIn ? listData.possibleVotes.map(pv => ({
154 | name: pv.name,
155 | votes: item[pv.name] || 0
156 | })) : undefined,
157 | score
158 | };
159 | }).sort((a, b) => b.score - a.score)
160 | }));
161 | }
162 |
163 | export function createItem(token, list = 'todo', title, description) {
164 | if (token !== 'developer') {
165 | return Promise.reject(new Error('Not logged in as developer'));
166 |
167 | } else {
168 | let newItem = {
169 | id: Math.random() + '',
170 | list,
171 | title,
172 | description
173 | };
174 |
175 | allItems[newItem.id] = newItem;
176 | lists[list].items.push(newItem);
177 |
178 | return delay(500).then(() => ({
179 | ...newItem,
180 | score: 0,
181 | votes: lists[list].possibleVotes.map(pv => ({
182 | name: pv.name,
183 | votes: 0
184 | })),
185 | userVotes: lists[list].possibleVotes.map(pv => ({
186 | name: pv.name,
187 | votes: 0
188 | }))
189 | }));
190 | }
191 | }
192 |
193 | export function vote(token, itemId, voteName, value) {
194 | if (token !== 'developer') {
195 | return Promise.reject(
196 | new Error('Not logged in as developer')
197 | );
198 |
199 | } else {
200 | var listId = allItems[itemId].list,
201 | listData = lists[listId],
202 | pv = listData.possibleVotes.filter(pv => pv.name === voteName)[0];
203 |
204 | if (pv.currency) {
205 | usedCurrencies[pv.currency] += value;
206 | }
207 |
208 | allItems[itemId][voteName] = (allItems[itemId][voteName] || 0) + value;
209 |
210 | return delay(500).then(() => true);
211 | }
212 | }
213 |
214 | export function configItem(token, itemId, config) {
215 | var item = allItems[itemId];
216 |
217 | if (token !== 'developer') {
218 | return Promise.reject(
219 | new Error('Not logged in as developer')
220 | );
221 |
222 | } else {
223 | Object.assign(item, config);
224 |
225 | return delay(500).then(() => true);
226 | }
227 | }
228 |
--------------------------------------------------------------------------------
/src/components/wrapper/wrapper.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import UUID from 'uuid/v4';
3 | import 'whatwg-fetch';
4 | import Influence from 'Components/influence/influence';
5 | import Account from 'Components/account/account';
6 | import Topic from 'Components/topic/topic';
7 | import CreateTopic from 'Components/create-topic/create-topic';
8 | import * as api from 'Utils/js/api';
9 | import * as devapi from 'Utils/js/api.dev';
10 | import './wrapper-style';
11 |
12 | // Specify BEM block name
13 | const block = 'wrapper';
14 |
15 | export default class Wrapper extends React.Component {
16 | constructor(props) {
17 | super(props);
18 |
19 | this._api = props.development ? devapi : api;
20 | this._supportedBrowser = typeof localStorage === 'object';
21 |
22 | this.state = {
23 | list: 'todo',
24 | selfInfo: undefined,
25 | listInfo: undefined,
26 | isFetchingSelf: false,
27 | isFetchingList: false,
28 | isLoginActive: false,
29 | isVoting: 0,
30 | newTopics: []
31 | };
32 | }
33 |
34 | render() {
35 | let { isVoting, isFetchingList, isFetchingSelf, isLoginActive } = this.state,
36 | { selfInfo, listInfo, editItem, editItemTitle, editItemDescription } = this.state,
37 | maxVoteInfo = listInfo && listInfo.possibleVotes.map(() => 0);
38 |
39 | if ( !this._supportedBrowser ) {
40 | return Your browser is not supported.
;
41 | }
42 |
43 | if ( isLoginActive ) {
44 | return Logging in...
;
45 | }
46 |
47 | if ( listInfo ) listInfo.items.forEach(item => {
48 | if ( item.userVotes ) {
49 | maxVoteInfo.forEach((max, idx) => {
50 | let votes = item.userVotes[idx].votes;
51 |
52 | if (votes > max) maxVoteInfo[idx] = votes;
53 | });
54 | }
55 | });
56 |
57 | return (
58 |
59 |
60 |
61 | Vote and Prioritize
62 |
63 |
64 |
70 |
71 |
72 | This mini-application allows you to browse and vote on new features for webpack. Log in with
73 | your GitHub credentials and you will notice that you have a certain amount of points/influence
74 | that can be used to vote for or against any of the features listed below. The following two
75 | sections describe the different types of influence and how they can be attained.
76 |
77 |
78 |
81 |
84 |
85 |
86 | { listInfo && (
87 |
88 | { listInfo.items.map(topic => (
89 |
90 |
97 |
98 | ))}
99 |
100 | )}
101 |
102 | { listInfo && listInfo.isAdmin ? (
103 |
117 | ) : null }
118 |
119 | );
120 | }
121 |
122 | componentDidMount() {
123 | let { selfInfo, listInfo } = this.state;
124 |
125 | if ( !this._supportedBrowser ) return;
126 |
127 | if ( this._api.isLoginActive() ) {
128 | this.setState({
129 | isLoginActive: true
130 | });
131 |
132 | this._api.continueLogin().then(token => {
133 | window.localStorage.voteAppToken = token;
134 | });
135 |
136 | } else {
137 | if (!selfInfo) this._updateUser();
138 | if (!listInfo) this._updateList();
139 | }
140 | }
141 |
142 | /**
143 | * Fetch user information and available influence
144 | *
145 | */
146 | _updateUser() {
147 | let { voteAppToken } = localStorage;
148 |
149 | if (voteAppToken) {
150 | this.setState({
151 | isFetchingSelf: true
152 | });
153 |
154 | this._api
155 | .getSelf(voteAppToken)
156 | .then(result => {
157 | this.setState({ selfInfo: result });
158 | })
159 | .catch(error => {
160 | console.error('Failed to fetch user information: ', error);
161 | this.setState({ selfInfo: null });
162 | })
163 | .then(() => {
164 | this.setState({ isFetchingSelf: false });
165 | });
166 | }
167 | }
168 |
169 | /**
170 | * Fetch the list of voting topics
171 | *
172 | */
173 | _updateList() {
174 | let { list } = this.state,
175 | { voteAppToken } = localStorage;
176 |
177 | this.setState({
178 | isFetchingList: true
179 | });
180 |
181 | this._api
182 | .getList(voteAppToken, list)
183 | .then(result => {
184 | this.setState({ listInfo: result });
185 | })
186 | .catch(error => {
187 | console.error('Failed to fetch topic list: ', error);
188 | this.setState({ listInfo: null });
189 | })
190 | .then(() => {
191 | this.setState({ isFetchingList: false });
192 | });
193 | }
194 |
195 | /**
196 | * Add a new editable topic
197 | *
198 | */
199 | _addTopic() {
200 | this.setState({
201 | newTopics: [
202 | ...this.state.newTopics,
203 | UUID()
204 | ]
205 | });
206 | }
207 |
208 | /**
209 | * Create a new topic for voting
210 | *
211 | * @param {string} id - The temporary
212 | * @param {string} title - The topic title
213 | * @param {string} description - The topic description
214 | */
215 | _createTopic(id, title = '', description = '') {
216 | let { list, listInfo } = this.state,
217 | { voteAppToken } = localStorage;
218 |
219 | return this._api
220 | .createItem(voteAppToken, list, title, description)
221 | .then(item => {
222 | this.setState({
223 | newTopics: this.state.newTopics.filter(uuid => uuid !== id),
224 | listInfo: listInfo && {
225 | ...listInfo,
226 | items: [
227 | ...listInfo.items,
228 | item
229 | ]
230 | }
231 | });
232 |
233 | return true;
234 | });
235 | }
236 |
237 | /**
238 | * Change a topics settings (e.g. archive status)
239 | *
240 | * @param {number} id - The ID of the topic
241 | * @param {object} options - The new settings
242 | */
243 | _changeTopicSettings(id, options = {}) {
244 | let { voteAppToken } = localStorage;
245 |
246 | return this._api
247 | .configItem(voteAppToken, id, options)
248 | .then(() => this._updateList())
249 | .then(() => true);
250 | }
251 |
252 | /**
253 | * Refresh user information and topic list
254 | *
255 | */
256 | _refresh() {
257 | this._updateUser();
258 | this._updateList();
259 | }
260 |
261 | /**
262 | * Register new vote on a topic
263 | *
264 | * @param {number} id - ID of the topic being voted on
265 | * @param {string} influence - The name of influence used
266 | * @param {number} difference - The change in influence
267 | * @param {string} currency - The type of influence used
268 | * @param {number} score - Amount influence is worth (?)
269 | */
270 | _vote(id, influence, difference, currency, score) {
271 | let { voteAppToken } = localStorage;
272 |
273 | if ( !difference ) return;
274 |
275 | this._localVote(id, influence, difference, currency, score);
276 |
277 | this._api
278 | .vote(voteAppToken, id, influence, difference)
279 | .catch(e => {
280 | console.error(e);
281 |
282 | // Revert local vote
283 | this._localVote(id, influence, -difference, currency, score);
284 | })
285 | .then(() => {
286 | this.setState({
287 | isVoting: this.state.isVoting - 1
288 | });
289 | });
290 | }
291 |
292 | /**
293 | * Update local data to reflect new vote
294 | *
295 | * @param {number} id - ID of the topic being voted on
296 | * @param {string} influence - The name of influence used
297 | * @param {number} difference - The change in influence
298 | * @param {string} currency - The type of influence used
299 | * @param {number} score - Amount influence is worth (?)
300 | */
301 | _localVote(id, influence, difference, currency, score) {
302 | let { listInfo, selfInfo } = this.state;
303 |
304 | this.setState({
305 | isVoting: this.state.isVoting + 1,
306 | listInfo: listInfo && {
307 | ...listInfo,
308 | items: this._updateByProperty(listInfo.items, 'id', id, item => ({
309 | ...item,
310 | votes: this._updateByProperty(item.votes, 'name', influence, vote => ({
311 | ...vote,
312 | votes: vote.votes + difference
313 | })),
314 | userVotes: this._updateByProperty(item.userVotes, 'name', influence, vote => ({
315 | ...vote,
316 | votes: vote.votes + difference
317 | })),
318 | score: item.score + score * difference
319 | }))
320 | },
321 | selfInfo: selfInfo && {
322 | ...selfInfo,
323 | currencies: this._updateByProperty(selfInfo.currencies, 'name', currency, currency => ({
324 | ...currency,
325 | used: currency.used + difference,
326 | remaining: currency.remaining - difference
327 | }))
328 | }
329 | });
330 | }
331 |
332 | /**
333 | * Update an object within an array
334 | *
335 | * @param {array} array - The array containing the object to update
336 | * @param {string} property - A key used to find the target object
337 | * @param {any} value - A value used to find the target object
338 | * @param {function} update - A callback to update that object (takes object as parameter)
339 | * @return {array} - The modified array
340 | */
341 | _updateByProperty(array, property, value, update) {
342 | return array.map(item => {
343 | if (item[property] === value) {
344 | return update(item);
345 |
346 | } else return item;
347 | });
348 | }
349 | }
350 |
--------------------------------------------------------------------------------
/dist/vote.bundle.js:
--------------------------------------------------------------------------------
1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("react")):"function"==typeof define&&define.amd?define(["react"],t):"object"==typeof exports?exports.vote=t(require("react")):e.vote=t(e.React)}(window,(function(e){return function(e){var t={};function n(o){if(t[o])return t[o].exports;var r=t[o]={i:o,l:!1,exports:{}};return e[o].call(r.exports,r,r.exports,n),r.l=!0,r.exports}return n.m=e,n.c=t,n.d=function(e,t,o){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:o})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(n.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var r in e)n.d(o,r,function(t){return e[t]}.bind(null,r));return o},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/",n(n.s=5)}([function(t,n){t.exports=e},function(e,t,n){e.exports=n(20)()},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var o,r=n(0),i=(o=r)&&o.__esModule?o:{default:o};n(12);var a={small:10,large:16,huge:19};t.default=function(e){var t=e.type,n=e.size,o=void 0===n?"small":n;!function(e,t){var n={};for(var o in e)t.indexOf(o)>=0||Object.prototype.hasOwnProperty.call(e,o)&&(n[o]=e[o])}(e,["type","size"]);return i.default.createElement("svg",{className:"currency-icon",width:a[o],height:a[o],viewBox:"0 0 78 78"},i.default.createElement("g",{className:"currency-icon--"+t},i.default.createElement("polygon",{points:"5 19.6258065 38.4980127 0 73 19.6258065 73 57.3677419 38.5 78 5 57.3677419"})))}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var o,r=function(){function e(e,t){for(var n=0;n0&&void 0!==arguments[0]?arguments[0]:{},t=arguments[1],n=this.props.topic;this.props.onVote(n.id,e.name,t,e.currency,e.score)}},{key:"_edit",value:function(e){this.setState({editing:!0})}},{key:"_changeTitle",value:function(e){this.setState({title:e.target.value})}},{key:"_changeDescription",value:function(e){this.setState({description:e.target.value})}},{key:"_changeSettings",value:function(e){e.label;var t=function(e,t){var n={};for(var o in e)t.indexOf(o)>=0||Object.prototype.hasOwnProperty.call(e,o)&&(n[o]=e[o]);return n}(e,["label"]),n=this.props.topic;this.props.onChangeSettings(n.id,t)}},{key:"_saveChanges",value:function(e){var t=this,n=this.props.topic,o=this.state,r=o.title,i=o.description;this.props.onChangeSettings(n.id,{title:r,description:i}).then((function(e){e&&t.setState({editing:!1})}))}}]),t}(r.default.Component);t.default=c},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var o=Object.assign||function(e){for(var t=1;tt&&(h[n]=o)}))})),a.default.createElement("div",{className:v},a.default.createElement("div",{className:v+"__top"},a.default.createElement("h1",{className:v+"__title"},"Vote and Prioritize"),a.default.createElement(c.default,{loading:o||r||n,userData:d,possibleVotes:p&&p.possibleVotes,refresh:this._refresh.bind(this),startLogin:this._api.startLogin})),a.default.createElement("p",{className:v+"__description"},"This mini-application allows you to browse and vote on new features for webpack. Log in with your GitHub credentials and you will notice that you have a certain amount of points/influence that can be used to vote for or against any of the features listed below. The following two sections describe the different types of influence and how they can be attained."),a.default.createElement("div",{className:v+"__influences"},a.default.createElement(u.default,{className:v+"__influence-section",type:"normal"}),a.default.createElement(u.default,{className:v+"__influence-section",type:"golden"})),p&&a.default.createElement("ul",{className:v+"__topics"},p.items.map((function(t){return a.default.createElement("li",{key:t.id},a.default.createElement(l.default,{user:d,admin:p.isAdmin,topic:t,votes:p.possibleVotes,onVote:e._vote.bind(e),onChangeSettings:e._changeTopicSettings.bind(e)}))}))),p&&p.isAdmin?a.default.createElement("div",{className:v+"__new"},this.state.newTopics.map((function(t){return a.default.createElement(f.default,{key:t,id:t,onCreate:e._createTopic.bind(e)})})),a.default.createElement("a",{className:v+"__add",onClick:this._addTopic.bind(this)},"Add a new topic...")):null)):a.default.createElement("div",null,"Your browser is not supported.")}},{key:"componentDidMount",value:function(){var e=this.state,t=e.selfInfo,n=e.listInfo;this._supportedBrowser&&(this._api.isLoginActive()?(this.setState({isLoginActive:!0}),this._api.continueLogin().then((function(e){window.localStorage.voteAppToken=e}))):(t||this._updateUser(),n||this._updateList()))}},{key:"_updateUser",value:function(){var e=this,t=localStorage.voteAppToken;t&&(this.setState({isFetchingSelf:!0}),this._api.getSelf(t).then((function(t){e.setState({selfInfo:t})})).catch((function(t){console.error("Failed to fetch user information: ",t),e.setState({selfInfo:null})})).then((function(){e.setState({isFetchingSelf:!1})})))}},{key:"_updateList",value:function(){var e=this,t=this.state.list,n=localStorage.voteAppToken;this.setState({isFetchingList:!0}),this._api.getList(n,t).then((function(t){e.setState({listInfo:t})})).catch((function(t){console.error("Failed to fetch topic list: ",t),e.setState({listInfo:null})})).then((function(){e.setState({isFetchingList:!1})}))}},{key:"_addTopic",value:function(){this.setState({newTopics:[].concat(y(this.state.newTopics),[(0,s.default)()])})}},{key:"_createTopic",value:function(e){var t=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"",i=this.state,a=i.list,s=i.listInfo,u=localStorage,c=u.voteAppToken;return this._api.createItem(c,a,n,r).then((function(n){return t.setState({newTopics:t.state.newTopics.filter((function(t){return t!==e})),listInfo:s&&o({},s,{items:[].concat(y(s.items),[n])})}),!0}))}},{key:"_changeTopicSettings",value:function(e){var t=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},o=localStorage,r=o.voteAppToken;return this._api.configItem(r,e,n).then((function(){return t._updateList()})).then((function(){return!0}))}},{key:"_refresh",value:function(){this._updateUser(),this._updateList()}},{key:"_vote",value:function(e,t,n,o,r){var i=this,a=localStorage.voteAppToken;n&&(this._localVote(e,t,n,o,r),this._api.vote(a,e,t,n).catch((function(a){console.error(a),i._localVote(e,t,-n,o,r)})).then((function(){i.setState({isVoting:i.state.isVoting-1})})))}},{key:"_localVote",value:function(e,t,n,r,i){var a=this,s=this.state,u=s.listInfo,c=s.selfInfo;this.setState({isVoting:this.state.isVoting+1,listInfo:u&&o({},u,{items:this._updateByProperty(u.items,"id",e,(function(e){return o({},e,{votes:a._updateByProperty(e.votes,"name",t,(function(e){return o({},e,{votes:e.votes+n})})),userVotes:a._updateByProperty(e.userVotes,"name",t,(function(e){return o({},e,{votes:e.votes+n})})),score:e.score+i*n})}))}),selfInfo:c&&o({},c,{currencies:this._updateByProperty(c.currencies,"name",r,(function(e){return o({},e,{used:e.used+n,remaining:e.remaining-n})}))})})}},{key:"_updateByProperty",value:function(e,t,n,o){return e.map((function(e){return e[t]===n?o(e):e}))}}]),t}(a.default.Component);t.default=b},function(e,t,n){var o=n(7),r=n(9);e.exports=function(e,t,n){var i=t&&n||0;"string"==typeof e&&(t="binary"==e?new Array(16):null,e=null);var a=(e=e||{}).random||(e.rng||o)();if(a[6]=15&a[6]|64,a[8]=63&a[8]|128,t)for(var s=0;s<16;++s)t[i+s]=a[s];return t||r(a)}},function(e,t,n){(function(t){var n,o=t.crypto||t.msCrypto;if(o&&o.getRandomValues){var r=new Uint8Array(16);n=function(){return o.getRandomValues(r),r}}if(!n){var i=new Array(16);n=function(){for(var e,t=0;t<16;t++)0==(3&t)&&(e=4294967296*Math.random()),i[t]=e>>>((3&t)<<3)&255;return i}}e.exports=n}).call(this,n(8))},function(e,t){var n;n=function(){return this}();try{n=n||new Function("return this")()}catch(e){"object"==typeof window&&(n=window)}e.exports=n},function(e,t){for(var n=[],o=0;o<256;++o)n[o]=(o+256).toString(16).substr(1);e.exports=function(e,t){var o=t||0,r=n;return r[e[o++]]+r[e[o++]]+r[e[o++]]+r[e[o++]]+"-"+r[e[o++]]+r[e[o++]]+"-"+r[e[o++]]+r[e[o++]]+"-"+r[e[o++]]+r[e[o++]]+"-"+r[e[o++]]+r[e[o++]]+r[e[o++]]+r[e[o++]]+r[e[o++]]+r[e[o++]]}},function(e,t){!function(e){"use strict";if(!e.fetch){var t="URLSearchParams"in e,n="Symbol"in e&&"iterator"in Symbol,o="FileReader"in e&&"Blob"in e&&function(){try{return new Blob,!0}catch(e){return!1}}(),r="FormData"in e,i="ArrayBuffer"in e;if(i)var a=["[object Int8Array]","[object Uint8Array]","[object Uint8ClampedArray]","[object Int16Array]","[object Uint16Array]","[object Int32Array]","[object Uint32Array]","[object Float32Array]","[object Float64Array]"],s=function(e){return e&&DataView.prototype.isPrototypeOf(e)},u=ArrayBuffer.isView||function(e){return e&&a.indexOf(Object.prototype.toString.call(e))>-1};h.prototype.append=function(e,t){e=f(e),t=d(t);var n=this.map[e];this.map[e]=n?n+","+t:t},h.prototype.delete=function(e){delete this.map[f(e)]},h.prototype.get=function(e){return e=f(e),this.has(e)?this.map[e]:null},h.prototype.has=function(e){return this.map.hasOwnProperty(f(e))},h.prototype.set=function(e,t){this.map[f(e)]=d(t)},h.prototype.forEach=function(e,t){for(var n in this.map)this.map.hasOwnProperty(n)&&e.call(t,this.map[n],n,this)},h.prototype.keys=function(){var e=[];return this.forEach((function(t,n){e.push(n)})),p(e)},h.prototype.values=function(){var e=[];return this.forEach((function(t){e.push(t)})),p(e)},h.prototype.entries=function(){var e=[];return this.forEach((function(t,n){e.push([n,t])})),p(e)},n&&(h.prototype[Symbol.iterator]=h.prototype.entries);var c=["DELETE","GET","HEAD","OPTIONS","POST","PUT"];_.prototype.clone=function(){return new _(this,{body:this._bodyInit})},g.call(_.prototype),g.call(E.prototype),E.prototype.clone=function(){return new E(this._bodyInit,{status:this.status,statusText:this.statusText,headers:new h(this.headers),url:this.url})},E.error=function(){var e=new E(null,{status:0,statusText:""});return e.type="error",e};var l=[301,302,303,307,308];E.redirect=function(e,t){if(-1===l.indexOf(t))throw new RangeError("Invalid status code");return new E(null,{status:t,headers:{location:e}})},e.Headers=h,e.Request=_,e.Response=E,e.fetch=function(e,t){return new Promise((function(n,r){var i=new _(e,t),a=new XMLHttpRequest;a.onload=function(){var e,t,o={status:a.status,statusText:a.statusText,headers:(e=a.getAllResponseHeaders()||"",t=new h,e.split(/\r?\n/).forEach((function(e){var n=e.split(":"),o=n.shift().trim();if(o){var r=n.join(":").trim();t.append(o,r)}})),t)};o.url="responseURL"in a?a.responseURL:o.headers.get("X-Request-URL");var r="response"in a?a.response:a.responseText;n(new E(r,o))},a.onerror=function(){r(new TypeError("Network request failed"))},a.ontimeout=function(){r(new TypeError("Network request failed"))},a.open(i.method,i.url,!0),"include"===i.credentials&&(a.withCredentials=!0),"responseType"in a&&o&&(a.responseType="blob"),i.headers.forEach((function(e,t){a.setRequestHeader(t,e)})),a.send(void 0===i._bodyInit?null:i._bodyInit)}))},e.fetch.polyfill=!0}function f(e){if("string"!=typeof e&&(e=String(e)),/[^a-z0-9\-#$%&'*+.\^_`|~]/i.test(e))throw new TypeError("Invalid character in header field name");return e.toLowerCase()}function d(e){return"string"!=typeof e&&(e=String(e)),e}function p(e){var t={next:function(){var t=e.shift();return{done:void 0===t,value:t}}};return n&&(t[Symbol.iterator]=function(){return t}),t}function h(e){this.map={},e instanceof h?e.forEach((function(e,t){this.append(t,e)}),this):Array.isArray(e)?e.forEach((function(e){this.append(e[0],e[1])}),this):e&&Object.getOwnPropertyNames(e).forEach((function(t){this.append(t,e[t])}),this)}function m(e){if(e.bodyUsed)return Promise.reject(new TypeError("Already read"));e.bodyUsed=!0}function y(e){return new Promise((function(t,n){e.onload=function(){t(e.result)},e.onerror=function(){n(e.error)}}))}function v(e){var t=new FileReader,n=y(t);return t.readAsArrayBuffer(e),n}function b(e){if(e.slice)return e.slice(0);var t=new Uint8Array(e.byteLength);return t.set(new Uint8Array(e)),t.buffer}function g(){return this.bodyUsed=!1,this._initBody=function(e){if(this._bodyInit=e,e)if("string"==typeof e)this._bodyText=e;else if(o&&Blob.prototype.isPrototypeOf(e))this._bodyBlob=e;else if(r&&FormData.prototype.isPrototypeOf(e))this._bodyFormData=e;else if(t&&URLSearchParams.prototype.isPrototypeOf(e))this._bodyText=e.toString();else if(i&&o&&s(e))this._bodyArrayBuffer=b(e.buffer),this._bodyInit=new Blob([this._bodyArrayBuffer]);else{if(!i||!ArrayBuffer.prototype.isPrototypeOf(e)&&!u(e))throw new Error("unsupported BodyInit type");this._bodyArrayBuffer=b(e)}else this._bodyText="";this.headers.get("content-type")||("string"==typeof e?this.headers.set("content-type","text/plain;charset=UTF-8"):this._bodyBlob&&this._bodyBlob.type?this.headers.set("content-type",this._bodyBlob.type):t&&URLSearchParams.prototype.isPrototypeOf(e)&&this.headers.set("content-type","application/x-www-form-urlencoded;charset=UTF-8"))},o&&(this.blob=function(){var e=m(this);if(e)return e;if(this._bodyBlob)return Promise.resolve(this._bodyBlob);if(this._bodyArrayBuffer)return Promise.resolve(new Blob([this._bodyArrayBuffer]));if(this._bodyFormData)throw new Error("could not read FormData body as blob");return Promise.resolve(new Blob([this._bodyText]))},this.arrayBuffer=function(){return this._bodyArrayBuffer?m(this)||Promise.resolve(this._bodyArrayBuffer):this.blob().then(v)}),this.text=function(){var e,t,n,o=m(this);if(o)return o;if(this._bodyBlob)return e=this._bodyBlob,t=new FileReader,n=y(t),t.readAsText(e),n;if(this._bodyArrayBuffer)return Promise.resolve(function(e){for(var t=new Uint8Array(e),n=new Array(t.length),o=0;o-1?o:n),this.mode=t.mode||this.mode||null,this.referrer=null,("GET"===this.method||"HEAD"===this.method)&&r)throw new TypeError("Body not allowed for GET or HEAD requests");this._initBody(r)}function w(e){var t=new FormData;return e.trim().split("&").forEach((function(e){if(e){var n=e.split("="),o=n.shift().replace(/\+/g," "),r=n.join("=").replace(/\+/g," ");t.append(decodeURIComponent(o),decodeURIComponent(r))}})),t}function E(e,t){t||(t={}),this.type="default",this.status="status"in t?t.status:200,this.ok=this.status>=200&&this.status<300,this.statusText="statusText"in t?t.statusText:"OK",this.headers=new h(t.headers),this.url=t.url||"",this._initBody(e)}}("undefined"!=typeof self?self:this)},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var o=i(n(0)),r=i(n(2));function i(e){return e&&e.__esModule?e:{default:e}}n(13);var a="influence";t.default=function(e){return"normal"===e.type?o.default.createElement("section",{className:a},o.default.createElement("h2",{className:a+"__header"},"Influence ",o.default.createElement(r.default,{size:"huge",type:"influence"})),o.default.createElement("p",{className:a+"__description"},o.default.createElement("em",null,"Influence")," is a unit of measure based on time you have been a member on GitHub. However, from 2017 on you will receive one influence per day.")):o.default.createElement("section",{className:a},o.default.createElement("h2",{className:a+"__header"},"Golden Influence ",o.default.createElement(r.default,{size:"huge",type:"goldenInfluence"})),o.default.createElement("p",{className:a+"__description"},o.default.createElement("em",null,"Golden Influence")," is equal to 100 ",o.default.createElement("i",null,"normal influence"),". Golden Influence is obtained by being a backer or sponsor on our ",o.default.createElement("a",{href:"https://opencollective.com/webpack"},"Open Collective page"),"."))}},function(e,t,n){},function(e,t,n){},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var o=function(){function e(e,t){for(var n=0;n=0||(r[n]=e[n]);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(o=0;o=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}(e,["minRows","maxRows","onHeightChange","useCacheForDOMMeasurements","inputRef"]));return t.style=s({},t.style,{height:this.state.height}),Math.max(t.style.maxHeight||1/0,this.state.maxHeight)0&&void 0!==arguments[0]?arguments[0]:"",t=""+Math.random();e.includes("webpack.js.org")?(window.localStorage.githubState=t,window.location="https://github.com/login/oauth/authorize?client_id=4d355e2799cb8926c665&scope=user:email&state="+t+"&allow_signup=false&redirect_uri="+encodeURIComponent(e)):alert("You can't login with GitHub OAuth on localhost. Please pass the `development` prop to the `Wrapper` in order to use `api.dev`.");return Promise.resolve()},t.continueLogin=function(){var e=/^\?code=([^&]*)&state=([^&]*)/.exec(window.location.search);if(e)return(t=e[1],n=e[2],n!==window.localStorage.githubState?Promise.reject(new Error("Request state doesn't match (Login was triggered by 3rd party)")):(delete window.localStorage.githubState,fetch(o+"/login",{headers:{"Content-Type":"application/json"},method:"POST",body:JSON.stringify({code:t,state:n})}).then((function(e){return e.json()})).then(r).then((function(e){if(!e.token)throw new Error("No token received from API");return e.token})))).then((function(e){return setTimeout((function(){var e=window.location.href;window.location=e.substr(0,e.length-window.location.search.length)}),100),e}));var t,n;return Promise.resolve()},t.getSelf=function(e){return fetch(o+"/self?token="+e,{mode:"cors"}).then((function(e){return e.json()})).then(r)},t.getList=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"todo";return fetch(o+"/list/"+t+(e?"?token="+e:""),{mode:"cors"}).then((function(e){return e.json()})).then(r)},t.createItem=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"todo",n=arguments[2],i=arguments[3];return fetch(o+"/list/"+t+"?token="+e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({title:n,description:i})}).then((function(e){return e.json()})).then(r)},t.vote=function(e,t,n,i){return fetch(o+"/vote/"+t+"/"+n+"?token="+e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({count:i})}).then((function(e){return e.json()})).then(r).then((function(e){return!0}))},t.configItem=function(e,t,n){return fetch(o+"/config/"+t+"?token="+e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({config:n})}).then((function(e){return e.json()})).then(r).then((function(e){return!0}))};var o="https://oswils44oj.execute-api.us-east-1.amazonaws.com/production/";function r(e){if(!e)throw new Error("No result received");if(e.errorMessage)throw new Error(e.errorMessage);return e}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var o=Object.assign||function(e){for(var t=1;t1&&void 0!==arguments[1]?arguments[1]:"todo",n="developer"===e,o=s[t];return c(500).then((function(){return{name:t,displayName:"Development Mode: "+t,description:"These items are simply for testing functionality...",lockable:!0,deletable:!0,archivable:!0,isAdmin:!0,possibleVotes:o.possibleVotes,items:s[t].items.map((function(e){var t=o.possibleVotes.map((function(t){return{name:t.name,votes:(e[t.name]||0)+Math.floor(100*Math.random())}})),r=o.possibleVotes.map((function(e,n){return e.score*t[n].votes})).reduce((function(e,t){return e+t}),0);return{id:e.id,list:e.list,title:e.title,locked:e.locked||!1,archived:e.archived||!1,description:e.description,votes:t,userVotes:n?o.possibleVotes.map((function(t){return{name:t.name,votes:e[t.name]||0}})):void 0,score:r}})).sort((function(e,t){return t.score-e.score}))}}))},t.createItem=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"todo",n=arguments[2],r=arguments[3];if("developer"!==e)return Promise.reject(new Error("Not logged in as developer"));var i={id:Math.random()+"",list:t,title:n,description:r};return u[i.id]=i,s[t].items.push(i),c(500).then((function(){return o({},i,{score:0,votes:s[t].possibleVotes.map((function(e){return{name:e.name,votes:0}})),userVotes:s[t].possibleVotes.map((function(e){return{name:e.name,votes:0}}))})}))},t.vote=function(e,t,n,o){if("developer"!==e)return Promise.reject(new Error("Not logged in as developer"));var i=u[t].list,a=s[i].possibleVotes.filter((function(e){return e.name===n}))[0];return a.currency&&(r[a.currency]+=o),u[t][n]=(u[t][n]||0)+o,c(500).then((function(){return!0}))},t.configItem=function(e,t,n){var o=u[t];return"developer"!==e?Promise.reject(new Error("Not logged in as developer")):(Object.assign(o,n),c(500).then((function(){return!0})))};var r={influence:100,goldenInfluence:100},i=1e3,a=300,s={todo:{possibleVotes:[{name:"influence",currency:"influence",score:1,color:"blue"},{name:"golden",currency:"goldenInfluence",score:100,color:"#bfa203"}],items:[{id:"1234",list:"todo",title:"Some Feature",description:"Mauris et sem a risus pharetra suscipit. Fusce gravida purus non nisi pulvinar, non lobortis tortor vehicula. Mauris at dui a ex vestibulum condimentum id sit amet nisl. Nam finibus ornare laoreet. Duis ultrices sollicitudin quam eu vulputate. Sed ac ante odio. Mauris fermentum vel tortor sit amet iaculis.",influence:15},{id:"2345",list:"todo",title:"Review Stuff",description:"Cras libero libero, elementum eu laoreet nec, convallis sit amet sem. Donec ut diam dignissim, hendrerit ante id, congue felis. In hac habitasse platea dictumst. Donec sit amet tellus et neque auctor consequat. Cras porta finibus turpis, id auctor lectus.",golden:20},{id:"3456",list:"todo",title:"Another Feature",description:"Curabitur pharetra facilisis mauris. Integer interdum venenatis metus quis dictum. Cras aliquet erat ut risus vestibulum, sed tincidunt enim maximus. Cras tincidunt vulputate ante vitae tincidunt. Cras quis erat eu augue aliquam pretium nec sit amet magna. Etiam nisi nunc, blandit vel hendrerit et, suscipit finibus nunc.",golden:20}]}},u={1234:s.todo.items[0],2345:s.todo.items[1],3456:s.todo.items[2]};function c(e){return new Promise((function(t){setTimeout(t,e)}))}},function(e,t,n){}])}));
--------------------------------------------------------------------------------