├── README.md ├── backend ├── .editorconfig ├── .env.example ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── Procfile ├── babel.config.js ├── package.json ├── prettier.config.js ├── src │ ├── @types │ │ └── express.d.ts │ ├── app.ts │ ├── middlewares │ │ └── auth.ts │ ├── routes │ │ ├── artist.routes.ts │ │ ├── auth │ │ │ ├── callback.routes.ts │ │ │ └── sessions.routes.ts │ │ ├── index.ts │ │ ├── playlist.routes.ts │ │ └── user.routes.ts │ ├── server.ts │ ├── services │ │ └── api.ts │ └── utils │ │ └── GenerateRandomString.ts ├── tsconfig.json ├── yarn-error.log └── yarn.lock └── frontend ├── .editorconfig ├── .env.example ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── package.json ├── prettier.config.js ├── public ├── _redirects ├── favicon.ico ├── index.html └── robots.txt ├── setupTests.ts ├── src ├── App.tsx ├── assets │ ├── ball.svg │ ├── ed-sheeran.png │ ├── matheus.jpeg │ ├── paulo.png │ └── spotify-logo.svg ├── components │ ├── AnimatedVectors │ │ ├── BemConhecido.tsx │ │ ├── ChamandoAtencao.tsx │ │ ├── Estourando.tsx │ │ ├── Popular.tsx │ │ ├── PoucoEscutado.tsx │ │ ├── Seguidores.tsx │ │ └── data │ │ │ ├── eye.json │ │ │ ├── fire.json │ │ │ ├── ghost.json │ │ │ ├── heart.json │ │ │ ├── music.json │ │ │ └── star.json │ ├── Header │ │ ├── About │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── Dropdown │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── index.tsx │ │ └── styles.ts │ ├── LineGraphAnimated │ │ ├── index.tsx │ │ └── styles.ts │ ├── Modal │ │ ├── index.tsx │ │ └── styles.css │ ├── Scroll │ │ └── index.ts │ ├── Spinner │ │ ├── index.tsx │ │ └── styles.ts │ └── SpotifyButton │ │ ├── index.tsx │ │ └── styles.ts ├── hooks │ ├── auth.tsx │ └── index.tsx ├── index.tsx ├── pages │ ├── Artists │ │ ├── ModalArtist │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── index.tsx │ │ └── styles.ts │ ├── Authenticate │ │ ├── index.tsx │ │ └── styles.ts │ ├── Error │ │ └── index.tsx │ ├── FavoriteTracks │ │ ├── index.tsx │ │ └── styles.ts │ ├── NotFound │ │ ├── index.tsx │ │ └── styles.ts │ ├── Playlists │ │ ├── ModalPlaylistTracks │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── index.tsx │ │ └── styles.ts │ ├── SignIn │ │ ├── index.tsx │ │ └── styles.ts │ └── _layouts │ │ ├── auth │ │ ├── index.tsx │ │ └── styles.ts │ │ └── default │ │ ├── index.tsx │ │ └── styles.ts ├── react-app-env.d.ts ├── routes │ ├── Route.tsx │ └── index.tsx ├── services │ ├── api.ts │ └── history.ts ├── styles │ ├── animations.ts │ └── global.ts └── utils │ ├── audio.ts │ ├── formatValue.ts │ ├── getPopularity.ts │ └── toggleFullScreen.ts ├── tsconfig.json └── yarn.lock /README.md: -------------------------------------------------------------------------------- 1 |

2 |

3 | Codify 4 |

5 |

6 | Uma forma incrível de você ter acesso a curiosidades da sua conta do Spotify! 🎧 7 |

8 |

9 | GitHub top language 10 | GitHub language count 11 | Stars 12 | Repository Size 13 |

14 | 15 | 16 |

17 | Sobre   |    18 | Requisitos   |    19 | Começando   |    20 | Node.js   |    21 | ReactJS 22 |

23 | 24 | ## :page_with_curl: Sobre 25 | Codify é uma aplicação criada a partir do [**Spotify’s Web API**](https://developer.spotify.com/documentation/web-api/) para coletar informações de sua conta Spotify. 26 | 27 | Nela o usuário tem acesso aos seus artistas mais escutados, músicas mais curtidas e suas playlists, dentre diversas outras curiosidades incríveis. 28 | 29 | Nesse projeto tivemos como principal objetivo aprender a consumir uma API externa e estudar toda a documentação por trás dela. Além disso, no processo tivemos que estudar sobre a biblioteca de Audio do JavaScript para podermos tocar músicas. 30 | 31 | **Node.js**: realiza todas as chamadas a API do Spotify e customizamos as respostas pra serem da forma que queremos. Serve todos os dados para o front-end. 32 | 33 | **ReactJS**: é uma página Web no qual o usuário terá acesso a informações da sua conta do Spotify. 34 | 35 | ## :books: Requisitos 36 | - Ter [**Git**](https://git-scm.com/) para clonar o projeto. 37 | - Ter [**Node.js**](https://nodejs.org/en/) instalado. 38 | - Ter [**Yarn**](https://classic.yarnpkg.com/pt-BR/docs/install/) instalado. 39 | - Ter credencias do Spotify. 40 | 41 | ## :lock: Credenciais do Spotify para rodar o projeto localmente 42 | 43 | Para você poder rodar o projeto localmente na sua máquina é preciso ter uma [**conta de desenvolvedor no Spotify**](https://developer.spotify.com/dashboard/) (para criar essa conta é totalmente gratuito e pode usar sua própria conta do Spotify). 44 | 45 | Com a conta criada basta clicar no botão **CREATE AN APP** e preencher os dados que forem pedidos. 46 | 47 | Após isso você terá acesso ao Dashboard da sua aplicação. No lado esquerdo estará suas credenciais, Client ID e Client Secret que serão usuadas para prencher o arquivo .env do backend. 48 | 49 | Por fim, no lado direito clique no botão **EDIT SETTINGS**. No modal que abrir haverá um compo chamado **Redirects URIs**, nele você irá preencher com a URL em que o seu backend estará rodando com a rota **/callback** (Ex: http://localhost:3333/callback). Após isso basta clicar em **SAVE**. 50 | 51 | ## :rocket: Começando 52 | ``` bash 53 | # Clonar o projeto: 54 | $ git clone https://github.com/TwoDevsForDevs/codify 55 | 56 | # Entrar no diretório: 57 | $ cd codify 58 | ``` 59 | 60 | ## :gear: Iniciando back-end 61 | ```bash 62 | # Entrar no diretório do back-end: 63 | $ cd backend 64 | 65 | # Instalar as dependências: 66 | $ yarn 67 | 68 | # Rodar a aplicação: 69 | $ yarn dev:server 70 | ``` 71 | 72 | ## :computer: Iniciando front-end 73 | ```bash 74 | # Entrar no diretório do front-end: 75 | $ cd frontend 76 | 77 | # Instalar as dependências: 78 | $ yarn 79 | 80 | # Rodar a aplicação: 81 | $ yarn start 82 | ``` 83 | 84 | Feito com ❤️ por [Matheus Pires](https://github.com/MatheusPires99) e [Paulo Henrique](https://github.com/paulohenriquepm) 👋🏻 85 | -------------------------------------------------------------------------------- /backend/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true -------------------------------------------------------------------------------- /backend/.env.example: -------------------------------------------------------------------------------- 1 | CLIENT_ID= 2 | CLIENT_SECRET= 3 | REDIRECT_URI= 4 | APP_URL=http://localhost:3000 -------------------------------------------------------------------------------- /backend/.eslintignore: -------------------------------------------------------------------------------- 1 | /*.js 2 | node_modules 3 | dist 4 | -------------------------------------------------------------------------------- /backend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "extends": [ 7 | "airbnb-base", 8 | "plugin:@typescript-eslint/recommended", 9 | "prettier/@typescript-eslint", 10 | "plugin:prettier/recommended" 11 | ], 12 | "globals": { 13 | "Atomics": "readonly", 14 | "SharedArrayBuffer": "readonly" 15 | }, 16 | "parser": "@typescript-eslint/parser", 17 | "parserOptions": { 18 | "ecmaVersion": 2018, 19 | "sourceType": "module" 20 | }, 21 | "plugins": [ 22 | "@typescript-eslint", 23 | "prettier" 24 | ], 25 | "rules": { 26 | "prettier/prettier": "error", 27 | "class-methods-use-this": "off", 28 | "@typescript-eslint/camelcase": "off", 29 | "no-useless-constructor": "off", 30 | "no-plusplus": "off", 31 | "@typescript-eslint/no-unused-vars": ["error", { 32 | "argsIgnorePattern": "_" 33 | }], 34 | "@typescript-eslint/interface-name-prefix": ["error", { "prefixWithI": "always" }], 35 | "import/extensions": [ 36 | "error", 37 | "ignorePackages", 38 | { 39 | "ts": "never" 40 | } 41 | ] 42 | }, 43 | "settings": { 44 | "import/resolver": { 45 | "typescript": {} 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode 3 | build 4 | .env 5 | dist -------------------------------------------------------------------------------- /backend/Procfile: -------------------------------------------------------------------------------- 1 | web:yarn start 2 | -------------------------------------------------------------------------------- /backend/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', { targets: { node: 'current' } }], 4 | '@babel/preset-typescript' 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "build": "babel src --extensions \".js,.ts\" --out-dir dist --copy-files", 8 | "dev:server": "ts-node-dev --transpileOnly --ignore-watch node_modules src/server.ts", 9 | "start": "ts-node dist/server.js" 10 | }, 11 | "dependencies": { 12 | "axios": "^0.19.2", 13 | "cookie-parser": "^1.4.5", 14 | "cors": "^2.8.5", 15 | "dotenv": "^8.2.0", 16 | "express": "^4.17.1", 17 | "request": "^2.88.2", 18 | "spotify-web-api-node": "^4.0.0", 19 | "ts-node": "^8.10.2" 20 | }, 21 | "devDependencies": { 22 | "@babel/cli": "^7.10.3", 23 | "@babel/core": "^7.10.3", 24 | "@babel/node": "^7.10.3", 25 | "@babel/preset-env": "^7.10.3", 26 | "@babel/preset-typescript": "^7.10.1", 27 | "@types/cookie-parser": "^1.4.2", 28 | "@types/cors": "^2.8.6", 29 | "@types/express": "^4.17.6", 30 | "@types/request": "^2.48.4", 31 | "@types/spotify-web-api-node": "^4.0.1", 32 | "@typescript-eslint/eslint-plugin": "^2.31.0", 33 | "@typescript-eslint/parser": "^2.31.0", 34 | "eslint": "^6.8.0", 35 | "eslint-config-airbnb-base": "^14.1.0", 36 | "eslint-config-prettier": "^6.11.0", 37 | "eslint-import-resolver-typescript": "^2.0.0", 38 | "eslint-plugin-import": "^2.20.2", 39 | "eslint-plugin-prettier": "^3.1.3", 40 | "prettier": "^2.0.5", 41 | "ts-node-dev": "^1.0.0-pre.44", 42 | "typescript": "^3.8.3" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /backend/prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | trailingComma: "all", 4 | arrowParens: "avoid" 5 | }; 6 | -------------------------------------------------------------------------------- /backend/src/@types/express.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace Express { 2 | // eslint-disable-next-line @typescript-eslint/interface-name-prefix 3 | export interface Request { 4 | accessToken: string; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /backend/src/app.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | 3 | import express from 'express'; 4 | import cors from 'cors'; 5 | import cookieParser from 'cookie-parser'; 6 | 7 | import routes from './routes'; 8 | 9 | const app = express(); 10 | 11 | app.use(cors()); 12 | app.use(cookieParser()); 13 | app.use(routes); 14 | 15 | export default app; 16 | -------------------------------------------------------------------------------- /backend/src/middlewares/auth.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | 3 | import api from '../services/api'; 4 | 5 | export default function auth( 6 | request: Request, 7 | response: Response, 8 | next: NextFunction, 9 | ): void | Response { 10 | const authHeader = request.headers.authorization; 11 | 12 | if (!authHeader) { 13 | return response.status(401).json({ error: 'Token not provided' }); 14 | } 15 | 16 | api.defaults.headers.authorization = authHeader; 17 | 18 | return next(); 19 | } 20 | -------------------------------------------------------------------------------- /backend/src/routes/artist.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | 3 | import api from '../services/api'; 4 | 5 | const artistRouter = Router(); 6 | 7 | interface IAlbum { 8 | images: IImages[]; 9 | } 10 | 11 | interface ITrack { 12 | id: string; 13 | album: IAlbum; 14 | name: string; 15 | preview_url: string; 16 | uri: string; 17 | } 18 | 19 | interface IRelatedArtist { 20 | id: string; 21 | name: string; 22 | images: IImages[]; 23 | uri: string; 24 | genres: string[]; 25 | } 26 | 27 | interface IImages { 28 | url: string; 29 | } 30 | 31 | interface IArtistFollwers { 32 | total: number; 33 | } 34 | 35 | interface IArtist { 36 | id: string; 37 | followers: IArtistFollwers; 38 | genres: string[]; 39 | images: IImages[]; 40 | name: string; 41 | popularity: number; 42 | uri: string; 43 | topTracks: ITrack[]; 44 | relatedArtists: IRelatedArtist[]; 45 | } 46 | 47 | artistRouter.get('/:id', async (req, res) => { 48 | const [ 49 | artistResponse, 50 | artistTopTracksResponse, 51 | relatedArtistsResponse, 52 | ] = await Promise.all([ 53 | api.get(`artists/${req.params.id}`), 54 | api.get(`artists/${req.params.id}/top-tracks?market=BR`), 55 | api.get(`artists/${req.params.id}/related-artists`), 56 | ]); 57 | 58 | const artist: IArtist = artistResponse.data; 59 | const artistTopTracks: ITrack[] = artistTopTracksResponse.data.tracks; 60 | const relatedArtists: IRelatedArtist[] = relatedArtistsResponse.data.artists; 61 | 62 | const formattedArtistTopTracks = artistTopTracks.map(topTrack => ({ 63 | id: topTrack.id, 64 | image: topTrack.album.images[0].url, 65 | name: topTrack.name, 66 | preview_url: topTrack.preview_url, 67 | uri: topTrack.uri, 68 | })); 69 | 70 | const formattedRelatedArtists = relatedArtists.map(relatedArtist => ({ 71 | id: relatedArtist.id, 72 | image: relatedArtist.images[0].url, 73 | name: relatedArtist.name, 74 | uri: relatedArtist.uri, 75 | genres: [relatedArtist.genres[0], relatedArtist?.genres[1]], 76 | })); 77 | 78 | const formattedArtist = { 79 | id: artist.id, 80 | name: artist.name, 81 | avatar: artist.images[0].url, 82 | uri: artist.uri, 83 | followers: artist.followers.total, 84 | genres: artist.genres, 85 | popularity: artist.popularity, 86 | topTracks: formattedArtistTopTracks, 87 | relatedArtists: formattedRelatedArtists, 88 | }; 89 | 90 | return res.json(formattedArtist); 91 | }); 92 | 93 | export default artistRouter; 94 | -------------------------------------------------------------------------------- /backend/src/routes/auth/callback.routes.ts: -------------------------------------------------------------------------------- 1 | import request from 'request'; 2 | import { Router } from 'express'; 3 | import querystring from 'querystring'; 4 | 5 | const callbackRouter = Router(); 6 | 7 | const clientId = process.env.CLIENT_ID; 8 | const clientSecret = process.env.CLIENT_SECRET; 9 | const redirectURI = process.env.REDIRECT_URI; 10 | 11 | const stateKey = '@codify/auth_state'; 12 | 13 | callbackRouter.get('/', (req, res) => { 14 | const code = req.query.code || null; 15 | const state = req.query.state || null; 16 | const storedState = req.cookies ? req.cookies[stateKey] : null; 17 | 18 | if (state === null || state !== storedState) { 19 | res.redirect( 20 | `${process.env.APP_URL}/error/${querystring.stringify({ 21 | error: 'state_mismatch', 22 | })}`, 23 | ); 24 | } else { 25 | res.clearCookie(stateKey); 26 | const authOptions = { 27 | url: 'https://accounts.spotify.com/api/token', 28 | form: { 29 | code, 30 | redirect_uri: redirectURI, 31 | grant_type: 'authorization_code', 32 | }, 33 | headers: { 34 | Authorization: `Basic ${Buffer.from( 35 | `${clientId}:${clientSecret}`, 36 | ).toString('base64')}`, 37 | }, 38 | json: true, 39 | }; 40 | 41 | request.post(authOptions, (error, response, body) => { 42 | if (!error && response.statusCode === 200) { 43 | const { access_token, refresh_token, expires_in } = body; 44 | 45 | res.redirect( 46 | `${ 47 | process.env.APP_URL 48 | }/spotify-authentication?${querystring.stringify({ 49 | access_token, 50 | refresh_token, 51 | expires_in, 52 | })}`, 53 | ); 54 | } 55 | }); 56 | } 57 | }); 58 | 59 | export default callbackRouter; 60 | -------------------------------------------------------------------------------- /backend/src/routes/auth/sessions.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import querystring from 'querystring'; 3 | 4 | import generateRandomString from '../../utils/GenerateRandomString'; 5 | 6 | const sessionsRouter = Router(); 7 | 8 | const redirectURI = process.env.REDIRECT_URI; 9 | const stateKey = '@codify/auth_state'; 10 | 11 | sessionsRouter.get('/', (req, res) => { 12 | const state = generateRandomString(16); 13 | res.cookie(stateKey, state); 14 | 15 | const scope = 16 | 'user-read-private user-read-email user-read-playback-state user-read-recently-played user-top-read playlist-read-private'; 17 | 18 | res.redirect( 19 | `https://accounts.spotify.com/authorize?${querystring.stringify({ 20 | response_type: 'code', 21 | client_id: process.env.CLIENT_ID, 22 | scope, 23 | redirect_uri: redirectURI, 24 | state, 25 | show_dialog: true, 26 | })}`, 27 | ); 28 | }); 29 | 30 | export default sessionsRouter; 31 | -------------------------------------------------------------------------------- /backend/src/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | 3 | import sessionsRouter from './auth/sessions.routes'; 4 | import callbackRouter from './auth/callback.routes'; 5 | import userRouter from './user.routes'; 6 | import playlistRouter from './playlist.routes'; 7 | import artistRouter from './artist.routes'; 8 | 9 | import authMiddleware from '../middlewares/auth'; 10 | 11 | const routes = Router(); 12 | 13 | routes.use('/sessions', sessionsRouter); 14 | routes.use('/callback', callbackRouter); 15 | 16 | routes.use(authMiddleware); 17 | 18 | routes.use('/me', userRouter); 19 | routes.use('/playlist', playlistRouter); 20 | routes.use('/artist', artistRouter); 21 | 22 | export default routes; 23 | -------------------------------------------------------------------------------- /backend/src/routes/playlist.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | 3 | import api from '../services/api'; 4 | 5 | const playlistRouter = Router(); 6 | 7 | interface IImages { 8 | url: string; 9 | } 10 | 11 | interface IPlaylistFollwers { 12 | total: number; 13 | } 14 | 15 | interface ITrack { 16 | total: number; 17 | } 18 | 19 | interface IPlaylist { 20 | id: string; 21 | name: string; 22 | images: IImages[]; 23 | uri: string; 24 | followers: IPlaylistFollwers; 25 | tracks: ITrack; 26 | } 27 | 28 | playlistRouter.get('/:id', async (req, res) => { 29 | const response = await api.get(`playlists/${req.params.id}`); 30 | 31 | const playlist: IPlaylist = response.data; 32 | 33 | const formattedPlaylist = { 34 | id: playlist.id, 35 | name: playlist.name, 36 | avatar: playlist.images[0].url, 37 | uri: playlist.uri, 38 | followers: playlist.followers.total, 39 | totalTracks: playlist.tracks.total, 40 | }; 41 | 42 | return res.json(formattedPlaylist); 43 | }); 44 | 45 | interface IArtist { 46 | name: string; 47 | } 48 | 49 | interface IAlbum { 50 | images: IImages[]; 51 | } 52 | 53 | interface ITrack { 54 | id: string; 55 | artists: IArtist[]; 56 | album: IAlbum; 57 | name: string; 58 | preview_url: string; 59 | uri: string; 60 | } 61 | 62 | interface ITracks { 63 | track: ITrack; 64 | } 65 | 66 | playlistRouter.get('/tracks/:id', async (req, res) => { 67 | const response = await api.get(`playlists/${req.params.id}/tracks?limit=100`); 68 | 69 | const tracks: ITracks[] = response.data.items; 70 | 71 | const formattedTracks = tracks.map(item => ({ 72 | id: item?.track.id, 73 | name: item?.track.name, 74 | preview: item?.track.preview_url, 75 | uri: item?.track.uri, 76 | artistName: item?.track.artists[0].name, 77 | albumImage: item?.track.album.images[0].url, 78 | })); 79 | 80 | return res.json(formattedTracks); 81 | }); 82 | 83 | export default playlistRouter; 84 | -------------------------------------------------------------------------------- /backend/src/routes/user.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | 3 | import api from '../services/api'; 4 | 5 | const userRouter = Router(); 6 | 7 | userRouter.get('/', async (req, res) => { 8 | const response = await api.get('/me'); 9 | 10 | const { id, type, display_name, email, images } = response.data; 11 | 12 | const user = { 13 | id, 14 | type, 15 | display_name, 16 | email, 17 | avatar: images.length ? images[0].url : '', 18 | }; 19 | 20 | return res.json(user); 21 | }); 22 | 23 | interface IImages { 24 | url: string; 25 | } 26 | 27 | interface IArtistFollwers { 28 | total: number; 29 | } 30 | 31 | interface ITopArtists { 32 | id: string; 33 | name: string; 34 | images: IImages[]; 35 | type: string; 36 | uri: string; 37 | followers: IArtistFollwers; 38 | popularity: number; 39 | topTrackPreview: string; 40 | topTrackName: string; 41 | } 42 | 43 | userRouter.get('/top-artists', async (req, res) => { 44 | const response = await api.get('/me/top/artists', { 45 | params: { 46 | limit: 5, 47 | }, 48 | }); 49 | 50 | const artists: ITopArtists[] = response.data.items; 51 | 52 | await Promise.all( 53 | artists.map(async artist => { 54 | const tracksResponse = await api.get( 55 | `/artists/${artist.id}/top-tracks?country=BR`, 56 | ); 57 | 58 | Object.assign(artist, { 59 | topTrackPreview: tracksResponse.data.tracks[0].preview_url, 60 | topTrackName: tracksResponse.data.tracks[0].name, 61 | }); 62 | }), 63 | ); 64 | 65 | return res.json(artists); 66 | }); 67 | 68 | interface IPlaylists { 69 | id: string; 70 | name: string; 71 | images: IImages[]; 72 | uri: string; 73 | } 74 | 75 | userRouter.get('/playlists', async (req, res) => { 76 | const response = await api.get('/me/playlists'); 77 | 78 | const playlists: IPlaylists[] = response.data.items; 79 | 80 | const formattedPlaylists = playlists.map(playlist => ({ 81 | id: playlist.id, 82 | name: playlist.name, 83 | avatar: playlist.images[0].url, 84 | uri: playlist.uri, 85 | })); 86 | 87 | return res.json(formattedPlaylists); 88 | }); 89 | 90 | interface IFavoriteTrackAlbum { 91 | id: string; 92 | name: string; 93 | uri: string; 94 | images: IImages[]; 95 | } 96 | 97 | interface IFavoriteTrackArtists { 98 | id: string; 99 | name: string; 100 | uri: string; 101 | image?: string; 102 | } 103 | 104 | interface IFavoriteTrack { 105 | id: string; 106 | name: string; 107 | popularity: number; 108 | preview_url: string; 109 | uri: string; 110 | album: IFavoriteTrackAlbum; 111 | artists: IFavoriteTrackArtists[]; 112 | } 113 | 114 | userRouter.get('/favorite-tracks', async (req, res) => { 115 | const response = await api.get('/me/top/tracks', { 116 | params: { 117 | limit: 10, 118 | }, 119 | }); 120 | 121 | const favoriteTracks: IFavoriteTrack[] = response.data.items; 122 | 123 | const formattedFavoriteTracks = favoriteTracks.map(track => ({ 124 | id: track.id, 125 | name: track.name, 126 | popularity: track.popularity, 127 | preview_url: track.preview_url, 128 | uri: track.uri, 129 | album: { 130 | id: track.album.id, 131 | name: track.album.name, 132 | uri: track.album.uri, 133 | image: track.album.images[0].url, 134 | }, 135 | artist: { 136 | id: track.artists[0].id, 137 | name: track.artists[0].name, 138 | uri: track.artists[0].uri, 139 | }, 140 | })); 141 | 142 | await Promise.all( 143 | formattedFavoriteTracks.map(async formattedTrack => { 144 | const artistImageResponse = await api.get( 145 | `/artists/${formattedTrack.artist.id}`, 146 | ); 147 | 148 | const { images } = artistImageResponse.data; 149 | 150 | if (images.url) { 151 | Object.assign(formattedTrack.artist, { 152 | image: images[0].url, 153 | }); 154 | } 155 | }), 156 | ); 157 | 158 | return res.json(formattedFavoriteTracks); 159 | }); 160 | 161 | export default userRouter; 162 | -------------------------------------------------------------------------------- /backend/src/server.ts: -------------------------------------------------------------------------------- 1 | import app from './app'; 2 | 3 | app.listen(process.env.PORT || 3333, () => { 4 | console.log('🚀 Server started on port 3333!'); 5 | }); 6 | -------------------------------------------------------------------------------- /backend/src/services/api.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const api = axios.create({ 4 | baseURL: 'https://api.spotify.com/v1', 5 | }); 6 | 7 | export default api; 8 | -------------------------------------------------------------------------------- /backend/src/utils/GenerateRandomString.ts: -------------------------------------------------------------------------------- 1 | const generateRandomString = (length: number): string => { 2 | let text = ''; 3 | const possible = 4 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 5 | 6 | for (let i = 0; i < length; i++) { 7 | text += possible.charAt(Math.floor(Math.random() * possible.length)); 8 | } 9 | return text; 10 | }; 11 | 12 | export default generateRandomString; 13 | -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | "outDir": "./dist", /* Redirect output structure to the directory. */ 16 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true, /* Enable all strict type-checking options. */ 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | "strictPropertyInitialization": false, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | 63 | /* Advanced Options */ 64 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 65 | }, 66 | "exclude": [ 67 | "node_modules", 68 | "./jest.config.js", 69 | "./prettier.config.js", 70 | ] 71 | } 72 | -------------------------------------------------------------------------------- /frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | # end_of_line = lf 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true -------------------------------------------------------------------------------- /frontend/.env.example: -------------------------------------------------------------------------------- 1 | REACT_APP_API_URL=http://localhost:3333 2 | -------------------------------------------------------------------------------- /frontend/.eslintignore: -------------------------------------------------------------------------------- 1 | **/*.js 2 | node_modules 3 | build 4 | src/react-app-env.d.ts 5 | -------------------------------------------------------------------------------- /frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true 5 | }, 6 | "extends": [ 7 | "plugin:react/recommended", 8 | "airbnb", 9 | "plugin:@typescript-eslint/recommended", 10 | "prettier/@typescript-eslint", 11 | "plugin:prettier/recommended" 12 | ], 13 | "globals": { 14 | "Atomics": "readonly", 15 | "SharedArrayBuffer": "readonly" 16 | }, 17 | "parser": "@typescript-eslint/parser", 18 | "parserOptions": { 19 | "ecmaFeatures": { 20 | "jsx": true 21 | }, 22 | "ecmaVersion": 2018, 23 | "sourceType": "module" 24 | }, 25 | "plugins": [ 26 | "react", 27 | "react-hooks", 28 | "@typescript-eslint", 29 | "prettier" 30 | ], 31 | "rules": { 32 | "prettier/prettier": "error", 33 | "react-hooks/rules-of-hooks": "error", 34 | "react-hooks/exhaustive-deps": "warn", 35 | "react/jsx-filename-extension": [1, { "extensions": [".tsx"] }], 36 | "import/prefer-default-export": "off", 37 | "react/jsx-one-expression-per-line": "off", 38 | "react/jsx-props-no-spreading": "off", 39 | "react/prop-types": "off", 40 | "@typescript-eslint/camelcase": "off", 41 | "no-unused-expressions": "off", 42 | "no-plusplus": "off", 43 | "@typescript-eslint/interface-name-prefix": ["error", { "prefixWithI": "always" }], 44 | "@typescript-eslint/explicit-function-return-type": [ 45 | "error", 46 | { 47 | "allowExpressions": true 48 | } 49 | ], 50 | "import/extensions": [ 51 | "error", 52 | "ignorePackages", 53 | { 54 | "ts": "never", 55 | "tsx": "never" 56 | } 57 | ] 58 | }, 59 | "settings": { 60 | "import/resolver": { 61 | "typescript": {} 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env 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 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@react-hook/window-size": "^2.0.5", 7 | "@testing-library/jest-dom": "^4.2.4", 8 | "@testing-library/react": "^9.3.2", 9 | "@testing-library/user-event": "^7.1.2", 10 | "@types/jest": "^24.0.0", 11 | "@types/node": "^12.0.0", 12 | "@types/react": "^16.9.0", 13 | "@types/react-dom": "^16.9.0", 14 | "axios": "^0.19.2", 15 | "date-fns": "^2.14.0", 16 | "history": "^4.10.1", 17 | "js-cookie": "^2.2.1", 18 | "lottie-react-web": "^2.2.2", 19 | "lottie-web": "^5.6.10", 20 | "polished": "^3.6.3", 21 | "react": "^16.13.1", 22 | "react-dom": "^16.13.1", 23 | "react-icons": "^3.10.0", 24 | "react-modal": "^3.11.2", 25 | "react-perfect-scrollbar": "^1.5.8", 26 | "react-router-dom": "^5.2.0", 27 | "react-scripts": "3.4.1", 28 | "react-spring": "^8.0.27", 29 | "react-toastify": "^6.0.5", 30 | "spotify-web-api-js": "^1.4.0", 31 | "styled-components": "^5.1.0", 32 | "typescript": "~3.7.2" 33 | }, 34 | "scripts": { 35 | "start": "react-scripts start", 36 | "build": "CI= react-scripts build", 37 | "test": "react-scripts test", 38 | "eject": "react-scripts eject" 39 | }, 40 | "eslintConfig": { 41 | "extends": "react-app" 42 | }, 43 | "browserslist": { 44 | "production": [ 45 | ">0.2%", 46 | "not dead", 47 | "not op_mini all" 48 | ], 49 | "development": [ 50 | "last 1 chrome version", 51 | "last 1 firefox version", 52 | "last 1 safari version" 53 | ] 54 | }, 55 | "devDependencies": { 56 | "@types/js-cookie": "^2.2.6", 57 | "@types/react-modal": "^3.10.5", 58 | "@types/react-router-dom": "^5.1.5", 59 | "@types/styled-components": "^5.1.0", 60 | "@typescript-eslint/eslint-plugin": "^2.32.0", 61 | "@typescript-eslint/parser": "^2.32.0", 62 | "eslint": "^6.8.0", 63 | "eslint-config-airbnb": "^18.1.0", 64 | "eslint-config-prettier": "^6.11.0", 65 | "eslint-import-resolver-typescript": "^2.0.0", 66 | "eslint-plugin-import": "^2.20.2", 67 | "eslint-plugin-jsx-a11y": "^6.2.3", 68 | "eslint-plugin-prettier": "^3.1.3", 69 | "eslint-plugin-react": "^7.19.0", 70 | "eslint-plugin-react-hooks": "^2.5.1", 71 | "prettier": "^2.0.5" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /frontend/prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | trailingComma: "all", 4 | arrowParens: "avoid" 5 | }; 6 | -------------------------------------------------------------------------------- /frontend/public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 2 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwoDevsForDevs/codify/bcb0a1f958afd4eca01cc93f9563a555e96652fb/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Codify 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import "@testing-library/jest-dom/extend-expect"; 6 | -------------------------------------------------------------------------------- /frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter as Router } from 'react-router-dom'; 3 | import { ToastContainer } from 'react-toastify'; 4 | 5 | import Routes from './routes'; 6 | 7 | import AppProvider from './hooks/index'; 8 | 9 | import GlobalStyles from './styles/global'; 10 | 11 | const App: React.FC = () => { 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | }; 23 | 24 | export default App; 25 | -------------------------------------------------------------------------------- /frontend/src/assets/ball.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | ball 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /frontend/src/assets/ed-sheeran.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwoDevsForDevs/codify/bcb0a1f958afd4eca01cc93f9563a555e96652fb/frontend/src/assets/ed-sheeran.png -------------------------------------------------------------------------------- /frontend/src/assets/matheus.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwoDevsForDevs/codify/bcb0a1f958afd4eca01cc93f9563a555e96652fb/frontend/src/assets/matheus.jpeg -------------------------------------------------------------------------------- /frontend/src/assets/paulo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwoDevsForDevs/codify/bcb0a1f958afd4eca01cc93f9563a555e96652fb/frontend/src/assets/paulo.png -------------------------------------------------------------------------------- /frontend/src/assets/spotify-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/components/AnimatedVectors/BemConhecido.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Lottie from 'lottie-react-web'; 3 | 4 | import star from './data/star.json'; 5 | 6 | const BemConhecido: React.FC = () => { 7 | return ( 8 | 13 | ); 14 | }; 15 | 16 | export default BemConhecido; 17 | -------------------------------------------------------------------------------- /frontend/src/components/AnimatedVectors/ChamandoAtencao.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Lottie from 'lottie-react-web'; 3 | 4 | import eye from './data/eye.json'; 5 | 6 | const ChamandoAtencao: React.FC = () => { 7 | return ( 8 | 13 | ); 14 | }; 15 | 16 | export default ChamandoAtencao; 17 | -------------------------------------------------------------------------------- /frontend/src/components/AnimatedVectors/Estourando.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Lottie from 'lottie-react-web'; 3 | 4 | import fire from './data/fire.json'; 5 | 6 | const PoucoEscutado: React.FC = () => { 7 | return ( 8 | 13 | ); 14 | }; 15 | 16 | export default PoucoEscutado; 17 | -------------------------------------------------------------------------------- /frontend/src/components/AnimatedVectors/Popular.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Lottie from 'lottie-react-web'; 3 | 4 | import music from './data/music.json'; 5 | 6 | const Popular: React.FC = () => { 7 | return ( 8 | 13 | ); 14 | }; 15 | 16 | export default Popular; 17 | -------------------------------------------------------------------------------- /frontend/src/components/AnimatedVectors/PoucoEscutado.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Lottie from 'lottie-react-web'; 3 | 4 | import ghost from './data/ghost.json'; 5 | 6 | const PoucoEscutado: React.FC = () => { 7 | return ( 8 | 13 | ); 14 | }; 15 | 16 | export default PoucoEscutado; 17 | -------------------------------------------------------------------------------- /frontend/src/components/AnimatedVectors/Seguidores.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Lottie from 'lottie-react-web'; 3 | 4 | import heart from './data/heart.json'; 5 | 6 | const Seguidores: React.FC = () => { 7 | return ( 8 | 13 | ); 14 | }; 15 | 16 | export default Seguidores; 17 | -------------------------------------------------------------------------------- /frontend/src/components/AnimatedVectors/data/eye.json: -------------------------------------------------------------------------------- 1 | {"v":"5.4.4","fr":30,"ip":0,"op":66,"w":600,"h":600,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 4","td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[300,300,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[93.4,0],[62.5,-83],[-5.1,-6.8],[-93.4,0],[-62.5,83],[5.1,6.8]],"o":[[-93.4,0],[-5.1,6.8],[62.5,83.1],[93.4,0],[5.1,-6.8],[-62.5,-83.1]],"v":[[0,-145.7],[-240.6,-11.6],[-240.6,11.5],[0,145.7],[240.6,11.6],[240.6,-11.5]],"c":true},"ix":2,"x":"var $bm_rt;\n$bm_rt = thisComp.layer('Shape Layer 1').content('Shape 1').content('Path 1').path;"},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.2,1,0.47843137254901963,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,1,0.47843137254901963,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":67,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 3","tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":8,"s":[300,300,0],"e":[272,300,0],"to":[-4.667,0,0],"ti":[4.667,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.167,"y":0.167},"t":12,"s":[272,300,0],"e":[272,300,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0},"t":28,"s":[272,300,0],"e":[300,272,0],"to":[4.667,-4.667,0],"ti":[-4.667,4.667,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.167,"y":0.167},"t":32,"s":[300,272,0],"e":[300,272,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0},"t":35,"s":[300,272,0],"e":[328,300,0],"to":[4.667,4.667,0],"ti":[-4.667,-4.667,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.167,"y":0.167},"t":39,"s":[328,300,0],"e":[328,300,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0},"t":55,"s":[328,300,0],"e":[300,300,0],"to":[-4.667,0,0],"ti":[4.667,0,0]},{"t":59}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"f","pt":{"a":0,"k":{"i":[[27.5,-1.7],[-2.2,33.4],[-27.6,1.8],[2.2,-33.4]],"o":[[-33.4,2.1],[1.7,-27.6],[33.4,-2.1],[-1.8,27.7]],"v":[[4.1,55.7],[-54.7,-3.1],[-3,-54.8],[55.8,4]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 2"}],"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[51.1,-3.3],[-3.9,62.1],[-51.2,3.2],[3.9,-62.1]],"o":[[-62,3.9],[3.2,-51.2],[62,-3.9],[-3.3,51.1]],"v":[[6.7,102.6],[-102.6,-6.7],[-6.7,-102.6],[102.6,6.7]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.2,1,0.47843137254901963,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,1,0.47843137254901963,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":67,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Shape Layer 2","td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":8,"s":[300,300,0],"e":[286,300,0],"to":[-2.333,0,0],"ti":[2.333,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.167,"y":0.167},"t":12,"s":[286,300,0],"e":[286,300,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0},"t":28,"s":[286,300,0],"e":[300,286,0],"to":[2.333,-2.333,0],"ti":[-2.333,2.333,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.167,"y":0.167},"t":32,"s":[300,286,0],"e":[300,286,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0},"t":35,"s":[300,286,0],"e":[314,300,0],"to":[2.333,2.333,0],"ti":[-2.333,-2.333,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.167,"y":0.167},"t":39,"s":[314,300,0],"e":[314,300,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0},"t":55,"s":[314,300,0],"e":[300,300,0],"to":[-2.333,0,0],"ti":[2.333,0,0]},{"t":59}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[51.1,-3.3],[-3.9,62.1],[-51.2,3.2],[3.9,-62.1]],"o":[[-62,3.9],[3.2,-51.2],[62,-3.9],[-3.3,51.1]],"v":[[6.7,102.6],[-102.6,-6.7],[-6.7,-102.6],[102.6,6.7]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.2,1,0.47843137254901963,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,1,0.47843137254901963,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":67,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Shape Layer 1","tt":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[300,300,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[93.4,0],[62.5,-83],[-0.017,-4.102],[-2.533,-3.377],[-93.4,0],[-62.5,83],[-0.009,4.06],[2.559,3.412]],"o":[[-93.4,0],[-2.567,3.423],[0.017,4.048],[62.5,83.1],[93.4,0],[2.541,-3.388],[0.009,-4.09],[-62.5,-83.1]],"v":[[0,-145.7],[-240.6,-11.6],[-244.425,0.031],[-240.6,11.5],[0,145.7],[240.6,11.6],[244.425,0.094],[240.6,-11.5]],"c":true}],"e":[{"i":[[93.4,0],[76.575,92.6],[-0.017,-4.102],[-2.533,-3.377],[-93.4,0],[-62.5,83],[0.309,4.049],[2.543,-3.424]],"o":[[-93.4,0],[-2.567,3.423],[0.017,4.048],[62.5,83.1],[93.4,0],[2.541,-3.388],[0.013,0.062],[-58.587,78.875]],"v":[[0,105.738],[-240.575,5.15],[-244.425,0.031],[-240.6,5.25],[0,105.7],[240.6,9.1],[244.425,0.094],[240.587,9.125]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":4,"s":[{"i":[[93.4,0],[76.575,92.6],[-0.017,-4.102],[-2.533,-3.377],[-93.4,0],[-62.5,83],[0.309,4.049],[2.543,-3.424]],"o":[[-93.4,0],[-2.567,3.423],[0.017,4.048],[62.5,83.1],[93.4,0],[2.541,-3.388],[0.013,0.062],[-58.587,78.875]],"v":[[0,105.738],[-240.575,5.15],[-244.425,0.031],[-240.6,5.25],[0,105.7],[240.6,9.1],[244.425,0.094],[240.587,9.125]],"c":true}],"e":[{"i":[[93.4,0],[62.5,-83],[-0.017,-4.102],[-2.533,-3.377],[-93.4,0],[-62.5,83],[-0.009,4.06],[2.559,3.412]],"o":[[-93.4,0],[-2.567,3.423],[0.017,4.048],[62.5,83.1],[93.4,0],[2.541,-3.388],[0.009,-4.09],[-62.5,-83.1]],"v":[[0,-145.7],[-240.6,-11.6],[-244.425,0.031],[-240.6,11.5],[0,145.7],[240.6,11.6],[244.425,0.094],[240.6,-11.5]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":8,"s":[{"i":[[93.4,0],[62.5,-83],[-0.017,-4.102],[-2.533,-3.377],[-93.4,0],[-62.5,83],[-0.009,4.06],[2.559,3.412]],"o":[[-93.4,0],[-2.567,3.423],[0.017,4.048],[62.5,83.1],[93.4,0],[2.541,-3.388],[0.009,-4.09],[-62.5,-83.1]],"v":[[0,-145.7],[-240.6,-11.6],[-244.425,0.031],[-240.6,11.5],[0,145.7],[240.6,11.6],[244.425,0.094],[240.6,-11.5]],"c":true}],"e":[{"i":[[93.4,0],[62.5,-83],[-0.017,-4.102],[-2.533,-3.377],[-93.4,0],[-62.5,83],[-0.009,4.06],[2.559,3.412]],"o":[[-93.4,0],[-2.567,3.423],[0.017,4.048],[62.5,83.1],[93.4,0],[2.541,-3.388],[0.009,-4.09],[-62.5,-83.1]],"v":[[0,-145.7],[-240.6,-11.6],[-244.425,0.031],[-240.6,11.5],[0,145.7],[240.6,11.6],[244.425,0.094],[240.6,-11.5]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":20,"s":[{"i":[[93.4,0],[62.5,-83],[-0.017,-4.102],[-2.533,-3.377],[-93.4,0],[-62.5,83],[-0.009,4.06],[2.559,3.412]],"o":[[-93.4,0],[-2.567,3.423],[0.017,4.048],[62.5,83.1],[93.4,0],[2.541,-3.388],[0.009,-4.09],[-62.5,-83.1]],"v":[[0,-145.7],[-240.6,-11.6],[-244.425,0.031],[-240.6,11.5],[0,145.7],[240.6,11.6],[244.425,0.094],[240.6,-11.5]],"c":true}],"e":[{"i":[[93.4,0],[76.575,92.6],[-0.017,-4.102],[-2.533,-3.377],[-93.4,0],[-62.5,83],[0.309,4.049],[2.543,-3.424]],"o":[[-93.4,0],[-2.567,3.423],[0.017,4.048],[62.5,83.1],[93.4,0],[2.541,-3.388],[0.013,0.062],[-58.587,78.875]],"v":[[0,105.738],[-240.575,5.15],[-244.425,0.031],[-240.6,5.25],[0,105.7],[240.6,9.1],[244.425,0.094],[240.587,9.125]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":24,"s":[{"i":[[93.4,0],[76.575,92.6],[-0.017,-4.102],[-2.533,-3.377],[-93.4,0],[-62.5,83],[0.309,4.049],[2.543,-3.424]],"o":[[-93.4,0],[-2.567,3.423],[0.017,4.048],[62.5,83.1],[93.4,0],[2.541,-3.388],[0.013,0.062],[-58.587,78.875]],"v":[[0,105.738],[-240.575,5.15],[-244.425,0.031],[-240.6,5.25],[0,105.7],[240.6,9.1],[244.425,0.094],[240.587,9.125]],"c":true}],"e":[{"i":[[93.4,0],[62.5,-83],[-0.017,-4.102],[-2.533,-3.377],[-93.4,0],[-62.5,83],[-0.009,4.06],[2.559,3.412]],"o":[[-93.4,0],[-2.567,3.423],[0.017,4.048],[62.5,83.1],[93.4,0],[2.541,-3.388],[0.009,-4.09],[-62.5,-83.1]],"v":[[0,-145.7],[-240.6,-11.6],[-244.425,0.031],[-240.6,11.5],[0,145.7],[240.6,11.6],[244.425,0.094],[240.6,-11.5]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":28,"s":[{"i":[[93.4,0],[62.5,-83],[-0.017,-4.102],[-2.533,-3.377],[-93.4,0],[-62.5,83],[-0.009,4.06],[2.559,3.412]],"o":[[-93.4,0],[-2.567,3.423],[0.017,4.048],[62.5,83.1],[93.4,0],[2.541,-3.388],[0.009,-4.09],[-62.5,-83.1]],"v":[[0,-145.7],[-240.6,-11.6],[-244.425,0.031],[-240.6,11.5],[0,145.7],[240.6,11.6],[244.425,0.094],[240.6,-11.5]],"c":true}],"e":[{"i":[[93.4,0],[62.5,-83],[-0.017,-4.102],[-2.533,-3.377],[-93.4,0],[-62.5,83],[-0.009,4.06],[2.559,3.412]],"o":[[-93.4,0],[-2.567,3.423],[0.017,4.048],[62.5,83.1],[93.4,0],[2.541,-3.388],[0.009,-4.09],[-62.5,-83.1]],"v":[[0,-145.7],[-240.6,-11.6],[-244.425,0.031],[-240.6,11.5],[0,145.7],[240.6,11.6],[244.425,0.094],[240.6,-11.5]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":47,"s":[{"i":[[93.4,0],[62.5,-83],[-0.017,-4.102],[-2.533,-3.377],[-93.4,0],[-62.5,83],[-0.009,4.06],[2.559,3.412]],"o":[[-93.4,0],[-2.567,3.423],[0.017,4.048],[62.5,83.1],[93.4,0],[2.541,-3.388],[0.009,-4.09],[-62.5,-83.1]],"v":[[0,-145.7],[-240.6,-11.6],[-244.425,0.031],[-240.6,11.5],[0,145.7],[240.6,11.6],[244.425,0.094],[240.6,-11.5]],"c":true}],"e":[{"i":[[93.4,0],[76.575,92.6],[-0.017,-4.102],[-2.533,-3.377],[-93.4,0],[-62.5,83],[0.309,4.049],[2.543,-3.424]],"o":[[-93.4,0],[-2.567,3.423],[0.017,4.048],[62.5,83.1],[93.4,0],[2.541,-3.388],[0.013,0.062],[-58.587,78.875]],"v":[[0,105.738],[-240.575,5.15],[-244.425,0.031],[-240.6,5.25],[0,105.7],[240.6,9.1],[244.425,0.094],[240.587,9.125]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":51,"s":[{"i":[[93.4,0],[76.575,92.6],[-0.017,-4.102],[-2.533,-3.377],[-93.4,0],[-62.5,83],[0.309,4.049],[2.543,-3.424]],"o":[[-93.4,0],[-2.567,3.423],[0.017,4.048],[62.5,83.1],[93.4,0],[2.541,-3.388],[0.013,0.062],[-58.587,78.875]],"v":[[0,105.738],[-240.575,5.15],[-244.425,0.031],[-240.6,5.25],[0,105.7],[240.6,9.1],[244.425,0.094],[240.587,9.125]],"c":true}],"e":[{"i":[[93.4,0],[62.5,-83],[-0.017,-4.102],[-2.533,-3.377],[-93.4,0],[-62.5,83],[-0.009,4.06],[2.559,3.412]],"o":[[-93.4,0],[-2.567,3.423],[0.017,4.048],[62.5,83.1],[93.4,0],[2.541,-3.388],[0.009,-4.09],[-62.5,-83.1]],"v":[[0,-145.7],[-240.6,-11.6],[-244.425,0.031],[-240.6,11.5],[0,145.7],[240.6,11.6],[244.425,0.094],[240.6,-11.5]],"c":true}]},{"t":55}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.2,1,0.47843137254901963,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,1,0.47843137254901963,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":67,"st":0,"bm":0}],"markers":[]} -------------------------------------------------------------------------------- /frontend/src/components/AnimatedVectors/data/heart.json: -------------------------------------------------------------------------------- 1 | {"v":"4.10.1","fr":60,"ip":47,"op":119,"w":400,"h":400,"nm":"Icon-Heart-Final","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"ExplodingHeartGroup","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.718],"y":[1.135]},"o":{"x":[0.389],"y":[0]},"n":["0p718_1p135_0p389_0"],"t":0,"s":[0],"e":[100]},{"t":31}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[199.972,192.185,0],"ix":2},"a":{"a":0,"k":[-1.442,-100.281,0],"ix":1},"s":{"a":0,"k":[15,15,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sr","sy":1,"d":1,"pt":{"a":0,"k":5,"ix":3},"p":{"a":0,"k":[0,0],"ix":4},"r":{"a":0,"k":0,"ix":5},"ir":{"a":0,"k":525.076,"ix":6},"is":{"a":0,"k":0,"ix":8},"or":{"a":0,"k":1050.152,"ix":7},"os":{"a":0,"k":0,"ix":9},"ix":1,"nm":"Polystar Path 1","mn":"ADBE Vector Shape - Star","hd":false},{"ty":"st","c":{"a":0,"k":[0.34968778491,0.452370464802,0.487561285496,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":9,"ix":5},"lc":1,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":0,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Polystar 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[74.499,-5.882],[0,0],[-60.534,-66.165],[0,0],[-52.158,56.387],[3.072,53.59],[56.311,5.631],[0,0]],"o":[[-53.495,4.223],[0,0],[60.534,66.165],[0,0],[52.087,-56.311],[-1.956,-34.122],[-56.311,-5.631],[0,0]],"v":[[-93.184,-123.883],[-160.757,-61.942],[-112.893,85.874],[1.136,167.524],[96.864,98.544],[157.398,-50.68],[84.194,-122.476],[-3.087,-70.388]],"c":true},"ix":2},"nm":"HeartPath","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":12,"s":[1,0.239215686917,0.337254911661,1],"e":[0.310591161251,0.885156273842,0.6975248456,1]},{"t":18}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":0,"s":[0,0],"e":[-614,842],"to":[0,0],"ti":[0,0]},{"t":30}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.799,0.799],"y":[1,1]},"o":{"x":[1,1],"y":[0,0]},"n":["0p799_1_1_0","0p799_1_1_0"],"t":0,"s":[100,100],"e":[0,0]},{"t":30}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"HeartShape 5","np":2,"cix":2,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[74.499,-5.882],[0,0],[-60.534,-66.165],[0,0],[-52.158,56.387],[3.072,53.59],[56.311,5.631],[0,0]],"o":[[-53.495,4.223],[0,0],[60.534,66.165],[0,0],[52.087,-56.311],[-1.956,-34.122],[-56.311,-5.631],[0,0]],"v":[[-93.184,-123.883],[-160.757,-61.942],[-112.893,85.874],[1.136,167.524],[96.864,98.544],[157.398,-50.68],[84.194,-122.476],[-3.087,-70.388]],"c":true},"ix":2},"nm":"HeartPath","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":11,"s":[1,0.239215686917,0.337254911661,1],"e":[0.329518973827,0.78351098299,1,1]},{"t":18}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":0,"s":[0,0],"e":[614,842],"to":[0,0],"ti":[0,0]},{"t":30}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.799,0.799],"y":[1,1]},"o":{"x":[1,1],"y":[0,0]},"n":["0p799_1_1_0","0p799_1_1_0"],"t":0,"s":[100,100],"e":[0,0]},{"t":30}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"HeartShape 4","np":2,"cix":2,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[74.499,-5.882],[0,0],[-60.534,-66.165],[0,0],[-52.158,56.387],[3.072,53.59],[56.311,5.631],[0,0]],"o":[[-53.495,4.223],[0,0],[60.534,66.165],[0,0],[52.087,-56.311],[-1.956,-34.122],[-56.311,-5.631],[0,0]],"v":[[-93.184,-123.883],[-160.757,-61.942],[-112.893,85.874],[1.136,167.524],[96.864,98.544],[157.398,-50.68],[84.194,-122.476],[-3.087,-70.388]],"c":true},"ix":2},"nm":"HeartPath","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":12,"s":[1,0.239215686917,0.337254911661,1],"e":[0.995572924614,0.522447049618,0.944912016392,1]},{"t":18}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":0,"s":[0,0],"e":[994,-308],"to":[0,0],"ti":[0,0]},{"t":30}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.799,0.799],"y":[1,1]},"o":{"x":[1,1],"y":[0,0]},"n":["0p799_1_1_0","0p799_1_1_0"],"t":0,"s":[100,100],"e":[0,0]},{"t":30}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"HeartShape 3","np":2,"cix":2,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[74.499,-5.882],[0,0],[-60.534,-66.165],[0,0],[-52.158,56.387],[3.072,53.59],[56.311,5.631],[0,0]],"o":[[-53.495,4.223],[0,0],[60.534,66.165],[0,0],[52.087,-56.311],[-1.956,-34.122],[-56.311,-5.631],[0,0]],"v":[[-93.184,-123.883],[-160.757,-61.942],[-112.893,85.874],[1.136,167.524],[96.864,98.544],[157.398,-50.68],[84.194,-122.476],[-3.087,-70.388]],"c":true},"ix":2},"nm":"HeartPath","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":11,"s":[1,0.239215686917,0.337254911661,1],"e":[0.505887031555,0.611846029758,0.96999078989,1]},{"t":19}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":0,"s":[0,0],"e":[-994,-308],"to":[0,0],"ti":[0,0]},{"t":30}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.799,0.799],"y":[1,1]},"o":{"x":[1,1],"y":[0,0]},"n":["0p799_1_1_0","0p799_1_1_0"],"t":0,"s":[100,100],"e":[0,0]},{"t":30}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"HeartShape 2","np":2,"cix":2,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[74.499,-5.882],[0,0],[-60.534,-66.165],[0,0],[-52.158,56.387],[3.072,53.59],[56.311,5.631],[0,0]],"o":[[-53.495,4.223],[0,0],[60.534,66.165],[0,0],[52.087,-56.311],[-1.956,-34.122],[-56.311,-5.631],[0,0]],"v":[[-93.184,-123.883],[-160.757,-61.942],[-112.893,85.874],[1.136,167.524],[96.864,98.544],[157.398,-50.68],[84.194,-122.476],[-3.087,-70.388]],"c":true},"ix":2},"nm":"HeartPath","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.239215686917,0.337254911661,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":0,"s":[0,0],"e":[0,-1045],"to":[0,0],"ti":[0,0]},{"t":30}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.799,0.799],"y":[1,1]},"o":{"x":[1,1],"y":[0,0]},"n":["0p799_1_1_0","0p799_1_1_0"],"t":0,"s":[100,100],"e":[0,0]},{"t":30}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"HeartShape","np":2,"cix":2,"ix":6,"mn":"ADBE Vector Group","hd":false}],"ip":47,"op":121,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"HeartButton","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[200.033,199.997,0],"ix":2},"a":{"a":0,"k":[-2.346,-0.37,0],"ix":1},"s":{"a":0,"k":[39.104,39.104,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-9.914,-6.085],[-16.074,11.9],[-18.211,20.805],[1.001,36.589],[19.725,21.65],[32.981,0.368],[19.11,-13.545],[3.494,-5.44],[10.207,7.244],[6.185,-0.069],[3.351,-3.678],[0.182,-6.669],[-21.571,-24.644],[-21.895,-16.215]],"o":[[9.926,-6.085],[21.842,-16.169],[21.571,-24.644],[-0.182,-6.669],[-3.351,-3.678],[-6.103,-0.068],[-10.218,7.243],[-3.485,-5.438],[-19.062,-13.527],[-32.981,0.368],[-19.725,21.65],[-1.001,36.589],[18.184,20.775],[16.066,11.898]],"v":[[3.41,125.072],[43.41,98.072],[102.41,47.072],[146.41,-40.928],[125.41,-94.928],[68.41,-120.928],[23.41,-106.928],[3.41,-85.928],[-16.559,-106.928],[-61.559,-120.928],[-118.559,-94.928],[-139.559,-40.928],[-95.559,47.072],[-36.559,98.072]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.341176470588,0.341176470588,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-6,26.89],"ix":2},"a":{"a":0,"k":[0,8],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":5,"s":[0,0],"e":[32,32]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":7,"s":[32,32],"e":[120,120]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":20,"s":[120,120],"e":[90,90]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":29,"s":[90,90],"e":[105,105]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":36,"s":[105,105],"e":[100,100]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":41,"s":[100,100],"e":[105,105]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":77,"s":[105,105],"e":[100,100]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":118,"s":[100,100],"e":[105,105]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":154,"s":[105,105],"e":[100,100]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":195,"s":[100,100],"e":[105,105]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":231,"s":[105,105],"e":[100,100]},{"t":272}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"HeartShape","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-9.914,-6.085],[-16.074,11.9],[-18.211,20.805],[1.001,36.589],[19.725,21.65],[32.981,0.368],[19.11,-13.545],[3.494,-5.44],[10.207,7.244],[6.185,-0.069],[3.351,-3.678],[0.182,-6.669],[-21.571,-24.644],[-21.895,-16.215]],"o":[[9.926,-6.085],[21.842,-16.169],[21.571,-24.644],[-0.182,-6.669],[-3.351,-3.678],[-6.103,-0.068],[-10.218,7.243],[-3.485,-5.438],[-19.062,-13.527],[-32.981,0.368],[-19.725,21.65],[-1.001,36.589],[18.184,20.775],[16.066,11.898]],"v":[[-3.59,143.962],[36.41,116.962],[95.41,65.962],[139.41,-22.038],[118.41,-76.038],[61.41,-102.038],[16.41,-88.038],[-3.59,-67.038],[-23.559,-88.038],[-68.559,-102.038],[-125.559,-76.038],[-146.559,-22.038],[-102.559,65.962],[-43.559,116.962]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.611397027969,1,0.941709578037,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[1,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":1,"s":[0,0],"e":[32,32]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":3,"s":[32,32],"e":[154,154]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":16,"s":[154,154],"e":[90,90]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":28,"s":[90,90],"e":[132,132]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":35,"s":[132,132],"e":[100,100]},{"t":40}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"HeartShapeColoured 2","np":2,"cix":2,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-9.914,-6.085],[-16.074,11.9],[-18.211,20.805],[1.001,36.589],[19.725,21.65],[32.981,0.368],[19.11,-13.545],[3.494,-5.44],[10.207,7.244],[6.185,-0.069],[3.351,-3.678],[0.182,-6.669],[-21.571,-24.644],[-21.895,-16.215]],"o":[[9.926,-6.085],[21.842,-16.169],[21.571,-24.644],[-0.182,-6.669],[-3.351,-3.678],[-6.103,-0.068],[-10.218,7.243],[-3.485,-5.438],[-19.062,-13.527],[-32.981,0.368],[-19.725,21.65],[-1.001,36.589],[18.184,20.775],[16.066,11.898]],"v":[[-3.59,143.962],[36.41,116.962],[95.41,65.962],[139.41,-22.038],[118.41,-76.038],[61.41,-102.038],[16.41,-88.038],[-3.59,-67.038],[-23.559,-88.038],[-68.559,-102.038],[-125.559,-76.038],[-146.559,-22.038],[-102.559,65.962],[-43.559,116.962]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.802050232887,0.548207759857,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[1,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":1,"s":[0,0],"e":[32,32]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":3,"s":[32,32],"e":[183,183]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":19,"s":[183,183],"e":[90,90]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":28,"s":[90,90],"e":[132,132]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":35,"s":[132,132],"e":[100,100]},{"t":40}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"HeartShapeColoured 3","np":2,"cix":2,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[99.715,99.715],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-2.347,-0.37],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"n":["0p667_1_0p167_0p167","0p667_1_0p167_0p167"],"t":0,"s":[120,120],"e":[584,584]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":12,"s":[584,584],"e":[430,430]},{"t":19}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"ButtonCircle","np":2,"cix":2,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p833_1_0p333_0","0p833_1_0p333_0"],"t":99,"s":[100,100],"e":[105,105]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0,0]},"n":["0p667_1_0p167_0","0p667_1_0p167_0"],"t":124,"s":[105,105],"e":[100,100]},{"t":157}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":0,"cix":2,"ix":5,"mn":"ADBE Vector Group","hd":false}],"ip":47,"op":121,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"BurstStar","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":54,"s":[100],"e":[0]},{"t":67}],"ix":11},"r":{"a":0,"k":1,"ix":10},"p":{"a":0,"k":[198.108,187.972,0],"ix":2},"a":{"a":0,"k":[-4.32,-7.171,0],"ix":1},"s":{"a":0,"k":[161.457,161.457,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-4,-17],[-4,-75]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":3,"s":[0],"e":[100]},{"t":29}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.143],"y":[0]},"n":["0_1_0p143_0"],"t":23,"s":[0],"e":[100]},{"t":45}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.899302423,0.759528160095,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":7,"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"rp","c":{"a":0,"k":8,"ix":1},"o":{"a":0,"k":0,"ix":2},"m":1,"ix":4,"tr":{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[-3,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":45,"ix":4},"so":{"a":0,"k":100,"ix":5},"eo":{"a":0,"k":100,"ix":6},"nm":"Transform"},"nm":"Repeater 1","mn":"ADBE Vector Filter - Repeater","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Line","np":4,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":47,"op":121,"st":0,"bm":0}]} -------------------------------------------------------------------------------- /frontend/src/components/Header/About/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FaGithub, FaLinkedin } from 'react-icons/fa'; 3 | 4 | import matheusImg from '../../../assets/matheus.jpeg'; 5 | import pauloImg from '../../../assets/paulo.png'; 6 | 7 | import { Container, Devs, Dev, DevInfo } from './styles'; 8 | 9 | interface IAboutProps { 10 | showAbout: boolean; 11 | } 12 | 13 | const About: React.FC = ({ showAbout }) => { 14 | return ( 15 | 16 |
17 |

Sobre o app

18 |

19 | Codify é uma aplicação criada a partir do 20 | 25 | Spotify’s Web API 26 | 27 | para coletar informações de sua conta Spotify. Não somos, de forma 28 | alguma, afiliados com o Spotify. 29 |

30 |
31 | 32 |
33 |

Privacidade

34 |

35 | Codify requer acesso à sua conta do Spotify mas não armazena nenhum 36 | dado em servidor. 37 |

38 |
39 | 40 |
41 |

Quem somos

42 | 43 | 44 | 45 | Matheus Pires 46 | 47 | 48 | Matheus Pires 49 |
50 | 55 | 56 | 57 | 58 | 63 | 64 | 65 |
66 |
67 |
68 | 69 | 70 | Paulo Henrique 71 | 72 | 73 | Paulo Henrique 74 |
75 | 80 | 81 | 82 | 83 | 88 | 89 | 90 |
91 |
92 |
93 |
94 |
95 |
96 | ); 97 | }; 98 | 99 | export default About; 100 | -------------------------------------------------------------------------------- /frontend/src/components/Header/About/styles.ts: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components'; 2 | import { lighten } from 'polished'; 3 | 4 | import { 5 | aboutDropdown, 6 | aboutDropdownMobile, 7 | incresedFadeUp, 8 | } from '../../../styles/animations'; 9 | 10 | interface IAboutProps { 11 | showAbout: boolean; 12 | } 13 | 14 | export const Container = styled.div` 15 | background: #19191a; 16 | padding: 40px; 17 | width: 636px; 18 | box-shadow: 2px 2px 16px #000; 19 | z-index: 100; 20 | overflow: hidden; 21 | 22 | position: absolute; 23 | left: 0px; 24 | top: 54px; 25 | 26 | visibility: hidden; 27 | ${props => 28 | props.showAbout && 29 | css` 30 | animation: ${aboutDropdown} 0.8s forwards cubic-bezier(0.19, 0.8, 0.28, 1); 31 | `} 32 | 33 | section { 34 | display: flex; 35 | flex-direction: column; 36 | 37 | & + section { 38 | margin-top: 24px; 39 | } 40 | 41 | h1 { 42 | color: #fff; 43 | font-size: 32px; 44 | line-height: 32px; 45 | margin-bottom: 16px; 46 | 47 | ${props => 48 | props.showAbout && 49 | css` 50 | opacity: 0; 51 | animation: ${incresedFadeUp} 1.2s forwards 52 | cubic-bezier(0.19, 1, 0.22, 1); 53 | animation-delay: 0.1s; 54 | `} 55 | } 56 | 57 | h3 { 58 | color: #7a8185; 59 | text-transform: uppercase; 60 | font-size: 16px; 61 | margin-bottom: 12px; 62 | 63 | &.privacy-title { 64 | animation-delay: 0.3s; 65 | } 66 | 67 | &.we-title { 68 | animation-delay: 0.5s; 69 | } 70 | 71 | ${props => 72 | props.showAbout && 73 | css` 74 | opacity: 0; 75 | animation: ${incresedFadeUp} 1.2s forwards 76 | cubic-bezier(0.19, 1, 0.22, 1); 77 | `} 78 | } 79 | 80 | p { 81 | font-size: 18px; 82 | color: #fff; 83 | line-height: 1.8; 84 | 85 | ${props => 86 | props.showAbout && 87 | css` 88 | opacity: 0; 89 | animation: ${incresedFadeUp} 1.2s forwards 90 | cubic-bezier(0.19, 1, 0.22, 1); 91 | `} 92 | 93 | &.about-text { 94 | animation-delay: 0.2s; 95 | } 96 | 97 | &.privacy-text { 98 | animation-delay: 0.4s; 99 | } 100 | 101 | a { 102 | margin: 0 4px; 103 | color: #1db954; 104 | transition: color 0.2s; 105 | 106 | &:hover { 107 | color: ${lighten(0.12, '#1db954')}; 108 | } 109 | } 110 | } 111 | } 112 | 113 | @media (max-width: 992px) { 114 | padding: 32px; 115 | 116 | right: 0; 117 | left: auto; 118 | 119 | ${props => 120 | props.showAbout && 121 | css` 122 | animation: ${aboutDropdownMobile} 0.8s forwards 123 | cubic-bezier(0.19, 0.8, 0.28, 1); 124 | `} 125 | 126 | section { 127 | h1 { 128 | font-size: 24px; 129 | line-height: 24px; 130 | margin-bottom: 12px; 131 | } 132 | 133 | h3 { 134 | margin-bottom: 8px; 135 | } 136 | 137 | p { 138 | font-size: 14px; 139 | } 140 | } 141 | } 142 | 143 | @media (max-width: 768px) { 144 | width: 92vw; 145 | } 146 | `; 147 | 148 | export const Devs = styled.div` 149 | display: flex; 150 | 151 | ${props => 152 | props.showAbout && 153 | css` 154 | opacity: 0; 155 | animation: ${incresedFadeUp} 1.2s forwards cubic-bezier(0.19, 1, 0.22, 1); 156 | `} 157 | 158 | animation-delay: 0.6s; 159 | 160 | @media (max-width: 992px) { 161 | flex-direction: column; 162 | } 163 | `; 164 | 165 | export const Dev = styled.div` 166 | background: #2d2f30; 167 | border-radius: 20px; 168 | width: 100%; 169 | 170 | display: flex; 171 | align-items: center; 172 | 173 | & + div { 174 | margin-left: 32px; 175 | } 176 | 177 | img { 178 | width: 80px; 179 | height: 80px; 180 | border-top-left-radius: 20px; 181 | border-bottom-left-radius: 20px; 182 | } 183 | 184 | @media (max-width: 992px) { 185 | & + div { 186 | margin-top: 24px; 187 | margin-left: 0; 188 | } 189 | 190 | img { 191 | width: 64px; 192 | height: 64px; 193 | } 194 | } 195 | `; 196 | 197 | export const DevInfo = styled.aside` 198 | margin-left: 16px; 199 | 200 | display: flex; 201 | flex-direction: column; 202 | 203 | strong { 204 | color: #fff; 205 | font-size: 16px; 206 | margin-bottom: 12px; 207 | } 208 | 209 | div { 210 | display: flex; 211 | align-items: center; 212 | justify-content: flex-start; 213 | 214 | a { 215 | display: flex; 216 | justify-content: center; 217 | align-items: center; 218 | 219 | & + a { 220 | margin-left: 12px; 221 | } 222 | 223 | &:nth-child(1):hover svg { 224 | color: #ccc; 225 | } 226 | 227 | &:nth-child(2):hover svg { 228 | color: #0e76a8; 229 | } 230 | 231 | svg { 232 | color: #fff; 233 | width: 23px; 234 | height: 23px; 235 | transition: color 0.2s; 236 | } 237 | } 238 | } 239 | 240 | @media (max-width: 992px) { 241 | strong { 242 | margin-bottom: 6px; 243 | } 244 | 245 | div { 246 | a { 247 | svg { 248 | width: 20px; 249 | height: 20px; 250 | } 251 | } 252 | } 253 | } 254 | `; 255 | -------------------------------------------------------------------------------- /frontend/src/components/Header/Dropdown/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useCallback } from 'react'; 2 | import { FiLogOut, FiMaximize, FiGithub } from 'react-icons/fi'; 3 | 4 | import { toggleFullScreen } from '../../../utils/toggleFullScreen'; 5 | 6 | import { useAuth } from '../../../hooks/auth'; 7 | 8 | import { Container } from './styles'; 9 | 10 | interface IDropdownProps { 11 | showDropdown: boolean; 12 | setShowDropdown: (state: boolean) => void; 13 | } 14 | 15 | const Dropdown: React.FC = ({ 16 | showDropdown, 17 | setShowDropdown, 18 | }) => { 19 | const [fullScreen, setFullScreen] = useState(false); 20 | const { signOut } = useAuth(); 21 | 22 | const handleFullScreen = useCallback(() => { 23 | setFullScreen(!fullScreen); 24 | }, [fullScreen]); 25 | 26 | useEffect(() => { 27 | document.addEventListener('fullscreenchange', handleFullScreen); 28 | 29 | setShowDropdown(false); 30 | }, [handleFullScreen, setShowDropdown]); 31 | 32 | useEffect(() => { 33 | return () => { 34 | document.removeEventListener('fullscreenchange', handleFullScreen); 35 | 36 | setShowDropdown(false); 37 | }; 38 | }, [handleFullScreen, setShowDropdown]); 39 | 40 | return ( 41 | 42 |
    43 |
  • 44 | 48 |
  • 49 |
  • 50 | 55 | 56 | Contribue 57 | 58 |
  • 59 |
  • 60 | 64 |
  • 65 |
66 |
67 | ); 68 | }; 69 | 70 | export default Dropdown; 71 | -------------------------------------------------------------------------------- /frontend/src/components/Header/Dropdown/styles.ts: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components'; 2 | 3 | import { 4 | profileOptionsDropdown, 5 | profileOptionsDropdownMobile, 6 | incresedFadeUp, 7 | } from '../../../styles/animations'; 8 | 9 | interface IDropdownMenuProps { 10 | showDropdown: boolean; 11 | } 12 | 13 | export const Container = styled.div` 14 | background: #19191a; 15 | padding: 8px 0; 16 | width: 325px; 17 | box-shadow: 2px 2px 16px #000; 18 | z-index: 100; 19 | 20 | position: absolute; 21 | right: 0px; 22 | top: 54px; 23 | 24 | visibility: hidden; 25 | ${props => 26 | props.showDropdown && 27 | css` 28 | animation: ${profileOptionsDropdown} 0.75s forwards 29 | cubic-bezier(0.19, 0.8, 0.28, 1); 30 | `} 31 | 32 | ul { 33 | overflow: hidden; 34 | 35 | li { 36 | height: 60px; 37 | padding: 0 24px; 38 | cursor: pointer; 39 | 40 | display: flex; 41 | justify-content: flex-start; 42 | 43 | ${props => 44 | props.showDropdown && 45 | css` 46 | opacity: 0; 47 | animation: ${incresedFadeUp} 0.8s forwards 48 | cubic-bezier(0.19, 1, 0.22, 1); 49 | `} 50 | 51 | &:nth-child(1) { 52 | animation-delay: 0.2s; 53 | } 54 | 55 | &:nth-child(2) { 56 | animation-delay: 0.3s; 57 | } 58 | 59 | &:nth-child(3) { 60 | animation-delay: 0.4s; 61 | } 62 | 63 | &:hover svg { 64 | color: #1db954; 65 | } 66 | 67 | button, 68 | a { 69 | width: 100%; 70 | color: #fff; 71 | font-size: 18px; 72 | font-weight: 500; 73 | background: transparent; 74 | border: 0; 75 | 76 | display: flex; 77 | align-items: center; 78 | 79 | svg { 80 | width: 22px; 81 | height: 22px; 82 | color: #fff; 83 | margin-right: 16px; 84 | transition: color 0.2s; 85 | } 86 | } 87 | } 88 | } 89 | 90 | @media (max-width: 992px) { 91 | left: 0px; 92 | top: 54px; 93 | 94 | ${props => 95 | props.showDropdown && 96 | css` 97 | animation: ${profileOptionsDropdownMobile} 0.75s forwards 98 | cubic-bezier(0.19, 0.8, 0.28, 1); 99 | `} 100 | } 101 | 102 | @media (max-width: 768px) { 103 | width: 220px; 104 | 105 | ul { 106 | li { 107 | height: 54px; 108 | padding: 0 16px; 109 | 110 | button, 111 | a { 112 | font-size: 14px; 113 | 114 | svg { 115 | width: 20px; 116 | height: 20px; 117 | } 118 | } 119 | } 120 | } 121 | } 122 | `; 123 | -------------------------------------------------------------------------------- /frontend/src/components/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback } from 'react'; 2 | import { NavLink } from 'react-router-dom'; 3 | import { FaUserCircle } from 'react-icons/fa'; 4 | import { MdLink, MdMenu, MdClose } from 'react-icons/md'; 5 | import { FiMoreVertical } from 'react-icons/fi'; 6 | 7 | import { useAuth } from '../../hooks/auth'; 8 | 9 | import DropdownMenu from './Dropdown'; 10 | import About from './About'; 11 | 12 | import { Container, Content, Nav, NavigationMenu, ProfileData } from './styles'; 13 | 14 | const Header: React.FC = () => { 15 | const [showDropdown, setShowDropdown] = useState(false); 16 | const [showAbout, setShowAbout] = useState(false); 17 | const [showMenu, setShowMenu] = useState(false); 18 | 19 | const { user } = useAuth(); 20 | 21 | const handleDropdown = useCallback(() => { 22 | setShowDropdown(!showDropdown); 23 | setShowAbout(false); 24 | }, [showDropdown]); 25 | 26 | const handleAbout = useCallback(() => { 27 | setShowAbout(!showAbout); 28 | setShowDropdown(false); 29 | }, [showAbout]); 30 | 31 | const handleMenu = useCallback(() => { 32 | setShowMenu(!showMenu); 33 | setShowDropdown(false); 34 | setShowAbout(false); 35 | }, [showMenu]); 36 | 37 | const closeMenu = useCallback(() => { 38 | setShowMenu(false); 39 | }, []); 40 | 41 | return ( 42 | 43 | 44 | 90 | 91 | 92 | {user.avatar ? ( 93 | {user.display_name} 94 | ) : ( 95 | 96 | )} 97 | 98 | {user.display_name} 99 | 100 | 103 | 104 | 108 | 109 | 110 | 111 | ); 112 | }; 113 | 114 | export default Header; 115 | -------------------------------------------------------------------------------- /frontend/src/components/Header/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Container = styled.header` 4 | width: 100%; 5 | min-height: 80px; 6 | margin-bottom: 80px; 7 | 8 | display: flex; 9 | 10 | @media (max-width: 992px) { 11 | min-height: 64px; 12 | margin-bottom: 24px; 13 | } 14 | `; 15 | 16 | export const Content = styled.div` 17 | max-width: 1366px; 18 | width: 100%; 19 | margin: 0 auto; 20 | padding: 0 32px; 21 | 22 | display: flex; 23 | justify-content: space-between; 24 | align-items: center; 25 | 26 | @media (max-width: 992px) { 27 | padding: 0 16px 0 10px; 28 | 29 | flex-direction: row-reverse; 30 | } 31 | `; 32 | 33 | interface INavProps { 34 | showAbout: boolean; 35 | } 36 | 37 | export const Nav = styled.nav` 38 | position: relative; 39 | 40 | display: flex; 41 | 42 | .about-button { 43 | background: ${props => 44 | props.showAbout ? 'rgba(51, 255, 122, 0.15)' : 'transparent'}; 45 | border: 0; 46 | padding: 6px 10px; 47 | border-radius: 20px; 48 | color: ${props => (props.showAbout ? '#33ff7a' : '#fff')}; 49 | font-weight: bold; 50 | transition: background 0.2s; 51 | 52 | display: flex; 53 | align-items: center; 54 | 55 | svg { 56 | margin-right: 8px; 57 | color: ${props => (props.showAbout ? '#33ff7a' : '#fff')}; 58 | } 59 | } 60 | 61 | .mobile-menu-button { 62 | border: 0; 63 | background: transparent; 64 | 65 | display: none; 66 | justify-content: center; 67 | align-items: center; 68 | } 69 | 70 | @media (max-width: 992px) { 71 | .about-button { 72 | margin-right: 16px; 73 | } 74 | 75 | .mobile-menu-button { 76 | position: relative; 77 | z-index: 103; 78 | 79 | display: flex; 80 | } 81 | } 82 | `; 83 | 84 | interface INavigationMenuProps { 85 | showAbout: boolean; 86 | showMenu: boolean; 87 | } 88 | 89 | export const NavigationMenu = styled.div` 90 | display: flex; 91 | align-items: center; 92 | 93 | div { 94 | display: flex; 95 | align-items: center; 96 | 97 | > a { 98 | margin-left: 32px; 99 | color: #fff; 100 | font-weight: bold; 101 | position: relative; 102 | transition: opacity 0.2s; 103 | 104 | &:hover { 105 | opacity: 0.7; 106 | } 107 | } 108 | } 109 | 110 | .selected::after { 111 | content: ''; 112 | width: 100%; 113 | height: 3px; 114 | background: #1db954; 115 | position: absolute; 116 | bottom: -16px; 117 | left: 0; 118 | } 119 | 120 | @media (max-width: 992px) { 121 | position: fixed; 122 | width: 100%; 123 | height: 100%; 124 | top: 0; 125 | left: 0; 126 | z-index: 102; 127 | background: rgba(0, 0, 0, 0.9); 128 | 129 | visibility: ${props => (props.showMenu ? 'visible' : 'hidden')}; 130 | opacity: ${props => (props.showMenu ? 1 : 0)}; 131 | transition: opacity 0.2s; 132 | 133 | display: flex; 134 | justify-content: center; 135 | flex-direction: column; 136 | 137 | div { 138 | flex-direction: column; 139 | justify-content: center; 140 | 141 | > a { 142 | font-size: 18px; 143 | margin-left: 0px; 144 | 145 | & + a { 146 | margin-top: 40px; 147 | } 148 | } 149 | } 150 | 151 | .selected::after { 152 | bottom: -10px; 153 | } 154 | } 155 | `; 156 | 157 | interface IProfileDataProps { 158 | showDropdown: boolean; 159 | } 160 | 161 | export const ProfileData = styled.aside` 162 | position: relative; 163 | 164 | display: flex; 165 | align-items: center; 166 | 167 | img { 168 | width: 40px; 169 | height: 40px; 170 | border-radius: 20px; 171 | } 172 | 173 | span { 174 | color: #fff; 175 | font-weight: bold; 176 | margin: 0 16px; 177 | } 178 | 179 | > button { 180 | background: ${props => 181 | props.showDropdown ? 'rgba(51, 255, 122, 0.15)' : 'transparent'}; 182 | border: 0; 183 | border-radius: 50%; 184 | width: 32px; 185 | height: 32px; 186 | transition: background 0.2s; 187 | 188 | display: flex; 189 | justify-content: center; 190 | align-items: center; 191 | 192 | svg { 193 | color: ${props => (props.showDropdown ? '#33ff7a' : '#fff')}; 194 | } 195 | } 196 | 197 | @media (max-width: 992px) { 198 | flex-direction: row-reverse; 199 | 200 | span { 201 | display: none; 202 | } 203 | 204 | > button { 205 | margin-right: 8px; 206 | } 207 | } 208 | 209 | @media (max-width: 768px) { 210 | img { 211 | width: 32px; 212 | height: 32px; 213 | border-radius: 16px; 214 | } 215 | } 216 | `; 217 | -------------------------------------------------------------------------------- /frontend/src/components/LineGraphAnimated/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { HTMLAttributes } from 'react'; 2 | 3 | import { Container } from './styles'; 4 | 5 | type Props = HTMLAttributes; 6 | 7 | const LineGraphAnimated: React.FC = ({ className }) => { 8 | return ( 9 | 10 |
11 |
12 |
13 |
14 | 15 | ); 16 | }; 17 | 18 | export default LineGraphAnimated; 19 | -------------------------------------------------------------------------------- /frontend/src/components/LineGraphAnimated/styles.ts: -------------------------------------------------------------------------------- 1 | import styled, { keyframes } from 'styled-components'; 2 | 3 | const animateLine = keyframes` 4 | 0% { 5 | transform: scaleY(0); 6 | } 7 | 50% { 8 | transform: scaleY(1); 9 | } 10 | 100% { 11 | transform: scaleY(0); 12 | } 13 | `; 14 | 15 | export const Container = styled.div` 16 | display: flex; 17 | 18 | div { 19 | width: 2px; 20 | height: 10px; 21 | border-radius: 2px; 22 | animation-iteration-count: infinite; 23 | animation-timing-function: linear; 24 | background: #1db954; 25 | 26 | & + div { 27 | margin-left: 1px; 28 | } 29 | 30 | &.line1 { 31 | animation: ${animateLine} 0.8s linear infinite; 32 | } 33 | 34 | &.line2 { 35 | animation: ${animateLine} 1s linear infinite; 36 | } 37 | 38 | &.line3 { 39 | animation: ${animateLine} 0.8s linear infinite; 40 | } 41 | 42 | &.line4 { 43 | animation: ${animateLine} 1.2s linear infinite; 44 | } 45 | } 46 | `; 47 | -------------------------------------------------------------------------------- /frontend/src/components/Modal/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import ReactModal from 'react-modal'; 3 | 4 | import './styles.css'; 5 | 6 | interface IModalProps { 7 | isOpen: boolean; 8 | setIsOpen: () => void; 9 | } 10 | 11 | const Modal: React.FC = ({ children, isOpen, setIsOpen }) => { 12 | const [modalStatus, setModalStatus] = useState(isOpen); 13 | 14 | useEffect(() => { 15 | setModalStatus(isOpen); 16 | }, [isOpen]); 17 | 18 | return ( 19 | 42 | {children} 43 | 44 | ); 45 | }; 46 | 47 | export default Modal; 48 | -------------------------------------------------------------------------------- /frontend/src/components/Modal/styles.css: -------------------------------------------------------------------------------- 1 | @keyframes openModal { 2 | from { 3 | opacity: 0; 4 | clip-path: inset(100% 100% 100% 100% round 16px); 5 | } 6 | to { 7 | opacity: 1; 8 | clip-path: inset(0px 0px 0px 0px round 16px); 9 | } 10 | } 11 | 12 | .ReactModal__Content { 13 | width: 100%; 14 | max-width: 1240px; 15 | height: 640px; 16 | padding: 56px 32px 0 56px !important; 17 | 18 | animation: openModal 0.4s; 19 | } 20 | 21 | @media (max-width: 1220px) { 22 | .ReactModal__Content { 23 | width: 93%; 24 | padding: 24px !important; 25 | } 26 | } 27 | 28 | @media (max-width: 768px) { 29 | .ReactModal__Content { 30 | height: 80vh; 31 | } 32 | } 33 | 34 | .ReactModal__Overlay { 35 | z-index: 9999; 36 | } 37 | 38 | .ReactModal__Body--open { 39 | overflow-y: hidden; 40 | } 41 | -------------------------------------------------------------------------------- /frontend/src/components/Scroll/index.ts: -------------------------------------------------------------------------------- 1 | import styled, { keyframes } from 'styled-components'; 2 | import PerfectScrollbar from 'react-perfect-scrollbar'; 3 | 4 | const scrollHeightAnimation = keyframes` 5 | from { 6 | height: 0%; 7 | opacity: 0; 8 | }, 9 | to { 10 | max-height: 583px; 11 | opacity: 1; 12 | } 13 | `; 14 | 15 | const Scroll = styled(PerfectScrollbar)` 16 | max-height: 583px; 17 | padding-right: 40px; 18 | width: 100%; 19 | 20 | display: flex; 21 | align-items: flex-start; 22 | 23 | .ps__rail-y { 24 | /* margin: 32px 0px; */ 25 | width: 4px; 26 | background: rgba(255, 255, 255, 0.1); 27 | border-radius: 5px; 28 | 29 | opacity: 1 !important; 30 | animation: ${scrollHeightAnimation} 1s forwards 31 | cubic-bezier(0.19, 1, 0.22, 1); 32 | animation-delay: 2s; 33 | 34 | &:hover { 35 | opacity: 1; 36 | background: inherit; 37 | 38 | > .ps__thumb-y { 39 | width: 4px; 40 | } 41 | } 42 | 43 | .ps__thumb-y { 44 | background: #33ff7a; 45 | width: 4px; 46 | height: 60px; 47 | right: 0px; 48 | 49 | &:hover { 50 | width: 4px; 51 | } 52 | } 53 | } 54 | 55 | @media (max-width: 1200px) { 56 | .ps__rail-y { 57 | display: none; 58 | } 59 | } 60 | 61 | @media (max-width: 992px) { 62 | padding-right: 0px; 63 | } 64 | 65 | @media (max-width: 768px) { 66 | max-height: 77vh; 67 | flex-direction: column; 68 | } 69 | `; 70 | 71 | export default Scroll; 72 | -------------------------------------------------------------------------------- /frontend/src/components/Spinner/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Container } from './styles'; 4 | 5 | interface ISpinnerProps { 6 | width?: number; 7 | height?: number; 8 | } 9 | 10 | const Spinner: React.FC = ({ width = 60, height = 60 }) => { 11 | return ( 12 | 13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | 26 | ); 27 | }; 28 | 29 | export default Spinner; 30 | -------------------------------------------------------------------------------- /frontend/src/components/Spinner/styles.ts: -------------------------------------------------------------------------------- 1 | import styled, { keyframes } from 'styled-components'; 2 | 3 | const circleBounceDelay = keyframes` 4 | 0%, 5 | 80%, 6 | 100% { 7 | -webkit-transform: scale(0); 8 | transform: scale(0); 9 | } 10 | 40% { 11 | -webkit-transform: scale(1); 12 | transform: scale(1); 13 | } 14 | `; 15 | 16 | export const Container = styled.div` 17 | margin: 0 auto; 18 | position: absolute; 19 | left: 50%; 20 | top: 50%; 21 | transform: translate(-50%, -50%); 22 | 23 | .sk-child { 24 | width: 100%; 25 | height: 100%; 26 | position: absolute; 27 | left: 0; 28 | top: 0; 29 | } 30 | 31 | .sk-child:before { 32 | content: ''; 33 | display: block; 34 | margin: 0 auto; 35 | width: 15%; 36 | height: 15%; 37 | background-color: #fff; 38 | border-radius: 100%; 39 | -webkit-animation: ${circleBounceDelay} 1.2s infinite ease-in-out both; 40 | animation: ${circleBounceDelay} 1.2s infinite ease-in-out both; 41 | } 42 | 43 | .sk-circle2 { 44 | -webkit-transform: rotate(30deg); 45 | -ms-transform: rotate(30deg); 46 | transform: rotate(30deg); 47 | } 48 | 49 | .sk-circle3 { 50 | -webkit-transform: rotate(60deg); 51 | -ms-transform: rotate(60deg); 52 | transform: rotate(60deg); 53 | } 54 | 55 | .sk-circle4 { 56 | -webkit-transform: rotate(90deg); 57 | -ms-transform: rotate(90deg); 58 | transform: rotate(90deg); 59 | } 60 | 61 | .sk-circle5 { 62 | -webkit-transform: rotate(120deg); 63 | -ms-transform: rotate(120deg); 64 | transform: rotate(120deg); 65 | } 66 | 67 | .sk-circle6 { 68 | -webkit-transform: rotate(150deg); 69 | -ms-transform: rotate(150deg); 70 | transform: rotate(150deg); 71 | } 72 | 73 | .sk-circle7 { 74 | -webkit-transform: rotate(180deg); 75 | -ms-transform: rotate(180deg); 76 | transform: rotate(180deg); 77 | } 78 | 79 | .sk-circle8 { 80 | -webkit-transform: rotate(210deg); 81 | -ms-transform: rotate(210deg); 82 | transform: rotate(210deg); 83 | } 84 | 85 | .sk-circle9 { 86 | -webkit-transform: rotate(240deg); 87 | -ms-transform: rotate(240deg); 88 | transform: rotate(240deg); 89 | } 90 | .sk-circle10 { 91 | -webkit-transform: rotate(270deg); 92 | -ms-transform: rotate(270deg); 93 | transform: rotate(270deg); 94 | } 95 | 96 | .sk-circle11 { 97 | -webkit-transform: rotate(300deg); 98 | -ms-transform: rotate(300deg); 99 | transform: rotate(300deg); 100 | } 101 | 102 | .sk-circle12 { 103 | -webkit-transform: rotate(330deg); 104 | -ms-transform: rotate(330deg); 105 | transform: rotate(330deg); 106 | } 107 | 108 | .sk-circle2:before { 109 | -webkit-animation-delay: -1.1s; 110 | animation-delay: -1.1s; 111 | } 112 | 113 | .sk-circle3:before { 114 | -webkit-animation-delay: -1s; 115 | animation-delay: -1s; 116 | } 117 | 118 | .sk-circle4:before { 119 | -webkit-animation-delay: -0.9s; 120 | animation-delay: -0.9s; 121 | } 122 | 123 | .sk-circle5:before { 124 | -webkit-animation-delay: -0.8s; 125 | animation-delay: -0.8s; 126 | } 127 | 128 | .sk-circle6:before { 129 | -webkit-animation-delay: -0.7s; 130 | animation-delay: -0.7s; 131 | } 132 | 133 | .sk-circle7:before { 134 | -webkit-animation-delay: -0.6s; 135 | animation-delay: -0.6s; 136 | } 137 | 138 | .sk-circle8:before { 139 | -webkit-animation-delay: -0.5s; 140 | animation-delay: -0.5s; 141 | } 142 | 143 | .sk-circle9:before { 144 | -webkit-animation-delay: -0.4s; 145 | animation-delay: -0.4s; 146 | } 147 | 148 | .sk-circle10:before { 149 | -webkit-animation-delay: -0.3s; 150 | animation-delay: -0.3s; 151 | } 152 | 153 | .sk-circle11:before { 154 | -webkit-animation-delay: -0.2s; 155 | animation-delay: -0.2s; 156 | } 157 | 158 | .sk-circle12:before { 159 | -webkit-animation-delay: -0.1s; 160 | animation-delay: -0.1s; 161 | } 162 | 163 | @media (max-width: 992px) { 164 | .sk-child { 165 | width: 85%; 166 | height: 85%; 167 | 168 | &:before { 169 | width: 13%; 170 | height: 13%; 171 | } 172 | } 173 | } 174 | `; 175 | -------------------------------------------------------------------------------- /frontend/src/components/SpotifyButton/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FaSpotify } from 'react-icons/fa'; 3 | 4 | import { Container } from './styles'; 5 | 6 | interface ISpotifyButtonProps { 7 | href: string; 8 | } 9 | 10 | const SpotifyButton: React.FC = ({ children, href }) => { 11 | return ( 12 | 13 | 14 | {children} 15 | 16 | ); 17 | }; 18 | 19 | export default SpotifyButton; 20 | -------------------------------------------------------------------------------- /frontend/src/components/SpotifyButton/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { lighten } from 'polished'; 3 | 4 | export const Container = styled.a` 5 | background: #1db954; 6 | color: #fff; 7 | font-size: 18px; 8 | font-weight: bold; 9 | padding: 32px 48px; 10 | border-radius: 10px; 11 | transition: background 0.2s; 12 | 13 | display: flex; 14 | align-items: center; 15 | justify-content: center; 16 | 17 | &:hover { 18 | background: ${lighten(0.03, '#1db954')}; 19 | } 20 | 21 | @media (max-width: 992px) { 22 | font-size: 14px; 23 | padding: 24px 40px; 24 | } 25 | 26 | svg { 27 | margin-right: 16px; 28 | width: 24px; 29 | height: 24px; 30 | 31 | @media (max-width: 992px) { 32 | width: 20px; 33 | height: 20px; 34 | } 35 | } 36 | `; 37 | -------------------------------------------------------------------------------- /frontend/src/hooks/auth.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useCallback, useState, useContext } from 'react'; 2 | import { addHours } from 'date-fns'; 3 | 4 | import api from '../services/api'; 5 | 6 | interface IUserData { 7 | id: string; 8 | type: string; 9 | display_name: string; 10 | email: string; 11 | avatar: string; 12 | } 13 | 14 | interface IAuthState { 15 | access_token: string; 16 | user: IUserData; 17 | exp: Date; 18 | } 19 | 20 | interface IAuthContextData { 21 | user: IUserData; 22 | exp: Date; 23 | getCredentials(): void; 24 | signOut(): void; 25 | } 26 | 27 | const AuthContext = createContext({} as IAuthContextData); 28 | 29 | export const AuthProvider: React.FC = ({ children }) => { 30 | const [data, setData] = useState(() => { 31 | const access_token = localStorage.getItem('@Spotify:access_token'); 32 | const user = localStorage.getItem('@Spotify:user'); 33 | const exp = localStorage.getItem('@Spotify:exp'); 34 | 35 | if (access_token && user && exp) { 36 | api.defaults.headers.authorization = `Bearer ${access_token}`; 37 | 38 | return { access_token, user: JSON.parse(user), exp: JSON.parse(exp) }; 39 | } 40 | 41 | return {} as IAuthState; 42 | }); 43 | 44 | const getCredentials = useCallback(async () => { 45 | const hashParams = {} as any; 46 | 47 | const query = window.location.search.replace('?', ''); 48 | const entries = query.split('&'); 49 | 50 | entries.forEach(entry => { 51 | const [key, value] = entry.split('='); 52 | hashParams[key] = value; 53 | }); 54 | 55 | const { access_token } = hashParams; 56 | 57 | api.defaults.headers.authorization = `Bearer ${access_token}`; 58 | 59 | const response = await api.get('/me'); 60 | 61 | localStorage.setItem('@Spotify:access_token', access_token); 62 | localStorage.setItem('@Spotify:user', JSON.stringify(response.data)); 63 | 64 | const exp = addHours(new Date(), 1); 65 | 66 | localStorage.setItem('@Spotify:exp', JSON.stringify(exp)); 67 | 68 | setData({ 69 | access_token, 70 | user: response.data, 71 | exp, 72 | }); 73 | 74 | window.location.href = '/top-artists'; 75 | }, []); 76 | 77 | const signOut = useCallback(() => { 78 | localStorage.removeItem('@Spotify:access_token'); 79 | localStorage.removeItem('@Spotify:user'); 80 | localStorage.removeItem('@Spotify:exp'); 81 | 82 | setData({} as IAuthState); 83 | }, []); 84 | 85 | return ( 86 | 89 | {children} 90 | 91 | ); 92 | }; 93 | 94 | export function useAuth(): IAuthContextData { 95 | const context = useContext(AuthContext); 96 | 97 | if (!context) { 98 | throw new Error('useAuth must be used within an AuthProvider'); 99 | } 100 | 101 | return context; 102 | } 103 | -------------------------------------------------------------------------------- /frontend/src/hooks/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { AuthProvider } from './auth'; 4 | 5 | const AppProvider: React.FC = ({ children }) => { 6 | return {children}; 7 | }; 8 | 9 | export default AppProvider; 10 | -------------------------------------------------------------------------------- /frontend/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | ReactDOM.render( 6 | 7 | 8 | , 9 | document.getElementById('root'), 10 | ); 11 | -------------------------------------------------------------------------------- /frontend/src/pages/Artists/ModalArtist/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useCallback } from 'react'; 2 | import { useTransition } from 'react-spring'; 3 | import { 4 | FaSpotify, 5 | FaTimes, 6 | FaChevronLeft, 7 | FaChevronRight, 8 | FaPlayCircle, 9 | FaPauseCircle, 10 | } from 'react-icons/fa'; 11 | import { toast } from 'react-toastify'; 12 | 13 | import api from '../../../services/api'; 14 | 15 | import formatValue from '../../../utils/formatValue'; 16 | 17 | import Modal from '../../../components/Modal'; 18 | import SpotifyButton from '../../../components/SpotifyButton'; 19 | import Spinner from '../../../components/Spinner'; 20 | import Scroll from '../../../components/Scroll'; 21 | import Seguidores from '../../../components/AnimatedVectors/Seguidores'; 22 | import PoucoEscutado from '../../../components/AnimatedVectors/PoucoEscutado'; 23 | import ChamandoAtencao from '../../../components/AnimatedVectors/ChamandoAtencao'; 24 | import BemConhecido from '../../../components/AnimatedVectors/BemConhecido'; 25 | import Popular from '../../../components/AnimatedVectors/Popular'; 26 | import Estourando from '../../../components/AnimatedVectors/Estourando'; 27 | 28 | import getPopularity from '../../../utils/getPopularity'; 29 | import { playAudioWithFade, pauseAudioWithFade } from '../../../utils/audio'; 30 | 31 | import { 32 | Container, 33 | LeftContent, 34 | Content, 35 | Genres, 36 | ArtistInfo, 37 | ArtistTopTracks, 38 | TopTracksList, 39 | TopTrack, 40 | CloseModal, 41 | RelatedArtists, 42 | RelatedArtistsList, 43 | RelatedArtist, 44 | } from './styles'; 45 | 46 | interface IModalProps { 47 | isOpen: boolean; 48 | setIsOpen: () => void; 49 | artistId: string; 50 | } 51 | 52 | interface ITopTrack { 53 | id: string; 54 | image: string; 55 | name: string; 56 | preview_url: string; 57 | audio: HTMLAudioElement; 58 | uri: string; 59 | playing: number; 60 | } 61 | 62 | interface IRelatedArtist { 63 | id: string; 64 | name: string; 65 | image: string; 66 | uri: string; 67 | genres: string[]; 68 | } 69 | 70 | interface IArtist { 71 | id: string; 72 | name: string; 73 | followers: number; 74 | formattedFollowers: number; 75 | genres: string[]; 76 | avatar: string; 77 | popularity: number; 78 | popularityTag: string; 79 | uri: string; 80 | topTracks: ITopTrack[]; 81 | } 82 | 83 | const ModalArtist: React.FC = ({ 84 | isOpen, 85 | setIsOpen, 86 | artistId, 87 | }) => { 88 | const [artist, setArtist] = useState({} as IArtist); 89 | const [topTracks, setTopTracks] = useState([]); 90 | const [relatedArtists, setRelatedArtists] = useState([]); 91 | const [loading, setLoading] = useState(true); 92 | const [slideTracks, setSlideTracks] = useState(false); 93 | const [slideRelatedArtists, setSlideRelatedArtists] = useState(false); 94 | 95 | useEffect(() => { 96 | async function loadArtist(): Promise { 97 | try { 98 | const { data } = await api.get(`artist/${artistId}`); 99 | 100 | const popularity = getPopularity(data.popularity); 101 | 102 | const artistData = { 103 | ...data, 104 | popularityTag: popularity, 105 | formattedFollowers: formatValue(data.followers), 106 | }; 107 | 108 | const topTracksFormatted = data.topTracks 109 | .slice(0, 6) 110 | .map((track: ITopTrack) => ({ 111 | ...track, 112 | audio: new Audio(`${track.preview_url}`), 113 | playing: 0, 114 | })); 115 | 116 | setArtist(artistData); 117 | setTopTracks(topTracksFormatted); 118 | setRelatedArtists(data.relatedArtists.slice(0, 6)); 119 | } catch (err) { 120 | toast.error('Não foi possível carregar as informações do artista.'); 121 | } finally { 122 | setLoading(false); 123 | } 124 | } 125 | 126 | loadArtist(); 127 | }, [artistId]); 128 | 129 | const handleSlideTracks = useCallback(() => { 130 | setSlideTracks(!slideTracks); 131 | }, [slideTracks]); 132 | 133 | const handleSlideRelatedArtists = useCallback(() => { 134 | setSlideRelatedArtists(!slideRelatedArtists); 135 | }, [slideRelatedArtists]); 136 | 137 | const handlePlay = useCallback( 138 | id => { 139 | setTopTracks( 140 | topTracks.map(track => 141 | track.id === id ? { ...track, playing: 1 } : track, 142 | ), 143 | ); 144 | }, 145 | [topTracks], 146 | ); 147 | 148 | const handlePause = useCallback( 149 | id => { 150 | setTopTracks( 151 | topTracks.map(track => 152 | track.id === id ? { ...track, playing: 0 } : track, 153 | ), 154 | ); 155 | }, 156 | [topTracks], 157 | ); 158 | 159 | const topTracksWithTransition = useTransition(topTracks, track => track.id, { 160 | from: { 161 | opacity: 0, 162 | transform: 'scale(0.8)', 163 | }, 164 | enter: { 165 | opacity: 1, 166 | transform: 'scale(1)', 167 | }, 168 | trail: 150, 169 | }); 170 | 171 | const relatedArtistsWithTransition = useTransition( 172 | relatedArtists, 173 | related => related.id, 174 | { 175 | from: { 176 | opacity: 0, 177 | transform: 'scale(0.8)', 178 | }, 179 | enter: { 180 | opacity: 1, 181 | transform: 'scale(1)', 182 | }, 183 | trail: 150, 184 | }, 185 | ); 186 | 187 | return ( 188 | 189 | 190 | {loading ? ( 191 | 192 | ) : ( 193 | <> 194 | 195 | 196 |
197 | {artist.name} 198 |
199 | 200 | 201 | Abrir no Spotify 202 | 203 |
204 | 205 | 206 |

{artist.name}

207 | 208 | {artist.genres.map(genre => ( 209 | {genre} 210 | ))} 211 | 212 | 213 | 214 |
215 | {artist.popularityTag === 'Pouco escutado' && ( 216 | 217 | )} 218 | {artist.popularityTag === 'Chamando Atenção' && ( 219 | 220 | )} 221 | {artist.popularityTag === 'Bem conhecido' && ( 222 | 223 | )} 224 | {artist.popularityTag === 'Popular' && } 225 | {artist.popularityTag === 'Estourando' && } 226 | 227 |
228 | Popularidade 229 | {artist.popularityTag} 230 |
231 |
232 | 233 |
234 | 235 | 236 |
237 | Seguidores 238 | {artist.formattedFollowers} 239 |
240 |
241 |
242 | 243 | 244 |
245 |

Músicas mais tocadas

246 | 247 | 263 |
264 | 265 | 266 | {topTracksWithTransition.map( 267 | ({ item, key, props }, index) => ( 268 | { 273 | pauseAudioWithFade(item.audio, 250); 274 | handlePause(item.id); 275 | }} 276 | > 277 | {item.name} 278 | 279 |
280 | 281 | {index + 1}. {item.name} 282 | 283 | 284 |
285 | 295 | 305 | 306 | 307 | 308 | 309 |
310 |
311 |
312 | ), 313 | )} 314 |
315 |
316 | 317 | 318 |
319 |

Artistas relacionados

320 | 321 | 337 |
338 | 339 | 340 | {relatedArtistsWithTransition.map( 341 | ({ item, key, props }, index) => ( 342 | 343 | {item.name} 344 | 345 |
346 | 347 | {index + 1}. {item.name} 348 | 349 | 350 | 357 |
358 |
359 | ), 360 | )} 361 |
362 |
363 |
364 |
365 | 366 | 367 | 368 | 369 | 370 | )} 371 |
372 |
373 | ); 374 | }; 375 | 376 | export default ModalArtist; 377 | -------------------------------------------------------------------------------- /frontend/src/pages/Artists/ModalArtist/styles.ts: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components'; 2 | import { animated } from 'react-spring'; 3 | 4 | import { fade, fadeUp, fadeScaleDown } from '../../../styles/animations'; 5 | 6 | export const Container = styled.div` 7 | display: flex; 8 | `; 9 | 10 | export const LeftContent = styled.aside` 11 | padding-bottom: 56px; 12 | margin-right: 104px; 13 | 14 | position: sticky; 15 | top: 10px; 16 | 17 | div { 18 | width: 400px; 19 | height: 400px; 20 | margin-bottom: 16px; 21 | border-radius: 10px; 22 | overflow: hidden; 23 | 24 | img { 25 | width: 100%; 26 | height: 100%; 27 | border-radius: 10px; 28 | 29 | opacity: 0; 30 | animation: ${fadeScaleDown} 1.3s forwards cubic-bezier(0.19, 0.8, 0.28, 1); 31 | animation-delay: 0.1s; 32 | } 33 | } 34 | 35 | a { 36 | width: 100%; 37 | 38 | opacity: 0; 39 | animation: ${fadeUp} 1.5s forwards cubic-bezier(0.19, 1, 0.22, 1); 40 | animation-delay: 0.4s; 41 | 42 | justify-content: center; 43 | } 44 | 45 | @media (max-width: 1220px) { 46 | margin-right: 48px; 47 | } 48 | 49 | @media (max-width: 992px) { 50 | padding-bottom: 24px; 51 | width: 100%; 52 | 53 | div { 54 | width: 100%; 55 | height: 100%; 56 | } 57 | } 58 | 59 | @media (max-width: 768px) { 60 | position: initial; 61 | } 62 | `; 63 | 64 | export const Content = styled.div` 65 | width: 100%; 66 | padding-bottom: 32px; 67 | 68 | display: flex; 69 | flex-direction: column; 70 | 71 | h1 { 72 | font-size: 56px; 73 | line-height: 1.14; 74 | color: #fff; 75 | 76 | opacity: 0; 77 | animation: ${fadeUp} 1.5s forwards cubic-bezier(0.19, 1, 0.22, 1); 78 | animation-delay: 0.7s; 79 | } 80 | 81 | @media (max-width: 992px) { 82 | h1 { 83 | font-size: 40px; 84 | } 85 | } 86 | `; 87 | 88 | export const Genres = styled.div` 89 | margin: 24px 0; 90 | 91 | display: flex; 92 | align-items: center; 93 | 94 | span { 95 | background: #424548; 96 | color: #fff; 97 | font-weight: 500; 98 | padding: 8px 16px; 99 | border-radius: 24px; 100 | font-size: 14px; 101 | 102 | opacity: 0; 103 | animation: ${fadeUp} 1.5s forwards cubic-bezier(0.19, 1, 0.22, 1); 104 | animation-delay: 0.9s; 105 | 106 | & + span { 107 | margin-left: 16px; 108 | } 109 | } 110 | 111 | @media (max-width: 992px) { 112 | margin: 16px 0; 113 | flex-wrap: wrap; 114 | 115 | span { 116 | padding: 8px 12px; 117 | font-size: 12px; 118 | 119 | & + span { 120 | margin-left: 0px; 121 | } 122 | } 123 | } 124 | `; 125 | 126 | export const ArtistInfo = styled.section` 127 | width: 100%; 128 | 129 | display: flex; 130 | align-items: center; 131 | 132 | .followers { 133 | margin-left: 24px; 134 | } 135 | 136 | > div { 137 | background: #2d2f30; 138 | border-radius: 10px; 139 | width: 100%; 140 | 141 | opacity: 0; 142 | animation: ${fadeUp} 1.5s forwards cubic-bezier(0.19, 1, 0.22, 1); 143 | 144 | display: flex; 145 | align-items: center; 146 | 147 | &:nth-child(1) { 148 | animation-delay: 1.1s; 149 | } 150 | 151 | &:nth-child(2) { 152 | animation-delay: 1.2s; 153 | } 154 | 155 | div[role='button'] { 156 | width: 80px !important; 157 | height: 80px !important; 158 | margin: 0 !important; 159 | } 160 | 161 | .info { 162 | display: flex; 163 | flex-direction: column; 164 | 165 | span { 166 | font-size: 14px; 167 | font-weight: bold; 168 | } 169 | 170 | strong { 171 | color: #fff; 172 | margin-top: 8px; 173 | font-size: 18px; 174 | } 175 | } 176 | } 177 | 178 | @media (max-width: 1200px) { 179 | flex-direction: column; 180 | 181 | .followers { 182 | margin-left: 0px; 183 | margin-top: 12px; 184 | } 185 | } 186 | 187 | @media (max-width: 992px) { 188 | > div { 189 | div[role='button'] { 190 | width: 64px !important; 191 | height: 64px !important; 192 | } 193 | 194 | .info { 195 | span { 196 | font-size: 12px; 197 | } 198 | 199 | strong { 200 | font-size: 16px; 201 | margin-top: 0px; 202 | } 203 | } 204 | } 205 | } 206 | `; 207 | 208 | export const ArtistTopTracks = styled.section` 209 | width: 100%; 210 | margin-top: 32px; 211 | overflow-x: hidden; 212 | 213 | display: flex; 214 | flex-direction: column; 215 | 216 | > div { 217 | margin-bottom: 24px; 218 | 219 | opacity: 0; 220 | animation: ${fadeUp} 1.5s forwards cubic-bezier(0.19, 1, 0.22, 1); 221 | animation-delay: 1.3s; 222 | 223 | display: flex; 224 | justify-content: space-between; 225 | 226 | h3 { 227 | color: #fff; 228 | } 229 | 230 | nav { 231 | display: flex; 232 | 233 | button { 234 | background: none; 235 | border: 0; 236 | 237 | &:disabled { 238 | pointer-events: none; 239 | } 240 | 241 | & + button { 242 | margin-left: 32px; 243 | } 244 | 245 | svg { 246 | color: #7a8185; 247 | transition: color 0.2s; 248 | 249 | &:hover { 250 | color: #fff; 251 | } 252 | } 253 | } 254 | } 255 | } 256 | 257 | @media (max-width: 992px) { 258 | margin-top: 24px; 259 | 260 | > div { 261 | h3 { 262 | font-size: 18px; 263 | } 264 | 265 | button { 266 | & + button { 267 | margin-left: 24px; 268 | } 269 | } 270 | } 271 | } 272 | `; 273 | 274 | interface ITopTracksList { 275 | slideTracks: boolean; 276 | } 277 | 278 | export const TopTracksList = styled.ul` 279 | max-width: 608px; 280 | width: 100%; 281 | 282 | display: flex; 283 | 284 | transform: ${props => 285 | props.slideTracks 286 | ? 'translate3d(calc(-100% - 15px), 0px, 0px)' 287 | : 'translate3d(calc(0% - 0px), 0px, 0px)'}; 288 | transition: transform 1s cubic-bezier(0.19, 1, 0.22, 1); 289 | `; 290 | 291 | interface IIsPlaying { 292 | playing: number; 293 | } 294 | 295 | export const TopTrack = styled(animated.li)` 296 | min-width: 192px; 297 | height: 256px; 298 | position: relative; 299 | border-radius: 10px; 300 | overflow: hidden; 301 | 302 | transition: transform 1.5s cubic-bezier(0.19, 1, 0.22, 1), 303 | opacity 1s cubic-bezier(0.19, 1, 0.22, 1); 304 | transition-delay: 0.8s; 305 | 306 | display: flex; 307 | flex-direction: column; 308 | 309 | & + li { 310 | margin-left: 16px; 311 | } 312 | 313 | &:hover { 314 | img { 315 | transform: scale(1.1); 316 | } 317 | 318 | div { 319 | height: 108px; 320 | 321 | strong { 322 | position: absolute; 323 | top: 0; 324 | 325 | margin-top: 16px; 326 | height: 40px; 327 | white-space: normal; 328 | } 329 | 330 | button, 331 | a { 332 | visibility: visible; 333 | opacity: 1; 334 | } 335 | } 336 | } 337 | 338 | img { 339 | width: 100%; 340 | border-top-left-radius: 10px; 341 | border-top-right-radius: 10px; 342 | transition: transform 0.3s; 343 | } 344 | 345 | div { 346 | position: absolute; 347 | bottom: 0; 348 | 349 | background: #272727; 350 | border-bottom-left-radius: 10px; 351 | border-bottom-right-radius: 10px; 352 | height: 64px; 353 | width: 100%; 354 | padding: 0 16px; 355 | z-index: 99; 356 | transition: height 0.3s; 357 | 358 | display: flex; 359 | align-items: center; 360 | 361 | strong { 362 | display: block; 363 | color: #fff; 364 | white-space: nowrap; 365 | text-overflow: ellipsis; 366 | overflow: hidden; 367 | } 368 | 369 | footer { 370 | margin-top: 48px; 371 | 372 | display: flex; 373 | align-items: center; 374 | 375 | button { 376 | visibility: hidden; 377 | background: none; 378 | border: 0; 379 | transition: transform 0.2s; 380 | 381 | &:hover { 382 | transform: scale(1.2); 383 | } 384 | } 385 | 386 | ${props => 387 | props.playing 388 | ? css` 389 | .playButton { 390 | display: none; 391 | } 392 | 393 | .pauseButton { 394 | display: block; 395 | } 396 | ` 397 | : css` 398 | .playButton { 399 | display: block; 400 | } 401 | 402 | .pauseButton { 403 | display: none; 404 | } 405 | `} 406 | 407 | a { 408 | margin-left: 8px; 409 | opacity: 0; 410 | visibility: hidden; 411 | transition: transform 0.2s; 412 | 413 | &:hover { 414 | transform: scale(1.2); 415 | } 416 | } 417 | } 418 | } 419 | `; 420 | 421 | export const RelatedArtists = styled.section` 422 | width: 100%; 423 | margin-top: 32px; 424 | overflow-x: hidden; 425 | 426 | display: flex; 427 | flex-direction: column; 428 | 429 | > div { 430 | margin-bottom: 24px; 431 | 432 | opacity: 0; 433 | animation: ${fadeUp} 1.5s forwards cubic-bezier(0.19, 1, 0.22, 1); 434 | animation-delay: 1.5s; 435 | 436 | display: flex; 437 | justify-content: space-between; 438 | 439 | h3 { 440 | color: #fff; 441 | } 442 | 443 | nav { 444 | display: flex; 445 | 446 | button { 447 | background: none; 448 | border: 0; 449 | 450 | &:disabled { 451 | pointer-events: none; 452 | } 453 | 454 | & + button { 455 | margin-left: 32px; 456 | } 457 | 458 | svg { 459 | color: #7a8185; 460 | transition: color 0.2s; 461 | 462 | &:hover { 463 | color: #fff; 464 | } 465 | } 466 | } 467 | } 468 | } 469 | 470 | @media (max-width: 992px) { 471 | margin-top: 24px; 472 | 473 | > div { 474 | h3 { 475 | font-size: 18px; 476 | } 477 | 478 | button { 479 | & + button { 480 | margin-left: 24px; 481 | } 482 | } 483 | } 484 | } 485 | `; 486 | 487 | interface IRelatedArtistsList { 488 | slideRelatedArtists: boolean; 489 | } 490 | 491 | export const RelatedArtistsList = styled.ul` 492 | max-width: 608px; 493 | width: 100%; 494 | 495 | display: flex; 496 | 497 | transform: ${props => 498 | props.slideRelatedArtists 499 | ? 'translate3d(calc(-100% - 15px), 0px, 0px)' 500 | : 'translate3d(calc(0% - 0px), 0px, 0px)'}; 501 | transition: transform 1s cubic-bezier(0.19, 1, 0.22, 1); 502 | `; 503 | 504 | export const RelatedArtist = styled(animated.li)` 505 | min-width: 192px; 506 | height: 256px; 507 | position: relative; 508 | border-radius: 10px; 509 | overflow: hidden; 510 | 511 | transition: transform 1.5s cubic-bezier(0.19, 1, 0.22, 1), 512 | opacity 1s cubic-bezier(0.19, 1, 0.22, 1); 513 | transition-delay: 1.4s; 514 | 515 | display: flex; 516 | flex-direction: column; 517 | 518 | & + li { 519 | margin-left: 16px; 520 | } 521 | 522 | &:hover { 523 | img { 524 | transform: scale(1.1); 525 | } 526 | 527 | div { 528 | height: 132px; 529 | 530 | strong { 531 | position: absolute; 532 | top: 0; 533 | 534 | margin-top: 16px; 535 | } 536 | 537 | footer { 538 | display: flex; 539 | flex-direction: column; 540 | align-items: flex-start; 541 | 542 | p, 543 | a { 544 | opacity: 1; 545 | visibility: visible; 546 | } 547 | } 548 | } 549 | } 550 | 551 | img { 552 | width: 100%; 553 | border-top-left-radius: 10px; 554 | border-top-right-radius: 10px; 555 | transition: transform 0.3s; 556 | } 557 | 558 | div { 559 | position: absolute; 560 | bottom: 0; 561 | 562 | background: #272727; 563 | border-bottom-left-radius: 10px; 564 | border-bottom-right-radius: 10px; 565 | height: 64px; 566 | width: 100%; 567 | padding: 0 16px; 568 | z-index: 99; 569 | transition: height 0.3s; 570 | 571 | display: flex; 572 | align-items: center; 573 | 574 | strong { 575 | display: block; 576 | color: #fff; 577 | white-space: nowrap; 578 | text-overflow: ellipsis; 579 | overflow: hidden; 580 | } 581 | 582 | footer { 583 | margin-top: 48px; 584 | 585 | display: none; 586 | 587 | p { 588 | visibility: hidden; 589 | opacity: 0; 590 | font-size: 12px; 591 | color: #7a8185; 592 | font-weight: 500; 593 | line-height: 1.3; 594 | 595 | position: absolute; 596 | top: 44px; 597 | } 598 | 599 | a { 600 | visibility: hidden; 601 | opacity: 0; 602 | margin-top: 24px; 603 | transition: transform 0.2s; 604 | 605 | &:hover { 606 | transform: scale(1.2); 607 | } 608 | } 609 | } 610 | } 611 | `; 612 | 613 | export const CloseModal = styled.button` 614 | background: transparent; 615 | border: 0; 616 | position: absolute; 617 | right: 16px; 618 | top: 40px; 619 | margin-right: 4px; 620 | transition: transform 0.2s; 621 | 622 | opacity: 0; 623 | animation: ${fade} 1s forwards cubic-bezier(0.19, 1, 0.22, 1); 624 | animation-delay: 0.8s; 625 | 626 | display: flex; 627 | justify-content: center; 628 | align-items: center; 629 | 630 | &:hover { 631 | transform: scale(1.2); 632 | } 633 | 634 | svg { 635 | width: 28px; 636 | height: 28px; 637 | } 638 | 639 | @media (max-width: 992px) { 640 | margin-right: 0; 641 | top: 8px; 642 | right: 8px; 643 | 644 | svg { 645 | width: 22px; 646 | height: 22px; 647 | } 648 | } 649 | `; 650 | -------------------------------------------------------------------------------- /frontend/src/pages/Artists/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useCallback } from 'react'; 2 | import { useTransition } from 'react-spring'; 3 | import { GiMicrophone } from 'react-icons/gi'; 4 | import { FaPlay } from 'react-icons/fa'; 5 | import { toast } from 'react-toastify'; 6 | 7 | import ModalArtist from './ModalArtist'; 8 | import LineGraphAnimated from '../../components/LineGraphAnimated'; 9 | import Spinner from '../../components/Spinner'; 10 | 11 | import formatValue from '../../utils/formatValue'; 12 | import getPopularity from '../../utils/getPopularity'; 13 | import { playAudioWithFade, pauseAudioWithFade } from '../../utils/audio'; 14 | 15 | import api from '../../services/api'; 16 | 17 | import { 18 | Container, 19 | LeftContent, 20 | TopArtists, 21 | Artist, 22 | ArtistInfo, 23 | } from './styles'; 24 | 25 | interface IImages { 26 | url: string; 27 | } 28 | 29 | interface IArtistFollwers { 30 | total: number; 31 | } 32 | 33 | interface ITopArtists { 34 | id: string; 35 | name: string; 36 | images: IImages[]; 37 | type: string; 38 | uri: string; 39 | followers: IArtistFollwers; 40 | formattedFollowers: number; 41 | popularity: number; 42 | popularityTag: string; 43 | audio: HTMLAudioElement; 44 | topTrackPreview: string; 45 | topTrackName: string; 46 | } 47 | 48 | const Artists: React.FC = () => { 49 | const [topArtists, setTopArtists] = useState([]); 50 | const [firstTopArtist, setFirstTopArtist] = useState( 51 | {} as ITopArtists, 52 | ); 53 | const [loading, setLoading] = useState(false); 54 | const [toggleModal, setToggleModal] = useState(false); 55 | const [artistId, setArtistId] = useState(''); 56 | 57 | useEffect(() => { 58 | async function loadTopArtists(): Promise { 59 | try { 60 | setLoading(true); 61 | 62 | const response = await api.get('/me/top-artists'); 63 | 64 | const data = response.data.map((artist: ITopArtists) => ({ 65 | ...artist, 66 | formattedFollowers: formatValue(artist.followers.total), 67 | popularityTag: getPopularity(artist.popularity), 68 | audio: new Audio(`${artist.topTrackPreview}`), 69 | })); 70 | 71 | setTopArtists(data); 72 | setFirstTopArtist(data[0]); 73 | setLoading(false); 74 | } catch (err) { 75 | toast.error('Não foi possível carregar os artistas.'); 76 | } finally { 77 | setLoading(false); 78 | } 79 | } 80 | 81 | loadTopArtists(); 82 | }, []); 83 | 84 | const handleModal = useCallback(() => { 85 | setToggleModal(!toggleModal); 86 | }, [toggleModal]); 87 | 88 | const artistsWithTransition = useTransition( 89 | topArtists, 90 | topArtist => topArtist.id, 91 | { 92 | from: { 93 | opacity: 0, 94 | transform: 'scale(0.8)', 95 | }, 96 | enter: { 97 | opacity: 1, 98 | transform: 'scale(1)', 99 | }, 100 | trail: 125, 101 | }, 102 | ); 103 | 104 | return ( 105 | 106 | {loading ? ( 107 | 108 | ) : ( 109 | <> 110 | {toggleModal && ( 111 | 116 | )} 117 | 118 | 119 |
120 | 121 |
122 |

123 | Escutando 124 | {firstTopArtist.name} 125 |

126 |

127 | Quando se trata dos seus artistas favoritos, ninguém faz igual a/o 128 | {firstTopArtist.name}! 129 |

130 |
131 | 132 | 133 | {artistsWithTransition.map(({ item, key, props }, index) => ( 134 | playAudioWithFade(item.audio)} 138 | onMouseLeave={() => pauseAudioWithFade(item.audio)} 139 | onClick={() => { 140 | setArtistId(item.id); 141 | handleModal(); 142 | }} 143 | > 144 | {item.name} 145 |
146 | #{index + 1} 147 |

{item.name}

148 |
149 |
150 | 151 | 152 |
153 | 154 | 155 |
156 | Seguidores 157 |

{item.formattedFollowers}

158 |
159 |
160 | Popularidade 161 |

{item.popularityTag}

162 |
163 |
164 | Mais tocada 165 |

{item.topTrackName}

166 |
167 |
168 |
169 | ))} 170 |
171 | 172 | )} 173 |
174 | ); 175 | }; 176 | 177 | export default Artists; 178 | -------------------------------------------------------------------------------- /frontend/src/pages/Artists/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { animated } from 'react-spring'; 3 | 4 | import { fadeUp } from '../../styles/animations'; 5 | 6 | export const Container = styled.div` 7 | display: flex; 8 | align-items: center; 9 | 10 | @media (max-width: 1220px) { 11 | flex-direction: column; 12 | justify-content: center; 13 | align-items: center; 14 | } 15 | `; 16 | 17 | export const LeftContent = styled.div` 18 | margin-right: 72px; 19 | 20 | display: flex; 21 | flex-direction: column; 22 | align-items: flex-start; 23 | 24 | div { 25 | background: linear-gradient(134.4deg, #20ac9a, #1db954 52%, #91c040); 26 | padding: 16px; 27 | border-radius: 10px; 28 | 29 | opacity: 0; 30 | animation: ${fadeUp} 1.5s forwards cubic-bezier(0.19, 1, 0.22, 1); 31 | animation-delay: 0.1s; 32 | 33 | display: flex; 34 | justify-content: center; 35 | align-items: center; 36 | } 37 | 38 | h1 { 39 | font-size: 64px; 40 | color: #fff; 41 | line-height: 1.25; 42 | margin: 32px 0; 43 | 44 | opacity: 0; 45 | animation: ${fadeUp} 1.5s forwards cubic-bezier(0.19, 1, 0.22, 1); 46 | animation-delay: 0.3s; 47 | 48 | display: flex; 49 | flex-wrap: wrap; 50 | 51 | .green { 52 | color: #33ff7a; 53 | } 54 | } 55 | 56 | p { 57 | font-size: 18px; 58 | line-height: 2; 59 | 60 | opacity: 0; 61 | animation: ${fadeUp} 1.5s forwards cubic-bezier(0.19, 1, 0.22, 1); 62 | animation-delay: 0.5s; 63 | } 64 | 65 | @media (max-width: 1220px) { 66 | position: relative; 67 | top: 0px; 68 | margin-right: 0px; 69 | margin-bottom: 32px; 70 | 71 | align-items: center; 72 | 73 | div { 74 | padding: 14px; 75 | 76 | svg { 77 | width: 24px; 78 | height: 24px; 79 | } 80 | } 81 | 82 | h1 { 83 | margin: 24px 0; 84 | font-size: 40px; 85 | text-align: center; 86 | 87 | display: flex; 88 | flex-direction: column; 89 | align-items: center; 90 | 91 | .green { 92 | width: 343px; 93 | } 94 | } 95 | 96 | p { 97 | font-size: 14px; 98 | text-align: center; 99 | } 100 | } 101 | `; 102 | 103 | export const TopArtists = styled.div` 104 | display: grid; 105 | grid-template-columns: auto auto auto auto auto auto; 106 | grid-gap: 24px; 107 | 108 | @media (max-width: 992px) { 109 | grid-template-columns: 1fr 1fr; 110 | } 111 | 112 | @media (max-width: 768px) { 113 | width: 320px; 114 | grid-template-columns: 1fr; 115 | } 116 | `; 117 | 118 | export const Artist = styled(animated.div)` 119 | position: relative; 120 | border-radius: 10px; 121 | overflow: hidden; 122 | width: 240px; 123 | height: 240px; 124 | cursor: pointer; 125 | 126 | transition: transform 1.5s cubic-bezier(0.19, 1, 0.22, 1), 127 | opacity 1s cubic-bezier(0.19, 1, 0.22, 1); 128 | transition-delay: 0.05s; 129 | 130 | &:hover > img { 131 | transform: scale(1.1); 132 | } 133 | 134 | &:nth-child(1) { 135 | grid-column-start: 1; 136 | grid-column-end: 3; 137 | } 138 | 139 | &:nth-child(2) { 140 | grid-column-start: 3; 141 | grid-column-end: 5; 142 | } 143 | 144 | &:nth-child(3) { 145 | grid-column-start: 5; 146 | grid-column-end: 7; 147 | } 148 | 149 | &:nth-child(4) { 150 | grid-row-start: 2; 151 | grid-column-start: 2; 152 | grid-column-end: 4; 153 | } 154 | 155 | &:nth-child(5) { 156 | grid-row-start: 2; 157 | grid-column-start: 4; 158 | grid-column-end: 6; 159 | } 160 | 161 | img { 162 | width: 100%; 163 | height: 100%; 164 | border-radius: 10px; 165 | opacity: 0.8; 166 | transition: all 0.3s; 167 | } 168 | 169 | .name { 170 | color: #fff; 171 | position: absolute; 172 | bottom: 16px; 173 | left: 16px; 174 | padding-right: 16px; 175 | 176 | display: flex; 177 | flex-direction: column; 178 | 179 | h3 { 180 | width: fit-content; 181 | font-size: 24px; 182 | border-radius: 12px; 183 | line-height: 1.2; 184 | } 185 | 186 | span { 187 | font-weight: bold; 188 | width: fit-content; 189 | } 190 | } 191 | 192 | &:hover .followers { 193 | visibility: visible; 194 | opacity: 1; 195 | transform: translateY(0); 196 | transition: transform 1.5s cubic-bezier(0.19, 1, 0.22, 1), 197 | visibility 1s cubic-bezier(0.19, 1, 0.22, 1), 198 | opacity 1s cubic-bezier(0.19, 1, 0.22, 1); 199 | transition-delay: 0.2s; 200 | } 201 | 202 | &:hover .popularity { 203 | visibility: visible; 204 | opacity: 1; 205 | transform: translateY(44px); 206 | transition: transform 1.5s cubic-bezier(0.19, 1, 0.22, 1), 207 | visibility 1s cubic-bezier(0.19, 1, 0.22, 1), 208 | opacity 1s cubic-bezier(0.19, 1, 0.22, 1); 209 | transition-delay: 0.4s; 210 | } 211 | 212 | &:hover .top-track { 213 | visibility: visible; 214 | opacity: 1; 215 | transform: translateY(88px); 216 | transition: transform 1.5s cubic-bezier(0.19, 1, 0.22, 1), 217 | visibility 1s cubic-bezier(0.19, 1, 0.22, 1), 218 | opacity 1s cubic-bezier(0.19, 1, 0.22, 1); 219 | transition-delay: 0.6s; 220 | } 221 | 222 | div.playingAnimationContainer { 223 | position: absolute; 224 | right: 16px; 225 | top: 16px; 226 | background: #121212; 227 | width: 25px; 228 | height: 25px; 229 | border-radius: 50%; 230 | 231 | display: flex; 232 | align-items: center; 233 | justify-content: center; 234 | 235 | svg { 236 | padding-left: 2px; 237 | } 238 | } 239 | 240 | .lineGraph { 241 | display: none; 242 | } 243 | 244 | &:hover svg { 245 | &.playCircle { 246 | display: none; 247 | } 248 | } 249 | 250 | &:hover .lineGraph { 251 | display: flex; 252 | } 253 | 254 | @media (max-width: 992px) { 255 | grid-column: initial !important; 256 | grid-row-start: initial !important; 257 | width: 100%; 258 | height: 100%; 259 | 260 | div.playingAnimationContainer { 261 | width: 40px; 262 | height: 40px; 263 | 264 | svg { 265 | padding-left: 3px; 266 | width: 20px; 267 | height: 20px; 268 | } 269 | } 270 | 271 | .lineGraph div { 272 | width: 4px; 273 | height: 18px; 274 | } 275 | 276 | .name { 277 | h3 { 278 | font-size: 32px; 279 | } 280 | 281 | span { 282 | font-size: 20px; 283 | } 284 | } 285 | 286 | &:hover .followers { 287 | transform: translateY(0); 288 | } 289 | 290 | &:hover .popularity { 291 | transform: translateY(70px); 292 | } 293 | 294 | &:hover .top-track { 295 | transform: translateY(140px); 296 | } 297 | } 298 | 299 | @media (max-width: 768px) { 300 | width: 320px; 301 | height: 320px; 302 | 303 | .name { 304 | h3 { 305 | font-size: 28px; 306 | } 307 | 308 | span { 309 | font-size: 16px; 310 | } 311 | } 312 | } 313 | `; 314 | 315 | export const ArtistInfo = styled.div` 316 | position: absolute; 317 | top: 16px; 318 | left: 16px; 319 | color: #fff; 320 | width: 200px; 321 | 322 | display: flex; 323 | flex-direction: column; 324 | 325 | .info { 326 | position: absolute; 327 | visibility: hidden; 328 | opacity: 0; 329 | width: 100%; 330 | 331 | span { 332 | font-weight: bold; 333 | font-size: 10px; 334 | } 335 | 336 | h4 { 337 | font-size: 14px; 338 | white-space: nowrap; 339 | overflow: hidden; 340 | text-overflow: ellipsis; 341 | } 342 | } 343 | 344 | .followers { 345 | transform: translateY(40px); 346 | } 347 | 348 | .popularity { 349 | transform: translateY(80px); 350 | } 351 | 352 | .top-track { 353 | transform: translateY(120px); 354 | } 355 | 356 | @media (max-width: 992px) { 357 | width: 300px; 358 | 359 | .info { 360 | span { 361 | font-size: 20px; 362 | } 363 | 364 | h4 { 365 | font-size: 24px; 366 | line-height: initial; 367 | } 368 | } 369 | 370 | .followers { 371 | transform: translateY(40px); 372 | } 373 | 374 | .popularity { 375 | transform: translateY(110px); 376 | } 377 | 378 | .top-track { 379 | transform: translateY(180px); 380 | } 381 | } 382 | 383 | @media (max-width: 768px) { 384 | .info { 385 | span { 386 | font-size: 18px; 387 | } 388 | 389 | h4 { 390 | font-size: 22px; 391 | line-height: initial; 392 | } 393 | } 394 | } 395 | `; 396 | -------------------------------------------------------------------------------- /frontend/src/pages/Authenticate/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { FaSpotify } from 'react-icons/fa'; 3 | 4 | import { useAuth } from '../../hooks/auth'; 5 | 6 | import { Container } from './styles'; 7 | 8 | const Authenticate: React.FC = () => { 9 | const { getCredentials } = useAuth(); 10 | 11 | useEffect(() => { 12 | getCredentials(); 13 | }, [getCredentials]); 14 | 15 | return ( 16 | 17 | 18 | 19 | ); 20 | }; 21 | 22 | export default Authenticate; 23 | -------------------------------------------------------------------------------- /frontend/src/pages/Authenticate/styles.ts: -------------------------------------------------------------------------------- 1 | import styled, { keyframes } from 'styled-components'; 2 | 3 | const logoAnimated = keyframes` 4 | 0% { 5 | transform: translateY(0); 6 | } 7 | 50% { 8 | transform: translateY(-24px); 9 | } 10 | 100% { 11 | transform: translateY(0px); 12 | } 13 | `; 14 | 15 | export const Container = styled.div` 16 | animation: ${logoAnimated} 1.5s infinite ease-in-out; 17 | `; 18 | -------------------------------------------------------------------------------- /frontend/src/pages/Error/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Error: React.FC = () => { 4 | return

Error

; 5 | }; 6 | 7 | export default Error; 8 | -------------------------------------------------------------------------------- /frontend/src/pages/FavoriteTracks/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useCallback } from 'react'; 2 | import { 3 | FaHeart, 4 | FaPauseCircle, 5 | FaPlayCircle, 6 | FaSpotify, 7 | } from 'react-icons/fa'; 8 | import { toast } from 'react-toastify'; 9 | import { useTransition } from 'react-spring'; 10 | 11 | import { Container, LeftContent, UserFavoriteTracks, Track } from './styles'; 12 | 13 | import api from '../../services/api'; 14 | 15 | import getPopularity from '../../utils/getPopularity'; 16 | import { playAudioWithFade, pauseAudioWithFade } from '../../utils/audio'; 17 | 18 | import Spinner from '../../components/Spinner'; 19 | 20 | interface IAlbum { 21 | id: string; 22 | name: string; 23 | uri: string; 24 | image: string; 25 | } 26 | 27 | interface IArtist { 28 | id: string; 29 | name: string; 30 | uri: string; 31 | } 32 | 33 | interface ITrack { 34 | id: string; 35 | name: string; 36 | popularity: number; 37 | popularityTag: string; 38 | uri: string; 39 | preview_url: string; 40 | audio: HTMLAudioElement; 41 | album: IAlbum; 42 | artist: IArtist; 43 | playing: number; 44 | } 45 | 46 | const FavoriteTracks: React.FC = () => { 47 | const [loading, setLoading] = useState(false); 48 | const [favoriteTracks, setFavoriteTracks] = useState([]); 49 | 50 | useEffect(() => { 51 | async function loadFavoriteTracks(): Promise { 52 | try { 53 | setLoading(true); 54 | 55 | const response = await api.get('/me/favorite-tracks'); 56 | 57 | const tracksData = response.data.map((track: ITrack) => ({ 58 | ...track, 59 | audio: new Audio(`${track.preview_url}`), 60 | playing: 0, 61 | popularityTag: getPopularity(track.popularity), 62 | })); 63 | 64 | setFavoriteTracks(tracksData); 65 | } catch (err) { 66 | toast.error('Não foi possível carregar suas músicas favoritas.'); 67 | } finally { 68 | setLoading(false); 69 | } 70 | } 71 | 72 | loadFavoriteTracks(); 73 | }, []); 74 | 75 | const handlePlay = useCallback( 76 | id => { 77 | setFavoriteTracks( 78 | favoriteTracks.map(track => 79 | track.id === id ? { ...track, playing: 1 } : track, 80 | ), 81 | ); 82 | }, 83 | [favoriteTracks], 84 | ); 85 | 86 | const handlePause = useCallback( 87 | id => { 88 | setFavoriteTracks( 89 | favoriteTracks.map(track => 90 | track.id === id ? { ...track, playing: 0 } : track, 91 | ), 92 | ); 93 | }, 94 | [favoriteTracks], 95 | ); 96 | 97 | const tracksWithTransition = useTransition( 98 | favoriteTracks, 99 | track => track.id, 100 | { 101 | from: { 102 | opacity: 0, 103 | transform: 'translateY(80px)', 104 | }, 105 | enter: { 106 | opacity: 1, 107 | transform: 'translateY(0px)', 108 | }, 109 | trail: 150, 110 | }, 111 | ); 112 | 113 | return ( 114 | 115 | {loading ? ( 116 | 117 | ) : ( 118 | <> 119 | 120 |
121 | 122 |
123 |

124 | Visualize Suas 125 | Músicas Favoritas 126 |

127 |

As 10 músicas mais escutadas por você!

128 |
129 | 130 | 131 | {tracksWithTransition.map(({ item, key, props }, index) => ( 132 | { 137 | pauseAudioWithFade(item.audio, 100); 138 | handlePause(item.id); 139 | }} 140 | > 141 |
142 | {item.name} 143 |
144 | 145 |
146 | 147 | {index + 1}. {item.name} 148 | 149 | {item.artist.name} 150 |
151 | 152 | 178 | 179 | ))} 180 |
181 | 182 | )} 183 |
184 | ); 185 | }; 186 | 187 | export default FavoriteTracks; 188 | -------------------------------------------------------------------------------- /frontend/src/pages/FavoriteTracks/styles.ts: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components'; 2 | import { animated } from 'react-spring'; 3 | 4 | import { fadeUp } from '../../styles/animations'; 5 | 6 | export const Container = styled.div` 7 | display: flex; 8 | align-items: flex-start; 9 | 10 | @media (max-width: 1220px) { 11 | width: 100%; 12 | 13 | flex-direction: column; 14 | justify-content: center; 15 | align-items: center; 16 | } 17 | 18 | @media (max-width: 992px) { 19 | width: 100%; 20 | } 21 | `; 22 | 23 | export const LeftContent = styled.div` 24 | position: sticky; 25 | top: 71px; 26 | margin-right: 72px; 27 | 28 | display: flex; 29 | flex-direction: column; 30 | align-items: flex-start; 31 | 32 | div { 33 | background: linear-gradient(134.4deg, #20ac9a, #1db954 52%, #91c040); 34 | padding: 16px; 35 | border-radius: 10px; 36 | 37 | opacity: 0; 38 | animation: ${fadeUp} 1.5s forwards cubic-bezier(0.19, 1, 0.22, 1); 39 | animation-delay: 0.1s; 40 | 41 | display: flex; 42 | justify-content: center; 43 | align-items: center; 44 | } 45 | 46 | h1 { 47 | font-size: 64px; 48 | color: #fff; 49 | line-height: 1.25; 50 | margin: 32px 0; 51 | 52 | display: flex; 53 | flex-wrap: wrap; 54 | 55 | opacity: 0; 56 | animation: ${fadeUp} 1.5s forwards cubic-bezier(0.19, 1, 0.22, 1); 57 | animation-delay: 0.3s; 58 | 59 | .green { 60 | color: #33ff7a; 61 | } 62 | } 63 | 64 | p { 65 | font-size: 18px; 66 | line-height: 2; 67 | 68 | opacity: 0; 69 | animation: ${fadeUp} 1.5s forwards cubic-bezier(0.19, 1, 0.22, 1); 70 | animation-delay: 0.5s; 71 | } 72 | 73 | @media (max-width: 1220px) { 74 | position: relative; 75 | top: 0px; 76 | margin-right: 0px; 77 | margin-bottom: 32px; 78 | 79 | align-items: center; 80 | 81 | h1 { 82 | display: flex; 83 | flex-direction: column; 84 | align-items: center; 85 | } 86 | } 87 | 88 | @media (max-width: 992px) { 89 | div { 90 | padding: 14px; 91 | 92 | svg { 93 | width: 24px; 94 | height: 24px; 95 | } 96 | } 97 | 98 | h1 { 99 | margin: 24px 0; 100 | font-size: 40px; 101 | text-align: center; 102 | 103 | display: flex; 104 | flex-direction: column; 105 | 106 | .green { 107 | margin-left: 0px; 108 | } 109 | } 110 | 111 | p { 112 | font-size: 14px; 113 | } 114 | } 115 | `; 116 | 117 | export const UserFavoriteTracks = styled.div` 118 | display: flex; 119 | flex-direction: column; 120 | 121 | @media (max-width: 992px) { 122 | width: 100%; 123 | } 124 | `; 125 | 126 | interface IIsPlaying { 127 | playing: number; 128 | } 129 | 130 | export const Track = styled(animated.div)` 131 | background: #252527; 132 | padding: 0 24px; 133 | border-radius: 10px; 134 | height: 80px; 135 | width: 700px; 136 | 137 | display: flex; 138 | align-items: center; 139 | 140 | transition: transform 1.5s cubic-bezier(0.19, 1, 0.22, 1), 141 | opacity 1s cubic-bezier(0.19, 1, 0.22, 1); 142 | transition-delay: 0.05s; 143 | 144 | & + div { 145 | margin-top: 16px; 146 | } 147 | 148 | .track-image { 149 | min-width: 80px; 150 | width: 80px; 151 | height: 80px; 152 | margin-right: 16px; 153 | overflow: hidden; 154 | 155 | img { 156 | width: 100%; 157 | height: 100%; 158 | } 159 | } 160 | 161 | .track-info { 162 | margin-right: 24px; 163 | 164 | display: flex; 165 | flex-direction: column; 166 | 167 | strong { 168 | font-size: 18px; 169 | color: #fff; 170 | } 171 | 172 | span { 173 | color: #7a8185; 174 | font-size: 14px; 175 | font-weight: bold; 176 | margin-top: 8px; 177 | } 178 | } 179 | 180 | aside { 181 | margin-left: auto; 182 | 183 | display: flex; 184 | align-items: center; 185 | 186 | button { 187 | background: transparent; 188 | border: 0; 189 | transition: transform 0.2s; 190 | 191 | &:hover { 192 | transform: scale(1.2); 193 | } 194 | 195 | svg { 196 | width: 24px; 197 | height: 24px; 198 | } 199 | } 200 | 201 | ${props => 202 | props.playing 203 | ? css` 204 | .playButton { 205 | display: none; 206 | } 207 | 208 | .pauseButton { 209 | display: block; 210 | } 211 | ` 212 | : css` 213 | .playButton { 214 | display: block; 215 | } 216 | 217 | .pauseButton { 218 | display: none; 219 | } 220 | `} 221 | 222 | a { 223 | margin-left: 16px; 224 | transition: transform 0.2s; 225 | 226 | &:hover { 227 | transform: scale(1.2); 228 | } 229 | 230 | svg { 231 | width: 24px; 232 | height: 24px; 233 | } 234 | } 235 | } 236 | 237 | @media (max-width: 992px) { 238 | width: 100%; 239 | height: 100%; 240 | } 241 | 242 | @media (max-width: 768px) { 243 | padding: 16px 24px; 244 | 245 | .track-image { 246 | display: none; 247 | } 248 | 249 | .track-info { 250 | margin-right: 16px; 251 | 252 | strong { 253 | font-size: 16px; 254 | } 255 | } 256 | 257 | aside { 258 | button { 259 | svg { 260 | width: 22px; 261 | height: 22px; 262 | } 263 | } 264 | 265 | a { 266 | margin-left: 12px; 267 | 268 | svg { 269 | width: 22px; 270 | height: 22px; 271 | } 272 | } 273 | } 274 | } 275 | `; 276 | -------------------------------------------------------------------------------- /frontend/src/pages/NotFound/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import PoucoEscutado from '../../components/AnimatedVectors/PoucoEscutado'; 4 | import SpotifyButton from '../../components/SpotifyButton'; 5 | 6 | import { Container } from './styles'; 7 | 8 | const NotFound: React.FC = () => { 9 | return ( 10 | 11 | 12 | 13 |

404 Error.

14 |

Não conseguimos encontrar a página que você estava procurando.

15 | 16 | Voltar a página inicial 17 |
18 | ); 19 | }; 20 | 21 | export default NotFound; 22 | -------------------------------------------------------------------------------- /frontend/src/pages/NotFound/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Container = styled.div` 4 | width: 500px; 5 | padding: 16px; 6 | 7 | display: flex; 8 | flex-direction: column; 9 | 10 | div[role='button'] { 11 | width: 220px !important; 12 | height: 220px !important; 13 | margin: 0 !important; 14 | } 15 | 16 | h1 { 17 | font-size: 72px; 18 | color: #fff; 19 | line-height: 1.25; 20 | margin: 32px 0; 21 | } 22 | 23 | p { 24 | font-size: 20px; 25 | font-weight: 500; 26 | color: #7a8185; 27 | margin-bottom: 32px; 28 | line-height: 1.5; 29 | } 30 | 31 | @media (max-width: 992px) { 32 | div[role='button'] { 33 | width: 150px !important; 34 | height: 150px !important; 35 | } 36 | 37 | h1 { 38 | font-size: 52px; 39 | margin: 24px 0; 40 | } 41 | 42 | p { 43 | font-size: 16px; 44 | margin-bottom: 24px; 45 | } 46 | } 47 | `; 48 | -------------------------------------------------------------------------------- /frontend/src/pages/Playlists/ModalPlaylistTracks/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useCallback } from 'react'; 2 | import { 3 | FaPlayCircle, 4 | FaPauseCircle, 5 | FaSpotify, 6 | FaTimes, 7 | FaRecordVinyl, 8 | FaUsers, 9 | } from 'react-icons/fa'; 10 | import { useTransition } from 'react-spring'; 11 | import { toast } from 'react-toastify'; 12 | 13 | import formatValue from '../../../utils/formatValue'; 14 | 15 | import Modal from '../../../components/Modal'; 16 | import SpotifyButton from '../../../components/SpotifyButton'; 17 | import Spinner from '../../../components/Spinner'; 18 | import Scroll from '../../../components/Scroll'; 19 | 20 | import api from '../../../services/api'; 21 | import { playAudioWithFade, pauseAudioWithFade } from '../../../utils/audio'; 22 | 23 | import { 24 | Container, 25 | LeftContent, 26 | Content, 27 | PlaylistInfo, 28 | TracksList, 29 | Track, 30 | CloseModal, 31 | } from './styles'; 32 | 33 | interface IModalProps { 34 | isOpen: boolean; 35 | setIsOpen: () => void; 36 | playlistId: string; 37 | } 38 | 39 | interface ITrack { 40 | id: string; 41 | name: string; 42 | preview: string; 43 | uri: string; 44 | artistName: string; 45 | albumImage: string; 46 | audio: HTMLAudioElement; 47 | playing: number; 48 | } 49 | 50 | interface IPlaylist { 51 | id: string; 52 | name: string; 53 | avatar: string; 54 | uri: string; 55 | followers: number; 56 | formattedFollowers: number; 57 | totalTracks: number; 58 | } 59 | 60 | const ModalPlaylistTracks: React.FC = ({ 61 | isOpen, 62 | setIsOpen, 63 | playlistId, 64 | }) => { 65 | const [loading, setLoading] = useState(true); 66 | const [tracks, setTracks] = useState([]); 67 | const [playlist, setPlaylist] = useState({} as IPlaylist); 68 | 69 | useEffect(() => { 70 | async function loadPlaylist(): Promise { 71 | try { 72 | const [playlistResponse, tracksResponse] = await Promise.all([ 73 | api.get(`/playlist/${playlistId}`), 74 | api.get(`/playlist/tracks/${playlistId}`), 75 | ]); 76 | 77 | const playlistData = { 78 | ...playlistResponse.data, 79 | formattedFollowers: formatValue(playlistResponse.data.followers), 80 | }; 81 | 82 | const tracksData = tracksResponse.data.map((track: ITrack) => ({ 83 | ...track, 84 | audio: new Audio(`${track.preview}`), 85 | playing: 0, 86 | })); 87 | 88 | setPlaylist(playlistData); 89 | setTracks(tracksData); 90 | } catch (err) { 91 | toast.error('Não foi possível carregar as músicas da playlist.'); 92 | } finally { 93 | setLoading(false); 94 | } 95 | } 96 | 97 | loadPlaylist(); 98 | }, [playlistId]); 99 | 100 | const tracksWithTransition = useTransition(tracks, track => track.id, { 101 | from: { 102 | opacity: 0, 103 | transform: 'translateY(40px)', 104 | }, 105 | enter: { 106 | opacity: 1, 107 | transform: 'translateY(0)', 108 | }, 109 | leave: { 110 | opacity: 0, 111 | }, 112 | trail: 100, 113 | }); 114 | 115 | const handlePlay = useCallback( 116 | id => { 117 | setTracks( 118 | tracks.map(track => 119 | track.id === id ? { ...track, playing: 1 } : track, 120 | ), 121 | ); 122 | }, 123 | [tracks], 124 | ); 125 | 126 | const handlePause = useCallback( 127 | id => { 128 | setTracks( 129 | tracks.map(track => 130 | track.id === id ? { ...track, playing: 0 } : track, 131 | ), 132 | ); 133 | }, 134 | [tracks], 135 | ); 136 | 137 | return ( 138 | 139 | 140 | {loading ? ( 141 | 142 | ) : ( 143 | <> 144 | 145 | 146 |
147 | {playlist.name} 148 |
149 | 150 | 151 | Abrir no Spotify 152 | 153 |
154 | 155 | 156 |

{playlist.name}

157 | 158 | 159 | 169 | 170 | 171 | 172 | {tracksWithTransition.map(({ item, key, props }, index) => ( 173 | { 178 | pauseAudioWithFade(item.audio, 100); 179 | handlePause(item.id); 180 | }} 181 | > 182 |
183 | {item.name} 184 |
185 | 186 |
187 | 188 | {index + 1}. {item.name} 189 | 190 | {item.artistName} 191 |
192 | 193 | 219 | 220 | ))} 221 |
222 |
223 |
224 | 225 | 226 | 227 | 228 | 229 | )} 230 |
231 |
232 | ); 233 | }; 234 | 235 | export default ModalPlaylistTracks; 236 | -------------------------------------------------------------------------------- /frontend/src/pages/Playlists/ModalPlaylistTracks/styles.ts: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components'; 2 | import { animated } from 'react-spring'; 3 | 4 | import { 5 | fade, 6 | fadeUp, 7 | fadeScaleDown, 8 | scaleDown, 9 | } from '../../../styles/animations'; 10 | 11 | export const Container = styled.div` 12 | display: flex; 13 | `; 14 | 15 | export const LeftContent = styled.aside` 16 | padding-bottom: 56px; 17 | margin-right: 104px; 18 | 19 | position: sticky; 20 | top: 10px; 21 | 22 | div { 23 | width: 400px; 24 | height: 400px; 25 | margin-bottom: 16px; 26 | border-radius: 10px; 27 | overflow: hidden; 28 | 29 | img { 30 | width: 100%; 31 | height: 100%; 32 | 33 | opacity: 0; 34 | animation: ${fadeScaleDown} 1.3s forwards cubic-bezier(0.19, 0.8, 0.28, 1); 35 | animation-delay: 0.1s; 36 | } 37 | } 38 | 39 | a { 40 | width: 100%; 41 | 42 | opacity: 0; 43 | animation: ${fadeUp} 1.5s forwards cubic-bezier(0.19, 0.8, 0.28, 1); 44 | animation-delay: 0.4s; 45 | } 46 | 47 | @media (max-width: 1220px) { 48 | margin-right: 48px; 49 | } 50 | 51 | @media (max-width: 992px) { 52 | padding-bottom: 24px; 53 | width: 100%; 54 | 55 | div { 56 | width: 100%; 57 | height: 100%; 58 | } 59 | } 60 | 61 | @media (max-width: 768px) { 62 | position: initial; 63 | } 64 | `; 65 | 66 | export const Content = styled.div` 67 | width: 100%; 68 | 69 | display: flex; 70 | flex-direction: column; 71 | 72 | h1 { 73 | font-size: 56px; 74 | line-height: 1.14; 75 | color: #fff; 76 | 77 | opacity: 0; 78 | animation: ${fadeUp} 1.5s forwards cubic-bezier(0.19, 1, 0.22, 1); 79 | animation-delay: 0.6s; 80 | } 81 | 82 | @media (max-width: 1220px) { 83 | h1 { 84 | font-size: 40px; 85 | } 86 | } 87 | `; 88 | 89 | export const PlaylistInfo = styled.section` 90 | margin: 24px 0; 91 | 92 | display: flex; 93 | align-items: center; 94 | justify-content: space-between; 95 | 96 | opacity: 0; 97 | animation: ${fadeUp} 1.5s forwards cubic-bezier(0.19, 1, 0.22, 1); 98 | animation-delay: 0.8s; 99 | 100 | aside { 101 | display: flex; 102 | align-items: center; 103 | 104 | div { 105 | background: rgba(51, 255, 122, 0.15); 106 | color: #fff; 107 | font-weight: bold; 108 | font-size: 14px; 109 | padding: 10px 14px; 110 | border-radius: 26px; 111 | 112 | display: flex; 113 | align-items: center; 114 | 115 | & + div { 116 | margin-left: 8px; 117 | } 118 | 119 | svg { 120 | width: 18px; 121 | height: 18px; 122 | margin-right: 8px; 123 | } 124 | 125 | strong { 126 | margin-right: 4px; 127 | } 128 | } 129 | } 130 | 131 | nav { 132 | display: flex; 133 | align-items: center; 134 | 135 | button { 136 | background: none; 137 | border: none; 138 | 139 | & + button { 140 | margin-left: 32px; 141 | } 142 | 143 | svg { 144 | color: #7a8185; 145 | transition: color 0.2s; 146 | 147 | &:hover { 148 | color: #fff; 149 | } 150 | } 151 | } 152 | } 153 | 154 | @media (max-width: 1220px) { 155 | margin: 16px 0; 156 | 157 | aside { 158 | div { 159 | font-size: 12px; 160 | padding: 8px 12px; 161 | 162 | svg { 163 | width: 16px; 164 | height: 16px; 165 | } 166 | } 167 | } 168 | } 169 | `; 170 | 171 | export const TracksList = styled.section` 172 | padding-bottom: 56px; 173 | 174 | display: flex; 175 | flex-direction: column; 176 | 177 | @media (max-width: 1220px) { 178 | padding-bottom: 42px; 179 | } 180 | `; 181 | 182 | interface IIsPlaying { 183 | playing: number; 184 | } 185 | 186 | export const Track = styled(animated.div)` 187 | background: #252527; 188 | padding: 0 24px; 189 | border-radius: 10px; 190 | height: 80px; 191 | 192 | display: flex; 193 | align-items: center; 194 | 195 | transition: opacity 1s cubic-bezier(0.19, 1, 0.22, 1), 196 | transform 1.5s cubic-bezier(0.19, 1, 0.22, 1); 197 | transition-delay: 0.2s; 198 | 199 | & + div { 200 | margin-top: 16px; 201 | } 202 | 203 | .track-image { 204 | min-width: 80px; 205 | width: 80px; 206 | height: 80px; 207 | margin-right: 16px; 208 | overflow: hidden; 209 | 210 | img { 211 | width: 100%; 212 | height: 100%; 213 | 214 | animation: ${scaleDown} 1.3s forwards cubic-bezier(0.2, 0.6, 0.35, 1); 215 | animation-delay: 0.8s; 216 | } 217 | } 218 | 219 | .track-info { 220 | margin-right: 24px; 221 | 222 | display: flex; 223 | flex-direction: column; 224 | 225 | strong { 226 | font-size: 18px; 227 | color: #fff; 228 | } 229 | 230 | span { 231 | color: #7a8185; 232 | font-size: 14px; 233 | font-weight: bold; 234 | margin-top: 8px; 235 | } 236 | } 237 | 238 | aside { 239 | margin-left: auto; 240 | 241 | display: flex; 242 | align-items: center; 243 | 244 | button { 245 | background: transparent; 246 | border: 0; 247 | transition: transform 0.2s; 248 | 249 | &:hover { 250 | transform: scale(1.2); 251 | } 252 | 253 | svg { 254 | width: 24px; 255 | height: 24px; 256 | } 257 | } 258 | 259 | ${props => 260 | props.playing 261 | ? css` 262 | .playButton { 263 | display: none; 264 | } 265 | 266 | .pauseButton { 267 | display: block; 268 | } 269 | ` 270 | : css` 271 | .playButton { 272 | display: block; 273 | } 274 | 275 | .pauseButton { 276 | display: none; 277 | } 278 | `} 279 | 280 | a { 281 | margin-left: 16px; 282 | transition: transform 0.2s; 283 | 284 | &:hover { 285 | transform: scale(1.2); 286 | } 287 | 288 | svg { 289 | width: 24px; 290 | height: 24px; 291 | } 292 | } 293 | } 294 | 295 | @media (max-width: 1200px) { 296 | .track-image { 297 | display: none; 298 | } 299 | } 300 | 301 | @media (max-width: 992px) { 302 | width: 100%; 303 | height: 100%; 304 | padding: 16px 24px; 305 | 306 | .track-info { 307 | margin-right: 16px; 308 | 309 | strong { 310 | font-size: 16px; 311 | } 312 | } 313 | 314 | aside { 315 | button { 316 | svg { 317 | width: 22px; 318 | height: 22px; 319 | } 320 | } 321 | 322 | a { 323 | margin-left: 12px; 324 | 325 | svg { 326 | width: 22px; 327 | height: 22px; 328 | } 329 | } 330 | } 331 | } 332 | `; 333 | 334 | export const CloseModal = styled.button` 335 | background: transparent; 336 | border: 0; 337 | position: absolute; 338 | right: 16px; 339 | top: 40px; 340 | margin-right: 4px; 341 | transition: transform 0.2s; 342 | 343 | display: flex; 344 | justify-content: center; 345 | align-items: center; 346 | 347 | opacity: 0; 348 | animation: ${fade} 1s forwards cubic-bezier(0.19, 1, 0.22, 1); 349 | animation-delay: 0.8s; 350 | 351 | &:hover { 352 | transform: scale(1.2); 353 | } 354 | 355 | svg { 356 | width: 28px; 357 | height: 28px; 358 | } 359 | 360 | @media (max-width: 992px) { 361 | margin-right: 0; 362 | top: 8px; 363 | right: 8px; 364 | 365 | svg { 366 | width: 22px; 367 | height: 22px; 368 | } 369 | } 370 | `; 371 | -------------------------------------------------------------------------------- /frontend/src/pages/Playlists/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useCallback } from 'react'; 2 | import { useTransition } from 'react-spring'; 3 | import { FaHeadphones } from 'react-icons/fa'; 4 | import { toast } from 'react-toastify'; 5 | 6 | import api from '../../services/api'; 7 | 8 | import Spinner from '../../components/Spinner'; 9 | import ModalPlaylistTracks from './ModalPlaylistTracks'; 10 | 11 | import { Container, LeftContent, UserPlaylists, Playlist } from './styles'; 12 | 13 | interface IPlaylists { 14 | id: string; 15 | name: string; 16 | avatar: string; 17 | uri: string; 18 | } 19 | 20 | const Playlists: React.FC = () => { 21 | const [playlists, setPlaylist] = useState([]); 22 | const [toggleModal, setToggleModal] = useState(false); 23 | const [playlistId, setPlaylistId] = useState(''); 24 | const [loading, setLoading] = useState(false); 25 | 26 | const handleModal = useCallback(() => { 27 | setToggleModal(!toggleModal); 28 | }, [toggleModal]); 29 | 30 | useEffect(() => { 31 | async function loadUserPlaylists(): Promise { 32 | try { 33 | setLoading(true); 34 | 35 | const response = await api.get('/me/playlists'); 36 | 37 | setPlaylist(response.data); 38 | } catch (err) { 39 | toast.error('Não foi possível carregar as playlists.'); 40 | } finally { 41 | setLoading(false); 42 | } 43 | } 44 | 45 | loadUserPlaylists(); 46 | }, []); 47 | 48 | const playlistsWithTransition = useTransition( 49 | playlists, 50 | playlist => playlist.id, 51 | { 52 | from: { 53 | opacity: 0, 54 | transform: 'scale(0.8)', 55 | }, 56 | enter: { 57 | opacity: 1, 58 | transform: 'scale(1)', 59 | }, 60 | trail: 125, 61 | }, 62 | ); 63 | 64 | return ( 65 | 66 | {loading ? ( 67 | 68 | ) : ( 69 | <> 70 | {toggleModal && ( 71 | 76 | )} 77 | 78 | 79 |
80 | 81 |
82 |

83 | Visualize Suas 84 | Playlists 85 |

86 |

87 | Quando você é o DJ da festa, essas são sempre as mais tocadas! 88 |

89 |
90 | 91 | 92 | {playlistsWithTransition.map(({ item, key, props }, index) => ( 93 | { 97 | setPlaylistId(item.id); 98 | handleModal(); 99 | }} 100 | > 101 | {item.name} 102 |
103 | 104 | {index + 1}. {item.name} 105 | 106 |
107 |
108 | ))} 109 |
110 | 111 | )} 112 |
113 | ); 114 | }; 115 | 116 | export default Playlists; 117 | -------------------------------------------------------------------------------- /frontend/src/pages/Playlists/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { animated } from 'react-spring'; 3 | 4 | import { fadeUp } from '../../styles/animations'; 5 | 6 | export const Container = styled.div` 7 | display: flex; 8 | align-items: flex-start; 9 | 10 | @media (max-width: 1220px) { 11 | flex-direction: column; 12 | justify-content: center; 13 | align-items: center; 14 | } 15 | `; 16 | 17 | export const LeftContent = styled.div` 18 | position: sticky; 19 | top: 71px; 20 | margin-right: 72px; 21 | 22 | display: flex; 23 | flex-direction: column; 24 | align-items: flex-start; 25 | 26 | div { 27 | background: linear-gradient(134.4deg, #20ac9a, #1db954 52%, #91c040); 28 | padding: 16px; 29 | border-radius: 10px; 30 | 31 | opacity: 0; 32 | animation: ${fadeUp} 1.5s forwards cubic-bezier(0.19, 1, 0.22, 1); 33 | animation-delay: 0.1s; 34 | 35 | display: flex; 36 | justify-content: center; 37 | align-items: center; 38 | } 39 | 40 | h1 { 41 | font-size: 64px; 42 | color: #fff; 43 | line-height: 1.25; 44 | margin: 32px 0; 45 | 46 | opacity: 0; 47 | animation: ${fadeUp} 1.5s forwards cubic-bezier(0.19, 1, 0.22, 1); 48 | animation-delay: 0.3s; 49 | 50 | display: flex; 51 | flex-wrap: wrap; 52 | 53 | .green { 54 | color: #33ff7a; 55 | } 56 | } 57 | 58 | p { 59 | font-size: 18px; 60 | line-height: 2; 61 | 62 | opacity: 0; 63 | animation: ${fadeUp} 1.5s forwards cubic-bezier(0.19, 1, 0.22, 1); 64 | animation-delay: 0.5s; 65 | } 66 | 67 | @media (max-width: 1220px) { 68 | position: relative; 69 | top: 0px; 70 | margin-right: 0px; 71 | margin-bottom: 32px; 72 | 73 | align-items: center; 74 | 75 | div { 76 | padding: 14px; 77 | 78 | svg { 79 | width: 24px; 80 | height: 24px; 81 | } 82 | } 83 | 84 | h1 { 85 | margin: 24px 0; 86 | font-size: 52px; 87 | 88 | display: flex; 89 | flex-direction: column; 90 | align-items: center; 91 | } 92 | 93 | p { 94 | font-size: 14px; 95 | text-align: center; 96 | } 97 | } 98 | 99 | @media (max-width: 992px) { 100 | div { 101 | padding: 14px; 102 | 103 | svg { 104 | width: 24px; 105 | height: 24px; 106 | } 107 | } 108 | 109 | h1 { 110 | margin: 24px 0; 111 | font-size: 40px; 112 | text-align: center; 113 | 114 | display: flex; 115 | flex-direction: column; 116 | 117 | .green { 118 | margin-left: 0px; 119 | } 120 | } 121 | 122 | p { 123 | font-size: 14px; 124 | } 125 | } 126 | `; 127 | 128 | export const UserPlaylists = styled.div` 129 | display: grid; 130 | grid-template-columns: 1fr 1fr 1fr; 131 | grid-gap: 24px; 132 | 133 | @media (max-width: 768px) { 134 | width: 100%; 135 | grid-template-columns: 1fr 1fr; 136 | } 137 | `; 138 | 139 | export const Playlist = styled(animated.div)` 140 | border-radius: 10px; 141 | width: 240px; 142 | overflow: hidden; 143 | cursor: pointer; 144 | 145 | display: flex; 146 | flex-direction: column; 147 | 148 | transition: transform 1.5s cubic-bezier(0.19, 1, 0.22, 1), 149 | opacity 1s cubic-bezier(0.19, 1, 0.22, 1); 150 | transition-delay: 0.05s; 151 | 152 | &:hover > img { 153 | transform: scale(1.1); 154 | } 155 | 156 | img { 157 | width: 100%; 158 | height: 240px; 159 | border-top-left-radius: 10px; 160 | border-top-right-radius: 10px; 161 | transition: all 0.3s; 162 | } 163 | 164 | div { 165 | background: #272727; 166 | border-bottom-left-radius: 10px; 167 | border-bottom-right-radius: 10px; 168 | height: 64px; 169 | padding: 0 16px; 170 | z-index: 99; 171 | 172 | display: flex; 173 | align-items: center; 174 | 175 | strong { 176 | display: block; 177 | color: #fff; 178 | white-space: nowrap; 179 | overflow: hidden; 180 | text-overflow: ellipsis; 181 | } 182 | } 183 | 184 | @media (max-width: 992px) { 185 | width: 100%; 186 | 187 | img { 188 | height: 100%; 189 | } 190 | 191 | div { 192 | height: 48px; 193 | 194 | strong { 195 | font-size: 14px; 196 | } 197 | } 198 | } 199 | `; 200 | -------------------------------------------------------------------------------- /frontend/src/pages/SignIn/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import edSheeran from '../../assets/ed-sheeran.png'; 4 | 5 | import SpotifyButton from '../../components/SpotifyButton'; 6 | 7 | import { 8 | Container, 9 | SignInContainer, 10 | ArtistImage, 11 | SignUpContainer, 12 | } from './styles'; 13 | 14 | const SignIn: React.FC = () => { 15 | return ( 16 | 17 | 18 |

19 |
20 | Descubra 21 |
22 |
23 | como 24 |
25 |
26 | você 27 |
28 |
29 | escuta. 30 |
31 |

32 | 33 |

34 | Descubra suas músicas mais escutadas e artistas preferidos da sua 35 | conta do Spotify. 36 |

37 | 38 | Continue com o Spotify 39 | 40 |
41 | 42 | 43 | Ed Sheeran 44 | 45 | 46 | 47 | Não possui uma conta no Spotify? 48 | 49 | 54 | Crie sua conta gratuita agora 55 | 56 | 57 |
58 | ); 59 | }; 60 | 61 | export default SignIn; 62 | -------------------------------------------------------------------------------- /frontend/src/pages/SignIn/styles.ts: -------------------------------------------------------------------------------- 1 | import styled, { keyframes } from 'styled-components'; 2 | import { lighten } from 'polished'; 3 | 4 | import { fadeUp } from '../../styles/animations'; 5 | 6 | const wordUp = keyframes` 7 | from { 8 | transform: translateY(110%); 9 | } 10 | to { 11 | transform: translateY(0); 12 | } 13 | `; 14 | 15 | const fadeScaleUp = keyframes` 16 | from { 17 | opacity: 0; 18 | transform: scale(0.8); 19 | } 20 | to { 21 | opacity: 1; 22 | transform: scale(1); 23 | } 24 | `; 25 | 26 | const fadeLeft = keyframes` 27 | from { 28 | transform: translateX(80px); 29 | opacity: 0; 30 | } 31 | to { 32 | transform: translateX(0px); 33 | opacity: 1; 34 | } 35 | `; 36 | 37 | export const Container = styled.div` 38 | padding: 32px; 39 | max-width: 1366px; 40 | width: 100%; 41 | 42 | display: flex; 43 | align-items: center; 44 | justify-content: center; 45 | 46 | @media (max-width: 1160px) { 47 | padding-top: 0px; 48 | } 49 | `; 50 | 51 | export const SignInContainer = styled.div` 52 | width: 500px; 53 | margin-right: 120px; 54 | 55 | display: flex; 56 | flex-direction: column; 57 | align-items: flex-start; 58 | 59 | h1 { 60 | font-size: 72px; 61 | color: #fff; 62 | height: 252px; 63 | position: relative; 64 | overflow-wrap: break-word; 65 | 66 | div { 67 | line-height: 93.6px; 68 | position: absolute; 69 | top: 0px; 70 | overflow: hidden; 71 | 72 | @media (max-width: 768px) { 73 | line-height: 50.6px; 74 | } 75 | 76 | span { 77 | display: inline-block; 78 | overflow: hidden; 79 | 80 | transform: translateY(110%); 81 | animation: ${wordUp} 0.8s forwards cubic-bezier(0.075, 0.82, 0.165, 1); 82 | } 83 | } 84 | 85 | .word-container:nth-child(1) { 86 | left: 0px; 87 | 88 | span { 89 | animation-delay: 0.2s; 90 | } 91 | } 92 | 93 | .word-container:nth-child(2) { 94 | top: 72px; 95 | left: 0px; 96 | 97 | span { 98 | animation-delay: 0.3s; 99 | } 100 | } 101 | 102 | .word-container:nth-child(3) { 103 | top: 72px; 104 | left: 230.375px; 105 | 106 | @media (max-width: 768px) { 107 | left: 168.375px; 108 | } 109 | 110 | span { 111 | animation-delay: 0.4s; 112 | } 113 | } 114 | 115 | .word-container:nth-child(4) { 116 | top: 144px; 117 | left: 0px; 118 | 119 | span { 120 | animation-delay: 0.5s; 121 | } 122 | } 123 | 124 | .green { 125 | color: #33ff7a; 126 | } 127 | } 128 | 129 | p { 130 | margin-bottom: 24px; 131 | font-size: 20px; 132 | line-height: 2; 133 | 134 | opacity: 0; 135 | animation: ${fadeUp} 1.5s forwards cubic-bezier(0.19, 1, 0.22, 1); 136 | animation-delay: 0.8s; 137 | } 138 | 139 | a { 140 | opacity: 0; 141 | animation: ${fadeUp} 1.5s forwards cubic-bezier(0.19, 1, 0.22, 1); 142 | animation-delay: 1s; 143 | 144 | @media (max-width: 1160px) { 145 | width: 100%; 146 | } 147 | } 148 | 149 | @media (max-width: 1160px) { 150 | margin-right: 0px; 151 | } 152 | 153 | @media (max-width: 768px) { 154 | h1 { 155 | font-size: 52px; 156 | height: 200px; 157 | 158 | .word-container:nth-child(2), 159 | .word-container:nth-child(3) { 160 | top: 64px; 161 | } 162 | 163 | .word-container:nth-child(4) { 164 | top: 128px; 165 | } 166 | } 167 | 168 | p { 169 | font-size: 16px; 170 | } 171 | } 172 | `; 173 | 174 | export const ArtistImage = styled.div` 175 | opacity: 0; 176 | animation: ${fadeScaleUp} 1.5s forwards cubic-bezier(0.19, 1, 0.22, 1); 177 | animation-delay: 0.8s; 178 | 179 | @media (max-width: 1160px) { 180 | display: none; 181 | } 182 | 183 | img { 184 | width: 600px; 185 | } 186 | `; 187 | 188 | export const SignUpContainer = styled.div` 189 | position: absolute; 190 | bottom: 24px; 191 | left: 24px; 192 | font-size: 16px; 193 | 194 | opacity: 0; 195 | animation: ${fadeLeft} 1.5s forwards cubic-bezier(0.19, 1, 0.22, 1); 196 | animation-delay: 1s; 197 | 198 | display: flex; 199 | flex-direction: column; 200 | 201 | span { 202 | color: #fff; 203 | font-weight: bold; 204 | 205 | @media (max-width: 992px) { 206 | font-size: 14px; 207 | } 208 | } 209 | 210 | a { 211 | color: #1db954; 212 | font-weight: bold; 213 | margin-top: 8px; 214 | transition: all 0.2s; 215 | 216 | &:hover { 217 | color: ${lighten(0.03, '#1db954')}; 218 | } 219 | 220 | @media (max-width: 992px) { 221 | font-size: 14px; 222 | } 223 | } 224 | `; 225 | -------------------------------------------------------------------------------- /frontend/src/pages/_layouts/auth/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Wrapper } from './styles'; 4 | 5 | const AuthLayout: React.FC = ({ children }) => { 6 | return {children}; 7 | }; 8 | 9 | export default AuthLayout; 10 | -------------------------------------------------------------------------------- /frontend/src/pages/_layouts/auth/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Wrapper = styled.div` 4 | height: 100%; 5 | 6 | display: flex; 7 | justify-content: center; 8 | align-items: center; 9 | `; 10 | -------------------------------------------------------------------------------- /frontend/src/pages/_layouts/default/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Header from '../../../components/Header'; 4 | 5 | import { Wrapper, Main } from './styles'; 6 | 7 | const DefaultLayout: React.FC = ({ children }) => { 8 | return ( 9 | 10 |
11 | 12 |
{children}
13 | 14 | ); 15 | }; 16 | 17 | export default DefaultLayout; 18 | -------------------------------------------------------------------------------- /frontend/src/pages/_layouts/default/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Wrapper = styled.div` 4 | height: 100%; 5 | 6 | display: flex; 7 | flex-direction: column; 8 | `; 9 | 10 | export const Main = styled.main` 11 | max-width: 1366px; 12 | width: 100%; 13 | margin: 0 auto; 14 | padding: 0 32px 32px; 15 | 16 | display: flex; 17 | 18 | @media (max-width: 1220px) { 19 | padding: 0 16px 24px; 20 | 21 | justify-content: center; 22 | align-items: center; 23 | } 24 | `; 25 | -------------------------------------------------------------------------------- /frontend/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /frontend/src/routes/Route.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Redirect, RouteProps } from 'react-router-dom'; 3 | import { parseISO, isAfter } from 'date-fns'; 4 | 5 | import { useAuth } from '../hooks/auth'; 6 | 7 | import AuthLayout from '../pages/_layouts/auth'; 8 | import DefaultLayout from '../pages/_layouts/default'; 9 | 10 | interface IRouteProps extends RouteProps { 11 | isPrivate?: boolean; 12 | component: React.ComponentType; 13 | } 14 | 15 | const RouteWrapper: React.FC = ({ 16 | component: Component, 17 | isPrivate = false, 18 | ...rest 19 | }) => { 20 | const { user, exp, signOut } = useAuth(); 21 | 22 | let signed = !!user; 23 | 24 | if (exp) { 25 | if (isAfter(new Date(), parseISO(exp.toString()))) { 26 | signed = false; 27 | signOut(); 28 | } 29 | } 30 | 31 | if (!signed && isPrivate) { 32 | return ; 33 | } 34 | 35 | if (signed && !isPrivate) { 36 | return ; 37 | } 38 | 39 | const Layout = signed ? DefaultLayout : AuthLayout; 40 | 41 | return ( 42 | ( 45 | 46 | 47 | 48 | )} 49 | /> 50 | ); 51 | }; 52 | 53 | export default RouteWrapper; 54 | -------------------------------------------------------------------------------- /frontend/src/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch } from 'react-router-dom'; 3 | 4 | import Route from './Route'; 5 | 6 | import SignIn from '../pages/SignIn'; 7 | import Authenticate from '../pages/Authenticate'; 8 | 9 | import Artists from '../pages/Artists'; 10 | import Playlists from '../pages/Playlists'; 11 | import FavoriteTracks from '../pages/FavoriteTracks'; 12 | 13 | import Error from '../pages/Error'; 14 | import NotFound from '../pages/NotFound'; 15 | 16 | const Routes: React.FC = () => { 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ); 30 | }; 31 | 32 | export default Routes; 33 | -------------------------------------------------------------------------------- /frontend/src/services/api.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const api = axios.create({ 4 | baseURL: process.env.REACT_APP_API_URL, 5 | }); 6 | 7 | export default api; 8 | -------------------------------------------------------------------------------- /frontend/src/services/history.ts: -------------------------------------------------------------------------------- 1 | import { createBrowserHistory } from 'history'; 2 | 3 | const history = createBrowserHistory(); 4 | 5 | export default history; 6 | -------------------------------------------------------------------------------- /frontend/src/styles/animations.ts: -------------------------------------------------------------------------------- 1 | import { keyframes } from 'styled-components'; 2 | 3 | const fade = keyframes` 4 | from { 5 | opacity: 0; 6 | } 7 | to { 8 | opacity: 1; 9 | } 10 | `; 11 | 12 | const fadeUp = keyframes` 13 | from { 14 | transform: translateY(40px); 15 | opacity: 0; 16 | } 17 | to { 18 | transform: translateY(0px); 19 | opacity: 1; 20 | } 21 | `; 22 | 23 | const incresedFadeUp = keyframes` 24 | from { 25 | transform: translateY(140px); 26 | opacity: 0; 27 | } 28 | to { 29 | transform: translateY(0px); 30 | opacity: 1; 31 | } 32 | `; 33 | 34 | const scaleDown = keyframes` 35 | from { 36 | transform: scale3d(3,3,3); 37 | } 38 | to { 39 | transform: scaleX(1); 40 | } 41 | `; 42 | 43 | const fadeScaleDown = keyframes` 44 | from { 45 | clip-path: inset(0px 100% 100% 0px round 10px); 46 | transform: scale3d(2.2, 2.2, 2.2); 47 | opacity: 0; 48 | } 49 | to { 50 | clip-path: inset(0px 0px 0px 0px round 10px); 51 | transform: scale3d(1, 1, 1); 52 | opacity: 1; 53 | } 54 | `; 55 | 56 | const profileOptionsDropdown = keyframes` 57 | from { 58 | visibility: hidden; 59 | clip-path: inset(0px 0px 100% 100% round 20px); 60 | transform: translateY(20px); 61 | } 62 | to { 63 | visibility: visible; 64 | clip-path: inset(0px 0px 0px 0px round 20px); 65 | transform: translateY(0); 66 | } 67 | `; 68 | 69 | const profileOptionsDropdownMobile = keyframes` 70 | from { 71 | visibility: hidden; 72 | clip-path: inset(0px 100% 100% 0px round 20px); 73 | transform: translateY(20px); 74 | } 75 | to { 76 | visibility: visible; 77 | clip-path: inset(0px 0px 0px 0px round 20px); 78 | transform: translateY(0); 79 | } 80 | `; 81 | 82 | const aboutDropdown = keyframes` 83 | from { 84 | visibility: hidden; 85 | clip-path: inset(0px 100% 100% 0px round 20px); 86 | transform: translateY(20px); 87 | } 88 | to { 89 | visibility: visible; 90 | clip-path: inset(0px 0px 0px 0px round 20px); 91 | transform: translateY(0); 92 | } 93 | `; 94 | 95 | const aboutDropdownMobile = keyframes` 96 | from { 97 | visibility: hidden; 98 | clip-path: inset(0px 0px 100% 100% round 20px); 99 | transform: translateY(20px); 100 | } 101 | to { 102 | visibility: visible; 103 | clip-path: inset(0px 0px 0px 0px round 20px); 104 | transform: translateY(0); 105 | } 106 | `; 107 | 108 | export { 109 | fade, 110 | fadeUp, 111 | incresedFadeUp, 112 | fadeScaleDown, 113 | scaleDown, 114 | profileOptionsDropdown, 115 | profileOptionsDropdownMobile, 116 | aboutDropdown, 117 | aboutDropdownMobile, 118 | }; 119 | -------------------------------------------------------------------------------- /frontend/src/styles/global.ts: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from 'styled-components'; 2 | 3 | import 'react-perfect-scrollbar/dist/css/styles.css'; 4 | import 'react-toastify/dist/ReactToastify.css'; 5 | 6 | import ball from '../assets/ball.svg'; 7 | 8 | export default createGlobalStyle` 9 | * { 10 | margin: 0; 11 | padding: 0; 12 | outline: 0; 13 | box-sizing: border-box; 14 | } 15 | 16 | *:focus { 17 | outline: 0; 18 | } 19 | 20 | html, body, #root { 21 | height: 100%; 22 | } 23 | 24 | body { 25 | -webkit-font-smoothing: antialiased; 26 | background: #121212; 27 | height: 100%; 28 | background-image: url(${ball}); 29 | background-repeat: repeat; 30 | cursor: default; 31 | } 32 | 33 | body, input, button { 34 | font: 16px Montserrat, sans-serif; 35 | color: #b3b3b3; 36 | line-height: 20px; 37 | overflow-x: hidden; 38 | } 39 | 40 | a { 41 | text-decoration: none; 42 | } 43 | 44 | ul { 45 | list-style: none; 46 | } 47 | 48 | button { 49 | cursor: pointer; 50 | } 51 | 52 | ::-webkit-scrollbar { 53 | width: 16px; 54 | } 55 | 56 | ::-webkit-scrollbar-thumb { 57 | background-color: hsla(0, 0%, 100%, 0.3); 58 | } 59 | 60 | /* Toastify */ 61 | .Toastify__toast { 62 | border-radius: 4px; 63 | padding: 10px 16px; 64 | font-size: 16px; 65 | } 66 | `; 67 | -------------------------------------------------------------------------------- /frontend/src/utils/audio.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | let timer: number; 3 | 4 | const playAudioWithFade = ( 5 | audio: HTMLAudioElement, 6 | fadeTiming = 1000, 7 | ): void => { 8 | const volCounter = 10; 9 | audio.volume = 0; 10 | 11 | // eslint-disable-next-line react-hooks/exhaustive-deps 12 | timer = setTimeout(() => { 13 | audio.play(); 14 | 15 | const volumeFade = setInterval(() => { 16 | audio.volume = volCounter / 10; 17 | }, 100); 18 | 19 | setTimeout(() => { 20 | clearInterval(volumeFade); 21 | }, fadeTiming); 22 | }, fadeTiming); 23 | }; 24 | 25 | const pauseAudioWithFade = ( 26 | audio: HTMLAudioElement, 27 | fadeTiming = 1000, 28 | ): void => { 29 | clearTimeout(timer); 30 | let volCounter = audio.volume * 10; 31 | 32 | const volumeFade = setInterval(() => { 33 | volCounter--; 34 | audio.volume = Math.max(volCounter / 10, 0); 35 | }, 100); 36 | 37 | setTimeout(() => { 38 | clearInterval(volumeFade); 39 | audio.pause(); 40 | }, fadeTiming); 41 | }; 42 | 43 | export { playAudioWithFade, pauseAudioWithFade }; 44 | -------------------------------------------------------------------------------- /frontend/src/utils/formatValue.ts: -------------------------------------------------------------------------------- 1 | const formatValue = (value: number): string => 2 | Intl.NumberFormat('pt-BR').format(value); 3 | 4 | export default formatValue; 5 | -------------------------------------------------------------------------------- /frontend/src/utils/getPopularity.ts: -------------------------------------------------------------------------------- 1 | const getPopularity = (value: number): string => { 2 | if (value < 30) { 3 | return 'Pouco escutado'; 4 | } 5 | if (value < 55) { 6 | return 'Chamando Atenção'; 7 | } 8 | if (value < 80) { 9 | return 'Bem conhecido'; 10 | } 11 | if (value < 88) { 12 | return 'Popular'; 13 | } 14 | 15 | return 'Estourando'; 16 | }; 17 | 18 | export default getPopularity; 19 | -------------------------------------------------------------------------------- /frontend/src/utils/toggleFullScreen.ts: -------------------------------------------------------------------------------- 1 | const openFullscreen = (): void => { 2 | document.documentElement.requestFullscreen(); 3 | }; 4 | 5 | const closeFullscreen = (): void => { 6 | document.exitFullscreen(); 7 | }; 8 | 9 | export const toggleFullScreen = (fullScreen: boolean): void => { 10 | fullScreen ? closeFullscreen() : openFullscreen(); 11 | }; 12 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react" 21 | }, 22 | "include": [ 23 | "src" 24 | ] 25 | } 26 | --------------------------------------------------------------------------------