├── public ├── image │ ├── cover.png │ ├── favicon.png │ └── loader.gif ├── css │ └── main.css └── js │ └── script.js ├── src ├── controllers │ └── website.controller.ts ├── routes │ └── website.routing.ts ├── server.ts └── webapp.service.ts ├── tsconfig.schemas-tore-schema.json ├── tsconfig.json ├── tslint.json ├── .gitignore ├── tsdoc.json ├── README.md ├── LICENSE ├── package.json └── views └── index.ejs /public/image/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/im-parsa/LiveSoccer/HEAD/public/image/cover.png -------------------------------------------------------------------------------- /public/image/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/im-parsa/LiveSoccer/HEAD/public/image/favicon.png -------------------------------------------------------------------------------- /public/image/loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/im-parsa/LiveSoccer/HEAD/public/image/loader.gif -------------------------------------------------------------------------------- /src/controllers/website.controller.ts: -------------------------------------------------------------------------------- 1 | export const main = async (req: any, res: any) => 2 | { 3 | res.render('index.ejs'); 4 | }; 5 | -------------------------------------------------------------------------------- /tsconfig.schemas-tore-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "incremental": false 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "target": "esnext", 3 | "module": "commonjs", 4 | "compilerOptions": 5 | { 6 | "esModuleInterop": true 7 | }, 8 | "compilerSection": 9 | { 10 | "types": [ "node" ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "trailing-comma": [ false ] 9 | }, 10 | "rulesDirectory": [] 11 | } 12 | -------------------------------------------------------------------------------- /src/routes/website.routing.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { main } from '../controllers/website.controller'; 3 | 4 | const router = express.Router(); 5 | 6 | router.get('/', main); 7 | 8 | 9 | export const websiteRouter = router; 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build and Release Folders 2 | bin-debug/ 3 | bin-release/ 4 | [Oo]bj/ 5 | [Bb]in/ 6 | 7 | # Other files and folders 8 | .settings/ 9 | 10 | # Executables 11 | *.swf 12 | *.air 13 | *.ipa 14 | *.apk 15 | 16 | # Project files, i.e. `.project`, `.actionScriptProperties` and `.flexProperties` 17 | # should NOT be excluded as they contain compiler settings and other important 18 | # information for Eclipse / Flash Builder. 19 | -------------------------------------------------------------------------------- /tsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json", 3 | 4 | "supportForTags": { 5 | "@default": true, 6 | "@allOf": true, 7 | "@see": true, 8 | "@deprecated": true 9 | }, 10 | 11 | "tagDefinitions": [ 12 | { 13 | "tagName": "@default", 14 | "syntaxKind": "block", 15 | "allowMultiple": false 16 | }, 17 | { 18 | "tagName": "@allOf", 19 | "syntaxKind": "block", 20 | "allowMultiple": false 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import moment from "moment"; 2 | import { webapp } from './webapp.service'; 3 | 4 | const port = process.env.PORT || 3535; 5 | 6 | console.log('|' + ' ✅ ' + moment().format(' h:mm:ss a ') + ' - ' + moment().locale("en").format('MMMM Do YYYY')); 7 | 8 | webapp.listen(port, () => 9 | { 10 | console.log('|' + ' ✅ ' + moment().format(' h:mm:ss a ') + ' - ' + `App running on port ${port}...`); 11 | }); 12 | 13 | process.on('unhandledRejection', (err: { name: string, message: string }) => 14 | { 15 | console.log('------------------ ERROR ------------------') 16 | console.log(err.name) 17 | console.log(err.message) 18 | console.log('-------------------------------------------') 19 | }); 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/im-parsa/livesoccer) 2 | 3 | # LiveSoccer (football-standing) 4 | > The Open-Source Live Football Standing 5 | 6 |

7 | GitHub language count 8 | GitHub last commit 9 |

10 | 11 | ## 🤝 Contributing 12 | 13 | 1. [Fork the repository](https://github.com/im-parsa/LiveSoccer/fork) 14 | 2. Clone your fork: `git clone https://github.com/your-username/LiveSoccer.git` 15 | 3. Create your feature branch: `git checkout -b my-new-feature` 16 | 4. Stage changes `git add .` 17 | 5. Commit your changes: `cz` OR `npm run commit` do not use `git commit` 18 | 6. Push to the branch: `git push origin my-new-feature` 19 | 7. Submit a pull request 20 | 21 | 22 | ## 📝 Credits 23 | 24 | [@im-parsa](https://github.com/im-parsa) 25 | -------------------------------------------------------------------------------- /src/webapp.service.ts: -------------------------------------------------------------------------------- 1 | import express, { Application, Response, NextFunction } from 'express'; 2 | import rateLimit from 'express-rate-limit'; 3 | import path from 'path'; 4 | import { websiteRouter } from './routes/website.routing'; 5 | 6 | const app: Application = express(), 7 | limiter = rateLimit( 8 | { 9 | max: 15, 10 | windowMs: 60 * 1000, 11 | message: 'You are in block list IPs' 12 | }); 13 | 14 | app.set('view engine', 'ejs'); 15 | app.set('views', (path.join(__dirname, '../views'))); 16 | 17 | app.use(express.urlencoded({ extended: false })); 18 | app.use(express.json()); 19 | 20 | app.use(express.static(path.join(__dirname, '../public'))); 21 | 22 | app.use('/user', limiter); 23 | 24 | app.use(express.urlencoded({ extended: true, limit: '10kb' })); 25 | app.use(express.json({ limit: '10kb' })); 26 | 27 | app.use((req: any, res: Response, next: NextFunction) => 28 | { 29 | req.requestTime = new Date().toISOString(); 30 | next(); 31 | }); 32 | 33 | app.use('/', websiteRouter); 34 | 35 | export const webapp = app; 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Parsa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "livesoccer", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "./src/server.ts", 6 | "scripts": { 7 | "start": "ts-node ./src/server.ts" 8 | }, 9 | "engines": { 10 | "node": "16.0.0" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/im-parsa/livesoccer.git" 15 | }, 16 | "keywords": [], 17 | "author": "", 18 | "license": "ISC", 19 | "bugs": { 20 | "url": "https://github.com/im-parsa/livesoccer/issues" 21 | }, 22 | "homepage": "https://github.com/im-parsa/livesoccer#readme", 23 | "dependencies": { 24 | "@types/ejs": "^3.1.0", 25 | "@types/express": "^4.17.13", 26 | "@types/express-rate-limit": "^5.1.3", 27 | "@types/moment": "^2.13.0", 28 | "@types/node-fetch": "^3.0.3", 29 | "dotenv": "^10.0.0", 30 | "ejs": "^3.1.6", 31 | "express": "^4.17.1", 32 | "express-rate-limit": "^5.4.1", 33 | "moment": "^2.29.1", 34 | "node-fetch": "^3.0.0", 35 | "nodemon": "^2.0.13", 36 | "npm": "^8.0.0", 37 | "path": "^0.12.7", 38 | "ts-node": "^10.2.1", 39 | "typescript": "^4.4.3" 40 | }, 41 | "devDependencies": { 42 | "@types/express": "^4.17.13", 43 | "@types/express-rate-limit": "^5.1.3", 44 | "@types/moment": "^2.13.0", 45 | "@types/node": "^16.10.3", 46 | "@types/node-fetch": "^3.0.3", 47 | "nodemon": "^2.0.13", 48 | "ts-node": "^10.2.1", 49 | "typescript": "^4.4.3" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | LiveSoccer - The Open-Source Live Football Standing 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |

42 | 43 |
44 | 45 | 46 | 47 | 48 | 49 |
50 |
51 | 52 |
53 | 54 |

55 |
56 | 57 |
58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 |
#TeamPWDL+-+/-P
79 | 80 |
81 | 82 |
83 | loading... 84 |
85 | 86 |
87 | 88 | 93 |
94 | 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /public/css/main.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Quicksand:wght@300;400;500;600;700&display=swap"); 2 | 3 | html[theme="light"] 4 | { 5 | --base-textColor: rgb(45, 51, 59); 6 | --base-bold-textColor: rgb(40, 45, 52); 7 | --base-bolder-textColor: rgb(34, 39, 46); 8 | 9 | --base: white; 10 | --base-dark: #eeeeee; 11 | --base-darker: #e1e1e1; 12 | } 13 | 14 | html[theme="dark"] 15 | { 16 | --base-textColor: #a9a9a9; 17 | --base-bold-textColor: #cecece; 18 | --base-bolder-textColor: #fff; 19 | 20 | --base: rgb(45, 51, 59); 21 | --base-dark: rgb(32, 36, 42); 22 | --base-darker: rgb(21, 24, 30); 23 | } 24 | 25 | a 26 | { 27 | color: currentColor; 28 | text-decoration: none; 29 | } 30 | 31 | a:hover 32 | { 33 | color: var(--base-bolder-textColor); 34 | } 35 | 36 | * 37 | { 38 | margin: 0; 39 | padding: 0; 40 | box-sizing: border-box; 41 | transition: all .2s; 42 | } 43 | body 44 | { 45 | padding: 1em; 46 | font-family: Quicksand, sans-serif; 47 | cursor: default; 48 | user-select: none; 49 | background: var(--base-darker); 50 | color: var(--base-bold-textColor); 51 | } 52 | 53 | #themeSwitch 54 | { 55 | display: flex; 56 | align-items: center; 57 | justify-content: center; 58 | position: absolute; 59 | top: 10px; 60 | left: 10px; 61 | background: var(--base); 62 | width: 50px; 63 | height: 50px; 64 | border-radius: 0.25em; 65 | font-size: 25px; 66 | cursor: pointer; 67 | } 68 | #themeSwitch:hover 69 | { 70 | background: var(--base-dark); 71 | } 72 | 73 | #github 74 | { 75 | display: flex; 76 | align-items: center; 77 | justify-content: center; 78 | position: absolute; 79 | top: 10px; 80 | right: 10px; 81 | background: var(--base); 82 | width: 50px; 83 | height: 50px; 84 | border-radius: 0.25em; 85 | font-size: 25px; 86 | cursor: pointer; 87 | } 88 | #github:hover 89 | { 90 | background: var(--base-dark); 91 | } 92 | 93 | .copyright 94 | { 95 | display: flex; 96 | justify-content: center; 97 | align-items: center; 98 | margin: 50px auto auto; 99 | } 100 | 101 | .container 102 | { 103 | max-width: 1200px; 104 | margin: 0 auto; 105 | display: flex; 106 | flex-direction: column; 107 | align-items: center; 108 | gap: 0.5em; 109 | } 110 | .comp-select 111 | { 112 | width: 80%; 113 | display: flex; 114 | justify-content: center; 115 | flex-wrap: wrap; 116 | gap: 0.5em; 117 | margin-bottom: 1em; 118 | } 119 | 120 | .comp-select .comp input 121 | { 122 | display: none; 123 | } 124 | 125 | .comp-select .comp label 126 | { 127 | padding: 0.25em; 128 | display: flex; 129 | border-radius: 0.25em; 130 | background-color: var(--base); 131 | opacity: 0.5; 132 | cursor: pointer; 133 | } 134 | 135 | .comp-select .comp label:hover, 136 | .comp-select .comp input:checked + label 137 | { 138 | opacity: 1; 139 | } 140 | 141 | .comp-select .comp label img 142 | { 143 | width: 2.2em; 144 | height: 2.2em; 145 | object-fit: contain; 146 | } 147 | 148 | .comp-title 149 | { 150 | display: flex; 151 | gap: 0.5em; 152 | align-items: center; 153 | color: var(--base-bolder-textColor); 154 | margin-bottom: 10px; 155 | } 156 | .comp-title .comp-flag 157 | { 158 | width: 1.75em; 159 | height: 1.75em; 160 | object-fit: cover; 161 | border-radius: 0.25em; 162 | } 163 | 164 | .comp-loading 165 | { 166 | display: none; 167 | } 168 | .comp-loading.load 169 | { 170 | display: block; 171 | } 172 | .comp-loading img 173 | { 174 | width: 3em; 175 | } 176 | 177 | .comp-table-container 178 | { 179 | padding: 0.5em; 180 | border: 0.1em solid var(--base-dark); 181 | background-color:var(--base); 182 | border-radius: 0.5em; 183 | } 184 | .comp-table 185 | { 186 | border-collapse: collapse; 187 | width: 100%; 188 | } 189 | .comp-table tbody tr:hover 190 | { 191 | background-color: var(--base-dark); 192 | } 193 | .comp-table tbody tr:not(:last-child) 194 | { 195 | border-bottom: 0.1em solid var(--base-dark); 196 | } 197 | .comp-table th, 198 | .comp-table td 199 | { 200 | text-align: center; 201 | padding: 0.3em 0.75em; 202 | } 203 | .comp-table .team 204 | { 205 | text-align: left; 206 | } 207 | .comp-table .team .name-short 208 | { 209 | display: none; 210 | } 211 | .comp-table .team .name-full 212 | { 213 | max-width: 20ch; 214 | white-space: nowrap; 215 | overflow: hidden; 216 | text-overflow: ellipsis; 217 | } 218 | 219 | .comp-table-body td 220 | { 221 | font-weight: 500; 222 | } 223 | 224 | .comp-table-body td.team 225 | { 226 | display: flex; 227 | gap: 0.5em; 228 | align-items: center; 229 | } 230 | .comp-table-body td.team img 231 | { 232 | width: 1.5em; 233 | height: 1.5em; 234 | object-fit: contain; 235 | } 236 | 237 | .comp-table-container 238 | { 239 | width: 100%; 240 | font-size: 26px; 241 | } 242 | .comp-title 243 | { 244 | font-size: 40px; 245 | margin-bottom: 20px; 246 | } 247 | .comp-select .comp label img 248 | { 249 | width: 50px; 250 | height: 50px; 251 | } 252 | .name-short 253 | { 254 | display: block !important; 255 | } 256 | .name-full 257 | { 258 | display: none; 259 | } 260 | 261 | @media (max-width: 650px) 262 | { 263 | .comp-table-container 264 | { 265 | font-size: 15px; 266 | } 267 | .comp-table th, 268 | .comp-table td 269 | { 270 | text-align: center; 271 | padding: 0.3em 0.5em; 272 | } 273 | .comp-title 274 | { 275 | text-align: center; 276 | display: contents; 277 | font-size: 20px; 278 | } 279 | .comp-select 280 | { 281 | width: 72%; 282 | } 283 | .comp-table .for, 284 | .comp-table .against, 285 | .comp-table .won, 286 | .comp-table .drawn, 287 | .comp-table .lost 288 | { 289 | display: none; 290 | } 291 | 292 | .copyright h2 293 | { 294 | font-size: 20px; 295 | } 296 | 297 | } 298 | -------------------------------------------------------------------------------- /public/js/script.js: -------------------------------------------------------------------------------- 1 | const competitionTableUrl = 2 | (phaseId, langId) => 3 | `https://sportapi.widgets.sports.gracenote.com/football/gettable/phaseid/${phaseId}/languagecode/${langId}.json?c=115&module=football&type=standing`, 4 | 5 | compFlag = (code) => `https://alexsobolenko.github.io/flag-icons/flags/4x3/${code}.svg`, 6 | teamLogo = (id) => `https://images.sports.gracenote.com/images/lib/basic/sport/football/club/logo/small/${id}.png`, 7 | compLogo = (id) => `https://images.sports.gracenote.com/images/lib/basic/sport/football/competition/logo/small/${id}.png`, 8 | 9 | compSelectEl = document.querySelector('.comp-select'), 10 | compNameEl = document.querySelector('.comp-name'), 11 | themeSwitch = document.querySelector('#themeSwitch'), 12 | themeSwitchIcon = document.querySelector('.themeSwitchIcon'), 13 | compFlagEl = document.querySelector('.comp-flag'), 14 | compLoadingEl = document.querySelector('.comp-loading'), 15 | compTableBodyEl = document.querySelector('.comp-table-body'), 16 | competitions = 17 | [ 18 | {compId: 52, phaseId: 167860, langId: 1, country: 'eng', name: 'Premier League' }, 19 | {compId: 67, phaseId: 168197, langId: 1, country: 'esp', name: 'LaLiga' }, 20 | {compId: 56, phaseId: 167919, langId: 1, country: 'deu', name: 'Bundesliga' }, 21 | {compId: 53, phaseId: 168951, langId: 1, country: 'ita', name: 'Serie A' }, 22 | {compId: 54, phaseId: 167978, langId: 1, country: 'fra', name: 'Ligue 1' }, 23 | {compId: 2, phaseId: 167633, langId: 1, country: 'nld', name: 'Eredivisie' }, 24 | {compId: 48, phaseId: 167628, langId: 1, country: 'bel', name: 'Pro League' }, 25 | {compId: 57, phaseId: 168944, langId: 1, country: 'tur', name: 'Süper Lig'}, 26 | {compId: 69, phaseId: 168416, langId: 1, country: 'prt', name: 'Liga Portugal'}, 27 | {compId: 79, phaseId: 165923, langId: 2, country: 'swe', name: 'Allsvenskan'}, 28 | {compId: 199, phaseId: 166840, langId: 2, country: 'bra', name: 'Série A'}, 29 | {compId: 197, phaseId: 168368, langId: 2, country: 'arg', name: 'Liga Profesional'}, 30 | {compId: 978, phaseId: 167645, langId: 2, country: 'can', name: 'Premier League'}, 31 | {compId: 105, phaseId: 165917, langId: 2, country: 'jpn', name: 'J1 League'}, 32 | ], 33 | 34 | loadTableData = async (comp) => 35 | { 36 | compLoading(true) 37 | compTableBodyEl.innerHTML = ''; 38 | compNameEl.innerText = comp.name 39 | compFlagEl.src = compFlag(comp.country); 40 | 41 | const response = await fetch(competitionTableUrl(comp.phaseId, comp.langId)); 42 | const data = await response.json(); 43 | 44 | data.forEach((team) => 45 | { 46 | compTableBodyEl.append(createTeamTableRow(team)) 47 | }) 48 | compLoading(false) 49 | }, 50 | 51 | createTeamTableRow = (team) => 52 | { 53 | const tableRowEl = document.createElement('tr'); 54 | tableRowEl.classList.add('comp-table-team-row'); 55 | tableRowEl.innerHTML = 56 | ` 57 | ${team.c_Rank} 58 | 59 | ${team.c_Team} 60 | ${team.c_Team} 61 | ${team.c_TeamShort} 62 | 63 | ${team.n_Matches} 64 | ${team.n_MatchesWon} 65 | ${team.n_MatchesDrawn} 66 | ${team.n_MatchesLost} 67 | ${team.n_GoalsFor} 68 | ${team.n_GoalsAgainst} 69 | ${team.n_GoalsFor - team.n_GoalsAgainst} 70 | ${team.n_Points} 71 | ` 72 | return tableRowEl; 73 | }, 74 | 75 | compLoading = (load) => 76 | { 77 | load? compLoadingEl.classList.add('load') : compLoadingEl.classList.remove('load'); 78 | }, 79 | 80 | themeSwitchF = () => 81 | { 82 | let htmlElement = document.querySelector(`html`), 83 | theme = localStorage.getItem('theme') || 'light'; 84 | 85 | if (theme === 'dark') 86 | { 87 | themeSwitchIcon.classList.add('fa-moon'); 88 | themeSwitchIcon.classList.remove('fa-sun'); 89 | 90 | htmlElement.setAttribute('theme', 'dark'); 91 | } 92 | else if (theme === 'light') 93 | { 94 | themeSwitchIcon.classList.add('fa-sun'); 95 | themeSwitchIcon.classList.remove('fa-moon'); 96 | 97 | htmlElement.setAttribute('theme', 'light'); 98 | } 99 | } 100 | 101 | loadTableData(competitions[0]).then(() => console.log('loadTableData function loaded')); 102 | themeSwitchF(); 103 | 104 | competitions.forEach((comp,index)=> 105 | { 106 | const compBtnEl = document.createElement('div'); 107 | compBtnEl.classList.add('comp'); 108 | 109 | const compRadioEl = document.createElement('input'); 110 | compRadioEl.type = 'radio'; 111 | compRadioEl.name='comp'; 112 | compRadioEl.id = comp.compId; 113 | compRadioEl.checked = index === 0; 114 | 115 | const compLabelEl = document.createElement('label'); 116 | compLabelEl.setAttribute('for',comp.compId); 117 | 118 | const compImgEl = document.createElement('img'); 119 | compImgEl.src = compLogo(comp.compId); 120 | compLabelEl.append(compImgEl); 121 | compBtnEl.append(compRadioEl, compLabelEl) 122 | compLabelEl.addEventListener('click', ()=> 123 | { 124 | loadTableData(comp).then(() => console.log('loadTableData function loaded')) 125 | }) 126 | compSelectEl.append(compBtnEl) 127 | }) 128 | 129 | themeSwitch.addEventListener('click', () => 130 | { 131 | let htmlElement = document.querySelector(`html`), 132 | theme = localStorage.getItem('theme') || 'light'; 133 | 134 | if (theme === 'dark') 135 | { 136 | themeSwitchIcon.classList.add('fa-sun'); 137 | themeSwitchIcon.classList.remove('fa-moon'); 138 | 139 | localStorage.setItem('theme', 'light'); 140 | htmlElement.setAttribute('theme', 'light'); 141 | } 142 | else if (theme === 'light') 143 | { 144 | themeSwitchIcon.classList.add('fa-moon'); 145 | themeSwitchIcon.classList.remove('fa-sun'); 146 | localStorage.setItem('theme', 'dark'); 147 | htmlElement.setAttribute('theme', 'dark'); 148 | } 149 | } 150 | ) 151 | --------------------------------------------------------------------------------