@xcpcio/board-app 0.6.4 → 0.13.0

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 (112) hide show
  1. package/.eslintrc.json +6 -0
  2. package/LICENSE +1 -1
  3. package/README.md +13 -30
  4. package/cypress.config.ts +14 -0
  5. package/dist/_headers +3 -0
  6. package/dist/about.html +11 -0
  7. package/dist/assets/_...all_-27c7ae93.css +1 -0
  8. package/dist/assets/_...all_-d56798b5.js +3 -0
  9. package/dist/assets/_name_-8eab6137.js +1 -0
  10. package/dist/assets/about-a8cb8700.js +11 -0
  11. package/dist/assets/app-37f77a84.js +65 -0
  12. package/dist/assets/board-layout-deaedfc1.js +1 -0
  13. package/dist/assets/en-caedd340.js +1 -0
  14. package/dist/assets/home-49c336e5.js +1 -0
  15. package/dist/assets/index-a270cacd.css +5 -0
  16. package/dist/assets/index-layout-d65c80ea.js +1 -0
  17. package/dist/assets/test-0a3d6f7a.js +1 -0
  18. package/dist/assets/user-108782a1.js +1 -0
  19. package/dist/assets/virtual_pwa-register-1c1b9161.js +1 -0
  20. package/dist/assets/workbox-window.prod.es5-a7b12eab.js +2 -0
  21. package/dist/assets/zh-CN-86269804.js +1 -0
  22. package/dist/favicon-dark.svg +1 -0
  23. package/dist/favicon.svg +1 -0
  24. package/dist/index.html +1 -171
  25. package/dist/manifest.webmanifest +1 -0
  26. package/dist/pwa-192x192.png +0 -0
  27. package/dist/pwa-512x512.png +0 -0
  28. package/dist/robots.txt +4 -0
  29. package/dist/safari-pinned-tab.svg +41 -0
  30. package/dist/sitemap.xml +1 -0
  31. package/dist/ssr-manifest.json +486 -0
  32. package/dist/sw.js +1 -0
  33. package/dist/test.html +1 -0
  34. package/dist/workbox-b8d87ee1.js +1 -0
  35. package/package.json +94 -50
  36. package/public/_headers +3 -0
  37. package/public/favicon-dark.svg +1 -0
  38. package/public/favicon.svg +1 -0
  39. package/public/pwa-192x192.png +0 -0
  40. package/public/pwa-512x512.png +0 -0
  41. package/public/safari-pinned-tab.svg +41 -0
  42. package/src/App.vue +33 -0
  43. package/src/auto-imports.d.ts +909 -0
  44. package/src/components/ContestIndex.vue +227 -0
  45. package/src/components/Footer.vue +94 -0
  46. package/src/components/GoBack.vue +22 -0
  47. package/src/components/NavBar.vue +152 -0
  48. package/src/components/SearchInput.vue +50 -0
  49. package/src/components/TheCounter.vue +19 -0
  50. package/src/components/TheInput.vue +20 -0
  51. package/src/components/board/Balloon.vue +5 -0
  52. package/src/components/board/Board.vue +396 -0
  53. package/src/components/board/BottomStatistics.vue +159 -0
  54. package/src/components/board/ContestStateBadge.vue +41 -0
  55. package/src/components/board/Export.vue +75 -0
  56. package/src/components/board/Modal.vue +107 -0
  57. package/src/components/board/ModalMenu.vue +64 -0
  58. package/src/components/board/OptionsModal.vue +179 -0
  59. package/src/components/board/Progress.less +442 -0
  60. package/src/components/board/Progress.vue +229 -0
  61. package/src/components/board/SecondLevelMenu.vue +190 -0
  62. package/src/components/board/Standings.less +1162 -0
  63. package/src/components/board/Standings.vue +154 -0
  64. package/src/components/board/StandingsAnnotate.vue +38 -0
  65. package/src/components/board/Statistics.vue +77 -0
  66. package/src/components/board/SubmissionsTable.vue +312 -0
  67. package/src/components/board/SubmissionsTableModal.vue +52 -0
  68. package/src/components/board/TeamAwards.vue +93 -0
  69. package/src/components/board/TeamInfoModal.vue +128 -0
  70. package/src/components/board/TeamProblemBlock.vue +100 -0
  71. package/src/components/board/TeamUI.vue +161 -0
  72. package/src/components/board/Utility.vue +28 -0
  73. package/src/components/icon/GirlIcon.vue +80 -0
  74. package/src/components/icon/RightArrowIcon.vue +26 -0
  75. package/src/components/icon/StarIcon.vue +19 -0
  76. package/src/components/table/TablePagination.vue +108 -0
  77. package/src/components.d.ts +44 -0
  78. package/src/composables/dark.ts +4 -0
  79. package/src/composables/pagination.ts +81 -0
  80. package/src/composables/statistics.ts +280 -0
  81. package/src/composables/useLocalStorage.ts +29 -0
  82. package/src/composables/useQueryBoardData.ts +43 -0
  83. package/src/composables/utils.ts +11 -0
  84. package/src/layouts/board-layout.vue +14 -0
  85. package/src/layouts/default.vue +10 -0
  86. package/src/layouts/home.vue +12 -0
  87. package/src/layouts/index-layout.vue +15 -0
  88. package/src/main.ts +36 -0
  89. package/src/modules/README.md +11 -0
  90. package/src/modules/i18n.ts +52 -0
  91. package/src/modules/nprogress.ts +15 -0
  92. package/src/modules/pinia.ts +18 -0
  93. package/src/modules/pwa.ts +15 -0
  94. package/src/modules/toast.ts +10 -0
  95. package/src/pages/[...all].vue +34 -0
  96. package/src/pages/about.md +21 -0
  97. package/src/pages/hi/[name].vue +50 -0
  98. package/src/pages/index.vue +129 -0
  99. package/src/pages/test.vue +57 -0
  100. package/src/shims.d.ts +16 -0
  101. package/src/stores/user.ts +36 -0
  102. package/src/styles/color.css +51 -0
  103. package/src/styles/main.css +30 -0
  104. package/src/styles/markdown.css +28 -0
  105. package/src/styles/submission-status.css +123 -0
  106. package/src/types.ts +3 -0
  107. package/tsconfig.json +39 -0
  108. package/uno.config.ts +65 -0
  109. package/vite.config.ts +176 -0
  110. package/dist/favicon.ico +0 -0
  111. package/dist/umi.00ae29f6.js +0 -1
  112. package/dist/umi.bd64c248.css +0 -1
@@ -0,0 +1,396 @@
1
+ <script setup lang="ts">
2
+ import _ from "lodash";
3
+ import { useRouteQuery } from "@vueuse/router";
4
+ import { Rank, RankOptions, createContest, createSubmissions, createTeams, getTimeDiff } from "@xcpcio/core";
5
+ import type { Contest, Submissions, Teams } from "@xcpcio/core";
6
+ import { ContestState, type Contest as IContest, type Submissions as ISubmissions, type Teams as ITeams } from "@xcpcio/types";
7
+
8
+ import type { Item } from "~/components/board/SecondLevelMenu.vue";
9
+
10
+ const route = useRoute();
11
+ const title = useTitle();
12
+ const { t } = useI18n();
13
+
14
+ const firstLoaded = ref(false);
15
+ const contestData = ref({} as Contest);
16
+ const teamsData = ref([] as Teams);
17
+ const submissionsData = ref([] as Submissions);
18
+ const rank = ref({} as Rank);
19
+ const now = ref(new Date());
20
+ const rankOptions = ref(new RankOptions());
21
+
22
+ (() => {
23
+ const filterOrganizations = useLocalStorageForFilterOrganizations();
24
+ const filterTeams = useLocalStorageForFilterTeams();
25
+
26
+ if (filterOrganizations.value.length > 0) {
27
+ rankOptions.value.setFilterOrganizations(filterOrganizations.value);
28
+ }
29
+
30
+ if (filterTeams.value.length > 0) {
31
+ rankOptions.value.setFilterTeams(filterTeams.value);
32
+ }
33
+ })();
34
+
35
+ const currentGroup = ref("all");
36
+ function onChangeCurrentGroup(nextGroup: string) {
37
+ if (nextGroup === rankOptions.value.group) {
38
+ return;
39
+ }
40
+
41
+ rankOptions.value.setGroup(nextGroup);
42
+ }
43
+ (() => {
44
+ const currentGroupFromRouteQuery = useRouteQuery(
45
+ "group",
46
+ "all",
47
+ { transform: String },
48
+ );
49
+
50
+ currentGroup.value = currentGroupFromRouteQuery.value;
51
+ rankOptions.value.setGroup(currentGroupFromRouteQuery.value);
52
+ })();
53
+
54
+ function reBuildRank() {
55
+ const newRank = new Rank(contestData.value, teamsData.value, submissionsData.value);
56
+ newRank.options = _.cloneDeep(rankOptions.value);
57
+ newRank.buildRank();
58
+ rank.value = newRank;
59
+ }
60
+
61
+ const { data, isError, error } = useQueryBoardData(route.path, now);
62
+ watch(data, async () => {
63
+ if (data.value === null || data.value === undefined) {
64
+ return;
65
+ }
66
+
67
+ contestData.value = createContest(data.value?.contest as IContest);
68
+ title.value = `${contestData.value.name} - XCPCIO Board`;
69
+
70
+ teamsData.value = createTeams(data.value?.teams as ITeams);
71
+ submissionsData.value = createSubmissions(data.value?.submissions as ISubmissions);
72
+
73
+ if (rankOptions.value.enableFilterSubmissionsByTimestamp) {
74
+ return;
75
+ }
76
+
77
+ reBuildRank();
78
+
79
+ firstLoaded.value = true;
80
+ });
81
+
82
+ const isReBuildRank = ref(false);
83
+ watch(rankOptions.value, () => {
84
+ if (firstLoaded.value === false) {
85
+ return;
86
+ }
87
+
88
+ if (!rank.value.options.isNeedReBuildRank(rankOptions.value)) {
89
+ rank.value.options = _.cloneDeep(rankOptions.value);
90
+ return;
91
+ }
92
+
93
+ if (isReBuildRank.value === true) {
94
+ return;
95
+ }
96
+
97
+ isReBuildRank.value = true;
98
+
99
+ reBuildRank();
100
+
101
+ isReBuildRank.value = false;
102
+ });
103
+
104
+ const typeMenuList = ref<Array<Item>>([
105
+ {
106
+ title: "type_menu.rank",
107
+ keyword: "rank",
108
+ isDefault: true,
109
+ },
110
+ {
111
+ title: "type_menu.submissions",
112
+ keyword: "submissions",
113
+ },
114
+ {
115
+ title: "type_menu.statistics",
116
+ keyword: "statistics",
117
+ },
118
+ {
119
+ title: "type_menu.export",
120
+ keyword: "export",
121
+ },
122
+ {
123
+ title: "type_menu.utility",
124
+ keyword: "utility",
125
+ },
126
+ {
127
+ title: "type_menu.options",
128
+ keyword: "options",
129
+ isModal: true,
130
+ },
131
+ ]);
132
+
133
+ const group = computed(() => {
134
+ return rank.value.contest.group;
135
+ });
136
+
137
+ const groupMenuList = computed(() => {
138
+ const res = Array<Item>();
139
+
140
+ for (const [k, v] of group.value) {
141
+ const item = {
142
+ titles: v.names,
143
+ defaultLang: v.defaultLang,
144
+ keyword: k,
145
+ isDefault: v.isDefault,
146
+ };
147
+
148
+ res.push(item);
149
+ }
150
+
151
+ return res;
152
+ });
153
+
154
+ const currentType = ref("rank");
155
+ const isHiddenOptionsModal = ref(true);
156
+
157
+ function onChangeCurrentType(type: string) {
158
+ if (type === "options") {
159
+ isHiddenOptionsModal.value = false;
160
+ }
161
+ }
162
+
163
+ const startTime = computed(() => {
164
+ const time = rank.value.contest.startTime.format("YYYY-MM-DD HH:mm:ss");
165
+ return `${t("standings.start_time")}${t("common.colon")}${time}`;
166
+ });
167
+
168
+ const endTime = computed(() => {
169
+ const time = rank.value.contest.endTime.format("YYYY-MM-DD HH:mm:ss");
170
+ return `${t("standings.end_time")}${t("common.colon")}${time}`;
171
+ });
172
+
173
+ const elapsedTime = computed(() => {
174
+ const time = rank.value.contest.getContestElapsedTime(now.value);
175
+ return `${t("standings.elapsed")}${t("common.colon")}${time}`;
176
+ });
177
+
178
+ const remainingTime = computed(() => {
179
+ const time = rank.value.contest.getContestRemainingTime(now.value);
180
+ return `${t("standings.remaining")}${t("common.colon")}${time}`;
181
+ });
182
+
183
+ const contestState = computed(() => {
184
+ if (rank.value.options.enableFilterSubmissionsByTimestamp) {
185
+ return ContestState.PAUSED;
186
+ }
187
+
188
+ return rank.value.contest.getContestState();
189
+ });
190
+
191
+ const pausedTime = computed(() => {
192
+ return getTimeDiff(rank.value.options.timestamp);
193
+ });
194
+
195
+ const setNowIntervalId = setInterval(() => {
196
+ now.value = new Date();
197
+ }, 1000);
198
+
199
+ onUnmounted(() => {
200
+ clearInterval(setNowIntervalId);
201
+ });
202
+
203
+ const wrapperWidthClass = "sm:w-[1280px] xl:w-screen";
204
+ const widthClass = "sm:w-[1260px] xl:w-screen";
205
+ </script>
206
+
207
+ <template>
208
+ <div>
209
+ <div v-if="!firstLoaded">
210
+ <div
211
+ :class="[wrapperWidthClass]"
212
+ flex justify-center items-center
213
+ >
214
+ {{ t("common.loading") }}...
215
+
216
+ <div v-if="isError">
217
+ {{ error }}
218
+ </div>
219
+ </div>
220
+ </div>
221
+
222
+ <div
223
+ v-if="firstLoaded"
224
+ :class="[wrapperWidthClass]"
225
+ flex flex-col justify-center items-center
226
+ >
227
+ <div
228
+ v-if="rank.contest.banner"
229
+ >
230
+ <div
231
+ :class="[widthClass]"
232
+ mb-4
233
+ flex justify-center items-center
234
+ >
235
+ <div class="max-w-[92%]">
236
+ <img
237
+ :src="['data:image/png;base64,', rank.contest.banner?.base64].join('')"
238
+ alt="banner"
239
+ >
240
+ </div>
241
+ </div>
242
+ </div>
243
+
244
+ <div
245
+ class="title"
246
+ :class="[widthClass]"
247
+ flex justify-center
248
+ text-center text-3xl font-normal font-serif
249
+ >
250
+ <div class="max-w-[92%]">
251
+ {{ rank.contest.name }}
252
+ </div>
253
+ </div>
254
+
255
+ <div
256
+ :class="[widthClass]"
257
+ mt-4
258
+ flex flex-row justify-center
259
+ >
260
+ <div class="w-[92%]">
261
+ <div class="flex font-bold font-mono">
262
+ <div class="float-left">
263
+ {{ startTime }}<sup class="pl-0.5">{{ rank.contest.startTime.format("z") }}</sup>
264
+ </div>
265
+ <div class="flex-1">
266
+ <ContestStateBadge
267
+ :state="contestState"
268
+ :pending-time="rank.contest.getContestPendingTime(now)"
269
+ :paused-time="pausedTime"
270
+ />
271
+ </div>
272
+ <div class="float-right">
273
+ {{ endTime }}<sup class="pl-0.5">{{ rank.contest.endTime.format("z") }}</sup>
274
+ </div>
275
+ </div>
276
+
277
+ <div class="mt-2">
278
+ <Progress
279
+ v-model:rank-options="rankOptions"
280
+ :width="rank.contest.getContestProgressRatio(now)"
281
+ :state="rank.contest.getContestState(now)"
282
+ :need-scroll="true"
283
+ :rank="rank"
284
+ :elapsed-time="rank.contest.getContestElapsedTime(now)"
285
+ />
286
+ </div>
287
+
288
+ <div class="mt-2 flex font-bold font-mono">
289
+ <div class="float-left">
290
+ {{ elapsedTime }}
291
+ </div>
292
+ <div class="flex-1">
293
+ <StandingsAnnotate />
294
+ </div>
295
+ <div class="float-right">
296
+ {{ remainingTime }}
297
+ </div>
298
+ </div>
299
+
300
+ <div class="mt-4 flex">
301
+ <div class="float-left">
302
+ <SecondLevelMenu
303
+ v-model:current-item="currentGroup"
304
+ :items="groupMenuList"
305
+ query-param-name="group"
306
+ :on-change="onChangeCurrentGroup"
307
+ />
308
+ </div>
309
+ <div class="flex-1" />
310
+ <div class="float-right">
311
+ <SecondLevelMenu
312
+ v-model:current-item="currentType"
313
+ :items="typeMenuList"
314
+ :reverse-order="true"
315
+ query-param-name="type"
316
+ :on-change="onChangeCurrentType"
317
+ />
318
+ </div>
319
+ </div>
320
+ </div>
321
+ </div>
322
+
323
+ <div
324
+ mt-4
325
+ :class="[widthClass]"
326
+ flex justify-center
327
+ >
328
+ <div
329
+ class="max-w-[92%]"
330
+ >
331
+ <div
332
+ v-if="currentType === 'rank'"
333
+ >
334
+ <Standings
335
+ :rank="rank"
336
+ />
337
+ </div>
338
+
339
+ <div
340
+ v-if="currentType === 'submissions'"
341
+ class="w-[88vw]"
342
+ >
343
+ <SubmissionsTable
344
+ w-full
345
+ :rank="rank"
346
+ :submissions="rank.getSubmissions()"
347
+ />
348
+ </div>
349
+
350
+ <div
351
+ v-if="currentType === 'statistics'"
352
+ >
353
+ <Statistics
354
+ :rank="rank"
355
+ />
356
+ </div>
357
+
358
+ <div
359
+ v-if="currentType === 'export'"
360
+ >
361
+ <Export
362
+ :rank="rank"
363
+ />
364
+ </div>
365
+
366
+ <div
367
+ v-if="currentType === 'utility'"
368
+ >
369
+ <Utility
370
+ :rank="rank"
371
+ />
372
+ </div>
373
+ </div>
374
+ </div>
375
+ </div>
376
+
377
+ <OptionsModal
378
+ v-if="!isHiddenOptionsModal"
379
+ v-model:is-hidden="isHiddenOptionsModal"
380
+ v-model:rank-options="rankOptions"
381
+ :rank="rank"
382
+ />
383
+ </div>
384
+ </template>
385
+
386
+ <style scoped>
387
+ .title {
388
+ --scroll-bar: 0;
389
+ font-variant: tabular-nums;
390
+ line-height: 1.5715;
391
+ box-sizing: border-box;
392
+ position: relative;
393
+ overflow: hidden;
394
+ text-align: center;
395
+ }
396
+ </style>
@@ -0,0 +1,159 @@
1
+ <script setup lang="ts">
2
+ import type { ProblemStatistics, Rank } from "@xcpcio/core";
3
+
4
+ const props = defineProps<{
5
+ rank: Rank,
6
+ }>();
7
+
8
+ const { t } = useI18n();
9
+
10
+ const rank = computed(() => props.rank);
11
+
12
+ function getColSpan() {
13
+ let res = 3;
14
+
15
+ if (rank.value.contest.organization) {
16
+ res++;
17
+ }
18
+
19
+ if (rank.value.contest.badge) {
20
+ res++;
21
+ }
22
+
23
+ return res;
24
+ }
25
+
26
+ function getAttemptedRatio(p: ProblemStatistics): string {
27
+ if (p.submittedNum === 0) {
28
+ return "NaN";
29
+ }
30
+
31
+ const ratio = Math.floor(p.attemptedNum * 100 / p.submittedNum);
32
+ return `${ratio}%`;
33
+ }
34
+
35
+ function getAcceptedRatio(p: ProblemStatistics): string {
36
+ if (p.submittedNum === 0) {
37
+ return "NaN";
38
+ }
39
+
40
+ const ratio = Math.floor(p.acceptedNum * 100 / p.submittedNum);
41
+ return `${ratio}%`;
42
+ }
43
+
44
+ function getDict(p: ProblemStatistics): string {
45
+ if (p.attemptedNum === 0) {
46
+ return "NaN";
47
+ }
48
+
49
+ return `${p.dict}%`;
50
+ }
51
+
52
+ function getFirstSolved(p: ProblemStatistics): string {
53
+ if (p.firstSolveSubmissions.length === 0) {
54
+ return "Null";
55
+ }
56
+
57
+ const t = Math.floor(p.firstSolveSubmissions[0].timestamp / 60);
58
+ return `${t}`;
59
+ }
60
+
61
+ function getLastSolved(p: ProblemStatistics): string {
62
+ if (p.lastSolveSubmissions.length === 0) {
63
+ return "Null";
64
+ }
65
+
66
+ const t = Math.floor(p.lastSolveSubmissions[0].timestamp / 60);
67
+ return `${t}`;
68
+ }
69
+ </script>
70
+
71
+ <template>
72
+ <tr class="statistics-0">
73
+ <td class="empty" :colspan="getColSpan()" />
74
+ <td class="stnd">
75
+ <b>{{ t("standings.statistics.submitted") }}</b>
76
+ </td>
77
+ <template v-for="p in rank.contest.problems" :key="p.id">
78
+ <td class="stnd">
79
+ <b>{{ p.statistics.submittedNum }}</b>
80
+ </td>
81
+ </template>
82
+ </tr>
83
+
84
+ <tr class="statistics-1">
85
+ <td class="empty" :colspan="getColSpan()" />
86
+ <td class="stnd">
87
+ <b>{{ t("standings.statistics.attempted") }}</b>
88
+ </td>
89
+ <template v-for="p in rank.contest.problems" :key="p.id">
90
+ <td class="stnd">
91
+ <b>{{ p.statistics.attemptedNum }}</b>
92
+ <br>
93
+ <b>
94
+ ({{ getAttemptedRatio(p.statistics) }})
95
+ </b>
96
+ </td>
97
+ </template>
98
+ </tr>
99
+
100
+ <tr class="statistics-0">
101
+ <td class="empty" :colspan="getColSpan()" />
102
+ <td class="stnd">
103
+ <b>{{ t("standings.statistics.accepted") }}</b>
104
+ </td>
105
+ <template v-for="p in rank.contest.problems" :key="p.id">
106
+ <td class="stnd">
107
+ <b>{{ p.statistics.acceptedNum }}</b>
108
+ <br>
109
+ <b>
110
+ ({{ getAcceptedRatio(p.statistics) }})
111
+ </b>
112
+ </td>
113
+ </template>
114
+ </tr>
115
+
116
+ <tr class="statistics-1">
117
+ <td class="empty" :colspan="getColSpan()" />
118
+ <td class="stnd">
119
+ <b>{{ t("standings.statistics.dict") }}</b>
120
+ </td>
121
+ <template v-for="p in rank.contest.problems" :key="p.id">
122
+ <td class="stnd">
123
+ <b>{{ p.statistics.attemptedNum - p.statistics.acceptedNum }}</b>
124
+ <br>
125
+ <b>
126
+ ({{ getDict(p.statistics) }})
127
+ </b>
128
+ </td>
129
+ </template>
130
+ </tr>
131
+
132
+ <tr class="statistics-0">
133
+ <td class="empty" :colspan="getColSpan()" />
134
+ <td class="stnd">
135
+ <b>{{ t("standings.statistics.first_solved") }}</b>
136
+ </td>
137
+ <template v-for="p in rank.contest.problems" :key="p.id">
138
+ <td class="stnd">
139
+ <b>{{ getFirstSolved(p.statistics) }}</b>
140
+ </td>
141
+ </template>
142
+ </tr>
143
+
144
+ <tr class="statistics-1">
145
+ <td class="empty" :colspan="getColSpan()" />
146
+ <td class="stnd">
147
+ <b>{{ t("standings.statistics.last_solved") }}</b>
148
+ </td>
149
+ <template v-for="p in rank.contest.problems" :key="p.id">
150
+ <td class="stnd">
151
+ <b>{{ getLastSolved(p.statistics) }}</b>
152
+ </td>
153
+ </template>
154
+ </tr>
155
+ </template>
156
+
157
+ <style scoped lang="less">
158
+ @import "./Standings.less";
159
+ </style>
@@ -0,0 +1,41 @@
1
+ <script setup lang="ts">
2
+ import { ContestState } from "@xcpcio/types";
3
+
4
+ const props = defineProps<{
5
+ state: ContestState,
6
+ pendingTime?: string,
7
+ pausedTime?: string,
8
+ }>();
9
+ </script>
10
+
11
+ <template>
12
+ <div
13
+ flex flex-row items-center justify-center
14
+ >
15
+ <div
16
+ class="label"
17
+ :class="props.state"
18
+ />
19
+ <div>
20
+ {{ props.state }}
21
+ </div>
22
+
23
+ <div
24
+ v-if="props.pendingTime && props.state === ContestState.PENDING"
25
+ ml-2
26
+ >
27
+ {{ props.pendingTime }}
28
+ </div>
29
+
30
+ <div
31
+ v-if="props.pausedTime && props.state === ContestState.PAUSED"
32
+ ml-2
33
+ >
34
+ {{ props.pausedTime }}
35
+ </div>
36
+ </div>
37
+ </template>
38
+
39
+ <style scoped lang="less">
40
+ @import "./Progress.less";
41
+ </style>
@@ -0,0 +1,75 @@
1
+ <script setup lang="ts">
2
+ import type { Rank } from "@xcpcio/core";
3
+ import { rankToCodeforcesGymDAT } from "@xcpcio/core";
4
+ import { ModelSelect } from "vue-search-select";
5
+ import { useToast } from "vue-toast-notification";
6
+
7
+ const props = defineProps<{
8
+ rank: Rank,
9
+ }>();
10
+
11
+ const $toast = useToast();
12
+
13
+ const { copy, isSupported } = useClipboard();
14
+
15
+ const rank = computed(() => props.rank);
16
+
17
+ const currentItem = ref({ value: "cf-dat", text: "Codeforces Gym Ghost(dat)" });
18
+ const options = ref([
19
+ { value: "cf-dat", text: "Codeforces Gym Ghost(dat)" },
20
+ ]);
21
+
22
+ function onClickForCfDatDownload() {
23
+ const dat = rankToCodeforcesGymDAT(rank.value);
24
+ downloadSingleFile(dat, "contest.dat");
25
+ }
26
+
27
+ function onClickForCfDatCopyToClipboard() {
28
+ if (!isSupported.value) {
29
+ $toast.warning("clipboard is not supported");
30
+ return;
31
+ }
32
+
33
+ const dat = rankToCodeforcesGymDAT(rank.value);
34
+ copy(dat);
35
+
36
+ $toast.success("Copy Success");
37
+ }
38
+ </script>
39
+
40
+ <template>
41
+ <div
42
+ flex flex-col
43
+ >
44
+ <div
45
+ w-160
46
+ font-bold
47
+ >
48
+ <ModelSelect
49
+ v-model="currentItem"
50
+ :options="options"
51
+ placeholder="Export Type"
52
+ />
53
+ </div>
54
+
55
+ <div
56
+ v-if="currentItem.value === 'cf-dat'"
57
+ mt-8
58
+ flex flex-row justify-center gap-4
59
+ >
60
+ <button
61
+ btn
62
+ @click="onClickForCfDatDownload()"
63
+ >
64
+ Download
65
+ </button>
66
+
67
+ <button
68
+ btn
69
+ @click="onClickForCfDatCopyToClipboard()"
70
+ >
71
+ Copy to Clipboard
72
+ </button>
73
+ </div>
74
+ </div>
75
+ </template>