@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.
Files changed (113) hide show
  1. package/dist/404.html +1 -1
  2. package/dist/assets/Balloon-BUgwq0Kx.js +1 -0
  3. package/dist/assets/Board-BfbUwmvq.css +1 -0
  4. package/dist/assets/Board-D7hrVoFu.js +190 -0
  5. package/dist/assets/ContestStateBadge-DgwznQrX.css +1 -0
  6. package/dist/assets/ContestStateBadge-Dt7AgnYd.js +1 -0
  7. package/dist/assets/Countdown-B97p6HbD.js +1 -0
  8. package/dist/assets/DataSourceInput-B67_jwrR.js +1 -0
  9. package/dist/assets/NavBar-CnD7iUz_.js +1 -0
  10. package/dist/assets/NavBar-O1kqnfE4.css +1 -0
  11. package/dist/assets/Resolver-CRBstFE3.css +1 -0
  12. package/dist/assets/Resolver-DQ3kkz9L.js +1 -0
  13. package/dist/assets/RightArrowIcon-F6HBDgDW.js +1 -0
  14. package/dist/assets/TheInput-D67yPyFW.js +1 -0
  15. package/dist/assets/Tooltip-BR6I8-SM.js +27 -0
  16. package/dist/assets/Tooltip-C9jfn33Z.css +1 -0
  17. package/dist/assets/_..-D9qurBBH.css +1 -0
  18. package/dist/assets/_...all_-BDXlESlP.js +1 -0
  19. package/dist/assets/_...all_-Cek5Lnmd.js +6 -0
  20. package/dist/assets/_plugin-vue_export-helper-BDNMzG2s.js +1 -0
  21. package/dist/assets/balloon-DpIcef0j.js +1 -0
  22. package/dist/assets/board-KpBI6_ac.js +1 -0
  23. package/dist/assets/board-layout-DD2-Z-AS.js +1 -0
  24. package/dist/assets/constant-Dl-MmgU0.js +1 -0
  25. package/dist/assets/countdown-bqehvGJt.js +1 -0
  26. package/dist/assets/default-CSWSpjqo.js +1 -0
  27. package/dist/assets/headless-BSf0YrvV.js +1 -0
  28. package/dist/assets/index-4yhXaony.css +1 -0
  29. package/dist/assets/index-ClRmeGtV.js +12 -0
  30. package/dist/assets/index-layout-Dxv-LU9c.js +1 -0
  31. package/dist/assets/pages-DZG1eNR4.css +1 -0
  32. package/dist/assets/{index-D0V3pqsw.js → pages-DyUshEIY.js} +2 -2
  33. package/dist/assets/pagination-2onPPYtS.js +21 -0
  34. package/dist/assets/person-DrN1htE6.js +1 -0
  35. package/dist/assets/query-XskWvdZB.js +20 -0
  36. package/dist/assets/rank-Bk_Yo4wU.js +1 -0
  37. package/dist/assets/rating-DSZOgEeb.js +1 -0
  38. package/dist/assets/rating-DiCQYzia.css +1 -0
  39. package/dist/assets/resolver-DOjgimki.js +1 -0
  40. package/dist/assets/route-block-ChiJXsfH.js +1 -0
  41. package/dist/assets/utils-Cfm-h8Z9.js +1 -0
  42. package/dist/assets/virtual_intlify-i18n-0-BmghNkij.js +1 -0
  43. package/dist/assets/virtual_intlify-i18n-1-DzpDgiG5.js +1 -0
  44. package/dist/assets/vue.runtime.esm-bundler-DTMOfGWR.js +4 -0
  45. package/dist/index.html +1 -1
  46. package/package.json +45 -45
  47. package/src/auto-imports.d.ts +6 -4
  48. package/src/components/board/Board.vue +3 -0
  49. package/src/components/board/Export.vue +5 -1
  50. package/src/components/board/Progress.vue +1 -0
  51. package/src/components/board/SeatMapView.vue +6 -3
  52. package/src/components/board/SubmissionsTable.vue +1 -2
  53. package/src/components/board/SubmitHeatMap.less +11 -3
  54. package/src/components/board/SubmitHeatMap.vue +56 -86
  55. package/src/components/board/TeamCard.vue +22 -14
  56. package/src/components/board/TeamOverview.vue +1 -0
  57. package/src/components/board/TeamUI.vue +16 -12
  58. package/src/components/board/Utility.vue +5 -3
  59. package/src/components/flowbite/Tooltip.vue +79 -13
  60. package/src/components.d.ts +0 -1
  61. package/src/composables/constant.ts +5 -15
  62. package/src/composables/runtimeConfig.ts +21 -0
  63. package/src/composables/useQueryBoardData.ts +3 -2
  64. package/src/main.ts +2 -1
  65. package/src/modules/i18n.ts +2 -1
  66. package/src/pages/[...all].vue +6 -2
  67. package/src/pages/index.vue +2 -2
  68. package/src/shims.d.ts +15 -8
  69. package/src/types.ts +16 -0
  70. package/vite.config.ts +4 -0
  71. package/dist/assets/Balloon.vue_vue_type_script_setup_true_lang-DHemO8cG.js +0 -1
  72. package/dist/assets/Board-C6kTRGEP.css +0 -1
  73. package/dist/assets/Board-DxiquVyO.js +0 -194
  74. package/dist/assets/ContestStateBadge-Bgdroopp.js +0 -1
  75. package/dist/assets/ContestStateBadge-D6uf8wH-.css +0 -1
  76. package/dist/assets/Countdown-ovUCuJ35.js +0 -1
  77. package/dist/assets/DataSourceInput.vue_vue_type_script_setup_true_lang-DgkCadgz.js +0 -1
  78. package/dist/assets/NavBar-DfKQ1ytp.js +0 -1
  79. package/dist/assets/NavBar-aT2xEvYz.css +0 -1
  80. package/dist/assets/Resolver-BQcsyOHn.js +0 -1
  81. package/dist/assets/Resolver-DV9FleNP.css +0 -1
  82. package/dist/assets/RightArrowIcon-BKvYPM1O.js +0 -1
  83. package/dist/assets/TheInput.vue_vue_type_script_setup_true_lang-OA6Dkh4w.js +0 -1
  84. package/dist/assets/Tooltip-DzyPDwbo.css +0 -1
  85. package/dist/assets/Tooltip.vue_vue_type_script_setup_true_lang-DWNdRDhE.js +0 -27
  86. package/dist/assets/_..-5I0UWqRG.css +0 -1
  87. package/dist/assets/_...all_-BtYYdFga.js +0 -1
  88. package/dist/assets/_...all_-XVDapPdm.js +0 -6
  89. package/dist/assets/_plugin-vue_export-helper-DlAUqK2U.js +0 -1
  90. package/dist/assets/board-layout-LZdDEkNj.js +0 -1
  91. package/dist/assets/constant-CYOTMbJW.js +0 -1
  92. package/dist/assets/dayjs-Ccpn6fIh.js +0 -1
  93. package/dist/assets/default-CRpkLq3n.js +0 -1
  94. package/dist/assets/en-DGp_RGnz.js +0 -1
  95. package/dist/assets/headless-l1XzJj3O.js +0 -1
  96. package/dist/assets/index-B34Y_Z57.js +0 -15
  97. package/dist/assets/index-B3NvUps3.js +0 -1
  98. package/dist/assets/index-B_303hcz.js +0 -1
  99. package/dist/assets/index-Bojw84ll.css +0 -1
  100. package/dist/assets/index-BuFplOHa.js +0 -1
  101. package/dist/assets/index-CeJCbVnH.js +0 -1
  102. package/dist/assets/index-CgXDtxXW.css +0 -1
  103. package/dist/assets/index-D-msPqye.css +0 -1
  104. package/dist/assets/index-DOf6OpNV.js +0 -1
  105. package/dist/assets/index-layout-BDulxfyN.js +0 -1
  106. package/dist/assets/pagination-DkTeb3KN.js +0 -3
  107. package/dist/assets/person-gKEre052.js +0 -1
  108. package/dist/assets/rank-DJDWVDFD.js +0 -1
  109. package/dist/assets/route-block-B_A1xBdJ.js +0 -1
  110. package/dist/assets/use-vmodel-Bem9C7yk.js +0 -20
  111. package/dist/assets/zh-CN-BUehjmhL.js +0 -1
  112. package/src/components/board/HeatMapTooltip.less +0 -115
  113. 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 }, () => null)];
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
- <HeatMapTooltip v-if="seatId" :content="getTooltipContent(seatId)" position="bottom">
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
- </HeatMapTooltip>
184
+ <template #popper>
185
+ {{ getTooltipContent(seatId) }}
186
+ </template>
187
+ </Tooltip>
185
188
  <div
186
189
  v-else
187
190
  :style="getSeatStyle(section)"
@@ -150,8 +150,7 @@ const submissions = computed(() => {
150
150
  if (o.orgIds.length === 0
151
151
  && o.teamIds.length === 0
152
152
  && o.languages.length === 0
153
- && o.statuses.length === 0
154
- ) {
153
+ && o.statuses.length === 0) {
155
154
  return true;
156
155
  }
157
156
 
@@ -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 submissions = computed(() => rank.value.getSubmissions());
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
- const intervalDescriptions = Array.from({ length: mapTimeDiffLength }, (_, index) => {
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: Map<any, any>, type: "correct" | "incorrect") {
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 / intervalSize), mapTimeDiffLength - 1);
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: `${intervalDescriptions[i]}${count} ${type} submissions`,
73
+ description: `${descriptions[i]}${count} ${type} submissions`,
62
74
  }));
63
75
  }
64
76
 
65
- function calcPreciseQuan(sortedValues: number[], p: number): number {
66
- if (sortedValues.length === 0) {
77
+ function getHeatLevel(value: number, maxValue: number): number {
78
+ if (value <= 0 || maxValue <= 0) {
67
79
  return 0;
68
80
  }
69
81
 
70
- const position = (sortedValues.length - 1) * p;
71
- const index = Math.floor(position);
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 correctThresholds = calcStaticThresholds(correctCounts);
94
+ const correctMax = Math.max(0, ...correctCounts);
119
95
 
120
96
  const incorrectCounts = incorrectHeatMap.map(i => i.count);
121
- const incorrectThresholds = calcStaticThresholds(incorrectCounts);
97
+ const incorrectMax = Math.max(0, ...incorrectCounts);
122
98
 
123
- correctHeatMap.forEach(i => i.level = getDynamicHeatLevel(i.count, correctThresholds));
124
- incorrectHeatMap.forEach(i => i.level = getDynamicHeatLevel(i.count, incorrectThresholds));
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-3
149
- gap-10 space-y-0
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-350px h-100px"
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-1
152
+ gap-0.5
177
153
  >
178
154
  <div
179
155
  v-for="(correctItem, index) in heatMap.correctHeatMap" :key="`correct-${index}`"
180
156
  >
181
- <HeatMapTooltip
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-12px
189
- rounded-1
190
- shadow
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
- </HeatMapTooltip>
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-1
174
+ gap-0.5
200
175
  >
201
176
  <div
202
177
  v-for="(incorrectItem, index) in heatMap.incorrectHeatMap" :key="`incorrect-${index}`"
203
178
  >
204
- <HeatMapTooltip
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-12px
212
- rounded-1
213
- shadow
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
- </HeatMapTooltip>
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
- <img
51
+ <div
52
52
  v-if="team.organization?.logo"
53
- :src="getImageSource(team.organization.logo, DATA_HOST)"
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
- <img
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
- :src="getImageSource(team.badge, DATA_HOST)"
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
- <HeatMapTooltip position="top">
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 #tooltip-content>
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
- </HeatMapTooltip>
79
- <HeatMapTooltip v-if="organizationName" position="top">
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 #tooltip-content>
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
- </HeatMapTooltip>
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
- v-if="team?.organization?.logo"
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
- <Badge
111
- :image="team?.organization?.logo"
112
- width-class="h-full w-auto"
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
- v-if="team.badge"
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
- <Badge
156
- :image="team.badge"
157
- width-class="h-full w-auto"
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 (window.DATA_SOURCE) {
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 (window.DATA_SOURCE) {
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 (window.DATA_SOURCE) {
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 tooltipTargetEl = ref(null);
12
- const tooltipTriggerEl = ref(null);
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
- const placement = computed(() => {
18
- return props.placement ?? "auto";
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
- if (tooltipTargetEl.value && tooltipTriggerEl.value) {
23
- const options: TooltipOptions = {
24
- placement: placement.value as unknown as undefined,
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
 
@@ -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
- if (!window) {
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
- if (!window) {
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
- if (!window) {
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 = `${window.DATA_HOST}${endpoint}`;
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 = window.REFETCH_INTERVAL;
101
+ let refetchInterval: number | false = getRuntimeConfig().refetchInterval ?? 15_000;
101
102
 
102
103
  if (queryOnce) {
103
104
  refetchInterval = false;