@xcpcio/board-app 0.6.3 → 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,108 @@
1
+ <script setup lang="ts">
2
+ import type { Pagination } from "~/composables/pagination";
3
+
4
+ const props = defineProps<{
5
+ pagination: Pagination,
6
+ }>();
7
+ const emit = defineEmits(["update:pagination"]);
8
+
9
+ const p = computed({
10
+ get() {
11
+ return props.pagination;
12
+ },
13
+ set(value) {
14
+ emit("update:pagination", value);
15
+ },
16
+ });
17
+
18
+ const class_pagination_ix = "flex items-center justify-center px-3 py-2 text-sm text-gray-500 bg-white border border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white leading-tight";
19
+ </script>
20
+
21
+ <template>
22
+ <nav class="flex flex-col items-start justify-between p-4 md:flex-row md:items-center space-y-3 md:space-y-0" aria-label="Table navigation">
23
+ <span class="text-sm font-normal text-gray-500 dark:text-gray-400">
24
+ Showing
25
+ <span class="font-semibold text-gray-900 dark:text-white">{{ p.currentLeft }}-{{ Math.max(0, p.currentRight - 1) }}</span>
26
+ of
27
+ <span class="font-semibold text-gray-900 dark:text-white">{{ p.totalSize }}</span>
28
+ </span>
29
+
30
+ <ul class="inline-flex items-stretch font-mono -space-x-px">
31
+ <li>
32
+ <a
33
+ class="ml-0 h-full flex items-center justify-center border border-gray-300 rounded-l-lg bg-white px-3 py-1.5 text-gray-500 dark:border-gray-700 dark:bg-gray-800 hover:bg-gray-100 dark:text-gray-400 hover:text-gray-700 dark:hover:bg-gray-700 dark:hover:text-white"
34
+ hover="cursor-pointer"
35
+ @click="p.onPageChange({ diff: -1 })"
36
+ >
37
+ <span class="sr-only">Previous</span>
38
+ <svg class="h-5 w-5" aria-hidden="true" fill="currentColor" viewbox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
39
+ <path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
40
+ </svg>
41
+ </a>
42
+ </li>
43
+
44
+ <li v-if="p.currentPage !== 0">
45
+ <a
46
+ :class="class_pagination_ix"
47
+ hover="cursor-pointer"
48
+ @click=" p.onPageChange({ to: 0 })"
49
+ >
50
+ 1
51
+ </a>
52
+ </li>
53
+
54
+ <li v-for="pn in p.leftDecrPage" :key="pn">
55
+ <a
56
+ :class="class_pagination_ix"
57
+ hover="cursor-pointer"
58
+ @click="p.onPageChange({ to: pn })"
59
+ >
60
+ {{ pn + 1 }}
61
+ </a>
62
+ </li>
63
+
64
+ <li>
65
+ <a
66
+ aria-current="page"
67
+ class="z-10 flex items-center justify-center border border-primary-300 bg-primary-50 px-3 py-2 text-sm leading-tight text-primary-600 dark:border-gray-700 dark:bg-gray-700 hover:bg-primary-100 dark:text-white hover:text-primary-700"
68
+ hover="cursor-pointer"
69
+ >
70
+ {{ p.currentPage + 1 }}
71
+ </a>
72
+ </li>
73
+
74
+ <li v-for="pn in p.rightIncrPage" :key="pn">
75
+ <a
76
+ hover="cursor-pointer"
77
+ :class="class_pagination_ix"
78
+ @click="p.onPageChange({ to: pn })"
79
+ >
80
+ {{ pn + 1 }}
81
+ </a>
82
+ </li>
83
+
84
+ <li v-if="p.currentPage !== p.totalPage - 1 && p.totalPage > 1">
85
+ <a
86
+ hover="cursor-pointer"
87
+ :class="class_pagination_ix"
88
+ @click="p.onPageChange({ to: p.totalPage - 1 })"
89
+ >
90
+ {{ p.totalPage }}
91
+ </a>
92
+ </li>
93
+
94
+ <li>
95
+ <a
96
+ class="h-full flex items-center justify-center border border-gray-300 rounded-r-lg bg-white px-3 py-1.5 leading-tight text-gray-500 dark:border-gray-700 dark:bg-gray-800 hover:bg-gray-100 dark:text-gray-400 hover:text-gray-700 dark:hover:bg-gray-700 dark:hover:text-white"
97
+ hover="cursor-pointer"
98
+ @click="p.onPageChange({ diff: 1 })"
99
+ >
100
+ <span class="sr-only">Next</span>
101
+ <svg class="h-5 w-5" aria-hidden="true" fill="currentColor" viewbox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
102
+ <path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
103
+ </svg>
104
+ </a>
105
+ </li>
106
+ </ul>
107
+ </nav>
108
+ </template>
@@ -0,0 +1,44 @@
1
+ /* eslint-disable */
2
+ /* prettier-ignore */
3
+ // @ts-nocheck
4
+ // Generated by unplugin-vue-components
5
+ // Read more: https://github.com/vuejs/core/pull/3399
6
+ export {}
7
+
8
+ declare module 'vue' {
9
+ export interface GlobalComponents {
10
+ Balloon: typeof import('./components/board/Balloon.vue')['default']
11
+ Board: typeof import('./components/board/Board.vue')['default']
12
+ BottomStatistics: typeof import('./components/board/BottomStatistics.vue')['default']
13
+ ContestIndex: typeof import('./components/ContestIndex.vue')['default']
14
+ ContestStateBadge: typeof import('./components/board/ContestStateBadge.vue')['default']
15
+ Export: typeof import('./components/board/Export.vue')['default']
16
+ Footer: typeof import('./components/Footer.vue')['default']
17
+ GirlIcon: typeof import('./components/icon/GirlIcon.vue')['default']
18
+ GoBack: typeof import('./components/GoBack.vue')['default']
19
+ Modal: typeof import('./components/board/Modal.vue')['default']
20
+ ModalMenu: typeof import('./components/board/ModalMenu.vue')['default']
21
+ NavBar: typeof import('./components/NavBar.vue')['default']
22
+ OptionsModal: typeof import('./components/board/OptionsModal.vue')['default']
23
+ Progress: typeof import('./components/board/Progress.vue')['default']
24
+ RightArrowIcon: typeof import('./components/icon/RightArrowIcon.vue')['default']
25
+ RouterLink: typeof import('vue-router')['RouterLink']
26
+ RouterView: typeof import('vue-router')['RouterView']
27
+ SearchInput: typeof import('./components/SearchInput.vue')['default']
28
+ SecondLevelMenu: typeof import('./components/board/SecondLevelMenu.vue')['default']
29
+ Standings: typeof import('./components/board/Standings.vue')['default']
30
+ StandingsAnnotate: typeof import('./components/board/StandingsAnnotate.vue')['default']
31
+ StarIcon: typeof import('./components/icon/StarIcon.vue')['default']
32
+ Statistics: typeof import('./components/board/Statistics.vue')['default']
33
+ SubmissionsTable: typeof import('./components/board/SubmissionsTable.vue')['default']
34
+ SubmissionsTableModal: typeof import('./components/board/SubmissionsTableModal.vue')['default']
35
+ TablePagination: typeof import('./components/table/TablePagination.vue')['default']
36
+ TeamAwards: typeof import('./components/board/TeamAwards.vue')['default']
37
+ TeamInfoModal: typeof import('./components/board/TeamInfoModal.vue')['default']
38
+ TeamProblemBlock: typeof import('./components/board/TeamProblemBlock.vue')['default']
39
+ TeamUI: typeof import('./components/board/TeamUI.vue')['default']
40
+ TheCounter: typeof import('./components/TheCounter.vue')['default']
41
+ TheInput: typeof import('./components/TheInput.vue')['default']
42
+ Utility: typeof import('./components/board/Utility.vue')['default']
43
+ }
44
+ }
@@ -0,0 +1,4 @@
1
+ // these APIs are auto-imported from @vueuse/core
2
+ export const isDark = useDark();
3
+ export const toggleDark = useToggle(isDark);
4
+ export const preferredDark = usePreferredDark();
@@ -0,0 +1,81 @@
1
+ export class Pagination {
2
+ totalSize: number;
3
+
4
+ currentPage: number;
5
+ pageSize: number;
6
+
7
+ constructor() {
8
+ this.totalSize = 0;
9
+
10
+ this.currentPage = 0;
11
+ this.pageSize = 16;
12
+ }
13
+
14
+ get totalPage() {
15
+ return Math.floor((this.totalSize + this.pageSize - 1) / this.pageSize);
16
+ }
17
+
18
+ get currentLeft() {
19
+ return this.currentPage * this.pageSize;
20
+ }
21
+
22
+ get currentRight() {
23
+ return Math.min(this.totalSize, (this.currentPage + 1) * this.pageSize);
24
+ }
25
+
26
+ get leftDecrPage() {
27
+ const res = [];
28
+
29
+ let step = 1;
30
+ let cur = this.currentPage - step;
31
+
32
+ while (cur > 0) {
33
+ res.push(cur);
34
+
35
+ step = step << 1;
36
+ cur -= step;
37
+ }
38
+
39
+ return res.reverse();
40
+ }
41
+
42
+ get rightIncrPage() {
43
+ const res = [];
44
+
45
+ let step = 1;
46
+ let cur = this.currentPage + step;
47
+
48
+ while (cur + 1 < this.totalPage) {
49
+ res.push(cur);
50
+
51
+ step = step << 1;
52
+ cur += step;
53
+ }
54
+
55
+ return res;
56
+ }
57
+
58
+ onPageChange(options: {
59
+ to?: number,
60
+ diff?: number,
61
+ }) {
62
+ const totalPage = this.totalPage;
63
+
64
+ let to = this.currentPage;
65
+
66
+ if (options?.to !== undefined) {
67
+ to = options.to;
68
+ }
69
+
70
+ if (options?.diff !== undefined) {
71
+ const diff = options.diff;
72
+ to = to + diff;
73
+ }
74
+
75
+ if (to < 0 || to >= totalPage) {
76
+ return;
77
+ }
78
+
79
+ this.currentPage = to;
80
+ }
81
+ }
@@ -0,0 +1,280 @@
1
+ import { type Rank, type Team, getTimeDiff } from "@xcpcio/core";
2
+
3
+ function getChartObj(title: string, xText: string, yText: string, cat: any, series: any, colors: any) {
4
+ return {
5
+ colors,
6
+ chart:
7
+ window.innerWidth < 992
8
+ ? {
9
+ type: "bar",
10
+ backgroundColor: "transparent",
11
+ }
12
+ : {
13
+ type: "column",
14
+ backgroundColor: "transparent",
15
+ height: "420px",
16
+ },
17
+ title: {
18
+ text: title,
19
+ },
20
+ xAxis: {
21
+ categories: cat,
22
+ labels: {
23
+ style: {
24
+ fontSize: "16px",
25
+ },
26
+ },
27
+ title: {
28
+ text: xText,
29
+ style: {
30
+ fontSize: "16px",
31
+ },
32
+ },
33
+ },
34
+ yAxis: {
35
+ min: 0,
36
+ title: {
37
+ text: yText,
38
+ style: {
39
+ fontSize: "16px",
40
+ height: "320px",
41
+ },
42
+ },
43
+ stackLabels: {
44
+ enabled: true,
45
+ style: {
46
+ fontSize: "16px",
47
+ },
48
+ },
49
+ },
50
+ tooltip: {
51
+ enabled: true,
52
+ headerFormat: "",
53
+ pointFormat: "{series.name}:{point.y}",
54
+ },
55
+ plotOptions: {
56
+ bar: {
57
+ stacking: "normal",
58
+ },
59
+ column: {
60
+ stacking: "normal",
61
+ },
62
+ },
63
+ credits: {
64
+ enabled: false,
65
+ },
66
+ series,
67
+ };
68
+ }
69
+
70
+ export function getProblemChart(rank: Rank) {
71
+ const problemList: any = [];
72
+
73
+ rank.contest.problems.forEach((p, ix) => {
74
+ problemList.push({
75
+ problem_id: p.label,
76
+ index: ix,
77
+ solved: p.statistics.acceptedNum,
78
+ });
79
+ });
80
+
81
+ problemList.sort((a: any, b: any) => {
82
+ if (a.solved > b.solved) {
83
+ return -1;
84
+ }
85
+
86
+ if (a.solved < b.solved) {
87
+ return 1;
88
+ }
89
+
90
+ if (a.index < b.index) {
91
+ return -1;
92
+ }
93
+
94
+ if (a.index > b.index) {
95
+ return 1;
96
+ }
97
+
98
+ return 0;
99
+ });
100
+
101
+ const { cat, series, colors } = (() => {
102
+ const cat: any = [];
103
+ const data: any = [];
104
+
105
+ problemList.forEach((problem: any) => {
106
+ cat.push(problem.problem_id);
107
+ data.push(problem.solved);
108
+ });
109
+
110
+ const series = [
111
+ {
112
+ name: "队伍数",
113
+ showInLegend: false,
114
+ data,
115
+ },
116
+ ];
117
+
118
+ const colors = ["rgb(124, 181, 236)"];
119
+ return { cat, series, colors };
120
+ })();
121
+
122
+ return getChartObj("题目通过数统计", "题目编号", "通过数", cat, series, colors);
123
+ }
124
+
125
+ export function getTeamChart(rank: Rank) {
126
+ const num = rank.rankStatistics.teamSolvedNum.map((n, ix) => {
127
+ return {
128
+ index: ix,
129
+ cnt: n,
130
+ };
131
+ });
132
+
133
+ const { cat, series, colors } = (() => {
134
+ const cat: any = [];
135
+ const data: any = [];
136
+
137
+ num.forEach((num: any) => {
138
+ cat.push(num.index);
139
+ data.push(num.cnt);
140
+ });
141
+
142
+ const series = [
143
+ {
144
+ name: "队伍数",
145
+ showInLegend: false,
146
+ data,
147
+ },
148
+ ];
149
+
150
+ const colors = ["rgb(124, 181, 236)"];
151
+ return { cat, series, colors };
152
+ })();
153
+
154
+ return getChartObj("队伍过题数统计", "过题数", "队伍数", cat, series, colors);
155
+ }
156
+
157
+ export function getSubmitChart(rank: Rank) {
158
+ const { cat, series, colors } = (() => {
159
+ const cat: any = [];
160
+ const Accepted: any = [];
161
+ const Rejected: any = [];
162
+ const Pending: any = [];
163
+
164
+ rank.contest.problems.forEach((p) => {
165
+ Accepted.push(p.statistics.acceptedNum);
166
+ Rejected.push(p.statistics.rejectedNum);
167
+ Pending.push(p.statistics.pendingNum);
168
+ cat.push(p.label);
169
+ });
170
+
171
+ const series = [
172
+ {
173
+ name: "Accepted",
174
+ showInLegend: false,
175
+ data: Accepted,
176
+ },
177
+ {
178
+ name: "Rejected",
179
+ showInLegend: false,
180
+ data: Rejected,
181
+ },
182
+ {
183
+ name: "Pending",
184
+ showInLegend: false,
185
+ data: Pending,
186
+ },
187
+ ];
188
+
189
+ const colors = ["#E1FFB5", "#FFD0D0", "#C8D6FA"];
190
+ return { cat, series, colors };
191
+ })();
192
+
193
+ return getChartObj("提交分类统计", "题目编号", "提交数", cat, series, colors);
194
+ }
195
+
196
+ export function getTeamPlaceChart(_rank: Rank, team: Team) {
197
+ const data = team.placeChartPoints.map((p) => {
198
+ return {
199
+ x: p.timePoint,
200
+ y: p.rank,
201
+ showX: getTimeDiff(p.timePoint),
202
+ lastSolved: p.lastSolvedProblem?.label ? `Last Solved Problem ${p.lastSolvedProblem?.label}` : "",
203
+ };
204
+ });
205
+
206
+ const options: Highcharts.Options = {
207
+ chart: {
208
+ type: "spline",
209
+ },
210
+ title: {
211
+ text: "排名变化趋势图",
212
+ },
213
+ series: [
214
+ {
215
+ showInLegend: false,
216
+ allowPointSelect: false,
217
+ name: "rank",
218
+ type: "spline",
219
+ data,
220
+ marker: {
221
+ symbol: "circle",
222
+ radius: 4,
223
+ lineWidth: 2,
224
+ fillColor: "yellow",
225
+ lineColor: "yellow",
226
+ },
227
+ },
228
+ ],
229
+ xAxis: [
230
+ {
231
+ allowDecimals: false,
232
+ title: {
233
+ text: "Time(min)",
234
+ },
235
+ crosshair: true,
236
+ },
237
+ ],
238
+ yAxis: [
239
+ {
240
+ allowDecimals: false,
241
+ title: {
242
+ text: "Place",
243
+ },
244
+ reversed: true,
245
+ gridLineWidth: 1,
246
+ },
247
+ ],
248
+ plotOptions: {
249
+ line: {
250
+ color: "#efbc47",
251
+ dataLabels: {
252
+ enabled: false,
253
+ },
254
+ enableMouseTracking: true,
255
+ marker: {
256
+ enabled: true,
257
+ fillColor: "#fff566",
258
+ },
259
+ },
260
+ },
261
+ tooltip: {
262
+ enabled: true,
263
+ headerFormat: "",
264
+ pointFormat: "Time: {point.showX} <br/> Place: {point.y} <br/> {point.lastSolved}",
265
+ },
266
+ credits: {
267
+ enabled: false,
268
+ },
269
+ exporting: {
270
+ enabled: true,
271
+ },
272
+ navigation: {
273
+ menuItemStyle: {
274
+ fontSize: "10px",
275
+ },
276
+ },
277
+ };
278
+
279
+ return options;
280
+ }
@@ -0,0 +1,29 @@
1
+ import type { SelectOptionItem } from "@xcpcio/core";
2
+
3
+ export function getLocalStorageKeyForFilterOrganizations() {
4
+ const route = useRoute();
5
+ const key = `filter-organizations-${route.path}`;
6
+
7
+ return key;
8
+ }
9
+
10
+ export function getLocalStorageKeyForFilterTeams() {
11
+ const route = useRoute();
12
+ const key = `filter-teams-${route.path}`;
13
+
14
+ return key;
15
+ }
16
+
17
+ export function useLocalStorageForFilterOrganizations() {
18
+ const route = useRoute();
19
+ const key = `filter-organizations-${route.path}`;
20
+
21
+ return useStorage(key, [] as Array<SelectOptionItem>);
22
+ }
23
+
24
+ export function useLocalStorageForFilterTeams() {
25
+ const route = useRoute();
26
+ const key = `filter-teams-${route.path}`;
27
+
28
+ return useStorage(key, [] as Array<SelectOptionItem>);
29
+ }
@@ -0,0 +1,43 @@
1
+ import { useQuery } from "@tanstack/vue-query";
2
+ import type { Contest, Submissions, Teams } from "@xcpcio/types";
3
+
4
+ const RETRY = 3;
5
+ const REFETCH_INTERVAL = 30 * 1000;
6
+
7
+ export interface BoardData {
8
+ contest: Contest;
9
+ teams: Teams;
10
+ submissions: Submissions;
11
+ }
12
+
13
+ async function fetcher(target: string, timestamp?: number): Promise<BoardData> {
14
+ const endpoint = target.startsWith("/") ? target.slice(1) : target;
15
+ const prefix = `${window.DATA_HOST}${endpoint}`;
16
+
17
+ const contestResp = await fetch(`${prefix}/config.json?t=${timestamp ?? 0}`);
18
+ const teamsResp = await fetch(`${prefix}/team.json?t=${timestamp ?? 0}`);
19
+ const submissionsResp = await fetch(`${prefix}/run.json?t=${timestamp ?? 0}`);
20
+
21
+ const p = Promise.all([
22
+ contestResp.json(),
23
+ teamsResp.json(),
24
+ submissionsResp.json()]).then((res) => {
25
+ return {
26
+ contest: res[0],
27
+ teams: res[1],
28
+ submissions: res[2],
29
+ };
30
+ });
31
+
32
+ return p;
33
+ }
34
+
35
+ export function useQueryBoardData(target: string, timestamp?: any) {
36
+ return useQuery({
37
+ queryKey: [target, timestamp.value.getTime()],
38
+ queryFn: () => fetcher(target, timestamp.value.getTime()),
39
+ retry: RETRY,
40
+ staleTime: REFETCH_INTERVAL,
41
+ refetchInterval: REFETCH_INTERVAL,
42
+ });
43
+ }
@@ -0,0 +1,11 @@
1
+ export function downloadSingleFile(content: string, filename: string) {
2
+ const blob = new Blob([content], { type: "text/plain" });
3
+ const url = URL.createObjectURL(blob);
4
+
5
+ const link = document.createElement("a");
6
+ link.href = url;
7
+ link.download = filename;
8
+ link.click();
9
+
10
+ URL.revokeObjectURL(url);
11
+ }
@@ -0,0 +1,14 @@
1
+ <template>
2
+ <NavBar
3
+ width="sm:w-[1280px] xl:w-screen"
4
+ />
5
+ <main
6
+ py-2
7
+ text-gray-700 dark:text-gray-200
8
+ >
9
+ <RouterView />
10
+ <Footer
11
+ class="sm:w-[1280px] xl:w-screen"
12
+ />
13
+ </main>
14
+ </template>
@@ -0,0 +1,10 @@
1
+ <template>
2
+ <NavBar />
3
+ <main
4
+ py-10
5
+ text-gray-700 dark:text-gray-200
6
+ >
7
+ <RouterView />
8
+ <Footer />
9
+ </main>
10
+ </template>
@@ -0,0 +1,12 @@
1
+ <template>
2
+ <main
3
+ px-4 py-10
4
+ text="center gray-700 dark:gray-200"
5
+ >
6
+ <RouterView />
7
+ <Footer />
8
+ <div mx-auto mt-5 text-center text-sm opacity-50>
9
+ [Home Layout]
10
+ </div>
11
+ </main>
12
+ </template>
@@ -0,0 +1,15 @@
1
+ <template>
2
+ <NavBar
3
+ width="sm:w-[1024px] lg:w-screen"
4
+ />
5
+ <main
6
+ class="lg:mt-[-52px]"
7
+ py-2
8
+ text-gray-700 dark:text-gray-200
9
+ >
10
+ <RouterView />
11
+ <Footer
12
+ class="sm:w-[1024px] lg:w-screen"
13
+ />
14
+ </main>
15
+ </template>
package/src/main.ts ADDED
@@ -0,0 +1,36 @@
1
+ import { ViteSSG } from "vite-ssg";
2
+ import { setupLayouts } from "virtual:generated-layouts";
3
+
4
+ import FloatingVue from "floating-vue";
5
+ import { VueQueryPlugin } from "@tanstack/vue-query";
6
+
7
+ // import Previewer from 'virtual:vue-component-preview'
8
+ import App from "./App.vue";
9
+ import type { UserModule } from "./types";
10
+ import generatedRoutes from "~pages";
11
+
12
+ import "floating-vue/dist/style.css";
13
+ import "vue-search-select/dist/VueSearchSelect.css";
14
+
15
+ import "@unocss/reset/tailwind.css";
16
+ import "@unocss/reset/tailwind-compat.css";
17
+ import "uno.css";
18
+
19
+ import "./styles/main.css";
20
+
21
+ const routes = setupLayouts(generatedRoutes);
22
+
23
+ // https://github.com/antfu/vite-ssg
24
+ export const createApp = ViteSSG(
25
+ App,
26
+ { routes, base: import.meta.env.BASE_URL },
27
+ (ctx) => {
28
+ // install all modules under `modules/`
29
+ Object.values(import.meta.glob<{ install: UserModule }>("./modules/*.ts", { eager: true }))
30
+ .forEach(i => i.install?.(ctx));
31
+
32
+ // ctx.app.use(Previewer)
33
+ ctx.app.use(FloatingVue);
34
+ ctx.app.use(VueQueryPlugin);
35
+ },
36
+ );