Skip to content

Commit 3a4ddf1

Browse files
authored
[wip] discoverable qtypes (#823)
- **[wip] add example question impls** - **make Question types externally available**
2 parents 53b4810 + dfab86e commit 3a4ddf1

File tree

11 files changed

+520
-2
lines changed

11 files changed

+520
-2
lines changed

packages/courses/src/index.ts

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,74 @@
11
import { markRaw } from 'vue';
22
import { Course } from './Course';
3-
import { Displayable, ViewComponent } from '@vue-skuilder/common-ui';
4-
import { DataShape, NameSpacer, ShapeDescriptor, ViewDescriptor } from '@vue-skuilder/common';
3+
import {
4+
Displayable,
5+
Question,
6+
ViewComponent,
7+
} from '@vue-skuilder/common-ui';
8+
import {
9+
DataShape,
10+
NameSpacer,
11+
ShapeDescriptor,
12+
ViewDescriptor,
13+
ViewData,
14+
Answer,
15+
Evaluation,
16+
} from '@vue-skuilder/common';
517

618
import defaultCourse from './default';
719

20+
/**
21+
* A `Course` is a container for a set of related `Question` types.
22+
*/
23+
export { Course };
24+
25+
26+
/**
27+
* The base class for all interactive course content.
28+
* A `Displayable` defines the data it requires and the Vue components used to render it.
29+
*/
30+
export { Displayable };
31+
32+
/**
33+
* The base class for all questions. It extends `Displayable` and adds
34+
* logic for evaluating user answers. This is the primary class developers
35+
* will extend to create new question types.
36+
*/
37+
export { Question };
38+
39+
/**
40+
* A type representing a Vue component that can be used to render a `Displayable`.
41+
*/
42+
export type { ViewComponent };
43+
44+
45+
46+
/**
47+
* Represents the actual data passed to a `Displayable`'s constructor.
48+
*/
49+
export type { ViewData };
50+
51+
/**
52+
* Represents a user's answer to a `Question`.
53+
*/
54+
export type { Answer };
55+
56+
/**
57+
* Represents the evaluation of a user's `Answer`.
58+
*/
59+
export type { Evaluation };
60+
61+
/**
62+
* A descriptor that uniquely identifies a `ViewComponent`.
63+
*/
64+
export type { ViewDescriptor };
65+
66+
/**
67+
* A descriptor that uniquely identifies a `DataShape`.
68+
*/
69+
export type { ShapeDescriptor };
70+
71+
872
import chess from './chess';
973
import french from './french';
1074
import math from './math';

packages/standalone-ui/src/main.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ import { useAuthStore } from '@vue-skuilder/common-ui';
2323
import '@vue-skuilder/courses/style';
2424
import '@vue-skuilder/common-ui/style';
2525

26+
// Import allCourses singleton and exampleCourse
27+
import { allCourses } from '@vue-skuilder/courses';
28+
import { exampleCourse } from './questions/exampleCourse';
29+
30+
// Add the example course to the allCourses singleton
31+
allCourses.courses.push(exampleCourse);
32+
2633
// theme configuration
2734
import config from '../skuilder.config.json';
2835

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { Question, ViewData, Answer } from '@vue-skuilder/courses';
2+
import { FieldType, DataShape } from '@vue-skuilder/common';
3+
import MultipleChoiceQuestionView from './MultipleChoiceQuestionView.vue';
4+
5+
export class MultipleChoiceQuestion extends Question {
6+
public static dataShapes: DataShape[] = [
7+
{
8+
name: 'MultipleChoiceQuestion',
9+
fields: [
10+
{ name: 'questionText', type: FieldType.STRING },
11+
{ name: 'options', type: FieldType.STRING }, // Comma-separated string of options
12+
{ name: 'correctAnswer', type: FieldType.STRING },
13+
],
14+
},
15+
];
16+
17+
public static views = [
18+
{ name: 'MultipleChoiceQuestionView', component: MultipleChoiceQuestionView },
19+
];
20+
21+
private questionText: string;
22+
private options: string[];
23+
private correctAnswer: string;
24+
25+
constructor(data: ViewData[]) {
26+
super(data);
27+
this.questionText = data[0].questionText as string;
28+
this.options = (data[0].options as string).split(',').map(s => s.trim());
29+
this.correctAnswer = data[0].correctAnswer as string;
30+
}
31+
32+
public dataShapes(): DataShape[] {
33+
return MultipleChoiceQuestion.dataShapes;
34+
}
35+
36+
public views() {
37+
// This will be dynamically populated or imported
38+
return MultipleChoiceQuestion.views;
39+
}
40+
41+
protected isCorrect(answer: Answer): boolean {
42+
return (answer.response as string) === this.correctAnswer;
43+
}
44+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<template>
2+
<div>
3+
<p>{{ questionText }}</p>
4+
<div v-for="(option, index) in options" :key="index">
5+
<input
6+
type="radio"
7+
:id="`option-${index}`"
8+
:value="option"
9+
v-model="selectedAnswer"
10+
/>
11+
<label :for="`option-${index}`">{{ option }}</label>
12+
</div>
13+
<button @click="submitAnswer">Submit</button>
14+
</div>
15+
</template>
16+
17+
<script setup lang="ts">
18+
import { ref, PropType } from 'vue';
19+
import { useViewable, useQuestionView } from '@vue-skuilder/common-ui';
20+
import { MultipleChoiceQuestion } from './MultipleChoiceQuestion';
21+
import { ViewData } from '@vue-skuilder/common';
22+
23+
const props = defineProps({
24+
questionText: {
25+
type: String,
26+
required: true,
27+
},
28+
options: {
29+
type: Array as () => string[],
30+
required: true,
31+
},
32+
data: {
33+
type: Array as PropType<ViewData[]>,
34+
required: true,
35+
},
36+
});
37+
38+
const selectedAnswer = ref('');
39+
40+
const viewableUtils = useViewable(props, () => {}, 'MultipleChoiceQuestionView');
41+
const questionUtils = useQuestionView<MultipleChoiceQuestion>(viewableUtils);
42+
43+
// Initialize question
44+
questionUtils.question.value = new MultipleChoiceQuestion(props.data);
45+
46+
const submitAnswer = () => {
47+
if (selectedAnswer.value) {
48+
questionUtils.submitAnswer({ response: selectedAnswer.value });
49+
}
50+
};
51+
</script>
52+
53+
<style scoped>
54+
/* Add some basic styling if needed */
55+
</style>
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { Question, ViewData, Answer } from '@vue-skuilder/courses';
2+
import { FieldType, DataShape } from '@vue-skuilder/common';
3+
import NumberRangeQuestionView from './NumberRangeQuestionView.vue';
4+
5+
export class NumberRangeQuestion extends Question {
6+
public static dataShapes: DataShape[] = [
7+
{
8+
name: 'NumberRangeQuestion',
9+
fields: [
10+
{ name: 'questionText', type: FieldType.STRING },
11+
{ name: 'min', type: FieldType.NUMBER },
12+
{ name: 'max', type: FieldType.NUMBER },
13+
],
14+
},
15+
];
16+
17+
public static views = [
18+
{ name: 'NumberRangeQuestionView', component: NumberRangeQuestionView },
19+
];
20+
21+
private questionText: string;
22+
private min: number;
23+
private max: number;
24+
25+
constructor(data: ViewData[]) {
26+
super(data);
27+
this.questionText = data[0].questionText as string;
28+
this.min = data[0].min as number;
29+
this.max = data[0].max as number;
30+
}
31+
32+
public dataShapes(): DataShape[] {
33+
return NumberRangeQuestion.dataShapes;
34+
}
35+
36+
public views() {
37+
// This will be dynamically populated or imported
38+
return NumberRangeQuestion.views;
39+
}
40+
41+
protected isCorrect(answer: Answer): boolean {
42+
const userAnswer = answer.response as number;
43+
return userAnswer >= this.min && userAnswer <= this.max;
44+
}
45+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<template>
2+
<div>
3+
<p>{{ questionText }}</p>
4+
<input type="number" v-model.number="userAnswer" @keyup.enter="submitAnswer" placeholder="Enter a number" />
5+
<button @click="submitAnswer">Submit</button>
6+
</div>
7+
</template>
8+
9+
<script setup lang="ts">
10+
import { ref, PropType } from 'vue';
11+
import { useViewable, useQuestionView } from '@vue-skuilder/common-ui';
12+
import { NumberRangeQuestion } from './NumberRangeQuestion';
13+
import { ViewData } from '@vue-skuilder/common';
14+
15+
const props = defineProps({
16+
questionText: {
17+
type: String,
18+
required: true,
19+
},
20+
data: {
21+
type: Array as PropType<ViewData[]>,
22+
required: true,
23+
},
24+
});
25+
26+
const userAnswer = ref<number | null>(null);
27+
28+
const viewableUtils = useViewable(props, () => {}, 'NumberRangeQuestionView');
29+
const questionUtils = useQuestionView<NumberRangeQuestion>(viewableUtils);
30+
31+
// Initialize question
32+
questionUtils.question.value = new NumberRangeQuestion(props.data);
33+
34+
const submitAnswer = () => {
35+
if (userAnswer.value !== null) {
36+
questionUtils.submitAnswer({ response: userAnswer.value });
37+
}
38+
};
39+
</script>
40+
41+
<style scoped>
42+
/* Add some basic styling if needed */
43+
</style>

0 commit comments

Comments
 (0)