@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,49 @@
|
|
|
1
|
+
import { defineStore } from 'pinia';
|
|
2
|
+
import { DataShape } from '@/base-course/Interfaces/DataShape';
|
|
3
|
+
import { CourseConfig } from '@/server/types';
|
|
4
|
+
import { FieldInputInstance } from '@/components/Edit/ViewableDataInputForm/FieldInput.types';
|
|
5
|
+
import { useFieldInputStore } from './useFieldInputStore';
|
|
6
|
+
import { ViewComponent } from '@/base-course/Displayable';
|
|
7
|
+
|
|
8
|
+
interface DataInputForm {
|
|
9
|
+
// current props
|
|
10
|
+
dataShape: DataShape | null;
|
|
11
|
+
course: CourseConfig | null;
|
|
12
|
+
|
|
13
|
+
shapeViews: ViewComponent[];
|
|
14
|
+
fields: FieldInputInstance[];
|
|
15
|
+
|
|
16
|
+
fieldStore: ReturnType<typeof useFieldInputStore>;
|
|
17
|
+
|
|
18
|
+
uploading: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface DataInputFormState {
|
|
22
|
+
dataInputForm: DataInputForm;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const useDataInputFormStore = defineStore('dataInputForm', {
|
|
26
|
+
state: (): DataInputFormState => ({
|
|
27
|
+
dataInputForm: {
|
|
28
|
+
dataShape: null,
|
|
29
|
+
course: null,
|
|
30
|
+
shapeViews: [],
|
|
31
|
+
|
|
32
|
+
fields: [],
|
|
33
|
+
fieldStore: useFieldInputStore(),
|
|
34
|
+
uploading: false,
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
37
|
+
// actions or getters if needed
|
|
38
|
+
actions: {
|
|
39
|
+
setDataShape(ds: DataShape) {
|
|
40
|
+
this.dataInputForm.dataShape = ds;
|
|
41
|
+
this.dataInputForm.fieldStore.dataShape = ds;
|
|
42
|
+
this.dataInputForm.fieldStore.$reset();
|
|
43
|
+
},
|
|
44
|
+
setCourse(course: CourseConfig) {
|
|
45
|
+
this.dataInputForm.course = course;
|
|
46
|
+
},
|
|
47
|
+
// etc. create any convenience setters or methods you wish
|
|
48
|
+
},
|
|
49
|
+
});
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { DataShape, FieldType, Status, fieldConverters } from '@vue-skuilder/common';
|
|
2
|
+
import { defineStore } from 'pinia';
|
|
3
|
+
|
|
4
|
+
export interface MediaInputs {
|
|
5
|
+
[key: `audio-${number}`]: unknown;
|
|
6
|
+
[key: `image-${number}`]: unknown;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface FieldInputStore {
|
|
10
|
+
// [key: string]: unknown;
|
|
11
|
+
|
|
12
|
+
// // Raw field values by field name
|
|
13
|
+
//
|
|
14
|
+
//
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The
|
|
18
|
+
*/
|
|
19
|
+
dataShape: DataShape | null;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Raw input field values by field name - the user-edited submissions
|
|
23
|
+
*/
|
|
24
|
+
inputs: Record<string, unknown>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* additions from the MediaDragDrop editor.
|
|
28
|
+
*/
|
|
29
|
+
mediaInputs: MediaInputs;
|
|
30
|
+
|
|
31
|
+
inputIsValid: boolean;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Validation status of each field - all must be true for the form to be valid and the data to be submissable
|
|
35
|
+
*/
|
|
36
|
+
validation: Record<string, boolean>;
|
|
37
|
+
/**
|
|
38
|
+
* Input prepared for sending to the database
|
|
39
|
+
*/
|
|
40
|
+
convertedInput: Record<string, unknown>;
|
|
41
|
+
/**
|
|
42
|
+
* Input prepared for local rendering in a preview
|
|
43
|
+
*/
|
|
44
|
+
previewInput: Record<string, unknown>;
|
|
45
|
+
|
|
46
|
+
// getFieldValue(fieldName: string): unknown;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const useFieldInputStore = defineStore('fieldStore', {
|
|
50
|
+
state: (): FieldInputStore => ({
|
|
51
|
+
dataShape: null as DataShape | null,
|
|
52
|
+
inputs: {},
|
|
53
|
+
mediaInputs: {} as MediaInputs,
|
|
54
|
+
inputIsValid: false,
|
|
55
|
+
|
|
56
|
+
validation: {},
|
|
57
|
+
convertedInput: {},
|
|
58
|
+
previewInput: {},
|
|
59
|
+
}),
|
|
60
|
+
|
|
61
|
+
getters: {
|
|
62
|
+
isValidated(): boolean {
|
|
63
|
+
return this.inputIsValid;
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
getPreview: (state) => {
|
|
67
|
+
if (state.inputIsValid) return state.previewInput;
|
|
68
|
+
else return {};
|
|
69
|
+
},
|
|
70
|
+
getConverted: (state) => {
|
|
71
|
+
if (state.inputIsValid) return state.convertedInput;
|
|
72
|
+
else return {};
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
actions: {
|
|
77
|
+
$reset() {
|
|
78
|
+
console.log(`[FieldInputStore].reset()`);
|
|
79
|
+
this.inputs = {};
|
|
80
|
+
this.mediaInputs = {};
|
|
81
|
+
this.validation = {};
|
|
82
|
+
this.convertedInput = {};
|
|
83
|
+
this.previewInput = {};
|
|
84
|
+
this.inputIsValid = false;
|
|
85
|
+
|
|
86
|
+
// Clear all media entries
|
|
87
|
+
const MAX_MEDIA_INPUTS = 10;
|
|
88
|
+
for (let i = 1; i <= MAX_MEDIA_INPUTS; i++) {
|
|
89
|
+
const audioKey = `audio-${i}` as keyof MediaInputs;
|
|
90
|
+
const imageKey = `image-${i}` as keyof MediaInputs;
|
|
91
|
+
|
|
92
|
+
delete this.mediaInputs[audioKey];
|
|
93
|
+
delete this.mediaInputs[imageKey];
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
setFieldValue(fieldName: string, value: unknown) {
|
|
98
|
+
this.inputs[fieldName] = value;
|
|
99
|
+
this.validate();
|
|
100
|
+
this.convert();
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
setMedia(
|
|
104
|
+
fieldName: keyof MediaInputs,
|
|
105
|
+
media: {
|
|
106
|
+
content_type: string;
|
|
107
|
+
data: Blob;
|
|
108
|
+
}
|
|
109
|
+
) {
|
|
110
|
+
console.log(`[FieldInputStore].setMedia: ${fieldName}`);
|
|
111
|
+
this.mediaInputs[fieldName] = media;
|
|
112
|
+
this.convert();
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
validate() {
|
|
116
|
+
this.validation = {};
|
|
117
|
+
|
|
118
|
+
if (this.dataShape === null) {
|
|
119
|
+
this.inputIsValid = false;
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
this.inputIsValid = true;
|
|
124
|
+
|
|
125
|
+
for (const field of this.dataShape.fields) {
|
|
126
|
+
if (field.validator) {
|
|
127
|
+
const result = field.validator.test(this.inputs[field.name] as unknown as string);
|
|
128
|
+
if (result.status === Status.ok) {
|
|
129
|
+
this.validation[field.name] = true;
|
|
130
|
+
} else {
|
|
131
|
+
this.inputIsValid = false;
|
|
132
|
+
this.validation[field.name] = false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
convert() {
|
|
139
|
+
this.convertedInput = {};
|
|
140
|
+
|
|
141
|
+
this.dataShape?.fields.forEach((f) => {
|
|
142
|
+
this.convertedInput[f.name] = fieldConverters[f.type].databaseConverter(
|
|
143
|
+
this.inputs[f.name]
|
|
144
|
+
);
|
|
145
|
+
this.previewInput[f.name] = fieldConverters[f.type].previewConverter(this.inputs[f.name]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Check for media entries
|
|
149
|
+
for (let i = 1; i < 11; i++) {
|
|
150
|
+
const index = `audio-${i}` as keyof MediaInputs;
|
|
151
|
+
const value = this.mediaInputs[index];
|
|
152
|
+
if (value) {
|
|
153
|
+
this.convertedInput[index] = fieldConverters[FieldType.AUDIO].databaseConverter(value);
|
|
154
|
+
this.previewInput[index] = fieldConverters[FieldType.AUDIO].previewConverter(value);
|
|
155
|
+
} else {
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
for (let i = 1; i < 11; i++) {
|
|
161
|
+
const index = `image-${i}` as keyof MediaInputs;
|
|
162
|
+
const value = this.mediaInputs[index];
|
|
163
|
+
if (value) {
|
|
164
|
+
this.convertedInput[index] = fieldConverters[FieldType.IMAGE].databaseConverter(value);
|
|
165
|
+
this.previewInput[index] = fieldConverters[FieldType.IMAGE].previewConverter(value);
|
|
166
|
+
} else {
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
clearMediaEntries() {
|
|
173
|
+
// Clear all media entries up to a reasonable limit
|
|
174
|
+
for (let i = 1; i <= 10; i++) {
|
|
175
|
+
delete this.mediaInputs[`audio-${i}`];
|
|
176
|
+
delete this.mediaInputs[`image-${i}`];
|
|
177
|
+
delete this.convertedInput[`audio-${i}`];
|
|
178
|
+
delete this.convertedInput[`image-${i}`];
|
|
179
|
+
delete this.previewInput[`audio-${i}`];
|
|
180
|
+
delete this.previewInput[`image-${i}`];
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
clearField(fieldName: string) {
|
|
185
|
+
delete this.inputs[fieldName];
|
|
186
|
+
delete this.validation[fieldName];
|
|
187
|
+
delete this.convertedInput[fieldName];
|
|
188
|
+
delete this.previewInput[fieldName];
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
declare module 'vuetify/lib' {
|
|
2
|
+
import Vuetify from 'vuetify';
|
|
3
|
+
export default Vuetify;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
declare module 'vuetify/types' {
|
|
7
|
+
import Vue from 'vue';
|
|
8
|
+
interface Vue {
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
10
|
+
$vuetify: any;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Re-export parsing utilities from common
|
|
2
|
+
export {
|
|
3
|
+
ParsedCard,
|
|
4
|
+
CardData,
|
|
5
|
+
parseCard,
|
|
6
|
+
parseBulkTextToCards,
|
|
7
|
+
isValidBulkFormat,
|
|
8
|
+
splitCardsText,
|
|
9
|
+
CardParserConfig,
|
|
10
|
+
CARD_DELIMITER
|
|
11
|
+
} from '@vue-skuilder/common';
|
|
12
|
+
|
|
13
|
+
// Re-export database utilities from db
|
|
14
|
+
export {
|
|
15
|
+
ImportResult,
|
|
16
|
+
BulkCardProcessorConfig,
|
|
17
|
+
importParsedCards,
|
|
18
|
+
validateProcessorConfig
|
|
19
|
+
} from '@vue-skuilder/db';
|
|
20
|
+
|
|
21
|
+
// Example usage for documentation purposes:
|
|
22
|
+
/*
|
|
23
|
+
import { CourseDBInterface } from '@vue-skuilder/db';
|
|
24
|
+
import { DataShape } from '@vue-skuilder/common';
|
|
25
|
+
import {
|
|
26
|
+
importParsedCards, // Renamed from processBulkCards
|
|
27
|
+
validateProcessorConfig,
|
|
28
|
+
parseCard, // Still useful for single card parsing
|
|
29
|
+
splitCardsText, // Still useful for understanding delimiters or manual splitting
|
|
30
|
+
isValidBulkFormat,
|
|
31
|
+
parseBulkTextToCards // New function for parsing bulk text to ParsedCard[]
|
|
32
|
+
} from './index'; // Assuming this index.ts correctly re-exports them
|
|
33
|
+
|
|
34
|
+
async function exampleUsage(courseDB: CourseDBInterface, dataShape: DataShape) {
|
|
35
|
+
// Example bulk text
|
|
36
|
+
const bulkText = `Card 1 Question
|
|
37
|
+
{{blank}}
|
|
38
|
+
tags: tagA, tagB
|
|
39
|
+
elo: 1500
|
|
40
|
+
---
|
|
41
|
+
---
|
|
42
|
+
Card 2 Question
|
|
43
|
+
Another {{blank}}
|
|
44
|
+
elo: 1200
|
|
45
|
+
tags: tagC`;
|
|
46
|
+
|
|
47
|
+
// Validate config for processing
|
|
48
|
+
const config = {
|
|
49
|
+
dataShape,
|
|
50
|
+
courseCode: 'COURSE-123',
|
|
51
|
+
userName: 'user123'
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const validation = validateProcessorConfig(config);
|
|
55
|
+
if (!validation.isValid) {
|
|
56
|
+
console.error(validation.errorMessage);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Step 1: Parse the bulk text into an array of ParsedCard objects
|
|
61
|
+
// isValidBulkFormat can be used as a preliminary check if needed
|
|
62
|
+
if (!isValidBulkFormat(bulkText)) {
|
|
63
|
+
console.error("Invalid bulk format");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const parsedCardsArray = parseBulkTextToCards(bulkText);
|
|
67
|
+
|
|
68
|
+
if (parsedCardsArray.length === 0 && bulkText.trim().length > 0) {
|
|
69
|
+
console.error("No cards could be parsed from the input.");
|
|
70
|
+
// Potentially show an error or return if no cards were parsed,
|
|
71
|
+
// even if the format had delimiters, maybe all cards were empty.
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Step 2: Process the array of parsed cards
|
|
76
|
+
const results = await importParsedCards(parsedCardsArray, courseDB, config);
|
|
77
|
+
|
|
78
|
+
// Check results
|
|
79
|
+
console.log(`Attempted to import ${parsedCardsArray.length} cards.`);
|
|
80
|
+
console.log(`Successfully imported: ${results.filter(r => r.status === 'success').length}`);
|
|
81
|
+
console.log(`Failed: ${results.filter(r => r.status === 'error').length}`);
|
|
82
|
+
|
|
83
|
+
// Example of parsing a single card (remains the same)
|
|
84
|
+
const singleCard = `Example card with a {{blank}}
|
|
85
|
+
elo: 1600
|
|
86
|
+
tags: tag1, tag2`;
|
|
87
|
+
const parsedSingle = parseCard(singleCard); // Renamed 'parsed' to 'parsedSingle' for clarity
|
|
88
|
+
console.log('Parsed single card:', {
|
|
89
|
+
markdown: parsedSingle?.markdown,
|
|
90
|
+
tags: parsedSingle?.tags,
|
|
91
|
+
elo: parsedSingle?.elo
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
*/
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-container class="about">
|
|
3
|
+
<v-row>
|
|
4
|
+
<v-col>
|
|
5
|
+
<h1 class="text-h2 mb-6">Todo.</h1>
|
|
6
|
+
|
|
7
|
+
<p class="text-body-1 mb-4">
|
|
8
|
+
This is a placeholder for the about page. It will be filled in with more information later.
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<v-btn :to="'./notes'" variant="text" color="primary"> Release Notes </v-btn>
|
|
12
|
+
</v-col>
|
|
13
|
+
</v-row>
|
|
14
|
+
</v-container>
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<script lang="ts">
|
|
18
|
+
import { defineComponent } from 'vue';
|
|
19
|
+
|
|
20
|
+
export default defineComponent({
|
|
21
|
+
name: 'AboutView',
|
|
22
|
+
components: {},
|
|
23
|
+
data() {
|
|
24
|
+
return {
|
|
25
|
+
type: 'Viewable',
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
</script>
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-container>
|
|
3
|
+
<v-row>
|
|
4
|
+
<v-col>
|
|
5
|
+
<h1 class="text-h3 mb-6">{{ title }}</h1>
|
|
6
|
+
|
|
7
|
+
<!-- Users Section -->
|
|
8
|
+
<v-card class="mb-6">
|
|
9
|
+
<v-card-title>
|
|
10
|
+
<h3>Users - {{ registeredUsers.length }}</h3>
|
|
11
|
+
</v-card-title>
|
|
12
|
+
<v-card-text>
|
|
13
|
+
<v-list>
|
|
14
|
+
<v-list-item v-for="u in registeredUsers" :key="u._id">
|
|
15
|
+
<v-list-item-title>User: {{ u.name }}</v-list-item-title>
|
|
16
|
+
</v-list-item>
|
|
17
|
+
</v-list>
|
|
18
|
+
</v-card-text>
|
|
19
|
+
</v-card>
|
|
20
|
+
|
|
21
|
+
<!-- Quilts Section -->
|
|
22
|
+
<v-card class="mb-6">
|
|
23
|
+
<v-card-title>
|
|
24
|
+
<h3>
|
|
25
|
+
<router-link to="/courses" class="text-decoration-none">Quilts</router-link>
|
|
26
|
+
- {{ courses.length }}
|
|
27
|
+
</h3>
|
|
28
|
+
</v-card-title>
|
|
29
|
+
<v-card-text>
|
|
30
|
+
<v-list>
|
|
31
|
+
<v-list-item v-for="c in courses" :key="c._id">
|
|
32
|
+
<v-list-item-title>
|
|
33
|
+
<router-link :to="`/q/${c._id}`" class="text-decoration-none">{{ c.name }}</router-link>
|
|
34
|
+
- {{ c._id }}
|
|
35
|
+
<v-btn density="compact" icon="mdi-close" variant="text" size="small" @click="removeCourse(c._id)" />
|
|
36
|
+
</v-list-item-title>
|
|
37
|
+
</v-list-item>
|
|
38
|
+
</v-list>
|
|
39
|
+
</v-card-text>
|
|
40
|
+
</v-card>
|
|
41
|
+
|
|
42
|
+
<!-- Classrooms Section -->
|
|
43
|
+
<v-card>
|
|
44
|
+
<v-card-title>
|
|
45
|
+
<h3>Classrooms - {{ classrooms.length }}</h3>
|
|
46
|
+
</v-card-title>
|
|
47
|
+
<v-card-text>
|
|
48
|
+
<v-list>
|
|
49
|
+
<v-list-item v-for="c in classrooms" :key="c._id">
|
|
50
|
+
<v-list-item-title>
|
|
51
|
+
<router-link :to="`/c/${c._id}`" class="text-decoration-none">{{ c.name }}</router-link>
|
|
52
|
+
{{ c.name }} - {{ c.teachers }} - {{ c.students.length }} students
|
|
53
|
+
</v-list-item-title>
|
|
54
|
+
</v-list-item>
|
|
55
|
+
</v-list>
|
|
56
|
+
</v-card-text>
|
|
57
|
+
</v-card>
|
|
58
|
+
</v-col>
|
|
59
|
+
</v-row>
|
|
60
|
+
</v-container>
|
|
61
|
+
</template>
|
|
62
|
+
|
|
63
|
+
<script lang="ts">
|
|
64
|
+
import { defineComponent } from 'vue';
|
|
65
|
+
import { AdminDBInterface, getDataLayer } from '@vue-skuilder/db';
|
|
66
|
+
import { GuestUsername } from '@vue-skuilder/db';
|
|
67
|
+
interface User {
|
|
68
|
+
_id: string;
|
|
69
|
+
name: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface Course {
|
|
73
|
+
_id: string;
|
|
74
|
+
name: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface Classroom {
|
|
78
|
+
_id: string;
|
|
79
|
+
name: string;
|
|
80
|
+
teachers: string[];
|
|
81
|
+
students: string[];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export default defineComponent({
|
|
85
|
+
name: 'AdminView',
|
|
86
|
+
|
|
87
|
+
data() {
|
|
88
|
+
return {
|
|
89
|
+
title: 'Admin Panel',
|
|
90
|
+
db: null as AdminDBInterface | null,
|
|
91
|
+
users: [] as User[],
|
|
92
|
+
courses: [] as Course[],
|
|
93
|
+
classrooms: [] as Classroom[],
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
computed: {
|
|
98
|
+
registeredUsers(): User[] {
|
|
99
|
+
return this.users.filter((u) => {
|
|
100
|
+
return !(u.name as string).startsWith(GuestUsername);
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
async created() {
|
|
106
|
+
try {
|
|
107
|
+
this.db = await getDataLayer().getAdminDB();
|
|
108
|
+
this.users = (await this.db.getUsers()) as unknown as User[];
|
|
109
|
+
this.courses = (await this.db.getCourses()) as unknown as Course[];
|
|
110
|
+
this.classrooms = await this.db.getClassrooms();
|
|
111
|
+
} catch (e) {
|
|
112
|
+
this.title = 'This page is for database admins only.';
|
|
113
|
+
throw `${JSON.stringify(e)} - ${e}\n\nNot an admin!`;
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
methods: {
|
|
118
|
+
removeCourse(id: string): void {
|
|
119
|
+
console.log(`Removing ${id}`);
|
|
120
|
+
if (this.db) {
|
|
121
|
+
this.db.removeCourse(id);
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
</script>
|
|
127
|
+
|
|
128
|
+
<style scoped></style>
|