├── .gitignore ├── README.md ├── chatter ├── .env ├── README.md ├── package.json ├── public │ ├── favicon.png │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── App.js │ ├── _index.scss │ ├── common │ │ ├── components │ │ │ └── UserProfile │ │ │ │ ├── UserProfile.js │ │ │ │ ├── _user-profile.scss │ │ │ │ └── index.js │ │ └── constants │ │ │ └── initialBottyMessage.js │ ├── components │ │ ├── ContactPanel │ │ │ ├── ContactPanel.js │ │ │ ├── _contact-panel.scss │ │ │ └── index.js │ │ ├── Messages │ │ │ ├── components │ │ │ │ ├── Footer.js │ │ │ │ ├── Header.js │ │ │ │ ├── Message.js │ │ │ │ ├── Messages.js │ │ │ │ └── TypingMessage.js │ │ │ ├── index.js │ │ │ └── styles │ │ │ │ └── _messages.scss │ │ └── UserList │ │ │ ├── UserList.js │ │ │ ├── _user-list.scss │ │ │ ├── constants │ │ │ └── users.js │ │ │ └── index.js │ ├── config.js │ ├── contexts │ │ └── LatestMessages │ │ │ ├── LatestMessages.js │ │ │ ├── constants │ │ │ └── initialMessages.js │ │ │ └── index.js │ ├── index.js │ ├── layouts │ │ └── CoreLayout │ │ │ ├── components │ │ │ ├── CoreLayout.js │ │ │ └── IconBackground.js │ │ │ ├── constants │ │ │ └── icons.js │ │ │ ├── index.js │ │ │ └── styles │ │ │ ├── _core-layout.scss │ │ │ └── _icon-background.scss │ └── styles │ │ ├── _fonts.scss │ │ └── _vars.scss └── yarn.lock ├── dark-mode ├── README.md ├── package.json ├── public │ ├── favicon.ico │ ├── favicon.png │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── common │ │ └── containers │ │ │ └── App.js │ ├── index.js │ ├── routes │ │ ├── App │ │ │ ├── components │ │ │ │ └── App.js │ │ │ ├── index.js │ │ │ └── styles │ │ │ │ └── _app.scss │ │ └── index.js │ └── styles │ │ ├── _dark-mode.scss │ │ └── _main.scss └── yarn.lock ├── issue_template.md ├── new_challenge_template.md ├── pull_request_template.md ├── rocket-ship ├── README.md ├── package.json ├── public │ ├── favicon.ico │ ├── favicon.png │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── index.js │ ├── routes │ │ ├── LaunchPad │ │ │ ├── components │ │ │ │ ├── LaunchPad.js │ │ │ │ └── Rocket │ │ │ │ │ ├── components │ │ │ │ │ ├── Rocket.js │ │ │ │ │ └── RocketCore.js │ │ │ │ │ ├── index.js │ │ │ │ │ └── styles │ │ │ │ │ └── _rocket.scss │ │ │ ├── index.js │ │ │ └── styles │ │ │ │ └── _launchpad.scss │ │ └── index.js │ └── styles │ │ └── _main.scss └── yarn.lock └── spootify ├── README.md ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── assets │ └── images │ │ ├── avatar.svg │ │ └── hero.svg ├── common │ ├── components │ │ ├── Header │ │ │ ├── Header.js │ │ │ ├── _header.scss │ │ │ └── index.js │ │ ├── Player │ │ │ ├── Player.js │ │ │ ├── _player.scss │ │ │ └── index.js │ │ └── SideBar │ │ │ ├── SideBar.js │ │ │ ├── _sidebar.scss │ │ │ └── index.js │ └── layouts │ │ └── CoreLayout.js ├── config.js ├── index.js ├── routes │ ├── Discover │ │ ├── components │ │ │ ├── Discover.js │ │ │ └── DiscoverBlock │ │ │ │ ├── components │ │ │ │ ├── DiscoverBlock.js │ │ │ │ └── DiscoverItem.js │ │ │ │ ├── index.js │ │ │ │ └── styles │ │ │ │ ├── _discover-block.scss │ │ │ │ └── _discover-item.scss │ │ ├── index.js │ │ └── styles │ │ │ └── _discover.scss │ └── index.js └── styles │ ├── _main.scss │ └── _vars.scss └── yarn.lock /.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 | .idea 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | /build 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # 4 |   5 | ### ⭐️ Looking for collaborators ⭐️ 6 | We're looking for people to come and help work on the latest challenge **Coinbee**. If you're interested, get in touch via our slack community or via my website [alexgurr.com](https://alexgurr.com)! 7 | 8 |   9 | # 10 | A series of **ReactJS coding challenges** with a variety of difficulties. Deep dive into the why [here](https://dev.to/alexgurr/react-coding-challenges-for-interviews-beginners-1hlk). 11 | 12 | Interested in some React fundamentals / philosophies? Check out the [react-philosophies](https://github.com/mithi/react-philosophies) GitHub repo. 13 | 14 | 15 |   16 | ## Sponsored 17 | 18 | [Time To Estimate](https://www.timetoestimate.com). A fun, simple way for agile teams to remotely estimate tasks together. Free, with no sign-up required. 19 | 20 | [mixmello](https://www.mixmello.com). Create remixed versions of your favourite Spotify playlists. 21 | 22 |   23 | ## The Challenges 24 | ### Easy 🙂 25 | ##### 🚀 [Rocket Ship](https://github.com/alexgurr/react-coding-challenges/tree/master/rocket-ship) 26 | Unnecessary re-renders, fine grained control. 27 | 28 |   29 | ### Medium 😐 30 | ##### 🌙 [Dark Mode](https://github.com/alexgurr/react-coding-challenges/tree/master/dark-mode) 31 | State / shared state, DOM manipulation. 32 | 33 | ##### 🐝 Coinbee ![soon](https://badgen.net/badge/status/coming%20soon/green?icon=) 34 | Data visualisation and graphing. API usage. 35 | 36 |   37 | ### Hard 😬 38 | ##### 🎧 [Spootify](https://github.com/alexgurr/react-coding-challenges/tree/master/spootify) 39 | Loading state, API usage. 40 | 41 | ##### 🤖 [Chatter](https://github.com/alexgurr/react-coding-challenges/tree/master/chatter) 42 | Web sockets, events, callbacks & React hooks. Talks to [Botty](https://github.com/alexgurr/botty). 43 | 44 |   45 | ## Future Challenges ![later](https://badgen.net/badge/status/coming%20later/yellow?icon=) 46 | ##### 🛒 shopit 47 | A product page with a shopping cart/checkout experience. 48 | 49 |   50 | ## What are the challenges for? 51 | They could be: 52 | - Short coding exercises, for use in interviews with candidates 53 | - Ways for you to test yourself / test your coding abilities under pressure 54 | - Fun exercises to help you learn React 55 | 56 |   57 | ## How do they work / how do I get started? 58 | The scaffolding of each challenges / app is done for you and each challenge has *create-react-app* as its foundation. 59 | 60 | - Clone the whole challenges repository 61 | - Run `yarn` or `npm install` in any of the individual challenge directories to install dependencies 62 | - Run `yarn start` or `npm start` to start the application on port 3000 (CRA default) 63 | - Each challenge has a README with requirements for you to complete 64 | 65 | *Some challenges might require usage of external APIs, but all information will be provided in the individual challenge readme.* 66 | 67 |   68 | ## Have you got the solutions? 69 | All the coding challenges have been completed to a high standard. Get an automatic invite to the solutions repository at [solutions.alexgurr.com](https://www.solutions.alexgurr.com). 70 | 71 |   72 | #### Why are the solutions invite only? 73 | People use these challenges for interviews. By putting the solutions behind a collaboration wall / invite-only repository we can discourage candidates from simply looking up the solutions. 74 | 75 |   76 | #### Can I search for GitHub users and see if they accessed the solutions? 77 | Yes! We track current / past collaborators, meaning if you want to check if a potential candidate had access / looked at the solutions you can simply search for them. You can do this by clicking the search icon in the top left at [solutions.alexgurr.com](https://www.solutions.alexgurr.com). and searching for them. 78 | 79 |   80 | ## Why does it take so long for updates / new challenges? 81 | I work on these challenges & solutions in my spare time, on top of a full time job and everything else that comes in life. Because of this, I don't always get a lot of time to maintain and add new challenges. Interested in becoming a collaborator or submitting your own challenge? **Reach out below or submit a new challenge!** 82 | 83 |   84 | ## Community ![slack-icon](https://puu.sh/Hse6N/da4145b9e1.png) 85 | We're on Slack - come and [join us](https://join.slack.com/t/reactcodingch-ywm3888/shared_invite/zt-o5ns0i1x-nUW_obRlBOAh2muJITqX~g)! 86 | 87 |   88 | ## Thoughts or feedback 💬 89 | Conflicting opinion about a challenge difficulty rating? Need some help or guidance? Got a challenge idea? Get in touch at [alexgurr.com](https://www.alexgurr.com). 90 | 91 |   92 | ## Contributing 💡 93 | We have an [issue template](https://github.com/alexgurr/react-coding-challenges/blob/master/issue_template.md), [pull request template](https://github.com/alexgurr/react-coding-challenges/blob/master/pull_request_template.md) and a [new challenge template](https://github.com/alexgurr/react-coding-challenges/blob/master/new_challenge_template.md). We encourage you to fill out the right template and open a PR / issue! 94 | 95 | ### What Makes A Good Challenge? 96 | - Clear requirements 97 | - Fun and engaging 98 | - Accurate difficulty level 99 | - Looks good (visually pleasing) 100 | - Realistic -- would someone ever need to build something like this in real life? 101 | - Easy to get started (minimal pre-requisites) 102 | -------------------------------------------------------------------------------- /chatter/.env: -------------------------------------------------------------------------------- 1 | SASS_PATH=src/styles 2 | -------------------------------------------------------------------------------- /chatter/README.md: -------------------------------------------------------------------------------- 1 | # Chatter Coding Challenge 🤖   ![hard](https://img.shields.io/badge/-Hard-red) ![time](https://img.shields.io/badge/%E2%8F%B0-60m-blue) 2 | 3 |   4 | # Goals / Outcomes ✨ 5 | - To test knowledge of using sockets (socket.io) and events 6 | - Understanding of callbacks, hooks and function references 7 | 8 |   9 | # Pre-requisites ✅ 10 | None 11 | 12 |   13 | # Requirements 📖 14 | Most of the work needs to be done in the `Messages` components. 15 | 16 | - Implement hooks such as `useEffect` and `useCallback` to handle events 17 | - Scroll to the bottom of the messages list when sending/receiving a message 18 | - Show the initial Botty message by default (can be found in `common/constants`) 19 | - Use **sockets** to: 20 | - Send the user's message to Botty 21 | - Show a typing message when Botty is typing 22 | - Handle incoming Botty messages and display them 23 | 24 |   25 | # Botty Socket Events 26 | See the [Botty server](https://github.com/alexgurr/botty) documentation for more information. 27 | - `bot-typing`: Emitted by Botty when they are typing in response to a user message. 28 | - `bot-message`: Emitted by Botty with a message payload in response to a user message. 29 | - `user-message`: Emitted by you/the client with a messsage payload 30 | 31 |   32 | # Message Classes 33 | We've provided `Message` components and classes. Here's some information about the classes. 34 | - `.message--last`: The last message in a group 35 | - `.message--typing`: The message the user sees when the recipient is typing 36 | - `.message--me`: Denotes a user message 37 | 38 |   39 | # Think about 💡 40 | - References to functions and current hook state 41 | - How to interact with socket.io, events and payloads 42 | - How React contexts work 43 | 44 |   45 | # What's Already Been Done 🏁 46 | - Socket setup/configuration with the [Botty server](https://github.com/alexgurr/botty) ([botty.alexgurr.com](https://botty.alexgurr.com)) 47 | - All UX and UI, including for messages 48 | - All components, including a message and typing message component 49 | - A context for setting the latest message, which will change the preview in the left user list 50 | - Hooks for playing send/receive sounds 51 | 52 |   53 | # Screenshots 🌄 54 |   55 | ![screenshot-desktop](https://puu.sh/Hp0C2/cb14e843de.png) 56 | screenshot-mobile 57 | -------------------------------------------------------------------------------- /chatter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatter", 3 | "version": "0.1.0", 4 | "private": true, 5 | "engines": { 6 | "node" : ">= 15.0.0" 7 | }, 8 | "dependencies": { 9 | "@mdi/font": "^5.9.55", 10 | "@react-hook/mouse-position": "^4.1.0", 11 | "animate.css": "^4.1.1", 12 | "classnames": "^2.2.6", 13 | "sass": "^1.43.4", 14 | "react": "^17.0.2", 15 | "react-dom": "^17.0.2", 16 | "react-scripts": "4.0.3", 17 | "socket.io-client": "^3.1.3", 18 | "use-sound": "^2.0.1", 19 | "web-vitals": "^0.2.4" 20 | }, 21 | "scripts": { 22 | "start": "react-scripts start", 23 | "build": "react-scripts build", 24 | "test": "react-scripts test", 25 | "eject": "react-scripts eject" 26 | }, 27 | "eslintConfig": { 28 | "extends": [ 29 | "react-app", 30 | "react-app/jest" 31 | ] 32 | }, 33 | "browserslist": { 34 | "production": [ 35 | ">0.2%", 36 | "not dead", 37 | "not op_mini all" 38 | ], 39 | "development": [ 40 | "last 1 chrome version", 41 | "last 1 firefox version", 42 | "last 1 safari version" 43 | ] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /chatter/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexgurr/react-coding-challenges/fc446a3dadc2aca69b7e55fc72ed62b413d382b4/chatter/public/favicon.png -------------------------------------------------------------------------------- /chatter/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 19 | 20 | 21 | 30 | Chatter 31 | 32 | 33 | 34 |
35 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /chatter/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexgurr/react-coding-challenges/fc446a3dadc2aca69b7e55fc72ed62b413d382b4/chatter/public/logo192.png -------------------------------------------------------------------------------- /chatter/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexgurr/react-coding-challenges/fc446a3dadc2aca69b7e55fc72ed62b413d382b4/chatter/public/logo512.png -------------------------------------------------------------------------------- /chatter/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.png", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /chatter/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /chatter/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CoreLayout from './layouts/CoreLayout'; 3 | 4 | export default function App() { 5 | return ( 6 | 7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /chatter/src/_index.scss: -------------------------------------------------------------------------------- 1 | @import "styles/fonts"; 2 | @import "~animate.css/animate.css"; 3 | @import '~@mdi/font/scss/materialdesignicons'; 4 | 5 | body { 6 | margin: 0; 7 | font-family: 'helveticaneue', sans-serif; 8 | -webkit-font-smoothing: antialiased; 9 | -moz-osx-font-smoothing: grayscale; 10 | height: 100vh; 11 | width: 100vw; 12 | font-weight: 400; 13 | } 14 | 15 | * { 16 | box-sizing: border-box; 17 | } 18 | 19 | code { 20 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 21 | monospace; 22 | } 23 | 24 | button, 25 | input { 26 | font-family: 'helveticaneue', sans-serif; 27 | } 28 | 29 | input { 30 | font-weight: 500; 31 | font-size: 18px; 32 | color: #334555; 33 | 34 | &:focus { 35 | outline: none; 36 | } 37 | 38 | &::placeholder { 39 | color: #3898EB; 40 | } 41 | } 42 | 43 | button { 44 | font-weight: 500; 45 | } 46 | 47 | .no-margin { 48 | margin: 0; 49 | } 50 | 51 | #root { 52 | height: 100%; 53 | } 54 | -------------------------------------------------------------------------------- /chatter/src/common/components/UserProfile/UserProfile.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './_user-profile.scss'; 3 | 4 | function getInitials(string) { 5 | return string.match(/\b(\w)/g).slice(0, 2).join('').toUpperCase(); 6 | } 7 | 8 | export default function UserProfile({ color, name, icon }) { 9 | return ( 10 |
11 | {icon ? :

{getInitials(name)}

} 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /chatter/src/common/components/UserProfile/_user-profile.scss: -------------------------------------------------------------------------------- 1 | .user-profile { 2 | margin-right: 20px; 3 | width: 60px; 4 | min-width: 60px; 5 | height: 60px; 6 | border-radius: 50%; 7 | display: flex; 8 | align-items: center; 9 | justify-content: center; 10 | 11 | > p, 12 | > i { 13 | margin: 0; 14 | color: white; 15 | font-size: 18px; 16 | font-weight: 500; 17 | } 18 | 19 | > i { 20 | font-size: 25px; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /chatter/src/common/components/UserProfile/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './UserProfile'; 2 | -------------------------------------------------------------------------------- /chatter/src/common/constants/initialBottyMessage.js: -------------------------------------------------------------------------------- 1 | export default 'Hi! My name\'s Botty.'; 2 | -------------------------------------------------------------------------------- /chatter/src/components/ContactPanel/ContactPanel.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import cx from 'classnames'; 3 | import './_contact-panel.scss'; 4 | 5 | export default function ContactPanel() { 6 | const [minimised, setMinimised] = useState(Boolean(localStorage.getItem('minimised'))); 7 | 8 | const onClick = () => { 9 | // Remember user preference 10 | localStorage.setItem('minimised', minimised ? '' : 'true'); 11 | 12 | setMinimised(!minimised); 13 | }; 14 | 15 | return ( 16 |
17 |
18 | 19 |
20 |
21 |

Botty

22 |
23 |
24 |
25 |
26 |

Email

27 |

botty@reactcodingchallenges.com

28 |
29 |
30 |

Phone

31 |

0498365942

32 |
33 |
34 |

Labels

35 |
36 |

Bot

37 |

React

38 |
39 |
40 |
41 |

Attachments

42 |
43 |

Dataset.csv

44 |

bot_face.jpg

45 |
46 |

View All

47 |
48 | 49 |
50 |
51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /chatter/src/components/ContactPanel/_contact-panel.scss: -------------------------------------------------------------------------------- 1 | @import 'vars'; 2 | 3 | .contact-panel { 4 | width: 100%; 5 | max-width: 400px; 6 | transition: width 0.5s ease-in-out; 7 | border-left: 1px solid #E4EDEF; 8 | background: white; 9 | display: flex; 10 | flex-direction: column; 11 | 12 | &__body { 13 | padding: 50px; 14 | padding-top: 40px; 15 | display: flex; 16 | flex-direction: column; 17 | flex: 1; 18 | overflow: hidden; 19 | transition: opacity 0.5s ease-in-out; 20 | overflow-y: auto; 21 | 22 | &__edit-btn { 23 | margin-top: auto; 24 | border: 0; 25 | border-radius: 24px; 26 | font-size: 20px; 27 | background: #D9EFFC; 28 | color: #4592DB; 29 | height: 50px; 30 | min-height: 50px; 31 | font-weight: 500; 32 | display: inline-block; 33 | width: 225px; 34 | margin-left: auto; 35 | margin-right: auto; 36 | cursor: pointer; 37 | 38 | &:focus { 39 | outline: none; 40 | } 41 | 42 | &:hover { 43 | background: #bbebff; 44 | } 45 | } 46 | 47 | &__link { 48 | color: #61A7E6; 49 | font-size: 18px; 50 | cursor: pointer; 51 | display: inline-flex; 52 | 53 | &:hover { 54 | color: darken(#61A7E6, 10%); 55 | } 56 | } 57 | 58 | &__labels { 59 | white-space: nowrap; 60 | 61 | > * { 62 | background: #D9EFFC; 63 | height: 35px; 64 | border-radius: 20px; 65 | display: inline-flex; 66 | align-items: center; 67 | padding-left: 20px; 68 | padding-right: 20px; 69 | font-weight: 500; 70 | color: #25455E; 71 | font-size: 16px; 72 | margin-right: 10px; 73 | cursor: pointer; 74 | 75 | &:hover { 76 | background: #bbebff; 77 | } 78 | 79 | > i { 80 | margin-left: 10px; 81 | color: #52A6FA; 82 | } 83 | } 84 | } 85 | 86 | &__attachments { 87 | color: #5E7182; 88 | font-size: 18px; 89 | white-space: nowrap; 90 | 91 | > p > i { 92 | margin-right: 15px; 93 | } 94 | } 95 | 96 | &__block { 97 | margin-bottom: 60px; 98 | } 99 | 100 | &__value { 101 | font-size: 18px; 102 | margin: 0; 103 | margin-top: 10px; 104 | color: #4A5861; 105 | } 106 | 107 | &__label { 108 | letter-spacing: 1.5px; 109 | font-weight: 500; 110 | color: #596872; 111 | font-size: 18px; 112 | margin: 0; 113 | 114 | &:not(:first-of-type) { 115 | margin-top: 20px; 116 | } 117 | } 118 | } 119 | 120 | &__header { 121 | background: #4DB8EF; 122 | padding: 40px; 123 | display: flex; 124 | flex-direction: column; 125 | padding-right: 10px; 126 | padding-top: 10px; 127 | transition: padding 0.5s ease-in-out; 128 | height: 260px; 129 | 130 | &__profile { 131 | color: white; 132 | margin-top: 20px; 133 | transition: opacity 0.5s ease-in-out; 134 | 135 | &__picture { 136 | width: 100px; 137 | height: 100px; 138 | border-radius: 50%; 139 | border: 3px solid white; 140 | display: flex; 141 | align-items: center; 142 | justify-content: center; 143 | font-size: 40px; 144 | } 145 | 146 | > h1 { 147 | font-weight: 400; 148 | margin-left: 10px; 149 | } 150 | } 151 | } 152 | 153 | &__toggle { 154 | font-size: 30px; 155 | margin-left: auto; 156 | color: white; 157 | cursor: pointer; 158 | transition: transform 0.2s ease-in-out; 159 | 160 | @media only screen and (min-width: 1500px) { 161 | display: none; 162 | } 163 | 164 | &:hover { 165 | transform: scale(1.05); 166 | } 167 | 168 | &::before { 169 | transform: none; 170 | } 171 | } 172 | 173 | @media only screen and (max-width: 1500px) { 174 | &--minimised { 175 | width: $contact-panel-min-width !important; 176 | 177 | > .contact-panel { 178 | &__body { 179 | opacity: 0; 180 | } 181 | 182 | &__header { 183 | padding-left: 0; 184 | 185 | .contact-panel { 186 | &__header { 187 | &__profile { 188 | opacity: 0; 189 | } 190 | } 191 | 192 | &__toggle { 193 | &::before { 194 | transform: scale(-1, 1); 195 | } 196 | } 197 | } 198 | } 199 | } 200 | } 201 | } 202 | 203 | @media only screen and (max-width: $mid-breakpoint) { 204 | position: fixed; 205 | right: 0; 206 | width: 100%; 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /chatter/src/components/ContactPanel/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './ContactPanel'; 2 | -------------------------------------------------------------------------------- /chatter/src/components/Messages/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const RETURN_KEY_CODE = 13; 4 | 5 | export default function Footer({ sendMessage, onChangeMessage, message }) { 6 | const onKeyDown = ({ keyCode }) => { 7 | if (keyCode !== RETURN_KEY_CODE ) { return; } 8 | 9 | sendMessage(); 10 | } 11 | 12 | return ( 13 |
14 | 20 |
21 | 22 | 23 | 24 | 25 |
26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /chatter/src/components/Messages/components/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import UserProfile from '../../../common/components/UserProfile'; 3 | 4 | export default function Header() { 5 | return ( 6 |
7 |
8 | 9 |
10 |

Botty

11 |

Cloud, The Internet

12 |
13 |
14 |
15 |
16 | 17 |

botty-beep-boop

18 |
19 |
20 | 21 |

5m

22 |
23 |
24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /chatter/src/components/Messages/components/Message.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import cx from 'classnames'; 3 | 4 | const ME = 'me'; 5 | 6 | export default function Message({ nextMessage, message, botTyping }) { 7 | return ( 8 |

20 | {message.message} 21 |

22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /chatter/src/components/Messages/components/Messages.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import io from 'socket.io-client'; 3 | import useSound from 'use-sound'; 4 | import config from '../../../config'; 5 | import LatestMessagesContext from '../../../contexts/LatestMessages/LatestMessages'; 6 | import TypingMessage from './TypingMessage'; 7 | import Header from './Header'; 8 | import Footer from './Footer'; 9 | import Message from './Message'; 10 | import '../styles/_messages.scss'; 11 | 12 | const socket = io( 13 | config.BOT_SERVER_ENDPOINT, 14 | { transports: ['websocket', 'polling', 'flashsocket'] } 15 | ); 16 | 17 | function Messages() { 18 | const [playSend] = useSound(config.SEND_AUDIO_URL); 19 | const [playReceive] = useSound(config.RECEIVE_AUDIO_URL); 20 | const { setLatestMessage } = useContext(LatestMessagesContext); 21 | 22 | return ( 23 |
24 |
25 |
26 |
27 |
28 |
29 | ); 30 | } 31 | 32 | export default Messages; 33 | -------------------------------------------------------------------------------- /chatter/src/components/Messages/components/TypingMessage.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | 3 | export default function Typing() { 4 | const [numberOfDots, setDots] = useState(1); 5 | 6 | const incrementDots = () => { 7 | setDots(numberOfDots === 3 ? 1 : numberOfDots + 1); 8 | }; 9 | 10 | useEffect(() => { 11 | const timeout = setTimeout(incrementDots, 500); 12 | 13 | return () => { 14 | clearTimeout(timeout); 15 | } 16 | }, [numberOfDots]); 17 | 18 | return ( 19 |

23 | {`Typing${''.padStart(numberOfDots, '.')}`} 24 |

25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /chatter/src/components/Messages/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './components/Messages'; 2 | -------------------------------------------------------------------------------- /chatter/src/components/Messages/styles/_messages.scss: -------------------------------------------------------------------------------- 1 | @import 'vars'; 2 | 3 | .messages { 4 | display: flex; 5 | flex-direction: column; 6 | flex: 1; 7 | 8 | @media only screen and (max-width: $mid-breakpoint) { 9 | margin-right: $contact-panel-min-width; 10 | } 11 | 12 | &__list { 13 | flex: 1; 14 | overflow-y: scroll; 15 | padding: 30px; 16 | display: flex; 17 | flex-direction: column; 18 | } 19 | 20 | &__header { 21 | display: flex; 22 | height: 100px; 23 | background: white; 24 | align-items: center; 25 | padding-left: 30px; 26 | padding-right: 30px; 27 | border-bottom: 1px solid #E4EDEF; 28 | justify-content: space-between; 29 | 30 | @media only screen and (max-width: 700px) { 31 | flex-direction: column; 32 | align-items: flex-start; 33 | height: 150px; 34 | justify-content: center; 35 | } 36 | 37 | &__online-dot { 38 | width: 12px; 39 | height: 12px; 40 | border-radius: 50%; 41 | background: #5BBD57; 42 | margin-left: 10px; 43 | margin-top: 3px; 44 | } 45 | 46 | &__left-content { 47 | &__text { 48 | > h1 { 49 | display: flex; 50 | align-items: center; 51 | margin: 0; 52 | color: #193147; 53 | font-weight: 400; 54 | font-size: 24px; 55 | } 56 | 57 | > p { 58 | margin: 0; 59 | color: #80909B; 60 | font-size: 18px; 61 | font-weight: 500; 62 | margin-top: 5px; 63 | } 64 | } 65 | 66 | > .user-profile { 67 | @media only screen and (max-width: 700px) { 68 | display: none; 69 | } 70 | } 71 | } 72 | 73 | &__status { 74 | font-weight: 500; 75 | display: flex; 76 | align-items: center; 77 | color: #3D5364; 78 | 79 | > i { 80 | margin-right: 10px; 81 | font-size: 20px; 82 | color: #8191A0; 83 | 84 | &.mdi { 85 | font-size: 24px; 86 | } 87 | } 88 | 89 | > p { 90 | font-size: 18px; 91 | } 92 | 93 | &:not(:last-of-type) { 94 | margin-right: 40px; 95 | } 96 | } 97 | 98 | &__left-content, 99 | &__right-content, 100 | &__status { 101 | display: flex; 102 | align-items: center; 103 | } 104 | 105 | &__right-content { 106 | @media only screen and (max-width: 780px) { 107 | flex-direction: column; 108 | align-items: flex-start; 109 | } 110 | 111 | @media only screen and (max-width: 700px) { 112 | margin-top: 10px; 113 | } 114 | } 115 | } 116 | 117 | &__footer { 118 | display: flex; 119 | height: 100px; 120 | background: white; 121 | align-items: center; 122 | padding-left: 30px; 123 | padding-right: 30px; 124 | border-top: 1px solid #E4EDEF; 125 | 126 | @media only screen and (max-width: 700px) { 127 | flex-direction: column; 128 | height: 120px; 129 | justify-content: center; 130 | } 131 | 132 | &__actions { 133 | display: flex; 134 | align-items: center; 135 | 136 | @media only screen and (max-width: 700px) { 137 | margin-left: auto; 138 | } 139 | 140 | > i { 141 | font-size: 25px; 142 | color: #8194A4; 143 | margin-right: 20px; 144 | cursor: pointer; 145 | transition: transform 0.2s ease-in-out; 146 | 147 | &:hover { 148 | color: #3898EB; 149 | transform: scale(1.05); 150 | } 151 | 152 | &.mdi { 153 | font-size: 30px; 154 | } 155 | } 156 | 157 | > button { 158 | background: none; 159 | border: 0; 160 | color: #3898EB; 161 | font-size: 18px; 162 | transition: transform 0.2s ease-in-out; 163 | will-change: transform; 164 | 165 | &:hover:not(:disabled) { 166 | transform: scale(1.05); 167 | } 168 | 169 | &:not(:disabled) { 170 | cursor: pointer; 171 | } 172 | 173 | &:focus { 174 | outline: none !important; 175 | } 176 | 177 | &:disabled { 178 | color: #B7C0CD; 179 | } 180 | } 181 | } 182 | 183 | > input { 184 | flex: 1; 185 | margin-right: 20px; 186 | height: 50px; 187 | border: 0; 188 | 189 | @media only screen and (max-width: 700px) { 190 | width: 100%; 191 | margin-right: 0; 192 | flex: unset; 193 | } 194 | } 195 | } 196 | 197 | &__message { 198 | padding: 20px; 199 | border-radius: 25px; 200 | font-weight: 500; 201 | width: fit-content; 202 | margin-bottom: 10px; 203 | margin-top: 10px; 204 | max-width: 60%; 205 | word-break: break-word; 206 | padding-top: 15px; 207 | padding-bottom: 12px; 208 | 209 | &--last:not(&--me) { 210 | border-bottom-left-radius: 0; 211 | } 212 | 213 | &--last.messages__message--me { 214 | border-bottom-right-radius: 0; 215 | } 216 | 217 | &:last-of-type { 218 | margin-bottom: 0; 219 | } 220 | 221 | ~ .messages__message:not(.messages__message--me) { 222 | margin-bottom: -5px; 223 | } 224 | 225 | &--typing { 226 | min-width: 100px; 227 | border-bottom-left-radius: 0; 228 | } 229 | 230 | &--me { 231 | background: #3898EB; 232 | color: white; 233 | margin-left: auto; 234 | border-bottom-left-radius: 25px; 235 | 236 | ~ .messages__message--me { 237 | margin-top: -5px; 238 | } 239 | } 240 | 241 | &:not(&--me) { 242 | background: white; 243 | color: #29475C; 244 | } 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /chatter/src/components/UserList/UserList.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import cx from 'classnames'; 3 | import LatestMessagesContext from '../../contexts/LatestMessages/LatestMessages'; 4 | import UserProfile from '../../common/components/UserProfile/UserProfile'; 5 | import USERS from './constants/users'; 6 | import './_user-list.scss'; 7 | 8 | function User({ icon, name, lastActive, isOnline, userId, color }) { 9 | const { messages } = useContext(LatestMessagesContext); 10 | 11 | return ( 12 |
13 | 14 |
15 |
16 |

{name}

17 |

18 | {isOnline ? 'Online' : lastActive} 19 |

20 |
21 |

{messages[userId]}

22 |
23 |
24 | ); 25 | } 26 | 27 | export default function UserList() { 28 | return ( 29 |
30 |
31 |
32 |

All Messages

33 | 34 |
35 | 36 |
37 |
38 | {USERS.map(user => )} 39 |
40 |
41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /chatter/src/components/UserList/_user-list.scss: -------------------------------------------------------------------------------- 1 | @import 'vars'; 2 | 3 | .user-list { 4 | width: 25%; 5 | height: 100%; 6 | border-right: 1px solid #E4EDEF; 7 | overflow: hidden; 8 | max-width: 500px; 9 | background: white; 10 | min-width: 350px; 11 | 12 | @media only screen and (max-width: $small-breakpoint) { 13 | min-width: 100px; 14 | width: fit-content; 15 | } 16 | 17 | &__header { 18 | padding-left: 25px; 19 | padding-right: 25px; 20 | display: flex; 21 | align-items: center; 22 | height: 100px; 23 | border-bottom: 1px solid #F0F4F7; 24 | 25 | @media only screen and (max-width: $small-breakpoint) { 26 | margin-right: auto; 27 | } 28 | 29 | @media only screen and (max-width: 700px) { 30 | height: 150px; 31 | } 32 | 33 | > i { 34 | margin-left: auto; 35 | color: #51A0E5; 36 | font-size: 25px; 37 | cursor: pointer; 38 | 39 | @media only screen and (max-width: $small-breakpoint) { 40 | margin-right: auto; 41 | } 42 | } 43 | 44 | &__left { 45 | display: flex; 46 | align-items: center; 47 | cursor: pointer; 48 | 49 | @media only screen and (max-width: $small-breakpoint) { 50 | display: none; 51 | } 52 | 53 | > i { 54 | font-size: 15px; 55 | color: #8493A5; 56 | margin-left: 10px; 57 | } 58 | 59 | > p { 60 | letter-spacing: 1px; 61 | font-weight: 500; 62 | color: #596872; 63 | font-size: 18px; 64 | margin: 0; 65 | } 66 | } 67 | } 68 | 69 | &__users { 70 | height: 100%; 71 | overflow-y: scroll; 72 | padding-bottom: 100px; 73 | 74 | @media only screen and (max-width: 700px) { 75 | padding-bottom: 150px; 76 | } 77 | 78 | &__user { 79 | height: 120px; 80 | display: flex; 81 | align-items: center; 82 | padding-left: 20px; 83 | padding-right: 20px; 84 | cursor: pointer; 85 | 86 | @media only screen and (max-width: $small-breakpoint) { 87 | > .user-profile { 88 | margin-right: 0; 89 | } 90 | } 91 | 92 | &:hover { 93 | background: #F3F4F6; 94 | } 95 | 96 | &:first-of-type { 97 | background: #EFF8FB; 98 | cursor: default; 99 | } 100 | 101 | &__profile { 102 | 103 | } 104 | 105 | &__right-content { 106 | flex: 1; 107 | width: 0; 108 | 109 | > p { 110 | color: #94A2AE; 111 | margin-bottom: 0; 112 | margin-top: 5px; 113 | text-overflow: ellipsis; 114 | overflow: hidden; 115 | white-space: nowrap; 116 | max-width: 60%; 117 | } 118 | 119 | @media only screen and (max-width: $small-breakpoint) { 120 | display: none; 121 | } 122 | 123 | } 124 | 125 | &__title { 126 | display: flex; 127 | 128 | > p { 129 | margin: 0; 130 | } 131 | 132 | > p:last-of-type { 133 | margin-left: auto; 134 | color: #94A2AE; 135 | } 136 | } 137 | 138 | &__online { 139 | background: #83C67E; 140 | color: white !important; 141 | padding: 5px; 142 | padding-left: 10px; 143 | padding-right: 10px; 144 | border-radius: 12px; 145 | font-size: 14px; 146 | letter-spacing: 1px; 147 | text-transform: lowercase; 148 | font-weight: 400; 149 | line-height: 15px; 150 | } 151 | 152 | &:not(:last-of-type) { 153 | border-bottom: 1px solid #F0F4F7; 154 | } 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /chatter/src/components/UserList/constants/users.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { name: 'Botty', userId: 'bot', icon: 'fas fa-comment-dots', isOnline: true, color: '#4DB8EF' }, 3 | { name: 'Brandon Andrews', userId: 'brandon', isOnline: false, lastActive: '3 hours go', color: '#DD95BA', lastMessage: 'Hello there!' }, 4 | { name: 'Clayton Day', userId: 'clayton', isOnline: false, lastActive: 'Yesterday', color: '#62D5D1', lastMessage: 'Yes of course. Thanks' }, 5 | { name: 'Bernice Clark', userId: 'bernice', isOnline: true, color: '#82D39F', lastMessage: 'This is a question regarding the' }, 6 | { name: 'Christine Fields', userId: 'christine', isOnline: true, lastActive: 'Jul 28', color: '#FFBB75', lastMessage: 'Do you need help with the price?' }, 7 | { name: 'Mike Morgan', userId: 'mike', isOnline: false, lastActive: 'Jul 27', color: '#F47E64', lastMessage: 'Choose the perfect accommodation' }, 8 | { name: 'Callie Schmidt', userId: 'callie', isOnline: false, lastActive: 'Jul 23', color: '#F57971', lastMessage: 'Yes thanks!' }, 9 | { name: 'Herbert Watkins', userId: 'herbert', isOnline: false, lastActive: 'Jul 23', color: '#B967B9', lastMessage: 'Of course, send as an email to' }, 10 | { name: 'Bessie Coleman', userId: 'bessie', isOnline: false, lastActive: 'Jul 23', color: '#4DB8EF', lastMessage: 'Sorry you couldn\'t read it' }, 11 | { name: 'Lottie Jordan', userId: 'lottie', isOnline: false, lastActive: 'Jul 23', color: '#62D5D1', lastMessage: '728 Feeney Street.' }, 12 | { name: 'Augusta Castillo', userId: 'augusta', isOnline: false, lastActive: 'Jul 23', color: '#82D39F', lastMessage: 'I got the transfer! :D' } 13 | ]; 14 | -------------------------------------------------------------------------------- /chatter/src/components/UserList/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './UserList'; 2 | -------------------------------------------------------------------------------- /chatter/src/config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | BOT_SERVER_ENDPOINT: 'https://botty.alexgurr.com', 3 | SEND_AUDIO_URL: 'https://puu.sh/GSHJ0/25fae22f76.mp3', 4 | RECEIVE_AUDIO_URL: 'https://puu.sh/GSHIU/df806a9cb8.mp3' 5 | }; 6 | -------------------------------------------------------------------------------- /chatter/src/contexts/LatestMessages/LatestMessages.js: -------------------------------------------------------------------------------- 1 | import React, { useState, createContext, useCallback } from 'react'; 2 | import initialMessages from './constants/initialMessages'; 3 | 4 | const LatestMessagesContext = createContext({}); 5 | 6 | export default LatestMessagesContext; 7 | 8 | export function LatestMessages({ children }) { 9 | const [messages, setMessages] = useState(initialMessages); 10 | 11 | const setLatestMessage = useCallback((userId, value) => { 12 | setMessages({ ...messages, [userId]: value }); 13 | }, [messages]); 14 | 15 | return ( 16 | 17 | {children} 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /chatter/src/contexts/LatestMessages/constants/initialMessages.js: -------------------------------------------------------------------------------- 1 | import INITIAL_BOTTY_MESSAGE from '../../../common/constants/initialBottyMessage'; 2 | 3 | export default { 4 | bot: INITIAL_BOTTY_MESSAGE, 5 | brandon: 'Hello there!', 6 | clayton: 'Yes of course. Thanks', 7 | bernice: 'This is a question regarding the fun time we had.', 8 | christine: 'Do you need help with the price?', 9 | mike: 'Choose the perfect accommodation', 10 | callie: 'Yes thanks!', 11 | herbert: 'Of course, send as an email to my address.', 12 | bessie: 'Sorry you couldn\'t read it', 13 | lottie: '728 Feeney Street.', 14 | augusta: 'I got the transfer! :D' 15 | }; 16 | -------------------------------------------------------------------------------- /chatter/src/contexts/LatestMessages/index.js: -------------------------------------------------------------------------------- 1 | export { default, LatestMessages } from './LatestMessages'; 2 | -------------------------------------------------------------------------------- /chatter/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import './_index.scss'; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root') 11 | ); 12 | -------------------------------------------------------------------------------- /chatter/src/layouts/CoreLayout/components/CoreLayout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { LatestMessages } from '../../../contexts/LatestMessages/LatestMessages'; 3 | import ContactPanel from '../../../components/ContactPanel'; 4 | import UserList from '../../../components/UserList'; 5 | import Messages from '../../../components/Messages'; 6 | import IconBackground from './IconBackground'; 7 | import '../styles/_core-layout.scss'; 8 | 9 | export default function CoreLayout() { 10 | return ( 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /chatter/src/layouts/CoreLayout/components/IconBackground.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ICONS from '../constants/icons'; 3 | import '../styles/_icon-background.scss'; 4 | 5 | const SPACING_PX = 125; 6 | const SPACING_MARGIN = SPACING_PX / 4; 7 | 8 | function getRandomNumber(min, max) { 9 | return Math.floor(Math.random() * (max - min)) + min; 10 | } 11 | 12 | function getRandomIcon() { 13 | return ICONS[getRandomNumber(0, ICONS.length)]; 14 | } 15 | 16 | function IconRow({ numberOfIcons }) { 17 | return ( 18 |
19 | {[...new Array(numberOfIcons)].map(() => { 20 | const icon = getRandomIcon(); 21 | 22 | return ( 23 |