@vue-skuilder/cli 0.1.9 → 0.1.10
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/standalone-ui-template/package.json +61 -0
- package/dist/standalone-ui-template/src/App.vue +62 -0
- package/dist/standalone-ui-template/src/ENVIRONMENT_VARS.ts +76 -0
- package/dist/standalone-ui-template/src/components/CourseFooter.vue +33 -0
- package/dist/standalone-ui-template/src/components/CourseHeader.vue +55 -0
- package/dist/standalone-ui-template/src/composables/useCourseConfig.ts +33 -0
- package/dist/standalone-ui-template/src/main.ts +201 -0
- package/dist/standalone-ui-template/src/questions/MultipleChoiceQuestion.ts +46 -0
- package/dist/standalone-ui-template/src/questions/MultipleChoiceQuestionView.vue +50 -0
- package/dist/standalone-ui-template/src/questions/NumberRangeQuestion.ts +44 -0
- package/dist/standalone-ui-template/src/questions/NumberRangeQuestionView.vue +43 -0
- package/dist/standalone-ui-template/src/questions/README.md +129 -0
- package/dist/standalone-ui-template/src/questions/SimpleTextQuestion.test.ts +25 -0
- package/dist/standalone-ui-template/src/questions/SimpleTextQuestion.ts +40 -0
- package/dist/standalone-ui-template/src/questions/SimpleTextQuestionView.vue +46 -0
- package/dist/standalone-ui-template/src/questions/exampleCourse.ts +10 -0
- package/dist/standalone-ui-template/src/questions/index.ts +117 -0
- package/dist/standalone-ui-template/src/router/index.ts +59 -0
- package/dist/standalone-ui-template/src/views/BrowseView.vue +41 -0
- package/dist/standalone-ui-template/src/views/HomeView.vue +35 -0
- package/dist/standalone-ui-template/src/views/ProgressView.vue +18 -0
- package/dist/standalone-ui-template/src/views/StudyView.vue +84 -0
- package/dist/standalone-ui-template/src/views/UserSettingsView.vue +175 -0
- package/dist/standalone-ui-template/src/views/UserStatsView.vue +76 -0
- package/dist/standalone-ui-template/tsconfig.json +17 -0
- package/dist/standalone-ui-template/vite.config.ts +103 -0
- package/dist/utils/template.d.ts +1 -1
- package/dist/utils/template.d.ts.map +1 -1
- package/dist/utils/template.js +8 -3
- package/dist/utils/template.js.map +1 -1
- package/package.json +11 -11
- package/src/utils/template.ts +9 -4
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vue-skuilder/standalone-ui",
|
|
3
|
+
"publishConfig": {
|
|
4
|
+
"access": "public"
|
|
5
|
+
},
|
|
6
|
+
"version": "0.1.10",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./dist-lib/questions.cjs.js",
|
|
9
|
+
"module": "./dist-lib/questions.mjs",
|
|
10
|
+
"types": "./dist-lib/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist-lib/index.d.ts",
|
|
14
|
+
"import": "./dist-lib/questions.mjs",
|
|
15
|
+
"require": "./dist-lib/questions.cjs.js"
|
|
16
|
+
},
|
|
17
|
+
"./questions": {
|
|
18
|
+
"types": "./dist-lib/index.d.ts",
|
|
19
|
+
"import": "./dist-lib/questions.mjs",
|
|
20
|
+
"require": "./dist-lib/questions.cjs.js"
|
|
21
|
+
},
|
|
22
|
+
"./style": "./dist-lib/assets/index.css"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist/",
|
|
26
|
+
"dist-lib/"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"dev": "vite",
|
|
30
|
+
"build": "npm run build:webapp && npm run build:lib",
|
|
31
|
+
"build:webapp": "vite build",
|
|
32
|
+
"build:lib": "BUILD_MODE=library vite build",
|
|
33
|
+
"preview": "vite preview",
|
|
34
|
+
"test:e2e": "cypress open",
|
|
35
|
+
"test:e2e:headless": "cypress run",
|
|
36
|
+
"ci:e2e": "vite dev & wait-on http://localhost:6173 && cypress run"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@mdi/font": "^7.3.67",
|
|
40
|
+
"@vue-skuilder/common-ui": "workspace:*",
|
|
41
|
+
"@vue-skuilder/courseware": "workspace:*",
|
|
42
|
+
"@vue-skuilder/db": "workspace:*",
|
|
43
|
+
"events": "^3.3.0",
|
|
44
|
+
"pinia": "^2.3.0",
|
|
45
|
+
"vue": "^3.5.13",
|
|
46
|
+
"vue-router": "^4.2.0",
|
|
47
|
+
"vuetify": "^3.7.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/cypress": "1.1.6",
|
|
51
|
+
"@types/events": "^3",
|
|
52
|
+
"@vitejs/plugin-vue": "^5.2.1",
|
|
53
|
+
"cypress": "14.1.0",
|
|
54
|
+
"typescript": "^5.7.2",
|
|
55
|
+
"vite": "^6.0.9",
|
|
56
|
+
"vite-plugin-dts": "^4.3.0",
|
|
57
|
+
"vue-tsc": "^1.8.0",
|
|
58
|
+
"wait-on": "8.0.2"
|
|
59
|
+
},
|
|
60
|
+
"stableVersion": "0.1.10"
|
|
61
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-app>
|
|
3
|
+
<course-header :title="courseConfig.title" :logo="courseConfig.logo" />
|
|
4
|
+
|
|
5
|
+
<v-main>
|
|
6
|
+
<router-view />
|
|
7
|
+
</v-main>
|
|
8
|
+
|
|
9
|
+
<!-- <course-footer :links="courseConfig.links" :copyright="courseConfig.copyright" /> -->
|
|
10
|
+
<SkMouseTrap />
|
|
11
|
+
|
|
12
|
+
<v-footer app class="pa-0" color="transparent">
|
|
13
|
+
<v-card flat width="100%" class="text-center">
|
|
14
|
+
<v-card-text class="text-body-2 text-medium-emphasis">
|
|
15
|
+
<v-icon small class="me-1">mdi-keyboard</v-icon>
|
|
16
|
+
Tip: Hold <kbd>Ctrl</kbd> to see keyboard shortcuts or press <kbd>?</kbd> to view all shortcuts
|
|
17
|
+
</v-card-text>
|
|
18
|
+
</v-card>
|
|
19
|
+
</v-footer>
|
|
20
|
+
</v-app>
|
|
21
|
+
</template>
|
|
22
|
+
|
|
23
|
+
<script setup lang="ts">
|
|
24
|
+
import { computed, onMounted, watch } from 'vue';
|
|
25
|
+
import { useTheme } from 'vuetify';
|
|
26
|
+
import { useCourseConfig } from './composables/useCourseConfig';
|
|
27
|
+
import CourseHeader from './components/CourseHeader.vue';
|
|
28
|
+
import CourseFooter from './components/CourseFooter.vue';
|
|
29
|
+
import { SkMouseTrap, SkldrMouseTrap, useConfigStore } from '@vue-skuilder/common-ui';
|
|
30
|
+
|
|
31
|
+
const { courseConfig } = useCourseConfig();
|
|
32
|
+
const configStore = useConfigStore();
|
|
33
|
+
const theme = useTheme();
|
|
34
|
+
|
|
35
|
+
// Use the configStore dark mode instead of courseConfig
|
|
36
|
+
const dark = computed(() => {
|
|
37
|
+
return configStore.config.darkMode;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Watch for dark mode changes and update Vuetify theme
|
|
41
|
+
watch(
|
|
42
|
+
dark,
|
|
43
|
+
(newVal) => {
|
|
44
|
+
theme.global.name.value = newVal ? 'dark' : 'light';
|
|
45
|
+
},
|
|
46
|
+
{ immediate: true }
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
onMounted(() => {
|
|
50
|
+
// Add a global shortcut to show the keyboard shortcuts dialog
|
|
51
|
+
SkldrMouseTrap.addBinding({
|
|
52
|
+
hotkey: '?',
|
|
53
|
+
command: 'Show keyboard shortcuts',
|
|
54
|
+
callback: () => {
|
|
55
|
+
const keyboardButton = document.querySelector('.mdi-keyboard');
|
|
56
|
+
if (keyboardButton) {
|
|
57
|
+
(keyboardButton as HTMLElement).click();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
</script>
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// vue-skuilder/packages/standalone-ui/src/ENVIRONMENT_VARS.ts
|
|
2
|
+
type ProtocolString = 'http' | 'https';
|
|
3
|
+
|
|
4
|
+
import config from '../skuilder.config.json';
|
|
5
|
+
|
|
6
|
+
export interface Environment {
|
|
7
|
+
/**
|
|
8
|
+
* URL to the remote couchDB instance that the app connects to.
|
|
9
|
+
* Loaded from VITE_COUCHDB_SERVER_URL environment variable.
|
|
10
|
+
*/
|
|
11
|
+
COUCHDB_SERVER_URL: string;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Protocol for the CouchDB server.
|
|
15
|
+
* Loaded from VITE_COUCHDB_SERVER_PROTOCOL environment variable.
|
|
16
|
+
*/
|
|
17
|
+
COUCHDB_SERVER_PROTOCOL: ProtocolString;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Static course IDs to load.
|
|
21
|
+
* Loaded from VITE_STATIC_COURSE_IDS environment variable (comma-separated string).
|
|
22
|
+
*/
|
|
23
|
+
STATIC_COURSE_ID: string;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Type of data layer to use - couchdb live backend or
|
|
27
|
+
* statically built and served from the app.
|
|
28
|
+
*/
|
|
29
|
+
DATALAYER_TYPE: 'couch' | 'static';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A global flag to enable debug messaging mode.
|
|
33
|
+
* Loaded from VITE_DEBUG environment variable ('true' or 'false').
|
|
34
|
+
*/
|
|
35
|
+
DEBUG: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Default fallback values if environment variables are not set
|
|
39
|
+
const defaultEnv: Environment = {
|
|
40
|
+
COUCHDB_SERVER_URL: 'localhost:5984/', // Sensible default for local dev
|
|
41
|
+
COUCHDB_SERVER_PROTOCOL: 'http',
|
|
42
|
+
STATIC_COURSE_ID: 'not_set',
|
|
43
|
+
DATALAYER_TYPE: 'couch',
|
|
44
|
+
DEBUG: false, // Default to false if VITE_DEBUG is not 'true'
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// --- Read Environment Variables using Vite's import.meta.env ---
|
|
48
|
+
// Vite replaces these variables at build time with the values from your .env files.
|
|
49
|
+
|
|
50
|
+
if (config.dataLayerType !== 'couch' && config.dataLayerType !== 'static') {
|
|
51
|
+
throw new Error('Invalid data layer type');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const ENV: Environment = {
|
|
55
|
+
// Use the value from import.meta.env if available, otherwise use the default
|
|
56
|
+
COUCHDB_SERVER_URL: import.meta.env.VITE_COUCHDB_SERVER || defaultEnv.COUCHDB_SERVER_URL,
|
|
57
|
+
|
|
58
|
+
// Ensure the protocol is one of the allowed types
|
|
59
|
+
COUCHDB_SERVER_PROTOCOL: (import.meta.env.VITE_COUCHDB_PROTOCOL === 'https'
|
|
60
|
+
? 'https'
|
|
61
|
+
: 'http') as ProtocolString,
|
|
62
|
+
|
|
63
|
+
STATIC_COURSE_ID: config.course,
|
|
64
|
+
|
|
65
|
+
DATALAYER_TYPE: config.dataLayerType,
|
|
66
|
+
|
|
67
|
+
// Environment variables are always strings, so compare VITE_DEBUG to 'true'
|
|
68
|
+
DEBUG: import.meta.env.VITE_DEBUG === 'true',
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Optional: Log the resolved environment in development mode for debugging
|
|
72
|
+
if (import.meta.env.DEV) {
|
|
73
|
+
console.log('Resolved Environment Variables:', ENV);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export default ENV;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-footer app padless>
|
|
3
|
+
<v-container fluid>
|
|
4
|
+
<v-row justify="center" align="center">
|
|
5
|
+
<v-col cols="12" sm="8" class="text-center">
|
|
6
|
+
<div v-if="links && links.length" class="d-flex justify-center mb-2">
|
|
7
|
+
<v-btn
|
|
8
|
+
v-for="link in links"
|
|
9
|
+
:key="link.text"
|
|
10
|
+
:href="link.url"
|
|
11
|
+
text
|
|
12
|
+
variant="plain"
|
|
13
|
+
size="small"
|
|
14
|
+
class="mx-1"
|
|
15
|
+
>
|
|
16
|
+
{{ link.text }}
|
|
17
|
+
</v-btn>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="text-body-2 text-medium-emphasis">
|
|
20
|
+
{{ copyright }}
|
|
21
|
+
</div>
|
|
22
|
+
</v-col>
|
|
23
|
+
</v-row>
|
|
24
|
+
</v-container>
|
|
25
|
+
</v-footer>
|
|
26
|
+
</template>
|
|
27
|
+
|
|
28
|
+
<script setup lang="ts">
|
|
29
|
+
defineProps<{
|
|
30
|
+
links?: Array<{ text: string; url: string }>;
|
|
31
|
+
copyright?: string;
|
|
32
|
+
}>();
|
|
33
|
+
</script>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<v-app-bar>
|
|
4
|
+
<v-app-bar-nav-icon v-if="isMobile" @click="drawer = !drawer"></v-app-bar-nav-icon>
|
|
5
|
+
|
|
6
|
+
<v-toolbar-title class="d-flex align-center">
|
|
7
|
+
<img v-if="logo" :src="logo" alt="Course Logo" height="32" class="mr-2" />
|
|
8
|
+
{{ title }}
|
|
9
|
+
</v-toolbar-title>
|
|
10
|
+
|
|
11
|
+
<v-spacer></v-spacer>
|
|
12
|
+
|
|
13
|
+
<div v-if="!isMobile" class="d-flex">
|
|
14
|
+
<v-btn v-for="item in menuItems" :key="item.text" :to="item.path" text>
|
|
15
|
+
{{ item.text }}
|
|
16
|
+
</v-btn>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<v-divider vertical class="mx-2"></v-divider>
|
|
20
|
+
|
|
21
|
+
<UserLoginAndRegistrationContainer :show-login-button="true" redirect-to-path="/study" />
|
|
22
|
+
</v-app-bar>
|
|
23
|
+
|
|
24
|
+
<v-navigation-drawer v-model="drawer" temporary v-if="isMobile">
|
|
25
|
+
<v-list>
|
|
26
|
+
<v-list-item v-for="item in menuItems" :key="item.text" :to="item.path" @click="drawer = false">
|
|
27
|
+
<v-list-item-title>{{ item.text }}</v-list-item-title>
|
|
28
|
+
</v-list-item>
|
|
29
|
+
</v-list>
|
|
30
|
+
</v-navigation-drawer>
|
|
31
|
+
</div>
|
|
32
|
+
</template>
|
|
33
|
+
|
|
34
|
+
<script setup lang="ts">
|
|
35
|
+
import { ref, computed } from 'vue';
|
|
36
|
+
import { useDisplay } from 'vuetify';
|
|
37
|
+
import { UserLoginAndRegistrationContainer } from '@vue-skuilder/common-ui';
|
|
38
|
+
|
|
39
|
+
// Define props
|
|
40
|
+
defineProps<{
|
|
41
|
+
title?: string;
|
|
42
|
+
logo?: string;
|
|
43
|
+
}>();
|
|
44
|
+
|
|
45
|
+
const { mobile } = useDisplay();
|
|
46
|
+
const isMobile = computed(() => mobile.value);
|
|
47
|
+
const drawer = ref(false);
|
|
48
|
+
|
|
49
|
+
const menuItems = ref([
|
|
50
|
+
{ text: 'Home', path: '/' },
|
|
51
|
+
{ text: 'Study', path: '/study' },
|
|
52
|
+
{ text: 'Browse', path: '/browse' },
|
|
53
|
+
// Progress view not implemented - will be accessible via UserChip->Stats
|
|
54
|
+
]);
|
|
55
|
+
</script>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ref, readonly } from 'vue';
|
|
2
|
+
import config from '../../skuilder.config.json';
|
|
3
|
+
|
|
4
|
+
// This would be replaced by actual course configuration in a real implementation
|
|
5
|
+
const defaultConfig = {
|
|
6
|
+
title: config.title ? config.title : '[UNSET] Course Title',
|
|
7
|
+
description: 'This is the devenv test course setup.',
|
|
8
|
+
logo: '',
|
|
9
|
+
darkMode: false,
|
|
10
|
+
links: [
|
|
11
|
+
{ text: 'About', url: '/about' },
|
|
12
|
+
{ text: 'Help', url: '/help' },
|
|
13
|
+
],
|
|
14
|
+
copyright: '',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function useCourseConfig() {
|
|
18
|
+
const courseConfig = ref(defaultConfig);
|
|
19
|
+
|
|
20
|
+
// Later this would load from a configuration file or API
|
|
21
|
+
const loadConfig = async () => {
|
|
22
|
+
// In a real implementation, this would load configuration
|
|
23
|
+
// courseConfig.value = await loadCourseConfig();
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Initialize
|
|
27
|
+
loadConfig();
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
courseConfig: readonly(courseConfig),
|
|
31
|
+
loadConfig,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import ENV from './ENVIRONMENT_VARS';
|
|
2
|
+
import '@mdi/font/css/materialdesignicons.css';
|
|
3
|
+
|
|
4
|
+
import { createApp } from 'vue';
|
|
5
|
+
import { createPinia } from 'pinia';
|
|
6
|
+
import App from './App.vue';
|
|
7
|
+
import router from './router';
|
|
8
|
+
|
|
9
|
+
// Vuetify
|
|
10
|
+
import 'vuetify/styles';
|
|
11
|
+
import { createVuetify } from 'vuetify';
|
|
12
|
+
import * as components from 'vuetify/components';
|
|
13
|
+
import * as directives from 'vuetify/directives';
|
|
14
|
+
import { aliases, mdi } from 'vuetify/iconsets/mdi';
|
|
15
|
+
|
|
16
|
+
// data layer
|
|
17
|
+
import { initializeDataLayer, getDataLayer } from '@vue-skuilder/db';
|
|
18
|
+
|
|
19
|
+
// auth store
|
|
20
|
+
import { useAuthStore } from '@vue-skuilder/common-ui';
|
|
21
|
+
|
|
22
|
+
// styles from component library packages
|
|
23
|
+
import '@vue-skuilder/courseware/style';
|
|
24
|
+
import '@vue-skuilder/common-ui/style';
|
|
25
|
+
|
|
26
|
+
// Import allCourseWare singleton and exampleCourse
|
|
27
|
+
import { allCourseWare } from '@vue-skuilder/courseware';
|
|
28
|
+
import { exampleCourse } from './questions/exampleCourse';
|
|
29
|
+
|
|
30
|
+
// Add the example course to the allCourseWare singleton
|
|
31
|
+
allCourseWare.courses.push(exampleCourse);
|
|
32
|
+
|
|
33
|
+
// theme configuration
|
|
34
|
+
import config from '../skuilder.config.json';
|
|
35
|
+
|
|
36
|
+
(async () => {
|
|
37
|
+
// For static data layer, load manifest
|
|
38
|
+
let dataLayerOptions: any = {
|
|
39
|
+
COUCHDB_SERVER_URL: ENV.COUCHDB_SERVER_URL,
|
|
40
|
+
COUCHDB_SERVER_PROTOCOL: ENV.COUCHDB_SERVER_PROTOCOL,
|
|
41
|
+
COURSE_IDS: [config.course ? config.course : 'default-course'],
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
if (config.dataLayerType === 'static') {
|
|
45
|
+
// Load manifest for static mode
|
|
46
|
+
const courseId = config.course;
|
|
47
|
+
if (!courseId) {
|
|
48
|
+
throw new Error('Course ID required for static data layer');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const manifestResponse = await fetch(`/static-courses/${courseId}/manifest.json`);
|
|
53
|
+
if (!manifestResponse.ok) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`Failed to load manifest: ${manifestResponse.status} ${manifestResponse.statusText}`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
const manifest = await manifestResponse.json();
|
|
59
|
+
console.log(`Loaded manifest for course ${courseId}`);
|
|
60
|
+
console.log(JSON.stringify(manifest));
|
|
61
|
+
|
|
62
|
+
dataLayerOptions = {
|
|
63
|
+
staticContentPath: '/static-courses',
|
|
64
|
+
manifests: {
|
|
65
|
+
[courseId]: manifest,
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error('[DEBUG] Failed to load course manifest:', error);
|
|
70
|
+
throw new Error(`Could not load course manifest for ${courseId}: ${error}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
await initializeDataLayer({
|
|
76
|
+
type: (config.dataLayerType || 'couch') as 'couch' | 'static',
|
|
77
|
+
options: dataLayerOptions,
|
|
78
|
+
});
|
|
79
|
+
console.log('[DEBUG] Data layer initialized successfully');
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error('[DEBUG] Data layer initialization failed:', error);
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
const pinia = createPinia();
|
|
85
|
+
|
|
86
|
+
// Apply theme configuration from skuilder.config.json
|
|
87
|
+
const themeConfig = config.theme
|
|
88
|
+
? {
|
|
89
|
+
defaultTheme: config.theme.defaultMode || 'light',
|
|
90
|
+
themes: {
|
|
91
|
+
light: config.theme.light,
|
|
92
|
+
dark: config.theme.dark,
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
: {
|
|
96
|
+
defaultTheme: 'light',
|
|
97
|
+
themes: {
|
|
98
|
+
light: {
|
|
99
|
+
dark: false,
|
|
100
|
+
colors: {
|
|
101
|
+
primary: '#1976D2',
|
|
102
|
+
secondary: '#424242',
|
|
103
|
+
accent: '#82B1FF',
|
|
104
|
+
error: '#F44336',
|
|
105
|
+
info: '#2196F3',
|
|
106
|
+
success: '#4CAF50',
|
|
107
|
+
warning: '#FF9800',
|
|
108
|
+
background: '#FFFFFF',
|
|
109
|
+
surface: '#FFFFFF',
|
|
110
|
+
'surface-bright': '#FFFFFF',
|
|
111
|
+
'surface-light': '#EEEEEE',
|
|
112
|
+
'surface-variant': '#E3F2FD',
|
|
113
|
+
'on-surface-variant': '#1976D2',
|
|
114
|
+
'primary-darken-1': '#1565C0',
|
|
115
|
+
'secondary-darken-1': '#212121',
|
|
116
|
+
'on-primary': '#FFFFFF',
|
|
117
|
+
'on-secondary': '#FFFFFF',
|
|
118
|
+
'on-background': '#212121',
|
|
119
|
+
'on-surface': '#212121',
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
dark: {
|
|
123
|
+
dark: true,
|
|
124
|
+
colors: {
|
|
125
|
+
primary: '#2196F3',
|
|
126
|
+
secondary: '#90A4AE',
|
|
127
|
+
accent: '#82B1FF',
|
|
128
|
+
error: '#FF5252',
|
|
129
|
+
info: '#2196F3',
|
|
130
|
+
success: '#4CAF50',
|
|
131
|
+
warning: '#FFC107',
|
|
132
|
+
background: '#121212',
|
|
133
|
+
surface: '#1E1E1E',
|
|
134
|
+
'surface-bright': '#2C2C2C',
|
|
135
|
+
'surface-light': '#2C2C2C',
|
|
136
|
+
'surface-variant': '#1A237E',
|
|
137
|
+
'on-surface-variant': '#82B1FF',
|
|
138
|
+
'primary-darken-1': '#1976D2',
|
|
139
|
+
'secondary-darken-1': '#546E7A',
|
|
140
|
+
'on-primary': '#000000',
|
|
141
|
+
'on-secondary': '#000000',
|
|
142
|
+
'on-background': '#FFFFFF',
|
|
143
|
+
'on-surface': '#FFFFFF',
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const vuetify = createVuetify({
|
|
150
|
+
components,
|
|
151
|
+
directives,
|
|
152
|
+
theme: themeConfig,
|
|
153
|
+
icons: {
|
|
154
|
+
defaultSet: 'mdi',
|
|
155
|
+
aliases,
|
|
156
|
+
sets: {
|
|
157
|
+
mdi,
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const app = createApp(App);
|
|
163
|
+
|
|
164
|
+
app.use(router);
|
|
165
|
+
app.use(vuetify);
|
|
166
|
+
app.use(pinia);
|
|
167
|
+
|
|
168
|
+
const { piniaPlugin } = await import('@vue-skuilder/common-ui');
|
|
169
|
+
app.use(piniaPlugin, { pinia });
|
|
170
|
+
|
|
171
|
+
await useAuthStore().init();
|
|
172
|
+
|
|
173
|
+
// Initialize config store to load user settings (including dark mode)
|
|
174
|
+
const { useConfigStore } = await import('@vue-skuilder/common-ui');
|
|
175
|
+
await useConfigStore().init();
|
|
176
|
+
|
|
177
|
+
// Auto-register user for the course in standalone mode
|
|
178
|
+
if (config.course) {
|
|
179
|
+
try {
|
|
180
|
+
const authStore = useAuthStore();
|
|
181
|
+
const user = getDataLayer().getUserDB();
|
|
182
|
+
|
|
183
|
+
// Check if user is already registered for the course
|
|
184
|
+
const courseRegistrations = await user.getCourseRegistrationsDoc();
|
|
185
|
+
const isRegistered = courseRegistrations.courses.some(c => c.courseID === config.course);
|
|
186
|
+
|
|
187
|
+
if (!isRegistered) {
|
|
188
|
+
console.log(`[Standalone] Auto-registering user for course: ${config.course}`);
|
|
189
|
+
await user.registerForCourse(config.course, false); // non-preview mode
|
|
190
|
+
console.log(`[Standalone] Auto-registration completed for course: ${config.course}`);
|
|
191
|
+
} else {
|
|
192
|
+
console.log(`[Standalone] User already registered for course: ${config.course}`);
|
|
193
|
+
}
|
|
194
|
+
} catch (error) {
|
|
195
|
+
console.warn(`[Standalone] Failed to auto-register for course ${config.course}:`, error);
|
|
196
|
+
// Don't block app startup on registration failure
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
app.mount('#app');
|
|
201
|
+
})();
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { ViewData, Answer, Question } from '@vue-skuilder/courseware';
|
|
2
|
+
import { FieldType, DataShape, DataShapeName } 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' as DataShapeName,
|
|
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
|
+
// @ts-expect-error TS6133: Used in Vue template
|
|
22
|
+
private _questionText: string;
|
|
23
|
+
// @ts-expect-error TS6133: Used in Vue template
|
|
24
|
+
private options: string[];
|
|
25
|
+
private correctAnswer: string;
|
|
26
|
+
|
|
27
|
+
constructor(data: ViewData[]) {
|
|
28
|
+
super(data);
|
|
29
|
+
this._questionText = data[0].questionText as string;
|
|
30
|
+
this.options = (data[0].options as string).split(',').map((s) => s.trim());
|
|
31
|
+
this.correctAnswer = data[0].correctAnswer as string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public dataShapes(): DataShape[] {
|
|
35
|
+
return MultipleChoiceQuestion.dataShapes;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public views() {
|
|
39
|
+
// This will be dynamically populated or imported
|
|
40
|
+
return MultipleChoiceQuestion.views;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
protected isCorrect(answer: Answer): boolean {
|
|
44
|
+
return (answer.response as string) === this.correctAnswer;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<p>{{ questionText }}</p>
|
|
4
|
+
<div v-for="(option, index) in options" :key="index">
|
|
5
|
+
<input type="radio" :id="`option-${index}`" :value="option" v-model="selectedAnswer" />
|
|
6
|
+
<label :for="`option-${index}`">{{ option }}</label>
|
|
7
|
+
</div>
|
|
8
|
+
<button @click="submitAnswer">Submit</button>
|
|
9
|
+
</div>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<script setup lang="ts">
|
|
13
|
+
import { ref, PropType } from 'vue';
|
|
14
|
+
import { useViewable, useQuestionView } from '@vue-skuilder/courseware';
|
|
15
|
+
import { MultipleChoiceQuestion } from './MultipleChoiceQuestion';
|
|
16
|
+
import { ViewData } from '@vue-skuilder/common';
|
|
17
|
+
|
|
18
|
+
const props = defineProps({
|
|
19
|
+
questionText: {
|
|
20
|
+
type: String,
|
|
21
|
+
required: true,
|
|
22
|
+
},
|
|
23
|
+
options: {
|
|
24
|
+
type: Array as () => string[],
|
|
25
|
+
required: true,
|
|
26
|
+
},
|
|
27
|
+
data: {
|
|
28
|
+
type: Array as PropType<ViewData[]>,
|
|
29
|
+
required: true,
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const selectedAnswer = ref('');
|
|
34
|
+
|
|
35
|
+
const viewableUtils = useViewable(props, () => {}, 'MultipleChoiceQuestionView');
|
|
36
|
+
const questionUtils = useQuestionView<MultipleChoiceQuestion>(viewableUtils);
|
|
37
|
+
|
|
38
|
+
// Initialize question
|
|
39
|
+
questionUtils.question.value = new MultipleChoiceQuestion(props.data);
|
|
40
|
+
|
|
41
|
+
const submitAnswer = () => {
|
|
42
|
+
if (selectedAnswer.value) {
|
|
43
|
+
questionUtils.submitAnswer({ response: selectedAnswer.value });
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<style scoped>
|
|
49
|
+
/* Add some basic styling if needed */
|
|
50
|
+
</style>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { ViewData, Answer, Question } from '@vue-skuilder/courseware';
|
|
2
|
+
import { FieldType, DataShape, DataShapeName } 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' as DataShapeName,
|
|
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 = [{ name: 'NumberRangeQuestionView', component: NumberRangeQuestionView }];
|
|
18
|
+
|
|
19
|
+
// @ts-expect-error TS6133: Used in Vue template
|
|
20
|
+
private questionText: string;
|
|
21
|
+
private min: number;
|
|
22
|
+
private max: number;
|
|
23
|
+
|
|
24
|
+
constructor(data: ViewData[]) {
|
|
25
|
+
super(data);
|
|
26
|
+
this.questionText = data[0].questionText as string;
|
|
27
|
+
this.min = data[0].min as number;
|
|
28
|
+
this.max = data[0].max as number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public dataShapes(): DataShape[] {
|
|
32
|
+
return NumberRangeQuestion.dataShapes;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public views() {
|
|
36
|
+
// This will be dynamically populated or imported
|
|
37
|
+
return NumberRangeQuestion.views;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
protected isCorrect(answer: Answer): boolean {
|
|
41
|
+
const userAnswer = answer.response as number;
|
|
42
|
+
return userAnswer >= this.min && userAnswer <= this.max;
|
|
43
|
+
}
|
|
44
|
+
}
|