├── 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 | [](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 |
8 |
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 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 | loading...
84 |
85 |

86 |
87 |
88 |
89 |
90 | LiveSoccer a product of im-parsa.
91 |
92 |
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 |
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 |
--------------------------------------------------------------------------------