@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.
- package/LICENCE +661 -0
- package/README.md +64 -0
- package/dist/assets/Roboto-Black-B0ZKieaB.woff +0 -0
- package/dist/assets/Roboto-Black-VhoA2qKx.woff2 +0 -0
- package/dist/assets/Roboto-BlackItalic-D0gSnuIb.woff +0 -0
- package/dist/assets/Roboto-BlackItalic-D4yie1YO.woff2 +0 -0
- package/dist/assets/Roboto-Bold-D9plYbeK.woff +0 -0
- package/dist/assets/Roboto-Bold-hN3duQhD.woff2 +0 -0
- package/dist/assets/Roboto-BoldItalic-BWDm51uc.woff2 +0 -0
- package/dist/assets/Roboto-BoldItalic-CyLKvOHD.woff +0 -0
- package/dist/assets/Roboto-Light-Cu-PAxXt.woff +0 -0
- package/dist/assets/Roboto-Light-DHTugVNA.woff2 +0 -0
- package/dist/assets/Roboto-LightItalic-CZg5kHIB.woff +0 -0
- package/dist/assets/Roboto-LightItalic-JQyp2Y3P.woff2 +0 -0
- package/dist/assets/Roboto-Medium-ByKogCTi.woff2 +0 -0
- package/dist/assets/Roboto-Medium-b81vv18W.woff +0 -0
- package/dist/assets/Roboto-MediumItalic-DFQ-RYa0.woff +0 -0
- package/dist/assets/Roboto-MediumItalic-i1eR0KbF.woff2 +0 -0
- package/dist/assets/Roboto-Regular-BX5l9hRW.woff +0 -0
- package/dist/assets/Roboto-Regular-C6rbFxYz.woff2 +0 -0
- package/dist/assets/Roboto-RegularItalic-BjnLZsam.woff +0 -0
- package/dist/assets/Roboto-RegularItalic-CvPUdkvM.woff2 +0 -0
- package/dist/assets/Roboto-Thin-BfJvJcog.woff +0 -0
- package/dist/assets/Roboto-Thin-NicBC1pN.woff2 +0 -0
- package/dist/assets/Roboto-ThinItalic-CKlCjrO_.woff2 +0 -0
- package/dist/assets/Roboto-ThinItalic-DnIWFxRE.woff +0 -0
- package/dist/assets/index-CQ-sNKGW.css +14 -0
- package/dist/assets/index-EbqpUgvM.js +161 -0
- package/dist/assets/materialdesignicons-webfont-B7mPwVP_.ttf +0 -0
- package/dist/assets/materialdesignicons-webfont-CSr8KVlo.eot +0 -0
- package/dist/assets/materialdesignicons-webfont-Dp5v-WZN.woff2 +0 -0
- package/dist/assets/materialdesignicons-webfont-PXm3-2wK.woff +0 -0
- package/dist/assets/workbox-window.prod.es5-p40uij6f.js +1 -0
- package/dist/favicon.ico +0 -0
- package/dist/img/icons/safari-pinned-tab.svg +149 -0
- package/dist/index.html +19 -0
- package/dist/manifest.json +20 -0
- package/dist/manifest.webmanifest +1 -0
- package/dist/robots.txt +2 -0
- package/dist/sw.js +1 -0
- package/dist/workbox-1be04862.js +1 -0
- package/package.json +105 -0
- package/src/App.vue +156 -0
- package/src/ENVIRONMENT_VARS.ts +79 -0
- package/src/components/Classrooms/ClassroomCtrlPanel.vue +206 -0
- package/src/components/Classrooms/CreateClassroom.vue +159 -0
- package/src/components/Classrooms/JoinCode.vue +83 -0
- package/src/components/Courses/CourseCardBrowser.vue +365 -0
- package/src/components/Courses/CourseEditor.vue +164 -0
- package/src/components/Courses/CourseInformation.vue +164 -0
- package/src/components/Courses/CourseRouter.vue +116 -0
- package/src/components/Courses/CourseStubCard.vue +76 -0
- package/src/components/Courses/EloModeration.vue +122 -0
- package/src/components/Courses/TagInformation.vue +209 -0
- package/src/components/Edit/BulkImport/CardPreviewList.vue +345 -0
- package/src/components/Edit/BulkImportView.vue +633 -0
- package/src/components/Edit/CardBrowser.vue +79 -0
- package/src/components/Edit/ComponentRegistration/ComponentRegistration.vue +235 -0
- package/src/components/Edit/ComponentRegistration/UnregisteredComponentsTable.vue +19 -0
- package/src/components/Edit/CourseEditor.vue +162 -0
- package/src/components/Edit/NavigationStrategy/NavigationStrategyEditor.vue +170 -0
- package/src/components/Edit/NavigationStrategy/NavigationStrategyList.vue +92 -0
- package/src/components/Edit/TagsInput.vue +247 -0
- package/src/components/Edit/ViewableDataInputForm/DataInputForm.vue +524 -0
- package/src/components/Edit/ViewableDataInputForm/FieldInput.types.ts +33 -0
- package/src/components/Edit/ViewableDataInputForm/FieldInputs/AudioInput.vue +188 -0
- package/src/components/Edit/ViewableDataInputForm/FieldInputs/ChessPuzzleInput.vue +79 -0
- package/src/components/Edit/ViewableDataInputForm/FieldInputs/FieldInput.css +12 -0
- package/src/components/Edit/ViewableDataInputForm/FieldInputs/ImageInput.vue +231 -0
- package/src/components/Edit/ViewableDataInputForm/FieldInputs/IntegerInput.vue +49 -0
- package/src/components/Edit/ViewableDataInputForm/FieldInputs/MarkdownInput.vue +34 -0
- package/src/components/Edit/ViewableDataInputForm/FieldInputs/MediaDragDropUploader.vue +246 -0
- package/src/components/Edit/ViewableDataInputForm/FieldInputs/MidiInput.vue +113 -0
- package/src/components/Edit/ViewableDataInputForm/FieldInputs/NumberInput.vue +49 -0
- package/src/components/Edit/ViewableDataInputForm/FieldInputs/StringInput.vue +49 -0
- package/src/components/Edit/ViewableDataInputForm/FieldInputs/typeValidators.ts +49 -0
- package/src/components/Edit/ViewableDataInputForm/OptionsFieldInput.ts +161 -0
- package/src/components/Study/SessionConfiguration.vue +371 -0
- package/src/components/TextSwap.vue +65 -0
- package/src/components/User/UserStats.vue +30 -0
- package/src/dev/DataInputFormTester.vue +117 -0
- package/src/dev/readme.md +3 -0
- package/src/enums.ts +0 -0
- package/src/glyphs.txt +933 -0
- package/src/main.ts +45 -0
- package/src/plugins/vuetify.ts +41 -0
- package/src/registerServiceWorker.ts +18 -0
- package/src/router.ts +184 -0
- package/src/server/index.spec.ts +192 -0
- package/src/server/index.ts +71 -0
- package/src/shims-vue.d.ts +5 -0
- package/src/store.mock.ts +122 -0
- package/src/stores/useDataInputFormStore.ts +49 -0
- package/src/stores/useFieldInputStore.ts +191 -0
- package/src/types/shims-vuetify.d.ts +12 -0
- package/src/types/svg.d.ts +4 -0
- package/src/utils/bulkImport/index.ts +94 -0
- package/src/views/About.vue +29 -0
- package/src/views/Admin.vue +128 -0
- package/src/views/Classrooms.vue +258 -0
- package/src/views/Courses.vue +265 -0
- package/src/views/Home.vue +154 -0
- package/src/views/Login.vue +75 -0
- package/src/views/ReleaseNotes.vue +20 -0
- package/src/views/SignUp.vue +32 -0
- package/src/views/Study.vue +261 -0
- package/src/views/User.vue +109 -0
- package/src/vite-env.d.ts +1 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-container fluid>
|
|
3
|
+
<v-row class="pa-4" justify="space-around">
|
|
4
|
+
<!-- Student Classes -->
|
|
5
|
+
<v-col v-if="studentClasses.length > 0" cols="12" sm="12" md="4">
|
|
6
|
+
<v-card>
|
|
7
|
+
<v-toolbar color="primary">
|
|
8
|
+
<v-toolbar-title>My Classes</v-toolbar-title>
|
|
9
|
+
</v-toolbar>
|
|
10
|
+
|
|
11
|
+
<v-list>
|
|
12
|
+
<transition-group name="component-fade" mode="out-in">
|
|
13
|
+
<v-list-item v-for="classroom in studentClasses" :key="classroom._id" :value="classroom">
|
|
14
|
+
<v-list-item-title>
|
|
15
|
+
{{ classroom.name }}
|
|
16
|
+
</v-list-item-title>
|
|
17
|
+
|
|
18
|
+
<template #append>
|
|
19
|
+
<v-btn
|
|
20
|
+
size="small"
|
|
21
|
+
color="secondary"
|
|
22
|
+
:loading="spinnerMap[classroom._id] !== undefined"
|
|
23
|
+
@click="leaveClass(classroom._id)"
|
|
24
|
+
>
|
|
25
|
+
Leave this class
|
|
26
|
+
</v-btn>
|
|
27
|
+
</template>
|
|
28
|
+
</v-list-item>
|
|
29
|
+
</transition-group>
|
|
30
|
+
</v-list>
|
|
31
|
+
</v-card>
|
|
32
|
+
</v-col>
|
|
33
|
+
|
|
34
|
+
<!-- Teacher Classes -->
|
|
35
|
+
<v-col v-if="teacherClasses.length > 0" cols="12" sm="12" md="4">
|
|
36
|
+
<v-card>
|
|
37
|
+
<v-toolbar color="primary">
|
|
38
|
+
<v-toolbar-title>Classes I Manage</v-toolbar-title>
|
|
39
|
+
</v-toolbar>
|
|
40
|
+
|
|
41
|
+
<v-list>
|
|
42
|
+
<transition-group name="component-fade" mode="out-in">
|
|
43
|
+
<v-list-item v-for="classroom in teacherClasses" :key="classroom._id" :value="classroom">
|
|
44
|
+
<v-list-item-title>
|
|
45
|
+
{{ classroom.name }}
|
|
46
|
+
</v-list-item-title>
|
|
47
|
+
|
|
48
|
+
<template #append>
|
|
49
|
+
<v-btn
|
|
50
|
+
size="small"
|
|
51
|
+
color="secondary"
|
|
52
|
+
:loading="spinnerMap[classroom._id] !== undefined"
|
|
53
|
+
:to="`/classrooms/${classroom._id}`"
|
|
54
|
+
>
|
|
55
|
+
Open
|
|
56
|
+
</v-btn>
|
|
57
|
+
</template>
|
|
58
|
+
</v-list-item>
|
|
59
|
+
</transition-group>
|
|
60
|
+
</v-list>
|
|
61
|
+
</v-card>
|
|
62
|
+
</v-col>
|
|
63
|
+
|
|
64
|
+
<!-- Empty State Message -->
|
|
65
|
+
<v-col v-if="studentClasses.length === 0 && teacherClasses.length === 0" class="text-h5 text-center">
|
|
66
|
+
You are not in any classes! Join your class below if someone has given you a joincode. Or else, start your own!
|
|
67
|
+
</v-col>
|
|
68
|
+
</v-row>
|
|
69
|
+
|
|
70
|
+
<v-divider class="my-4"></v-divider>
|
|
71
|
+
|
|
72
|
+
<!-- Join Class Form -->
|
|
73
|
+
<v-row class="pa-4">
|
|
74
|
+
<v-col cols="12" sm="12" md="8" lg="6" xl="6">
|
|
75
|
+
<v-card>
|
|
76
|
+
<v-toolbar color="primary">
|
|
77
|
+
<v-toolbar-title>Join a class</v-toolbar-title>
|
|
78
|
+
</v-toolbar>
|
|
79
|
+
<v-card-text>
|
|
80
|
+
<v-text-field v-model="joinCode" label="Class Code" variant="outlined"></v-text-field>
|
|
81
|
+
<v-btn color="primary" @click="joinClass">Join</v-btn>
|
|
82
|
+
</v-card-text>
|
|
83
|
+
</v-card>
|
|
84
|
+
</v-col>
|
|
85
|
+
</v-row>
|
|
86
|
+
|
|
87
|
+
<!-- New Class Dialog -->
|
|
88
|
+
<v-dialog v-model="newClassDialog" fullscreen transition="dialog-bottom-transition" :scrim="false">
|
|
89
|
+
<template #activator="{ props }">
|
|
90
|
+
<v-btn color="primary" v-bind="props">Start a new Class</v-btn>
|
|
91
|
+
</template>
|
|
92
|
+
<classroom-editor @classroom-editing-complete="processResponse" />
|
|
93
|
+
</v-dialog>
|
|
94
|
+
</v-container>
|
|
95
|
+
</template>
|
|
96
|
+
|
|
97
|
+
<script lang="ts">
|
|
98
|
+
import { defineComponent } from 'vue';
|
|
99
|
+
import { Status, ServerRequestType, JoinClassroom, LeaveClassroom, DeleteClassroom } from '@vue-skuilder/common';
|
|
100
|
+
import { getDataLayer, UserDBInterface } from '@vue-skuilder/db';
|
|
101
|
+
import serverRequest from '@/server/index';
|
|
102
|
+
import { alertUser } from '@vue-skuilder/common-ui';
|
|
103
|
+
import ClassroomEditor from '@/components/Classrooms/CreateClassroom.vue';
|
|
104
|
+
import { getCurrentUser } from '@vue-skuilder/common-ui';
|
|
105
|
+
|
|
106
|
+
interface CourseReg {
|
|
107
|
+
_id: string;
|
|
108
|
+
name: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export default defineComponent({
|
|
112
|
+
name: 'ClassroomsView',
|
|
113
|
+
|
|
114
|
+
components: {
|
|
115
|
+
ClassroomEditor,
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
beforeRouteEnter(to, from, next) {
|
|
119
|
+
// todo ?
|
|
120
|
+
// See https://router.vuejs.org/guide/advanced/data-fetching.html#fetching-before-navigation
|
|
121
|
+
// this.refreshData().then(() => {
|
|
122
|
+
// next();
|
|
123
|
+
// });
|
|
124
|
+
next();
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
data() {
|
|
128
|
+
return {
|
|
129
|
+
classes: [] as string[],
|
|
130
|
+
joinCode: '',
|
|
131
|
+
studentClasses: [] as CourseReg[],
|
|
132
|
+
teacherClasses: [] as CourseReg[],
|
|
133
|
+
spinnerMap: {} as { [index: string]: boolean },
|
|
134
|
+
newClassDialog: false,
|
|
135
|
+
user: null as UserDBInterface | null,
|
|
136
|
+
};
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
created() {
|
|
140
|
+
this.refreshData();
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
methods: {
|
|
144
|
+
async refreshData() {
|
|
145
|
+
this.$data.user = await getCurrentUser();
|
|
146
|
+
const registrations = (await this.user!.getUserClassrooms()).registrations;
|
|
147
|
+
const studentClasses: CourseReg[] = [];
|
|
148
|
+
const teacherClasses: CourseReg[] = [];
|
|
149
|
+
|
|
150
|
+
registrations.forEach(async (reg) => {
|
|
151
|
+
const cfg = (
|
|
152
|
+
await getDataLayer().getClassroomDB(
|
|
153
|
+
reg.classID,
|
|
154
|
+
reg.registeredAs == 'teacher' || reg.registeredAs == 'student' ? reg.registeredAs : 'student'
|
|
155
|
+
)
|
|
156
|
+
).getConfig();
|
|
157
|
+
console.log(`Registered class: ${JSON.stringify(cfg)}`);
|
|
158
|
+
const regItem = {
|
|
159
|
+
_id: reg.classID,
|
|
160
|
+
name: cfg.name,
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
if (reg.registeredAs === 'student') {
|
|
164
|
+
studentClasses.push(regItem);
|
|
165
|
+
} else if (reg.registeredAs === 'teacher') {
|
|
166
|
+
teacherClasses.push(regItem);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
this.studentClasses = studentClasses;
|
|
171
|
+
this.teacherClasses = teacherClasses;
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
async deleteClass(classId: string) {
|
|
175
|
+
const result = await serverRequest<DeleteClassroom>({
|
|
176
|
+
type: ServerRequestType.DELETE_CLASSROOM,
|
|
177
|
+
user: this.user?.getUsername() || '',
|
|
178
|
+
classID: classId,
|
|
179
|
+
response: null,
|
|
180
|
+
});
|
|
181
|
+
if (result.response && result.response.ok) {
|
|
182
|
+
alertUser({
|
|
183
|
+
text: `Class deleted successfully.`,
|
|
184
|
+
status: Status.ok,
|
|
185
|
+
});
|
|
186
|
+
} else {
|
|
187
|
+
alertUser({
|
|
188
|
+
text: `Failed to delete class. Please try again.`,
|
|
189
|
+
status: Status.error,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
async leaveClass(classID: string) {
|
|
195
|
+
this.spinnerMap[classID] = true;
|
|
196
|
+
console.log(`Attempting to drop class: ${classID}`);
|
|
197
|
+
|
|
198
|
+
const result = await serverRequest<LeaveClassroom>({
|
|
199
|
+
type: ServerRequestType.LEAVE_CLASSROOM,
|
|
200
|
+
data: {
|
|
201
|
+
classID: classID,
|
|
202
|
+
},
|
|
203
|
+
user: this.user?.getUsername() || '',
|
|
204
|
+
response: null,
|
|
205
|
+
});
|
|
206
|
+
if (result.response && result.response.ok) {
|
|
207
|
+
if (this.user) {
|
|
208
|
+
await this.user!.dropFromClassroom(classID);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
delete this.spinnerMap[classID];
|
|
213
|
+
this.refreshData();
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
async joinClass() {
|
|
217
|
+
const result = await serverRequest<JoinClassroom>({
|
|
218
|
+
type: ServerRequestType.JOIN_CLASSROOM,
|
|
219
|
+
data: {
|
|
220
|
+
joinCode: this.joinCode,
|
|
221
|
+
registerAs: 'student',
|
|
222
|
+
user: this.user?.getUsername() || '',
|
|
223
|
+
},
|
|
224
|
+
user: this.user?.getUsername() || '',
|
|
225
|
+
response: null,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
if (result.response && result.response.ok) {
|
|
229
|
+
console.log(`Adding registration to userDB...`);
|
|
230
|
+
if (this.user) {
|
|
231
|
+
await registerUserForClassroom(this.user.getUsername(), result.response!.id_course, 'student');
|
|
232
|
+
alertUser({
|
|
233
|
+
text: `Successfully joined class: ${result.response.course_name}.`,
|
|
234
|
+
status: Status.ok,
|
|
235
|
+
});
|
|
236
|
+
} else {
|
|
237
|
+
alertUser({
|
|
238
|
+
text: `Failed to join class. [Unknown current user.]`,
|
|
239
|
+
status: Status.error,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
} else {
|
|
243
|
+
if (result.response) {
|
|
244
|
+
alertUser({
|
|
245
|
+
text: result.response.errorText!,
|
|
246
|
+
status: Status.error,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
this.refreshData();
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
async processResponse() {
|
|
254
|
+
this.newClassDialog = !this.newClassDialog;
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
</script>
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-container fluid>
|
|
3
|
+
<!-- Fixed Action Button - Updated position classes -->
|
|
4
|
+
<v-btn
|
|
5
|
+
color="primary"
|
|
6
|
+
fixed
|
|
7
|
+
location="bottom right"
|
|
8
|
+
icon="mdi-plus"
|
|
9
|
+
size="large"
|
|
10
|
+
class="mb-4 mr-4"
|
|
11
|
+
data-cy="create-course-fab"
|
|
12
|
+
v-bind="newCourseAttrs"
|
|
13
|
+
v-on="newCourseAttrs.on"
|
|
14
|
+
/>
|
|
15
|
+
|
|
16
|
+
<!-- Main Content Area -->
|
|
17
|
+
<v-row>
|
|
18
|
+
<!-- My Quilts Panel -->
|
|
19
|
+
<v-col cols="12">
|
|
20
|
+
<v-expansion-panels v-model="myQuiltsPanel">
|
|
21
|
+
<v-expansion-panel>
|
|
22
|
+
<v-expansion-panel-title data-cy="registered-quilts-panel">My Registered Quilts</v-expansion-panel-title>
|
|
23
|
+
<v-expansion-panel-text>
|
|
24
|
+
<v-row>
|
|
25
|
+
<v-col v-for="course in registeredCourses" :key="course.courseID" cols="12" sm="6" md="4" lg="3">
|
|
26
|
+
<v-card variant="outlined" density="compact" class="pa-2">
|
|
27
|
+
<div class="d-flex align-center justify-space-between">
|
|
28
|
+
<div data-cy="registered-course" class="d-flex align-center">
|
|
29
|
+
<router-link
|
|
30
|
+
:to="`/q/${course.name.replaceAll(' ', '_')}`"
|
|
31
|
+
class="text-subtitle-2"
|
|
32
|
+
data-cy="registered-course-title"
|
|
33
|
+
>
|
|
34
|
+
{{ course.name }}
|
|
35
|
+
</router-link>
|
|
36
|
+
<v-icon v-if="!course.public" size="x-small" class="ml-1">mdi-eye-off</v-icon>
|
|
37
|
+
</div>
|
|
38
|
+
<v-btn
|
|
39
|
+
size="x-small"
|
|
40
|
+
variant="text"
|
|
41
|
+
color="error"
|
|
42
|
+
data-cy="drop-course-button"
|
|
43
|
+
:loading="spinnerMap[course.courseID] !== undefined"
|
|
44
|
+
@click="dropCourse(course.courseID)"
|
|
45
|
+
>
|
|
46
|
+
Drop
|
|
47
|
+
</v-btn>
|
|
48
|
+
</div>
|
|
49
|
+
</v-card>
|
|
50
|
+
</v-col>
|
|
51
|
+
</v-row>
|
|
52
|
+
</v-expansion-panel-text>
|
|
53
|
+
</v-expansion-panel>
|
|
54
|
+
</v-expansion-panels>
|
|
55
|
+
</v-col>
|
|
56
|
+
|
|
57
|
+
<!-- Available Quilts Section -->
|
|
58
|
+
<v-col cols="12" class="mt-4">
|
|
59
|
+
<h2 class="text-h5 mb-3">Available Quilts</h2>
|
|
60
|
+
<v-row>
|
|
61
|
+
<v-col v-for="course in displayedAvailableCourses" :key="course.courseID" cols="12" sm="6" md="4" lg="3">
|
|
62
|
+
<course-stub-card data-cy="available-course-card" :course-id="course.courseID" @refresh="refreshData" />
|
|
63
|
+
</v-col>
|
|
64
|
+
</v-row>
|
|
65
|
+
|
|
66
|
+
<!-- Show More Button -->
|
|
67
|
+
<v-row v-if="hasMoreCourses" justify="center" class="mt-2">
|
|
68
|
+
<v-btn variant="text" color="primary" data-cy="courses-show-more-button" @click="toggleShowMore">
|
|
69
|
+
{{ showAllCourses ? 'Show Less' : 'Show More' }}
|
|
70
|
+
</v-btn>
|
|
71
|
+
</v-row>
|
|
72
|
+
</v-col>
|
|
73
|
+
</v-row>
|
|
74
|
+
|
|
75
|
+
<!-- New Course Dialog -->
|
|
76
|
+
<v-dialog v-model="newCourseDialog" fullscreen transition="dialog-bottom-transition" :scrim="false">
|
|
77
|
+
<course-editor @course-editing-complete="processResponse()" />
|
|
78
|
+
</v-dialog>
|
|
79
|
+
</v-container>
|
|
80
|
+
</template>
|
|
81
|
+
|
|
82
|
+
<script lang="ts">
|
|
83
|
+
import { defineComponent } from 'vue';
|
|
84
|
+
import CourseEditor from '@/components/Courses/CourseEditor.vue';
|
|
85
|
+
import CourseStubCard from '@/components/Courses/CourseStubCard.vue';
|
|
86
|
+
import _ from 'lodash';
|
|
87
|
+
import serverRequest from '../server';
|
|
88
|
+
import { ServerRequestType, CourseConfig, Status } from '@vue-skuilder/common';
|
|
89
|
+
import { alertUser, getCurrentUser } from '@vue-skuilder/common-ui';
|
|
90
|
+
import { UserDBInterface, getDataLayer } from '@vue-skuilder/db';
|
|
91
|
+
|
|
92
|
+
type DBCourseConfig = CourseConfig & {
|
|
93
|
+
courseID: string;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export default defineComponent({
|
|
97
|
+
name: 'CoursesView',
|
|
98
|
+
|
|
99
|
+
components: {
|
|
100
|
+
CourseEditor,
|
|
101
|
+
CourseStubCard,
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
data() {
|
|
105
|
+
return {
|
|
106
|
+
existingCourses: [] as DBCourseConfig[],
|
|
107
|
+
registeredCourses: [] as DBCourseConfig[],
|
|
108
|
+
awaitingCreateCourse: false,
|
|
109
|
+
spinnerMap: {} as { [key: string]: boolean },
|
|
110
|
+
newCourseDialog: false,
|
|
111
|
+
user: null as UserDBInterface | null,
|
|
112
|
+
myQuiltsPanel: 0, // Controls expansion panel
|
|
113
|
+
showAllCourses: false,
|
|
114
|
+
coursesPerPage: 8,
|
|
115
|
+
};
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
computed: {
|
|
119
|
+
availableCourses(): DBCourseConfig[] {
|
|
120
|
+
const availableCourses = _.without(this.existingCourses, ...this.registeredCourses);
|
|
121
|
+
const user = this.user?.getUsername();
|
|
122
|
+
|
|
123
|
+
const viewableCourses = availableCourses.filter((course) => {
|
|
124
|
+
if (!user) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
const viewable: boolean =
|
|
128
|
+
course.public ||
|
|
129
|
+
course.creator === user ||
|
|
130
|
+
course.admins.indexOf(user) !== -1 ||
|
|
131
|
+
course.moderators.indexOf(user) !== -1;
|
|
132
|
+
|
|
133
|
+
return viewable;
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
return viewableCourses;
|
|
137
|
+
},
|
|
138
|
+
displayedAvailableCourses(): DBCourseConfig[] {
|
|
139
|
+
if (this.showAllCourses) {
|
|
140
|
+
return this.availableCourses;
|
|
141
|
+
}
|
|
142
|
+
return this.availableCourses.slice(0, this.coursesPerPage);
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
hasMoreCourses(): boolean {
|
|
146
|
+
return this.availableCourses.length > this.coursesPerPage;
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
newCourseAttrs() {
|
|
150
|
+
return {
|
|
151
|
+
attrs: {
|
|
152
|
+
'aria-label': 'Create new quilt',
|
|
153
|
+
},
|
|
154
|
+
on: {
|
|
155
|
+
click: () => (this.newCourseDialog = true),
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
async created() {
|
|
162
|
+
this.user = await getCurrentUser();
|
|
163
|
+
this.refreshData();
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
methods: {
|
|
167
|
+
processResponse(): void {
|
|
168
|
+
this.newCourseDialog = false;
|
|
169
|
+
this.refreshData();
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
toggleShowMore() {
|
|
173
|
+
this.showAllCourses = !this.showAllCourses;
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
async refreshData(): Promise<void> {
|
|
177
|
+
console.log(`Pulling user course data...`);
|
|
178
|
+
try {
|
|
179
|
+
const userCourseIDs = (await this.user!.getCourseRegistrationsDoc()).courses
|
|
180
|
+
.filter((c) => {
|
|
181
|
+
return c.status === 'active' || c.status === 'maintenance-mode' || c.status === undefined;
|
|
182
|
+
})
|
|
183
|
+
.map((c) => {
|
|
184
|
+
return c.courseID;
|
|
185
|
+
});
|
|
186
|
+
console.log(`userCourseIDs: ${userCourseIDs}`);
|
|
187
|
+
|
|
188
|
+
this.existingCourses = (await getDataLayer().getCoursesDB().getCourseList()) as DBCourseConfig[];
|
|
189
|
+
console.log(`existingCourses: \n\t${this.existingCourses.map((c) => c.name).join(',\n\t')}`);
|
|
190
|
+
|
|
191
|
+
this.registeredCourses = this.existingCourses.filter((course) => {
|
|
192
|
+
let match: boolean = false;
|
|
193
|
+
userCourseIDs.forEach((id: string) => {
|
|
194
|
+
if (course.courseID === id) {
|
|
195
|
+
match = true;
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
return match;
|
|
199
|
+
});
|
|
200
|
+
} catch (e) {
|
|
201
|
+
console.error(`Error refreshing course data:`, e);
|
|
202
|
+
alertUser({
|
|
203
|
+
status: Status.error,
|
|
204
|
+
text: `Failed to load courses: ${e || 'Database access error'}`,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
async createCourse(): Promise<void> {
|
|
209
|
+
this.awaitingCreateCourse = true;
|
|
210
|
+
const resp = await serverRequest({
|
|
211
|
+
type: ServerRequestType.CREATE_COURSE,
|
|
212
|
+
data: {
|
|
213
|
+
name: 'testCourseName',
|
|
214
|
+
description: 'All of these courses will be the same!',
|
|
215
|
+
public: true,
|
|
216
|
+
deleted: false,
|
|
217
|
+
creator: this.user!.getUsername(),
|
|
218
|
+
admins: [this.user!.getUsername()],
|
|
219
|
+
moderators: [],
|
|
220
|
+
dataShapes: [],
|
|
221
|
+
questionTypes: [],
|
|
222
|
+
},
|
|
223
|
+
user: this.user!.getUsername(),
|
|
224
|
+
response: null,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
alertUser({
|
|
228
|
+
status: resp.response!,
|
|
229
|
+
text: `Course ${JSON.stringify(resp)} created`,
|
|
230
|
+
});
|
|
231
|
+
this.awaitingCreateCourse = false;
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
async addCourse(course: string): Promise<void> {
|
|
235
|
+
this.spinnerMap[course] = true;
|
|
236
|
+
console.log(`Attempting to register for ${course}.`);
|
|
237
|
+
await this.user?.registerForCourse(course);
|
|
238
|
+
delete this.spinnerMap[course];
|
|
239
|
+
this.refreshData();
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
async dropCourse(course: string): Promise<void> {
|
|
243
|
+
this.spinnerMap[course] = true;
|
|
244
|
+
console.log(`Attempting to drop ${course}.`);
|
|
245
|
+
await this.user?.dropCourse(course);
|
|
246
|
+
delete this.spinnerMap[course];
|
|
247
|
+
this.refreshData();
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
</script>
|
|
252
|
+
|
|
253
|
+
<style scoped>
|
|
254
|
+
.component-fade-enter-active,
|
|
255
|
+
.component-fade-leave-active {
|
|
256
|
+
transition: all 0.65s ease;
|
|
257
|
+
}
|
|
258
|
+
.component-fade-enter,
|
|
259
|
+
.component-fade-leave-to {
|
|
260
|
+
opacity: 0;
|
|
261
|
+
}
|
|
262
|
+
.componnent-fade-move {
|
|
263
|
+
transition: transform 1s;
|
|
264
|
+
}
|
|
265
|
+
</style>
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-container fluid class="fill-height">
|
|
3
|
+
<v-row align="center">
|
|
4
|
+
<v-col cols="12" class="text-left">
|
|
5
|
+
<h1 :class="{ 'text-h3': display.xs, 'text-h2': display.smAndUp }">
|
|
6
|
+
<span class="font-weight-thin">edu</span>
|
|
7
|
+
<span class="font-weight-bold">Quilt</span>
|
|
8
|
+
</h1>
|
|
9
|
+
<p class="text-h5">
|
|
10
|
+
<em>quilt</em>: (n) a <text-swap ref="swap1" :text="label" /> of
|
|
11
|
+
<text-swap ref="swap2" :text="adjective" /> courseware that <text-swap ref="swap3" :text="subject" /> can
|
|
12
|
+
<text-swap ref="swap4" :text="verb" />
|
|
13
|
+
</p>
|
|
14
|
+
<v-spacer class="my-12"></v-spacer>
|
|
15
|
+
<div class="text-subtitle-1">(get cozy)</div>
|
|
16
|
+
</v-col>
|
|
17
|
+
</v-row>
|
|
18
|
+
</v-container>
|
|
19
|
+
</template>
|
|
20
|
+
|
|
21
|
+
<script lang="ts">
|
|
22
|
+
import TextSwap from '@/components/TextSwap.vue';
|
|
23
|
+
import { defineComponent } from 'vue';
|
|
24
|
+
import { useDisplay } from 'vuetify';
|
|
25
|
+
import { ITextSwap } from '@/components/TextSwap.vue';
|
|
26
|
+
|
|
27
|
+
interface Data {
|
|
28
|
+
swapIntervalID: number | null;
|
|
29
|
+
label: string[];
|
|
30
|
+
adjective: string[];
|
|
31
|
+
subject: string[];
|
|
32
|
+
verb: string[];
|
|
33
|
+
swapsReady: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default defineComponent({
|
|
37
|
+
name: 'HomeView',
|
|
38
|
+
|
|
39
|
+
components: {
|
|
40
|
+
TextSwap,
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
setup() {
|
|
44
|
+
const display = useDisplay();
|
|
45
|
+
return { display };
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
data(): Data {
|
|
49
|
+
return {
|
|
50
|
+
swapIntervalID: null,
|
|
51
|
+
label: ['collection', 'patchwork', 'network', 'jumble'],
|
|
52
|
+
adjective: ['interactive', 'adaptive', 'interlinked', 'intelligent'],
|
|
53
|
+
subject: ['anyone', 'everyone', 'you'],
|
|
54
|
+
verb: ['edit', 'start', 'study', 'improve'],
|
|
55
|
+
swapsReady: false,
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
computed: {
|
|
60
|
+
swaps(): ITextSwap[] {
|
|
61
|
+
return [
|
|
62
|
+
this.$refs.swap1 as ITextSwap,
|
|
63
|
+
this.$refs.swap2 as ITextSwap,
|
|
64
|
+
this.$refs.swap3 as ITextSwap,
|
|
65
|
+
this.$refs.swap4 as ITextSwap,
|
|
66
|
+
];
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
mounted() {
|
|
71
|
+
this.$nextTick(() => {
|
|
72
|
+
this.swapsReady = true;
|
|
73
|
+
this.swapIntervalID = window.setInterval(this.randomSwap, 7000);
|
|
74
|
+
});
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
beforeUnmount() {
|
|
78
|
+
if (this.swapIntervalID !== null) {
|
|
79
|
+
clearInterval(this.swapIntervalID);
|
|
80
|
+
this.swapIntervalID = null;
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
methods: {
|
|
85
|
+
randomSwap(): void {
|
|
86
|
+
if (!this.swapsReady) return;
|
|
87
|
+
|
|
88
|
+
this.swaps.forEach((s) => {
|
|
89
|
+
if (Math.random() < 0.33) s.next();
|
|
90
|
+
});
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
</script>
|
|
95
|
+
|
|
96
|
+
<style scoped>
|
|
97
|
+
.section:first {
|
|
98
|
+
padding: 0px 0px 0px 0px;
|
|
99
|
+
}
|
|
100
|
+
.pitch {
|
|
101
|
+
padding: 50px 0px 0px 0px;
|
|
102
|
+
}
|
|
103
|
+
.section {
|
|
104
|
+
width: 100%;
|
|
105
|
+
transform: skewY(-4deg) !important;
|
|
106
|
+
padding: 200px 0px;
|
|
107
|
+
margin: 0px 0px;
|
|
108
|
+
background-color: transparent;
|
|
109
|
+
}
|
|
110
|
+
.section > * {
|
|
111
|
+
transform: skewY(4deg);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.step1 {
|
|
115
|
+
background-color: skyblue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.step2 {
|
|
119
|
+
background-color: red;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.eduFade {
|
|
123
|
+
animation: fadein 2s;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.quiltedFade {
|
|
127
|
+
/* margin-top: 25px; */
|
|
128
|
+
/* font-size: 21px; */
|
|
129
|
+
/* text-align: center; */
|
|
130
|
+
|
|
131
|
+
animation: delayfadein 2s ease-in-out;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
@keyframes delayfadein {
|
|
135
|
+
0% {
|
|
136
|
+
opacity: 0;
|
|
137
|
+
}
|
|
138
|
+
33% {
|
|
139
|
+
opacity: 0;
|
|
140
|
+
}
|
|
141
|
+
100% {
|
|
142
|
+
opacity: 1;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
@keyframes fadein {
|
|
147
|
+
from {
|
|
148
|
+
opacity: 0;
|
|
149
|
+
}
|
|
150
|
+
to {
|
|
151
|
+
opacity: 1;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
</style>
|