@xcpcio/board-app 0.29.0 → 0.31.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 (46) hide show
  1. package/dist/assets/Board-3c31679a.css +1 -0
  2. package/dist/assets/Board-ca17ed93.js +3 -0
  3. package/dist/assets/DataSourceInput.vue_vue_type_script_setup_true_lang-39c4cbe1.js +1 -0
  4. package/dist/assets/{TheInput.vue_vue_type_script_setup_true_lang-1dafe5cb.js → TheInput.vue_vue_type_script_setup_true_lang-34252075.js} +1 -1
  5. package/dist/assets/{_...all_-d54370e4.js → _...all_-69526490.js} +1 -1
  6. package/dist/assets/{_name_-c7264d89.js → _name_-ec4f9242.js} +1 -1
  7. package/dist/assets/{about-55b7ba92.js → about-07cc8f15.js} +1 -1
  8. package/dist/assets/board-82816108.js +1 -0
  9. package/dist/assets/{board-layout-e4e0bae6.js → board-layout-5322ed1f.js} +1 -1
  10. package/dist/assets/{en-644e039f.js → en-06ebe6c4.js} +1 -1
  11. package/dist/assets/{headless-3466779c.js → headless-21807d6b.js} +1 -1
  12. package/dist/assets/{home-37e85146.js → home-93d9a706.js} +1 -1
  13. package/dist/assets/index-096b8dfd.js +1 -0
  14. package/dist/assets/{index-a0a089d2.css → index-2935d261.css} +1 -1
  15. package/dist/assets/index-a835bec8.js +1 -0
  16. package/dist/assets/{index-f1d19db1.js → index-cefad7d4.js} +43 -43
  17. package/dist/assets/{index-layout-c91f7cfd.js → index-layout-2da78a05.js} +1 -1
  18. package/dist/assets/{test-133c6df2.js → test-f6a5238f.js} +1 -1
  19. package/dist/assets/{useQueryBoardData-c0567554.js → useQueryBoardData-4ec8c75f.js} +1 -1
  20. package/dist/assets/{user-6ae7ff90.js → user-33c76882.js} +1 -1
  21. package/dist/assets/{virtual_pwa-register-7ea4486e.js → virtual_pwa-register-1f370632.js} +1 -1
  22. package/dist/assets/{zh-CN-44b801f0.js → zh-CN-582540fd.js} +1 -1
  23. package/dist/index.html +3 -3
  24. package/dist/sw.js +1 -1
  25. package/package.json +3 -3
  26. package/src/auto-imports.d.ts +3 -0
  27. package/src/components/Balloon.vue +2 -1
  28. package/src/components/Countdown.vue +104 -0
  29. package/src/components/CustomCountdown.vue +25 -0
  30. package/src/components/board/Board.vue +6 -0
  31. package/src/components/board/ProblemInfoModal.vue +6 -0
  32. package/src/components/board/SubmissionsTable.vue +260 -58
  33. package/src/components/board/TeamAwards.vue +49 -19
  34. package/src/components/board/TeamInfoModal.vue +20 -7
  35. package/src/components/board/Utility.vue +17 -0
  36. package/src/components/flowbite/Tooltip.vue +1 -1
  37. package/src/components.d.ts +2 -0
  38. package/src/composables/constant.ts +1 -0
  39. package/src/pages/countdown/index.vue +10 -0
  40. package/src/pages/index.vue +13 -6
  41. package/src/styles/submission-status-filter.css +123 -0
  42. package/dist/assets/Board-2cad79eb.css +0 -1
  43. package/dist/assets/Board-9442407d.js +0 -3
  44. package/dist/assets/DataSourceInput.vue_vue_type_script_setup_true_lang-d16f2a12.js +0 -1
  45. package/dist/assets/board-88a98e6a.js +0 -1
  46. package/dist/assets/index-bc173751.js +0 -1
@@ -1,23 +1,210 @@
1
1
  <script setup lang="ts">
2
- import type { Rank, Submissions } from "@xcpcio/core";
2
+ import { MultiSelect } from "vue-search-select";
3
+
4
+ import type { Rank, SelectOptionItem, Submissions } from "@xcpcio/core";
3
5
  import { Submission } from "@xcpcio/core";
6
+ import type { SubmissionStatus } from "@xcpcio/types";
4
7
  import { SubmissionStatusToString } from "@xcpcio/types";
5
8
 
6
9
  import { Pagination } from "~/composables/pagination";
7
10
 
11
+ import "~/styles/submission-status-filter.css";
12
+
13
+ interface FilterOptions {
14
+ orgNames: string[];
15
+ teamIds: string[];
16
+ languages: string[];
17
+ statuses: SubmissionStatus[];
18
+ }
19
+
20
+ interface EnableFilterOptions {
21
+ organization?: boolean,
22
+ team?: boolean,
23
+ language?: boolean,
24
+ status?: boolean,
25
+ }
26
+
8
27
  const props = defineProps<{
9
28
  rank: Rank,
10
29
  submissions: Submissions,
11
30
  pageSize?: number,
12
31
  removeBorder?: boolean,
32
+ enableFilter?: EnableFilterOptions,
13
33
  }>();
14
34
 
15
35
  const rank = computed(() => props.rank);
36
+ const enableFilter = computed(() => props.enableFilter);
37
+ const enableFilterButton = computed(() => {
38
+ if (!enableFilter.value) {
39
+ return false;
40
+ }
41
+
42
+ for (const [_k, v] of Object.entries(enableFilter.value)) {
43
+ if (v === true) {
44
+ return true;
45
+ }
46
+ }
47
+
48
+ return false;
49
+ });
50
+
51
+ const filterOptions = ref<FilterOptions>({
52
+ orgNames: [],
53
+ teamIds: [],
54
+ languages: [],
55
+ statuses: [],
56
+ });
57
+
58
+ const orgOptions = computed(() => {
59
+ const res = rank.value.organizations.map((o) => {
60
+ return {
61
+ value: o,
62
+ text: o,
63
+ };
64
+ });
65
+
66
+ return res;
67
+ });
68
+
69
+ const orgSelectedItems = ref<Array<SelectOptionItem>>([]);
70
+ const orgLastSelectItem = ref({});
71
+
72
+ function orgOnSelect(selectedItems: Array<SelectOptionItem>, lastSelectItem: SelectOptionItem) {
73
+ orgSelectedItems.value = selectedItems;
74
+ orgLastSelectItem.value = lastSelectItem;
75
+ }
76
+
77
+ const teamsOptions = computed(() => {
78
+ const res = rank.value.originTeams.map((t) => {
79
+ return {
80
+ value: t.id,
81
+ text: t.organization ? `${t.name} - ${t.organization}` : t.name,
82
+ };
83
+ });
84
+
85
+ return res;
86
+ });
87
+
88
+ const teamsSelectedItems = ref<Array<SelectOptionItem>>([]);
89
+ const teamsLastSelectItem = ref({});
90
+
91
+ function teamsOnSelect(selectedItems: Array<SelectOptionItem>, lastSelectItem: SelectOptionItem) {
92
+ teamsSelectedItems.value = selectedItems;
93
+ teamsLastSelectItem.value = lastSelectItem;
94
+ }
95
+
96
+ const languageOptions = computed(() => {
97
+ const languages = rank.value.languages;
98
+
99
+ const res = languages.map((l) => {
100
+ return {
101
+ value: l,
102
+ text: l,
103
+ };
104
+ });
105
+
106
+ return res;
107
+ });
108
+
109
+ const languageSelectedItems = ref<Array<SelectOptionItem>>([]);
110
+ const languageLastSelectItem = ref({});
111
+
112
+ function languageOnSelect(selectedItems: Array<SelectOptionItem>, lastSelectItem: SelectOptionItem) {
113
+ languageSelectedItems.value = selectedItems;
114
+ languageLastSelectItem.value = lastSelectItem;
115
+ }
116
+
117
+ const statusOptions = computed(() => {
118
+ const statuses = rank.value.statuses;
119
+
120
+ const res = statuses.map((s) => {
121
+ return {
122
+ value: s,
123
+ text: SubmissionStatusToString[s],
124
+ };
125
+ });
126
+
127
+ return res;
128
+ });
129
+
130
+ const statusSelectedItems = ref<Array<SelectOptionItem>>([]);
131
+ const statusLastSelectItem = ref({});
132
+
133
+ function statusOnSelect(selectedItems: Array<SelectOptionItem>, lastSelectItem: SelectOptionItem) {
134
+ statusSelectedItems.value = selectedItems;
135
+ statusLastSelectItem.value = lastSelectItem;
136
+ }
137
+
138
+ function statusCustomAttr(option: SelectOptionItem) {
139
+ return option.value.toString();
140
+ }
141
+
16
142
  const submissions = computed(() => {
17
- const s = props.submissions;
18
- return s.sort(Submission.compare).reverse();
143
+ const ss = props.submissions;
144
+ return ss.filter((s) => {
145
+ const o = filterOptions.value;
146
+
147
+ if (o.orgNames.length === 0
148
+ && o.teamIds.length === 0
149
+ && o.languages.length === 0
150
+ && o.statuses.length === 0
151
+ ) {
152
+ return true;
153
+ }
154
+
155
+ if (o.teamIds.length > 0) {
156
+ for (const t of o.teamIds) {
157
+ if (t === s.teamId) {
158
+ return true;
159
+ }
160
+ }
161
+ }
162
+
163
+ if (o.orgNames.length > 0) {
164
+ const team = rank.value.teamsMap.get(s.teamId);
165
+ for (const n of o.orgNames) {
166
+ if (n === team?.organization) {
167
+ return true;
168
+ }
169
+ }
170
+ }
171
+
172
+ if (o.languages.length > 0) {
173
+ for (const l of o.languages) {
174
+ if (l === s.language) {
175
+ return true;
176
+ }
177
+ }
178
+ }
179
+
180
+ if (o.statuses.length > 0) {
181
+ for (const sta of o.statuses) {
182
+ if (sta === s.status) {
183
+ return true;
184
+ }
185
+ }
186
+ }
187
+
188
+ return false;
189
+ }).sort(Submission.compare).reverse();
19
190
  });
20
191
 
192
+ function onFilter() {
193
+ const newFilterOptions: FilterOptions = {
194
+ orgNames: [],
195
+ teamIds: [],
196
+ languages: [],
197
+ statuses: [],
198
+ };
199
+
200
+ newFilterOptions.orgNames = orgSelectedItems.value.map(o => o.value);
201
+ newFilterOptions.teamIds = teamsSelectedItems.value.map(t => t.value);
202
+ newFilterOptions.languages = languageSelectedItems.value.map(l => l.value);
203
+ newFilterOptions.statuses = statusSelectedItems.value.map(s => s.value as SubmissionStatus);
204
+
205
+ filterOptions.value = newFilterOptions;
206
+ }
207
+
21
208
  const p = ref(new Pagination());
22
209
 
23
210
  p.value.currentPage = 0;
@@ -69,79 +256,94 @@ function getProblemLabelColorStyle(s: Submission) {
69
256
  <template>
70
257
  <section>
71
258
  <div
72
- class="mx-auto w-full"
259
+ mx-auto w-full
73
260
  px-4
74
261
  >
75
262
  <div
76
- class="relative overflow-hidden bg-white dark:bg-gray-800"
263
+ relative overflow-hidden
264
+ bg-white dark:bg-gray-800
77
265
  :class="{
78
266
  'shadow-md': props.removeBorder !== true,
79
267
  'sm:rounded-sm': props.removeBorder !== true,
80
268
  }"
81
269
  >
82
270
  <div
83
- class="lg:flex-row lg:items-center lg:justify-between space-y-3 lg:space-x-4 lg:space-y-0"
271
+ space-y-3
84
272
  flex flex-col
85
273
  px-4 py-3
274
+ lg:flex-row lg:items-center lg:justify-between
275
+ lg:space-x-4 lg:space-y-0
86
276
  >
87
277
  <div
88
- class="flex flex-shrink-0 flex-col md:flex-row md:items-center lg:justify-end space-y-3 md:space-x-3 md:space-y-0"
278
+ flex flex-shrink-0 flex-col
279
+ md:flex-row md:items-center
280
+ lg:justify-end space-y-3
281
+ md:space-x-3 md:space-y-0
89
282
  >
90
- <button
91
- v-if="notShowing"
92
- type="button"
93
- class="flex items-center justify-center rounded-lg bg-primary-700 px-4 py-2 text-sm font-medium text-white dark:bg-primary-600 hover:bg-primary-800 focus:outline-none focus:ring-4 focus:ring-primary-300 dark:hover:bg-primary-700 dark:focus:ring-primary-800"
283
+ <div
284
+ v-if="rank.contest.organization && enableFilter?.organization"
285
+ w-48
94
286
  >
95
- <svg
96
- class="mr-2 h-3.5 w-3.5"
97
- fill="currentColor"
98
- viewbox="0 0 20 20"
99
- xmlns="http://www.w3.org/2000/svg"
100
- aria-hidden="true"
101
- >
102
- <path
103
- clip-rule="evenodd"
104
- fill-rule="evenodd"
105
- d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z"
106
- />
107
- </svg>
108
- Add new product
109
- </button>
110
-
111
- <button
112
- v-if="notShowing"
113
- type="button"
114
- class="flex flex-shrink-0 items-center justify-center border border-gray-200 rounded-lg bg-white px-3 py-2 text-sm font-medium text-gray-900 focus:z-10 dark:border-gray-600 dark:bg-gray-800 hover:bg-gray-100 dark:text-gray-400 hover:text-primary-700 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:hover:bg-gray-700 dark:hover:text-white dark:focus:ring-gray-700"
287
+ <MultiSelect
288
+ :options="orgOptions"
289
+ :selected-options="orgSelectedItems"
290
+ :placeholder="rank.contest.organization"
291
+ @select="orgOnSelect"
292
+ />
293
+ </div>
294
+
295
+ <div
296
+ v-if="enableFilter?.team"
297
+ w-48
115
298
  >
116
- <svg
117
- class="mr-2 h-4 w-4"
118
- xmlns="http://www.w3.org/2000/svg"
119
- aria-hidden="true"
120
- fill="none"
121
- viewbox="0 0 24 24"
122
- stroke-width="1.5"
123
- stroke="currentColor"
124
- >
125
- <path
126
- stroke-linecap="round"
127
- stroke-linejoin="round"
128
- d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"
129
- />
130
- </svg>
131
- Update stocks 1/250
132
- </button>
133
-
134
- <button
135
- v-if="notShowing"
136
- type="button"
137
- class="flex flex-shrink-0 items-center justify-center border border-gray-200 rounded-lg bg-white px-3 py-2 text-sm font-medium text-gray-900 focus:z-10 dark:border-gray-600 dark:bg-gray-800 hover:bg-gray-100 dark:text-gray-400 hover:text-primary-700 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:hover:bg-gray-700 dark:hover:text-white dark:focus:ring-gray-700"
299
+ <MultiSelect
300
+ :options="teamsOptions"
301
+ :selected-options="teamsSelectedItems"
302
+ placeholder="Team"
303
+ @select="teamsOnSelect"
304
+ />
305
+ </div>
306
+
307
+ <div
308
+ v-if="enableFilter?.status"
309
+ w-64
310
+ >
311
+ <MultiSelect
312
+ :options="statusOptions"
313
+ :selected-options="statusSelectedItems"
314
+ placeholder="Status"
315
+ :custom-attr="statusCustomAttr"
316
+ @select="statusOnSelect"
317
+ />
318
+ </div>
319
+
320
+ <div
321
+ v-if="enableFilter?.language && languageOptions.length > 0"
322
+ w-48
138
323
  >
139
- <div
140
- i-pajamas-export
141
- class="mr-2 h-4 w-4"
324
+ <MultiSelect
325
+ :options="languageOptions"
326
+ :selected-options="languageSelectedItems"
327
+ placeholder="Language"
328
+ @select="languageOnSelect"
142
329
  />
143
- Export
144
- </button>
330
+ </div>
331
+
332
+ <div
333
+ v-if="enableFilterButton"
334
+ >
335
+ <button
336
+ type="button"
337
+ class="flex flex-shrink-0 items-center justify-center border border-gray-200 rounded-lg bg-white px-3 py-2 text-sm font-medium text-gray-900 focus:z-10 dark:border-gray-600 dark:bg-gray-800 hover:bg-gray-100 dark:text-gray-400 hover:text-primary-700 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:hover:bg-gray-700 dark:hover:text-white dark:focus:ring-gray-700"
338
+ @click="onFilter"
339
+ >
340
+ <div
341
+ i-material-symbols-search
342
+ mr-1 h-5 w-5
343
+ />
344
+ Filter
345
+ </button>
346
+ </div>
145
347
  </div>
146
348
  </div>
147
349
 
@@ -53,6 +53,28 @@ const medal = computed((): Medal | null => {
53
53
 
54
54
  return null;
55
55
  });
56
+
57
+ const place = computed(() => {
58
+ let r = team.value.rank;
59
+
60
+ if (rank.value.contest.organization) {
61
+ r = team.value.organizationRank;
62
+ }
63
+
64
+ if (r === 1) {
65
+ return "1st Place";
66
+ }
67
+
68
+ if (r === 2) {
69
+ return "2nd Place";
70
+ }
71
+
72
+ if (r === 3) {
73
+ return "3rd Place";
74
+ }
75
+
76
+ return "";
77
+ });
56
78
  </script>
57
79
 
58
80
  <template>
@@ -61,31 +83,39 @@ const medal = computed((): Medal | null => {
61
83
  text-xl
62
84
  font-mono font-semibold italic
63
85
  flex flex-col gap-2
64
- justify-center
86
+ justify-center items-center
65
87
  >
66
- <div>
67
- Team Rank: {{ team.rank }}
68
- </div>
69
-
70
88
  <div
71
- v-if="team.organization && team.organizationRank !== -1"
89
+ flex flex-col
90
+ items-start
91
+ gap-y-2
72
92
  >
73
- {{ rank.contest.organization }} Rank: {{ team.organizationRank }}
74
- </div>
93
+ <div
94
+ v-if="medal"
95
+ >
96
+ {{ medal.text }} Medalist
97
+ </div>
75
98
 
76
- <div
77
- v-if="firstSolvedProblems.length"
78
- flex flex-col gap-2
79
- >
80
- <div>
81
- First Solved Problem {{ firstSolvedProblems }}
99
+ <div
100
+ v-if="place.length > 0"
101
+ >
102
+ {{ place }}
82
103
  </div>
83
- </div>
84
104
 
85
- <div
86
- v-if="medal"
87
- >
88
- {{ medal.text }} Medal
105
+ <div
106
+ v-if="firstSolvedProblems.length"
107
+ flex flex-col gap-2
108
+ >
109
+ <div>
110
+ First Solved Problem {{ firstSolvedProblems }}
111
+ </div>
112
+ </div>
113
+
114
+ <div
115
+ v-if="team.solvedProblemNum > 0"
116
+ >
117
+ Solved {{ team.solvedProblemNum }} {{ team.solvedProblemNum > 1 ? 'Problems' : 'Problem' }}
118
+ </div>
89
119
  </div>
90
120
  </div>
91
121
  </template>
@@ -80,13 +80,26 @@ const types = [TYPE_SUBMISSIONS, TYPE_STATISTICS, TYPE_AWARDS];
80
80
  width-class="h-8 w-8"
81
81
  />
82
82
 
83
- <h3
84
- text-gray-900 dark:text-white
85
- text-2xl
86
- font-sans font-semibold italic
87
- >
88
- {{ headerTitle }}
89
- </h3>
83
+ <Tooltip>
84
+ <h3
85
+ text-gray-900 dark:text-white
86
+ text-2xl
87
+ font-sans font-semibold italic
88
+ >
89
+ {{ headerTitle }}
90
+ </h3>
91
+
92
+ <template #popper>
93
+ <div
94
+ flex flex-col
95
+ justify-start items-start
96
+ >
97
+ <div>
98
+ TeamID: {{ team.id }}
99
+ </div>
100
+ </div>
101
+ </template>
102
+ </Tooltip>
90
103
  </div>
91
104
 
92
105
  <ModalMenu
@@ -15,6 +15,10 @@ const resolverUrl = computed(() => {
15
15
  function goBalloon() {
16
16
  router.push(`/balloon/?data-source=${route.path}`);
17
17
  }
18
+
19
+ function goCountdown() {
20
+ router.push(`/countdown/?data-source=${route.path}`);
21
+ }
18
22
  </script>
19
23
 
20
24
  <template>
@@ -39,6 +43,19 @@ function goBalloon() {
39
43
  {{ t('type_menu.balloon') }}
40
44
  </button>
41
45
 
46
+ <button
47
+ btn
48
+ title="Countdown"
49
+ @click="goCountdown"
50
+ >
51
+ {{ t('type_menu.countdown') }}
52
+ </button>
53
+ </div>
54
+
55
+ <div
56
+ w-full
57
+ flex mt-4 gap-4
58
+ >
42
59
  <button
43
60
  btn
44
61
  title="Submissions"
@@ -42,7 +42,7 @@ onMounted(() => {
42
42
  ref="tooltipTargetEl"
43
43
  role="tooltip"
44
44
  class="tooltip inline-block absolute invisible px-3 py-2 transition-opacity duration-300 shadow-sm opacity-0"
45
- z-9999
45
+ z-99999
46
46
  rounded
47
47
  text-base text-white font-medium
48
48
  bg-gray-900 dark:bg-gray-700
@@ -15,8 +15,10 @@ declare module 'vue' {
15
15
  BottomStatistics: typeof import('./components/board/BottomStatistics.vue')['default']
16
16
  ContestIndexUI: typeof import('./components/ContestIndexUI.vue')['default']
17
17
  ContestStateBadge: typeof import('./components/board/ContestStateBadge.vue')['default']
18
+ Countdown: typeof import('./components/Countdown.vue')['default']
18
19
  CustomBalloon: typeof import('./components/CustomBalloon.vue')['default']
19
20
  CustomBoard: typeof import('./components/CustomBoard.vue')['default']
21
+ CustomCountdown: typeof import('./components/CustomCountdown.vue')['default']
20
22
  DataSourceInput: typeof import('./components/DataSourceInput.vue')['default']
21
23
  Export: typeof import('./components/board/Export.vue')['default']
22
24
  Footer: typeof import('./components/Footer.vue')['default']
@@ -1,3 +1,4 @@
1
1
  export const TITLE_SUFFIX = "Board - XCPCIO";
2
2
  export const BALLOON_TITLE_SUFFIX = "Balloon - XCPCIO";
3
3
  export const RESOLVER_TITLE_SUFFIX = "Resolver - XCPCIO";
4
+ export const COUNTDOWN_TITLE_SUFFIX = "Countdown - XCPCIO";
@@ -0,0 +1,10 @@
1
+ <template>
2
+ <div>
3
+ <CustomCountdown />
4
+ </div>
5
+ </template>
6
+
7
+ <route lang="yaml">
8
+ meta:
9
+ layout: headless
10
+ </route>
@@ -79,15 +79,22 @@ watch(searchText, () => {
79
79
  >
80
80
  <div>
81
81
  <div
82
- v-if="isFetching"
82
+ v-if="isFetching || error"
83
+ mt-4 mb-4
83
84
  class="sm:w-[1000px] lg:w-screen"
84
- flex justify-center
85
+ flex justify-center items-center
85
86
  >
86
- {{ t("common.loading") }}...
87
- </div>
87
+ <div
88
+ v-if="isFetching"
89
+ >
90
+ {{ t("common.loading") }}...
91
+ </div>
88
92
 
89
- <div v-if="error">
90
- {{ error }}
93
+ <div
94
+ v-if="error"
95
+ >
96
+ {{ error }}
97
+ </div>
91
98
  </div>
92
99
 
93
100
  <div