├── .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 | 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 | 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 |