@vue-skuilder/standalone-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/.env.development +6 -0
- package/README.md +22 -0
- package/cypress/e2e/auth.cy.js +6 -0
- package/cypress/e2e/smoke.cy.js +10 -0
- package/cypress/support/commands.js +36 -0
- package/cypress/support/e2e.js +17 -0
- package/cypress.config.js +17 -0
- package/index.html +17 -0
- package/package.json +34 -0
- package/skuilder.config.json +5 -0
- package/src/App.vue +46 -0
- package/src/ENVIRONMENT_VARS.ts +76 -0
- package/src/components/CourseFooter.vue +33 -0
- package/src/components/CourseHeader.vue +43 -0
- package/src/composables/useCourseConfig.ts +33 -0
- package/src/main.ts +60 -0
- package/src/router/index.ts +41 -0
- package/src/views/HomeView.vue +35 -0
- package/src/views/ProgressView.vue +18 -0
- package/src/views/StudyView.vue +87 -0
- package/tsconfig.json +22 -0
- package/tsconfig.node.json +9 -0
- package/vite.config.ts +66 -0
package/.env.development
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Standalone UI Package
|
|
2
|
+
|
|
3
|
+
A template interface for single-course Skuilder applications.
|
|
4
|
+
|
|
5
|
+
Contrasting against `platform-ui`, this package:
|
|
6
|
+
|
|
7
|
+
- Focuses on delivering a specific, scoped curriculum
|
|
8
|
+
- Provides simplified navigation and course-specific theming
|
|
9
|
+
|
|
10
|
+
Within the monorepo, this package acts as a blueprint for the scaffolding tool while providing a reference implementation.
|
|
11
|
+
|
|
12
|
+
When courses are generated via the Skuilder CLI, this template transforms into customized, independent applications.
|
|
13
|
+
|
|
14
|
+
## Core Dependencies
|
|
15
|
+
|
|
16
|
+
Vue 3, Vuetify 3
|
|
17
|
+
|
|
18
|
+
## Development
|
|
19
|
+
|
|
20
|
+
The existing `./skuilder.config.js` file contains a devenv config that loads an `anatomy` course from the existing test DB docker image.
|
|
21
|
+
|
|
22
|
+
Before `yarn dev` of the standalone package, the testDB docker image must be loaded via `yarn couchdb:start` in project root.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Basic smoke test for standalone-ui
|
|
2
|
+
describe('Smoke Test', () => {
|
|
3
|
+
it('should load the application', () => {
|
|
4
|
+
cy.visit('/');
|
|
5
|
+
|
|
6
|
+
// Verify that the application loaded
|
|
7
|
+
// Update selector as needed based on your actual UI
|
|
8
|
+
cy.get('body').should('be.visible');
|
|
9
|
+
});
|
|
10
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// standalone-ui/cypress/support/commands.js
|
|
2
|
+
// Custom commands for Cypress tests
|
|
3
|
+
|
|
4
|
+
// Command for logging in a user
|
|
5
|
+
Cypress.Commands.add('login', (username, password) => {
|
|
6
|
+
cy.visit('/login');
|
|
7
|
+
cy.get('input[name="username"]').type(username);
|
|
8
|
+
cy.get('input[name="password"]').type(password);
|
|
9
|
+
cy.contains('button', 'Log in').click();
|
|
10
|
+
|
|
11
|
+
// Optional: Wait for login to complete and redirect
|
|
12
|
+
cy.url().should('not.include', '/login');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// Command for registering a new user
|
|
16
|
+
Cypress.Commands.add('registerUser', (username = null, password = 'securePassword123') => {
|
|
17
|
+
// Generate a unique username if none provided
|
|
18
|
+
const finalUsername = username || `testuser${Date.now()}`;
|
|
19
|
+
|
|
20
|
+
// Visit the signup page
|
|
21
|
+
cy.visit('/signup');
|
|
22
|
+
|
|
23
|
+
// Fill out the registration form
|
|
24
|
+
cy.get('input[name="username"]').type(finalUsername);
|
|
25
|
+
cy.get('input[name="password"]').type(password);
|
|
26
|
+
cy.get('input[name="retypedPassword"]').type(password);
|
|
27
|
+
|
|
28
|
+
// Submit the form
|
|
29
|
+
cy.contains('button', 'Create Account').click();
|
|
30
|
+
|
|
31
|
+
// Wait for registration to complete
|
|
32
|
+
cy.url().should('include', `/u/${finalUsername}/new`);
|
|
33
|
+
|
|
34
|
+
// Return the created username so it can be used in tests
|
|
35
|
+
return cy.wrap(finalUsername);
|
|
36
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// standalone-ui/cypress/support/e2e.js
|
|
2
|
+
// This is the main support file for Cypress E2E tests
|
|
3
|
+
|
|
4
|
+
// Import commands.js using ES2015 syntax:
|
|
5
|
+
import './commands';
|
|
6
|
+
|
|
7
|
+
// Cypress exception handling
|
|
8
|
+
Cypress.on('uncaught:exception', (err) => {
|
|
9
|
+
// Log the error for debugging
|
|
10
|
+
console.log('Uncaught exception:', err.message);
|
|
11
|
+
|
|
12
|
+
// If the error is from PouchDB, ignore it
|
|
13
|
+
if (err.message.includes('not_found') || err.message.includes('missing')) {
|
|
14
|
+
return false; // Prevents Cypress from failing the test
|
|
15
|
+
}
|
|
16
|
+
return true; // Otherwise, fail the test
|
|
17
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineConfig } from 'cypress';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
e2e: {
|
|
5
|
+
baseUrl: 'http://localhost:6173', // package/standalone-ui vite dev server port
|
|
6
|
+
supportFile: 'cypress/support/e2e.js',
|
|
7
|
+
specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
|
|
8
|
+
setupNodeEvents(on, config) {
|
|
9
|
+
// implement node event listeners here
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
// increase the default timeout for slower operations
|
|
13
|
+
defaultCommandTimeout: 10000,
|
|
14
|
+
// Viewport configuration
|
|
15
|
+
viewportWidth: 1280,
|
|
16
|
+
viewportHeight: 800,
|
|
17
|
+
});
|
package/index.html
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" href="/favicon.ico" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>Skuilder Course</title>
|
|
8
|
+
<link
|
|
9
|
+
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons"
|
|
10
|
+
rel="stylesheet"
|
|
11
|
+
/>
|
|
12
|
+
</head>
|
|
13
|
+
<body>
|
|
14
|
+
<div id="app"></div>
|
|
15
|
+
<script type="module" src="/src/main.ts"></script>
|
|
16
|
+
</body>
|
|
17
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vue-skuilder/standalone-ui",
|
|
3
|
+
"publishConfig": {
|
|
4
|
+
"access": "public"
|
|
5
|
+
},
|
|
6
|
+
"version": "0.1.1",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"dev": "vite",
|
|
10
|
+
"build": "vite build",
|
|
11
|
+
"preview": "vite preview",
|
|
12
|
+
"test:e2e": "cypress open",
|
|
13
|
+
"test:e2e:headless": "cypress run",
|
|
14
|
+
"ci:e2e": "vite dev & wait-on http://localhost:6173 && cypress run"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@mdi/font": "^7.3.67",
|
|
18
|
+
"@vue-skuilder/common-ui": "workspace:*",
|
|
19
|
+
"@vue-skuilder/db": "workspace:*",
|
|
20
|
+
"pinia": "^2.3.0",
|
|
21
|
+
"vue": "^3.5.13",
|
|
22
|
+
"vue-router": "^4.2.0",
|
|
23
|
+
"vuetify": "^3.7.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/cypress": "1.1.6",
|
|
27
|
+
"@vitejs/plugin-vue": "^5.2.1",
|
|
28
|
+
"cypress": "14.1.0",
|
|
29
|
+
"typescript": "^5.7.2",
|
|
30
|
+
"vite": "^6.0.9",
|
|
31
|
+
"vue-tsc": "^1.8.0",
|
|
32
|
+
"wait-on": "8.0.2"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/App.vue
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-app :theme="theme">
|
|
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 } from 'vue';
|
|
25
|
+
import { useCourseConfig } from './composables/useCourseConfig';
|
|
26
|
+
import CourseHeader from './components/CourseHeader.vue';
|
|
27
|
+
import CourseFooter from './components/CourseFooter.vue';
|
|
28
|
+
import { SkMouseTrap, SkldrMouseTrap } from '@vue-skuilder/common-ui';
|
|
29
|
+
|
|
30
|
+
const { courseConfig } = useCourseConfig();
|
|
31
|
+
const theme = computed(() => (courseConfig.darkMode ? 'dark' : 'light'));
|
|
32
|
+
|
|
33
|
+
onMounted(() => {
|
|
34
|
+
// Add a global shortcut to show the keyboard shortcuts dialog
|
|
35
|
+
SkldrMouseTrap.addBinding({
|
|
36
|
+
hotkey: '?',
|
|
37
|
+
command: 'Show keyboard shortcuts',
|
|
38
|
+
callback: () => {
|
|
39
|
+
const keyboardButton = document.querySelector('.mdi-keyboard');
|
|
40
|
+
if (keyboardButton) {
|
|
41
|
+
(keyboardButton as HTMLElement).click();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
</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,43 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-app-bar>
|
|
3
|
+
<v-app-bar-nav-icon v-if="isMobile"></v-app-bar-nav-icon>
|
|
4
|
+
|
|
5
|
+
<v-toolbar-title class="d-flex align-center">
|
|
6
|
+
<img v-if="logo" :src="logo" alt="Course Logo" height="32" class="mr-2" />
|
|
7
|
+
{{ title }}
|
|
8
|
+
</v-toolbar-title>
|
|
9
|
+
|
|
10
|
+
<v-spacer></v-spacer>
|
|
11
|
+
|
|
12
|
+
<div v-if="!isMobile" class="d-flex">
|
|
13
|
+
<v-btn v-for="item in menuItems" :key="item.text" :to="item.path" text>
|
|
14
|
+
{{ item.text }}
|
|
15
|
+
</v-btn>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<v-divider vertical class="mx-2"></v-divider>
|
|
19
|
+
|
|
20
|
+
<UserLoginAndRegistrationContainer :show-login-button="true" redirect-to-path="/study" />
|
|
21
|
+
</v-app-bar>
|
|
22
|
+
</template>
|
|
23
|
+
|
|
24
|
+
<script setup lang="ts">
|
|
25
|
+
import { ref, computed, onMounted } from 'vue';
|
|
26
|
+
import { useDisplay } from 'vuetify';
|
|
27
|
+
import { UserLoginAndRegistrationContainer, useAuthStore } from '@vue-skuilder/common-ui';
|
|
28
|
+
import { getDataLayer } from '@vue-skuilder/db';
|
|
29
|
+
|
|
30
|
+
const props = defineProps<{
|
|
31
|
+
title: string;
|
|
32
|
+
logo?: string;
|
|
33
|
+
}>();
|
|
34
|
+
|
|
35
|
+
const { mobile } = useDisplay();
|
|
36
|
+
const isMobile = computed(() => mobile.value);
|
|
37
|
+
|
|
38
|
+
const menuItems = ref([
|
|
39
|
+
{ text: 'Home', path: '/' },
|
|
40
|
+
{ text: 'Study', path: '/study' },
|
|
41
|
+
{ text: 'Progress', path: '/progress' },
|
|
42
|
+
]);
|
|
43
|
+
</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
|
+
}
|
package/src/main.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
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 } from '@vue-skuilder/db';
|
|
18
|
+
|
|
19
|
+
// auth store
|
|
20
|
+
import { useAuthStore } from '@vue-skuilder/common-ui';
|
|
21
|
+
|
|
22
|
+
(async () => {
|
|
23
|
+
await initializeDataLayer({
|
|
24
|
+
type: 'pouch',
|
|
25
|
+
options: {
|
|
26
|
+
COUCHDB_SERVER_URL: ENV.COUCHDB_SERVER_URL,
|
|
27
|
+
COUCHDB_SERVER_PROTOCOL: ENV.COUCHDB_SERVER_PROTOCOL,
|
|
28
|
+
COURSE_IDS: [ENV.STATIC_COURSE_ID],
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
const pinia = createPinia();
|
|
32
|
+
|
|
33
|
+
const vuetify = createVuetify({
|
|
34
|
+
components,
|
|
35
|
+
directives,
|
|
36
|
+
theme: {
|
|
37
|
+
defaultTheme: 'light',
|
|
38
|
+
},
|
|
39
|
+
icons: {
|
|
40
|
+
defaultSet: 'mdi',
|
|
41
|
+
aliases,
|
|
42
|
+
sets: {
|
|
43
|
+
mdi,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const app = createApp(App);
|
|
49
|
+
|
|
50
|
+
app.use(router);
|
|
51
|
+
app.use(vuetify);
|
|
52
|
+
app.use(pinia);
|
|
53
|
+
|
|
54
|
+
const { piniaPlugin } = await import('@vue-skuilder/common-ui');
|
|
55
|
+
app.use(piniaPlugin, { pinia });
|
|
56
|
+
|
|
57
|
+
await useAuthStore().init();
|
|
58
|
+
|
|
59
|
+
app.mount('#app');
|
|
60
|
+
})();
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
|
|
2
|
+
import HomeView from '../views/HomeView.vue';
|
|
3
|
+
import StudyView from '../views/StudyView.vue';
|
|
4
|
+
import ProgressView from '../views/ProgressView.vue';
|
|
5
|
+
import { UserLogin, UserRegistration } from '@vue-skuilder/common-ui';
|
|
6
|
+
|
|
7
|
+
const routes: Array<RouteRecordRaw> = [
|
|
8
|
+
{
|
|
9
|
+
path: '/',
|
|
10
|
+
name: 'home',
|
|
11
|
+
component: HomeView,
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
path: '/study',
|
|
15
|
+
name: 'study',
|
|
16
|
+
component: StudyView,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
path: '/progress',
|
|
20
|
+
name: 'progress',
|
|
21
|
+
component: ProgressView,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
path: '/login',
|
|
25
|
+
name: 'login',
|
|
26
|
+
component: UserLogin,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
path: '/register',
|
|
30
|
+
alias: '/signup',
|
|
31
|
+
name: 'Register',
|
|
32
|
+
component: UserRegistration,
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const router = createRouter({
|
|
37
|
+
history: createWebHistory(),
|
|
38
|
+
routes,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export default router;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-container>
|
|
3
|
+
<v-row>
|
|
4
|
+
<v-col cols="12" class="text-center">
|
|
5
|
+
<h1 class="text-h3 mb-6">{{ courseConfig.title }}</h1>
|
|
6
|
+
<p class="text-body-1 mb-6">{{ courseConfig.description }}</p>
|
|
7
|
+
|
|
8
|
+
<div v-if="isLoggedIn">
|
|
9
|
+
<v-btn color="primary" size="large" to="/study"> Start Learning </v-btn>
|
|
10
|
+
</div>
|
|
11
|
+
<div v-else class="d-flex flex-column align-center">
|
|
12
|
+
<p class="text-body-1 mb-4">Please login to start learning</p>
|
|
13
|
+
<UserLoginAndRegistrationContainer v-model="authDialogOpen" redirect-to-path="/study">
|
|
14
|
+
<template #activator="{ props }">
|
|
15
|
+
<v-btn color="primary" size="large" v-bind="props"> Login / Sign Up </v-btn>
|
|
16
|
+
</template>
|
|
17
|
+
</UserLoginAndRegistrationContainer>
|
|
18
|
+
</div>
|
|
19
|
+
</v-col>
|
|
20
|
+
</v-row>
|
|
21
|
+
</v-container>
|
|
22
|
+
</template>
|
|
23
|
+
|
|
24
|
+
<script setup lang="ts">
|
|
25
|
+
import { ref, onMounted, computed } from 'vue';
|
|
26
|
+
import { useCourseConfig } from '../composables/useCourseConfig';
|
|
27
|
+
import { UserLoginAndRegistrationContainer, useAuthStore } from '@vue-skuilder/common-ui';
|
|
28
|
+
import { getDataLayer } from '@vue-skuilder/db';
|
|
29
|
+
|
|
30
|
+
const { courseConfig } = useCourseConfig();
|
|
31
|
+
const authStore = useAuthStore();
|
|
32
|
+
const authDialogOpen = ref(false);
|
|
33
|
+
|
|
34
|
+
const isLoggedIn = computed(() => authStore.isLoggedIn);
|
|
35
|
+
</script>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-container>
|
|
3
|
+
<v-row>
|
|
4
|
+
<v-col cols="12">
|
|
5
|
+
<h1 class="text-h4 mb-4">Your Progress</h1>
|
|
6
|
+
|
|
7
|
+
<!-- Placeholder for progress component -->
|
|
8
|
+
<v-card class="pa-4">
|
|
9
|
+
<p>Progress tracking will be shown here.</p>
|
|
10
|
+
</v-card>
|
|
11
|
+
</v-col>
|
|
12
|
+
</v-row>
|
|
13
|
+
</v-container>
|
|
14
|
+
</template>
|
|
15
|
+
|
|
16
|
+
<script setup lang="ts">
|
|
17
|
+
// Would import from common-ui or db package
|
|
18
|
+
</script>
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-container>
|
|
3
|
+
<v-row>
|
|
4
|
+
<v-col cols="12">
|
|
5
|
+
<h1 class="text-h4 mb-4">Study Session</h1>
|
|
6
|
+
|
|
7
|
+
<div v-if="sessionPrepared">
|
|
8
|
+
<StudySession
|
|
9
|
+
:content-sources="sessionContentSources"
|
|
10
|
+
:session-time-limit="sessionTimeLimit"
|
|
11
|
+
:user="user"
|
|
12
|
+
:session-config="studySessionConfig"
|
|
13
|
+
:data-layer="dataLayer"
|
|
14
|
+
:get-view-component="getViewComponent"
|
|
15
|
+
@session-finished="handleSessionFinished"
|
|
16
|
+
/>
|
|
17
|
+
</div>
|
|
18
|
+
<div v-else>
|
|
19
|
+
<v-card class="pa-4">
|
|
20
|
+
<v-progress-circular indeterminate color="primary"></v-progress-circular>
|
|
21
|
+
<p class="mt-4">Preparing study session...</p>
|
|
22
|
+
</v-card>
|
|
23
|
+
</div>
|
|
24
|
+
</v-col>
|
|
25
|
+
</v-row>
|
|
26
|
+
</v-container>
|
|
27
|
+
</template>
|
|
28
|
+
|
|
29
|
+
<script setup lang="ts">
|
|
30
|
+
import { ref, onMounted } from 'vue';
|
|
31
|
+
import { ContentSourceID, getDataLayer, UserDBInterface } from '@vue-skuilder/db';
|
|
32
|
+
import { StudySession, type StudySessionConfig } from '@vue-skuilder/common-ui';
|
|
33
|
+
import { allCourses } from '@vue-skuilder/courses';
|
|
34
|
+
import { useRouter } from 'vue-router';
|
|
35
|
+
import ENV from '../ENVIRONMENT_VARS';
|
|
36
|
+
|
|
37
|
+
const router = useRouter();
|
|
38
|
+
const user = getDataLayer().getUserDB();
|
|
39
|
+
const dataLayer = getDataLayer();
|
|
40
|
+
const sessionPrepared = ref(false);
|
|
41
|
+
const sessionTimeLimit = ref(5); // 5 minutes
|
|
42
|
+
const sessionContentSources = ref<ContentSourceID[]>([]);
|
|
43
|
+
const studySessionConfig = ref<StudySessionConfig>({
|
|
44
|
+
likesConfetti: true,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Function to get view component from courses
|
|
48
|
+
const getViewComponent = (view_id: string) => allCourses.getView(view_id);
|
|
49
|
+
|
|
50
|
+
// Initialize study session with course from environment vars
|
|
51
|
+
const initStudySession = async () => {
|
|
52
|
+
// Check if course ID is valid
|
|
53
|
+
if (!ENV.STATIC_COURSE_ID || ENV.STATIC_COURSE_ID === 'not_set') {
|
|
54
|
+
console.error('[StudyView] No course ID specified in environment vars!');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
console.log(`[StudyView] Starting study session for course: ${ENV.STATIC_COURSE_ID}`);
|
|
59
|
+
|
|
60
|
+
// Set the content source to the course ID from environment vars
|
|
61
|
+
sessionContentSources.value = [
|
|
62
|
+
{
|
|
63
|
+
type: 'course',
|
|
64
|
+
id: ENV.STATIC_COURSE_ID,
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
// Mark the session as prepared
|
|
69
|
+
sessionPrepared.value = true;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Handle session finished
|
|
73
|
+
const handleSessionFinished = () => {
|
|
74
|
+
// router.go(0); // Refresh the page
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Initialize on component mount
|
|
78
|
+
onMounted(async () => {
|
|
79
|
+
// user.value = await getCurrentUser();
|
|
80
|
+
|
|
81
|
+
// if (user.value) {
|
|
82
|
+
await initStudySession();
|
|
83
|
+
// } else {
|
|
84
|
+
// console.error('[StudyView] No user available!');
|
|
85
|
+
// }
|
|
86
|
+
});
|
|
87
|
+
</script>
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"jsx": "preserve",
|
|
9
|
+
"resolveJsonModule": true,
|
|
10
|
+
"isolatedModules": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"lib": ["ESNext", "DOM"],
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"noEmit": true,
|
|
15
|
+
"baseUrl": ".",
|
|
16
|
+
"paths": {
|
|
17
|
+
"@/*": ["./src/*"]
|
|
18
|
+
},
|
|
19
|
+
"types": ["vite/client"]
|
|
20
|
+
},
|
|
21
|
+
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
|
|
22
|
+
}
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// packages/standalone-ui/vite.config.ts
|
|
2
|
+
import { defineConfig } from 'vite';
|
|
3
|
+
import vue from '@vitejs/plugin-vue';
|
|
4
|
+
import { resolve } from 'path';
|
|
5
|
+
import { fileURLToPath, URL } from 'node:url'; // Import necessary Node.js modules
|
|
6
|
+
|
|
7
|
+
export default defineConfig({
|
|
8
|
+
plugins: [vue()],
|
|
9
|
+
resolve: {
|
|
10
|
+
alias: {
|
|
11
|
+
// Alias for internal src paths
|
|
12
|
+
'@': fileURLToPath(new URL('./src', import.meta.url)), // Use fileURLToPath and URL for robustness
|
|
13
|
+
|
|
14
|
+
// --- Link workspace packages to their SOURCE ---
|
|
15
|
+
// Adjust relative paths as needed based on your monorepo structure
|
|
16
|
+
'@vue-skuilder/common-ui': resolve(__dirname, '../../packages/common-ui/src/index.ts'),
|
|
17
|
+
|
|
18
|
+
// Add events alias if needed (often required by dependencies)
|
|
19
|
+
events: 'events',
|
|
20
|
+
|
|
21
|
+
// You might need to alias vue-router if dedupe isn't enough, though unlikely
|
|
22
|
+
// 'vue-router': resolve(__dirname, 'node_modules/vue-router'),
|
|
23
|
+
},
|
|
24
|
+
extensions: ['.js', '.ts', '.json', '.vue'], // Keep standard extensions
|
|
25
|
+
dedupe: [
|
|
26
|
+
// Ensure single instances of core libs and workspace packages
|
|
27
|
+
'vue',
|
|
28
|
+
'vuetify',
|
|
29
|
+
'pinia',
|
|
30
|
+
'vue-router',
|
|
31
|
+
'@vue-skuilder/db',
|
|
32
|
+
'@vue-skuilder/common',
|
|
33
|
+
'@vue-skuilder/common-ui',
|
|
34
|
+
'@vue-skuilder/courses',
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
// --- Important for linked source dependencies ---
|
|
38
|
+
optimizeDeps: {
|
|
39
|
+
// Help Vite pre-bundle dependencies from linked packages
|
|
40
|
+
include: [
|
|
41
|
+
'@vue-skuilder/common-ui',
|
|
42
|
+
'@vue-skuilder/db',
|
|
43
|
+
'@vue-skuilder/common',
|
|
44
|
+
'@vue-skuilder/courses',
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
server: {
|
|
48
|
+
port: 6173, // distinct from platform-ui
|
|
49
|
+
},
|
|
50
|
+
build: {
|
|
51
|
+
sourcemap: true, // Keep sourcemaps for build
|
|
52
|
+
target: 'es2020', // Match target from platform-ui
|
|
53
|
+
minify: 'terser', // Match minify from platform-ui
|
|
54
|
+
terserOptions: {
|
|
55
|
+
// Match terserOptions from platform-ui
|
|
56
|
+
keep_classnames: true,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
// Add define block if standalone-ui needs process polyfills (like platform-ui did)
|
|
60
|
+
define: {
|
|
61
|
+
global: 'window',
|
|
62
|
+
'process.env': process.env,
|
|
63
|
+
'process.browser': true,
|
|
64
|
+
'process.version': JSON.stringify(process.version),
|
|
65
|
+
},
|
|
66
|
+
});
|