diff --git a/.gitignore b/.gitignore index 4ff17093c..6b7093956 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,3 @@ node_modules npm-debug.log* yarn-debug.log* yarn-error.log* -yarn.lock diff --git a/README.md b/README.md index 91f2280d0..86c6c70ca 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,101 @@ -# React Coding Challenges + -A series of easy through to hard, **React.js coding challenges**. +# +  +### โญ๏ธ Looking for collaborators โญ๏ธ +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)! -# When could these be useful? -- Usage as short coding exercises, such as for interviews -- To test yourself/test your coding abilities under pressure -- For beginners looking for a fun way to learn React +  +# +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). -# How Do They Work? -The scaffolding is done for you. Simply run `yarn` in any of the directories to start the application. Each application has a README with its requirements. +Interested in some React fundamentals / philosophies? Check out the [react-philosophies](https://github.com/mithi/react-philosophies) GitHub repo. -# Have you got the solutions? -I have completed all the coding challenges to a high standard. If you're interested in being invited to the solutions GitHub repository, get in touch. -# What's coming next? -I'll be adding new coding challenges as I come up with ideas! If you have any ideas you think could be suitable, get in touch via www.alexgurr.com. +  +## Sponsored -# Contents -## Easy ๐Ÿ˜ƒ -#### Rocket Ship -Unecessary re-renders, fine grained control + [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. -## Medium ๐Ÿ˜ฌ -#### Dark Mode -State/Shared State, DOM manipulation + [mixmello](https://www.mixmello.com). Create remixed versions of your favourite Spotify playlists. -## Hard ๐Ÿคจ -#### Spootify -Loading state, API usage +  +## The Challenges +### Easy ๐Ÿ™‚ +##### ๐Ÿš€ [Rocket Ship](https://github.com/alexgurr/react-coding-challenges/tree/master/rocket-ship) +Unnecessary re-renders, fine grained control. + +  +### Medium ๐Ÿ˜ +##### ๐ŸŒ™ [Dark Mode](https://github.com/alexgurr/react-coding-challenges/tree/master/dark-mode) +State / shared state, DOM manipulation. + +##### ๐Ÿ Coinbee ![soon](https://badgen.net/badge/status/coming%20soon/green?icon=) +Data visualisation and graphing. API usage. + +  +### Hard ๐Ÿ˜ฌ +##### ๐ŸŽง [Spootify](https://github.com/alexgurr/react-coding-challenges/tree/master/spootify) +Loading state, API usage. + +##### ๐Ÿค– [Chatter](https://github.com/alexgurr/react-coding-challenges/tree/master/chatter) +Web sockets, events, callbacks & React hooks. Talks to [Botty](https://github.com/alexgurr/botty). + +  +## Future Challenges ![later](https://badgen.net/badge/status/coming%20later/yellow?icon=) +##### ๐Ÿ›’ shopit +A product page with a shopping cart/checkout experience. + +  +## What are the challenges for? +They could be: +- Short coding exercises, for use in interviews with candidates +- Ways for you to test yourself / test your coding abilities under pressure +- Fun exercises to help you learn React + +  +## How do they work / how do I get started? +The scaffolding of each challenges / app is done for you and each challenge has *create-react-app* as its foundation. + +- Clone the whole challenges repository +- Run `yarn` or `npm install` in any of the individual challenge directories to install dependencies +- Run `yarn start` or `npm start` to start the application on port 3000 (CRA default) +- Each challenge has a README with requirements for you to complete + +*Some challenges might require usage of external APIs, but all information will be provided in the individual challenge readme.* + +  +## Have you got the solutions? +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). + +  +#### Why are the solutions invite only? +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. + +  +#### Can I search for GitHub users and see if they accessed the solutions? +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. + +  +## Why does it take so long for updates / new challenges? +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!** + +  +## Community ![slack-icon](https://puu.sh/Hse6N/da4145b9e1.png) +We're on Slack - come and [join us](https://join.slack.com/t/reactcodingch-ywm3888/shared_invite/zt-o5ns0i1x-nUW_obRlBOAh2muJITqX~g)! + +  +## Thoughts or feedback ๐Ÿ’ฌ +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). + +  +## Contributing ๐Ÿ’ก +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! + +### What Makes A Good Challenge? +- Clear requirements +- Fun and engaging +- Accurate difficulty level +- Looks good (visually pleasing) +- Realistic -- would someone ever need to build something like this in real life? +- Easy to get started (minimal pre-requisites) diff --git a/chatter/.env b/chatter/.env new file mode 100644 index 000000000..56bcc2dd8 --- /dev/null +++ b/chatter/.env @@ -0,0 +1 @@ +SASS_PATH=src/styles diff --git a/chatter/README.md b/chatter/README.md new file mode 100644 index 000000000..7e6fb1bd3 --- /dev/null +++ b/chatter/README.md @@ -0,0 +1,56 @@ +# Chatter Coding Challenge ๐Ÿค–   ![hard](https://img.shields.io/badge/-Hard-red) ![time](https://img.shields.io/badge/%E2%8F%B0-60m-blue) + +  +# Goals / Outcomes โœจ +- To test knowledge of using sockets (socket.io) and events +- Understanding of callbacks, hooks and function references + +  +# Pre-requisites โœ… +None + +  +# Requirements ๐Ÿ“– +Most of the work needs to be done in the `Messages` components. + +- Implement hooks such as `useEffect` and `useCallback` to handle events +- Scroll to the bottom of the messages list when sending/receiving a message +- Show the initial Botty message by default (can be found in `common/constants`) +- Use **sockets** to: + - Send the user's message to Botty + - Show a typing message when Botty is typing + - Handle incoming Botty messages and display them + +  +# Botty Socket Events +See the [Botty server](https://github.com/alexgurr/botty) documentation for more information. +- `bot-typing`: Emitted by Botty when they are typing in response to a user message. +- `bot-message`: Emitted by Botty with a message payload in response to a user message. +- `user-message`: Emitted by you/the client with a messsage payload + +  +# Message Classes +We've provided `Message` components and classes. Here's some information about the classes. +- `.message--last`: The last message in a group +- `.message--typing`: The message the user sees when the recipient is typing +- `.message--me`: Denotes a user message + +  +# Think about ๐Ÿ’ก +- References to functions and current hook state +- How to interact with socket.io, events and payloads +- How React contexts work + +  +# What's Already Been Done ๐Ÿ +- Socket setup/configuration with the [Botty server](https://github.com/alexgurr/botty) ([botty.alexgurr.com](https://botty.alexgurr.com)) +- All UX and UI, including for messages +- All components, including a message and typing message component +- A context for setting the latest message, which will change the preview in the left user list +- Hooks for playing send/receive sounds + +  +# Screenshots ๐ŸŒ„ +  +![screenshot-desktop](https://puu.sh/Hp0C2/cb14e843de.png) +screenshot-mobile diff --git a/chatter/package.json b/chatter/package.json new file mode 100644 index 000000000..e066eca92 --- /dev/null +++ b/chatter/package.json @@ -0,0 +1,45 @@ +{ + "name": "chatter", + "version": "0.1.0", + "private": true, + "engines": { + "node" : ">= 15.0.0" + }, + "dependencies": { + "@mdi/font": "^5.9.55", + "@react-hook/mouse-position": "^4.1.0", + "animate.css": "^4.1.1", + "classnames": "^2.2.6", + "sass": "^1.43.4", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "react-scripts": "4.0.3", + "socket.io-client": "^3.1.3", + "use-sound": "^2.0.1", + "web-vitals": "^0.2.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/chatter/public/favicon.png b/chatter/public/favicon.png new file mode 100644 index 000000000..a7502d898 Binary files /dev/null and b/chatter/public/favicon.png differ diff --git a/chatter/public/index.html b/chatter/public/index.html new file mode 100644 index 000000000..4aec35995 --- /dev/null +++ b/chatter/public/index.html @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + Chatter + + + +
+ + + diff --git a/chatter/public/logo192.png b/chatter/public/logo192.png new file mode 100644 index 000000000..fc44b0a37 Binary files /dev/null and b/chatter/public/logo192.png differ diff --git a/chatter/public/logo512.png b/chatter/public/logo512.png new file mode 100644 index 000000000..a4e47a654 Binary files /dev/null and b/chatter/public/logo512.png differ diff --git a/chatter/public/manifest.json b/chatter/public/manifest.json new file mode 100644 index 000000000..e83b198a4 --- /dev/null +++ b/chatter/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.png", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/chatter/public/robots.txt b/chatter/public/robots.txt new file mode 100644 index 000000000..e9e57dc4d --- /dev/null +++ b/chatter/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/chatter/src/App.js b/chatter/src/App.js new file mode 100644 index 000000000..c634c8739 --- /dev/null +++ b/chatter/src/App.js @@ -0,0 +1,8 @@ +import React from 'react'; +import CoreLayout from './layouts/CoreLayout'; + +export default function App() { + return ( + + ); +} diff --git a/chatter/src/_index.scss b/chatter/src/_index.scss new file mode 100644 index 000000000..0ecb79f78 --- /dev/null +++ b/chatter/src/_index.scss @@ -0,0 +1,53 @@ +@import "http://github.com/styles/fonts"; +@import "http://github.com/~animate.css/animate.css"; +@import 'http://github.com/~@mdi/font/scss/materialdesignicons'; + +body { + margin: 0; + font-family: 'helveticaneue', sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + height: 100vh; + width: 100vw; + font-weight: 400; +} + +* { + box-sizing: border-box; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} + +button, +input { + font-family: 'helveticaneue', sans-serif; +} + +input { + font-weight: 500; + font-size: 18px; + color: #334555; + + &:focus { + outline: none; + } + + &::placeholder { + color: #3898EB; + } +} + +button { + font-weight: 500; +} + +.no-margin { + margin: 0; +} + +#root { + height: 100%; +} diff --git a/chatter/src/common/components/UserProfile/UserProfile.js b/chatter/src/common/components/UserProfile/UserProfile.js new file mode 100644 index 000000000..e777b5be6 --- /dev/null +++ b/chatter/src/common/components/UserProfile/UserProfile.js @@ -0,0 +1,14 @@ +import React from 'react'; +import './_user-profile.scss'; + +function getInitials(string) { + return string.match(/\b(\w)/g).slice(0, 2).join('').toUpperCase(); +} + +export default function UserProfile({ color, name, icon }) { + return ( +
+ {icon ? :

{getInitials(name)}

} +
+ ); +} diff --git a/chatter/src/common/components/UserProfile/_user-profile.scss b/chatter/src/common/components/UserProfile/_user-profile.scss new file mode 100644 index 000000000..951ab0bc7 --- /dev/null +++ b/chatter/src/common/components/UserProfile/_user-profile.scss @@ -0,0 +1,22 @@ +.user-profile { + margin-right: 20px; + width: 60px; + min-width: 60px; + height: 60px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + + > p, + > i { + margin: 0; + color: white; + font-size: 18px; + font-weight: 500; + } + + > i { + font-size: 25px; + } +} diff --git a/chatter/src/common/components/UserProfile/index.js b/chatter/src/common/components/UserProfile/index.js new file mode 100644 index 000000000..ab8c3d6b6 --- /dev/null +++ b/chatter/src/common/components/UserProfile/index.js @@ -0,0 +1 @@ +export { default } from './UserProfile'; diff --git a/chatter/src/common/constants/initialBottyMessage.js b/chatter/src/common/constants/initialBottyMessage.js new file mode 100644 index 000000000..44f7102f6 --- /dev/null +++ b/chatter/src/common/constants/initialBottyMessage.js @@ -0,0 +1 @@ +export default 'Hi! My name\'s Botty.'; diff --git a/chatter/src/components/ContactPanel/ContactPanel.js b/chatter/src/components/ContactPanel/ContactPanel.js new file mode 100644 index 000000000..33e11383a --- /dev/null +++ b/chatter/src/components/ContactPanel/ContactPanel.js @@ -0,0 +1,52 @@ +import React, { useState } from 'react'; +import cx from 'classnames'; +import './_contact-panel.scss'; + +export default function ContactPanel() { + const [minimised, setMinimised] = useState(Boolean(localStorage.getItem('minimised'))); + + const onClick = () => { + // Remember user preference + localStorage.setItem('minimised', minimised ? '' : 'true'); + + setMinimised(!minimised); + }; + + return ( +
+
+ +
+
+

Botty

+
+
+
+
+

Email

+

botty@reactcodingchallenges.com

+
+
+

Phone

+

0498365942

+
+
+

Labels

+
+

Bot

+

React

+
+
+
+

Attachments

+
+

Dataset.csv

+

bot_face.jpg

+
+

View All

+
+ +
+
+ ); +} diff --git a/chatter/src/components/ContactPanel/_contact-panel.scss b/chatter/src/components/ContactPanel/_contact-panel.scss new file mode 100644 index 000000000..b4ce2412a --- /dev/null +++ b/chatter/src/components/ContactPanel/_contact-panel.scss @@ -0,0 +1,208 @@ +@import 'http://github.com/vars'; + +.contact-panel { + width: 100%; + max-width: 400px; + transition: width 0.5s ease-in-out; + border-left: 1px solid #E4EDEF; + background: white; + display: flex; + flex-direction: column; + + &__body { + padding: 50px; + padding-top: 40px; + display: flex; + flex-direction: column; + flex: 1; + overflow: hidden; + transition: opacity 0.5s ease-in-out; + overflow-y: auto; + + &__edit-btn { + margin-top: auto; + border: 0; + border-radius: 24px; + font-size: 20px; + background: #D9EFFC; + color: #4592DB; + height: 50px; + min-height: 50px; + font-weight: 500; + display: inline-block; + width: 225px; + margin-left: auto; + margin-right: auto; + cursor: pointer; + + &:focus { + outline: none; + } + + &:hover { + background: #bbebff; + } + } + + &__link { + color: #61A7E6; + font-size: 18px; + cursor: pointer; + display: inline-flex; + + &:hover { + color: darken(#61A7E6, 10%); + } + } + + &__labels { + white-space: nowrap; + + > * { + background: #D9EFFC; + height: 35px; + border-radius: 20px; + display: inline-flex; + align-items: center; + padding-left: 20px; + padding-right: 20px; + font-weight: 500; + color: #25455E; + font-size: 16px; + margin-right: 10px; + cursor: pointer; + + &:hover { + background: #bbebff; + } + + > i { + margin-left: 10px; + color: #52A6FA; + } + } + } + + &__attachments { + color: #5E7182; + font-size: 18px; + white-space: nowrap; + + > p > i { + margin-right: 15px; + } + } + + &__block { + margin-bottom: 60px; + } + + &__value { + font-size: 18px; + margin: 0; + margin-top: 10px; + color: #4A5861; + } + + &__label { + letter-spacing: 1.5px; + font-weight: 500; + color: #596872; + font-size: 18px; + margin: 0; + + &:not(:first-of-type) { + margin-top: 20px; + } + } + } + + &__header { + background: #4DB8EF; + padding: 40px; + display: flex; + flex-direction: column; + padding-right: 10px; + padding-top: 10px; + transition: padding 0.5s ease-in-out; + height: 260px; + + &__profile { + color: white; + margin-top: 20px; + transition: opacity 0.5s ease-in-out; + + &__picture { + width: 100px; + height: 100px; + border-radius: 50%; + border: 3px solid white; + display: flex; + align-items: center; + justify-content: center; + font-size: 40px; + } + + > h1 { + font-weight: 400; + margin-left: 10px; + } + } + } + + &__toggle { + font-size: 30px; + margin-left: auto; + color: white; + cursor: pointer; + transition: transform 0.2s ease-in-out; + + @media only screen and (min-width: 1500px) { + display: none; + } + + &:hover { + transform: scale(1.05); + } + + &::before { + transform: none; + } + } + + @media only screen and (max-width: 1500px) { + &--minimised { + width: $contact-panel-min-width !important; + + > .contact-panel { + &__body { + opacity: 0; + } + + &__header { + padding-left: 0; + + .contact-panel { + &__header { + &__profile { + opacity: 0; + } + } + + &__toggle { + &::before { + transform: scale(-1, 1); + } + } + } + } + } + } + } + + @media only screen and (max-width: $mid-breakpoint) { + position: fixed; + right: 0; + width: 100%; + } +} diff --git a/chatter/src/components/ContactPanel/index.js b/chatter/src/components/ContactPanel/index.js new file mode 100644 index 000000000..59a889a3f --- /dev/null +++ b/chatter/src/components/ContactPanel/index.js @@ -0,0 +1 @@ +export { default } from './ContactPanel'; diff --git a/chatter/src/components/Messages/components/Footer.js b/chatter/src/components/Messages/components/Footer.js new file mode 100644 index 000000000..a0f36e352 --- /dev/null +++ b/chatter/src/components/Messages/components/Footer.js @@ -0,0 +1,28 @@ +import React from 'react'; + +const RETURN_KEY_CODE = 13; + +export default function Footer({ sendMessage, onChangeMessage, message }) { + const onKeyDown = ({ keyCode }) => { + if (keyCode !== RETURN_KEY_CODE ) { return; } + + sendMessage(); + } + + return ( +
+ +
+ + + + +
+
+ ); +} diff --git a/chatter/src/components/Messages/components/Header.js b/chatter/src/components/Messages/components/Header.js new file mode 100644 index 000000000..4e6a9b3e7 --- /dev/null +++ b/chatter/src/components/Messages/components/Header.js @@ -0,0 +1,26 @@ +import React from 'react'; +import UserProfile from '../../../common/components/UserProfile'; + +export default function Header() { + return ( +
+
+ +
+

Botty

+

Cloud, The Internet

+
+
+
+
+ +

botty-beep-boop

+
+
+ +

5m

+
+
+
+ ); +} diff --git a/chatter/src/components/Messages/components/Message.js b/chatter/src/components/Messages/components/Message.js new file mode 100644 index 000000000..5b51bd06b --- /dev/null +++ b/chatter/src/components/Messages/components/Message.js @@ -0,0 +1,23 @@ +import React from 'react'; +import cx from 'classnames'; + +const ME = 'me'; + +export default function Message({ nextMessage, message, botTyping }) { + return ( +

+ {message.message} +

+ ); +} diff --git a/chatter/src/components/Messages/components/Messages.js b/chatter/src/components/Messages/components/Messages.js new file mode 100644 index 000000000..4d388a8b0 --- /dev/null +++ b/chatter/src/components/Messages/components/Messages.js @@ -0,0 +1,32 @@ +import React, { useContext } from 'react'; +import io from 'socket.io-client'; +import useSound from 'use-sound'; +import config from '../../../config'; +import LatestMessagesContext from '../../../contexts/LatestMessages/LatestMessages'; +import TypingMessage from './TypingMessage'; +import Header from './Header'; +import Footer from './Footer'; +import Message from './Message'; +import '../styles/_messages.scss'; + +const socket = io( + config.BOT_SERVER_ENDPOINT, + { transports: ['websocket', 'polling', 'flashsocket'] } +); + +function Messages() { + const [playSend] = useSound(config.SEND_AUDIO_URL); + const [playReceive] = useSound(config.RECEIVE_AUDIO_URL); + const { setLatestMessage } = useContext(LatestMessagesContext); + + return ( +
+
+
+
+
+
+ ); +} + +export default Messages; diff --git a/chatter/src/components/Messages/components/TypingMessage.js b/chatter/src/components/Messages/components/TypingMessage.js new file mode 100644 index 000000000..61413b9f8 --- /dev/null +++ b/chatter/src/components/Messages/components/TypingMessage.js @@ -0,0 +1,26 @@ +import React, { useEffect, useState } from 'react'; + +export default function Typing() { + const [numberOfDots, setDots] = useState(1); + + const incrementDots = () => { + setDots(numberOfDots === 3 ? 1 : numberOfDots + 1); + }; + + useEffect(() => { + const timeout = setTimeout(incrementDots, 500); + + return () => { + clearTimeout(timeout); + } + }, [numberOfDots]); + + return ( +

+ {`Typing${''.padStart(numberOfDots, '.')}`} +

+ ); +} diff --git a/chatter/src/components/Messages/index.js b/chatter/src/components/Messages/index.js new file mode 100644 index 000000000..a2260ac0f --- /dev/null +++ b/chatter/src/components/Messages/index.js @@ -0,0 +1 @@ +export { default } from './components/Messages'; diff --git a/chatter/src/components/Messages/styles/_messages.scss b/chatter/src/components/Messages/styles/_messages.scss new file mode 100644 index 000000000..a5b66ee84 --- /dev/null +++ b/chatter/src/components/Messages/styles/_messages.scss @@ -0,0 +1,246 @@ +@import 'http://github.com/vars'; + +.messages { + display: flex; + flex-direction: column; + flex: 1; + + @media only screen and (max-width: $mid-breakpoint) { + margin-right: $contact-panel-min-width; + } + + &__list { + flex: 1; + overflow-y: scroll; + padding: 30px; + display: flex; + flex-direction: column; + } + + &__header { + display: flex; + height: 100px; + background: white; + align-items: center; + padding-left: 30px; + padding-right: 30px; + border-bottom: 1px solid #E4EDEF; + justify-content: space-between; + + @media only screen and (max-width: 700px) { + flex-direction: column; + align-items: flex-start; + height: 150px; + justify-content: center; + } + + &__online-dot { + width: 12px; + height: 12px; + border-radius: 50%; + background: #5BBD57; + margin-left: 10px; + margin-top: 3px; + } + + &__left-content { + &__text { + > h1 { + display: flex; + align-items: center; + margin: 0; + color: #193147; + font-weight: 400; + font-size: 24px; + } + + > p { + margin: 0; + color: #80909B; + font-size: 18px; + font-weight: 500; + margin-top: 5px; + } + } + + > .user-profile { + @media only screen and (max-width: 700px) { + display: none; + } + } + } + + &__status { + font-weight: 500; + display: flex; + align-items: center; + color: #3D5364; + + > i { + margin-right: 10px; + font-size: 20px; + color: #8191A0; + + &.mdi { + font-size: 24px; + } + } + + > p { + font-size: 18px; + } + + &:not(:last-of-type) { + margin-right: 40px; + } + } + + &__left-content, + &__right-content, + &__status { + display: flex; + align-items: center; + } + + &__right-content { + @media only screen and (max-width: 780px) { + flex-direction: column; + align-items: flex-start; + } + + @media only screen and (max-width: 700px) { + margin-top: 10px; + } + } + } + + &__footer { + display: flex; + height: 100px; + background: white; + align-items: center; + padding-left: 30px; + padding-right: 30px; + border-top: 1px solid #E4EDEF; + + @media only screen and (max-width: 700px) { + flex-direction: column; + height: 120px; + justify-content: center; + } + + &__actions { + display: flex; + align-items: center; + + @media only screen and (max-width: 700px) { + margin-left: auto; + } + + > i { + font-size: 25px; + color: #8194A4; + margin-right: 20px; + cursor: pointer; + transition: transform 0.2s ease-in-out; + + &:hover { + color: #3898EB; + transform: scale(1.05); + } + + &.mdi { + font-size: 30px; + } + } + + > button { + background: none; + border: 0; + color: #3898EB; + font-size: 18px; + transition: transform 0.2s ease-in-out; + will-change: transform; + + &:hover:not(:disabled) { + transform: scale(1.05); + } + + &:not(:disabled) { + cursor: pointer; + } + + &:focus { + outline: none !important; + } + + &:disabled { + color: #B7C0CD; + } + } + } + + > input { + flex: 1; + margin-right: 20px; + height: 50px; + border: 0; + + @media only screen and (max-width: 700px) { + width: 100%; + margin-right: 0; + flex: unset; + } + } + } + + &__message { + padding: 20px; + border-radius: 25px; + font-weight: 500; + width: fit-content; + margin-bottom: 10px; + margin-top: 10px; + max-width: 60%; + word-break: break-word; + padding-top: 15px; + padding-bottom: 12px; + + &--last:not(&--me) { + border-bottom-left-radius: 0; + } + + &--last.messages__message--me { + border-bottom-right-radius: 0; + } + + &:last-of-type { + margin-bottom: 0; + } + + ~ .messages__message:not(.messages__message--me) { + margin-bottom: -5px; + } + + &--typing { + min-width: 100px; + border-bottom-left-radius: 0; + } + + &--me { + background: #3898EB; + color: white; + margin-left: auto; + border-bottom-left-radius: 25px; + + ~ .messages__message--me { + margin-top: -5px; + } + } + + &:not(&--me) { + background: white; + color: #29475C; + } + } +} diff --git a/chatter/src/components/UserList/UserList.js b/chatter/src/components/UserList/UserList.js new file mode 100644 index 000000000..1501d2977 --- /dev/null +++ b/chatter/src/components/UserList/UserList.js @@ -0,0 +1,42 @@ +import React, { useContext } from 'react'; +import cx from 'classnames'; +import LatestMessagesContext from '../../contexts/LatestMessages/LatestMessages'; +import UserProfile from '../../common/components/UserProfile/UserProfile'; +import USERS from './constants/users'; +import './_user-list.scss'; + +function User({ icon, name, lastActive, isOnline, userId, color }) { + const { messages } = useContext(LatestMessagesContext); + + return ( +
+ +
+
+

{name}

+

+ {isOnline ? 'Online' : lastActive} +

+
+

{messages[userId]}

+
+
+ ); +} + +export default function UserList() { + return ( +
+
+
+

All Messages

+ +
+ +
+
+ {USERS.map(user => )} +
+
+ ); +} diff --git a/chatter/src/components/UserList/_user-list.scss b/chatter/src/components/UserList/_user-list.scss new file mode 100644 index 000000000..3ebdf0898 --- /dev/null +++ b/chatter/src/components/UserList/_user-list.scss @@ -0,0 +1,157 @@ +@import 'http://github.com/vars'; + +.user-list { + width: 25%; + height: 100%; + border-right: 1px solid #E4EDEF; + overflow: hidden; + max-width: 500px; + background: white; + min-width: 350px; + + @media only screen and (max-width: $small-breakpoint) { + min-width: 100px; + width: fit-content; + } + + &__header { + padding-left: 25px; + padding-right: 25px; + display: flex; + align-items: center; + height: 100px; + border-bottom: 1px solid #F0F4F7; + + @media only screen and (max-width: $small-breakpoint) { + margin-right: auto; + } + + @media only screen and (max-width: 700px) { + height: 150px; + } + + > i { + margin-left: auto; + color: #51A0E5; + font-size: 25px; + cursor: pointer; + + @media only screen and (max-width: $small-breakpoint) { + margin-right: auto; + } + } + + &__left { + display: flex; + align-items: center; + cursor: pointer; + + @media only screen and (max-width: $small-breakpoint) { + display: none; + } + + > i { + font-size: 15px; + color: #8493A5; + margin-left: 10px; + } + + > p { + letter-spacing: 1px; + font-weight: 500; + color: #596872; + font-size: 18px; + margin: 0; + } + } + } + + &__users { + height: 100%; + overflow-y: scroll; + padding-bottom: 100px; + + @media only screen and (max-width: 700px) { + padding-bottom: 150px; + } + + &__user { + height: 120px; + display: flex; + align-items: center; + padding-left: 20px; + padding-right: 20px; + cursor: pointer; + + @media only screen and (max-width: $small-breakpoint) { + > .user-profile { + margin-right: 0; + } + } + + &:hover { + background: #F3F4F6; + } + + &:first-of-type { + background: #EFF8FB; + cursor: default; + } + + &__profile { + + } + + &__right-content { + flex: 1; + width: 0; + + > p { + color: #94A2AE; + margin-bottom: 0; + margin-top: 5px; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + max-width: 60%; + } + + @media only screen and (max-width: $small-breakpoint) { + display: none; + } + + } + + &__title { + display: flex; + + > p { + margin: 0; + } + + > p:last-of-type { + margin-left: auto; + color: #94A2AE; + } + } + + &__online { + background: #83C67E; + color: white !important; + padding: 5px; + padding-left: 10px; + padding-right: 10px; + border-radius: 12px; + font-size: 14px; + letter-spacing: 1px; + text-transform: lowercase; + font-weight: 400; + line-height: 15px; + } + + &:not(:last-of-type) { + border-bottom: 1px solid #F0F4F7; + } + } + } +} diff --git a/chatter/src/components/UserList/constants/users.js b/chatter/src/components/UserList/constants/users.js new file mode 100644 index 000000000..bc5896aed --- /dev/null +++ b/chatter/src/components/UserList/constants/users.js @@ -0,0 +1,13 @@ +export default [ + { name: 'Botty', userId: 'bot', icon: 'fas fa-comment-dots', isOnline: true, color: '#4DB8EF' }, + { name: 'Brandon Andrews', userId: 'brandon', isOnline: false, lastActive: '3 hours go', color: '#DD95BA', lastMessage: 'Hello there!' }, + { name: 'Clayton Day', userId: 'clayton', isOnline: false, lastActive: 'Yesterday', color: '#62D5D1', lastMessage: 'Yes of course. Thanks' }, + { name: 'Bernice Clark', userId: 'bernice', isOnline: true, color: '#82D39F', lastMessage: 'This is a question regarding the' }, + { name: 'Christine Fields', userId: 'christine', isOnline: true, lastActive: 'Jul 28', color: '#FFBB75', lastMessage: 'Do you need help with the price?' }, + { name: 'Mike Morgan', userId: 'mike', isOnline: false, lastActive: 'Jul 27', color: '#F47E64', lastMessage: 'Choose the perfect accommodation' }, + { name: 'Callie Schmidt', userId: 'callie', isOnline: false, lastActive: 'Jul 23', color: '#F57971', lastMessage: 'Yes thanks!' }, + { name: 'Herbert Watkins', userId: 'herbert', isOnline: false, lastActive: 'Jul 23', color: '#B967B9', lastMessage: 'Of course, send as an email to' }, + { name: 'Bessie Coleman', userId: 'bessie', isOnline: false, lastActive: 'Jul 23', color: '#4DB8EF', lastMessage: 'Sorry you couldn\'t read it' }, + { name: 'Lottie Jordan', userId: 'lottie', isOnline: false, lastActive: 'Jul 23', color: '#62D5D1', lastMessage: '728 Feeney Street.' }, + { name: 'Augusta Castillo', userId: 'augusta', isOnline: false, lastActive: 'Jul 23', color: '#82D39F', lastMessage: 'I got the transfer! :D' } +]; diff --git a/chatter/src/components/UserList/index.js b/chatter/src/components/UserList/index.js new file mode 100644 index 000000000..03ef75b2b --- /dev/null +++ b/chatter/src/components/UserList/index.js @@ -0,0 +1 @@ +export { default } from './UserList'; diff --git a/chatter/src/config.js b/chatter/src/config.js new file mode 100644 index 000000000..7635b1f32 --- /dev/null +++ b/chatter/src/config.js @@ -0,0 +1,5 @@ +export default { + BOT_SERVER_ENDPOINT: 'https://botty.alexgurr.com/', + SEND_AUDIO_URL: 'https://puu.sh/GSHJ0/25fae22f76.mp3', + RECEIVE_AUDIO_URL: 'https://puu.sh/GSHIU/df806a9cb8.mp3' +}; diff --git a/chatter/src/contexts/LatestMessages/LatestMessages.js b/chatter/src/contexts/LatestMessages/LatestMessages.js new file mode 100644 index 000000000..7a376e510 --- /dev/null +++ b/chatter/src/contexts/LatestMessages/LatestMessages.js @@ -0,0 +1,20 @@ +import React, { useState, createContext, useCallback } from 'react'; +import initialMessages from './constants/initialMessages'; + +const LatestMessagesContext = createContext({}); + +export default LatestMessagesContext; + +export function LatestMessages({ children }) { + const [messages, setMessages] = useState(initialMessages); + + const setLatestMessage = useCallback((userId, value) => { + setMessages({ ...messages, [userId]: value }); + }, [messages]); + + return ( + + {children} + + ); +} diff --git a/chatter/src/contexts/LatestMessages/constants/initialMessages.js b/chatter/src/contexts/LatestMessages/constants/initialMessages.js new file mode 100644 index 000000000..583aecb73 --- /dev/null +++ b/chatter/src/contexts/LatestMessages/constants/initialMessages.js @@ -0,0 +1,15 @@ +import INITIAL_BOTTY_MESSAGE from '../../../common/constants/initialBottyMessage'; + +export default { + bot: INITIAL_BOTTY_MESSAGE, + brandon: 'Hello there!', + clayton: 'Yes of course. Thanks', + bernice: 'This is a question regarding the fun time we had.', + christine: 'Do you need help with the price?', + mike: 'Choose the perfect accommodation', + callie: 'Yes thanks!', + herbert: 'Of course, send as an email to my address.', + bessie: 'Sorry you couldn\'t read it', + lottie: '728 Feeney Street.', + augusta: 'I got the transfer! :D' +}; diff --git a/chatter/src/contexts/LatestMessages/index.js b/chatter/src/contexts/LatestMessages/index.js new file mode 100644 index 000000000..a13d57866 --- /dev/null +++ b/chatter/src/contexts/LatestMessages/index.js @@ -0,0 +1 @@ +export { default, LatestMessages } from './LatestMessages'; diff --git a/chatter/src/index.js b/chatter/src/index.js new file mode 100644 index 000000000..4a9135e76 --- /dev/null +++ b/chatter/src/index.js @@ -0,0 +1,11 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import App from './App'; +import './_index.scss'; + +ReactDOM.render( + + + , + document.getElementById('root') +); diff --git a/chatter/src/layouts/CoreLayout/components/CoreLayout.js b/chatter/src/layouts/CoreLayout/components/CoreLayout.js new file mode 100644 index 000000000..908778c94 --- /dev/null +++ b/chatter/src/layouts/CoreLayout/components/CoreLayout.js @@ -0,0 +1,20 @@ +import React from 'react'; +import { LatestMessages } from '../../../contexts/LatestMessages/LatestMessages'; +import ContactPanel from '../../../components/ContactPanel'; +import UserList from '../../../components/UserList'; +import Messages from '../../../components/Messages'; +import IconBackground from './IconBackground'; +import '../styles/_core-layout.scss'; + +export default function CoreLayout() { + return ( +
+ + + + + + +
+ ); +} diff --git a/chatter/src/layouts/CoreLayout/components/IconBackground.js b/chatter/src/layouts/CoreLayout/components/IconBackground.js new file mode 100644 index 000000000..8caef381c --- /dev/null +++ b/chatter/src/layouts/CoreLayout/components/IconBackground.js @@ -0,0 +1,51 @@ +import React from 'react'; +import ICONS from '../constants/icons'; +import '../styles/_icon-background.scss'; + +const SPACING_PX = 125; +const SPACING_MARGIN = SPACING_PX / 4; + +function getRandomNumber(min, max) { + return Math.floor(Math.random() * (max - min)) + min; +} + +function getRandomIcon() { + return ICONS[getRandomNumber(0, ICONS.length)]; +} + +function IconRow({ numberOfIcons }) { + return ( +
+ {[...new Array(numberOfIcons)].map(() => { + const icon = getRandomIcon(); + + return ( +