├── .env ├── src ├── react-app-env.d.ts ├── assets │ ├── audios │ │ ├── azul_baron.ogg │ │ ├── firstblood.ogg │ │ ├── azul_dragao.ogg │ │ ├── vermelha_baron.ogg │ │ ├── vermelha_dragao.ogg │ │ ├── campeao_eliminado.ogg │ │ ├── azul_inib_destruido.ogg │ │ ├── azul_torre_destruida.ogg │ │ ├── vermelha_inib_destruido.ogg │ │ └── vermelha_torre_destruida.ogg │ └── images │ │ ├── dragon-mountain.svg │ │ ├── kill.svg │ │ ├── inhibitor.svg │ │ ├── gold.svg │ │ ├── twitter.svg │ │ ├── tower.svg │ │ ├── github.svg │ │ ├── loading.svg │ │ ├── dragon-infernal.svg │ │ ├── dragon-ocean.svg │ │ ├── dragon-cloud.svg │ │ ├── league-of-legends.svg │ │ ├── baron.svg │ │ ├── dragon-elder.svg │ │ └── galaxy.svg ├── index.tsx ├── theme │ ├── Theme.ts │ ├── ThemeContext.tsx │ └── Themes.ts ├── components │ ├── LiveStatusGameCard │ │ ├── MiniHealthBar.tsx │ │ ├── types │ │ │ ├── detailsLiveTypes.ts │ │ │ ├── detailsPersistentTypes.ts │ │ │ └── windowLiveTypes.ts │ │ ├── ItemsDisplay.tsx │ │ ├── LiveGame.tsx │ │ ├── styles │ │ │ └── playerStatusStyle.css │ │ ├── LiveAPIWatcher.tsx │ │ └── PlayersTable.tsx │ ├── Footer │ │ ├── styles │ │ │ └── footerStyle.css │ │ └── Footer.tsx │ ├── Navbar │ │ ├── Navbar.tsx │ │ ├── SoundToggler.tsx │ │ ├── ThemeToggler.tsx │ │ └── styles │ │ │ └── navbarStyle.css │ └── LiveGameCard │ │ ├── types │ │ ├── scheduleType.ts │ │ └── liveGameTypes.ts │ │ ├── LiveGameCard.tsx │ │ ├── ScheduleGameCard.tsx │ │ ├── styles │ │ └── livegameStyle.css │ │ ├── LiveGames.tsx │ │ └── GameCardList.tsx ├── App.tsx ├── styles │ └── global.css └── utils │ └── LoLEsportsAPI.ts ├── README.md ├── tsconfig.json ├── .gitignore ├── public └── index.html ├── package.json └── LICENSE /.env: -------------------------------------------------------------------------------- 1 | GENERATE_SOURCEMAP=false -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/assets/audios/azul_baron.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aureom/live-lol-esports/HEAD/src/assets/audios/azul_baron.ogg -------------------------------------------------------------------------------- /src/assets/audios/firstblood.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aureom/live-lol-esports/HEAD/src/assets/audios/firstblood.ogg -------------------------------------------------------------------------------- /src/assets/audios/azul_dragao.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aureom/live-lol-esports/HEAD/src/assets/audios/azul_dragao.ogg -------------------------------------------------------------------------------- /src/assets/audios/vermelha_baron.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aureom/live-lol-esports/HEAD/src/assets/audios/vermelha_baron.ogg -------------------------------------------------------------------------------- /src/assets/audios/vermelha_dragao.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aureom/live-lol-esports/HEAD/src/assets/audios/vermelha_dragao.ogg -------------------------------------------------------------------------------- /src/assets/audios/campeao_eliminado.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aureom/live-lol-esports/HEAD/src/assets/audios/campeao_eliminado.ogg -------------------------------------------------------------------------------- /src/assets/audios/azul_inib_destruido.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aureom/live-lol-esports/HEAD/src/assets/audios/azul_inib_destruido.ogg -------------------------------------------------------------------------------- /src/assets/audios/azul_torre_destruida.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aureom/live-lol-esports/HEAD/src/assets/audios/azul_torre_destruida.ogg -------------------------------------------------------------------------------- /src/assets/audios/vermelha_inib_destruido.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aureom/live-lol-esports/HEAD/src/assets/audios/vermelha_inib_destruido.ogg -------------------------------------------------------------------------------- /src/assets/audios/vermelha_torre_destruida.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aureom/live-lol-esports/HEAD/src/assets/audios/vermelha_torre_destruida.ogg -------------------------------------------------------------------------------- /src/assets/images/dragon-mountain.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/kill.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | import {ThemeProvider} from "./theme/ThemeContext" 6 | 7 | ReactDOM.render( 8 | 9 | 10 | 11 | 12 | , 13 | document.getElementById('root') 14 | ); 15 | -------------------------------------------------------------------------------- /src/assets/images/inhibitor.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/gold.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/theme/Theme.ts: -------------------------------------------------------------------------------- 1 | export interface Theme { 2 | '--theme-switch-notch': string, 3 | '--theme-switch-bg': string, 4 | '--logo-color': string, 5 | '--card-color': string, 6 | '--background': string, 7 | '--gray-line': string, 8 | '--text': string, 9 | '--text-highlight': string, 10 | '--title': string, 11 | '--red': string, 12 | '--green': string, 13 | '--green-positive': string, 14 | '--blue': string, 15 | '--blue-dark': string, 16 | '--blue-twitter': string, 17 | 'background': string, 18 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Live LoL Esports 2 | 3 | ## 📦 Dependências e programas utilizados 4 | 5 | | Nome | Uso no projeto | 6 | | ------------------------------------------------ | ------------------------------------------------------------ | 7 | | [WebStorm](https://www.jetbrains.com/webstorm/) | IDE | 8 | | [React](https://reactjs.org/) | FrameWork | 9 | | [Documentação da API](https://github.com/vickz84259/lolesports-api-docs) | Documentação da API de esports por [vickz84259](https://github.com/vickz84259) | -------------------------------------------------------------------------------- /src/assets/images/twitter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/components/LiveStatusGameCard/MiniHealthBar.tsx: -------------------------------------------------------------------------------- 1 | 2 | type Props = { 3 | currentHealth: number, 4 | maxHealth: number 5 | } 6 | 7 | export function MiniHealthBar({ currentHealth, maxHealth }: Props) { 8 | return ( 9 |
10 |
11 | {currentHealth}/{maxHealth} 12 |
13 |
14 |
15 | ); 16 | } 17 | 18 | function percentage(partialValue: number, totalValue: number) { 19 | return (100 * partialValue) / totalValue; 20 | } -------------------------------------------------------------------------------- /src/assets/images/tower.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Footer/styles/footerStyle.css: -------------------------------------------------------------------------------- 1 | .footer-container { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | 6 | width: 100%; 7 | height: 5rem; 8 | 9 | box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.4); 10 | background-color: var(--card-color); 11 | } 12 | 13 | .footer-logo{ 14 | display: flex; 15 | align-items: center; 16 | justify-content: center; 17 | flex-direction: column; 18 | } 19 | 20 | .footer-img { 21 | height: 2.5rem; 22 | width: 2.5rem; 23 | margin: 0 1rem; 24 | } 25 | 26 | .footer-img > path { 27 | fill: var(--logo-color); 28 | } 29 | 30 | .footer-icon { 31 | color: red; 32 | font: 500 1.25rem 'Inter', sans-serif; 33 | } 34 | -------------------------------------------------------------------------------- /src/components/Footer/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import './styles/footerStyle.css' 3 | 4 | import {ReactComponent as GitHubLogoSVG} from '../../assets/images/github.svg'; 5 | import {ReactComponent as TwitterLogoSVG} from '../../assets/images/twitter.svg'; 6 | 7 | export function Footer() { 8 | 9 | return ( 10 | 18 | ); 19 | } -------------------------------------------------------------------------------- /src/assets/images/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Navbar/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import './styles/navbarStyle.css' 3 | import {ReactComponent as LoLLogoSVG} from '../../assets/images/league-of-legends.svg'; 4 | 5 | import {Link} from "react-router-dom"; 6 | import { ThemeToggler } from "./ThemeToggler"; 7 | import {SoundToggler} from "./SoundToggler"; 8 | 9 | export function Navbar() { 10 | return ( 11 | 23 | ); 24 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # idea 26 | .idea 27 | 28 | # User-specific stuff: 29 | .idea/workspace.xml 30 | .idea/tasks.xml 31 | .idea/dictionaries 32 | .idea/vcs.xml 33 | .idea/jsLibraryMappings.xml 34 | 35 | # Sensitive or high-churn files: 36 | .idea/dataSources.ids 37 | .idea/dataSources.xml 38 | .idea/dataSources.local.xml 39 | .idea/sqlDataSources.xml 40 | .idea/dynamic.xml 41 | .idea/uiDesigner.xml 42 | 43 | # Gradle: 44 | .idea/gradle.xml 45 | .idea/libraries 46 | 47 | # Mongo Explorer plugin: 48 | .idea/mongoSettings.xml -------------------------------------------------------------------------------- /src/theme/ThemeContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { Dispatch, SetStateAction } from "react"; 2 | import { Theme } from './Theme' 3 | import { THEMES } from './Themes' 4 | 5 | export type ThemeType = "dark" | "light"; 6 | 7 | interface ThemeContextProps { 8 | themeType: ThemeType; 9 | theme: Theme; 10 | setCurrentTheme: Dispatch>; 11 | } 12 | 13 | export const ThemeContext = React.createContext({ 14 | themeType: "dark", 15 | theme: THEMES["dark"], 16 | } as ThemeContextProps); 17 | 18 | export const ThemeProvider: React.FC = ({children}) => { 19 | const [currentTheme, setCurrentTheme] = React.useState("light"); 20 | 21 | return ( 22 | 27 | {children} 28 | 29 | ) 30 | } 31 | 32 | export const useTheme = () => React.useContext(ThemeContext); -------------------------------------------------------------------------------- /src/assets/images/loading.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/components/LiveStatusGameCard/types/detailsLiveTypes.ts: -------------------------------------------------------------------------------- 1 | export interface PerkMetadata { 2 | styleId: number; 3 | subStyleId: number; 4 | perks: number[]; 5 | } 6 | 7 | export interface Participant { 8 | participantId: number; 9 | level: number; 10 | kills: number; 11 | deaths: number; 12 | assists: number; 13 | totalGoldEarned: number; 14 | creepScore: number; 15 | killParticipation: number; 16 | championDamageShare: number; 17 | wardsPlaced: number; 18 | wardsDestroyed: number; 19 | attackDamage: number; 20 | abilityPower: number; 21 | criticalChance: number; 22 | attackSpeed: number; 23 | lifeSteal: number; 24 | armor: number; 25 | magicResistance: number; 26 | tenacity: number; 27 | items: number[]; 28 | perkMetadata: PerkMetadata; 29 | abilities: string[]; 30 | } 31 | 32 | export interface Frame { 33 | rfc460Timestamp: Date; 34 | participants: Participant[]; 35 | } 36 | 37 | export interface Detais { 38 | frames: Frame[]; 39 | } -------------------------------------------------------------------------------- /src/assets/images/dragon-infernal.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/dragon-ocean.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | LoL Live Esports 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | -------------------------------------------------------------------------------- /src/assets/images/dragon-cloud.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Navbar/SoundToggler.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from "react"; 2 | import './styles/navbarStyle.css' 3 | 4 | // Anti ESA na live do Baiano 5 | export function SoundToggler() { 6 | const [toggled, setToggled] = useState(true); 7 | 8 | useEffect(() => { 9 | const soundData = localStorage.getItem("sound"); 10 | if(soundData) { 11 | if (soundData === "mute") { 12 | setToggled(false); 13 | } else if (soundData === "unmute") { 14 | setToggled(true) 15 | } 16 | } 17 | }); 18 | 19 | const handleClick = () => { 20 | if(toggled) { 21 | localStorage.setItem("sound", "mute"); 22 | }else{ 23 | localStorage.setItem("sound", "unmute"); 24 | } 25 | 26 | setToggled((s) => !s); 27 | } 28 | 29 | return ( 30 |
31 |
32 |
{`${toggled ? "🔊" : "🔈"}`}
33 |
34 |
35 | ); 36 | } -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import './styles/global.css' 2 | import 'react-toastify/dist/ReactToastify.min.css'; 3 | 4 | import {LiveGame} from "./components/LiveStatusGameCard/LiveGame"; 5 | import {HashRouter, Switch, Route, Redirect} from "react-router-dom"; 6 | import {Footer} from "./components/Footer/Footer"; 7 | import {LiveGames} from "./components/LiveGameCard/LiveGames"; 8 | import {Navbar} from "./components/Navbar/Navbar"; 9 | import { useTheme } from './theme/ThemeContext' 10 | import React from "react"; 11 | 12 | function App() { 13 | const { theme } = useTheme(); 14 | 15 | return ( 16 | 17 |
18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 |
26 |
27 |
28 |
29 | ); 30 | } 31 | 32 | export default App; 33 | -------------------------------------------------------------------------------- /src/components/LiveGameCard/types/scheduleType.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface Pages { 3 | older: string; 4 | newer: string; 5 | } 6 | 7 | export interface League { 8 | name: string; 9 | slug: string; 10 | image: string; 11 | } 12 | 13 | export interface Result { 14 | outcome: string; 15 | gameWins: number; 16 | } 17 | 18 | export interface Record { 19 | wins: number; 20 | losses: number; 21 | } 22 | 23 | export interface Team { 24 | name: string; 25 | code: string; 26 | image: string; 27 | result: Result; 28 | record: Record; 29 | } 30 | 31 | export interface Strategy { 32 | type: string; 33 | count: number; 34 | } 35 | 36 | export interface Match { 37 | id: string; 38 | flags: string[]; 39 | teams: Team[]; 40 | strategy: Strategy; 41 | } 42 | 43 | export interface Event { 44 | startTime: Date; 45 | state: string; 46 | type: string; 47 | blockName: string; 48 | league: League; 49 | match: Match; 50 | } 51 | 52 | export interface Schedule { 53 | pages: Pages; 54 | events: Event[]; 55 | } 56 | 57 | export interface Data { 58 | schedule: Schedule; 59 | } 60 | 61 | export interface ScheduleData { 62 | data: Data; 63 | } 64 | 65 | -------------------------------------------------------------------------------- /src/assets/images/league-of-legends.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/styles/global.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | transition: all 0.2s ease; 6 | 7 | } 8 | 9 | /*:root { 10 | --card-color: #FFF; 11 | --background: #F2F3F5; 12 | --gray-line: #DCDDE0; 13 | --text: #666666; 14 | --text-highlight: #B3B9FF; 15 | --title: #2E384D; 16 | --red: #E83F5B; 17 | --green: #4CD62B; 18 | --green-positive: #61b875; 19 | --blue: #5965E0; 20 | --blue-dark: #4953B8; 21 | --blue-twitter: #2AA9E0; 22 | }*/ 23 | 24 | body { 25 | background: var(--background); 26 | color: var(--text); 27 | } 28 | 29 | @media(max-width: 1080px) { 30 | html { 31 | font-size: 93.75%; 32 | } 33 | } 34 | 35 | @media(max-width: 720px) { 36 | html { 37 | font-size: 87.5%; 38 | } 39 | } 40 | 41 | body, input, textarea, button { 42 | font: 400 1rem 'Inter', sans-serif; 43 | } 44 | 45 | span, h1, h2, h3, h4 { 46 | color: var(--text); 47 | } 48 | 49 | button { 50 | cursor: pointer; 51 | } 52 | 53 | a { 54 | color: inherit; 55 | text-decoration: none; 56 | } 57 | 58 | .container { 59 | min-height: 85vh; 60 | 61 | max-width: 1080px; 62 | margin: 0 auto; 63 | padding: 2.5rem 2rem; 64 | 65 | display: flex; 66 | flex-direction: column; 67 | 68 | position: relative; 69 | } -------------------------------------------------------------------------------- /src/components/Navbar/ThemeToggler.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from "react"; 2 | import './styles/navbarStyle.css' 3 | 4 | import { useTheme } from "../../theme/ThemeContext"; 5 | 6 | export function ThemeToggler() { 7 | const { setCurrentTheme} = useTheme(); 8 | const [toggled, setToggled] = useState(false); 9 | 10 | useEffect(() => { 11 | const themeData = localStorage.getItem("theme"); 12 | if(themeData) { 13 | if (themeData === "light") { 14 | setCurrentTheme("light"); 15 | setToggled(false); 16 | } else { 17 | setCurrentTheme("dark"); 18 | setToggled(true) 19 | } 20 | } 21 | }); 22 | 23 | const handleClick = () => { 24 | if(toggled) { 25 | setCurrentTheme("light"); 26 | localStorage.setItem("theme", "light"); 27 | }else{ 28 | setCurrentTheme("dark"); 29 | localStorage.setItem("theme", "dark"); 30 | } 31 | 32 | setToggled((s) => !s); 33 | } 34 | 35 | return ( 36 |
37 |
38 |
🌙
39 |
40 |
41 | ); 42 | } -------------------------------------------------------------------------------- /src/assets/images/baron.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/LiveStatusGameCard/types/detailsPersistentTypes.ts: -------------------------------------------------------------------------------- 1 | export interface Tournament { 2 | id: string; 3 | } 4 | 5 | export interface League { 6 | id: string; 7 | slug: string; 8 | image: string; 9 | name: string; 10 | } 11 | 12 | export interface Strategy { 13 | count: number; 14 | } 15 | 16 | export interface Result { 17 | gameWins: number; 18 | } 19 | 20 | export interface Team { 21 | id: string; 22 | name: string; 23 | code: string; 24 | image: string; 25 | result: Result; 26 | } 27 | 28 | export interface Team2 { 29 | id: string; 30 | side: string; 31 | } 32 | 33 | export interface Game { 34 | number: number; 35 | id: string; 36 | state: string; 37 | teams: Team2[]; 38 | vods: any[]; 39 | } 40 | 41 | export interface Match { 42 | strategy: Strategy; 43 | teams: Team[]; 44 | games: Game[]; 45 | } 46 | 47 | export interface Stream { 48 | parameter: string; 49 | locale: string; 50 | provider: string; 51 | countries: string[]; 52 | offset: number; 53 | } 54 | 55 | export interface Event { 56 | id: string; 57 | type: string; 58 | tournament: Tournament; 59 | league: League; 60 | match: Match; 61 | streams: Stream[]; 62 | } 63 | 64 | export interface Data { 65 | event: Event; 66 | } 67 | 68 | export interface GameDetails { 69 | data: Data; 70 | } -------------------------------------------------------------------------------- /src/components/LiveGameCard/types/liveGameTypes.ts: -------------------------------------------------------------------------------- 1 | export interface League { 2 | id: string; 3 | slug: string; 4 | name: string; 5 | image: string; 6 | priority: number; 7 | } 8 | 9 | export interface Result { 10 | outcome: string; 11 | gameWins: number; 12 | } 13 | 14 | export interface Record { 15 | wins: number; 16 | losses: number; 17 | } 18 | 19 | export interface Team { 20 | id: string; 21 | name: string; 22 | slug: string; 23 | code: string; 24 | image: string; 25 | result: Result; 26 | record: Record; 27 | } 28 | 29 | export interface Strategy { 30 | type: string; 31 | count: number; 32 | } 33 | 34 | export interface Match { 35 | id: string; 36 | teams: Team[]; 37 | strategy: Strategy; 38 | } 39 | 40 | export interface Stream { 41 | parameter: string; 42 | locale: string; 43 | provider: string; 44 | countries: string[]; 45 | offset: number; 46 | } 47 | 48 | export interface Event { 49 | id: string; 50 | startTime: Date; 51 | state: string; 52 | type: string; 53 | blockName: string; 54 | league: League; 55 | match: Match; 56 | streams: Stream[]; 57 | } 58 | 59 | export interface Schedule { 60 | events: Event[]; 61 | } 62 | 63 | export interface Data { 64 | schedule: Schedule; 65 | } 66 | 67 | export interface LoLLiveGames { 68 | data: Data; 69 | } -------------------------------------------------------------------------------- /src/components/LiveGameCard/LiveGameCard.tsx: -------------------------------------------------------------------------------- 1 | import {Link} from "react-router-dom"; 2 | import {Event} from "./types/liveGameTypes"; 3 | 4 | type Props = { 5 | game: Event; 6 | } 7 | 8 | export function LiveGameCard({ game }: Props) { 9 | return ( 10 | 11 |
12 |

{game.league.name}

13 | 14 |
15 |
16 | {game.match.teams[0].name}/ 18 | 19 | {game.match.teams[0].name} 20 | 21 |
22 | 23 |

VS

24 | 25 |
26 | {game.match.teams[1].name}/ 28 | 29 | {game.match.teams[1].name} 30 | 31 |
32 |
33 |
34 | 35 | ); 36 | } -------------------------------------------------------------------------------- /src/components/LiveGameCard/ScheduleGameCard.tsx: -------------------------------------------------------------------------------- 1 | import {Link} from "react-router-dom"; 2 | import {Event} from "./types/scheduleType"; 3 | 4 | type Props = { 5 | game: Event; 6 | } 7 | 8 | export function ScheduleGameCard({ game }: Props) { 9 | return ( 10 | 11 |
12 |

{game.league.name}

13 | 14 |
15 |
16 | {game.match.teams[0].name}/ 18 | 19 | {game.match.teams[0].name} 20 | 21 |
22 | 23 |

VS

24 | 25 |
26 | {game.match.teams[1].name}/ 28 | 29 | {game.match.teams[1].name} 30 | 31 |
32 |
33 |
34 | 35 | ); 36 | } -------------------------------------------------------------------------------- /src/theme/Themes.ts: -------------------------------------------------------------------------------- 1 | import { ThemeType } from './ThemeContext' 2 | import { Theme } from './Theme' 3 | 4 | export const THEMES: Record = { 5 | light: { 6 | '--theme-switch-notch': "#2f363d", 7 | '--theme-switch-bg': "#FFF", 8 | '--logo-color': "#12181B", 9 | '--card-color': "#FFF", 10 | '--background': "#F2F3F5", 11 | '--gray-line': "#DCDDE0", 12 | '--text': "#666666", 13 | '--text-highlight': "#B3B9FF", 14 | '--title': "#2E384D", 15 | '--red': "#E83F5B", 16 | '--green': "#4CD62B", 17 | '--green-positive': "#61b875", 18 | '--blue': "#5965E0", 19 | '--blue-dark': "#4953B8", 20 | '--blue-twitter': "#2AA9E0", 21 | 'background': "var(--background)", 22 | }, 23 | dark: { 24 | '--theme-switch-notch': "#9E25FC", 25 | '--theme-switch-bg': "#6d18b0", 26 | '--logo-color': "#FFF", 27 | '--card-color': "#12181B", 28 | '--background': "#2A2E35", 29 | '--gray-line': "#2a2e35", 30 | '--text': "#B2BECD", 31 | '--text-highlight': "#B3B9FF", 32 | '--title': "#FFF", 33 | '--red': "#E83F5B", 34 | '--green': "#479335", 35 | '--green-positive': "#61b875", 36 | '--blue': "#5965E0", 37 | '--blue-dark': "#4953B8", 38 | '--blue-twitter': "#2AA9E0", 39 | 'background': "var(--background)", 40 | } 41 | } -------------------------------------------------------------------------------- /src/components/LiveStatusGameCard/types/windowLiveTypes.ts: -------------------------------------------------------------------------------- 1 | export interface ParticipantMetadata { 2 | participantId: number; 3 | esportsPlayerId: string; 4 | summonerName: string; 5 | championId: string; 6 | role: string; 7 | } 8 | 9 | export interface BlueTeamMetadata { 10 | esportsTeamId: string; 11 | participantMetadata: ParticipantMetadata[]; 12 | } 13 | 14 | export interface RedTeamMetadata { 15 | esportsTeamId: string; 16 | participantMetadata: ParticipantMetadata[]; 17 | } 18 | 19 | export interface GameMetadata { 20 | patchVersion: string; 21 | blueTeamMetadata: BlueTeamMetadata; 22 | redTeamMetadata: RedTeamMetadata; 23 | } 24 | 25 | export interface Participant { 26 | participantId: number; 27 | totalGold: number; 28 | level: number; 29 | kills: number; 30 | deaths: number; 31 | assists: number; 32 | creepScore: number; 33 | currentHealth: number; 34 | maxHealth: number; 35 | } 36 | 37 | export interface BlueTeam { 38 | totalGold: number; 39 | inhibitors: number; 40 | towers: number; 41 | barons: number; 42 | totalKills: number; 43 | dragons: string[]; 44 | participants: Participant[]; 45 | } 46 | 47 | export interface RedTeam { 48 | totalGold: number; 49 | inhibitors: number; 50 | towers: number; 51 | barons: number; 52 | totalKills: number; 53 | dragons: string[]; 54 | participants: Participant[]; 55 | } 56 | 57 | export interface Frame { 58 | rfc460Timestamp: Date; 59 | gameState: string; 60 | blueTeam: BlueTeam; 61 | redTeam: RedTeam; 62 | } 63 | 64 | export interface WindowLive { 65 | esportsGameId: string; 66 | esportsMatchId: string; 67 | gameMetadata: GameMetadata; 68 | frames: Frame[]; 69 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "homepage": "http://aureom.github.io/live-lol-esports", 3 | "name": "lol-live-eports-api", 4 | "version": "0.1.0", 5 | "private": true, 6 | "dependencies": { 7 | "@testing-library/jest-dom": "^5.11.4", 8 | "@testing-library/react": "^11.1.0", 9 | "@testing-library/user-event": "^12.1.10", 10 | "@types/axios": "^0.14.0", 11 | "@types/jest": "^26.0.15", 12 | "@types/node": "^12.0.0", 13 | "@types/react": "^17.0.0", 14 | "@types/react-dom": "^17.0.0", 15 | "@types/react-helmet": "^6.1.0", 16 | "@types/react-router-dom": "^5.1.7", 17 | "@types/socket.io-client": "^1.4.35", 18 | "axios": "^0.21.1", 19 | "bignumber.js": "^9.0.1", 20 | "gh-pages": "^3.1.0", 21 | "react": "^17.0.1", 22 | "react-dom": "^17.0.1", 23 | "react-helmet": "^6.1.0", 24 | "react-router-dom": "^5.2.0", 25 | "react-scripts": "4.0.3", 26 | "react-toastify": "^7.0.3", 27 | "riot-lol-api": "^4.3.2", 28 | "socket.io-client": "^3.1.1", 29 | "typescript": "^4.1.2", 30 | "use-sound": "^2.0.1", 31 | "web-vitals": "^1.0.1" 32 | }, 33 | "scripts": { 34 | "predeploy": "yarn run build", 35 | "deploy": "gh-pages -d build", 36 | "start": "react-scripts start", 37 | "build": "react-scripts build", 38 | "test": "react-scripts test", 39 | "eject": "react-scripts eject" 40 | }, 41 | "eslintConfig": { 42 | "extends": [ 43 | "react-app", 44 | "react-app/jest" 45 | ] 46 | }, 47 | "browserslist": { 48 | "production": [ 49 | ">0.2%", 50 | "not dead", 51 | "not op_mini all" 52 | ], 53 | "development": [ 54 | "last 1 chrome version", 55 | "last 1 firefox version", 56 | "last 1 safari version" 57 | ] 58 | }, 59 | "devDependencies": {} 60 | } 61 | -------------------------------------------------------------------------------- /src/components/LiveGameCard/styles/livegameStyle.css: -------------------------------------------------------------------------------- 1 | .live-game-card { 2 | width: 25rem; 3 | padding: 0.5rem; 4 | text-align: center; 5 | background: var(--card-color); 6 | box-shadow: 0 1px 3px -1px rgba(0, 0, 0, 0.4); 7 | border-radius: 5px; 8 | transition: all .2s ease-in-out; 9 | } 10 | 11 | .live-game-card > h3 { 12 | padding: 0.2rem 0; 13 | color: var(--text); 14 | } 15 | 16 | .live-game-card-content{ 17 | display: flex; 18 | justify-content: center; 19 | } 20 | 21 | .live-game-card-content > h1 { 22 | display: flex; 23 | align-items: center; 24 | padding: 0 1rem; 25 | color: var(--text); 26 | } 27 | 28 | .live-game-card:hover { 29 | transform: scale(1.05); 30 | } 31 | 32 | .live-game-card-team-image { 33 | width: 4rem; 34 | height: 4rem; 35 | 36 | border-radius: 5px; 37 | margin: 1rem 3rem; 38 | padding: 0.30rem; 39 | box-shadow: inset 0 1px 3px 1px rgba(0, 0, 0, 0.5); 40 | 41 | background: var(--background); 42 | } 43 | 44 | .live-game-card-title { 45 | font-weight: 500; 46 | color: var(--text); 47 | } 48 | 49 | .empty-games-list-container{ 50 | display: flex; 51 | flex-direction: column; 52 | justify-content: center; 53 | align-items: center; 54 | } 55 | 56 | .empty-games-galaxy{ 57 | margin: 5rem; 58 | width: 50%; 59 | } 60 | 61 | .games-list-container { 62 | display: flex; 63 | justify-content: center; 64 | } 65 | 66 | .games-list-items { 67 | width: 100%; 68 | display: grid; 69 | grid-template-columns: repeat(auto-fill, 25rem); 70 | grid-gap: 2rem; 71 | justify-content: center; 72 | align-items: center; 73 | } 74 | 75 | .games-separator { 76 | margin: 2rem 0; 77 | border-bottom: 1px solid var(--text); 78 | width: 100%; 79 | height: 3px; 80 | } 81 | 82 | .games-of-day { 83 | margin-bottom: 1rem; 84 | display: flex; 85 | align-items: center; 86 | justify-content: center; 87 | } -------------------------------------------------------------------------------- /src/components/LiveGameCard/LiveGames.tsx: -------------------------------------------------------------------------------- 1 | import './styles/livegameStyle.css' 2 | 3 | import {getLiveGames, getSchedule} from "../../utils/LoLEsportsAPI"; 4 | import {GameCardList} from "./GameCardList"; 5 | import {useEffect, useState} from "react"; 6 | 7 | import {Event as LiveEvents} from "./types/liveGameTypes"; 8 | import {Event as TodayEvent} from "./types/scheduleType"; 9 | 10 | export function LiveGames() { 11 | const [liveEvents, setLiveEvents] = useState([]) 12 | const [todayEvents, setTodayEvents] = useState([]) 13 | 14 | 15 | useEffect(() => { 16 | getLiveGames().then(response => { 17 | setLiveEvents(response.data.data.schedule.events.filter(filterByTeams)) 18 | }).catch(error => 19 | console.error(error) 20 | ) 21 | 22 | getSchedule().then(response => { 23 | setTodayEvents(response.data.data.schedule.events.filter(filterByTodayDate)); 24 | 25 | }).catch(error => 26 | console.error(error) 27 | ) 28 | }, []) 29 | 30 | document.title = "LoL Live Esports"; 31 | 32 | return ( 33 |
34 | 37 |
38 | ); 39 | } 40 | 41 | function filterByTeams(event: LiveEvents) { 42 | return event.match !== undefined; 43 | } 44 | 45 | let date = new Date(Date.now()); 46 | function filterByTodayDate(event: TodayEvent) { 47 | let eventDate = event.startTime.toString().split("T")[0].split("-"); 48 | 49 | if(parseInt(eventDate[0]) === date.getFullYear() && 50 | parseInt(eventDate[1]) === (date.getUTCMonth() + 1) && 51 | parseInt(eventDate[2]) === date.getDate()){ 52 | 53 | if(event.match === undefined) return false 54 | if(event.match.id === undefined) return false 55 | 56 | return true; 57 | }else{ 58 | return false; 59 | } 60 | } -------------------------------------------------------------------------------- /src/assets/images/dragon-elder.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/utils/LoLEsportsAPI.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | //export const ITEMS_URL = "https://ddragon.leagueoflegends.com/cdn/11.5.1/img/item/" 4 | export const ITEMS_URL = "https://ddragon.bangingheads.net/cdn/latest/img/item/" 5 | export const CHAMPIONS_URL = "https://ddragon.bangingheads.net/cdn/latest/img/champion/" 6 | 7 | const API_URL_PERSISTED = "https://esports-api.lolesports.com/persisted/gw" 8 | const API_URL_LIVE = "https://feed.lolesports.com/livestats/v1" 9 | const API_KEY = "0TvQnueqKa5mxJntVWt0w4LpLfEkrV1Ta8rQBb9Z" 10 | 11 | export function getLiveGames() { 12 | return axios.get(`${API_URL_PERSISTED}/getLive?hl=pt-BR`, { 13 | headers: { 14 | "x-api-key": API_KEY, 15 | }, 16 | }) 17 | } 18 | 19 | export function getSchedule() { 20 | return axios.get(`${API_URL_PERSISTED}/getSchedule?hl=pt-BR`, { 21 | headers: { 22 | "x-api-key": API_KEY, 23 | }, 24 | }) 25 | } 26 | 27 | export function getLiveWindowGame(gameId: string, date: string) { 28 | return axios.get(`${API_URL_LIVE}/window/${gameId}`, { 29 | params: { 30 | "hl": "pt-BR", 31 | "startingTime": date, 32 | }, 33 | headers: { 34 | "x-api-key": API_KEY, 35 | }, 36 | }) 37 | } 38 | 39 | export function getLiveDetailsGame(gameId: string, date: string) { 40 | return axios.get(`${API_URL_LIVE}/details/${gameId}`, { 41 | params: { 42 | "hl": "pt-BR", 43 | "startingTime": date, 44 | }, 45 | headers: { 46 | "x-api-key": API_KEY, 47 | }, 48 | }) 49 | } 50 | 51 | export function getGameDetails(gameId: string) { 52 | return axios.get(`${API_URL_PERSISTED}/getEventDetails`, { 53 | params: { 54 | "hl": "pt-BR", 55 | "id": gameId, 56 | }, 57 | headers: { 58 | "x-api-key": API_KEY, 59 | }, 60 | }) 61 | } 62 | 63 | 64 | export function getISODateMultiplyOf10() { 65 | const date = new Date(); 66 | date.setMilliseconds(0); 67 | 68 | if (date.getSeconds() % 10 !== 0) { 69 | date.setSeconds(date.getSeconds() - (date.getSeconds() % 10)); 70 | } 71 | 72 | date.setSeconds(date.getSeconds() - 60); 73 | 74 | return date.toISOString(); 75 | } 76 | -------------------------------------------------------------------------------- /src/components/Navbar/styles/navbarStyle.css: -------------------------------------------------------------------------------- 1 | .navbar-container { 2 | display: flex; 3 | justify-content: center; 4 | 5 | width: 100%; 6 | height: 5rem; 7 | 8 | background-color: var(--card-color); 9 | box-shadow: 0 1px 3px -1px rgba(0, 0, 0, 0.4); 10 | } 11 | 12 | .navbar-logo-container{ 13 | display: flex; 14 | justify-content: center; 15 | } 16 | 17 | .navbar-logo { 18 | display: flex; 19 | align-items: center; 20 | justify-content: center; 21 | flex-direction: column; 22 | } 23 | 24 | .navbar-container::before, .navbar-logo-container, .toggle-container { 25 | content: ''; 26 | flex-basis: 100%; 27 | } 28 | 29 | .navbar-icon-img { 30 | width: 2.5rem; 31 | } 32 | 33 | .navbar-icon-img > path { 34 | fill: var(--logo-color); 35 | } 36 | 37 | .navbar-icon { 38 | color: var(--title); 39 | font: 500 1.25rem 'Inter', sans-serif; 40 | margin-bottom: 0.5rem; 41 | } 42 | 43 | /* 44 | theme and sound toggle 45 | */ 46 | 47 | .settings-container { 48 | display: flex; 49 | align-items: center; 50 | width: 100%; 51 | } 52 | 53 | .sound-toggle { 54 | margin-left: 100%; 55 | cursor: pointer; 56 | height: 1.5rem; 57 | width: 2.5rem; 58 | border-radius: 50px; 59 | border: 3px solid var(--gray-line); 60 | background-color: var(--theme-switch-bg); 61 | } 62 | 63 | .theme-toggle { 64 | margin-left: 35%; 65 | cursor: pointer; 66 | height: 1.5rem; 67 | width: 2.5rem; 68 | border-radius: 50px; 69 | border: 3px solid var(--gray-line); 70 | background-color: var(--theme-switch-bg); 71 | } 72 | 73 | @media(max-width: 980px) { 74 | .theme-toggle { 75 | margin-left: 0; 76 | } 77 | .sound-toggle { 78 | margin-left: 0; 79 | } 80 | } 81 | 82 | .notch { 83 | display: flex; 84 | justify-content: center; 85 | align-items: center; 86 | height: 1.75rem; 87 | width: 1.75rem; 88 | border-radius: 50%; 89 | background-color: var(--theme-switch-notch); 90 | transform: translate(-50%, -20%); 91 | transition: all 0.5s; 92 | } 93 | 94 | .dark > .notch { 95 | transform: translate(50%, -20%); 96 | } 97 | 98 | .muted > .notch { 99 | transform: translate(50%, -20%); 100 | } -------------------------------------------------------------------------------- /src/components/LiveGameCard/GameCardList.tsx: -------------------------------------------------------------------------------- 1 | import {LiveGameCard} from "./LiveGameCard"; 2 | import {ScheduleGameCard} from "./ScheduleGameCard"; 3 | 4 | import {Event as LiveEvent} from "./types/liveGameTypes"; 5 | import {Event as TodayEvent} from "./types/scheduleType"; 6 | 7 | import Galaxy from "../../assets/images/galaxy.svg" 8 | 9 | type Props = { 10 | liveGames: LiveEvent[]; 11 | todayGames: TodayEvent[]; 12 | } 13 | 14 | export function GameCardList({ liveGames, todayGames }: Props) { 15 | return ( 16 |
17 | 18 | 19 |
20 | 21 | 22 |
23 | ); 24 | } 25 | 26 | type PropsLive = { 27 | liveGames: LiveEvent[]; 28 | } 29 | 30 | function LiveGames({liveGames}: PropsLive) { 31 | if (liveGames !== undefined && liveGames.length !== 0) { 32 | return ( 33 |
34 |
35 | {liveGames.map(game => ( 36 | 40 | ))} 41 |
42 |
43 | ); 44 | }else { 45 | return ( 46 |
47 | nenhum jogo ao vivo 48 |

NENHUM JOGO AO VIVO

49 |
50 | ); 51 | } 52 | } 53 | 54 | type PropsToday = { 55 | todayGames: TodayEvent[]; 56 | } 57 | 58 | function TodayGames({todayGames}: PropsToday) { 59 | if (todayGames !== undefined && todayGames.length !== 0) { 60 | 61 | let date = new Date(); 62 | 63 | return ( 64 |
65 |

JOGOS DO DIA

66 |
67 |
68 | {todayGames.map(game => ( 69 | 73 | ))} 74 |
75 |
76 |
77 | ); 78 | }else{ 79 | return ( 80 |
81 | ); 82 | } 83 | } -------------------------------------------------------------------------------- /src/components/LiveStatusGameCard/ItemsDisplay.tsx: -------------------------------------------------------------------------------- 1 | import {Frame} from "./types/detailsLiveTypes"; 2 | 3 | import {ITEMS_URL} from "../../utils/LoLEsportsAPI" 4 | 5 | type Props = { 6 | participantId: number, 7 | lastFrame: Frame 8 | } 9 | 10 | export function ItemsDisplay({ participantId, lastFrame }: Props) { 11 | 12 | const items = lastFrame.participants[participantId].items; 13 | 14 | /* 15 | A api da riot não nos retorna nada sobre o arauto, quando 16 | um jogador pega o arauto sua trinket some para sempre (a 17 | menos que ele retorne na base e compre outra), assim podemos 18 | supor que o jogador pegou o arauto 19 | 20 | Update: 21 | Infelizmente por todo o projeto ser client side o jogador 22 | sempre estará com arauto, futuramente se fomos fazer algo 23 | a logica do arauto poderá ser implementada server-side, 24 | retirando o arauto após 240s 25 | */ 26 | 27 | /*if (!(items.includes(3340) || items.includes(3363) || items.includes(3364))) { 28 | items.push(3513); // Supondo que o jogador que não possui ward está com arauto 29 | }*/ 30 | 31 | let trinket = -1; 32 | const itemsID = Array.from(new Set(items)).sort(sortItems); 33 | 34 | if(itemsID[0] !== undefined && (itemsID[0] === 3340 || itemsID[0] === 3363 || itemsID[0] === 3364)) { 35 | trinket = itemsID.shift() as number; 36 | } 37 | 38 | return ( 39 |
40 | {[...Array(6)].map((x, i) => { 41 | 42 | if(itemsID[i] !== undefined) { 43 | return ( 44 |
45 | 46 |
47 | ) 48 | }else{ 49 | return ( 50 |
51 | ) 52 | } 53 | 54 | }) 55 | } 56 | 57 | 58 | {trinket !== -1 ? 59 | ( 60 |
61 | 62 |
63 | ) 64 | : 65 | ( 66 |
67 | ) 68 | } 69 | 70 |
71 | ); 72 | } 73 | 74 | /* 75 | (3364, 3363, 3340) são os ids das trinkets (wards) 76 | para verificar se um jogar pegou o arauto, basicamente 77 | vemos se o jogador não possui nenhuma trinket, logo 78 | adicionamos o id 3513 (arauto) ao seus itens 79 | */ 80 | 81 | const sortItems = (a: number, b: number) => { // (3364, 3363, 3340) id das wards | 3513 id do arauto 82 | if(a === 3364 || a === 3363 || a === 3340 || a === 3513) return -1; 83 | if(b === 3364 || b === 3363 || b === 3340 || a === 3513) return 1; 84 | 85 | //return (a > b ? 1 : -1); 86 | return b - a; 87 | } -------------------------------------------------------------------------------- /src/components/LiveStatusGameCard/LiveGame.tsx: -------------------------------------------------------------------------------- 1 | import './styles/playerStatusStyle.css' 2 | 3 | import { 4 | getGameDetails, 5 | getISODateMultiplyOf10, 6 | getLiveDetailsGame, 7 | getLiveWindowGame 8 | } from "../../utils/LoLEsportsAPI"; 9 | import {useEffect, useState} from "react"; 10 | import {GameMetadata, Frame as FrameWindow} from "./types/windowLiveTypes"; 11 | import Loading from '../../assets/images/loading.svg' 12 | import {PlayersTable} from "./PlayersTable"; 13 | import BigNumber from "bignumber.js"; 14 | import {Frame as FrameDetails} from "./types/detailsLiveTypes"; 15 | import {GameDetails} from "./types/detailsPersistentTypes"; 16 | 17 | export function LiveGame({ match }: any) { 18 | const [lastFrameWindow, setLastFrameWindow] = useState(); 19 | const [lastFrameDetails, setLastFrameDetails] = useState(); 20 | const [gameData, setGameData] = useState(); 21 | const [metadata, setMetadata] = useState(); 22 | 23 | const matchId = match.params.gameid; 24 | const preGameId = new BigNumber(matchId); 25 | let gameId = BigNumber.sum(preGameId, 1).toString(); 26 | 27 | useEffect(() => { 28 | getLiveGameDetails(); 29 | /*getLiveWindow(); 30 | getLiveGameStatus();*/ 31 | 32 | const windowIntervalID = setInterval(() => { 33 | getLiveWindow(); 34 | getLiveGameStatus(); 35 | }, 500); 36 | 37 | return () => { 38 | clearInterval(windowIntervalID); 39 | } 40 | 41 | function getLiveWindow(){ 42 | let date = getISODateMultiplyOf10(); 43 | getLiveWindowGame(gameId, date).then(response => { 44 | let frames = response.data.frames; 45 | if(frames === undefined) return; 46 | 47 | setLastFrameWindow(frames[frames.length - 1]) 48 | setMetadata(response.data.gameMetadata) 49 | }); 50 | } 51 | 52 | function getLiveGameStatus() { 53 | let date = getISODateMultiplyOf10(); 54 | getLiveDetailsGame(gameId, date).then(response => { 55 | let frames = response.data.frames; 56 | if(frames === undefined) return; 57 | 58 | setLastFrameDetails(frames[frames.length - 1]) 59 | }); 60 | } 61 | 62 | function getLiveGameDetails() { 63 | getGameDetails(matchId).then(response => { 64 | let gameData: GameDetails = response.data; 65 | if(gameData === undefined) return; 66 | 67 | for (const game of gameData.data.event.match.games) { 68 | if(game.state === "inProgress"){ 69 | gameId = BigNumber.sum(preGameId, game.number).toString() 70 | console.log(gameId) 71 | } 72 | } 73 | setGameData(gameData); 74 | }) 75 | } 76 | }, [matchId]); 77 | 78 | /*if(gameId === "0") { 79 | return ( 80 | 81 | ) 82 | }*/ 83 | 84 | if(lastFrameWindow !== undefined && lastFrameDetails !== undefined && metadata !== undefined && gameData !== undefined) { 85 | return ( 86 | 87 | ); 88 | }else { 89 | return( 90 |
91 | game loading 92 |
93 | ) 94 | } 95 | } -------------------------------------------------------------------------------- /src/assets/images/galaxy.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/LiveStatusGameCard/styles/playerStatusStyle.css: -------------------------------------------------------------------------------- 1 | .mini-health-bar { 2 | display: flex; 3 | align-items: center; 4 | } 5 | 6 | .mini-health-bar > div { 7 | flex: 1; 8 | border-radius: 4px; 9 | min-width: 5rem; 10 | background: var(--gray-line); 11 | position: relative; 12 | } 13 | 14 | .mini-health-bar > div > div { 15 | height: 28px; 16 | border-radius: 4px; 17 | transition: width 1s; 18 | background: var(--green); 19 | } 20 | 21 | span.mini-current-health { 22 | position: absolute; 23 | top: 50%; 24 | left: 50%; 25 | transform: translate(-50%, -50%); 26 | font-size: 12px; 27 | font-weight: 400; 28 | } 29 | 30 | /* 31 | HEADER IMAGES 32 | */ 33 | 34 | .live-game-stats-header-team-images { 35 | display: flex; 36 | align-items: center; 37 | } 38 | 39 | .live-game-stats-header-team-images > h1 { 40 | flex: 1; 41 | text-align: center; 42 | } 43 | 44 | .live-game-stats-header-team-images > h1 > h3 { 45 | font-weight: 500; 46 | font-size: 1rem; 47 | } 48 | 49 | .live-game-stats-header-team-images > h3 { 50 | flex: 1; 51 | text-align: center; 52 | } 53 | 54 | .live-game-stats-header-team-images > .blue-team { 55 | flex: 1; 56 | display: flex; 57 | justify-content: center; 58 | 59 | width: 4rem; 60 | height: 4rem; 61 | 62 | border-radius: 5px; 63 | padding: 0.30rem; 64 | 65 | filter: drop-shadow(0px 0px 0px #222); 66 | 67 | } 68 | 69 | .live-game-stats-header-team-images > .red-team { 70 | display: flex; 71 | flex: 1; 72 | flex-direction: row-reverse; 73 | justify-content: center; 74 | text-align: right; 75 | 76 | width: 4rem; 77 | height: 4rem; 78 | 79 | border-radius: 5px; 80 | padding: 0.30rem; 81 | filter: drop-shadow(0px 0px 0px #222); 82 | } 83 | 84 | /* 85 | HEADER STATUS 86 | */ 87 | 88 | .live-game-stats-header-status { 89 | margin-top: 1.5rem; 90 | display: flex; 91 | } 92 | 93 | .live-game-stats-header-status > .blue-team > .team-stats > svg > path { 94 | fill: var(--blue-twitter); 95 | } 96 | 97 | .live-game-stats-header-status > .blue-team > .team-stats > svg > g { 98 | fill: var(--blue-twitter); 99 | } 100 | 101 | .live-game-stats-header-status > .red-team > .team-stats > svg > path { 102 | fill: var(--red); 103 | } 104 | 105 | .live-game-stats-header-status > .red-team > .team-stats > svg > g { 106 | fill: var(--red); 107 | } 108 | 109 | .live-game-stats-header-status > .blue-team { 110 | flex: 1; 111 | display: flex; 112 | justify-content: space-between; 113 | margin-right: 15rem; 114 | } 115 | 116 | @media(max-width: 1080px) { 117 | .live-game-stats-header-status > .blue-team { 118 | margin-right: 10rem; 119 | } 120 | } 121 | 122 | @media(max-width: 720px) { 123 | .live-game-stats-header-status > .blue-team { 124 | margin-right: 1rem; 125 | } 126 | } 127 | 128 | .live-game-stats-header-status > .red-team { 129 | display: flex; 130 | flex: 1; 131 | flex-direction: row-reverse; 132 | justify-content: space-between; 133 | text-align: right; 134 | } 135 | 136 | .team-stats { 137 | color: var(--text); 138 | display: inline-block; 139 | text-align: center; 140 | width: 24px; 141 | } 142 | 143 | .team-stats > span { 144 | display: flex; 145 | justify-content: center; 146 | } 147 | 148 | /* 149 | Gold bar 150 | */ 151 | 152 | .live-game-stats-header-gold { 153 | margin: 1rem 0; 154 | display: flex; 155 | } 156 | 157 | .live-game-stats-header-gold > .blue-team { 158 | border-bottom: 4px solid var(--blue-twitter); 159 | } 160 | 161 | .live-game-stats-header-gold > .red-team { 162 | border-bottom: 4px solid var(--red); 163 | } 164 | 165 | /* 166 | Status dragons 167 | */ 168 | .live-game-stats-header-dragons{ 169 | display: flex; 170 | min-height: 24px; 171 | } 172 | 173 | .live-game-stats-header-dragons > .blue-team { 174 | flex: 1; 175 | } 176 | 177 | .live-game-stats-header-dragons > .red-team { 178 | text-align: right; 179 | flex: 1; 180 | } 181 | 182 | .blue-team > .dragon { 183 | margin-right: 0.75rem; 184 | } 185 | 186 | .red-team > .dragon { 187 | margin-left: 0.75rem; 188 | } 189 | 190 | /* 191 | PLAYER 192 | */ 193 | 194 | .status-live-game-card { 195 | background: var(--card-color); 196 | box-shadow: 0 1px 3px -1px rgba(0, 0, 0, 0.4); 197 | border-radius: 4px; 198 | } 199 | 200 | .status-live-game-card-content { 201 | margin: 1.5rem 1.5rem; 202 | overflow-x: auto; 203 | } 204 | 205 | .status-live-game-card-table { 206 | width: 100%; 207 | } 208 | 209 | .player-champion-info { 210 | text-align: left; 211 | display: flex; 212 | align-items: center; 213 | } 214 | 215 | .player-champion-info-level{ 216 | position: absolute; 217 | transform: translate(-50%, 100%); 218 | font-size: 12px; 219 | } 220 | 221 | .player-champion-info-name { 222 | margin-left: 0.5rem; 223 | display: flex; 224 | flex-direction: column; 225 | } 226 | 227 | .player-card-player-name { 228 | font-size: 10px; 229 | } 230 | 231 | .player-champion { 232 | width: 32px; 233 | border-radius: 50%; 234 | background: var(--gray-line); 235 | } 236 | 237 | .player-stats{ 238 | color: var(--text) 239 | } 240 | 241 | .table-top-row-champion{ 242 | width: 12rem; 243 | } 244 | 245 | .table-top-row-vida{ 246 | height: 28px; 247 | width: 15rem; 248 | } 249 | 250 | .table-top-row-items { 251 | height: 28px; 252 | width: 12rem; 253 | } 254 | 255 | .table-top-row { 256 | height: 28px; 257 | } 258 | 259 | .player-stats { 260 | /*width: 64px;*/ 261 | height: 28px; 262 | line-height: 28px; 263 | font-size: 12px; 264 | font-weight: 400; 265 | background: var(--gray-line); 266 | text-align: center; 267 | border-radius: 3px; 268 | } 269 | 270 | .player-stats-kda { 271 | width: 28px; 272 | } 273 | 274 | /* 275 | 276 | */ 277 | 278 | .player-stats-items { 279 | height: 28px; 280 | 281 | display: flex; 282 | justify-content: center; 283 | align-items: center; 284 | 285 | background: var(--gray-line); 286 | border-radius: 4px; 287 | } 288 | 289 | .player-stats-item { 290 | flex: 1; 291 | 292 | display: flex; 293 | align-items: center; 294 | justify-content: center; 295 | } 296 | 297 | .player-stats-item > img { 298 | height: 26px; 299 | border-radius: 4px; 300 | } 301 | 302 | /* 303 | 304 | */ 305 | 306 | .player-gold-positive { 307 | color: var(--green-positive); 308 | font-weight: 500; 309 | } 310 | 311 | .player-gold-negative { 312 | color: var(--red); 313 | font-weight: 500; 314 | } 315 | 316 | .loading-game-container { 317 | display: flex; 318 | width: 100%; 319 | height: 60vh; 320 | justify-content: center; 321 | align-items: center; 322 | } 323 | 324 | .loading-game-image{ 325 | width: 20%; 326 | } 327 | 328 | /* 329 | Toast LiveAPIWatcher 330 | */ 331 | 332 | .toast-watcher { 333 | display: flex; 334 | align-items: center; 335 | } 336 | 337 | .toast-image { 338 | display: flex; 339 | 340 | width: 2rem; 341 | height: 2rem; 342 | 343 | margin-right: 1rem; 344 | } -------------------------------------------------------------------------------- /src/components/LiveStatusGameCard/LiveAPIWatcher.tsx: -------------------------------------------------------------------------------- 1 | import './styles/playerStatusStyle.css' 2 | 3 | import {GameMetadata, Participant} from "./types/windowLiveTypes"; 4 | 5 | import React, {useEffect, useState} from "react"; 6 | import {ToastContainer, toast} from 'react-toastify'; 7 | import {Frame as FrameWindow} from "./types/windowLiveTypes"; 8 | 9 | import useSound from "use-sound"; 10 | import {Team} from "./types/detailsPersistentTypes"; 11 | 12 | const firstblood = require("../../assets/audios/firstblood.ogg"); 13 | const kill = require("../../assets/audios/campeao_eliminado.ogg"); 14 | const tower_blue = require("../../assets/audios/azul_torre_destruida.ogg"); 15 | const tower_red = require("../../assets/audios/vermelha_torre_destruida.ogg"); 16 | const dragon_blue = require("../../assets/audios/azul_dragao.ogg"); 17 | const dragon_red = require("../../assets/audios/vermelha_dragao.ogg"); 18 | const baron_blue = require("../../assets/audios/azul_baron.ogg"); 19 | const baron_red = require("../../assets/audios/vermelha_baron.ogg"); 20 | const inib_blue = require("../../assets/audios/azul_inib_destruido.ogg"); 21 | const inib_red = require("../../assets/audios/vermelha_inib_destruido.ogg"); 22 | 23 | type Props = { 24 | lastFrameWindow: FrameWindow, 25 | gameMetadata: GameMetadata, 26 | blueTeam: Team, 27 | redTeam: Team, 28 | } 29 | 30 | type StatusWatcher = { 31 | inhibitors: { 32 | blue: number, 33 | red: number 34 | } 35 | dragons: { 36 | blue: number, 37 | red: number 38 | } 39 | towers: { 40 | blue: number, 41 | red: number 42 | } 43 | barons: { 44 | blue: number, 45 | red: number 46 | } 47 | participants: { 48 | blue: Participant[] 49 | red: Participant[] 50 | } 51 | } 52 | 53 | export function LiveAPIWatcher({ lastFrameWindow, gameMetadata, blueTeam, redTeam } : Props) { 54 | const [status, setStatus] = useState({ 55 | dragons: {blue: lastFrameWindow.blueTeam.dragons.length, red: lastFrameWindow.redTeam.dragons.length}, 56 | inhibitors: {blue: lastFrameWindow.blueTeam.inhibitors, red: lastFrameWindow.redTeam.inhibitors}, 57 | towers: {blue: lastFrameWindow.blueTeam.towers, red: lastFrameWindow.redTeam.towers}, 58 | barons: {blue: lastFrameWindow.blueTeam.barons, red: lastFrameWindow.redTeam.barons}, 59 | participants: {blue: lastFrameWindow.blueTeam.participants, red: lastFrameWindow.redTeam.participants} 60 | }) 61 | 62 | const [firstBloodPlay] = useSound(firstblood); 63 | 64 | useEffect(() => { 65 | const soundData = localStorage.getItem("sound"); 66 | let isMuted = false; 67 | if(soundData) { 68 | if (soundData === "mute") { 69 | isMuted = true; 70 | }else if(soundData === "unmute"){ 71 | isMuted = false; 72 | } 73 | } 74 | 75 | // Topo = prioridade para o som 76 | let isPlaying = isMuted; 77 | 78 | if(status.inhibitors.blue !== lastFrameWindow.blueTeam.inhibitors){ 79 | createToast(true, isPlaying, inib_red.default, "Destruiu um inibidor", blueTeam.image); 80 | isPlaying = true 81 | } 82 | 83 | if(status.inhibitors.red !== lastFrameWindow.redTeam.inhibitors){ 84 | createToast(false, isPlaying, inib_blue.default, "Destruiu um Inibidor", redTeam.image); 85 | isPlaying = true 86 | } 87 | 88 | if(status.barons.blue !== lastFrameWindow.blueTeam.barons){ 89 | createToast(true, isPlaying, baron_blue.default, "Derrotou o barão", blueTeam.image); 90 | isPlaying = true 91 | } 92 | 93 | if(status.barons.red !== lastFrameWindow.redTeam.barons){ 94 | createToast(false, isPlaying, baron_red.default, "Derrotou o barão", redTeam.image); 95 | isPlaying = true 96 | } 97 | 98 | if(status.dragons.blue !== lastFrameWindow.blueTeam.dragons.length){ 99 | createToast(true, isPlaying, dragon_blue.default, "Derrotou o dragão", blueTeam.image); 100 | isPlaying = true 101 | } 102 | 103 | if(status.dragons.red !== lastFrameWindow.redTeam.dragons.length){ 104 | createToast(false, isPlaying, dragon_red.default, "Derrotou o dragão", redTeam.image); 105 | isPlaying = true 106 | } 107 | 108 | if(status.towers.blue !== lastFrameWindow.blueTeam.towers){ 109 | createToast(true, isPlaying, tower_red.default, "Destruiu uma torre", blueTeam.image); 110 | isPlaying = true 111 | } 112 | 113 | if(status.towers.red !== lastFrameWindow.redTeam.towers){ 114 | createToast(false, isPlaying, tower_blue.default, "Destruiu uma torre", redTeam.image); 115 | isPlaying = true 116 | } 117 | 118 | for (let i = 0; i < status.participants.blue.length; i++) { 119 | if(status.participants.blue[i].kills !== lastFrameWindow.blueTeam.participants[i].kills){ 120 | createToast(true, isPlaying, kill.default, "Eliminou um inimigo", `http://ddragon.leagueoflegends.com/cdn/11.4.1/img/champion/${gameMetadata.blueTeamMetadata.participantMetadata[status.participants.blue[i].participantId - 1].championId}.png`) 121 | isPlaying = true 122 | } 123 | } 124 | 125 | for (let i = 0; i < status.participants.red.length; i++) { 126 | if(status.participants.red[i].kills !== lastFrameWindow.redTeam.participants[i].kills){ 127 | createToast(false, isPlaying, kill.default, "Eliminou um inimigo", `http://ddragon.leagueoflegends.com/cdn/11.4.1/img/champion/${gameMetadata.redTeamMetadata.participantMetadata[status.participants.red[i].participantId - 6].championId}.png`) 128 | isPlaying = true 129 | } 130 | } 131 | 132 | setStatus({ 133 | dragons: {blue: lastFrameWindow.blueTeam.dragons.length, red: lastFrameWindow.redTeam.dragons.length}, 134 | inhibitors: {blue: lastFrameWindow.blueTeam.inhibitors, red: lastFrameWindow.redTeam.inhibitors}, 135 | towers: {blue: lastFrameWindow.blueTeam.towers, red: lastFrameWindow.redTeam.towers}, 136 | barons: {blue: lastFrameWindow.blueTeam.barons, red: lastFrameWindow.redTeam.barons}, 137 | participants: {blue: lastFrameWindow.blueTeam.participants, red: lastFrameWindow.redTeam.participants}, 138 | }) 139 | }, [lastFrameWindow.blueTeam.totalKills, lastFrameWindow.blueTeam.dragons.length, lastFrameWindow.blueTeam.inhibitors, lastFrameWindow.redTeam.totalKills, lastFrameWindow.redTeam.dragons.length, lastFrameWindow.redTeam.inhibitors, firstBloodPlay, status.dragons.blue, status.dragons.red, status.barons.blue, status.barons.red, status.inhibitors.blue, status.inhibitors.red, status.towers.blue, status.towers.red, status.participants.blue, status.participants.red, lastFrameWindow.blueTeam.barons, lastFrameWindow.blueTeam.towers, lastFrameWindow.blueTeam.participants, lastFrameWindow.redTeam.barons, lastFrameWindow.redTeam.towers, lastFrameWindow.redTeam.participants, gameMetadata.blueTeamMetadata.participantMetadata, gameMetadata.redTeamMetadata.participantMetadata, blueTeam.image, redTeam.image]); 140 | 141 | return ( 142 | 143 | ); 144 | } 145 | 146 | function createToast(blueTeam: boolean, soundIsPlaying: boolean, sound: string, message: string, image: string) { 147 | if(!soundIsPlaying) { 148 | let audio = new Audio(sound); 149 | audio.load(); 150 | audio.volume = 0.20; 151 | audio.play(); 152 | } 153 | 154 | if(blueTeam){ 155 | toast.info( 156 |
157 |
158 | blue team 159 |
160 |

{message}

161 |
162 | , { 163 | pauseOnFocusLoss: false, 164 | position: toast.POSITION.TOP_LEFT 165 | } 166 | ) 167 | }else{ 168 | toast.error( 169 |
170 | red team 171 |

{message}

172 |
173 | , { 174 | pauseOnFocusLoss: false, 175 | position: toast.POSITION.TOP_RIGHT 176 | } 177 | ) 178 | } 179 | } -------------------------------------------------------------------------------- /src/components/LiveStatusGameCard/PlayersTable.tsx: -------------------------------------------------------------------------------- 1 | import './styles/playerStatusStyle.css' 2 | 3 | import { GameMetadata } from "./types/windowLiveTypes"; 4 | import {GameDetails} from "./types/detailsPersistentTypes"; 5 | 6 | import {MiniHealthBar} from "./MiniHealthBar"; 7 | import React, {useEffect, useState} from "react"; 8 | import {toast} from 'react-toastify'; 9 | import {Frame as FrameDetails} from "./types/detailsLiveTypes"; 10 | import {Frame as FrameWindow, Participant as ParticipantWindow} from "./types/windowLiveTypes"; 11 | 12 | import {ReactComponent as TowerSVG} from '../../assets/images/tower.svg'; 13 | import {ReactComponent as BaronSVG} from '../../assets/images/baron.svg'; 14 | import {ReactComponent as KillSVG} from '../../assets/images/kill.svg'; 15 | import {ReactComponent as GoldSVG} from '../../assets/images/gold.svg'; 16 | import {ReactComponent as InhibitorSVG} from '../../assets/images/inhibitor.svg'; 17 | 18 | import {ReactComponent as OceanDragonSVG} from '../../assets/images/dragon-ocean.svg'; 19 | import {ReactComponent as InfernalDragonSVG} from '../../assets/images/dragon-infernal.svg'; 20 | import {ReactComponent as CloudDragonSVG} from '../../assets/images/dragon-cloud.svg'; 21 | import {ReactComponent as MountainDragonSVG} from '../../assets/images/dragon-mountain.svg'; 22 | import {ReactComponent as ElderDragonSVG} from '../../assets/images/dragon-elder.svg'; 23 | import {ItemsDisplay} from "./ItemsDisplay"; 24 | 25 | import {Helmet} from "react-helmet"; 26 | import {LiveAPIWatcher} from "./LiveAPIWatcher"; 27 | import { CHAMPIONS_URL } from '../../utils/LoLEsportsAPI'; 28 | 29 | type Props = { 30 | lastFrameWindow: FrameWindow, 31 | lastFrameDetails: FrameDetails, 32 | gameMetadata: GameMetadata, 33 | gameDetails: GameDetails, 34 | } 35 | 36 | export function PlayersTable({ lastFrameWindow, lastFrameDetails, gameMetadata, gameDetails } : Props) { 37 | const [gameState, setGameState] = useState(GameState[lastFrameWindow.gameState as keyof typeof GameState]); 38 | 39 | useEffect(() => { 40 | let currentGameState: GameState = GameState[lastFrameWindow.gameState as keyof typeof GameState] 41 | 42 | if(currentGameState !== gameState){ 43 | setGameState(currentGameState); 44 | 45 | toast.info(`Status atual do jogo alterado: ${currentGameState.toUpperCase()}`, { 46 | position: "top-right", 47 | autoClose: 15000, 48 | hideProgressBar: false, 49 | closeOnClick: true, 50 | pauseOnHover: true, 51 | draggable: true, 52 | }); 53 | } 54 | }, [lastFrameWindow.gameState, gameState]); 55 | 56 | let blueTeam = gameDetails.data.event.match.teams[0]; 57 | let redTeam = gameDetails.data.event.match.teams[1]; 58 | 59 | const auxBlueTeam = blueTeam 60 | 61 | /* 62 | As vezes os times continuam errados mesmo apos verificar o ultimo frame, 63 | em ligas como TCL, por isso fazemos essa verificação pelo nome 64 | */ 65 | const summonerName = gameMetadata.blueTeamMetadata.participantMetadata[0].summonerName.split(" "); 66 | 67 | if(redTeam.code.startsWith(summonerName[0])){ // Temos que verificar apenas os primeiros caracteres pois os times academy usam o A, a mais na tag mas não nos nomes 68 | blueTeam = redTeam; 69 | redTeam = auxBlueTeam; 70 | } 71 | 72 | const goldPercentage = getGoldPercentage(lastFrameWindow.blueTeam.totalGold, lastFrameWindow.redTeam.totalGold); 73 | 74 | document.title = `${blueTeam.name} VS ${redTeam.name}`; 75 | 76 | return ( 77 |
78 | 79 | 80 |