@vue-skuilder/platform-ui 0.1.1

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 (108) hide show
  1. package/LICENCE +661 -0
  2. package/README.md +64 -0
  3. package/dist/assets/Roboto-Black-B0ZKieaB.woff +0 -0
  4. package/dist/assets/Roboto-Black-VhoA2qKx.woff2 +0 -0
  5. package/dist/assets/Roboto-BlackItalic-D0gSnuIb.woff +0 -0
  6. package/dist/assets/Roboto-BlackItalic-D4yie1YO.woff2 +0 -0
  7. package/dist/assets/Roboto-Bold-D9plYbeK.woff +0 -0
  8. package/dist/assets/Roboto-Bold-hN3duQhD.woff2 +0 -0
  9. package/dist/assets/Roboto-BoldItalic-BWDm51uc.woff2 +0 -0
  10. package/dist/assets/Roboto-BoldItalic-CyLKvOHD.woff +0 -0
  11. package/dist/assets/Roboto-Light-Cu-PAxXt.woff +0 -0
  12. package/dist/assets/Roboto-Light-DHTugVNA.woff2 +0 -0
  13. package/dist/assets/Roboto-LightItalic-CZg5kHIB.woff +0 -0
  14. package/dist/assets/Roboto-LightItalic-JQyp2Y3P.woff2 +0 -0
  15. package/dist/assets/Roboto-Medium-ByKogCTi.woff2 +0 -0
  16. package/dist/assets/Roboto-Medium-b81vv18W.woff +0 -0
  17. package/dist/assets/Roboto-MediumItalic-DFQ-RYa0.woff +0 -0
  18. package/dist/assets/Roboto-MediumItalic-i1eR0KbF.woff2 +0 -0
  19. package/dist/assets/Roboto-Regular-BX5l9hRW.woff +0 -0
  20. package/dist/assets/Roboto-Regular-C6rbFxYz.woff2 +0 -0
  21. package/dist/assets/Roboto-RegularItalic-BjnLZsam.woff +0 -0
  22. package/dist/assets/Roboto-RegularItalic-CvPUdkvM.woff2 +0 -0
  23. package/dist/assets/Roboto-Thin-BfJvJcog.woff +0 -0
  24. package/dist/assets/Roboto-Thin-NicBC1pN.woff2 +0 -0
  25. package/dist/assets/Roboto-ThinItalic-CKlCjrO_.woff2 +0 -0
  26. package/dist/assets/Roboto-ThinItalic-DnIWFxRE.woff +0 -0
  27. package/dist/assets/index-CQ-sNKGW.css +14 -0
  28. package/dist/assets/index-EbqpUgvM.js +161 -0
  29. package/dist/assets/materialdesignicons-webfont-B7mPwVP_.ttf +0 -0
  30. package/dist/assets/materialdesignicons-webfont-CSr8KVlo.eot +0 -0
  31. package/dist/assets/materialdesignicons-webfont-Dp5v-WZN.woff2 +0 -0
  32. package/dist/assets/materialdesignicons-webfont-PXm3-2wK.woff +0 -0
  33. package/dist/assets/workbox-window.prod.es5-p40uij6f.js +1 -0
  34. package/dist/favicon.ico +0 -0
  35. package/dist/img/icons/safari-pinned-tab.svg +149 -0
  36. package/dist/index.html +19 -0
  37. package/dist/manifest.json +20 -0
  38. package/dist/manifest.webmanifest +1 -0
  39. package/dist/robots.txt +2 -0
  40. package/dist/sw.js +1 -0
  41. package/dist/workbox-1be04862.js +1 -0
  42. package/package.json +105 -0
  43. package/src/App.vue +156 -0
  44. package/src/ENVIRONMENT_VARS.ts +79 -0
  45. package/src/components/Classrooms/ClassroomCtrlPanel.vue +206 -0
  46. package/src/components/Classrooms/CreateClassroom.vue +159 -0
  47. package/src/components/Classrooms/JoinCode.vue +83 -0
  48. package/src/components/Courses/CourseCardBrowser.vue +365 -0
  49. package/src/components/Courses/CourseEditor.vue +164 -0
  50. package/src/components/Courses/CourseInformation.vue +164 -0
  51. package/src/components/Courses/CourseRouter.vue +116 -0
  52. package/src/components/Courses/CourseStubCard.vue +76 -0
  53. package/src/components/Courses/EloModeration.vue +122 -0
  54. package/src/components/Courses/TagInformation.vue +209 -0
  55. package/src/components/Edit/BulkImport/CardPreviewList.vue +345 -0
  56. package/src/components/Edit/BulkImportView.vue +633 -0
  57. package/src/components/Edit/CardBrowser.vue +79 -0
  58. package/src/components/Edit/ComponentRegistration/ComponentRegistration.vue +235 -0
  59. package/src/components/Edit/ComponentRegistration/UnregisteredComponentsTable.vue +19 -0
  60. package/src/components/Edit/CourseEditor.vue +162 -0
  61. package/src/components/Edit/NavigationStrategy/NavigationStrategyEditor.vue +170 -0
  62. package/src/components/Edit/NavigationStrategy/NavigationStrategyList.vue +92 -0
  63. package/src/components/Edit/TagsInput.vue +247 -0
  64. package/src/components/Edit/ViewableDataInputForm/DataInputForm.vue +524 -0
  65. package/src/components/Edit/ViewableDataInputForm/FieldInput.types.ts +33 -0
  66. package/src/components/Edit/ViewableDataInputForm/FieldInputs/AudioInput.vue +188 -0
  67. package/src/components/Edit/ViewableDataInputForm/FieldInputs/ChessPuzzleInput.vue +79 -0
  68. package/src/components/Edit/ViewableDataInputForm/FieldInputs/FieldInput.css +12 -0
  69. package/src/components/Edit/ViewableDataInputForm/FieldInputs/ImageInput.vue +231 -0
  70. package/src/components/Edit/ViewableDataInputForm/FieldInputs/IntegerInput.vue +49 -0
  71. package/src/components/Edit/ViewableDataInputForm/FieldInputs/MarkdownInput.vue +34 -0
  72. package/src/components/Edit/ViewableDataInputForm/FieldInputs/MediaDragDropUploader.vue +246 -0
  73. package/src/components/Edit/ViewableDataInputForm/FieldInputs/MidiInput.vue +113 -0
  74. package/src/components/Edit/ViewableDataInputForm/FieldInputs/NumberInput.vue +49 -0
  75. package/src/components/Edit/ViewableDataInputForm/FieldInputs/StringInput.vue +49 -0
  76. package/src/components/Edit/ViewableDataInputForm/FieldInputs/typeValidators.ts +49 -0
  77. package/src/components/Edit/ViewableDataInputForm/OptionsFieldInput.ts +161 -0
  78. package/src/components/Study/SessionConfiguration.vue +371 -0
  79. package/src/components/TextSwap.vue +65 -0
  80. package/src/components/User/UserStats.vue +30 -0
  81. package/src/dev/DataInputFormTester.vue +117 -0
  82. package/src/dev/readme.md +3 -0
  83. package/src/enums.ts +0 -0
  84. package/src/glyphs.txt +933 -0
  85. package/src/main.ts +45 -0
  86. package/src/plugins/vuetify.ts +41 -0
  87. package/src/registerServiceWorker.ts +18 -0
  88. package/src/router.ts +184 -0
  89. package/src/server/index.spec.ts +192 -0
  90. package/src/server/index.ts +71 -0
  91. package/src/shims-vue.d.ts +5 -0
  92. package/src/store.mock.ts +122 -0
  93. package/src/stores/useDataInputFormStore.ts +49 -0
  94. package/src/stores/useFieldInputStore.ts +191 -0
  95. package/src/types/shims-vuetify.d.ts +12 -0
  96. package/src/types/svg.d.ts +4 -0
  97. package/src/utils/bulkImport/index.ts +94 -0
  98. package/src/views/About.vue +29 -0
  99. package/src/views/Admin.vue +128 -0
  100. package/src/views/Classrooms.vue +258 -0
  101. package/src/views/Courses.vue +265 -0
  102. package/src/views/Home.vue +154 -0
  103. package/src/views/Login.vue +75 -0
  104. package/src/views/ReleaseNotes.vue +20 -0
  105. package/src/views/SignUp.vue +32 -0
  106. package/src/views/Study.vue +261 -0
  107. package/src/views/User.vue +109 -0
  108. package/src/vite-env.d.ts +1 -0
@@ -0,0 +1,365 @@
1
+ <template>
2
+ <v-card>
3
+ <div v-if="updatePending" class="d-flex justify-center align-center pa-6">
4
+ <v-progress-circular indeterminate color="primary" />
5
+ </div>
6
+ <div v-else>
7
+ <paginating-toolbar
8
+ title="Exercises"
9
+ :page="page"
10
+ :pages="pages"
11
+ :subtitle="`(${questionCount})`"
12
+ @first="first"
13
+ @prev="prev"
14
+ @next="next"
15
+ @last="last"
16
+ @set-page="(n) => setPage(n)"
17
+ />
18
+
19
+ <v-list>
20
+ <template v-for="c in cards" :key="c.id">
21
+ <v-list-item
22
+ :class="{
23
+ 'bg-blue-grey-lighten-5': c.isOpen,
24
+ 'elevation-4': c.isOpen,
25
+ }"
26
+ density="compact"
27
+ data-cy="course-card"
28
+ >
29
+ <template #prepend>
30
+ <div>
31
+ <v-list-item-title :class="{ 'text-blue-grey-darken-1': c.isOpen }" class="font-weight-medium">
32
+ {{ cardPreview[c.id] }}
33
+ </v-list-item-title>
34
+ <v-list-item-subtitle>
35
+ {{ c.id.split('-').length === 3 ? c.id.split('-')[2] : '' }}
36
+ </v-list-item-subtitle>
37
+ </div>
38
+ </template>
39
+
40
+ <template #append>
41
+ <v-speed-dial
42
+ v-model="c.isOpen"
43
+ location="left center"
44
+ transition="slide-x-transition"
45
+ style="display: flex; flex-direction: row-reverse"
46
+ >
47
+ <template #activator="{ props }">
48
+ <v-btn
49
+ v-bind="props"
50
+ :icon="c.isOpen ? 'mdi-close' : 'mdi-plus'"
51
+ size="small"
52
+ variant="text"
53
+ @click="clearSelections(c.id)"
54
+ />
55
+ </template>
56
+
57
+ <v-btn
58
+ key="tags"
59
+ icon
60
+ size="small"
61
+ :variant="editMode !== 'tags' ? 'outlined' : 'elevated'"
62
+ :color="editMode === 'tags' ? 'teal' : 'teal-darken-3'"
63
+ @click.stop="editMode = 'tags'"
64
+ >
65
+ <v-icon>mdi-bookmark</v-icon>
66
+ </v-btn>
67
+
68
+ <v-btn
69
+ key="flag"
70
+ icon
71
+ size="small"
72
+ :variant="editMode !== 'flag' ? 'outlined' : 'elevated'"
73
+ :color="editMode === 'flag' ? 'error' : 'error-darken-3'"
74
+ @click.stop="editMode = 'flag'"
75
+ >
76
+ <v-icon>mdi-flag</v-icon>
77
+ </v-btn>
78
+ </v-speed-dial>
79
+ </template>
80
+ </v-list-item>
81
+
82
+ <div v-if="c.isOpen" class="px-4 py-2 bg-blue-grey-lighten-5">
83
+ <card-loader :qualified_id="c.id" :view-lookup="viewLookup" class="elevation-1" />
84
+
85
+ <tags-input
86
+ v-show="editMode === 'tags'"
87
+ :course-i-d="courseId"
88
+ :card-i-d="c.id.split('-')[1]"
89
+ class="mt-4"
90
+ />
91
+
92
+ <div v-show="editMode === 'flag'" class="mt-4">
93
+ <v-btn color="error" variant="outlined" @click="c.delBtn = true"> Delete this card </v-btn>
94
+ <span v-if="c.delBtn" class="ml-4">
95
+ <span class="mr-2">Are you sure?</span>
96
+ <v-btn color="error" variant="elevated" @click="deleteCard(c.id)"> Confirm </v-btn>
97
+ </span>
98
+ </div>
99
+ </div>
100
+ </template>
101
+ </v-list>
102
+
103
+ <paginating-toolbar
104
+ class="elevation-0"
105
+ :page="page"
106
+ :pages="pages"
107
+ @first="first"
108
+ @prev="prev"
109
+ @next="next"
110
+ @last="last"
111
+ @set-page="(n) => setPage(n)"
112
+ />
113
+ </div>
114
+ </v-card>
115
+ </template>
116
+
117
+ <script lang="ts">
118
+ import { displayableDataToViewData } from '@vue-skuilder/common';
119
+ import TagsInput from '@/components/Edit/TagsInput.vue';
120
+ import { PaginatingToolbar, ViewComponent, CardLoader, alertUser } from '@vue-skuilder/common-ui';
121
+ import { allCourses } from '@vue-skuilder/courses';
122
+ import { getDataLayer, CourseDBInterface, CardData, DisplayableData, Tag } from '@vue-skuilder/db';
123
+ import { defineComponent } from 'vue';
124
+ import { Status } from '@vue-skuilder/common';
125
+
126
+ function isConstructor(obj: unknown) {
127
+ try {
128
+ // @ts-expect-error - we are specifically probing an unknown object
129
+ new obj();
130
+ return true;
131
+ } catch (e) {
132
+ console.warn(`not a constructor: ${obj}, err: ${e}`);
133
+ return false;
134
+ }
135
+ }
136
+
137
+ export default defineComponent({
138
+ name: 'CourseCardBrowser',
139
+
140
+ components: {
141
+ CardLoader,
142
+ TagsInput,
143
+ PaginatingToolbar,
144
+ },
145
+
146
+ props: {
147
+ courseId: {
148
+ type: String,
149
+ required: true,
150
+ },
151
+ tagId: {
152
+ type: String,
153
+ required: false,
154
+ default: '',
155
+ },
156
+ },
157
+
158
+ data() {
159
+ return {
160
+ courseDB: null as CourseDBInterface | null,
161
+ page: 1,
162
+ pages: [] as number[],
163
+ cards: [] as { id: string; isOpen: boolean; delBtn: boolean }[],
164
+ cardData: {} as { [card: string]: string[] },
165
+ cardPreview: {} as { [card: string]: string },
166
+ editMode: 'none' as 'tags' | 'flag' | 'none',
167
+ delBtn: false,
168
+ updatePending: true,
169
+ userIsRegistered: false,
170
+ questionCount: 0,
171
+ tags: [] as Tag[],
172
+ viewLookup: (x: unknown) => allCourses.getView(x),
173
+ };
174
+ },
175
+
176
+ async created() {
177
+ try {
178
+ this.courseDB = getDataLayer().getCourseDB(this.courseId);
179
+
180
+ if (this.tagId) {
181
+ this.questionCount = (await this.courseDB.getTag(this.tagId)).taggedCards.length;
182
+ } else {
183
+ this.questionCount = (await this.courseDB!.getCourseInfo()).cardCount;
184
+ }
185
+
186
+ for (let i = 1; (i - 1) * 25 < this.questionCount; i++) {
187
+ this.pages.push(i);
188
+ }
189
+
190
+ await this.populateTableData();
191
+ } catch (error) {
192
+ console.error('Error initializing CourseCardBrowser:', error);
193
+ } finally {
194
+ this.updatePending = false;
195
+ }
196
+ },
197
+
198
+ methods: {
199
+ first() {
200
+ this.page = 1;
201
+ this.populateTableData();
202
+ },
203
+ prev() {
204
+ this.page--;
205
+ this.populateTableData();
206
+ },
207
+ next() {
208
+ this.page++;
209
+ this.populateTableData();
210
+ },
211
+ last() {
212
+ this.page = this.pages.length;
213
+ this.populateTableData();
214
+ },
215
+ setPage(n: number) {
216
+ this.page = n;
217
+ this.populateTableData();
218
+ },
219
+ clearSelections(exception: string = '') {
220
+ this.cards.forEach((card) => {
221
+ if (card.id !== exception) {
222
+ card.isOpen = false;
223
+ }
224
+ });
225
+ this.editMode = 'none';
226
+ this.delBtn = false;
227
+ },
228
+ async deleteCard(c: string) {
229
+ console.log(`Deleting card ${c}`);
230
+ const res = await this.courseDB!.removeCard(c.split('-')[1]);
231
+ if (res.ok) {
232
+ this.cards = this.cards.filter((card) => card.id != c);
233
+ this.clearSelections();
234
+ } else {
235
+ console.error(`Failed to delete card:\n\n${JSON.stringify(res)}`);
236
+ alertUser({
237
+ text: 'Failed to delete card',
238
+ status: Status.error,
239
+ });
240
+ }
241
+ },
242
+ async populateTableData() {
243
+ this.updatePending = true;
244
+ if (this.tagId) {
245
+ const tag = await this.courseDB!.getTag(this.tagId);
246
+ this.cards = tag.taggedCards.map((c) => {
247
+ return { id: `${this.courseId}-${c}`, isOpen: false, delBtn: false };
248
+ });
249
+ } else {
250
+ this.cards = (await this.courseDB!.getCardsByELO(0, 25)).map((c) => {
251
+ return {
252
+ id: c,
253
+ isOpen: false,
254
+ delBtn: false,
255
+ };
256
+ });
257
+ }
258
+
259
+ const toRemove: string[] = [];
260
+ const hydratedCardData = (
261
+ await this.courseDB!.getCourseDocs<CardData>(
262
+ this.cards.map((c) => c.id.split('-')[1]),
263
+ {
264
+ include_docs: true,
265
+ }
266
+ )
267
+ ).rows
268
+ .filter((r) => {
269
+ if (r.doc) {
270
+ return true;
271
+ } else {
272
+ console.error(`Card ${r.id}.doc not found.\ncard: ${JSON.stringify(r)}`);
273
+ // toRemove.push(r.id);
274
+ // if (this.tagId) {
275
+ // this.courseDB!.removeTagFromCard(r.id, this.tagId);
276
+ // }
277
+ return false;
278
+ }
279
+ })
280
+ .map((r) => r.doc!);
281
+
282
+ this.cards = this.cards.filter((c) => !toRemove.includes(c.id.split('-')[1]));
283
+
284
+ hydratedCardData.forEach((c) => {
285
+ if (c && c.id_displayable_data) {
286
+ this.cardData[c._id] = c.id_displayable_data;
287
+ }
288
+ });
289
+
290
+ try {
291
+ await Promise.all(
292
+ this.cards.map(async (c) => {
293
+ const _cardID: string = c.id.split('-')[1];
294
+
295
+ const tmpCardData = hydratedCardData.find((c) => c._id == _cardID);
296
+ if (!tmpCardData || !tmpCardData.id_displayable_data) {
297
+ console.error(`No valid data found for card ${_cardID}`);
298
+ return;
299
+ }
300
+ const tmpView: ViewComponent = allCourses.getView(
301
+ tmpCardData.id_view || 'default.question.BlanksCard.FillInView'
302
+ );
303
+
304
+ const tmpDataDocs = tmpCardData.id_displayable_data.map((id) => {
305
+ return this.courseDB!.getCourseDoc<DisplayableData>(id, {
306
+ attachments: false,
307
+ binary: true,
308
+ });
309
+ });
310
+
311
+ const allDocs = await Promise.all(tmpDataDocs);
312
+ await Promise.all(
313
+ allDocs.map((doc) => {
314
+ const tmpData = [];
315
+ tmpData.unshift(displayableDataToViewData(doc));
316
+
317
+ // [ ] remove/replace this after the vue 3 migration is complete
318
+ // see PR #510
319
+ if (isConstructor(tmpView)) {
320
+ const view = new tmpView();
321
+ view.data = tmpData;
322
+
323
+ this.cardPreview[c.id] = view.toString();
324
+ } else {
325
+ this.cardPreview[c.id] = tmpView.name ? tmpView.name : 'Unknown';
326
+ }
327
+ })
328
+ );
329
+ })
330
+ );
331
+ } catch (error) {
332
+ console.error('Error populating table data:', error);
333
+ } finally {
334
+ this.updatePending = false;
335
+ this.$forceUpdate();
336
+ }
337
+ },
338
+ },
339
+ });
340
+ </script>
341
+
342
+ <style scoped>
343
+ .component-fade-enter-active,
344
+ .component-fade-leave-active {
345
+ transition: opacity 0.5s ease;
346
+ }
347
+ .component-fade-enter, .component-fade-leave-to
348
+ /* .component-fade-leave-active below version 2.1.8 */ {
349
+ opacity: 0;
350
+ }
351
+
352
+ .component-scale-enter-active,
353
+ .component-scale-leave-active {
354
+ max-height: auto;
355
+ transform: scale(1, 1);
356
+ transform-origin: top;
357
+ transition: transform 0.3s ease, max-height 0.3s ease;
358
+ }
359
+ .component-scale-enter,
360
+ .component-fade-leave-to {
361
+ max-height: 0px;
362
+ transform: scale(1, 0);
363
+ overflow: hidden;
364
+ }
365
+ </style>
@@ -0,0 +1,164 @@
1
+ <template>
2
+ <v-card>
3
+ <v-toolbar flat color="primary" dark>
4
+ <v-card-title class="text-h6 font-weight-regular"> Start a New Quilt </v-card-title>
5
+ <v-spacer></v-spacer>
6
+ <v-btn icon @click="clearFormAndDismiss">
7
+ <v-icon icon="mdi-close"></v-icon>
8
+ </v-btn>
9
+ </v-toolbar>
10
+ <v-form>
11
+ <v-container>
12
+ <v-row class="cols sm md">
13
+ <v-text-field
14
+ v-model="courseName"
15
+ counter="30"
16
+ :rules="nameRules"
17
+ label="Quilt Name"
18
+ required
19
+ hint="Short and descriptive"
20
+ data-cy="course-name-input"
21
+ ></v-text-field>
22
+ </v-row>
23
+ <v-row class="cols sm md">
24
+ <v-textarea
25
+ v-model="description"
26
+ variant="outlined"
27
+ counter="300"
28
+ auto-grow
29
+ label="Quilt Description"
30
+ hint="Describe the course. What subject is covered? Who might be interested?"
31
+ data-cy="course-description-input"
32
+ >
33
+ </v-textarea>
34
+ </v-row>
35
+ <v-row class="cols sm md">
36
+ <label>Public or private quilt?</label>
37
+ <v-radio-group
38
+ v-model="publicCourse"
39
+ required
40
+ hint="Private quilts can be shared and collaborated on with other individual users, but will not be accessable without an invitation. A private quilt can be made public later."
41
+ persistent-hint
42
+ inline
43
+ data-cy="course-visibility-radio"
44
+ >
45
+ <v-radio label="Public" :value="true" data-cy="public-radio"></v-radio>
46
+ <v-radio label="Private" :value="false" data-cy="private-radio"></v-radio>
47
+ </v-radio-group>
48
+ </v-row>
49
+ <v-row class="cols sm md">
50
+ <v-btn :loading="updatePending" color="primary" data-cy="save-course-button" @click="submit">
51
+ Save Course Changes
52
+ </v-btn>
53
+ </v-row>
54
+ </v-container>
55
+ </v-form>
56
+ </v-card>
57
+ </template>
58
+
59
+ <script lang="ts">
60
+ import { defineComponent } from 'vue';
61
+ import Mousetrap from 'mousetrap';
62
+ import serverRequest from '../../server';
63
+ import { CourseConfig, CreateCourse, DataShape55, QuestionType55, ServerRequestType } from '@vue-skuilder/common';
64
+ import { alertUser, getCurrentUser } from '@vue-skuilder/common-ui';
65
+
66
+ export default defineComponent({
67
+ name: 'CourseEditor',
68
+
69
+ props: {
70
+ name: {
71
+ type: String,
72
+ required: false,
73
+ default: '',
74
+ },
75
+ },
76
+
77
+ emits: ['CourseEditingComplete'],
78
+
79
+ data() {
80
+ return {
81
+ mousetrap: new Mousetrap(this.$el),
82
+ id: '',
83
+ courseName: '',
84
+ description: '',
85
+ publicCourse: false,
86
+ deleted: false,
87
+ admins: [] as string[],
88
+ moderators: [] as string[],
89
+ dataShapes: [] as DataShape55[],
90
+ questionTypes: [] as QuestionType55[],
91
+ banner: undefined as Blob | undefined,
92
+ thumb: undefined as Blob | undefined,
93
+ updatePending: false,
94
+ nameRules: [
95
+ (value: string): string | boolean => {
96
+ const max = 30;
97
+ if (value.length > max) {
98
+ return `Course name must be ${max} characters or less`;
99
+ } else {
100
+ return true;
101
+ }
102
+ },
103
+ ],
104
+ };
105
+ },
106
+
107
+ created() {
108
+ this.mousetrap.bind('esc', this.clearFormAndDismiss);
109
+ },
110
+
111
+ methods: {
112
+ async submit() {
113
+ this.updatePending = true;
114
+
115
+ const u = await getCurrentUser();
116
+
117
+ const config: CourseConfig = {
118
+ name: this.courseName,
119
+ description: this.description,
120
+ public: this.publicCourse,
121
+ deleted: this.deleted,
122
+ creator: u.getUsername(),
123
+ admins: this.admins,
124
+ moderators: this.moderators,
125
+ dataShapes: this.dataShapes,
126
+ questionTypes: this.questionTypes,
127
+ };
128
+
129
+ const result = await serverRequest<CreateCourse>({
130
+ data: config,
131
+ type: ServerRequestType.CREATE_COURSE,
132
+ response: null,
133
+ user: u.getUsername(),
134
+ });
135
+
136
+ if (result.response && result.response.ok) {
137
+ alertUser({
138
+ text: `Course ${this.courseName} created.`,
139
+ status: result.response!.status,
140
+ });
141
+ this.clearFormAndDismiss();
142
+ } else {
143
+ alertUser({
144
+ text: `Failed to create course ${this.courseName}.`,
145
+ status: result.response!.status,
146
+ });
147
+ console.warn(`Resp: ${JSON.stringify(result.response)}`);
148
+ }
149
+
150
+ this.updatePending = false;
151
+ },
152
+
153
+ clearFormAndDismiss() {
154
+ this.description = '';
155
+ this.publicCourse = false;
156
+ this.deleted = false;
157
+ this.admins = [];
158
+ this.moderators = [];
159
+
160
+ this.$emit('CourseEditingComplete');
161
+ },
162
+ },
163
+ });
164
+ </script>
@@ -0,0 +1,164 @@
1
+ <template>
2
+ <div v-if="!updatePending">
3
+ <h1 class="text-h4 mb-2"><router-link to="/q">Quilts</router-link> / {{ courseConfig.name }}</h1>
4
+
5
+ <p class="text-body-2">
6
+ {{ courseConfig.description }}
7
+ </p>
8
+
9
+ <transition name="component-fade" mode="out-in">
10
+ <div v-if="userIsRegistered">
11
+ <router-link :to="`/study/${courseId}`" class="me-2">
12
+ <v-btn color="success">Start a study session</v-btn>
13
+ </router-link>
14
+ <router-link :to="`/edit/${courseId}`" class="me-2">
15
+ <v-btn data-cy="add-content-btn" color="indigo-lighten-1">
16
+ <v-icon start>mdi-plus</v-icon>
17
+ Add content
18
+ </v-btn>
19
+ </router-link>
20
+ <router-link :to="`/courses/${courseId}/elo`" class="me-2">
21
+ <v-btn color="green-darken-2" title="Rank course content for difficulty">
22
+ <v-icon start>mdi-format-list-numbered</v-icon>
23
+ Arrange
24
+ </v-btn>
25
+ </router-link>
26
+ <v-btn color="error" size="small" variant="outlined" @click="drop"> Drop this course </v-btn>
27
+ </div>
28
+ <div v-else>
29
+ <v-btn data-cy="register-btn" color="primary" class="me-2" @click="register">Register</v-btn>
30
+ <router-link :to="`/q/${courseId}/preview`">
31
+ <v-btn variant="outlined" color="primary" class="me-2">Start a trial study session</v-btn>
32
+ </router-link>
33
+ </div>
34
+ </transition>
35
+ <midi-config v-if="isPianoCourse" :_id="courseId" :user="user" class="my-3" />
36
+
37
+ <v-card class="my-2">
38
+ <v-toolbar density="compact">
39
+ <v-toolbar-title>Tags</v-toolbar-title>
40
+ <v-toolbar-items>
41
+ <v-btn variant="text">({{ tags.length }})</v-btn>
42
+ </v-toolbar-items>
43
+ </v-toolbar>
44
+ <v-card-text>
45
+ <span v-for="(tag, i) in tags" :key="i">
46
+ <router-link :to="`/q/${courseId}/tags/${tag.name}`">
47
+ <v-chip variant="tonal" class="me-2 mb-2">
48
+ {{ tag.name }}
49
+ </v-chip>
50
+ </router-link>
51
+ </span>
52
+ </v-card-text>
53
+ </v-card>
54
+
55
+ <course-card-browser class="my-3" :course-id="courseId" />
56
+ </div>
57
+ </template>
58
+
59
+ <script lang="ts">
60
+ import { defineComponent, PropType } from 'vue';
61
+ import { MidiConfig } from '@vue-skuilder/courses';
62
+ import CourseCardBrowser from './CourseCardBrowser.vue';
63
+ import { log } from '@vue-skuilder/common';
64
+ import { CourseDBInterface, Tag, UserDBInterface, getDataLayer } from '@vue-skuilder/db';
65
+ import { CourseConfig } from '@vue-skuilder/common';
66
+ import { getCurrentUser } from '@vue-skuilder/common-ui';
67
+
68
+ export default defineComponent({
69
+ name: 'CourseInformation',
70
+
71
+ components: {
72
+ MidiConfig,
73
+ CourseCardBrowser,
74
+ },
75
+
76
+ props: {
77
+ courseId: {
78
+ type: String as PropType<string>,
79
+ required: true,
80
+ },
81
+ },
82
+
83
+ data() {
84
+ return {
85
+ courseDB: null as CourseDBInterface | null,
86
+ nameRules: [
87
+ (value: string): string | boolean => {
88
+ const max = 30;
89
+ return value.length > max ? `Course name must be ${max} characters or less` : true;
90
+ },
91
+ ],
92
+ updatePending: true,
93
+ courseConfig: {} as CourseConfig,
94
+ userIsRegistered: false,
95
+ tags: [] as Tag[],
96
+ user: null as UserDBInterface | null,
97
+ };
98
+ },
99
+
100
+ computed: {
101
+ isPianoCourse(): boolean {
102
+ return this.courseConfig.name.toLowerCase().includes('piano');
103
+ },
104
+ },
105
+
106
+ async created() {
107
+ this.courseDB = getDataLayer().getCourseDB(this.courseId);
108
+ this.user = await getCurrentUser();
109
+
110
+ const userCourses = await this.user.getCourseRegistrationsDoc();
111
+ this.userIsRegistered =
112
+ userCourses.courses.filter((c) => {
113
+ return c.courseID === this.courseId && (c.status === 'active' || c.status === undefined);
114
+ }).length === 1;
115
+
116
+ this.courseConfig = (await this.courseDB!.getCourseConfig())!;
117
+ this.tags = (await this.courseDB!.getCourseTagStubs()).rows.map((r) => r.doc!);
118
+ this.updatePending = false;
119
+ },
120
+
121
+ methods: {
122
+ async register() {
123
+ log(`Registering for ${this.courseId}`);
124
+ const res = await this.user!.registerForCourse(this.courseId);
125
+ if (res.ok) {
126
+ this.userIsRegistered = true;
127
+ }
128
+ },
129
+
130
+ async drop() {
131
+ log(`Dropping course ${this.courseId}`);
132
+ const res = await this.user!.dropCourse(this.courseId);
133
+ if (res.ok) {
134
+ this.userIsRegistered = false;
135
+ }
136
+ },
137
+ },
138
+ });
139
+ </script>
140
+
141
+ <style scoped>
142
+ .component-fade-enter-active,
143
+ .component-fade-leave-active {
144
+ transition: opacity 0.5s ease;
145
+ }
146
+ .component-fade-enter,
147
+ .component-fade-leave-to {
148
+ opacity: 0;
149
+ }
150
+
151
+ .component-scale-enter-active,
152
+ .component-scale-leave-active {
153
+ max-height: auto;
154
+ transform: scale(1, 1);
155
+ transform-origin: top;
156
+ transition: transform 0.3s ease, max-height 0.3s ease;
157
+ }
158
+ .component-scale-enter,
159
+ .component-fade-leave-to {
160
+ max-height: 0px;
161
+ transform: scale(1, 0);
162
+ overflow: hidden;
163
+ }
164
+ </style>