@xcpcio/board-app 0.84.0 → 0.85.4
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/dist/404.html +1 -1
- package/dist/assets/Balloon-BUgwq0Kx.js +1 -0
- package/dist/assets/Board-BfbUwmvq.css +1 -0
- package/dist/assets/Board-D7hrVoFu.js +190 -0
- package/dist/assets/ContestStateBadge-DgwznQrX.css +1 -0
- package/dist/assets/ContestStateBadge-Dt7AgnYd.js +1 -0
- package/dist/assets/Countdown-B97p6HbD.js +1 -0
- package/dist/assets/DataSourceInput-B67_jwrR.js +1 -0
- package/dist/assets/NavBar-CnD7iUz_.js +1 -0
- package/dist/assets/NavBar-O1kqnfE4.css +1 -0
- package/dist/assets/Resolver-CRBstFE3.css +1 -0
- package/dist/assets/Resolver-DQ3kkz9L.js +1 -0
- package/dist/assets/RightArrowIcon-F6HBDgDW.js +1 -0
- package/dist/assets/TheInput-D67yPyFW.js +1 -0
- package/dist/assets/Tooltip-BR6I8-SM.js +27 -0
- package/dist/assets/Tooltip-C9jfn33Z.css +1 -0
- package/dist/assets/_..-D9qurBBH.css +1 -0
- package/dist/assets/_...all_-BDXlESlP.js +1 -0
- package/dist/assets/_...all_-Cek5Lnmd.js +6 -0
- package/dist/assets/_plugin-vue_export-helper-BDNMzG2s.js +1 -0
- package/dist/assets/balloon-DpIcef0j.js +1 -0
- package/dist/assets/board-KpBI6_ac.js +1 -0
- package/dist/assets/board-layout-DD2-Z-AS.js +1 -0
- package/dist/assets/constant-Dl-MmgU0.js +1 -0
- package/dist/assets/countdown-bqehvGJt.js +1 -0
- package/dist/assets/default-CSWSpjqo.js +1 -0
- package/dist/assets/headless-BSf0YrvV.js +1 -0
- package/dist/assets/index-4yhXaony.css +1 -0
- package/dist/assets/index-ClRmeGtV.js +12 -0
- package/dist/assets/index-layout-Dxv-LU9c.js +1 -0
- package/dist/assets/pages-DZG1eNR4.css +1 -0
- package/dist/assets/{index-D0V3pqsw.js → pages-DyUshEIY.js} +2 -2
- package/dist/assets/pagination-2onPPYtS.js +21 -0
- package/dist/assets/person-DrN1htE6.js +1 -0
- package/dist/assets/query-XskWvdZB.js +20 -0
- package/dist/assets/rank-Bk_Yo4wU.js +1 -0
- package/dist/assets/rating-DSZOgEeb.js +1 -0
- package/dist/assets/rating-DiCQYzia.css +1 -0
- package/dist/assets/resolver-DOjgimki.js +1 -0
- package/dist/assets/route-block-ChiJXsfH.js +1 -0
- package/dist/assets/utils-Cfm-h8Z9.js +1 -0
- package/dist/assets/virtual_intlify-i18n-0-BmghNkij.js +1 -0
- package/dist/assets/virtual_intlify-i18n-1-DzpDgiG5.js +1 -0
- package/dist/assets/vue.runtime.esm-bundler-DTMOfGWR.js +4 -0
- package/dist/index.html +1 -1
- package/package.json +45 -45
- package/src/auto-imports.d.ts +6 -4
- package/src/components/board/Board.vue +3 -0
- package/src/components/board/Export.vue +5 -1
- package/src/components/board/Progress.vue +1 -0
- package/src/components/board/SeatMapView.vue +6 -3
- package/src/components/board/SubmissionsTable.vue +1 -2
- package/src/components/board/SubmitHeatMap.less +11 -3
- package/src/components/board/SubmitHeatMap.vue +56 -86
- package/src/components/board/TeamCard.vue +22 -14
- package/src/components/board/TeamOverview.vue +1 -0
- package/src/components/board/TeamUI.vue +16 -12
- package/src/components/board/Utility.vue +5 -3
- package/src/components/flowbite/Tooltip.vue +79 -13
- package/src/components.d.ts +0 -1
- package/src/composables/constant.ts +5 -15
- package/src/composables/runtimeConfig.ts +21 -0
- package/src/composables/useQueryBoardData.ts +3 -2
- package/src/main.ts +2 -1
- package/src/modules/i18n.ts +2 -1
- package/src/pages/[...all].vue +6 -2
- package/src/pages/index.vue +2 -2
- package/src/shims.d.ts +15 -8
- package/src/types.ts +16 -0
- package/vite.config.ts +4 -0
- package/dist/assets/Balloon.vue_vue_type_script_setup_true_lang-DHemO8cG.js +0 -1
- package/dist/assets/Board-C6kTRGEP.css +0 -1
- package/dist/assets/Board-DxiquVyO.js +0 -194
- package/dist/assets/ContestStateBadge-Bgdroopp.js +0 -1
- package/dist/assets/ContestStateBadge-D6uf8wH-.css +0 -1
- package/dist/assets/Countdown-ovUCuJ35.js +0 -1
- package/dist/assets/DataSourceInput.vue_vue_type_script_setup_true_lang-DgkCadgz.js +0 -1
- package/dist/assets/NavBar-DfKQ1ytp.js +0 -1
- package/dist/assets/NavBar-aT2xEvYz.css +0 -1
- package/dist/assets/Resolver-BQcsyOHn.js +0 -1
- package/dist/assets/Resolver-DV9FleNP.css +0 -1
- package/dist/assets/RightArrowIcon-BKvYPM1O.js +0 -1
- package/dist/assets/TheInput.vue_vue_type_script_setup_true_lang-OA6Dkh4w.js +0 -1
- package/dist/assets/Tooltip-DzyPDwbo.css +0 -1
- package/dist/assets/Tooltip.vue_vue_type_script_setup_true_lang-DWNdRDhE.js +0 -27
- package/dist/assets/_..-5I0UWqRG.css +0 -1
- package/dist/assets/_...all_-BtYYdFga.js +0 -1
- package/dist/assets/_...all_-XVDapPdm.js +0 -6
- package/dist/assets/_plugin-vue_export-helper-DlAUqK2U.js +0 -1
- package/dist/assets/board-layout-LZdDEkNj.js +0 -1
- package/dist/assets/constant-CYOTMbJW.js +0 -1
- package/dist/assets/dayjs-Ccpn6fIh.js +0 -1
- package/dist/assets/default-CRpkLq3n.js +0 -1
- package/dist/assets/en-DGp_RGnz.js +0 -1
- package/dist/assets/headless-l1XzJj3O.js +0 -1
- package/dist/assets/index-B34Y_Z57.js +0 -15
- package/dist/assets/index-B3NvUps3.js +0 -1
- package/dist/assets/index-B_303hcz.js +0 -1
- package/dist/assets/index-Bojw84ll.css +0 -1
- package/dist/assets/index-BuFplOHa.js +0 -1
- package/dist/assets/index-CeJCbVnH.js +0 -1
- package/dist/assets/index-CgXDtxXW.css +0 -1
- package/dist/assets/index-D-msPqye.css +0 -1
- package/dist/assets/index-DOf6OpNV.js +0 -1
- package/dist/assets/index-layout-BDulxfyN.js +0 -1
- package/dist/assets/pagination-DkTeb3KN.js +0 -3
- package/dist/assets/person-gKEre052.js +0 -1
- package/dist/assets/rank-DJDWVDFD.js +0 -1
- package/dist/assets/route-block-B_A1xBdJ.js +0 -1
- package/dist/assets/use-vmodel-Bem9C7yk.js +0 -20
- package/dist/assets/zh-CN-BUehjmhL.js +0 -1
- package/src/components/board/HeatMapTooltip.less +0 -115
- package/src/components/board/HeatMapTooltip.vue +0 -68
|
@@ -85,7 +85,7 @@ function getNormalizedRow(row: Array<string | null>, maxCols: number): Array<str
|
|
|
85
85
|
if (row.length >= maxCols) {
|
|
86
86
|
return row;
|
|
87
87
|
}
|
|
88
|
-
return [...row, ...Array.from({ length: maxCols - row.length }
|
|
88
|
+
return [...row, ...Array.from({ length: maxCols - row.length }).fill(null) as Array<string | null>];
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
// Get grid style for a section
|
|
@@ -165,7 +165,7 @@ function getRowLabelStyle() {
|
|
|
165
165
|
<div :style="getSectionGridStyle(section)">
|
|
166
166
|
<template v-for="(row, rowIdx) in section.grid" :key="rowIdx">
|
|
167
167
|
<template v-for="(seatId, colIdx) in getNormalizedRow(row, getMaxCols(section))" :key="`${rowIdx}-${colIdx}`">
|
|
168
|
-
<
|
|
168
|
+
<Tooltip v-if="seatId" placement="bottom">
|
|
169
169
|
<div
|
|
170
170
|
class="seat"
|
|
171
171
|
:class="{
|
|
@@ -181,7 +181,10 @@ function getRowLabelStyle() {
|
|
|
181
181
|
>
|
|
182
182
|
{{ seatId }}
|
|
183
183
|
</div>
|
|
184
|
-
|
|
184
|
+
<template #popper>
|
|
185
|
+
{{ getTooltipContent(seatId) }}
|
|
186
|
+
</template>
|
|
187
|
+
</Tooltip>
|
|
185
188
|
<div
|
|
186
189
|
v-else
|
|
187
190
|
:style="getSeatStyle(section)"
|
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
@correct-colors: #9be9a8, #40c463, #30a14e, #216e39;
|
|
2
2
|
@incorrect-colors: #ff8c8c, #ff4d4f, #ff1a1a, #d40d0d;
|
|
3
|
+
@correct-colors-dark: #216e39, #30a14e, #40c463, #9be9a8;
|
|
4
|
+
@incorrect-colors-dark: #d40d0d, #ff1a1a, #ff4d4f, #ff8c8c;
|
|
3
5
|
|
|
4
|
-
.generate-heat-rules(@class, @colors) {
|
|
6
|
+
.generate-heat-rules(@class, @colors, @dark-colors) {
|
|
5
7
|
each(@colors, {
|
|
6
8
|
.@{class}[data-level="@{index}"] {
|
|
7
9
|
background-color: @value;
|
|
8
10
|
}
|
|
9
11
|
});
|
|
12
|
+
|
|
13
|
+
each(@dark-colors, {
|
|
14
|
+
.dark .@{class}[data-level="@{index}"] {
|
|
15
|
+
background-color: @value;
|
|
16
|
+
}
|
|
17
|
+
});
|
|
10
18
|
}
|
|
11
19
|
|
|
12
|
-
.generate-heat-rules(correct-heat, @correct-colors);
|
|
13
|
-
.generate-heat-rules(incorrect-heat, @incorrect-colors);
|
|
20
|
+
.generate-heat-rules(correct-heat, @correct-colors, @correct-colors-dark);
|
|
21
|
+
.generate-heat-rules(incorrect-heat, @incorrect-colors, @incorrect-colors-dark);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type { Rank } from "@xcpcio/core";
|
|
2
|
+
import type { Rank, Submission } from "@xcpcio/core";
|
|
3
3
|
import { isAccepted, isRejected } from "@xcpcio/core";
|
|
4
|
+
import "./SubmitHeatMap.less";
|
|
4
5
|
|
|
5
6
|
const props = defineProps<{
|
|
6
7
|
rank: Rank;
|
|
@@ -9,23 +10,32 @@ const props = defineProps<{
|
|
|
9
10
|
const { t } = useI18n();
|
|
10
11
|
|
|
11
12
|
const rank = computed(() => props.rank);
|
|
13
|
+
const submissions = computed(() => rank.value.getSubmissions());
|
|
12
14
|
|
|
13
15
|
const mapTimeDiffLength = 20;
|
|
14
|
-
const startTime = rank.value.contest.getStartTime();
|
|
15
|
-
const endTime = rank.value.contest.getEndTime();
|
|
16
|
-
const totalDuration = endTime.diff(startTime, "minute");
|
|
17
|
-
const intervalSize = totalDuration / mapTimeDiffLength;
|
|
18
16
|
|
|
19
|
-
const
|
|
17
|
+
const intervalSize = computed(() => {
|
|
18
|
+
const startTime = rank.value.contest.getStartTime();
|
|
19
|
+
const endTime = rank.value.contest.getEndTime();
|
|
20
|
+
const totalDuration = endTime.diff(startTime, "minute");
|
|
21
|
+
|
|
22
|
+
if (!Number.isFinite(totalDuration) || totalDuration <= 0) {
|
|
23
|
+
return 1;
|
|
24
|
+
}
|
|
20
25
|
|
|
21
|
-
|
|
22
|
-
const start = index * intervalSize;
|
|
23
|
-
const end = (index + 1) * intervalSize;
|
|
24
|
-
return `Between ${start} and ${end} minutes: `;
|
|
26
|
+
return totalDuration / mapTimeDiffLength;
|
|
25
27
|
});
|
|
26
28
|
|
|
29
|
+
const intervalDescriptions = computed(() =>
|
|
30
|
+
Array.from({ length: mapTimeDiffLength }, (_, index) => {
|
|
31
|
+
const start = index * intervalSize.value;
|
|
32
|
+
const end = (index + 1) * intervalSize.value;
|
|
33
|
+
return `Between ${start} and ${end} minutes: `;
|
|
34
|
+
}),
|
|
35
|
+
);
|
|
36
|
+
|
|
27
37
|
const submissionsByProblemId = computed(() => {
|
|
28
|
-
const result = new Map();
|
|
38
|
+
const result = new Map<string, { correct: Submission[]; incorrect: Submission[] }>();
|
|
29
39
|
|
|
30
40
|
rank.value.contest.problems.forEach((p) => {
|
|
31
41
|
result.set(p.id, { correct: [], incorrect: [] });
|
|
@@ -47,64 +57,30 @@ const submissionsByProblemId = computed(() => {
|
|
|
47
57
|
return result;
|
|
48
58
|
});
|
|
49
59
|
|
|
50
|
-
function generateHeatMap(submissions:
|
|
60
|
+
function generateHeatMap(submissions: Submission[], type: "correct" | "incorrect") {
|
|
51
61
|
const counts = Array.from<number>({ length: mapTimeDiffLength }).fill(0);
|
|
62
|
+
const size = intervalSize.value > 0 ? intervalSize.value : 1;
|
|
63
|
+
const descriptions = intervalDescriptions.value;
|
|
52
64
|
|
|
53
65
|
submissions.forEach((s) => {
|
|
54
|
-
const index = Math.min(Math.floor(s.timestampToMinute /
|
|
66
|
+
const index = Math.min(Math.floor(s.timestampToMinute / size), mapTimeDiffLength - 1);
|
|
55
67
|
counts[index]++;
|
|
56
68
|
});
|
|
57
69
|
|
|
58
70
|
return counts.map((count, i) => ({
|
|
59
71
|
count,
|
|
60
72
|
level: 0,
|
|
61
|
-
description: `${
|
|
73
|
+
description: `${descriptions[i]}${count} ${type} submissions`,
|
|
62
74
|
}));
|
|
63
75
|
}
|
|
64
76
|
|
|
65
|
-
function
|
|
66
|
-
if (
|
|
77
|
+
function getHeatLevel(value: number, maxValue: number): number {
|
|
78
|
+
if (value <= 0 || maxValue <= 0) {
|
|
67
79
|
return 0;
|
|
68
80
|
}
|
|
69
81
|
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
const fraction = position - index;
|
|
73
|
-
|
|
74
|
-
if (sortedValues[index + 1] !== undefined) {
|
|
75
|
-
return sortedValues[index] + fraction * (sortedValues[index + 1] - sortedValues[index]);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return sortedValues[index];
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function calcStaticThresholds(counts: number[]) {
|
|
82
|
-
const nonZeroCounts = counts.filter(c => c > 0);
|
|
83
|
-
|
|
84
|
-
if (nonZeroCounts.length === 0) {
|
|
85
|
-
return [0, 0, 0, 0, 0];
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const sorted = [...nonZeroCounts].sort((a, b) => a - b);
|
|
89
|
-
|
|
90
|
-
return [
|
|
91
|
-
0,
|
|
92
|
-
calcPreciseQuan(sorted, 0.2),
|
|
93
|
-
calcPreciseQuan(sorted, 0.4),
|
|
94
|
-
calcPreciseQuan(sorted, 0.6),
|
|
95
|
-
calcPreciseQuan(sorted, 0.8),
|
|
96
|
-
];
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function getDynamicHeatLevel(value: number, thresholds: number[]): number {
|
|
100
|
-
for (let i = thresholds.length - 1; i >= 0; i--) {
|
|
101
|
-
if (value === 0) {
|
|
102
|
-
return 0;
|
|
103
|
-
} else if (value >= thresholds[i]) {
|
|
104
|
-
return i;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
return 0;
|
|
82
|
+
const level = Math.ceil((value / maxValue) * 4);
|
|
83
|
+
return Math.min(Math.max(level, 1), 4);
|
|
108
84
|
}
|
|
109
85
|
|
|
110
86
|
const heatMapData = computed(() =>
|
|
@@ -115,13 +91,13 @@ const heatMapData = computed(() =>
|
|
|
115
91
|
const incorrectHeatMap = generateHeatMap(incorrect, "incorrect");
|
|
116
92
|
|
|
117
93
|
const correctCounts = correctHeatMap.map(i => i.count);
|
|
118
|
-
const
|
|
94
|
+
const correctMax = Math.max(0, ...correctCounts);
|
|
119
95
|
|
|
120
96
|
const incorrectCounts = incorrectHeatMap.map(i => i.count);
|
|
121
|
-
const
|
|
97
|
+
const incorrectMax = Math.max(0, ...incorrectCounts);
|
|
122
98
|
|
|
123
|
-
correctHeatMap.forEach(i => i.level =
|
|
124
|
-
incorrectHeatMap.forEach(i => i.level =
|
|
99
|
+
correctHeatMap.forEach(i => i.level = getHeatLevel(i.count, correctMax));
|
|
100
|
+
incorrectHeatMap.forEach(i => i.level = getHeatLevel(i.count, incorrectMax));
|
|
125
101
|
|
|
126
102
|
return {
|
|
127
103
|
label: p.label,
|
|
@@ -145,14 +121,14 @@ const heatMapData = computed(() =>
|
|
|
145
121
|
</span>
|
|
146
122
|
|
|
147
123
|
<div
|
|
148
|
-
grid grid-cols-
|
|
149
|
-
gap-
|
|
124
|
+
grid grid-cols-4
|
|
125
|
+
gap-6 space-y-0
|
|
150
126
|
place-items-center
|
|
151
127
|
>
|
|
152
128
|
<div
|
|
153
129
|
v-for="heatMap in heatMapData" :key="heatMap.label"
|
|
154
|
-
class="w-
|
|
155
|
-
flex flex-col
|
|
130
|
+
class="w-280px h-100px"
|
|
131
|
+
flex flex-col gap-2
|
|
156
132
|
items-center justify-center
|
|
157
133
|
border border-gray-100 dark:border-gray-600
|
|
158
134
|
rounded-md
|
|
@@ -173,54 +149,48 @@ const heatMapData = computed(() =>
|
|
|
173
149
|
|
|
174
150
|
<div
|
|
175
151
|
flex flex-row
|
|
176
|
-
gap-
|
|
152
|
+
gap-0.5
|
|
177
153
|
>
|
|
178
154
|
<div
|
|
179
155
|
v-for="(correctItem, index) in heatMap.correctHeatMap" :key="`correct-${index}`"
|
|
180
156
|
>
|
|
181
|
-
<
|
|
182
|
-
:content="correctItem.description"
|
|
183
|
-
position="top"
|
|
184
|
-
>
|
|
157
|
+
<Tooltip>
|
|
185
158
|
<div
|
|
186
159
|
:data-level="correctItem.level"
|
|
187
160
|
class="correct-heat"
|
|
188
|
-
size-
|
|
189
|
-
rounded-
|
|
190
|
-
|
|
191
|
-
border border-gray-100 dark:border-gray-600
|
|
161
|
+
size-10px
|
|
162
|
+
rounded-0.7
|
|
163
|
+
bg-gray-200 dark:bg-gray-700
|
|
192
164
|
/>
|
|
193
|
-
|
|
165
|
+
<template #popper>
|
|
166
|
+
{{ correctItem.description }}
|
|
167
|
+
</template>
|
|
168
|
+
</Tooltip>
|
|
194
169
|
</div>
|
|
195
170
|
</div>
|
|
196
171
|
|
|
197
172
|
<div
|
|
198
173
|
flex flex-row
|
|
199
|
-
gap-
|
|
174
|
+
gap-0.5
|
|
200
175
|
>
|
|
201
176
|
<div
|
|
202
177
|
v-for="(incorrectItem, index) in heatMap.incorrectHeatMap" :key="`incorrect-${index}`"
|
|
203
178
|
>
|
|
204
|
-
<
|
|
205
|
-
:content="incorrectItem.description"
|
|
206
|
-
position="top"
|
|
207
|
-
>
|
|
179
|
+
<Tooltip>
|
|
208
180
|
<div
|
|
209
181
|
:data-level="incorrectItem.level"
|
|
210
182
|
class="incorrect-heat"
|
|
211
|
-
size-
|
|
212
|
-
rounded-
|
|
213
|
-
|
|
214
|
-
border border-gray-100 dark:border-gray-600
|
|
183
|
+
size-10px
|
|
184
|
+
rounded-0.7
|
|
185
|
+
bg-gray-200 dark:bg-gray-700
|
|
215
186
|
/>
|
|
216
|
-
|
|
187
|
+
<template #popper>
|
|
188
|
+
{{ incorrectItem.description }}
|
|
189
|
+
</template>
|
|
190
|
+
</Tooltip>
|
|
217
191
|
</div>
|
|
218
192
|
</div>
|
|
219
193
|
</div>
|
|
220
194
|
</div>
|
|
221
195
|
</div>
|
|
222
196
|
</template>
|
|
223
|
-
|
|
224
|
-
<style scoped lang="less">
|
|
225
|
-
@import "./SubmitHeatMap.less";
|
|
226
|
-
</style>
|
|
@@ -48,20 +48,28 @@ const isGirl = computed(() => team.value.isGirl);
|
|
|
48
48
|
>
|
|
49
49
|
<template v-if="isVisible">
|
|
50
50
|
<div flex="~ items-center gap-3" mb="2">
|
|
51
|
-
<
|
|
51
|
+
<div
|
|
52
52
|
v-if="team.organization?.logo"
|
|
53
|
-
|
|
54
|
-
alt="org logo"
|
|
55
|
-
class="h-12 w-12"
|
|
53
|
+
class="h-12 w-12 flex items-center justify-center bg-transparent"
|
|
56
54
|
>
|
|
57
|
-
|
|
55
|
+
<img
|
|
56
|
+
:src="getImageSource(team.organization.logo, DATA_HOST)"
|
|
57
|
+
alt="org logo"
|
|
58
|
+
class="max-h-12 max-w-full object-contain mx-auto h-full w-auto block"
|
|
59
|
+
>
|
|
60
|
+
</div>
|
|
61
|
+
<div
|
|
58
62
|
v-if="team.badge"
|
|
59
|
-
|
|
60
|
-
alt="team badge"
|
|
61
|
-
class="h-12 w-12"
|
|
63
|
+
class="h-12 w-12 flex items-center justify-center bg-transparent"
|
|
62
64
|
>
|
|
65
|
+
<img
|
|
66
|
+
:src="getImageSource(team.badge, DATA_HOST)"
|
|
67
|
+
alt="team badge"
|
|
68
|
+
class="max-h-12 max-w-full object-contain mx-auto h-full w-auto block"
|
|
69
|
+
>
|
|
70
|
+
</div>
|
|
63
71
|
<div flex="1 ~ col" min-w-0>
|
|
64
|
-
<
|
|
72
|
+
<Tooltip placement="top">
|
|
65
73
|
<div
|
|
66
74
|
text="lg gray-900 dark:white"
|
|
67
75
|
font="semibold"
|
|
@@ -71,23 +79,23 @@ const isGirl = computed(() => team.value.isGirl);
|
|
|
71
79
|
<span v-if="isUnofficial" class="i-line-md:star-alt-filled" text="sm" />
|
|
72
80
|
<span v-if="isGirl" class="i-tabler:gender-female" text="sm pink-500" />
|
|
73
81
|
</div>
|
|
74
|
-
<template #
|
|
82
|
+
<template #popper>
|
|
75
83
|
<div>{{ t("team_info.team_id") }}{{ t("common.colon") }}{{ team.id }}</div>
|
|
76
84
|
<div>{{ t("team_info.team_name") }}{{ t("common.colon") }}{{ teamName }}</div>
|
|
77
85
|
</template>
|
|
78
|
-
</
|
|
79
|
-
<
|
|
86
|
+
</Tooltip>
|
|
87
|
+
<Tooltip v-if="organizationName" placement="top">
|
|
80
88
|
<div
|
|
81
89
|
text="sm gray-500 dark:gray-400"
|
|
82
90
|
class="line-clamp-1"
|
|
83
91
|
>
|
|
84
92
|
{{ organizationName }}
|
|
85
93
|
</div>
|
|
86
|
-
<template #
|
|
94
|
+
<template #popper>
|
|
87
95
|
<div>{{ t("team_info.org_id") }}{{ t("common.colon") }}{{ team.organization?.id }}</div>
|
|
88
96
|
<div>{{ t("team_info.org_name") }}{{ t("common.colon") }}{{ organizationName }}</div>
|
|
89
97
|
</template>
|
|
90
|
-
</
|
|
98
|
+
</Tooltip>
|
|
91
99
|
</div>
|
|
92
100
|
</div>
|
|
93
101
|
|
|
@@ -92,6 +92,7 @@ const tableRows = computed(() => {
|
|
|
92
92
|
const rows = [
|
|
93
93
|
{ label: t("team_info.rank"), value: team.value.rank, show: true },
|
|
94
94
|
{ label: t("team_info.team_name"), value: team.value.name.getOrDefault(lang.value), show: true },
|
|
95
|
+
{ label: t("team_info.description"), value: team.value.description?.getOrDefault(lang.value), show: !!team.value.description },
|
|
95
96
|
{ label: t("team_info.organization"), value: team.value.organization?.name.getOrDefault(lang.value), show: !!team.value.organization },
|
|
96
97
|
{ label: t("team_info.group"), value: groupNames.value, show: team.value.group.length > 0 },
|
|
97
98
|
{ label: t("team_info.members"), value: team.value.membersToString(lang.value), show: team.value.members.length > 0 },
|
|
@@ -104,13 +104,15 @@ function isRenderByVisible() {
|
|
|
104
104
|
:class="[getStandClassName(team)]"
|
|
105
105
|
>
|
|
106
106
|
<div
|
|
107
|
-
|
|
108
|
-
class="absolute left-0 top-0 bottom-0 flex items-center px-1"
|
|
107
|
+
class="absolute left-0 top-0 bottom-0 flex items-center justify-center px-1 w-12"
|
|
109
108
|
>
|
|
110
|
-
<
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
109
|
+
<div class="flex items-center justify-center w-full h-full">
|
|
110
|
+
<Badge
|
|
111
|
+
v-if="team?.organization?.logo"
|
|
112
|
+
:image="team?.organization?.logo"
|
|
113
|
+
width-class="max-h-full max-w-full object-contain mx-auto"
|
|
114
|
+
/>
|
|
115
|
+
</div>
|
|
114
116
|
</div>
|
|
115
117
|
<div flex flex-1 :class="team?.organization?.logo ? 'pl-10' : ''">
|
|
116
118
|
<div
|
|
@@ -149,13 +151,15 @@ function isRenderByVisible() {
|
|
|
149
151
|
:class="[getStandClassName(team)]"
|
|
150
152
|
>
|
|
151
153
|
<div
|
|
152
|
-
|
|
153
|
-
class="absolute left-0 top-0 bottom-0 flex items-center px-1"
|
|
154
|
+
class="absolute left-0 top-0 bottom-0 flex items-center justify-center px-1 w-12"
|
|
154
155
|
>
|
|
155
|
-
<
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
156
|
+
<div class="flex items-center justify-center w-full h-full">
|
|
157
|
+
<Badge
|
|
158
|
+
v-if="team.badge"
|
|
159
|
+
:image="team.badge"
|
|
160
|
+
width-class="max-h-full max-w-full object-contain mx-auto"
|
|
161
|
+
/>
|
|
162
|
+
</div>
|
|
159
163
|
</div>
|
|
160
164
|
<div
|
|
161
165
|
flex items-center justify-center
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { Rank } from "@xcpcio/core";
|
|
3
|
+
import { getRuntimeConfig } from "@board/composables/runtimeConfig";
|
|
3
4
|
|
|
4
5
|
const props = defineProps<{
|
|
5
6
|
rank: Rank;
|
|
@@ -10,9 +11,10 @@ const { t } = useI18n();
|
|
|
10
11
|
const route = useRoute();
|
|
11
12
|
const router = useRouter();
|
|
12
13
|
const component = useQueryForComponent();
|
|
14
|
+
const dataSource = getRuntimeConfig().dataSource;
|
|
13
15
|
|
|
14
16
|
function goResolver() {
|
|
15
|
-
if (
|
|
17
|
+
if (dataSource) {
|
|
16
18
|
component.value = "resolver";
|
|
17
19
|
} else {
|
|
18
20
|
router.push(`/resolver/?data-source=${route.path}`);
|
|
@@ -20,7 +22,7 @@ function goResolver() {
|
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
function goBalloon() {
|
|
23
|
-
if (
|
|
25
|
+
if (dataSource) {
|
|
24
26
|
component.value = "balloon";
|
|
25
27
|
} else {
|
|
26
28
|
router.push(`/balloon/?data-source=${route.path}`);
|
|
@@ -28,7 +30,7 @@ function goBalloon() {
|
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
function goCountdown() {
|
|
31
|
-
if (
|
|
33
|
+
if (dataSource) {
|
|
32
34
|
component.value = "countdown";
|
|
33
35
|
} else {
|
|
34
36
|
router.push(`/countdown/?data-source=${route.path}`);
|
|
@@ -1,32 +1,98 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { TooltipInterface, TooltipOptions } from "flowbite";
|
|
3
3
|
import { Tooltip } from "flowbite";
|
|
4
|
-
|
|
5
4
|
import "./Tooltip.css";
|
|
6
5
|
|
|
7
6
|
const props = defineProps<{
|
|
8
7
|
placement?: "left" | "right" | "top" | "bottom" | "auto";
|
|
9
8
|
}>();
|
|
10
9
|
|
|
11
|
-
const
|
|
12
|
-
const
|
|
10
|
+
const instanceId = `vue-tooltip-${getCurrentInstance()?.uid ?? Math.random().toString(36).slice(2)}`;
|
|
11
|
+
const tooltipTargetEl = ref<HTMLElement | null>(null);
|
|
12
|
+
const tooltipTriggerEl = ref<HTMLElement | null>(null);
|
|
13
|
+
const isTriggerVisible = useElementVisibility(tooltipTriggerEl);
|
|
14
|
+
const placement = computed(() => {
|
|
15
|
+
return props.placement ?? "auto";
|
|
16
|
+
});
|
|
13
17
|
|
|
14
|
-
// eslint-disable-next-line unused-imports/no-unused-vars
|
|
15
18
|
let tooltip: TooltipInterface | null = null;
|
|
19
|
+
let cleanup: (() => void) | null = null;
|
|
16
20
|
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
function createTooltip() {
|
|
22
|
+
if (tooltip) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!tooltipTargetEl.value || !tooltipTriggerEl.value) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const options: TooltipOptions = {
|
|
31
|
+
placement: placement.value as unknown as undefined,
|
|
32
|
+
triggerType: "none",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
tooltip = new Tooltip(tooltipTargetEl.value, tooltipTriggerEl.value, options, {
|
|
36
|
+
id: instanceId,
|
|
37
|
+
override: true,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function destroyTooltip() {
|
|
42
|
+
tooltip?.destroyAndRemoveInstance();
|
|
43
|
+
tooltip = null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function showTooltip() {
|
|
47
|
+
createTooltip();
|
|
48
|
+
tooltip?.show();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function hideTooltip() {
|
|
52
|
+
tooltip?.hide();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
watch(isTriggerVisible, (visible) => {
|
|
56
|
+
if (visible) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!tooltip) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
tooltip.hide();
|
|
65
|
+
destroyTooltip();
|
|
19
66
|
});
|
|
20
67
|
|
|
21
68
|
onMounted(() => {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
triggerType: "hover",
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
tooltip = new Tooltip(tooltipTargetEl.value, tooltipTriggerEl.value, options);
|
|
69
|
+
const triggerEl = tooltipTriggerEl.value;
|
|
70
|
+
if (!triggerEl) {
|
|
71
|
+
return;
|
|
29
72
|
}
|
|
73
|
+
|
|
74
|
+
const onMouseEnter = () => showTooltip();
|
|
75
|
+
const onMouseLeave = () => hideTooltip();
|
|
76
|
+
const onFocus = () => showTooltip();
|
|
77
|
+
const onBlur = () => hideTooltip();
|
|
78
|
+
|
|
79
|
+
triggerEl.addEventListener("mouseenter", onMouseEnter);
|
|
80
|
+
triggerEl.addEventListener("mouseleave", onMouseLeave);
|
|
81
|
+
triggerEl.addEventListener("focus", onFocus, true);
|
|
82
|
+
triggerEl.addEventListener("blur", onBlur, true);
|
|
83
|
+
|
|
84
|
+
cleanup = () => {
|
|
85
|
+
triggerEl.removeEventListener("mouseenter", onMouseEnter);
|
|
86
|
+
triggerEl.removeEventListener("mouseleave", onMouseLeave);
|
|
87
|
+
triggerEl.removeEventListener("focus", onFocus, true);
|
|
88
|
+
triggerEl.removeEventListener("blur", onBlur, true);
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
onBeforeUnmount(() => {
|
|
93
|
+
cleanup?.();
|
|
94
|
+
cleanup = null;
|
|
95
|
+
destroyTooltip();
|
|
30
96
|
});
|
|
31
97
|
</script>
|
|
32
98
|
|
package/src/components.d.ts
CHANGED
|
@@ -34,7 +34,6 @@ declare module 'vue' {
|
|
|
34
34
|
GiantsOptions: typeof import('./components/battle-of-giants/GiantsOptions.vue')['default']
|
|
35
35
|
GiantsScoreBoard: typeof import('./components/battle-of-giants/GiantsScoreBoard.vue')['default']
|
|
36
36
|
GoBack: typeof import('./components/GoBack.vue')['default']
|
|
37
|
-
HeatMapTooltip: typeof import('./components/board/HeatMapTooltip.vue')['default']
|
|
38
37
|
InfoModal: typeof import('./components/board/InfoModal.vue')['default']
|
|
39
38
|
Modal: typeof import('./components/board/Modal.vue')['default']
|
|
40
39
|
ModalMenu: typeof import('./components/board/ModalMenu.vue')['default']
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { getRuntimeConfig } from "./runtimeConfig";
|
|
2
|
+
|
|
1
3
|
export const APP_VERSION = __APP_VERSION__;
|
|
2
4
|
export const GITHUB_URL = __GITHUB_URL__;
|
|
3
5
|
export const GITHUB_SHA = __GITHUB_SHA__;
|
|
@@ -11,19 +13,11 @@ export const COUNTDOWN_TITLE_SUFFIX = "Countdown - XCPCIO";
|
|
|
11
13
|
export const SUBMISSION_TITLE_SUFFIX = "Submission - XCPCIO";
|
|
12
14
|
|
|
13
15
|
export const CDN_HOST = computed(() => {
|
|
14
|
-
|
|
15
|
-
return "";
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
return window.CDN_HOST;
|
|
16
|
+
return getRuntimeConfig().cdnHost ?? "";
|
|
19
17
|
});
|
|
20
18
|
|
|
21
19
|
export const DATA_HOST = computed(() => {
|
|
22
|
-
|
|
23
|
-
return "";
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
return window.DATA_HOST;
|
|
20
|
+
return getRuntimeConfig().dataHost ?? "";
|
|
27
21
|
});
|
|
28
22
|
|
|
29
23
|
export const RATING_DATA_HOST = computed(() => {
|
|
@@ -32,9 +26,5 @@ export const RATING_DATA_HOST = computed(() => {
|
|
|
32
26
|
});
|
|
33
27
|
|
|
34
28
|
export const DATA_REGION = computed(() => {
|
|
35
|
-
|
|
36
|
-
return "";
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return window.DATA_REGION;
|
|
29
|
+
return getRuntimeConfig().dataRegion ?? "";
|
|
40
30
|
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { RuntimeConfig } from "@board/types";
|
|
2
|
+
|
|
3
|
+
export function getRuntimeConfig(): RuntimeConfig {
|
|
4
|
+
if (typeof window === "undefined") {
|
|
5
|
+
return {};
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const config = window.RUNTIME_CONFIG ?? {};
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
pageType: config.pageType,
|
|
12
|
+
component: config.component,
|
|
13
|
+
dataSource: config.dataSource ?? window.DATA_SOURCE,
|
|
14
|
+
baseUrl: config.baseUrl ?? window.BASE_URL,
|
|
15
|
+
cdnHost: config.cdnHost ?? window.CDN_HOST,
|
|
16
|
+
dataHost: config.dataHost ?? window.DATA_HOST,
|
|
17
|
+
dataRegion: config.dataRegion ?? window.DATA_REGION,
|
|
18
|
+
defaultLang: config.defaultLang ?? window.DEFAULT_LANG,
|
|
19
|
+
refetchInterval: config.refetchInterval ?? window.REFETCH_INTERVAL,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Contest, Submissions, Teams } from "@xcpcio/types";
|
|
2
2
|
import { useQuery } from "@tanstack/vue-query";
|
|
3
|
+
import { getRuntimeConfig } from "./runtimeConfig";
|
|
3
4
|
|
|
4
5
|
export interface BoardData {
|
|
5
6
|
contest: Contest;
|
|
@@ -47,7 +48,7 @@ async function fetchAndAssignSeatMap(contest: Contest, baseUrl: string): Promise
|
|
|
47
48
|
|
|
48
49
|
async function fetch_board_data(target: string): Promise<BoardData> {
|
|
49
50
|
const endpoint = target.startsWith("/") ? target.slice(1) : target;
|
|
50
|
-
let prefix = `${
|
|
51
|
+
let prefix = `${getRuntimeConfig().dataHost ?? ""}${endpoint}`;
|
|
51
52
|
const options = {
|
|
52
53
|
allInOne: false,
|
|
53
54
|
};
|
|
@@ -97,7 +98,7 @@ async function fetch_board_data(target: string): Promise<BoardData> {
|
|
|
97
98
|
export function useQueryBoardData(target: string, queryOnce = false) {
|
|
98
99
|
const retry = 3;
|
|
99
100
|
const staleTime = 30_000;
|
|
100
|
-
let refetchInterval: number | false =
|
|
101
|
+
let refetchInterval: number | false = getRuntimeConfig().refetchInterval ?? 15_000;
|
|
101
102
|
|
|
102
103
|
if (queryOnce) {
|
|
103
104
|
refetchInterval = false;
|