@xcpcio/board-app 0.46.2 → 0.47.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 (50) hide show
  1. package/dist/404.html +1 -1
  2. package/dist/assets/Board-8cb099e8.js +1 -0
  3. package/dist/assets/DataSourceInput.vue_vue_type_script_setup_true_lang-669bf7c0.js +1 -0
  4. package/dist/assets/{TheInput.vue_vue_type_script_setup_true_lang-859ce89a.js → TheInput.vue_vue_type_script_setup_true_lang-ce96e373.js} +1 -1
  5. package/dist/assets/_...all_-729d96d7.js +6 -0
  6. package/dist/assets/_...all_-c60acc8c.css +1 -0
  7. package/dist/assets/_...all_-f3d30859.js +1 -0
  8. package/dist/assets/{_name_-07451050.js → _name_-fa12f56d.js} +1 -1
  9. package/dist/assets/{about-ccab3fef.js → about-6e5a2f3e.js} +1 -1
  10. package/dist/assets/board-6c9ef349.js +1 -0
  11. package/dist/assets/{board-layout-9f205eef.js → board-layout-1ad47386.js} +1 -1
  12. package/dist/assets/{headless-84b0ba2a.js → headless-cbd40573.js} +1 -1
  13. package/dist/assets/{home-51ee0a78.js → home-eb990606.js} +1 -1
  14. package/dist/assets/index-241beb5a.css +1 -0
  15. package/dist/assets/index-42e1a002.js +1 -0
  16. package/dist/assets/{index-ea1c658f.css → index-53dc9c92.css} +2 -2
  17. package/dist/assets/index-5e6a6648.js +1 -0
  18. package/dist/assets/{index-b8520a7a.js → index-d70fb8f0.js} +78 -78
  19. package/dist/assets/index-fd9c106b.js +1 -0
  20. package/dist/assets/{index-layout-69d341e2.js → index-layout-7a27b184.js} +1 -1
  21. package/dist/assets/pagination-fe331a94.js +3 -0
  22. package/dist/assets/{query-f9ac0577.js → query-ce1fd4de.js} +1 -1
  23. package/dist/assets/test-544e70d8.js +1 -0
  24. package/dist/assets/{user-c860815d.js → user-449d45cf.js} +1 -1
  25. package/dist/assets/{virtual_pwa-register-6184380c.js → virtual_pwa-register-c3a257bf.js} +1 -1
  26. package/dist/index.html +1 -1
  27. package/dist/sw.js +1 -1
  28. package/package.json +3 -3
  29. package/src/auto-imports.d.ts +9 -0
  30. package/src/components/NavBar.vue +10 -1
  31. package/src/components/rating/Rating.vue +82 -0
  32. package/src/components/rating/RatingBadge.vue +32 -0
  33. package/src/components/rating/RatingIndexUI.vue +173 -0
  34. package/src/components/rating/RatingInfoModal.vue +112 -0
  35. package/src/components/rating/RatingTable.vue +270 -0
  36. package/src/components/rating/RatingUserUI.vue +74 -0
  37. package/src/components/rating/rating.less +68 -0
  38. package/src/components.d.ts +6 -0
  39. package/src/composables/constant.ts +6 -0
  40. package/src/composables/rating.ts +217 -0
  41. package/src/pages/rating/[...all].vue +13 -0
  42. package/src/pages/rating/index.vue +86 -0
  43. package/vite.config.ts +8 -5
  44. package/dist/assets/Board-5a18de8e.js +0 -3
  45. package/dist/assets/DataSourceInput.vue_vue_type_script_setup_true_lang-3f2e71b7.js +0 -1
  46. package/dist/assets/_...all_-8b9261ff.js +0 -1
  47. package/dist/assets/board-fd754235.js +0 -1
  48. package/dist/assets/index-ae87348c.js +0 -1
  49. package/dist/assets/index-f9b2eb26.js +0 -1
  50. package/dist/assets/test-1cc6e80d.js +0 -1
@@ -0,0 +1,270 @@
1
+ <script setup lang="ts">
2
+ import { MultiSelect } from "vue-search-select";
3
+
4
+ import type { Rating, SelectOptionItem } from "@xcpcio/core";
5
+
6
+ import { Pagination } from "~/composables/pagination";
7
+
8
+ interface FilterOptions {
9
+ organizations: string[];
10
+ members: string[];
11
+ }
12
+
13
+ const props = defineProps<{
14
+ rating: Rating,
15
+ pageSize?: number;
16
+ removeBorder?: boolean;
17
+ }>();
18
+
19
+ const rating = computed(() => props.rating);
20
+
21
+ const filterOptions = ref<FilterOptions>({
22
+ organizations: [],
23
+ members: [],
24
+ });
25
+
26
+ const orgOptions = computed(() => {
27
+ let res = rating.value.users.map((u) => {
28
+ return u.organization;
29
+ });
30
+ res = Array.from(new Set(res));
31
+
32
+ return res.map((o) => {
33
+ return {
34
+ value: o,
35
+ text: o,
36
+ };
37
+ });
38
+ });
39
+
40
+ const orgSelectedItems = ref<Array<SelectOptionItem>>([]);
41
+ const orgLastSelectItem = ref({});
42
+ function orgOnSelect(selectedItems: Array<SelectOptionItem>, lastSelectItem: SelectOptionItem) {
43
+ orgSelectedItems.value = selectedItems;
44
+ orgLastSelectItem.value = lastSelectItem;
45
+ }
46
+
47
+ // const memberOptions = computed(() => {
48
+ // const se = new Set();
49
+ // rating.value.users.forEach((u) => {
50
+ // return u.members.forEach((m) => {
51
+ // se.add(m.name);
52
+ // });
53
+ // });
54
+ // const res = Array.from(se);
55
+
56
+ // return res.map((o) => {
57
+ // return {
58
+ // value: o,
59
+ // text: o,
60
+ // };
61
+ // });
62
+ // });
63
+
64
+ // const memberSelectedItems = ref<Array<SelectOptionItem>>([]);
65
+ // const memberLastSelectItem = ref({});
66
+
67
+ // function memberOnSelect(selectedItems: Array<SelectOptionItem>, lastSelectItem: SelectOptionItem) {
68
+ // memberSelectedItems.value = selectedItems;
69
+ // memberLastSelectItem.value = lastSelectItem;
70
+ // }
71
+
72
+ const users = computed(() => {
73
+ const us = rating.value.users;
74
+ return us.filter((u) => {
75
+ const o = filterOptions.value;
76
+
77
+ if (o.organizations.length === 0
78
+ && o.members.length === 0) {
79
+ return true;
80
+ }
81
+
82
+ for (const org of o.organizations) {
83
+ if (org === u.organization) {
84
+ return true;
85
+ }
86
+ }
87
+
88
+ for (const member of o.members) {
89
+ for (const m of u.members) {
90
+ if (member === m.name) {
91
+ return true;
92
+ }
93
+ }
94
+ }
95
+
96
+ return false;
97
+ });
98
+ });
99
+
100
+ function onFilter() {
101
+ const newFilterOptions: FilterOptions = {
102
+ organizations: [],
103
+ members: [],
104
+ };
105
+
106
+ newFilterOptions.organizations = orgSelectedItems.value.map(o => o.value);
107
+ // newFilterOptions.members = memberSelectedItems.value.map(o => o.value);
108
+
109
+ filterOptions.value = newFilterOptions;
110
+ }
111
+
112
+ const p = ref(new Pagination());
113
+
114
+ p.value.currentPage = 0;
115
+ p.value.pageSize = props.pageSize ?? 20;
116
+ p.value.totalSize = users.value.length;
117
+
118
+ watch(users, () => {
119
+ p.value.totalSize = users.value.length;
120
+
121
+ if (p.value.currentPage >= p.value.totalPage) {
122
+ p.value.currentPage = p.value.totalPage - 1;
123
+ }
124
+ });
125
+
126
+ const currentUsers = computed(() => {
127
+ return users.value.slice(p.value.currentLeft, p.value.currentRight);
128
+ });
129
+ </script>
130
+
131
+ <template>
132
+ <section>
133
+ <div
134
+ mx-auto w-full
135
+ >
136
+ <div
137
+ relative overflow-hidden
138
+ bg-white dark:bg-gray-800
139
+ :class="{
140
+ 'shadow-md': props.removeBorder !== true,
141
+ 'sm:rounded-sm': props.removeBorder !== true,
142
+ }"
143
+ >
144
+ <div
145
+ space-y-3
146
+ flex flex-col
147
+ px-4 py-3
148
+ lg:flex-row lg:items-center lg:justify-between
149
+ lg:space-x-4 lg:space-y-0
150
+ >
151
+ <div
152
+ flex flex-shrink-0 flex-col
153
+ md:flex-row md:items-center
154
+ lg:justify-end space-y-3
155
+ md:space-x-3 md:space-y-0
156
+ >
157
+ <div
158
+ w-108
159
+ >
160
+ <MultiSelect
161
+ :options="orgOptions"
162
+ :selected-options="orgSelectedItems"
163
+ placeholder="Organization"
164
+ @select="orgOnSelect"
165
+ />
166
+ </div>
167
+
168
+ <!-- <div
169
+ w-68
170
+ >
171
+ <MultiSelect
172
+ :options="memberOptions"
173
+ :selected-options="memberSelectedItems"
174
+ placeholder="Member"
175
+ @select="memberOnSelect"
176
+ />
177
+ </div> -->
178
+
179
+ <div>
180
+ <button
181
+ type="button"
182
+ 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"
183
+ @click="onFilter"
184
+ >
185
+ <div
186
+ i-material-symbols-search
187
+ mr-1 h-5 w-5
188
+ />
189
+ Filter
190
+ </button>
191
+ </div>
192
+ </div>
193
+ </div>
194
+
195
+ <div
196
+ class="overflow-x-auto"
197
+ >
198
+ <table
199
+ class="w-full text-left text-sm text-gray-500 dark:text-gray-400"
200
+ font-medium font-mono
201
+ >
202
+ <thead
203
+ text-xs
204
+ bg-gray-50 text-gray-700
205
+ dark:bg-gray-700 dark:text-gray-400
206
+ >
207
+ <tr>
208
+ <th
209
+ scope="col"
210
+ class="px-4 py-3"
211
+ w-68
212
+ >
213
+ Organization
214
+ </th>
215
+ <th
216
+ scope="col"
217
+ class="px-4 py-3"
218
+ w-128
219
+ >
220
+ TeamName
221
+ </th>
222
+ <th
223
+ scope="col"
224
+ class="px-4 py-3"
225
+ w-68
226
+ >
227
+ Members
228
+ </th>
229
+ <th
230
+ scope="col"
231
+ class="px-4 py-3"
232
+ >
233
+ Rating
234
+ </th>
235
+ <th
236
+ scope="col"
237
+ class="px-4 py-3"
238
+ >
239
+ MaxRating
240
+ </th>
241
+ <th
242
+ scope="col"
243
+ class="px-4 py-3"
244
+ >
245
+ MinRating
246
+ </th>
247
+ </tr>
248
+ </thead>
249
+
250
+ <tbody>
251
+ <template
252
+ v-for="(u, ix) in currentUsers"
253
+ :key="u.id"
254
+ >
255
+ <RatingUserUI
256
+ :ix="ix"
257
+ :rating-user="u"
258
+ />
259
+ </template>
260
+ </tbody>
261
+ </table>
262
+ </div>
263
+
264
+ <TablePagination
265
+ v-model:pagination="p"
266
+ />
267
+ </div>
268
+ </div>
269
+ </section>
270
+ </template>
@@ -0,0 +1,74 @@
1
+ <script setup lang="ts">
2
+ import type { RatingUser } from "@xcpcio/core";
3
+ import { RatingUtility } from "@xcpcio/core";
4
+
5
+ import "./rating.less";
6
+
7
+ const props = defineProps<{
8
+ ix: number,
9
+ ratingUser: RatingUser,
10
+ }>();
11
+
12
+ const u = computed(() => props.ratingUser);
13
+
14
+ const hiddenRatingInfoModal = ref(true);
15
+ function onClickRatingInfoModal() {
16
+ hiddenRatingInfoModal.value = false;
17
+ }
18
+ </script>
19
+
20
+ <template>
21
+ <tr
22
+ class="border-b dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700"
23
+ >
24
+ <td
25
+ class="whitespace-nowrap px-4 py-2 text-gray-900 dark:text-white"
26
+ >
27
+ {{ u.organization }}
28
+ </td>
29
+ <td>
30
+ <div
31
+ class="whitespace-nowrap px-4 py-2 text-gray-900 dark:text-white"
32
+ cursor-pointer
33
+ :class="RatingUtility.getRatingLevelClass(u.rating)"
34
+ @click="onClickRatingInfoModal"
35
+ >
36
+ {{ u.name }}
37
+ </div>
38
+
39
+ <div>
40
+ <RatingInfoModal
41
+ v-if="!hiddenRatingInfoModal"
42
+ v-model:isHidden="hiddenRatingInfoModal"
43
+ :rating-user="u"
44
+ />
45
+ </div>
46
+ </td>
47
+ <td
48
+ class="whitespace-nowrap px-4 py-2 text-gray-900 dark:text-white"
49
+ >
50
+ {{ u.members.map(m => m.name.trim()).join(" ") }}
51
+ </td>
52
+ <td
53
+ class="whitespace-nowrap px-4 py-2 text-gray-900 dark:text-white"
54
+ >
55
+ <RatingBadge
56
+ :rating="u.rating"
57
+ />
58
+ </td>
59
+ <td
60
+ class="whitespace-nowrap px-4 py-2 text-gray-900 dark:text-white"
61
+ >
62
+ <RatingBadge
63
+ :rating="u.maxRating"
64
+ />
65
+ </td>
66
+ <td
67
+ class="whitespace-nowrap px-4 py-2 text-gray-900 dark:text-white"
68
+ >
69
+ <RatingBadge
70
+ :rating="u.minRating"
71
+ />
72
+ </td>
73
+ </tr>
74
+ </template>
@@ -0,0 +1,68 @@
1
+ .recent-actions {
2
+ margin: 1em;
3
+ font-size: 0.9em;
4
+ }
5
+
6
+ .rated-user {
7
+ font-family: helvetica neue, Helvetica, Arial, sans-serif;
8
+ text-decoration: none;
9
+ font-weight: 700;
10
+ display: inline-block;
11
+ }
12
+
13
+ .user-black {
14
+ color: #000;
15
+ font-weight: 400;
16
+ }
17
+
18
+ .user-legendary {
19
+ color: red;
20
+ }
21
+
22
+ .user-legendary::first-letter {
23
+ color: #000;
24
+ }
25
+
26
+ .legendary-user-first-letter {
27
+ color: #000;
28
+ }
29
+
30
+ .user-red {
31
+ color: red;
32
+ }
33
+
34
+ .user-fire {
35
+ color: red;
36
+ }
37
+
38
+ .user-yellow {
39
+ color: #bb0;
40
+ }
41
+
42
+ .user-violet {
43
+ color: #a0a;
44
+ }
45
+
46
+ .user-orange {
47
+ color: #ff8c00;
48
+ }
49
+
50
+ .user-blue {
51
+ color: blue;
52
+ }
53
+
54
+ .user-cyan {
55
+ color: #03a89e;
56
+ }
57
+
58
+ .user-green {
59
+ color: green;
60
+ }
61
+
62
+ .user-gray {
63
+ color: gray;
64
+ }
65
+
66
+ .user-admin {
67
+ color: #000;
68
+ }
@@ -35,6 +35,12 @@ declare module 'vue' {
35
35
  ProblemBlock: typeof import('./components/board/ProblemBlock.vue')['default']
36
36
  ProblemInfoModal: typeof import('./components/board/ProblemInfoModal.vue')['default']
37
37
  Progress: typeof import('./components/board/Progress.vue')['default']
38
+ Rating: typeof import('./components/rating/Rating.vue')['default']
39
+ RatingBadge: typeof import('./components/rating/RatingBadge.vue')['default']
40
+ RatingIndexUI: typeof import('./components/rating/RatingIndexUI.vue')['default']
41
+ RatingInfoModal: typeof import('./components/rating/RatingInfoModal.vue')['default']
42
+ RatingTable: typeof import('./components/rating/RatingTable.vue')['default']
43
+ RatingUserUI: typeof import('./components/rating/RatingUserUI.vue')['default']
38
44
  RightArrowIcon: typeof import('./components/icon/RightArrowIcon.vue')['default']
39
45
  RouterLink: typeof import('vue-router')['RouterLink']
40
46
  RouterView: typeof import('vue-router')['RouterView']
@@ -1,4 +1,5 @@
1
1
  export const TITLE_SUFFIX = "Board - XCPCIO";
2
+ export const RATING_TITLE_SUFFIX = "Rating - XCPCIO";
2
3
  export const BALLOON_TITLE_SUFFIX = "Balloon - XCPCIO";
3
4
  export const RESOLVER_TITLE_SUFFIX = "Resolver - XCPCIO";
4
5
  export const COUNTDOWN_TITLE_SUFFIX = "Countdown - XCPCIO";
@@ -19,6 +20,11 @@ export const DATA_HOST = computed(() => {
19
20
  return window.DATA_HOST;
20
21
  });
21
22
 
23
+ export const RATING_DATA_HOST = computed(() => {
24
+ const dataHost = DATA_HOST.value;
25
+ return dataHost.replace("/data/", "/rating-data/");
26
+ });
27
+
22
28
  export const DATA_REGION = computed(() => {
23
29
  if (!window) {
24
30
  return "";
@@ -0,0 +1,217 @@
1
+ import { RatingLevelToString, RatingUtility } from "@xcpcio/core";
2
+ import type { RatingUser } from "@xcpcio/core";
3
+
4
+ interface RatingGraphData {
5
+ x: number;
6
+ y: number;
7
+ rank: number;
8
+ diffRating: string;
9
+ ratingTitle: string;
10
+ contestName: string;
11
+ contestTime: string;
12
+ teamName: string;
13
+ link?: string;
14
+ }
15
+
16
+ function getDiffRating(oldRating: number, newRating: number) {
17
+ const diff = newRating - oldRating;
18
+ if (newRating > oldRating) {
19
+ return `+${diff.toString()}`;
20
+ } else {
21
+ return diff.toString();
22
+ }
23
+ }
24
+
25
+ export function getRatingGraphOptions(
26
+ ratingUser: RatingUser,
27
+ ) {
28
+ const data: RatingGraphData[] = [];
29
+
30
+ {
31
+ let oldRating = 0;
32
+ for (const h of ratingUser.ratingHistories) {
33
+ const d: RatingGraphData = {} as RatingGraphData;
34
+ d.x = h.contestTime.unix() * 1000;
35
+ d.y = h.rating;
36
+ d.diffRating = getDiffRating(oldRating, h.rating);
37
+ d.rank = h.rank;
38
+ d.contestName = h.contestName;
39
+ d.link = `/board/${h.contestID}`;
40
+ d.contestTime = h.contestTime.format("YYYY-MM-DD HH:mm:ss");
41
+ d.teamName = h.teamName;
42
+ d.ratingTitle = RatingLevelToString[RatingUtility.getRatingLevel(h.rating)];
43
+ data.push(d);
44
+
45
+ oldRating = h.rating;
46
+ }
47
+ }
48
+
49
+ let tickPositions = [
50
+ 1200,
51
+ 1400,
52
+ 1600,
53
+ 1900,
54
+ 2100,
55
+ 2300,
56
+ 2400,
57
+ 2600,
58
+ 3000,
59
+ ];
60
+
61
+ {
62
+ const gap = 100;
63
+ let minRating = Math.max(0, ratingUser.minRating - gap);
64
+ let maxRating = ratingUser.maxRating + gap;
65
+
66
+ for (let i = 0; i < tickPositions.length; ++i) {
67
+ if (tickPositions[i] > maxRating) {
68
+ maxRating = tickPositions[i];
69
+ break;
70
+ }
71
+ }
72
+ for (let i = tickPositions.length - 1; i >= 0; --i) {
73
+ if (tickPositions[i] < minRating) {
74
+ minRating = tickPositions[i];
75
+ break;
76
+ }
77
+ }
78
+ if (minRating < 1200) {
79
+ tickPositions = [minRating, ...tickPositions];
80
+ }
81
+ if (maxRating > 3000) {
82
+ tickPositions.push(maxRating);
83
+ }
84
+
85
+ tickPositions = tickPositions.filter(
86
+ x => x >= minRating && x <= maxRating,
87
+ );
88
+ }
89
+
90
+ const options = {
91
+ chart: {
92
+ type: "line",
93
+ height: "408px",
94
+ },
95
+ title: {
96
+ text: null,
97
+ },
98
+ xAxis: {
99
+ tickWidth: 0,
100
+ gridLineWidth: 0.4,
101
+ minRange: 30 * 24 * 60 * 60 * 1000,
102
+ type: "datetime",
103
+ showFirstLabel: true,
104
+ showLastLabel: true,
105
+ dateTimeLabelFormats: {
106
+ month: "%b %Y",
107
+ },
108
+ },
109
+ yAxis: {
110
+ showEmpty: false,
111
+ showFirstLabel: false,
112
+ showLastLabel: false,
113
+ tickPositions,
114
+ tickWidth: 0,
115
+ gridLineWidth: 0.6,
116
+ labels: {
117
+ enabled: true,
118
+ format: "{value}",
119
+ },
120
+ title: {
121
+ text: null,
122
+ },
123
+ plotBands: [
124
+ {
125
+ from: 0,
126
+ to: 1199,
127
+ color: "#CCCCCC",
128
+ },
129
+ {
130
+ from: 1200,
131
+ to: 1399,
132
+ color: "#98FA87",
133
+ },
134
+ {
135
+ from: 1400,
136
+ to: 1599,
137
+ color: "#90DABD",
138
+ },
139
+ {
140
+ from: 1600,
141
+ to: 1899,
142
+ color: "#A9ACF9",
143
+ },
144
+ {
145
+ from: 1900,
146
+ to: 2099,
147
+ color: "#EF91F9",
148
+ },
149
+ {
150
+ from: 2100,
151
+ to: 2299,
152
+ color: "#F7CD91",
153
+ },
154
+ {
155
+ from: 2300,
156
+ to: 2399,
157
+ color: "#F5BD67",
158
+ },
159
+ {
160
+ from: 2400,
161
+ to: 2599,
162
+ color: "#Ef7F7B",
163
+ },
164
+ {
165
+ from: 2600,
166
+ to: 2999,
167
+ color: "#EB483F",
168
+ },
169
+ {
170
+ from: 3000,
171
+ to: 0x3F3F3F3F,
172
+ color: "#9C1E14",
173
+ },
174
+ ],
175
+ },
176
+ credits: {
177
+ enabled: false,
178
+ },
179
+ plotOptions: {
180
+ line: {
181
+ color: "#ffec3d",
182
+ dataLabels: {
183
+ enabled: false,
184
+ },
185
+ enableMouseTracking: true,
186
+ marker: {
187
+ enabled: true,
188
+ fillColor: "#fffb8f",
189
+ },
190
+ },
191
+ },
192
+ tooltip: {
193
+ enabled: true,
194
+ headerFormat: "",
195
+ useHTML: true,
196
+ shared: true,
197
+ shadow: true,
198
+ followPointer: false,
199
+ followTouchMove: false,
200
+ pointFormat: `= {point.y} ({point.diffRating}), {point.ratingTitle}
201
+ <br/>Rank: {point.rank}
202
+ <br/>TeamName: {point.teamName}
203
+ <br/>ContestTime: {point.contestTime}
204
+ <br/><a href="{point.link}">{point.contestName}</a>
205
+ <br/>`,
206
+ },
207
+ series: [
208
+ {
209
+ showInLegend: false,
210
+ allowPointSelect: true,
211
+ // name: ratingUser.name,
212
+ data,
213
+ },
214
+ ],
215
+ };
216
+ return options;
217
+ }
@@ -0,0 +1,13 @@
1
+ <script setup lang="ts">
2
+ const route = useRoute();
3
+
4
+ const id = route.path.split("/").pop() as string;
5
+ </script>
6
+
7
+ <template>
8
+ <div>
9
+ <Rating
10
+ :id="id"
11
+ />
12
+ </div>
13
+ </template>