├── .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 
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 
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 
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 🤖  
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 | 
56 |
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 |
24 |
25 |
26 |
Email
27 |
botty@reactcodingchallenges.com
28 |
29 |
30 |
Phone
31 |
0498365942
32 |
33 |
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 |
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 |
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 |
33 | );
34 | })}
35 |
36 | );
37 | }
38 |
39 | export default function IconBackground() {
40 | const { height, width } = document.body.getBoundingClientRect();
41 | const numberOfElsPerRow = parseInt((width / SPACING_PX).toFixed());
42 | const numberOfRows = parseInt((height / SPACING_PX).toFixed());
43 |
44 | return (
45 |
46 | {[...new Array(numberOfRows)].map(() => (
47 |
48 | ))}
49 |
50 | )
51 | }
52 |
--------------------------------------------------------------------------------
/chatter/src/layouts/CoreLayout/constants/icons.js:
--------------------------------------------------------------------------------
1 | export default [
2 | { name: 'far fa-circle', maxSize: 40 },
3 | { name: 'fas fa-circle', minSize: 7, maxSize: 10 },
4 | { name: 'far fa-star', maxSize: 40 },
5 | { name: 'fas fa-square', minSize: 7, maxSize: 10 },
6 | { name: 'far fa-square', maxSize: 40 },
7 | { name: 'mdi mdi-message-outline', minSize: 20, maxSize: 40, noRotation: true },
8 | { name: 'mdi mdi-triangle-wave', minSize: 20, maxSize: 40 },
9 | { name: 'mdi mdi-hexagon-outline', maxSize: 40 },
10 | { name: 'mdi mdi-triangle-outline', minSize: 20, maxSize: 40 }
11 | ];
12 |
--------------------------------------------------------------------------------
/chatter/src/layouts/CoreLayout/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './components/CoreLayout';
2 |
--------------------------------------------------------------------------------
/chatter/src/layouts/CoreLayout/styles/_core-layout.scss:
--------------------------------------------------------------------------------
1 | .core {
2 | display: flex;
3 | align-items: center;
4 | height: 100%;
5 | background: #EFF8FB;
6 | width: 100vw;
7 | overflow-x: hidden;
8 |
9 | > * {
10 | height: 100%;
11 | z-index: 2;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/chatter/src/layouts/CoreLayout/styles/_icon-background.scss:
--------------------------------------------------------------------------------
1 | .icon-background {
2 | position: absolute;
3 | display: flex;
4 | flex-direction: column;
5 | justify-content: space-between;
6 | width: 100%;
7 | height: 100%;
8 | overflow: hidden;
9 |
10 | &__row {
11 | display: flex;
12 | justify-content: space-between;
13 |
14 | > i {
15 | color: #E5EEF2;
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/chatter/src/styles/_vars.scss:
--------------------------------------------------------------------------------
1 | // Media breakpoints
2 |
3 | $small-breakpoint: 1000px;
4 | $mid-breakpoint: 1200px;
5 |
6 | // Other
7 |
8 | $contact-panel-min-width: 50px;
9 |
--------------------------------------------------------------------------------
/dark-mode/README.md:
--------------------------------------------------------------------------------
1 | # Dark Mode Coding Challenge 🌙  
2 |
3 |
4 | # Goals / Outcomes ✨
5 | - Using state and global state
6 | - DOM manipulation
7 |
8 |
9 | # Pre-requisites ✅
10 | None
11 |
12 |
13 | # Requirements 📖
14 | - Add dark-mode switching functionality to the *existing* dark-mode button
15 | - Utilise the *existing* dark-mode scss file by adding a `dark-mode` class to the root `html` element
16 | - When in Dark mode:
17 | - The button icon should be `faSun`
18 | - The button icon colour should be `(#FFA500)`. You can use the `color` prop on the `Icon` component.
19 |
20 |
21 | # Think about 💡
22 | - How we would use Dark mode on other potential routes/components in a bigger application. Would your solution work for this?
23 | - How we can apply a class to the `html` DOM element
24 |
25 |
26 |
27 | # What's Already Been Done 🏁
28 | - Basic app UI (mobile responsive)
29 | - Dark mode and light mode styles/themes
30 |
31 |
32 | # Screenshots 🌄
33 |
34 | 
35 | 
36 |
--------------------------------------------------------------------------------
/dark-mode/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dark-mode",
3 | "version": "1.0.0",
4 | "private": true,
5 | "engines": {
6 | "node" : ">= 15.0.0"
7 | },
8 | "dependencies": {
9 | "@fortawesome/fontawesome-svg-core": "^1.2.35",
10 | "@fortawesome/free-solid-svg-icons": "^5.15.3",
11 | "@fortawesome/react-fontawesome": "^0.1.14",
12 | "bulma": "^0.8.2",
13 | "sass": "^1.43.4",
14 | "react": "^17.0.2",
15 | "react-dom": "^17.0.2",
16 | "react-scripts": "4.0.3"
17 | },
18 | "scripts": {
19 | "start": "react-scripts start",
20 | "build": "react-scripts build",
21 | "test": "react-scripts test",
22 | "eject": "react-scripts eject"
23 | },
24 | "eslintConfig": {
25 | "extends": "react-app"
26 | },
27 | "browserslist": {
28 | "production": [
29 | ">0.2%",
30 | "not dead",
31 | "not op_mini all"
32 | ],
33 | "development": [
34 | "last 1 chrome version",
35 | "last 1 firefox version",
36 | "last 1 safari version"
37 | ]
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/dark-mode/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexgurr/react-coding-challenges/fc446a3dadc2aca69b7e55fc72ed62b413d382b4/dark-mode/public/favicon.ico
--------------------------------------------------------------------------------
/dark-mode/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexgurr/react-coding-challenges/fc446a3dadc2aca69b7e55fc72ed62b413d382b4/dark-mode/public/favicon.png
--------------------------------------------------------------------------------
/dark-mode/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | Dark Mode
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/dark-mode/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexgurr/react-coding-challenges/fc446a3dadc2aca69b7e55fc72ed62b413d382b4/dark-mode/public/logo192.png
--------------------------------------------------------------------------------
/dark-mode/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexgurr/react-coding-challenges/fc446a3dadc2aca69b7e55fc72ed62b413d382b4/dark-mode/public/logo512.png
--------------------------------------------------------------------------------
/dark-mode/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 |
--------------------------------------------------------------------------------
/dark-mode/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/dark-mode/src/common/containers/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function App({ children }) {
4 | return children;
5 | }
6 |
--------------------------------------------------------------------------------
/dark-mode/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import AppContainer from './common/containers/App';
4 | import './styles/_main.scss';
5 | import Routes from './routes';
6 |
7 | ReactDOM.render(
8 |
9 |
10 | ,
11 | document.getElementById('root')
12 | );
13 |
--------------------------------------------------------------------------------
/dark-mode/src/routes/App/components/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
3 | import { faMoon } from '@fortawesome/free-solid-svg-icons';
4 | import '../styles/_app.scss';
5 |
6 | function App() {
7 | return (
8 |
9 |
10 |
11 |
Dark Mode Challenge
12 |
13 |
14 | {/* --The button that should toggle dark mode-- */}
15 |
18 |
19 |
20 |
21 |
22 |
23 |
Lollipop powder powder. Cotton candy caramels chupa chups halvah muffin caramels apple pie topping cake. Topping chocolate bar pastry chocolate cake. Cupcake tart jujubes dragée jelly-o icing sugar plum. Chocolate bar lollipop candy canes. Biscuit croissant apple pie pudding caramels wafer tart tootsie roll macaroon. Croissant tiramisu chocolate bar carrot cake lemon drops halvah.
24 |
25 |
26 |
Marshmallow tiramisu liquorice bear claw chocolate bar bear claw tart. Muffin chupa chups pie. Brownie apple pie topping lemon drops marzipan toffee. Pudding macaroon icing ice cream bonbon cake tart. Pudding sugar plum chocolate cake cake biscuit pastry pastry chocolate bar tart. Lemon drops dessert gummies icing.
27 |
28 |
29 |
30 |
35 |
36 |
41 |
42 |
48 |
49 | );
50 | }
51 |
52 | export default App;
53 |
--------------------------------------------------------------------------------
/dark-mode/src/routes/App/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './components/App';
2 |
--------------------------------------------------------------------------------
/dark-mode/src/routes/App/styles/_app.scss:
--------------------------------------------------------------------------------
1 | .app {
2 | padding: 10%;
3 | max-width: 1000px;
4 | justify-content: center;
5 | display: flex;
6 | flex-direction: column;
7 | margin: auto;
8 | box-sizing: content-box;
9 |
10 | &__dark-mode-btn {
11 | border: 0;
12 | background: transparent;
13 | font-size: 30px;
14 | width: 30px;
15 | height: 30px;
16 | display: flex;
17 | align-items: center;
18 | justify-content: center;
19 | cursor: pointer;
20 |
21 | &:focus {
22 | outline: none;
23 | }
24 | }
25 |
26 | .buttons > .button {
27 | height: 30px;
28 | width: 100px;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/dark-mode/src/routes/index.js:
--------------------------------------------------------------------------------
1 | import Home from './App';
2 |
3 | // Use something like react-router-dom to manage multiple pages/routes
4 |
5 | export default Home;
6 |
--------------------------------------------------------------------------------
/dark-mode/src/styles/_dark-mode.scss:
--------------------------------------------------------------------------------
1 | .dark-mode {
2 | background: #121212;
3 |
4 | p {
5 | color: white;
6 | opacity: 87%;
7 | }
8 |
9 | .title {
10 | color: #BB86FC;
11 | }
12 |
13 | .subtitle {
14 | color: #03DAC6;
15 | }
16 |
17 | .is-primary {
18 | background-color: #BB86FC;
19 | color: black;
20 | }
21 |
22 | .is-primary:hover,
23 | .is-primary:focus {
24 | background-color: #BB86FC;
25 | opacity: 75%;
26 | color: black;
27 | }
28 |
29 | .is-link {
30 | background-color: #03DAC6;
31 | color: black;
32 | }
33 |
34 | .is-link:hover,
35 | .is-link:focus {
36 | background-color: #03DAC6;
37 | opacity: 75%;
38 | color: black;
39 | }
40 |
41 | input {
42 | background: rgba(255, 255, 255, 0.2);
43 | color: white;
44 |
45 | &::placeholder {
46 | color: white;
47 | }
48 |
49 | &:hover {
50 | border-color: rgba(255, 255, 255, 0.4);
51 | }
52 |
53 | &:focus {
54 | border-color: rgba(255, 255, 255, 0.6);
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/dark-mode/src/styles/_main.scss:
--------------------------------------------------------------------------------
1 | @charset "utf-8";
2 |
3 | // Import a Google Font
4 | @import url('https://fonts.googleapis.com/css?family=Nunito:400,700');
5 |
6 | // Set your brand colors
7 | $purple: #8a4d76;
8 | $pink: #fa7c91;
9 | $brown: #757763;
10 | $beige-light: #d0d1cd;
11 | $beige-lighter: #eff0eb;
12 |
13 | // Update Bulma's global variables
14 | $family-sans-serif: 'Nunito', sans-serif;
15 | $grey-dark: $brown;
16 | $grey-light: $beige-light;
17 | $primary: $purple;
18 | $link: $pink;
19 | $widescreen-enabled: false;
20 | $fullhd-enabled: false;
21 |
22 | // Update some of Bulma's component variables
23 | $body-background-color: $beige-lighter;
24 | $control-border-width: 2px;
25 | $input-border-color: transparent;
26 | $input-shadow: none;
27 |
28 | // Import only what you need from Bulma
29 | @import '~bulma/bulma.sass';
30 | @import './dark-mode';
31 |
--------------------------------------------------------------------------------
/issue_template.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ## Expected behaviour
4 |
5 |
6 | ## Actual behaviour
7 |
8 |
9 | ## I'm seeing this behaviour on
10 |
11 |
12 | - [ ] Desktop
13 | - [ ] Mobile
14 | - [ ] N/A
15 |
16 | ## OS
17 |
18 |
19 | ## I'm not a dummy, so I've checked these
20 | - [ ] I've run `yarn` / `npm install`.
21 | - [ ] I'm using an up to date nodeJS version.
22 | - [ ] My issue doesn't already exist
23 | - [ ] I'm not on an M1 Mac, or if I am, I googled the issue first.
24 |
25 | ## So how can we reproduce this?
26 |
27 |
28 | ### Awesome ⭐⭐⭐⭐⭐
29 | Provide a (link to a) minimal demo app showing the faulty behaviour.
30 |
31 | ### Sweet ⭐⭐⭐⭐
32 | Provide a concise code sample.
33 |
34 | ### Good ⭐⭐⭐
35 | Provide your own app and instructions how to reproduce the issue.
36 |
37 | ### Meh ⭐⭐
38 | Provide a code sample with a bunch of magic parameters which I need to interpolate by guessing to reconstruct the actual code.
39 |
40 | ### Worst 💩
41 | Say the source code can't be disclosed and refuse to provide any of the above. Expect this issue to be closed by a bunch of angry aliens 👽👽👽👽👽 that will hunt you down and 🔥 your 🖥. You've been warned 🚒.
42 |
--------------------------------------------------------------------------------
/new_challenge_template.md:
--------------------------------------------------------------------------------
1 | ## Challenge Name 📝
2 |
3 |
4 | ## How Hard Do You Think The challenge Is? 🔥
5 |
6 |
7 | - [ ] Easy
8 | - [ ] Medium
9 | - [ ] Hard
10 |
11 | ## Describe This Challenge ✍️
12 |
13 |
14 |
15 |
16 | ## Challenge Screenshots 🌄
17 |
18 |
19 | ## Challenge One Liner 💬
20 |
21 |
22 | ## Link To The Challenge Source Code 🔗
23 |
24 |
25 | ## Link To The Solution Source Code 🔗
26 |
27 |
28 | ## Challenge Checklist ✅
29 |
30 |
31 | - [ ] My app is responsive
32 | - [ ] All core UI/UX has been built
33 | - [ ] I have built the solution to a good standard
34 | - [ ] I have added a README file with clear requirements and prerequisites
35 | - [ ] I have checked out the solution and made sure it runs
36 | - [ ] My challenge uses the latest version of ReactJS
37 |
--------------------------------------------------------------------------------
/pull_request_template.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ## Proposed changes
4 |
5 |
6 | ## Types of changes
7 |
8 |
9 |
10 | - [ ] Bug fix (non-breaking change which fixes an issue)
11 | - [ ] New feature (non-breaking change which adds functionality)
12 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
13 | - [ ] Documentation Update (if none of the other choices apply)
14 |
15 | ## Further comments
16 |
17 |
18 |
--------------------------------------------------------------------------------
/rocket-ship/README.md:
--------------------------------------------------------------------------------
1 | # Rocket Ship Coding Challenge 🚀  
2 |
3 |
4 | ## Goals / Outcomes ✨
5 | - To test basic understanding of render lifecycles in both functional and class components
6 |
7 |
8 | ## Pre-requisites ✅
9 | None
10 |
11 |
12 | ## Requirements
13 | - Stop the Class rocket from taking off
14 | - Stop the Functional rocket from taking off
15 |
16 |
17 | ## Think about 💡
18 | - How we prevent components from re-rendering
19 |
20 |
21 | ## What's Already Been Done 🏁
22 | - Functional and class rocket components
23 | - UI/UX and animation
24 |
25 |
26 | ## Screenshots 🌄
27 |
28 | 
29 |
--------------------------------------------------------------------------------
/rocket-ship/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rocket-ship",
3 | "version": "1.0.0",
4 | "engines": {
5 | "node" : ">= 15.0.0"
6 | },
7 | "dependencies": {
8 | "sass": "^1.43.4",
9 | "react": "^17.0.2",
10 | "react-dom": "^17.0.2",
11 | "react-scripts": "4.0.3"
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 |
--------------------------------------------------------------------------------
/rocket-ship/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexgurr/react-coding-challenges/fc446a3dadc2aca69b7e55fc72ed62b413d382b4/rocket-ship/public/favicon.ico
--------------------------------------------------------------------------------
/rocket-ship/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexgurr/react-coding-challenges/fc446a3dadc2aca69b7e55fc72ed62b413d382b4/rocket-ship/public/favicon.png
--------------------------------------------------------------------------------
/rocket-ship/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | Rocket Ship
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/rocket-ship/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexgurr/react-coding-challenges/fc446a3dadc2aca69b7e55fc72ed62b413d382b4/rocket-ship/public/logo192.png
--------------------------------------------------------------------------------
/rocket-ship/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexgurr/react-coding-challenges/fc446a3dadc2aca69b7e55fc72ed62b413d382b4/rocket-ship/public/logo512.png
--------------------------------------------------------------------------------
/rocket-ship/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 |
--------------------------------------------------------------------------------
/rocket-ship/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/rocket-ship/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './styles/_main.scss';
4 | import Routes from './routes';
5 |
6 | ReactDOM.render(
7 | ,
8 | document.getElementById('root')
9 | );
10 |
--------------------------------------------------------------------------------
/rocket-ship/src/routes/LaunchPad/components/LaunchPad.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { ClassRocket, FunctionalRocket } from './Rocket';
3 | import '../styles/_launchpad.scss';
4 |
5 | export default function LaunchPad() {
6 | const [, triggerRerender] = useState(Date.now());
7 |
8 | useEffect(() => {
9 | setInterval(() => { triggerRerender(Date.now()); }, 500);
10 | }, [])
11 |
12 | return (
13 |
14 |
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/rocket-ship/src/routes/LaunchPad/components/Rocket/components/Rocket.js:
--------------------------------------------------------------------------------
1 | import React, { useState, Component } from 'react';
2 | import RocketCore from './RocketCore';
3 |
4 | export function FunctionalRocket() {
5 | const [initialLaunchTime] = useState(Date.now());
6 |
7 | return ;
8 | }
9 |
10 | export class ClassRocket extends Component {
11 | constructor(props) {
12 | super(props);
13 |
14 | this.state = {
15 | initialLaunchTime: Date.now()
16 | };
17 | }
18 |
19 | render() {
20 | const { initialLaunchTime } = this.state;
21 |
22 | return ;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/rocket-ship/src/routes/LaunchPad/components/Rocket/components/RocketCore.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import '../styles/_rocket.scss';
3 |
4 | const SECONDS_TO_TAKEOFF = 5;
5 | const MS_TO_TAKEOFF = SECONDS_TO_TAKEOFF * 1000;
6 | const FINAL_POSITION_BOTTOM_VAL = 'calc(400px)';
7 |
8 | function timeToPositionPercent(startTime) {
9 | const now = Date.now();
10 | const timeDiff = now - startTime;
11 |
12 | if (timeDiff >= MS_TO_TAKEOFF) { return FINAL_POSITION_BOTTOM_VAL; }
13 |
14 | return `calc(300px + ${((timeDiff / MS_TO_TAKEOFF) * 100).toFixed(0)}%)`;
15 | }
16 |
17 | function generateEmptyListEls(quantity) {
18 | return [...Array(quantity)].map(() => );
19 | }
20 |
21 | export default function RocketCore({ initialLaunchTime }) {
22 | return (
23 | <>
24 |
25 |
31 |
32 |
33 | {generateEmptyListEls(9)}
34 |
35 |
36 |
37 | {generateEmptyListEls(7)}
38 |
39 | >
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/rocket-ship/src/routes/LaunchPad/components/Rocket/index.js:
--------------------------------------------------------------------------------
1 | export { FunctionalRocket, ClassRocket } from './components/Rocket';
2 |
--------------------------------------------------------------------------------
/rocket-ship/src/routes/LaunchPad/components/Rocket/styles/_rocket.scss:
--------------------------------------------------------------------------------
1 | $white: #f5f5f5;
2 | $lightgrey: #dadada;
3 | $midgrey: #b4b2b2;
4 | $darkgrey: #554842;
5 | $red: #f01a19;
6 | $darkred: #a75248;
7 |
8 | // Kudos to https://codepen.io/eva_trostlos for the amazing CSS rocket
9 | .rocket {
10 | position: absolute;
11 | width: 80px;
12 | left: calc(50% - 60px);
13 | transition: bottom linear 0.5s;
14 | z-index: 2;
15 |
16 | &__body {
17 | width: 80px;
18 | left: calc(50% - 50px);
19 | animation: bounce 0.5s infinite;
20 |
21 | &:before {
22 | content: '';
23 | position: absolute;
24 | left: calc(50% - 24px);
25 | width: 48px;
26 | height: 13px;
27 | background-color: $darkgrey;
28 | bottom: -13px;
29 | border-bottom-right-radius: 60%;
30 | border-bottom-left-radius: 60%;
31 | }
32 |
33 | &__main {
34 | background-color: $lightgrey;
35 | height: 180px;
36 | left: calc(50% - 50px);
37 | border-top-right-radius: 100%;
38 | border-top-left-radius: 100%;
39 | border-bottom-left-radius: 50%;
40 | border-bottom-right-radius: 50%;
41 | border-top: 5px solid $white;
42 | }
43 |
44 | &__window {
45 | position: absolute;
46 | width: 50px;
47 | height: 50px;
48 | border-radius: 100%;
49 | background-color: $darkred;
50 | left: calc(50% - 25px);
51 | top: 50px;
52 | border: 5px solid $midgrey;
53 | box-sizing: border-box;
54 | }
55 |
56 | &__fin {
57 | position: absolute;
58 | z-index: -100;
59 | height: 55px;
60 | width: 50px;
61 | background-color: $darkred;
62 |
63 | &__left {
64 | left: -30px;
65 | top: calc(100% - 55px);
66 | border-top-left-radius: 80%;
67 | border-bottom-left-radius: 20%;
68 | }
69 |
70 | &__right {
71 | right: -30px;
72 | top: calc(100% - 55px);
73 | border-top-right-radius: 80%;
74 | border-bottom-right-radius: 20%;
75 | }
76 | }
77 | }
78 |
79 | &__exhaust {
80 | &__flame {
81 | position: absolute;
82 | width: 28px;
83 | background: linear-gradient(to bottom, transparent 10%, $white 100%);
84 | height: 150px;
85 | left: calc(50% - 14px);
86 | animation: exhaust 0.2s infinite;
87 | }
88 |
89 | &__fumes {
90 | li {
91 | width: 60px;
92 | height: 60px;
93 | background-color: $white;
94 | list-style: none;
95 | position: absolute;
96 | border-radius: 100%;
97 |
98 | &:first-child {
99 | width: 200px;
100 | height: 200px;
101 | top: 300px;
102 | animation: fumes 5s infinite;
103 | }
104 |
105 | &:nth-child(2) {
106 | width: 150px;
107 | height: 150px;
108 | left: -120px;
109 | top: 260px;
110 | animation: fumes 3.2s infinite;
111 | }
112 |
113 | &:nth-child(3) {
114 | width: 120px;
115 | height: 120px;
116 | left: -40px;
117 | top: 330px;
118 | animation: fumes 3s 1s infinite;
119 | }
120 |
121 | &:nth-child(4) {
122 | width: 100px;
123 | height: 100px;
124 | left: -170px;
125 | animation: fumes 4s 2s infinite;
126 | top: 380px;
127 | }
128 |
129 | &:nth-child(5) {
130 | width: 130px;
131 | height: 130px;
132 | left: -120px;
133 | top: 350px;
134 | animation: fumes 5s infinite;
135 | }
136 |
137 | &:nth-child(6) {
138 | width: 200px;
139 | height: 200px;
140 | left: -60px;
141 | top: 280px;
142 | animation: fumes2 10s infinite;
143 | }
144 |
145 | &:nth-child(7) {
146 | width: 100px;
147 | height: 100px;
148 | left: -100px;
149 | top: 320px;
150 | }
151 |
152 | &:nth-child(8) {
153 | width: 110px;
154 | height: 110px;
155 | left: 70px;
156 | top: 340px;
157 | }
158 |
159 | &:nth-child(9) {
160 | width: 90px;
161 | height: 90px;
162 | left: 150px;
163 | top: 380px;
164 | animation: fumes 20s infinite;
165 | }
166 | }
167 | }
168 | }
169 | }
170 |
171 | .stars {
172 | width: 50%;
173 | height: 50%;
174 | position: relative;
175 | justify-content: center;
176 | display: flex;
177 | align-items: center;
178 |
179 | li {
180 | list-style: none;
181 | position: absolute;
182 |
183 | &:before,
184 | &:after {
185 | content: '';
186 | position: absolute;
187 | background-color: $white;
188 | }
189 |
190 | &:before {
191 | width: 10px;
192 | height: 2px;
193 | border-radius: 50%;
194 | }
195 |
196 | &:after {
197 | height: 8px;
198 | width: 2px;
199 | left: 4px;
200 | top: -3px;
201 | }
202 |
203 | &:first-child {
204 | top: -30px;
205 | left: -210px;
206 | animation: twinkle 0.4s infinite;
207 | }
208 |
209 | &:nth-child(2) {
210 | top: 0;
211 | left: 60px;
212 | animation: twinkle 0.5s infinite;
213 |
214 | &:before {
215 | height: 1px;
216 | width: 5px;
217 | }
218 | &:after {
219 | width: 1px;
220 | height: 5px;
221 | top: -2px;
222 | left: 2px;
223 | }
224 | }
225 |
226 | &:nth-child(3) {
227 | left: 120px;
228 | top: 220px;
229 | animation: twinkle 1s infinite;
230 | }
231 |
232 | &:nth-child(4) {
233 | left: -100px;
234 | top: 200px;
235 | animation: twinkle 0.5s ease infinite;
236 | }
237 |
238 | &:nth-child(5) {
239 | left: 170px;
240 | top: 100px;
241 | animation: twinkle 0.4s ease infinite;
242 | }
243 |
244 | &:nth-child(6) {
245 | top: 87px;
246 | left: -79px;
247 | animation: twinkle 0.2s infinite;
248 | &:before {
249 | height: 1px;
250 | width: 5px;
251 | }
252 | &:after {
253 | width: 1px;
254 | height: 5px;
255 | top: -2px;
256 | left: 2px;
257 | }
258 | }
259 | }
260 | }
261 |
262 | @keyframes fumes {
263 | 50% {
264 | transform: scale(1.5);
265 | background-color: transparent;
266 | }
267 | 51% {
268 | transform: scale(0.8);
269 | }
270 | 100% {
271 | background-color: $white;
272 | transform: scale(1)
273 | }
274 | }
275 |
276 | @keyframes bounce {
277 | 0% {
278 | transform: translate3d(0px, 0px, 0);
279 | }
280 | 50% {
281 | transform: translate3d(0px, -4px, 0);
282 | }
283 | 100% {
284 | transform: translate3d(0px, 0px, 0);
285 | }
286 | }
287 |
288 | @keyframes exhaust {
289 | 0% {
290 | background: linear-gradient(to bottom, transparent 10%, $white 100%);
291 | }
292 | 50% {
293 | background: linear-gradient(to bottom, transparent 8%, $white 100%);
294 | }
295 | 75% {
296 | background: linear-gradient(to bottom, transparent 12%, $white 100%);
297 | }
298 | }
299 |
300 | @keyframes fumes2 {
301 | 50% {
302 | transform: scale(1.1);
303 | }
304 | }
305 |
306 | @keyframes twinkle {
307 | 80% {
308 | transform: scale(1.1);
309 | opacity: 0.7;
310 | }
311 | }
312 |
--------------------------------------------------------------------------------
/rocket-ship/src/routes/LaunchPad/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './components/LaunchPad';
2 |
--------------------------------------------------------------------------------
/rocket-ship/src/routes/LaunchPad/styles/_launchpad.scss:
--------------------------------------------------------------------------------
1 | $white: #ffffff;
2 | $midgrey: #b4b2b2;
3 |
4 | .launchpad {
5 | height: 100vh;
6 | width: 100vw;
7 | background: linear-gradient(to bottom, $midgrey 0%, $midgrey 70%, $white 100%);
8 | overflow: hidden;
9 | display: flex;
10 | align-items: center;
11 | justify-content: center;
12 | }
13 |
--------------------------------------------------------------------------------
/rocket-ship/src/routes/index.js:
--------------------------------------------------------------------------------
1 | import LaunchPad from './LaunchPad';
2 |
3 | // Use something like react-router-dom to manage multiple pages/routes
4 |
5 | export default LaunchPad;
6 |
--------------------------------------------------------------------------------
/rocket-ship/src/styles/_main.scss:
--------------------------------------------------------------------------------
1 | @charset "utf-8";
2 |
3 | html,
4 | #root,
5 | body {
6 | height: 100%;
7 | overflow: hidden;
8 | }
9 |
--------------------------------------------------------------------------------
/spootify/README.md:
--------------------------------------------------------------------------------
1 | # Spootify Coding Challenge 🎧  
2 |
3 |
4 | # Goals/Outcomes ✨
5 | - To test knowledge of consuming APIs and handling responses
6 | - Loading state and knowing where and how to make multiple API calls efficiently
7 |
8 |
9 | # Pre-requisites ✅
10 | - Add your Spotify client ID & secret to a `.env` file in root using the environment variables `REACT_APP_SPOTIFY_CLIENT_ID` and `REACT_APP_SPOTIFY_CLIENT_SECRET`
11 | - Note. **Never add this type of config to version control. This would usually come from your build server.**
12 |
13 |
14 | # Requirements 📖
15 | - Fetch and display *Released This Week* songs
16 | - Use the API path `new-releases`
17 | - Fetch and display *Featured Playlists*
18 | - Use the API path `featured-playlists`
19 | - Fetch and display *Browse* genres
20 | - Use the API path `categories`
21 | - Loading state/UI *(optional, current UX is already clean)*
22 |
23 |
24 | # Think about 💡
25 | - Taking a look at the Spotify API documentation
26 | - Do you resolve each API request one after the other or in parallel?
27 | - Where do you make the API requests?
28 | - How much logic do you offload out of the UI components?
29 |
30 |
31 | # What's Already Been Done 🏁
32 | - UI/UX for all elements, including previews (mobile responsive)
33 |
34 |
35 | # Screenshots 🌄
36 |
37 | 
38 |
39 |
--------------------------------------------------------------------------------
/spootify/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "author": "Alex Gurr",
3 | "name": "spootify",
4 | "version": "1.0.0",
5 | "private": true,
6 | "engines": {
7 | "node" : ">= 15.0.0"
8 | },
9 | "dependencies": {
10 | "@fortawesome/fontawesome-svg-core": "^1.2.35",
11 | "@fortawesome/free-solid-svg-icons": "^5.15.3",
12 | "@fortawesome/react-fontawesome": "^0.1.14",
13 | "animate.css": "^4.1.1",
14 | "axios": "^0.21.1",
15 | "classnames": "^2.2.6",
16 | "sass": "^1.43.4",
17 | "react": "^17.0.2",
18 | "react-dom": "^17.0.2",
19 | "react-scripts": "4.0.3"
20 | },
21 | "scripts": {
22 | "start": "react-scripts start",
23 | "build": "react-scripts build",
24 | "eject": "react-scripts eject"
25 | },
26 | "eslintConfig": {
27 | "extends": "react-app"
28 | },
29 | "browserslist": {
30 | "production": [
31 | ">0.2%",
32 | "not dead",
33 | "not op_mini all"
34 | ],
35 | "development": [
36 | "last 1 chrome version",
37 | "last 1 firefox version",
38 | "last 1 safari version"
39 | ]
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/spootify/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexgurr/react-coding-challenges/fc446a3dadc2aca69b7e55fc72ed62b413d382b4/spootify/public/favicon.ico
--------------------------------------------------------------------------------
/spootify/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | Spootify
28 |
46 |
47 |
48 |
49 |
50 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/spootify/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexgurr/react-coding-challenges/fc446a3dadc2aca69b7e55fc72ed62b413d382b4/spootify/public/logo192.png
--------------------------------------------------------------------------------
/spootify/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexgurr/react-coding-challenges/fc446a3dadc2aca69b7e55fc72ed62b413d382b4/spootify/public/logo512.png
--------------------------------------------------------------------------------
/spootify/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
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 |
--------------------------------------------------------------------------------
/spootify/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/spootify/src/assets/images/avatar.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spootify/src/assets/images/hero.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/spootify/src/common/components/Header/Header.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
3 | import { faMoon, faSun } from '@fortawesome/free-solid-svg-icons';
4 | import { ReactComponent as Hero } from '../../../assets/images/hero.svg';
5 | import './_header.scss';
6 |
7 | export default function Header() {
8 | return (
9 |
10 |
11 |
12 |
Your favourite tunes
13 | All and all
14 |
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/spootify/src/common/components/Header/_header.scss:
--------------------------------------------------------------------------------
1 | @import '../../../styles/vars';
2 |
3 | .header {
4 | height: 300px;
5 | background: $secondary-orange;
6 | overflow: hidden;
7 | display: flex;
8 | align-items: center;
9 | color: white;
10 | letter-spacing: 1px;
11 | font-weight: 300;
12 | justify-content: space-between;
13 | padding-right: 5%;
14 | min-height: 300px;
15 | margin-bottom: 50px;
16 | user-select: none;
17 |
18 | @media only screen and (max-height: 775px) {
19 | height: 100px;
20 | min-height: 100px;
21 | margin-bottom: 30px;
22 | }
23 |
24 | @media only screen and (max-width: 750px) {
25 | margin-bottom: 20px;
26 | }
27 |
28 | @media only screen and (max-width: 1300px) {
29 | padding-left: 5%;
30 | padding-right: unset;
31 | }
32 |
33 | @media only screen and (min-width: 415px) and (max-width: 600px) {
34 | padding: 20px !important;
35 | height: 100px;
36 | min-height: 100px;
37 | }
38 |
39 | @media only screen and (max-width: 415px) {
40 | padding: 20px !important;
41 | height: auto;
42 | min-height: 100px;
43 | }
44 |
45 | > div {
46 | margin-left: auto;
47 |
48 | @media only screen and (max-width: 1300px) {
49 | margin-left: unset;
50 | }
51 |
52 | @media only screen and (max-width: 650px) {
53 | margin-left: auto;
54 | margin-right: auto;
55 | }
56 |
57 | h1, h2 {
58 | font-weight: 500;
59 | text-shadow: $box-shadow;
60 | text-align: right;
61 |
62 | @media only screen and (max-width: 1300px) {
63 | text-align: unset;
64 | }
65 | }
66 |
67 | h1 {
68 | font-size: 60px;
69 | margin-bottom: 10px;
70 |
71 | @media only screen and (max-width: 600px) {
72 | font-size: 24px;
73 | }
74 |
75 | @media only screen and (max-height: 775px) {
76 | font-size: 24px;
77 | }
78 | }
79 |
80 | h2 {
81 | font-size: 45px;
82 | display: flex;
83 | align-items: center;
84 | justify-content: flex-end;
85 | margin-top: 0;
86 |
87 | @media only screen and (max-width: 1300px) {
88 | justify-content: unset;
89 | }
90 |
91 | @media only screen and (max-width: 600px) {
92 | font-size: 20px;
93 | }
94 |
95 | @media only screen and (max-height: 775px) {
96 | font-size: 20px;
97 | }
98 |
99 | > svg {
100 | margin-left: 10px;
101 | margin-right: 10px;
102 |
103 | &:first-of-type {
104 | color: #FFD60B;
105 | }
106 | }
107 |
108 | > svg:last-of-type {
109 | color: $deep-grey;
110 | }
111 | }
112 | }
113 |
114 | > svg {
115 | align-self: flex-end;
116 | height: 150%;
117 | width: auto;
118 | transform: scaleX(-1);
119 | margin-left: -50px;
120 | margin-bottom: -60px;
121 |
122 | @media only screen and (max-height: 775px) {
123 | height: 200%;
124 | margin-left: -20px;
125 | }
126 |
127 | @media only screen and (max-width: 1300px) {
128 | display: none;
129 | }
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/spootify/src/common/components/Header/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Header';
2 |
--------------------------------------------------------------------------------
/spootify/src/common/components/Player/Player.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
3 | import {
4 | faStepForward,
5 | faPlayCircle,
6 | faStepBackward,
7 | faEllipsisH
8 | } from '@fortawesome/free-solid-svg-icons';
9 | import { faHeart } from '@fortawesome/free-solid-svg-icons';
10 | import { faRandom } from '@fortawesome/free-solid-svg-icons';
11 | import { faRetweet } from '@fortawesome/free-solid-svg-icons';
12 | import { faVolumeDown } from '@fortawesome/free-solid-svg-icons';
13 | import './_player.scss';
14 |
15 | export default function Player() {
16 | return (
17 |
18 |
19 |
20 |
Nothing's playing
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/spootify/src/common/components/Player/_player.scss:
--------------------------------------------------------------------------------
1 | @import '../../../styles/vars';
2 |
3 | .player {
4 | position: absolute;
5 | width: calc(100% - 225px);
6 | background: rgba(255, 255, 255, 0.95);
7 | height: 100px;
8 | bottom: 0;
9 | right: 0;
10 | backdrop-filter: blur(5px);
11 | box-shadow: $mid-box-shadow;
12 | display: flex;
13 | justify-content: space-between;
14 | align-items: center;
15 | padding-left: 40px;
16 | padding-right: 40px;
17 | transition: height 0.2s ease-in-out;
18 |
19 | @media only screen and (max-width: 650px) {
20 | width: calc(100% - 75px);
21 | padding-left: 20px;
22 | padding-right: 20px;
23 | }
24 |
25 | @media only screen and (max-width: 400px) {
26 | width: calc(100% - 50px);
27 | }
28 |
29 | @media only screen and (max-height: 775px) {
30 | height: 70px;
31 | }
32 |
33 | svg {
34 | transition: transform 0.2s ease-in-out;
35 | cursor: pointer;
36 |
37 | &:hover {
38 | transform: scale(1.15);
39 | }
40 | }
41 |
42 | &__album {
43 | display: flex;
44 |
45 | @media only screen and (max-width: 450px) {
46 | display: none;
47 | }
48 |
49 | > span {
50 | margin-right: 20px;
51 | width: 50px;
52 | height: 50px;
53 | background: $primary-grey;
54 | border-radius: 6px;
55 | display: block;
56 | }
57 |
58 | > p {
59 | font-weight: 500;
60 | color: $deep-grey;
61 | font-size: 17px;
62 |
63 | @media only screen and (max-width: 1000px) {
64 | display: none;
65 | }
66 | }
67 | }
68 |
69 | &__controls {
70 | margin-left: 50px;
71 | display: flex;
72 | align-items: center;
73 | position: relative;
74 | justify-content: center;
75 |
76 | @media only screen and (max-width: 850px) {
77 | margin-left: 0;
78 | }
79 |
80 | > svg:nth-of-type(1),
81 | > svg:nth-of-type(3) {
82 | color: $primary-grey;
83 | }
84 |
85 | > svg:nth-of-type(2) {
86 | color: $primary-blue;
87 | font-size: 35px;
88 | margin-left: 15px;
89 | margin-right: 15px;
90 | z-index: 1;
91 | position: relative;
92 | }
93 |
94 | &:after {
95 | content: '';
96 | width: 25px;
97 | height: 25px;
98 | position: absolute;
99 | border-radius: 50%;
100 | z-index: 0;
101 | border: 10px solid $primary-blue;
102 | background: white;
103 | box-sizing: content-box !important;
104 | }
105 | }
106 |
107 | &__seekbar {
108 | flex: 1;
109 | height: 4px;
110 | background: $primary-grey;
111 | margin-left: 50px;
112 | margin-right: 50px;
113 | border-radius: 2px;
114 |
115 | @media only screen and (max-width: 850px) {
116 | margin-left: 20px;
117 | margin-right: 20px;
118 | }
119 | }
120 |
121 | &__actions {
122 | color: $primary-grey;
123 |
124 | > .fa-ellipsis-h {
125 | display: none;
126 | margin-right: 0 !important;
127 | }
128 |
129 | @media only screen and (max-width: 750px) {
130 | > svg:not(.fa-ellipsis-h) {
131 | display: none;
132 | width: 0 !important;
133 | margin: 0;
134 | }
135 |
136 | > .fa-ellipsis-h {
137 | display: unset;
138 | }
139 | }
140 |
141 | > svg:not(:last-of-type) {
142 | margin-right: 30px;
143 |
144 | @media only screen and (max-width: 800px) {
145 | margin-right: 15px;
146 | }
147 | }
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/spootify/src/common/components/Player/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Player';
2 |
--------------------------------------------------------------------------------
/spootify/src/common/components/SideBar/SideBar.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import cx from 'classnames';
3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
4 | import {
5 | faHeadphonesAlt,
6 | faHeart,
7 | faPlayCircle,
8 | faSearch, faStream,
9 | } from '@fortawesome/free-solid-svg-icons';
10 | import { ReactComponent as Avatar } from '../../../assets/images/avatar.svg';
11 | import './_sidebar.scss';
12 |
13 | function renderSideBarOption(link, icon, text, { selected } = {}) {
14 | return (
15 |
21 | )
22 | }
23 |
24 | export default function SideBar() {
25 | return (
26 |
27 |
31 |
32 | {renderSideBarOption('/', faHeadphonesAlt, 'Discover', { selected: true })}
33 | {renderSideBarOption('/search', faSearch, 'Search')}
34 | {renderSideBarOption('/favourites', faHeart, 'Favourites')}
35 | {renderSideBarOption('/playlists', faPlayCircle, 'Playlists')}
36 | {renderSideBarOption('/charts', faStream, 'Charts')}
37 |
38 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/spootify/src/common/components/SideBar/_sidebar.scss:
--------------------------------------------------------------------------------
1 | @import '../../../styles/vars';
2 |
3 | .sidebar {
4 | min-width: 225px;
5 | width: 225px;
6 | background: $primary-blue;
7 | height: 100%;
8 | display: flex;
9 | flex-direction: column;
10 | align-items: center;
11 | padding-top: 30px;
12 | padding-bottom: 30px;
13 | transition: all 0.2s ease-in-out;
14 | will-change: width, min-width;
15 |
16 | @media only screen and (max-width: 650px) {
17 | width: 75px;
18 | min-width: 75px;
19 | }
20 |
21 | @media only screen and (max-width: 400px) {
22 | width: 50px;
23 | min-width: 50px;
24 | }
25 |
26 | &__options {
27 | width: 100%;
28 | overflow-y: scroll;
29 |
30 | &::-webkit-scrollbar {
31 | display: none;
32 | }
33 | }
34 |
35 | &__option {
36 | display: flex;
37 | color: white;
38 | align-items: center;
39 | width: 100%;
40 | height: 75px;
41 | min-height: 75px;
42 | opacity: 0.5;
43 | padding-left: 40px;
44 | padding-right: 40px;
45 | user-select: none;
46 |
47 | @media only screen and (max-width: 650px) {
48 | padding-left: 30px;
49 | }
50 |
51 | @media only screen and (max-width: 400px) {
52 | padding-left: 17px;
53 | }
54 |
55 | > p {
56 | margin: 0;
57 | margin-left: 20px;
58 | font-size: 18px;
59 |
60 | @media only screen and (max-width: 650px) {
61 | display: none;
62 | }
63 | }
64 |
65 | &--selected {
66 | opacity: 1;
67 | }
68 |
69 | &--selected {
70 | pointer-events: none;
71 | background: linear-gradient(to right, $secondary-blue, transparent 95%);
72 | }
73 | }
74 |
75 | &__profile {
76 | width: 100%;
77 | display: flex;
78 | flex-direction: column;
79 | align-items: center;
80 | color: white;
81 | font-weight: 300;
82 | margin-bottom: 50px;
83 | margin-top: 20px;
84 |
85 | @media only screen and (max-width: 650px) {
86 | > p {
87 | display: none;
88 | }
89 | }
90 |
91 | > svg {
92 | width: 30%;
93 | height: auto;
94 | box-shadow: $mid-box-shadow;
95 | border-radius: 50%;
96 | }
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/spootify/src/common/components/SideBar/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './SideBar';
2 |
--------------------------------------------------------------------------------
/spootify/src/common/layouts/CoreLayout.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Header from '../components/Header';
3 | import SideBar from '../components/SideBar';
4 | import Player from '../components/Player';
5 |
6 | function CoreLayout({ children , history }) {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 | {children}
14 |
15 |
16 |
17 |
18 | );
19 | }
20 |
21 | export default CoreLayout;
22 |
--------------------------------------------------------------------------------
/spootify/src/config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | api: {
3 | baseUrl: 'https://api.spotify.com/v1',
4 | authUrl: 'https://accounts.spotify.com/api/token',
5 | clientId: process.env.REACT_APP_SPOTIFY_CLIENT_ID,
6 | clientSecret: process.env.REACT_APP_SPOTIFY_CLIENT_SECRET
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/spootify/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import Routes from './routes';
4 | import CoreLayout from './common/layouts/CoreLayout';
5 | import './styles/_main.scss';
6 |
7 | ReactDOM.render(
8 |
9 |
10 |
11 |
12 | ,
13 | document.getElementById('root')
14 | );
15 |
--------------------------------------------------------------------------------
/spootify/src/routes/Discover/components/Discover.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import DiscoverBlock from './DiscoverBlock/components/DiscoverBlock';
3 | import '../styles/_discover.scss';
4 |
5 | export default class Discover extends Component {
6 | constructor() {
7 | super();
8 |
9 | this.state = {
10 | newReleases: [],
11 | playlists: [],
12 | categories: []
13 | };
14 | }
15 |
16 | render() {
17 | const { newReleases, playlists, categories } = this.state;
18 |
19 | return (
20 |
21 |
22 |
23 |
24 |
25 | );
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/spootify/src/routes/Discover/components/DiscoverBlock/components/DiscoverBlock.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
3 | import { faChevronLeft, faChevronRight } from '@fortawesome/free-solid-svg-icons';
4 | import DiscoverItem from './DiscoverItem';
5 | import '../styles/_discover-block.scss';
6 |
7 | function scrollContainer(id, { isNegative } = {}) {
8 | return () => {
9 | const scrollableContainer = document.getElementById(id);
10 | const amount = isNegative ? -scrollableContainer.offsetWidth : scrollableContainer.offsetWidth;
11 |
12 | scrollableContainer.scrollLeft = scrollableContainer.scrollLeft + amount;
13 | };
14 | }
15 |
16 | export default function DiscoverBlock({ text, id, data, imagesKey = 'images' }) {
17 | return (
18 |
19 |
20 |
{text}
21 |
22 | {
23 | data.length ? (
24 |
25 |
29 |
33 |
34 | ) : null
35 | }
36 |
37 |
38 | {data.map(({ [imagesKey]: images, name }) => (
39 |
40 | ))}
41 |
42 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/spootify/src/routes/Discover/components/DiscoverBlock/components/DiscoverItem.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import '../styles/_discover-item.scss';
3 |
4 | export default function DiscoverItem({ images, name }) {
5 | return (
6 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/spootify/src/routes/Discover/components/DiscoverBlock/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './components/DiscoverBlock';
2 |
--------------------------------------------------------------------------------
/spootify/src/routes/Discover/components/DiscoverBlock/styles/_discover-block.scss:
--------------------------------------------------------------------------------
1 | @import '../../../../../styles/vars';
2 |
3 | .discover-block {
4 | &__header {
5 | display: flex;
6 | align-items: center;
7 | min-height: 20px;
8 |
9 | > h2 {
10 | color: $primary-grey;
11 | font-size: 15px;
12 | letter-spacing: 1px;
13 | margin: 0;
14 | }
15 |
16 | > span {
17 | background: #eee;
18 | flex: 1;
19 | height: 2px;
20 | display: block;
21 | margin-left: 20px;
22 | margin-right: 20px;
23 | }
24 |
25 | > div {
26 | font-size: 20px;
27 | display: flex;
28 |
29 | > svg {
30 | transition: transform 0.2s ease-in-out;
31 | cursor: pointer;
32 | color: $primary-blue;
33 |
34 | &:hover {
35 | transform: scale(1.15);
36 | }
37 | }
38 |
39 | > svg:first-of-type {
40 | margin-right: 20px;
41 | }
42 | }
43 |
44 | &__icon {
45 | &__disabled {
46 | color: unset;
47 | pointer-events: none;
48 | }
49 | }
50 | }
51 |
52 | &__row {
53 | display: flex;
54 | margin-bottom: 50px;
55 | width: 100%;
56 | overflow-x: scroll;
57 | height: 250px;
58 | scroll-behavior: smooth;
59 | margin-top: 20px;
60 |
61 | &::-webkit-scrollbar {
62 | display: none;
63 | }
64 |
65 | > * {
66 | margin-right: 50px;
67 | }
68 |
69 | @media only screen and (max-height: 750px) {
70 | margin-bottom: 20px;
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/spootify/src/routes/Discover/components/DiscoverBlock/styles/_discover-item.scss:
--------------------------------------------------------------------------------
1 | .discover-item {
2 | display: flex;
3 | flex-direction: column;
4 | align-items: center;
5 | width: 150px;
6 | margin: 20px;
7 |
8 | @media only screen and (max-width: 500px) {
9 | width: 100%;
10 | min-width: 100%;
11 | margin-right: 0 !important;
12 | }
13 |
14 | &:hover &__art {
15 | transform: scale(1.05);
16 | }
17 |
18 | &__art {
19 | height: 150px;
20 | width: 150px;
21 | border-radius: 6px;
22 | background-size: cover;
23 | transition: transform 0.15s ease-in-out;
24 | cursor: pointer;
25 | }
26 |
27 | &__title {
28 | font-size: 15px;
29 | text-align: center;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/spootify/src/routes/Discover/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './components/Discover';
2 |
--------------------------------------------------------------------------------
/spootify/src/routes/Discover/styles/_discover.scss:
--------------------------------------------------------------------------------
1 | .discover {
2 | .animate__animated.animate__fadeIn {
3 | --animate-duration: 1.5s;
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/spootify/src/routes/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Discover from './Discover';
3 |
4 | export default function Routes() {
5 | // Here you'd return an array of routes
6 | return ;
7 | }
8 |
--------------------------------------------------------------------------------
/spootify/src/styles/_main.scss:
--------------------------------------------------------------------------------
1 | @import '~animate.css/animate.min.css';
2 | @import url('https://fonts.googleapis.com/css2?family=Rubik:wght@300;400;500;600;700;800&display=swap');
3 | @import './vars';
4 |
5 | * {
6 | box-sizing: border-box;
7 | }
8 |
9 | a,
10 | p,
11 | h1,
12 | h2,
13 | h3,
14 | button {
15 | font-family: 'Rubik', sans-serif;
16 | }
17 |
18 | body {
19 | margin: 0;
20 | background: #F9F8FF;
21 | }
22 |
23 | html,
24 | body,
25 | #root {
26 | height: 100%;
27 | }
28 |
29 | .main {
30 | height: 100%;
31 | display: flex;
32 | border-radius: 12px;
33 | overflow: hidden;
34 | box-shadow: $mid-box-shadow;
35 | position: relative;
36 | background: white;
37 |
38 | &__content {
39 | display: flex;
40 | flex-direction: column;
41 | flex: 1;
42 | width: calc(100% - 225px);
43 |
44 | &__child {
45 | display: flex;
46 | flex-direction: column;
47 | flex: 1;
48 | padding: 50px;
49 | padding-top: 0;
50 | padding-bottom: 100px;
51 | overflow-y: scroll;
52 |
53 | @media only screen and (max-width: 750px) {
54 | padding: 20px;
55 | }
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/spootify/src/styles/_vars.scss:
--------------------------------------------------------------------------------
1 | $primary-blue: #564FD8;
2 | $secondary-blue: #7974DD;
3 | $secondary-orange: #FFB5A7;
4 | $primary-grey: #CECEDC;
5 | $deep-grey: #39383D;
6 |
7 | $box-shadow: 0px 4px 40px rgba(0, 0, 0, 0.05);
8 | $mid-box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.10);
9 | $darker-box-shadow: 0px 4px 40px rgba(0, 0, 0, 0.15);
10 |
--------------------------------------------------------------------------------