ortoni-report 3.0.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.md +29 -6
- package/dist/chunk-ZSIRUQUA.mjs +68 -0
- package/dist/cli/cli.js +21 -25686
- package/dist/cli/cli.mjs +2 -3021
- package/dist/ortoni-report.d.mts +3 -2
- package/dist/ortoni-report.d.ts +3 -2
- package/dist/ortoni-report.js +28 -29304
- package/dist/ortoni-report.mjs +18 -6645
- package/dist/views/head.hbs +1 -1
- package/dist/views/main.hbs +183 -51
- package/dist/views/testIcons.hbs +1 -1
- package/dist/views/userInfo.hbs +25 -6
- package/package.json +11 -10
- package/readme.md +9 -7
- package/dist/chunk-DW4XGLAZ.mjs +0 -22743
- package/dist/views/navbar.hbs +0 -35
package/dist/views/head.hbs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<head>
|
|
2
2
|
<meta charset="UTF-8" />
|
|
3
3
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
4
|
-
<meta name="description" content="Playwright HTML report by LetCode Koushik - V3.0.
|
|
4
|
+
<meta name="description" content="Playwright HTML report by LetCode Koushik - V3.0.1" />
|
|
5
5
|
<title>{{title}}</title>
|
|
6
6
|
<link rel="icon" href="https://raw.githubusercontent.com/ortoniKC/ortoni-report/refs/heads/main/favicon.png"
|
|
7
7
|
type="image/x-icon" />
|
package/dist/views/main.hbs
CHANGED
|
@@ -11,7 +11,11 @@
|
|
|
11
11
|
{{> sidebar}}
|
|
12
12
|
<main class="main-content">
|
|
13
13
|
<div id="dashboard-section" class="content-section">
|
|
14
|
+
{{#if logo}}
|
|
15
|
+
<img src="{{logo}}" alt="{{projectName}}" class="logoimage" />
|
|
16
|
+
{{else}}
|
|
14
17
|
<h1 class="title is-3">Dashboard</h1>
|
|
18
|
+
{{/if}}
|
|
15
19
|
<div class="columns is-multiline has-text-centered">
|
|
16
20
|
{{> summaryCard bg="hsl(var(--bulma-primary-h), var(--bulma-primary-s), var(--bulma-primary-l)) !important" status="all" statusHeader="All Tests" statusCount=totalCount}}
|
|
17
21
|
{{> summaryCard bg="hsl(var(--bulma-success-h), var(--bulma-success-s), var(--bulma-success-l)) !important" status="passed" statusHeader="Passed" statusCount=passCount}}
|
|
@@ -916,19 +920,20 @@
|
|
|
916
920
|
}
|
|
917
921
|
|
|
918
922
|
/**
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
+
* ====================================
|
|
924
|
+
* HISTORY MANAGER
|
|
925
|
+
* ====================================
|
|
926
|
+
*/
|
|
923
927
|
class HistoryManager {
|
|
924
928
|
/**
|
|
925
929
|
* Initialize the history manager
|
|
926
930
|
* @param {Array} testHistory - Test history data
|
|
927
931
|
*/
|
|
928
932
|
constructor(testHistory) {
|
|
929
|
-
this.testHistory = testHistory
|
|
930
|
-
this.testHistoriesMap = null
|
|
931
|
-
this.testHistoryTitle =
|
|
933
|
+
this.testHistory = testHistory
|
|
934
|
+
this.testHistoriesMap = null
|
|
935
|
+
this.testHistoryTitle = ""
|
|
936
|
+
this.activeErrorModal = null
|
|
932
937
|
}
|
|
933
938
|
|
|
934
939
|
/**
|
|
@@ -937,68 +942,188 @@
|
|
|
937
942
|
* @param {string} title - Test title
|
|
938
943
|
*/
|
|
939
944
|
setCurrentHistory(historyMap, title) {
|
|
940
|
-
this.testHistoriesMap = historyMap
|
|
941
|
-
this.testHistoryTitle = title
|
|
945
|
+
this.testHistoriesMap = historyMap
|
|
946
|
+
this.testHistoryTitle = title
|
|
942
947
|
}
|
|
943
948
|
|
|
944
949
|
/**
|
|
945
950
|
* Open history modal
|
|
946
951
|
*/
|
|
947
952
|
openHistory() {
|
|
948
|
-
const historyElement = document.getElementById("historyModal")
|
|
949
|
-
historyElement.classList.add(
|
|
953
|
+
const historyElement = document.getElementById("historyModal")
|
|
954
|
+
historyElement.classList.add("is-active")
|
|
950
955
|
|
|
951
|
-
let historyContent =
|
|
956
|
+
let historyContent = ""
|
|
952
957
|
if (this.testHistoriesMap && this.testHistoriesMap.length > 0) {
|
|
953
|
-
historyContent = this.testHistoriesMap
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
958
|
+
historyContent = this.testHistoriesMap
|
|
959
|
+
.map(
|
|
960
|
+
(h, index) => `
|
|
961
|
+
<tr>
|
|
962
|
+
<td>${h.run_date}</td>
|
|
963
|
+
<td>
|
|
964
|
+
<span class="tag is-${this.getStatusClass(h.status)}">
|
|
965
|
+
${h.status}
|
|
966
|
+
</span>
|
|
967
|
+
</td>
|
|
968
|
+
<td>${h.duration}</td>
|
|
969
|
+
<td>
|
|
970
|
+
${h.error_message
|
|
971
|
+
? `<div class="modal" id="error-${index}">
|
|
959
972
|
<div class="modal-background"></div>
|
|
960
|
-
<div class="modal-
|
|
961
|
-
|
|
973
|
+
<div class="modal-card">
|
|
974
|
+
<header class="modal-card-head">
|
|
975
|
+
<p class="modal-card-title">
|
|
976
|
+
<span class="icon-text">
|
|
977
|
+
<span class="icon">
|
|
978
|
+
<i class="fa-solid fa-exclamation-triangle"></i>
|
|
979
|
+
</span>
|
|
980
|
+
<span>Error Details</span>
|
|
981
|
+
</span>
|
|
982
|
+
</p>
|
|
983
|
+
<button class="delete" aria-label="close" onclick="closeErrorModal('error-${index}')"></button>
|
|
984
|
+
</header>
|
|
985
|
+
<section class="modal-card-body">
|
|
986
|
+
<div class="notification is-danger is-light">
|
|
987
|
+
<pre><code>${h.error_message}</code></pre>
|
|
988
|
+
</div>
|
|
989
|
+
</section>
|
|
990
|
+
<footer class="modal-card-foot">
|
|
991
|
+
<button class="button is-primary" onclick="closeErrorModal('error-${index}')">Close</button>
|
|
992
|
+
</footer>
|
|
962
993
|
</div>
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
994
|
+
</div>
|
|
995
|
+
<button class="button is-small is-link" onclick="showHistoryErrorMessage('error-${index}')">
|
|
996
|
+
<span class="icon">
|
|
997
|
+
<i class="fa-solid fa-exclamation-triangle"></i>
|
|
998
|
+
</span>
|
|
999
|
+
<span>View Error</span>
|
|
1000
|
+
</button>`
|
|
1001
|
+
: '<span class="tag is-success is-light">No Error</span>'
|
|
1002
|
+
}
|
|
1003
|
+
</td>
|
|
1004
|
+
</tr>
|
|
1005
|
+
`,
|
|
1006
|
+
)
|
|
1007
|
+
.join("")
|
|
967
1008
|
} else {
|
|
968
|
-
historyContent =
|
|
1009
|
+
historyContent = `
|
|
1010
|
+
<tr>
|
|
1011
|
+
<td colspan="4">
|
|
1012
|
+
<div class="notification is-info is-light">
|
|
1013
|
+
<p class="has-text-centered">
|
|
1014
|
+
<span class="icon">
|
|
1015
|
+
<i class="fa-solid fa-info-circle"></i>
|
|
1016
|
+
</span>
|
|
1017
|
+
<span>No history available for this test.</span>
|
|
1018
|
+
</p>
|
|
1019
|
+
</div>
|
|
1020
|
+
</td>
|
|
1021
|
+
</tr>
|
|
1022
|
+
`
|
|
969
1023
|
}
|
|
970
1024
|
|
|
971
1025
|
historyElement.innerHTML = `
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1026
|
+
<div class="modal-background"></div>
|
|
1027
|
+
<div class="modal-card">
|
|
1028
|
+
<header class="modal-card-head">
|
|
1029
|
+
<p class="modal-card-title">
|
|
1030
|
+
<span class="icon-text">
|
|
1031
|
+
<span class="icon">
|
|
1032
|
+
<i class="fa-solid fa-history"></i>
|
|
1033
|
+
</span>
|
|
1034
|
+
<span>Test History: ${this.testHistoryTitle}</span>
|
|
1035
|
+
</span>
|
|
1036
|
+
</p>
|
|
1037
|
+
<button class="delete" aria-label="close" onclick="closeHistoryModal()"></button>
|
|
1038
|
+
</header>
|
|
1039
|
+
<section class="modal-card-body">
|
|
1040
|
+
<div class="table-container">
|
|
1041
|
+
<table class="table is-hoverable is-fullwidth">
|
|
1042
|
+
<thead>
|
|
1043
|
+
<tr>
|
|
1044
|
+
<th title="Run Date">
|
|
1045
|
+
<span class="icon-text">
|
|
1046
|
+
<span class="icon">
|
|
1047
|
+
<i class="fa-solid fa-calendar"></i>
|
|
1048
|
+
</span>
|
|
1049
|
+
<span>Run Date</span>
|
|
1050
|
+
</span>
|
|
1051
|
+
</th>
|
|
1052
|
+
<th title="Status">
|
|
1053
|
+
<span class="icon-text">
|
|
1054
|
+
<span class="icon">
|
|
1055
|
+
<i class="fa-solid fa-check-circle"></i>
|
|
1056
|
+
</span>
|
|
1057
|
+
<span>Status</span>
|
|
1058
|
+
</span>
|
|
1059
|
+
</th>
|
|
1060
|
+
<th title="Duration">
|
|
1061
|
+
<span class="icon-text">
|
|
1062
|
+
<span class="icon">
|
|
1063
|
+
<i class="fa-solid fa-clock"></i>
|
|
1064
|
+
</span>
|
|
1065
|
+
<span>Duration</span>
|
|
1066
|
+
</span>
|
|
1067
|
+
</th>
|
|
1068
|
+
<th title="Details">
|
|
1069
|
+
<span class="icon-text">
|
|
1070
|
+
<span class="icon">
|
|
1071
|
+
<i class="fa-solid fa-info-circle"></i>
|
|
1072
|
+
</span>
|
|
1073
|
+
<span>Details</span>
|
|
1074
|
+
</span>
|
|
1075
|
+
</th>
|
|
1076
|
+
</tr>
|
|
1077
|
+
</thead>
|
|
1078
|
+
<tbody>
|
|
1079
|
+
${historyContent}
|
|
1080
|
+
</tbody>
|
|
1081
|
+
</table>
|
|
1082
|
+
</div>
|
|
1083
|
+
</section>
|
|
1084
|
+
<footer class="modal-card-foot">
|
|
1085
|
+
<button class="button is-primary" onclick="closeHistoryModal()">Close</button>
|
|
1086
|
+
</footer>
|
|
1087
|
+
</div>
|
|
1088
|
+
`
|
|
1089
|
+
|
|
1090
|
+
// Add keyboard event listener for ESC key
|
|
1091
|
+
document.addEventListener("keydown", this.handleEscKeyPress)
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* Get status class based on test status
|
|
1096
|
+
* @param {string} status - Test status
|
|
1097
|
+
* @returns {string} CSS class
|
|
1098
|
+
*/
|
|
1099
|
+
getStatusClass(status) {
|
|
1100
|
+
if (status && status.startsWith("passed")) return "success"
|
|
1101
|
+
if (status === "flaky") return "warning"
|
|
1102
|
+
if (status === "failed") return "danger"
|
|
1103
|
+
if (status === "skipped") return "info"
|
|
1104
|
+
if (status && status.includes("retry")) return "link"
|
|
1105
|
+
return "dark"
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
/**
|
|
1109
|
+
* Handle ESC key press to close modal
|
|
1110
|
+
* @param {KeyboardEvent} event - Keyboard event
|
|
1111
|
+
*/
|
|
1112
|
+
handleEscKeyPress = (event) => {
|
|
1113
|
+
if (event.key === "Escape") {
|
|
1114
|
+
this.closeHistoryModal()
|
|
1115
|
+
}
|
|
995
1116
|
}
|
|
996
1117
|
|
|
997
1118
|
/**
|
|
998
1119
|
* Close history modal
|
|
999
1120
|
*/
|
|
1000
1121
|
closeHistoryModal() {
|
|
1001
|
-
document.getElementById("historyModal")
|
|
1122
|
+
const historyElement = document.getElementById("historyModal")
|
|
1123
|
+
historyElement.classList.remove("is-active")
|
|
1124
|
+
|
|
1125
|
+
// Remove keyboard event listener
|
|
1126
|
+
document.removeEventListener("keydown", this.handleEscKeyPress)
|
|
1002
1127
|
}
|
|
1003
1128
|
|
|
1004
1129
|
/**
|
|
@@ -1006,10 +1131,17 @@
|
|
|
1006
1131
|
* @param {string} modalId - Modal ID
|
|
1007
1132
|
*/
|
|
1008
1133
|
showHistoryErrorMessage(modalId) {
|
|
1009
|
-
document.getElementById(modalId)?.classList.add(
|
|
1134
|
+
document.getElementById(modalId)?.classList.add("is-active")
|
|
1010
1135
|
}
|
|
1011
|
-
}
|
|
1012
1136
|
|
|
1137
|
+
/**
|
|
1138
|
+
* Close error modal
|
|
1139
|
+
* @param {string} modalId - Modal ID
|
|
1140
|
+
*/
|
|
1141
|
+
closeErrorModal(modalId) {
|
|
1142
|
+
document.getElementById(modalId)?.classList.remove("is-active")
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1013
1145
|
/**
|
|
1014
1146
|
* ====================================
|
|
1015
1147
|
* MOBILE RESPONSIVE MANAGER
|
package/dist/views/testIcons.hbs
CHANGED
package/dist/views/userInfo.hbs
CHANGED
|
@@ -113,16 +113,27 @@
|
|
|
113
113
|
<script>
|
|
114
114
|
const overallChart = document.getElementById('testChart');
|
|
115
115
|
new Chart(overallChart, {
|
|
116
|
-
type: "
|
|
116
|
+
type: "polarArea",
|
|
117
117
|
data: {
|
|
118
118
|
labels: ['Passed', 'Failed', 'Skipped', 'Flaky'],
|
|
119
119
|
datasets: [{
|
|
120
|
-
data: [{{ passCount }}, {{ failCount }}, {{ skipCount }}, {{ flakyCount }}],
|
|
121
|
-
backgroundColor: ['#28a745', '#ff6685', '#66d1ff', '#ffb70f']}]
|
|
122
|
-
|
|
120
|
+
data: [{{ passCount }}, {{ failCount }}, {{ skipCount }}, {{ flakyCount }}, {{ retryCount }}],
|
|
121
|
+
backgroundColor: ['#28a745', '#ff6685', '#66d1ff', '#ffb70f', '#69748c']}]
|
|
122
|
+
},
|
|
123
123
|
options: {
|
|
124
|
+
scales: {
|
|
125
|
+
r: {
|
|
126
|
+
ticks: {
|
|
127
|
+
display: false
|
|
128
|
+
},
|
|
129
|
+
grid: {
|
|
130
|
+
color: '#e0e0e0'
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
},
|
|
124
134
|
responsive: true,
|
|
125
135
|
maintainAspectRatio: false,
|
|
136
|
+
borderJoinStyle: 'bevel',
|
|
126
137
|
plugins: {
|
|
127
138
|
legend: {
|
|
128
139
|
position: 'bottom',
|
|
@@ -145,7 +156,8 @@
|
|
|
145
156
|
}
|
|
146
157
|
}
|
|
147
158
|
}
|
|
148
|
-
|
|
159
|
+
});
|
|
160
|
+
|
|
149
161
|
const projectChart = document.getElementById('projectChart');
|
|
150
162
|
const projectbarChart = document.getElementById('projectbarChart');
|
|
151
163
|
|
|
@@ -190,6 +202,8 @@
|
|
|
190
202
|
const failedTests = {{ json failedTests }};
|
|
191
203
|
const skippedTests = {{ json skippedTests }};
|
|
192
204
|
const retryTests = {{ json retryTests }};
|
|
205
|
+
const flakyTests = {{ json flakyTests }};
|
|
206
|
+
|
|
193
207
|
new Chart(projectbarChart, {
|
|
194
208
|
type: 'bar',
|
|
195
209
|
data: {
|
|
@@ -216,8 +230,13 @@
|
|
|
216
230
|
},
|
|
217
231
|
{
|
|
218
232
|
label: 'Flaky',
|
|
219
|
-
data:
|
|
233
|
+
data: flakyTests,
|
|
220
234
|
backgroundColor: '#ffb70f',
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
label: 'Retry',
|
|
238
|
+
data: retryTests,
|
|
239
|
+
backgroundColor: '#69748c',
|
|
221
240
|
}
|
|
222
241
|
]
|
|
223
242
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ortoni-report",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"description": "Playwright Report By LetCode with Koushik",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"tsc": "tsc",
|
|
@@ -20,12 +20,13 @@
|
|
|
20
20
|
"url": "git+https://github.com/ortoniKC/ortoni-report"
|
|
21
21
|
},
|
|
22
22
|
"keywords": [
|
|
23
|
-
"playwright
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"ortoni
|
|
23
|
+
"playwright",
|
|
24
|
+
"letcode",
|
|
25
|
+
"koushik",
|
|
26
|
+
"ortoni",
|
|
27
27
|
"ortoni-report",
|
|
28
|
-
"
|
|
28
|
+
"report",
|
|
29
|
+
"reporter"
|
|
29
30
|
],
|
|
30
31
|
"author": "Koushik Chatterjee (LetCode with Koushik)",
|
|
31
32
|
"license": "GPL-3.0-only",
|
|
@@ -38,14 +39,14 @@
|
|
|
38
39
|
"@types/express": "^5.0.0",
|
|
39
40
|
"@types/node": "^22.0.2",
|
|
40
41
|
"@types/sqlite3": "^3.1.11",
|
|
41
|
-
"ansi-to-html": "^0.7.2",
|
|
42
|
-
"commander": "^12.1.0",
|
|
43
|
-
"express": "^4.21.1",
|
|
44
|
-
"handlebars": "^4.7.8",
|
|
45
42
|
"tsup": "^8.4.0",
|
|
46
43
|
"typescript": "^4.9.4"
|
|
47
44
|
},
|
|
48
45
|
"peerDependencies": {
|
|
46
|
+
"ansi-to-html": "^0.7.2",
|
|
47
|
+
"commander": "^12.1.0",
|
|
48
|
+
"express": "^4.21.1",
|
|
49
|
+
"handlebars": "^4.7.8",
|
|
49
50
|
"sqlite": "^5.1.1",
|
|
50
51
|
"sqlite3": "^5.1.7"
|
|
51
52
|
},
|
package/readme.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Ortoni Report
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A comprehensive and visually appealing HTML report generator tailored for Playwright tests. Designed with powerful features and customizable options, Ortoni Report simplifies the process of reviewing and managing test results, making test reporting more intuitive and accessible.
|
|
4
4
|
|
|
5
5
|
### Live Demo: [Ortoni Report](https://ortoni.letcode.in/)
|
|
6
6
|
|
|
7
|
-

|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
@@ -12,7 +12,7 @@ Welcome to **Ortoni Report**, a comprehensive and visually appealing HTML report
|
|
|
12
12
|
|
|
13
13
|
### 1. **Organization & Navigation**
|
|
14
14
|
|
|
15
|
-
- **
|
|
15
|
+
- **Sidebar Navigation**: Offering a more structured and intuitive navigation experience.
|
|
16
16
|
- **Sidebar Sections**:
|
|
17
17
|
- **Dashboard**: View overall test statistics and summaries.
|
|
18
18
|
- **Tests**: Browse detailed test results, including logs, screenshots, and errors.
|
|
@@ -30,7 +30,7 @@ Welcome to **Ortoni Report**, a comprehensive and visually appealing HTML report
|
|
|
30
30
|
### 3. **Visualization & Insights**
|
|
31
31
|
|
|
32
32
|
- **Summary Statistics**: Total tests and distribution of passed, failed, skipped, and flaky tests with success rates.
|
|
33
|
-
- **Chart Visualizations**:
|
|
33
|
+
- **Chart Visualizations**: Plotarea chart for overall status, doghnut for projects and bar charts for project-specific comparisons.
|
|
34
34
|
- **All-New Colorful UI**: A vibrant, redesigned interface with better contrast and readability.
|
|
35
35
|
|
|
36
36
|
### 4. **Customization & Personalization**
|
|
@@ -72,11 +72,12 @@ const reportConfig: OrtoniReportConfig = {
|
|
|
72
72
|
open: process.env.CI ? "never" : "always", // default to never
|
|
73
73
|
folderPath: "report-db",
|
|
74
74
|
filename: "index.html",
|
|
75
|
+
logo:"logo.{png, jpg}",
|
|
75
76
|
title: "Ortoni Test Report",
|
|
76
77
|
showProject: !true,
|
|
77
78
|
projectName: "Ortoni-Report",
|
|
78
79
|
testType: "e2e",
|
|
79
|
-
authorName: "Koushik
|
|
80
|
+
authorName: "Koushik",
|
|
80
81
|
base64Image: false,
|
|
81
82
|
stdIO: false,
|
|
82
83
|
preferredTheme: "light",
|
|
@@ -105,11 +106,12 @@ const reportConfig = {
|
|
|
105
106
|
open: process.env.CI ? "never" : "always",
|
|
106
107
|
folderPath: "report-db",
|
|
107
108
|
filename: "index.html",
|
|
109
|
+
logo:"logo.{png, jpg}",
|
|
108
110
|
title: "Ortoni Test Report",
|
|
109
111
|
showProject: !true,
|
|
110
112
|
projectName: "Ortoni-Report",
|
|
111
113
|
testType: "e2e",
|
|
112
|
-
authorName: "Koushik
|
|
114
|
+
authorName: "Koushik",
|
|
113
115
|
base64Image: false,
|
|
114
116
|
stdIO: false,
|
|
115
117
|
preferredTheme: "light",
|
|
@@ -205,6 +207,6 @@ Thank you for using **Ortoni Report**! I'm committed to providing you with a sup
|
|
|
205
207
|
|
|
206
208
|
---
|
|
207
209
|
**Developer & Designer**
|
|
208
|
-
Koushik Chatterjee
|
|
210
|
+
[Koushik Chatterjee](https://letcode.in/contact)
|
|
209
211
|
|
|
210
212
|
**LetCode with Koushik**
|