├── public ├── _redirects ├── favicon.ico ├── robots.txt └── index.html ├── src ├── components │ └── App.js ├── utils │ ├── helpers.js │ ├── api.js │ └── _DATA.js ├── index.js └── index.css ├── .gitignore ├── package.json └── README.md /public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uidotdev/redux-course-2/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | export default function App() { 4 | return ( 5 |
6 | Redux Polls 7 |
8 | ) 9 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /src/utils/helpers.js: -------------------------------------------------------------------------------- 1 | export function isObject (item) { 2 | return Object.prototype.toString.call(item) === '[object Object]' 3 | } 4 | 5 | export function getPercentage (count, total) { 6 | return total === 0 ? 0 : parseInt(count / total * 100, 10) 7 | } 8 | 9 | export function getTextKeys () { 10 | return ['aText', 'bText', 'cText', 'dText'] 11 | } 12 | 13 | export function getVoteKeys () { 14 | return ['aVotes', 'bVotes', 'cVotes', 'dVotes'] 15 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as ReactDOM from 'react-dom' 3 | import App from './components/App' 4 | import './index.css' 5 | 6 | function ColorfulBorder() { 7 | return ( 8 | 9 | 16 | 17 | ) 18 | } 19 | 20 | ReactDOM.render( 21 | 22 | 23 | 24 | , 25 | document.getElementById('root') 26 | ) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "polls", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.3.2", 8 | "@testing-library/user-event": "^7.1.2", 9 | "react": "^16.13.1", 10 | "react-dom": "^16.13.1", 11 | "react-scripts": "3.4.1" 12 | }, 13 | "scripts": { 14 | "start": "react-scripts start", 15 | "build": "react-scripts build", 16 | "test": "react-scripts test", 17 | "eject": "react-scripts eject" 18 | }, 19 | "eslintConfig": { 20 | "extends": "react-app" 21 | }, 22 | "browserslist": { 23 | "production": [ 24 | ">0.2%", 25 | "not dead", 26 | "not op_mini all" 27 | ], 28 | "development": [ 29 | "last 1 chrome version", 30 | "last 1 firefox version", 31 | "last 1 safari version" 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | Redux Polls 13 | 14 | 15 | 16 |
17 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/utils/api.js: -------------------------------------------------------------------------------- 1 | import { 2 | _getUsers, 3 | _getPolls, 4 | _savePoll, 5 | _savePollAnswer 6 | } from './_DATA.js' 7 | import { isObject } from './helpers' 8 | 9 | function flattenPoll (poll) { 10 | return Object.keys(poll) 11 | .reduce((flattenedPoll, key) => { 12 | const val = poll[key] 13 | 14 | if (isObject(val)) { 15 | flattenedPoll[key + 'Text'] = val.text 16 | flattenedPoll[key + 'Votes'] = val.votes 17 | return flattenedPoll 18 | } 19 | 20 | flattenedPoll[key] = val 21 | return flattenedPoll 22 | }, {}) 23 | } 24 | 25 | function formatPolls (polls) { 26 | const pollIds = Object.keys(polls) 27 | 28 | return pollIds.reduce((formattedPolls, id) => { 29 | formattedPolls[id] = flattenPoll(polls[id]) 30 | return formattedPolls 31 | }, {}) 32 | } 33 | 34 | function formatUsers (users) { 35 | return Object.keys(users) 36 | .reduce((formattedUsers, id) => { 37 | const user = users[id] 38 | 39 | formattedUsers[id] = { 40 | ...user, 41 | answers: Object.keys(user.answers) 42 | } 43 | 44 | return formattedUsers 45 | }, {}) 46 | } 47 | 48 | export function getInitialData () { 49 | return Promise.all([ 50 | _getUsers(), 51 | _getPolls(), 52 | ]).then(([users, polls]) => ({ 53 | users: formatUsers(users), 54 | polls: formatPolls(polls), 55 | })) 56 | } 57 | 58 | export function savePoll (poll) { 59 | return _savePoll(poll) 60 | .then((p) => flattenPoll(p)) 61 | } 62 | 63 | export function savePollAnswer (args) { 64 | return _savePollAnswer(args) 65 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | UI.dev Logo 6 | 7 |
8 |

9 | 10 |

Redux Course Project #2 - Polls

11 | 12 | ### Info 13 | 14 | This is the repository for [UI.dev](https://ui.dev)'s "Redux #2" course project. 15 | 16 | For more information on the course, visit __[ui.dev/redux](https://ui.dev/redux/)__. 17 | 18 | ### Project 19 | 20 | This project is a polls app. 21 | 22 | You can view the final project __[here](https://ui-polls.netlify.app/)__ 23 | 24 | ### Branches 25 | 26 | Every `(Project)` video in the course coincides with a branch in this repo. If you want to compare your code with Tyler's or you just want to play around with the code, check out the different branches. 27 | 28 | Below every `(Project)` video in the course will be a direct link to both the commit for that video as well as its branch. 29 | 30 | 31 | 32 | ### Project Preview 33 | 34 | ![](https://user-images.githubusercontent.com/2933430/81010664-d78baf00-8e13-11ea-855e-359382ee9be9.png) 35 | 36 | ![](https://user-images.githubusercontent.com/2933430/81010657-d5c1eb80-8e13-11ea-9722-970250dcddec.png) 37 | 38 | ![](https://user-images.githubusercontent.com/2933430/81010652-d2c6fb00-8e13-11ea-8fa2-188b87f15db0.png) 39 | 40 | ![](https://user-images.githubusercontent.com/2933430/81010661-d65a8200-8e13-11ea-8db2-4571602b9fa1.png) -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import url("https://use.typekit.net/wrw1rqc.css"); 2 | 3 | :root { 4 | --black: #000; 5 | --white: #fff; 6 | --red: #f32827; 7 | --purple: #a42ce9; 8 | --blue: #2d7fea; 9 | --yellow: #f4f73e; 10 | --pink: #eb30c1; 11 | --gold: #ffd500; 12 | --aqua: #2febd2; 13 | --gray: #282c35; 14 | } 15 | 16 | *, 17 | *:before, 18 | *:after { 19 | box-sizing: inherit; 20 | } 21 | 22 | html { 23 | font-family: proxima-nova, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen-Sans, Ubuntu, Cantarell, Helvetica Neue, sans-serif; 24 | text-rendering: optimizeLegibility; 25 | -webkit-font-smoothing: antialiased; 26 | -moz-osx-font-smoothing: grayscale; 27 | box-sizing: border-box; 28 | font-size: 18px; 29 | } 30 | 31 | body { 32 | margin: 0; 33 | padding: 0; 34 | min-height: 100vh; 35 | background: var(--black); 36 | color: var(--white); 37 | } 38 | 39 | a { 40 | color: var(--gold); 41 | text-decoration: none; 42 | } 43 | 44 | li { 45 | list-style-type: none; 46 | } 47 | 48 | .border-container { 49 | padding: 0; 50 | margin: 0; 51 | display: flex; 52 | } 53 | 54 | .border-item { 55 | width: 20vw; 56 | height: 12px; 57 | } 58 | 59 | .container { 60 | position: relative; 61 | max-width: 1000px; 62 | min-width: 280px; 63 | margin: 0 auto; 64 | padding: 20px; 65 | } 66 | 67 | .loading { 68 | margin: 0 auto; 69 | } 70 | 71 | .option { 72 | border: 1px solid var(--white); 73 | margin: 10px; 74 | border-radius: 2px; 75 | } 76 | 77 | .option:hover { 78 | border: 2px solid var(--pink); 79 | cursor: pointer; 80 | } 81 | 82 | .chosen { 83 | border: 2px solid var(--aqua); 84 | } 85 | 86 | .result { 87 | display: flex; 88 | justify-content: space-between; 89 | } 90 | 91 | .poll-author { 92 | display: flex; 93 | justify-content: center; 94 | align-items: center; 95 | font-weight: bold; 96 | text-transform: uppercase; 97 | } 98 | 99 | .poll-author img { 100 | width: 40px; 101 | height: 40px; 102 | border-radius: 20px; 103 | margin: 0 10px; 104 | } 105 | 106 | .btn { 107 | text-transform: uppercase; 108 | margin: 35px auto; 109 | padding: 10px; 110 | border: 1px solid rgba(0,0,0,.29); 111 | cursor: pointer; 112 | background: #fff; 113 | font-size: 16px; 114 | width: 250px; 115 | position: relative; 116 | } 117 | 118 | .btn:hover { 119 | border-color: rgba(0,0,0,.5); 120 | text-decoration: none; 121 | } 122 | 123 | .btn:focus { 124 | outline: 0; 125 | font-weight: 700; 126 | border-width: 2px; 127 | } 128 | 129 | .center { 130 | text-align: center; 131 | } 132 | 133 | .active { 134 | font-weight: 900; 135 | } 136 | 137 | .nav { 138 | margin: 30px 0; 139 | } 140 | 141 | .nav a { 142 | margin: 5px 10px; 143 | } 144 | 145 | .nav:first-child { 146 | padding-left: 0; 147 | } 148 | 149 | ul { 150 | padding-left: 0; 151 | } 152 | 153 | li { 154 | list-style-type: none; 155 | padding: 10px; 156 | text-decoration: none; 157 | } 158 | 159 | .dashboard-toggle { 160 | display: flex; 161 | justify-content: center; 162 | align-items: center; 163 | font-size: 22px; 164 | margin-bottom: 30px; 165 | } 166 | 167 | .dashboard-toggle button { 168 | border: none; 169 | background: transparent; 170 | font-size: 22px; 171 | color: var(--gold); 172 | } 173 | 174 | .dashboard-list { 175 | font-size: 22px; 176 | letter-spacing: .4px; 177 | font-weight: 700; 178 | } 179 | 180 | .dashboard-list li { 181 | margin: 10px 0; 182 | } 183 | 184 | .dashboard-list a { 185 | color: var(--white); 186 | } 187 | 188 | .dashboard-list a:hover { 189 | font-weight: 900; 190 | } 191 | 192 | .add-poll-container { 193 | display: flex; 194 | flex-direction: column; 195 | align-items: center; 196 | } 197 | 198 | .poll-container { 199 | width: 60%; 200 | margin: 0px auto; 201 | display: flex; 202 | flex-direction: column; 203 | } 204 | 205 | .question { 206 | font-size: 30px; 207 | text-align: center; 208 | } 209 | 210 | .avatar { 211 | width: 40px; 212 | height: 40px; 213 | border-radius: 20px; 214 | margin-top: 10px; 215 | } 216 | 217 | .user { 218 | display: flex; 219 | } 220 | 221 | .user h1 { 222 | margin: 0; 223 | font-size: 30px; 224 | } 225 | 226 | .user img { 227 | width: 40px; 228 | height: 40px; 229 | border-radius: 20px; 230 | margin-right: 20px; 231 | } 232 | 233 | .user p { 234 | margin-top: 6px; 235 | margin-bottom: 6px; 236 | } 237 | 238 | @media (max-width: 500px) { 239 | .poll-container { 240 | width: 95%; 241 | flex-direction: column; 242 | } 243 | } 244 | 245 | .add-form { 246 | margin: 0 auto; 247 | width: 600px; 248 | } 249 | 250 | input { 251 | display: block; 252 | box-sizing: border-box; 253 | width: 100%; 254 | outline: none; 255 | border: none; 256 | border-radius: 0; 257 | appearance: none; 258 | margin-bottom: 20px; 259 | } 260 | 261 | .btn { 262 | max-width: 240px; 263 | min-width: 150px; 264 | padding: 10px; 265 | border: 2px solid black; 266 | color: var(--white); 267 | text-align: center; 268 | background: var(--pink); 269 | font-weight: 900; 270 | font-size: 15px; 271 | } 272 | 273 | .btn:hover { 274 | cursor: pointer; 275 | } 276 | 277 | .label { 278 | display: block; 279 | margin-bottom: 0.25em; 280 | font-size: 20px; 281 | } 282 | 283 | .input { 284 | padding: 5px; 285 | border-width: 1px; 286 | border-style: solid; 287 | border-color: lightgray; 288 | background-color: white; 289 | font-size: 18px; 290 | } 291 | .input:focus { 292 | border-color: gray; 293 | } 294 | -------------------------------------------------------------------------------- /src/utils/_DATA.js: -------------------------------------------------------------------------------- 1 | let users = { 2 | sarah_edo: { 3 | id: 'sarah_edo', 4 | name: 'Sarah Drasner', 5 | avatarURL: 'https://tylermcginnis.com/would-you-rather/sarah.jpg', 6 | answers: { 7 | "8xf0y6ziyjabvozdd253nd": 'a', 8 | "6ni6ok3ym7mf1p33lnez": 'a', 9 | "am8ehyc8byjqgar0jgpub9": 'b', 10 | "loxhs1bqm25b708cmbf3g": 'd' 11 | }, 12 | polls: ['8xf0y6ziyjabvozdd253nd', 'am8ehyc8byjqgar0jgpub9'] 13 | }, 14 | tylermcginnis: { 15 | id: 'tylermcginnis', 16 | name: 'Tyler McGinnis', 17 | avatarURL: 'https://tylermcginnis.com/would-you-rather/tyler.jpg', 18 | answers: { 19 | "vthrdm985a262al8qx3do": 'a', 20 | "xj352vofupe1dqz9emx13r": 'a', 21 | }, 22 | polls: ['loxhs1bqm25b708cmbf3g', 'vthrdm985a262al8qx3do'], 23 | }, 24 | dan_abramov: { 25 | id: 'dan_abramov', 26 | name: 'Dan Abramov', 27 | avatarURL: 'https://tylermcginnis.com/would-you-rather/dan.jpg', 28 | answers: { 29 | "xj352vofupe1dqz9emx13r": 'a', 30 | "vthrdm985a262al8qx3do": 'd', 31 | "6ni6ok3ym7mf1p33lnez": 'd' 32 | }, 33 | polls: ['6ni6ok3ym7mf1p33lnez', 'xj352vofupe1dqz9emx13r'], 34 | } 35 | } 36 | 37 | let polls = { 38 | "8xf0y6ziyjabvozdd253nd": { 39 | id: '8xf0y6ziyjabvozdd253nd', 40 | question: "Who is the best basketball player to ever live?", 41 | author: 'sarah_edo', 42 | timestamp: 1467166872634, 43 | a: { 44 | text: 'Michael Jordan', 45 | votes: ['sarah_edo'], 46 | }, 47 | b: { 48 | text: 'Jimmer Fredette', 49 | votes: [], 50 | }, 51 | c: { 52 | text: 'Lebron James', 53 | votes: [], 54 | }, 55 | d: { 56 | text: 'Kobe Bryant', 57 | votes: [], 58 | } 59 | }, 60 | "6ni6ok3ym7mf1p33lnez": { 61 | id: '6ni6ok3ym7mf1p33lnez', 62 | question: "How will we build UIs in 2019?", 63 | author: 'dan_abramov', 64 | timestamp: 1468479767190, 65 | a: { 66 | text: 'React.js', 67 | votes: ['sarah_edo'], 68 | }, 69 | b: { 70 | text: 'ReasonML', 71 | votes: [], 72 | }, 73 | c: { 74 | text: 'Vue.js', 75 | votes: [], 76 | }, 77 | d: { 78 | text: 'Angular.js', 79 | votes: ['dan_abramov'], 80 | } 81 | }, 82 | "am8ehyc8byjqgar0jgpub9": { 83 | id: 'am8ehyc8byjqgar0jgpub9', 84 | question: "What is your favorite book?", 85 | author: 'sarah_edo', 86 | timestamp: 1488579767190, 87 | a: { 88 | text: 'Harry Potter', 89 | votes: [], 90 | }, 91 | b: { 92 | text: 'Lord of the Rings', 93 | votes: ['sarah_edo'], 94 | }, 95 | c: { 96 | text: 'To Kill a Mockingbird', 97 | votes: [], 98 | }, 99 | d: { 100 | text: 'Other', 101 | votes: [], 102 | } 103 | }, 104 | "loxhs1bqm25b708cmbf3g": { 105 | id: 'loxhs1bqm25b708cmbf3g', 106 | question: "Which artist do you prefer?", 107 | author: 'tylermcginnis', 108 | timestamp: 1482579767190, 109 | a: { 110 | text: 'Chance the Rapper', 111 | votes: [], 112 | }, 113 | b: { 114 | text: 'Anderson .Paak', 115 | votes: [], 116 | }, 117 | c: { 118 | text: 'Childish Gambino', 119 | votes: [], 120 | }, 121 | d: { 122 | text: 'Kanye West', 123 | votes: ['sarah_edo'], 124 | } 125 | }, 126 | "vthrdm985a262al8qx3do": { 127 | id: 'vthrdm985a262al8qx3do', 128 | question: "Where is the best place to live?", 129 | author: 'tylermcginnis', 130 | timestamp: 1489579767190, 131 | a: { 132 | text: 'Eden, Utah', 133 | votes: ['tylermcginnis'], 134 | }, 135 | b: { 136 | text: 'Kauai, HI', 137 | votes: [], 138 | }, 139 | c: { 140 | text: 'San Francisco, CA', 141 | votes: [], 142 | }, 143 | d: { 144 | text: 'Other', 145 | votes: ['dan_abramov'], 146 | } 147 | }, 148 | "xj352vofupe1dqz9emx13r": { 149 | id: 'xj352vofupe1dqz9emx13r', 150 | question: "Who will win the election in 2020?", 151 | author: 'dan_abramov', 152 | timestamp: 1493579767190, 153 | a: { 154 | text: 'Kanye West', 155 | votes: ['dan_abramov'], 156 | }, 157 | b: { 158 | text: 'Donald Trump', 159 | votes: [], 160 | }, 161 | c: { 162 | text: 'Oprah Winfrey', 163 | votes: ['tylermcginnis'], 164 | }, 165 | d: { 166 | text: 'Dwayne Johnson', 167 | votes: [], 168 | } 169 | }, 170 | } 171 | 172 | function generateUID () { 173 | return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15) 174 | } 175 | 176 | export function _getUsers () { 177 | return new Promise((res, rej) => { 178 | setTimeout(() => res({...users}), 1000) 179 | }) 180 | } 181 | 182 | export function _getPolls () { 183 | return new Promise((res, rej) => { 184 | setTimeout(() => res({...polls}), 1000) 185 | }) 186 | } 187 | 188 | function formatPoll (poll) { 189 | return { 190 | ...poll, 191 | id: generateUID(), 192 | timestamp: Date.now(), 193 | a: { 194 | text: poll.a, 195 | votes: [], 196 | }, 197 | b: { 198 | text: poll.b, 199 | votes: [], 200 | }, 201 | c: { 202 | text: poll.c, 203 | votes: [], 204 | }, 205 | d: { 206 | text: poll.d, 207 | votes: [], 208 | }, 209 | } 210 | } 211 | 212 | export function _savePoll (poll) { 213 | return new Promise((res, rej) => { 214 | const formattedPoll = formatPoll(poll) 215 | 216 | setTimeout(() => { 217 | polls = { 218 | ...polls, 219 | [formattedPoll.id]: formattedPoll, 220 | } 221 | 222 | res(formattedPoll) 223 | }, 1000) 224 | }) 225 | } 226 | 227 | export function _savePollAnswer ({ authedUser, id, answer }) { 228 | return new Promise((res, rej) => { 229 | setTimeout(() => { 230 | const user = users[authedUser] 231 | const poll = polls[id] 232 | 233 | users = { 234 | ...users, 235 | [authedUser]: { 236 | ...user, 237 | answers: { 238 | ...user.answers, 239 | [id]: answer 240 | } 241 | } 242 | } 243 | 244 | polls = { 245 | ...polls, 246 | [id]: { 247 | ...poll, 248 | [answer]: { 249 | ...poll[answer], 250 | votes: poll[answer].votes.concat([authedUser]) 251 | } 252 | } 253 | } 254 | 255 | res() 256 | }, 500) 257 | }) 258 | } --------------------------------------------------------------------------------