@vue-skuilder/common-ui 0.1.4 → 0.1.6
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.
- package/dist/assets/index.css +2 -2
- package/dist/common-ui.es.js +1471 -295
- package/dist/common-ui.es.js.map +1 -1
- package/dist/common-ui.umd.js +2 -2
- package/dist/common-ui.umd.js.map +1 -1
- package/dist/components/HeatMap.types.d.ts +1 -0
- package/dist/components/HeatMap.types.d.ts.map +1 -0
- package/dist/components/PaginatingToolbar.types.d.ts +1 -0
- package/dist/components/PaginatingToolbar.types.d.ts.map +1 -0
- package/dist/components/SkMouseTrap.types.d.ts +1 -0
- package/dist/components/SkMouseTrap.types.d.ts.map +1 -0
- package/dist/components/SkMouseTrapToolTip.types.d.ts +1 -0
- package/dist/components/SkMouseTrapToolTip.types.d.ts.map +1 -0
- package/dist/components/SnackbarService.d.ts +1 -0
- package/dist/components/SnackbarService.d.ts.map +1 -0
- package/dist/components/StudySession.types.d.ts +1 -0
- package/dist/components/StudySession.types.d.ts.map +1 -0
- package/dist/components/auth/index.d.ts +1 -0
- package/dist/components/auth/index.d.ts.map +1 -0
- package/dist/components/cardRendering/MarkdownRendererHelpers.d.ts +1 -0
- package/dist/components/cardRendering/MarkdownRendererHelpers.d.ts.map +1 -0
- package/dist/components/studentInputs/BaseUserInput.d.ts +1 -0
- package/dist/components/studentInputs/BaseUserInput.d.ts.map +1 -0
- package/dist/components/studentInputs/RadioMultipleChoice.types.d.ts +1 -0
- package/dist/components/studentInputs/RadioMultipleChoice.types.d.ts.map +1 -0
- package/dist/composables/CompositionViewable.d.ts +1 -0
- package/dist/composables/CompositionViewable.d.ts.map +1 -0
- package/dist/composables/Displayable.d.ts +1 -0
- package/dist/composables/Displayable.d.ts.map +1 -0
- package/dist/composables/__tests__/useAuthUI.test.d.ts +2 -0
- package/dist/composables/__tests__/useAuthUI.test.d.ts.map +1 -0
- package/dist/composables/index.d.ts +2 -0
- package/dist/composables/index.d.ts.map +1 -0
- package/dist/composables/useAuthUI.d.ts +15 -0
- package/dist/composables/useAuthUI.d.ts.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/plugins/pinia.d.ts +1 -0
- package/dist/plugins/pinia.d.ts.map +1 -0
- package/dist/stores/useAuthStore.d.ts +21 -0
- package/dist/stores/useAuthStore.d.ts.map +1 -0
- package/dist/stores/useCardPreviewModeStore.d.ts +1 -0
- package/dist/stores/useCardPreviewModeStore.d.ts.map +1 -0
- package/dist/stores/useConfigStore.d.ts +1 -0
- package/dist/stores/useConfigStore.d.ts.map +1 -0
- package/dist/utils/SkldrMouseTrap.d.ts +1 -0
- package/dist/utils/SkldrMouseTrap.d.ts.map +1 -0
- package/package.json +8 -3
- package/src/components/CardBrowser.vue +81 -0
- package/src/components/CourseCardBrowser.vue +384 -0
- package/src/components/CourseInformation.vue +194 -0
- package/src/components/PaginatingToolbar.vue +1 -1
- package/src/components/SnackbarService.vue +1 -3
- package/src/components/StudySession.vue +52 -23
- package/src/components/TagsInput.vue +247 -0
- package/src/components/auth/UserChip.vue +146 -58
- package/src/components/auth/UserLoginAndRegistrationContainer.vue +17 -2
- package/src/components/cardRendering/MarkdownRendererHelpers.ts +2 -2
- package/src/components/studentInputs/BaseUserInput.ts +0 -1
- package/src/composables/__tests__/useAuthUI.test.ts +103 -0
- package/src/composables/index.ts +1 -0
- package/src/composables/useAuthUI.ts +67 -0
- package/src/index.ts +8 -0
- package/src/plugins/pinia.ts +1 -1
- package/src/stores/useAuthStore.ts +19 -0
|
@@ -0,0 +1,384 @@
|
|
|
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-if="editMode === 'full'"
|
|
43
|
+
v-model="c.isOpen"
|
|
44
|
+
location="left center"
|
|
45
|
+
transition="slide-x-transition"
|
|
46
|
+
style="display: flex; flex-direction: row-reverse"
|
|
47
|
+
persistent
|
|
48
|
+
>
|
|
49
|
+
<template #activator="{ props }">
|
|
50
|
+
<v-btn
|
|
51
|
+
v-bind="props"
|
|
52
|
+
:icon="c.isOpen ? 'mdi-close' : 'mdi-plus'"
|
|
53
|
+
size="small"
|
|
54
|
+
variant="text"
|
|
55
|
+
@click="clearSelections(c.id)"
|
|
56
|
+
/>
|
|
57
|
+
</template>
|
|
58
|
+
|
|
59
|
+
<v-btn
|
|
60
|
+
key="tags"
|
|
61
|
+
icon
|
|
62
|
+
size="small"
|
|
63
|
+
:variant="internalEditMode !== 'tags' ? 'outlined' : 'elevated'"
|
|
64
|
+
:color="internalEditMode === 'tags' ? 'teal' : 'teal-darken-3'"
|
|
65
|
+
@click.stop="internalEditMode = 'tags'"
|
|
66
|
+
>
|
|
67
|
+
<v-icon>mdi-bookmark</v-icon>
|
|
68
|
+
</v-btn>
|
|
69
|
+
|
|
70
|
+
<v-btn
|
|
71
|
+
key="flag"
|
|
72
|
+
icon
|
|
73
|
+
size="small"
|
|
74
|
+
:variant="internalEditMode !== 'flag' ? 'outlined' : 'elevated'"
|
|
75
|
+
:color="internalEditMode === 'flag' ? 'error' : 'error-darken-3'"
|
|
76
|
+
@click.stop="internalEditMode = 'flag'"
|
|
77
|
+
>
|
|
78
|
+
<v-icon>mdi-flag</v-icon>
|
|
79
|
+
</v-btn>
|
|
80
|
+
</v-speed-dial>
|
|
81
|
+
</template>
|
|
82
|
+
</v-list-item>
|
|
83
|
+
|
|
84
|
+
<div v-if="c.isOpen" class="px-4 py-2 bg-blue-grey-lighten-5">
|
|
85
|
+
<card-loader :qualified_id="c.id" :view-lookup="viewLookup" class="elevation-1" />
|
|
86
|
+
|
|
87
|
+
<tags-input
|
|
88
|
+
v-show="internalEditMode === 'tags'"
|
|
89
|
+
:course-i-d="courseId"
|
|
90
|
+
:card-i-d="c.id.split('-')[1]"
|
|
91
|
+
class="mt-4"
|
|
92
|
+
/>
|
|
93
|
+
|
|
94
|
+
<div v-show="internalEditMode === 'flag'" class="mt-4">
|
|
95
|
+
<v-btn color="error" variant="outlined" @click="c.delBtn = true"> Delete this card </v-btn>
|
|
96
|
+
<span v-if="c.delBtn" class="ml-4">
|
|
97
|
+
<span class="mr-2">Are you sure?</span>
|
|
98
|
+
<v-btn color="error" variant="elevated" @click="deleteCard(c.id)"> Confirm </v-btn>
|
|
99
|
+
</span>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</template>
|
|
103
|
+
</v-list>
|
|
104
|
+
|
|
105
|
+
<paginating-toolbar
|
|
106
|
+
class="elevation-0"
|
|
107
|
+
:page="page"
|
|
108
|
+
:pages="pages"
|
|
109
|
+
@first="first"
|
|
110
|
+
@prev="prev"
|
|
111
|
+
@next="next"
|
|
112
|
+
@last="last"
|
|
113
|
+
@set-page="(n) => setPage(n)"
|
|
114
|
+
/>
|
|
115
|
+
</div>
|
|
116
|
+
</v-card>
|
|
117
|
+
</template>
|
|
118
|
+
|
|
119
|
+
<script lang="ts">
|
|
120
|
+
import { defineComponent, PropType } from 'vue';
|
|
121
|
+
import { displayableDataToViewData, Status } from '@vue-skuilder/common';
|
|
122
|
+
import { getDataLayer, CourseDBInterface, CardData, DisplayableData, Tag } from '@vue-skuilder/db';
|
|
123
|
+
// local imports
|
|
124
|
+
import TagsInput from './TagsInput.vue';
|
|
125
|
+
import PaginatingToolbar from './PaginatingToolbar.vue';
|
|
126
|
+
import { ViewComponent } from '../composables/Displayable';
|
|
127
|
+
import CardLoader from './cardRendering/CardLoader.vue';
|
|
128
|
+
import { alertUser } from './SnackbarService';
|
|
129
|
+
|
|
130
|
+
function isConstructor(obj: unknown) {
|
|
131
|
+
try {
|
|
132
|
+
// @ts-expect-error - we are specifically probing an unknown object
|
|
133
|
+
new obj();
|
|
134
|
+
return true;
|
|
135
|
+
} catch (e) {
|
|
136
|
+
console.warn(`not a constructor: ${obj}, err: ${e}`);
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export default defineComponent({
|
|
142
|
+
name: 'CourseCardBrowser',
|
|
143
|
+
|
|
144
|
+
components: {
|
|
145
|
+
CardLoader,
|
|
146
|
+
TagsInput,
|
|
147
|
+
PaginatingToolbar,
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
props: {
|
|
151
|
+
courseId: {
|
|
152
|
+
type: String,
|
|
153
|
+
required: true,
|
|
154
|
+
},
|
|
155
|
+
tagId: {
|
|
156
|
+
type: String,
|
|
157
|
+
required: false,
|
|
158
|
+
default: '',
|
|
159
|
+
},
|
|
160
|
+
viewLookupFunction: {
|
|
161
|
+
type: Function,
|
|
162
|
+
required: true,
|
|
163
|
+
default: (x: unknown) => {
|
|
164
|
+
console.warn('No viewLookupFunction provided to CourseCardBrowser');
|
|
165
|
+
return null;
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
editMode: {
|
|
169
|
+
type: String as PropType<'none' | 'readonly' | 'full'>,
|
|
170
|
+
required: false,
|
|
171
|
+
default: 'full',
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
data() {
|
|
176
|
+
return {
|
|
177
|
+
courseDB: null as CourseDBInterface | null,
|
|
178
|
+
page: 1,
|
|
179
|
+
pages: [] as number[],
|
|
180
|
+
cards: [] as { id: string; isOpen: boolean; delBtn: boolean }[],
|
|
181
|
+
cardData: {} as { [card: string]: string[] },
|
|
182
|
+
cardPreview: {} as { [card: string]: string },
|
|
183
|
+
internalEditMode: 'none' as 'tags' | 'flag' | 'none',
|
|
184
|
+
delBtn: false,
|
|
185
|
+
updatePending: true,
|
|
186
|
+
userIsRegistered: false,
|
|
187
|
+
questionCount: 0,
|
|
188
|
+
tags: [] as Tag[],
|
|
189
|
+
viewLookup: this.viewLookupFunction,
|
|
190
|
+
};
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
async created() {
|
|
194
|
+
try {
|
|
195
|
+
this.courseDB = getDataLayer().getCourseDB(this.courseId);
|
|
196
|
+
|
|
197
|
+
if (this.tagId) {
|
|
198
|
+
this.questionCount = (await this.courseDB.getTag(this.tagId)).taggedCards.length;
|
|
199
|
+
} else {
|
|
200
|
+
this.questionCount = (await this.courseDB!.getCourseInfo()).cardCount;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
for (let i = 1; (i - 1) * 25 < this.questionCount; i++) {
|
|
204
|
+
this.pages.push(i);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
await this.populateTableData();
|
|
208
|
+
} catch (error) {
|
|
209
|
+
console.error('Error initializing CourseCardBrowser:', error);
|
|
210
|
+
} finally {
|
|
211
|
+
this.updatePending = false;
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
methods: {
|
|
216
|
+
first() {
|
|
217
|
+
this.page = 1;
|
|
218
|
+
this.populateTableData();
|
|
219
|
+
},
|
|
220
|
+
prev() {
|
|
221
|
+
this.page--;
|
|
222
|
+
this.populateTableData();
|
|
223
|
+
},
|
|
224
|
+
next() {
|
|
225
|
+
this.page++;
|
|
226
|
+
this.populateTableData();
|
|
227
|
+
},
|
|
228
|
+
last() {
|
|
229
|
+
this.page = this.pages.length;
|
|
230
|
+
this.populateTableData();
|
|
231
|
+
},
|
|
232
|
+
setPage(n: number) {
|
|
233
|
+
this.page = n;
|
|
234
|
+
this.populateTableData();
|
|
235
|
+
},
|
|
236
|
+
clearSelections(exception: string = '') {
|
|
237
|
+
this.cards.forEach((card) => {
|
|
238
|
+
if (card.id !== exception) {
|
|
239
|
+
card.isOpen = false;
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
this.internalEditMode = 'none';
|
|
243
|
+
this.delBtn = false;
|
|
244
|
+
},
|
|
245
|
+
async deleteCard(c: string) {
|
|
246
|
+
console.log(`Deleting card ${c}`);
|
|
247
|
+
const res = await this.courseDB!.removeCard(c.split('-')[1]);
|
|
248
|
+
if (res.ok) {
|
|
249
|
+
this.cards = this.cards.filter((card) => card.id != c);
|
|
250
|
+
this.clearSelections();
|
|
251
|
+
} else {
|
|
252
|
+
console.error(`Failed to delete card:\n\n${JSON.stringify(res)}`);
|
|
253
|
+
alertUser({
|
|
254
|
+
text: 'Failed to delete card',
|
|
255
|
+
status: Status.error,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
async populateTableData() {
|
|
260
|
+
this.updatePending = true;
|
|
261
|
+
if (this.tagId) {
|
|
262
|
+
const tag = await this.courseDB!.getTag(this.tagId);
|
|
263
|
+
this.cards = tag.taggedCards.map((c) => {
|
|
264
|
+
return { id: `${this.courseId}-${c}`, isOpen: false, delBtn: false };
|
|
265
|
+
});
|
|
266
|
+
} else {
|
|
267
|
+
this.cards = (await this.courseDB!.getCardsByELO(0, 25)).map((c) => {
|
|
268
|
+
return {
|
|
269
|
+
id: c,
|
|
270
|
+
isOpen: false,
|
|
271
|
+
delBtn: false,
|
|
272
|
+
};
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const toRemove: string[] = [];
|
|
277
|
+
const hydratedCardData = (
|
|
278
|
+
await this.courseDB!.getCourseDocs<CardData>(
|
|
279
|
+
this.cards.map((c) => c.id.split('-')[1]),
|
|
280
|
+
{
|
|
281
|
+
include_docs: true,
|
|
282
|
+
}
|
|
283
|
+
)
|
|
284
|
+
).rows
|
|
285
|
+
.filter((r) => {
|
|
286
|
+
if (r.doc) {
|
|
287
|
+
return true;
|
|
288
|
+
} else {
|
|
289
|
+
console.error(`Card ${r.id}.doc not found.\ncard: ${JSON.stringify(r)}`);
|
|
290
|
+
// toRemove.push(r.id);
|
|
291
|
+
// if (this.tagId) {
|
|
292
|
+
// this.courseDB!.removeTagFromCard(r.id, this.tagId);
|
|
293
|
+
// }
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
})
|
|
297
|
+
.map((r) => r.doc!);
|
|
298
|
+
|
|
299
|
+
this.cards = this.cards.filter((c) => !toRemove.includes(c.id.split('-')[1]));
|
|
300
|
+
|
|
301
|
+
hydratedCardData.forEach((c) => {
|
|
302
|
+
if (c && c.id_displayable_data) {
|
|
303
|
+
this.cardData[c._id] = c.id_displayable_data;
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
await Promise.all(
|
|
309
|
+
this.cards.map(async (c) => {
|
|
310
|
+
const _cardID: string = c.id.split('-')[1];
|
|
311
|
+
|
|
312
|
+
const tmpCardData = hydratedCardData.find((c) => c._id == _cardID);
|
|
313
|
+
if (!tmpCardData || !tmpCardData.id_displayable_data) {
|
|
314
|
+
console.error(`No valid data found for card ${_cardID}`);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const tmpView: ViewComponent = this.viewLookupFunction(
|
|
318
|
+
tmpCardData.id_view || 'default.question.BlanksCard.FillInView'
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
const tmpDataDocs = tmpCardData.id_displayable_data.map((id) => {
|
|
322
|
+
return this.courseDB!.getCourseDoc<DisplayableData>(id, {
|
|
323
|
+
attachments: false,
|
|
324
|
+
binary: true,
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
const allDocs = await Promise.all(tmpDataDocs);
|
|
329
|
+
await Promise.all(
|
|
330
|
+
allDocs.map((doc) => {
|
|
331
|
+
const tmpData = [];
|
|
332
|
+
tmpData.unshift(displayableDataToViewData(doc));
|
|
333
|
+
|
|
334
|
+
// [ ] remove/replace this after the vue 3 migration is complete
|
|
335
|
+
// see PR #510
|
|
336
|
+
if (isConstructor(tmpView)) {
|
|
337
|
+
const view = new tmpView();
|
|
338
|
+
view.data = tmpData;
|
|
339
|
+
|
|
340
|
+
this.cardPreview[c.id] = view.toString();
|
|
341
|
+
} else {
|
|
342
|
+
this.cardPreview[c.id] = tmpView.name ? tmpView.name : 'Unknown';
|
|
343
|
+
}
|
|
344
|
+
})
|
|
345
|
+
);
|
|
346
|
+
})
|
|
347
|
+
);
|
|
348
|
+
} catch (error) {
|
|
349
|
+
console.error('Error populating table data:', error);
|
|
350
|
+
} finally {
|
|
351
|
+
this.updatePending = false;
|
|
352
|
+
this.$forceUpdate();
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
});
|
|
357
|
+
</script>
|
|
358
|
+
|
|
359
|
+
<style scoped>
|
|
360
|
+
.component-fade-enter-active,
|
|
361
|
+
.component-fade-leave-active {
|
|
362
|
+
transition: opacity 0.5s ease;
|
|
363
|
+
}
|
|
364
|
+
.component-fade-enter, .component-fade-leave-to
|
|
365
|
+
/* .component-fade-leave-active below version 2.1.8 */ {
|
|
366
|
+
opacity: 0;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.component-scale-enter-active,
|
|
370
|
+
.component-scale-leave-active {
|
|
371
|
+
max-height: auto;
|
|
372
|
+
transform: scale(1, 1);
|
|
373
|
+
transform-origin: top;
|
|
374
|
+
transition:
|
|
375
|
+
transform 0.3s ease,
|
|
376
|
+
max-height 0.3s ease;
|
|
377
|
+
}
|
|
378
|
+
.component-scale-enter,
|
|
379
|
+
.component-fade-leave-to {
|
|
380
|
+
max-height: 0px;
|
|
381
|
+
transform: scale(1, 0);
|
|
382
|
+
overflow: hidden;
|
|
383
|
+
}
|
|
384
|
+
</style>
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div v-if="!updatePending">
|
|
3
|
+
<slot name="header" :course-config="courseConfig" :course-id="courseId">
|
|
4
|
+
<h1 class="text-h4 mb-2">{{ courseConfig.name }}</h1>
|
|
5
|
+
</slot>
|
|
6
|
+
|
|
7
|
+
<p class="text-body-2">
|
|
8
|
+
{{ courseConfig.description }}
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<slot
|
|
12
|
+
name="actions"
|
|
13
|
+
:user-is-registered="userIsRegistered"
|
|
14
|
+
:course-id="courseId"
|
|
15
|
+
:edit-mode="editMode"
|
|
16
|
+
:register="register"
|
|
17
|
+
:drop="drop"
|
|
18
|
+
>
|
|
19
|
+
<!-- Default fallback content if no actions slot provided -->
|
|
20
|
+
<transition name="component-fade" mode="out-in">
|
|
21
|
+
<div v-if="userIsRegistered">
|
|
22
|
+
<v-btn color="success" class="me-2">Start a study session</v-btn>
|
|
23
|
+
<v-btn v-if="editMode === 'full'" data-cy="add-content-btn" color="indigo-lighten-1" class="me-2">
|
|
24
|
+
<v-icon start>mdi-plus</v-icon>
|
|
25
|
+
Add content
|
|
26
|
+
</v-btn>
|
|
27
|
+
<v-btn
|
|
28
|
+
v-if="editMode === 'full'"
|
|
29
|
+
color="green-darken-2"
|
|
30
|
+
title="Rank course content for difficulty"
|
|
31
|
+
class="me-2"
|
|
32
|
+
>
|
|
33
|
+
<v-icon start>mdi-format-list-numbered</v-icon>
|
|
34
|
+
Arrange
|
|
35
|
+
</v-btn>
|
|
36
|
+
<v-btn v-if="editMode === 'full'" color="error" size="small" variant="outlined" @click="drop">
|
|
37
|
+
Drop this course
|
|
38
|
+
</v-btn>
|
|
39
|
+
</div>
|
|
40
|
+
<div v-else>
|
|
41
|
+
<v-btn data-cy="register-btn" color="primary" class="me-2" @click="register">Register</v-btn>
|
|
42
|
+
<v-btn variant="outlined" color="primary" class="me-2">Start a trial study session</v-btn>
|
|
43
|
+
</div>
|
|
44
|
+
</transition>
|
|
45
|
+
</slot>
|
|
46
|
+
|
|
47
|
+
<slot name="additional-content"></slot>
|
|
48
|
+
|
|
49
|
+
<v-card class="my-2">
|
|
50
|
+
<v-toolbar density="compact">
|
|
51
|
+
<v-toolbar-title>Tags</v-toolbar-title>
|
|
52
|
+
<v-toolbar-items>
|
|
53
|
+
<v-btn variant="text">({{ tags.length }})</v-btn>
|
|
54
|
+
</v-toolbar-items>
|
|
55
|
+
</v-toolbar>
|
|
56
|
+
<v-card-text>
|
|
57
|
+
<span v-for="(tag, i) in tags" :key="i">
|
|
58
|
+
<slot name="tag-link" :tag="tag" :course-id="courseId">
|
|
59
|
+
<v-chip variant="tonal" class="me-2 mb-2">
|
|
60
|
+
{{ tag.name }}
|
|
61
|
+
</v-chip>
|
|
62
|
+
</slot>
|
|
63
|
+
</span>
|
|
64
|
+
</v-card-text>
|
|
65
|
+
</v-card>
|
|
66
|
+
|
|
67
|
+
<course-card-browser
|
|
68
|
+
class="my-3"
|
|
69
|
+
:course-id="courseId"
|
|
70
|
+
:view-lookup-function="viewLookupFunction"
|
|
71
|
+
:edit-mode="editMode"
|
|
72
|
+
/>
|
|
73
|
+
</div>
|
|
74
|
+
</template>
|
|
75
|
+
|
|
76
|
+
<script lang="ts">
|
|
77
|
+
import { defineComponent, PropType } from 'vue';
|
|
78
|
+
// import { MidiConfig } from '@vue-skuilder/courses'; // Removed to break circular dependency
|
|
79
|
+
import CourseCardBrowser from './CourseCardBrowser.vue';
|
|
80
|
+
import { log } from '@vue-skuilder/common';
|
|
81
|
+
import { CourseDBInterface, Tag, UserDBInterface, getDataLayer } from '@vue-skuilder/db';
|
|
82
|
+
import { CourseConfig } from '@vue-skuilder/common';
|
|
83
|
+
import { getCurrentUser } from '../stores/useAuthStore';
|
|
84
|
+
|
|
85
|
+
export default defineComponent({
|
|
86
|
+
name: 'CourseInformation',
|
|
87
|
+
|
|
88
|
+
components: {
|
|
89
|
+
// MidiConfig, // Removed to break circular dependency
|
|
90
|
+
CourseCardBrowser,
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
props: {
|
|
94
|
+
courseId: {
|
|
95
|
+
type: String as PropType<string>,
|
|
96
|
+
required: true,
|
|
97
|
+
},
|
|
98
|
+
viewLookupFunction: {
|
|
99
|
+
type: Function,
|
|
100
|
+
required: false,
|
|
101
|
+
default: (x: unknown) => {
|
|
102
|
+
console.warn('No viewLookupFunction provided to CourseInformation');
|
|
103
|
+
return null;
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
editMode: {
|
|
107
|
+
type: String as PropType<'none' | 'readonly' | 'full'>,
|
|
108
|
+
required: false,
|
|
109
|
+
default: 'full',
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
data() {
|
|
114
|
+
return {
|
|
115
|
+
courseDB: null as CourseDBInterface | null,
|
|
116
|
+
nameRules: [
|
|
117
|
+
(value: string): string | boolean => {
|
|
118
|
+
const max = 30;
|
|
119
|
+
return value.length > max ? `Course name must be ${max} characters or less` : true;
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
updatePending: true,
|
|
123
|
+
courseConfig: {} as CourseConfig,
|
|
124
|
+
userIsRegistered: false,
|
|
125
|
+
tags: [] as Tag[],
|
|
126
|
+
user: null as UserDBInterface | null,
|
|
127
|
+
};
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
computed: {
|
|
131
|
+
// isPianoCourse removed - piano-specific logic should be in wrapper component
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
async created() {
|
|
135
|
+
this.courseDB = getDataLayer().getCourseDB(this.courseId);
|
|
136
|
+
this.user = await getCurrentUser();
|
|
137
|
+
|
|
138
|
+
const userCourses = await this.user.getCourseRegistrationsDoc();
|
|
139
|
+
this.userIsRegistered =
|
|
140
|
+
userCourses.courses.filter((c) => {
|
|
141
|
+
return c.courseID === this.courseId && (c.status === 'active' || c.status === undefined);
|
|
142
|
+
}).length === 1;
|
|
143
|
+
|
|
144
|
+
this.courseConfig = (await this.courseDB!.getCourseConfig())!;
|
|
145
|
+
this.tags = (await this.courseDB!.getCourseTagStubs()).rows.map((r) => r.doc!);
|
|
146
|
+
this.updatePending = false;
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
methods: {
|
|
150
|
+
async register() {
|
|
151
|
+
log(`Registering for ${this.courseId}`);
|
|
152
|
+
const res = await this.user!.registerForCourse(this.courseId);
|
|
153
|
+
if (res.ok) {
|
|
154
|
+
this.userIsRegistered = true;
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
async drop() {
|
|
159
|
+
log(`Dropping course ${this.courseId}`);
|
|
160
|
+
const res = await this.user!.dropCourse(this.courseId);
|
|
161
|
+
if (res.ok) {
|
|
162
|
+
this.userIsRegistered = false;
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
</script>
|
|
168
|
+
|
|
169
|
+
<style scoped>
|
|
170
|
+
.component-fade-enter-active,
|
|
171
|
+
.component-fade-leave-active {
|
|
172
|
+
transition: opacity 0.5s ease;
|
|
173
|
+
}
|
|
174
|
+
.component-fade-enter,
|
|
175
|
+
.component-fade-leave-to {
|
|
176
|
+
opacity: 0;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.component-scale-enter-active,
|
|
180
|
+
.component-scale-leave-active {
|
|
181
|
+
max-height: auto;
|
|
182
|
+
transform: scale(1, 1);
|
|
183
|
+
transform-origin: top;
|
|
184
|
+
transition:
|
|
185
|
+
transform 0.3s ease,
|
|
186
|
+
max-height 0.3s ease;
|
|
187
|
+
}
|
|
188
|
+
.component-scale-enter,
|
|
189
|
+
.component-fade-leave-to {
|
|
190
|
+
max-height: 0px;
|
|
191
|
+
transform: scale(1, 0);
|
|
192
|
+
overflow: hidden;
|
|
193
|
+
}
|
|
194
|
+
</style>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<v-toolbar density="compact">
|
|
3
3
|
<v-toolbar-title>
|
|
4
4
|
<span>{{ title }}</span>
|
|
5
|
-
<span v-if="subtitle" class="ms-2 text-subtitle-2">{{ subtitle }}</span>
|
|
5
|
+
<span v-if="subtitle" class="ms-2 text-subtitle-2" data-cy="paginating-toolbar-subtitle">{{ subtitle }}</span>
|
|
6
6
|
</v-toolbar-title>
|
|
7
7
|
<v-spacer></v-spacer>
|
|
8
8
|
<v-btn variant="text" icon color="secondary" :disabled="page == 1" @click="$emit('first')">
|
|
@@ -23,7 +23,7 @@ import { defineComponent } from 'vue';
|
|
|
23
23
|
import { Status } from '@vue-skuilder/common';
|
|
24
24
|
import { SnackbarOptions, setInstance } from './SnackbarService';
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
export default defineComponent({
|
|
27
27
|
name: 'SnackbarService',
|
|
28
28
|
|
|
29
29
|
data() {
|
|
@@ -66,6 +66,4 @@ const SnackbarService = defineComponent({
|
|
|
66
66
|
},
|
|
67
67
|
},
|
|
68
68
|
});
|
|
69
|
-
|
|
70
|
-
export default SnackbarService;
|
|
71
69
|
</script>
|