cyber-elx 1.1.10 → 1.1.11
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/DEV_DOC/README.md
CHANGED
|
@@ -13,6 +13,9 @@ Custom Payment page for course enrollment. Handles guest vs logged-in user state
|
|
|
13
13
|
### [StudentCourseDetailPageDev.md](StudentCourseDetailPageDev.md)
|
|
14
14
|
Custom Student Course Detail page for viewing individual courses. Displays course cover, header with logo/name, description, and accordion-style curriculum with chapters and lessons. Features a sticky price card sidebar with promo pricing, course info, buy/request button, and expiration text. Includes video modal for free preview playback and responsive 2-column to 1-column layout.
|
|
15
15
|
|
|
16
|
+
### [StudentCoursePlayerDev.md](StudentCoursePlayerDev.md)
|
|
17
|
+
Custom Student Course Player for enrolled students. Two-panel layout with sidebar (course card, description/files tabs, accordion chapters with progress tracking) and viewer area (video player with custom controls). Non-video elements (quiz, pdf, iframe, youtube) are rendered via a named slot by the parent component. Supports mobile responsive view switching and RTL for Arabic locales.
|
|
18
|
+
|
|
16
19
|
### [StudentCssDev.md](StudentCssDev.md)
|
|
17
20
|
CSS customization guide for the Student Dashboard. Covers navbar styling (background color, profile button, dropdown menu) and sidebar styling (background, active items, hover effects, submenu items). Includes important notes about using `::before` for backgrounds and required `!important` overrides.
|
|
18
21
|
|
|
@@ -123,7 +123,7 @@ The Student Course Detail Page is rendered when a student views a specific cours
|
|
|
123
123
|
- Expiration text
|
|
124
124
|
8. **Video Modal** - Overlay with video player for free previews
|
|
125
125
|
|
|
126
|
-
### Available translations
|
|
126
|
+
### Available translations (Use what you need)
|
|
127
127
|
|
|
128
128
|
- `courses-page.courses-list` → "Courses List"
|
|
129
129
|
- `courses-page.course-description` → "Course Description"
|
|
@@ -0,0 +1,700 @@
|
|
|
1
|
+
# CyberOcean Custom Student Course Player
|
|
2
|
+
|
|
3
|
+
## Summary
|
|
4
|
+
|
|
5
|
+
This document describes how to customize the **Student Course Player** using a Vue.js component. The player is a two-panel layout with a sidebar (course info, files, chapters) and a viewer area (video player or slot for other content types). It supports mobile responsiveness, progress tracking, downloadable course files, and RTL for Arabic locales.
|
|
6
|
+
|
|
7
|
+
> **IMPORTANT:** Your custom component handles **UI and styles only**. All logic and functionality (progress saving, quiz handling, PDF rendering) are handled by the parent component. The parent uses a **slot** to render non-video elements.
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
The Student Course Player is rendered when a student opens a course they are enrolled in. The parent component provides the user and course data. Your custom component receives these as props and renders the UI. For non-video elements (quiz, pdf, iframe, youtube), your component must provide a **named slot** that the parent will fill with the appropriate viewer.
|
|
12
|
+
|
|
13
|
+
**Key Features:**
|
|
14
|
+
- **Two-panel layout** - Sidebar with course info/chapters + Viewer for content
|
|
15
|
+
- **Mobile responsive** - Sidebar and player toggle on mobile (< 900px)
|
|
16
|
+
- **Course card** - Logo and name display
|
|
17
|
+
- **Description/Files tabs** - Toggle between course description and downloadable files
|
|
18
|
+
- **Accordion chapters** - Expandable chapters with element list
|
|
19
|
+
- **Element type icons** - Different icons for video, quiz, pdf, iframe, youtube
|
|
20
|
+
- **Progress tracking** - Green icons for completed elements via `course.progressData`
|
|
21
|
+
- **Video viewer** - Native HTML5 video player with custom styling
|
|
22
|
+
- **Element viewer slot** - Named slot for parent to render non-video content
|
|
23
|
+
- **RTL support** - Layout adjustment for Arabic locale
|
|
24
|
+
|
|
25
|
+
## Student Course Player
|
|
26
|
+
|
|
27
|
+
### Component Structure
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
31
|
+
│ .course-player │
|
|
32
|
+
│ ┌──────────────────┬────────────────────────────────────────────┤
|
|
33
|
+
│ │ .course-sidebar │ .course-viewer │
|
|
34
|
+
│ │ │ ┌────────────────────────────────────────┐ │
|
|
35
|
+
│ │ [Course Card] │ │ .course-viewer-mobile-title │ │
|
|
36
|
+
│ │ Logo + Name │ │ (Chapter/Course name + back button) │ │
|
|
37
|
+
│ │ │ ├────────────────────────────────────────┤ │
|
|
38
|
+
│ │ [Tabs] │ │ .course-viewer-title │ │
|
|
39
|
+
│ │ Description|Files│ │ (Current element title) │ │
|
|
40
|
+
│ │ │ ├────────────────────────────────────────┤ │
|
|
41
|
+
│ │ [Tab Content] │ │ <video> or <slot name="elementViewer"> │ │
|
|
42
|
+
│ │ Desc or Files │ │ │ │
|
|
43
|
+
│ │ │ │ │ │
|
|
44
|
+
│ │ [Chapters] │ │ │ │
|
|
45
|
+
│ │ ▼ Chapter 1 │ │ │ │
|
|
46
|
+
│ │ ● Lesson 1 │ │ │ │
|
|
47
|
+
│ │ ○ Lesson 2 │ │ │ │
|
|
48
|
+
│ │ ▶ Chapter 2 │ │ │ │
|
|
49
|
+
│ └──────────────────┴────────────────────────────────────────────┤
|
|
50
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Available Props
|
|
54
|
+
|
|
55
|
+
| Prop | Type | Description |
|
|
56
|
+
|------|------|-------------|
|
|
57
|
+
| `user` | Object | The current logged-in user object (see structure below) |
|
|
58
|
+
| `course` | Object | The course object with chapters, elements, files, and progress (see structure below) |
|
|
59
|
+
|
|
60
|
+
### The `user` Object
|
|
61
|
+
|
|
62
|
+
```js
|
|
63
|
+
{
|
|
64
|
+
id: "user-id",
|
|
65
|
+
name: "John Doe",
|
|
66
|
+
email: "john@example.com",
|
|
67
|
+
customer_locale: "en" // "en", "fr", "ar" - used for RTL detection
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### The `course` Object
|
|
72
|
+
|
|
73
|
+
```js
|
|
74
|
+
{
|
|
75
|
+
name: "Course Name",
|
|
76
|
+
description: "Course description text...",
|
|
77
|
+
logo: { path: "/uploads/logo.jpg" },
|
|
78
|
+
chaptersEnabled: true, // Whether chapters mode is enabled
|
|
79
|
+
files: [ // Downloadable course files
|
|
80
|
+
{
|
|
81
|
+
name: "resource.pdf",
|
|
82
|
+
path: "/uploads/resource.pdf",
|
|
83
|
+
size: 1048576 // Size in bytes
|
|
84
|
+
}
|
|
85
|
+
],
|
|
86
|
+
chapters: [ // Array of chapters
|
|
87
|
+
{
|
|
88
|
+
title: "Chapter 1: Introduction", // Or "i18n:key" for translation
|
|
89
|
+
elements: [ // Array of elements in this chapter
|
|
90
|
+
{
|
|
91
|
+
id: "el-1",
|
|
92
|
+
title: "Welcome Video",
|
|
93
|
+
type: "video", // "video", "quiz", "pdf", "iframe", "youtube", "video-iframe"
|
|
94
|
+
content: { path: "/uploads/video.mp4" }
|
|
95
|
+
}
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
],
|
|
99
|
+
progressData: { // Progress tracking (element ID → completed)
|
|
100
|
+
"el-1": true,
|
|
101
|
+
"el-2": false
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Element Types
|
|
107
|
+
|
|
108
|
+
| Type | Icon | Description |
|
|
109
|
+
|------|------|-------------|
|
|
110
|
+
| `video` | Play circle SVG | Uploaded video file (rendered by your component) |
|
|
111
|
+
| `youtube` | Play circle SVG | YouTube video (rendered by parent via slot) |
|
|
112
|
+
| `video-iframe` | Play circle SVG | Video in iframe (rendered by parent via slot) |
|
|
113
|
+
| `quiz` | `mdi-help-box-multiple` | Quiz/assessment (rendered by parent via slot) |
|
|
114
|
+
| `pdf` | `mdi-note` | PDF document (rendered by parent via slot) |
|
|
115
|
+
| `iframe` | `mdi-text-box` | Embedded iframe content (rendered by parent via slot) |
|
|
116
|
+
| (other) | `mdi-card` | Default icon for unknown types |
|
|
117
|
+
|
|
118
|
+
### Local State (data)
|
|
119
|
+
|
|
120
|
+
| Property | Type | Default | Description |
|
|
121
|
+
|----------|------|---------|-------------|
|
|
122
|
+
| `isNotMobile` | Boolean | `true` | Whether screen width > 900px |
|
|
123
|
+
| `courseDetailsCard` | String | `"description"` | Active tab: `"description"` or `"files"` |
|
|
124
|
+
| `currentMobileView` | String | `"sidebar"` | Mobile view: `"sidebar"` or `"player"` |
|
|
125
|
+
| `widthListener` | Function | `null` | Reference to resize event listener |
|
|
126
|
+
| `currentChapter` | Object | `null` | Currently selected chapter |
|
|
127
|
+
| `currentElement` | Object | `null` | Currently selected element |
|
|
128
|
+
| `selectedElement` | Object | `null` | Selected element for v-model binding |
|
|
129
|
+
| `activePanel` | Number | `0` | Index of open accordion panel (first chapter) |
|
|
130
|
+
|
|
131
|
+
### Computed Properties
|
|
132
|
+
|
|
133
|
+
| Property | Description |
|
|
134
|
+
|----------|-------------|
|
|
135
|
+
| `coursePageStyle` | Returns transform style for RTL support (Arabic locale shifts layout) |
|
|
136
|
+
|
|
137
|
+
### Methods
|
|
138
|
+
|
|
139
|
+
| Method | Parameters | Description |
|
|
140
|
+
|--------|------------|-------------|
|
|
141
|
+
| `onElementSelect(chapter, element)` | `chapter: Object, element: Object` | Handles element selection, updates current chapter/element, switches to player view on mobile |
|
|
142
|
+
| `bytesToHumanReadableSize(bytes)` | `bytes: Number` | Converts bytes to human-readable format (KB, MB, GB) |
|
|
143
|
+
| `downloadFile(path)` | `path: String` | Opens file URL in new tab for download |
|
|
144
|
+
| `capitalizeText(str)` | `str: String` | Capitalizes first letter of each word |
|
|
145
|
+
| `translateTitle(title)` | `title: String` | Translates title if it starts with `i18n:`, otherwise capitalizes |
|
|
146
|
+
| `defaultSelectFirstElement()` | - | Sets first element of first chapter as default selection |
|
|
147
|
+
|
|
148
|
+
### Page Sections
|
|
149
|
+
|
|
150
|
+
1. **Sidebar** (`.course-sidebar`)
|
|
151
|
+
- **Course Card** - Logo image and course name
|
|
152
|
+
- **Tab Buttons** - Toggle between Description and Files
|
|
153
|
+
- **Description Panel** - Shows `course.description`
|
|
154
|
+
- **Files Panel** - List of downloadable files with name, size, and download on click
|
|
155
|
+
- **Chapters Accordion** - Expandable panels for each chapter with element list
|
|
156
|
+
|
|
157
|
+
2. **Viewer** (`.course-viewer`)
|
|
158
|
+
- **Mobile Title Bar** - Chapter/course name with back button (mobile only)
|
|
159
|
+
- **Element Title** - Current element title
|
|
160
|
+
- **Video Viewer** - Native `<video>` element for video type elements
|
|
161
|
+
- **Element Viewer Slot** - Named slot for parent to render other element types
|
|
162
|
+
|
|
163
|
+
### The Element Viewer Slot
|
|
164
|
+
|
|
165
|
+
This is **critical** for the component to work properly. The parent component uses this slot to render non-video elements:
|
|
166
|
+
|
|
167
|
+
```html
|
|
168
|
+
<slot v-else name="elementViewer" :element="currentElement"></slot>
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**How it works:**
|
|
172
|
+
1. When `currentElement.type === 'video'`, your component renders the `<video>` element
|
|
173
|
+
2. For all other types (quiz, pdf, iframe, youtube, video-iframe), the slot is rendered
|
|
174
|
+
3. The parent fills this slot with the appropriate viewer (quiz component, PDF viewer, iframe, etc.)
|
|
175
|
+
4. The `element` is passed as a slot prop so the parent knows which element to render
|
|
176
|
+
|
|
177
|
+
**You must include this slot** in your template for non-video content to display.
|
|
178
|
+
|
|
179
|
+
### Events to Emit
|
|
180
|
+
|
|
181
|
+
| Event | When to Emit | Payload | Description |
|
|
182
|
+
|-------|--------------|---------|-------------|
|
|
183
|
+
| `onElementComplete` | When video is clicked or touched | `currentElement` | Notifies parent that element should be marked as complete |
|
|
184
|
+
|
|
185
|
+
**Example:**
|
|
186
|
+
```html
|
|
187
|
+
<video
|
|
188
|
+
@click="$emit('onElementComplete', currentElement)"
|
|
189
|
+
@touchstart="$emit('onElementComplete', currentElement)"
|
|
190
|
+
>
|
|
191
|
+
</video>
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Special URL Patterns
|
|
195
|
+
|
|
196
|
+
| Pattern | Description |
|
|
197
|
+
|---------|-------------|
|
|
198
|
+
| `@PS/images/video-poster.png` | Path to static assets (video poster image) |
|
|
199
|
+
|
|
200
|
+
### Available translations (Use what you need)
|
|
201
|
+
|
|
202
|
+
- `course-player.description` → "Description"
|
|
203
|
+
- `course-player.files` → "Files"
|
|
204
|
+
- `course-player.no-files-in-course` → "No files in this course"
|
|
205
|
+
- `course-player.content` → "Content"
|
|
206
|
+
- `course-player.awesome` → "Awesome!"
|
|
207
|
+
- `course-player.xp-earned` → "XP earned"
|
|
208
|
+
- `course-player.continue` → "Continue"
|
|
209
|
+
- `course-player.xp` → "XP"
|
|
210
|
+
- `course-player.course-progress` → "Course Progress"
|
|
211
|
+
- `course-player.lessons` → "lessons"
|
|
212
|
+
- `course-player.unit` → "UNIT"
|
|
213
|
+
- `course-player.tap-to-start` → "Tap to start learning"
|
|
214
|
+
- `course-player.finish` → "Finish"
|
|
215
|
+
- `course-player.course-resources` → "Course Resources"
|
|
216
|
+
|
|
217
|
+
If you want another text, just put it in English.
|
|
218
|
+
|
|
219
|
+
### Chapter Title Translation
|
|
220
|
+
|
|
221
|
+
Chapter titles can use the `i18n:` prefix for translation:
|
|
222
|
+
```js
|
|
223
|
+
{
|
|
224
|
+
title: "i18n:chapter-one.title" // Will call $t('chapter-one.title')
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Regular titles are capitalized automatically.
|
|
229
|
+
|
|
230
|
+
### Example Student Course Player:
|
|
231
|
+
```js
|
|
232
|
+
module.exports = {
|
|
233
|
+
name: "CoursePlayer",
|
|
234
|
+
props: [
|
|
235
|
+
'user',
|
|
236
|
+
'course',
|
|
237
|
+
],
|
|
238
|
+
template: /* html */`
|
|
239
|
+
<div class="course-player" :style="coursePageStyle">
|
|
240
|
+
<div class="d-flex course-page-container" style="background-color: #121523;">
|
|
241
|
+
<!-- Sidebar for Chapters -->
|
|
242
|
+
<div v-show="isNotMobile || currentMobileView == 'sidebar'" class="course-sidebar" :style="isNotMobile ? '' : 'width: 100% !important;'">
|
|
243
|
+
<!-- COURSE CARD -->
|
|
244
|
+
<div class="course-sidebar-heading" style="border: 2px solid #323851; border-radius: 7px; padding: 5px 10px; text-align: center;">
|
|
245
|
+
<img
|
|
246
|
+
:src="course.logo ? course.logo.path : '/images/placeholder.png'"
|
|
247
|
+
alt="Course Logo"
|
|
248
|
+
style="width: 100%; height: 80px; object-fit: contain;"
|
|
249
|
+
/>
|
|
250
|
+
<span style="color: white; font-weight: 300; font-size: 20px;">{{ course.name }}</span>
|
|
251
|
+
</div>
|
|
252
|
+
<!-- COURSE DETAILS CARD -->
|
|
253
|
+
<div class="course-sidebar-buttons" style="background-color: #323851; border-radius: 7px; border-bottom-left-radius: 0px; border-bottom-right-radius: 0px; margin-top: 5px; height: 30px; color: white; display: flex; padding: 0px 20px; padding-top: 1px;">
|
|
254
|
+
<span @click="courseDetailsCard = 'description'" class="opacity-pointer-on-hover course-sidebar-button-description" style="width: 50%; display: flex; align-items: center; justify-content: center; gap: 5px;">
|
|
255
|
+
<svg style="width: 15px; margin-top: -3px;" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
|
256
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" />
|
|
257
|
+
</svg>
|
|
258
|
+
{{ $t('course-player.description') }}
|
|
259
|
+
</span>
|
|
260
|
+
<span @click="courseDetailsCard = 'files'" class="opacity-pointer-on-hover course-sidebar-button-files" style="width: 50%; display: flex; align-items: center; justify-content: center; gap: 5px;">
|
|
261
|
+
<svg style="width: 17px; margin-top: -3px;" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
|
262
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="m9 13.5 3 3m0 0 3-3m-3 3v-6m1.06-4.19-2.12-2.12a1.5 1.5 0 0 0-1.061-.44H4.5A2.25 2.25 0 0 0 2.25 6v12a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18V9a2.25 2.25 0 0 0-2.25-2.25h-5.379a1.5 1.5 0 0 1-1.06-.44Z" />
|
|
263
|
+
</svg>
|
|
264
|
+
{{ $t('course-player.files') }}
|
|
265
|
+
</span>
|
|
266
|
+
</div>
|
|
267
|
+
<div class="course-sidebar-description" v-if="courseDetailsCard === 'description'" style="border: 2px solid rgb(50, 56, 81); border-radius: 7px; padding: 5px 10px; border-top-left-radius: 0px; border-top-right-radius: 0px; height: 200px; overflow: auto; text-align: center;">
|
|
268
|
+
<span style="color: white; font-weight: 300; font-size: 14px;">{{ course.description }}</span>
|
|
269
|
+
</div>
|
|
270
|
+
<div class="course-sidebar-files" v-if="courseDetailsCard === 'files'" style="border: 2px solid rgb(50, 56, 81); border-radius: 7px; padding: 5px 10px; border-top-left-radius: 0px; border-top-right-radius: 0px; height: 200px; overflow: auto; text-align: center;">
|
|
271
|
+
<div v-if="course.files.length == 0" style="color: #535c87; font-weight: 300; font-size: 16px; margin-top: 20px;">
|
|
272
|
+
{{ $t('course-player.no-files-in-course') }}
|
|
273
|
+
</div>
|
|
274
|
+
<v-list style="background-color: #00000000;">
|
|
275
|
+
<v-list-item
|
|
276
|
+
v-for="(file, index) in course.files"
|
|
277
|
+
:key="index"
|
|
278
|
+
style="height: fit-content; padding: 0px; border-bottom: 1px solid #282d45; margin-top: 7px;"
|
|
279
|
+
@click="downloadFile(file.path)">
|
|
280
|
+
|
|
281
|
+
<!-- Left Part (Icon) -->
|
|
282
|
+
<v-list-item-icon style="background-color: #00000000; width: 20px; height: 20px; margin: 10px 10px 0px 0px; justify-content: center; border-radius: 5px;">
|
|
283
|
+
<svg style="width: 30px; color: white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
|
284
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m.75 12 3 3m0 0 3-3m-3 3v-6m-1.5-9H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" />
|
|
285
|
+
</svg>
|
|
286
|
+
</v-list-item-icon>
|
|
287
|
+
|
|
288
|
+
<!-- Right Part (Name and Size) -->
|
|
289
|
+
<v-list-item-content style="text-align: left; padding: 0px; margin: 0px; color: white;">
|
|
290
|
+
<v-list-item-title style="font-size: 14px; font-weight: 400;">
|
|
291
|
+
{{ file.name }}
|
|
292
|
+
</v-list-item-title>
|
|
293
|
+
<v-list-item-subtitle style="color: white; font-weight: 200; font-size: 13px;">{{ bytesToHumanReadableSize(file.size) }}</v-list-item-subtitle>
|
|
294
|
+
</v-list-item-content>
|
|
295
|
+
|
|
296
|
+
</v-list-item>
|
|
297
|
+
</v-list>
|
|
298
|
+
</div>
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
<!-- CHAPTERS -->
|
|
302
|
+
<v-list dense style="background-color: #ff000000; margin-bottom: 50vh;" class="course-sidebar-chapters">
|
|
303
|
+
<v-expansion-panels v-model="activePanel" accordion>
|
|
304
|
+
<v-expansion-panel v-for="(chapter, index) in course.chapters" :key="index">
|
|
305
|
+
<v-expansion-panel-header>
|
|
306
|
+
{{ translateTitle(chapter.title) }}
|
|
307
|
+
</v-expansion-panel-header>
|
|
308
|
+
<v-expansion-panel-content class="expansion-panel-content">
|
|
309
|
+
<v-list-item-group v-model="selectedElement" @change="onElementSelect(chapter, $event)">
|
|
310
|
+
<v-list-item
|
|
311
|
+
v-for="(element, elIndex) in chapter.elements"
|
|
312
|
+
:key="elIndex"
|
|
313
|
+
:value="element"
|
|
314
|
+
style="border-bottom: 1px solid #ffffff0f;"
|
|
315
|
+
>
|
|
316
|
+
<v-list-item-content v-if="element.type == 'video'" style="font-size: 12px; color: white;">
|
|
317
|
+
<svg :style="(course.progressData[element.id]) ? 'color: #00dd04;' : ''" style="width: 19px; position: absolute; left: 7px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6">
|
|
318
|
+
<path fill-rule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm14.024-.983a1.125 1.125 0 0 1 0 1.966l-5.603 3.113A1.125 1.125 0 0 1 9 15.113V8.887c0-.857.921-1.4 1.671-.983l5.603 3.113Z" clip-rule="evenodd" />
|
|
319
|
+
</svg>
|
|
320
|
+
<span style="padding-left: 17px;">{{ capitalizeText(element.title) }}</span>
|
|
321
|
+
</v-list-item-content>
|
|
322
|
+
<v-list-item-content v-else-if="element.type == 'quiz'" style="font-size: 12px; color: white;" >
|
|
323
|
+
<v-icon :style="(course.progressData[element.id]) ? 'color: #00dd04;' : ''" style="width: 19px; position: absolute; left: 7px; color: white;font-size:18px ;" >mdi-help-box-multiple</v-icon>
|
|
324
|
+
<span style="padding-left: 17px;">{{ capitalizeText(element.title) }}</span>
|
|
325
|
+
</v-list-item-content>
|
|
326
|
+
<v-list-item-content v-else-if="element.type == 'pdf'" style="font-size: 12px; color: white;" >
|
|
327
|
+
<v-icon :style="(course.progressData[element.id]) ? 'color: #00dd04;' : ''" style="width: 19px; position: absolute; left: 7px; color: white;font-size:18px ;" >mdi-note</v-icon>
|
|
328
|
+
<span style="padding-left: 17px;">{{ capitalizeText(element.title) }}</span>
|
|
329
|
+
</v-list-item-content>
|
|
330
|
+
<v-list-item-content v-else-if="element.type == 'iframe'" style="font-size: 12px; color: white;" >
|
|
331
|
+
<v-icon :style="(course.progressData[element.id]) ? 'color: #00dd04;' : ''" style="width: 19px; position: absolute; left: 7px; color: white;font-size:18px ;" >mdi-text-box</v-icon>
|
|
332
|
+
<span style="padding-left: 17px;">{{ capitalizeText(element.title) }}</span>
|
|
333
|
+
</v-list-item-content>
|
|
334
|
+
<v-list-item-content v-else-if="element.type == 'youtube'" style="font-size: 12px; color: white;">
|
|
335
|
+
<svg :style="(course.progressData[element.id]) ? 'color: #00dd04;' : ''" style="width: 19px; position: absolute; left: 7px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6">
|
|
336
|
+
<path fill-rule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm14.024-.983a1.125 1.125 0 0 1 0 1.966l-5.603 3.113A1.125 1.125 0 0 1 9 15.113V8.887c0-.857.921-1.4 1.671-.983l5.603 3.113Z" clip-rule="evenodd" />
|
|
337
|
+
</svg>
|
|
338
|
+
<span style="padding-left: 17px;">{{ capitalizeText(element.title) }}</span>
|
|
339
|
+
</v-list-item-content>
|
|
340
|
+
<v-list-item-content v-else-if="element.type == 'video-iframe'" style="font-size: 12px; color: white;">
|
|
341
|
+
<svg :style="(course.progressData[element.id]) ? 'color: #00dd04;' : ''" style="width: 19px; position: absolute; left: 7px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6">
|
|
342
|
+
<path fill-rule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm14.024-.983a1.125 1.125 0 0 1 0 1.966l-5.603 3.113A1.125 1.125 0 0 1 9 15.113V8.887c0-.857.921-1.4 1.671-.983l5.603 3.113Z" clip-rule="evenodd" />
|
|
343
|
+
</svg>
|
|
344
|
+
<span style="padding-left: 17px;">{{ capitalizeText(element.title) }}</span>
|
|
345
|
+
</v-list-item-content>
|
|
346
|
+
<v-list-item-content v-else style="font-size: 12px; color: white;" >
|
|
347
|
+
<v-icon :style="(course.progressData[element.id]) ? 'color: #00dd04;' : ''" style="width: 19px; position: absolute; left: 7px; color: white;font-size:18px ;" >mdi-card</v-icon>
|
|
348
|
+
<span style="padding-left: 17px;">{{ capitalizeText(element.title) }}</span>
|
|
349
|
+
</v-list-item-content>
|
|
350
|
+
</v-list-item>
|
|
351
|
+
</v-list-item-group>
|
|
352
|
+
</v-expansion-panel-content>
|
|
353
|
+
</v-expansion-panel>
|
|
354
|
+
</v-expansion-panels>
|
|
355
|
+
</v-list>
|
|
356
|
+
|
|
357
|
+
</div>
|
|
358
|
+
|
|
359
|
+
<!-- Viewer for Videos or Quiz -->
|
|
360
|
+
<div v-show="isNotMobile || currentMobileView == 'player'" class="course-viewer">
|
|
361
|
+
<div class="course-viewer-mobile-title" style="color: white; font-weight: 400; font-size: 20px; padding: 7px 10px 5px; background-color: #ffffff17;">
|
|
362
|
+
<v-btn v-if="!isNotMobile" @click="currentMobileView = 'sidebar'" icon fav color="white" style="background-color: #ffffff24;">
|
|
363
|
+
<v-icon>mdi-chevron-left</v-icon>
|
|
364
|
+
</v-btn>
|
|
365
|
+
<span
|
|
366
|
+
:style="isNotMobile ? '' : 'display: inline-block; transform: translateY(2px); margin-left: 10px;'"
|
|
367
|
+
>
|
|
368
|
+
{{
|
|
369
|
+
course.chaptersEnabled ?
|
|
370
|
+
(currentChapter ? capitalizeText(currentChapter.title) : '') :
|
|
371
|
+
capitalizeText(course.name)
|
|
372
|
+
}}
|
|
373
|
+
</span>
|
|
374
|
+
</div>
|
|
375
|
+
<div class="course-viewer-title" style="height: 50px; background-color: black; color: white; font-weight: bold; font-size: 20px; padding: 10px 10px 5px;">
|
|
376
|
+
{{ currentElement ? capitalizeText(currentElement.title) : '' }}
|
|
377
|
+
</div>
|
|
378
|
+
|
|
379
|
+
<!-- Video Viewer -->
|
|
380
|
+
<video
|
|
381
|
+
v-if="currentElement && currentElement.type === 'video'"
|
|
382
|
+
controls
|
|
383
|
+
controlslist="nodownload"
|
|
384
|
+
class="video-viewer course-viewer-video"
|
|
385
|
+
:src="currentElement ? currentElement.content.path : ''"
|
|
386
|
+
poster="@PS/images/video-poster.png"
|
|
387
|
+
@touchstart="$emit('onElementComplete', currentElement); $event.target.paused ? $event.target.play() : $event.target.pause()"
|
|
388
|
+
@click="$emit('onElementComplete', currentElement);"
|
|
389
|
+
>
|
|
390
|
+
</video>
|
|
391
|
+
<!-- Element Viewer SLOT -->
|
|
392
|
+
<slot v-else name="elementViewer" :element="currentElement"></slot>
|
|
393
|
+
|
|
394
|
+
</div>
|
|
395
|
+
</div>
|
|
396
|
+
</div>
|
|
397
|
+
`,
|
|
398
|
+
data: /* js */`
|
|
399
|
+
function() {
|
|
400
|
+
return {
|
|
401
|
+
isNotMobile: true,
|
|
402
|
+
courseDetailsCard: "description", // OR "files"
|
|
403
|
+
currentMobileView: "sidebar",
|
|
404
|
+
widthListener: null,
|
|
405
|
+
currentChapter: null,
|
|
406
|
+
currentElement: null,
|
|
407
|
+
selectedElement: null,
|
|
408
|
+
activePanel: 0, // Open the first chapter by default
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
`,
|
|
412
|
+
computed: /* js */`
|
|
413
|
+
{
|
|
414
|
+
coursePageStyle() {
|
|
415
|
+
const isArabic = this.user && this.user.customer_locale === 'ar';
|
|
416
|
+
return {
|
|
417
|
+
transform: isArabic ? 'translateX(40px)' : 'translateX(-40px)'
|
|
418
|
+
};
|
|
419
|
+
},
|
|
420
|
+
}
|
|
421
|
+
`,
|
|
422
|
+
mounted: /* js */`
|
|
423
|
+
function() {
|
|
424
|
+
// Set default
|
|
425
|
+
this.defaultSelectFirstElement();
|
|
426
|
+
|
|
427
|
+
// Check if the screen is mobile
|
|
428
|
+
this.isNotMobile = window.innerWidth > 900;
|
|
429
|
+
this.widthListener = () => {
|
|
430
|
+
this.isNotMobile = window.innerWidth > 900;
|
|
431
|
+
};
|
|
432
|
+
window.addEventListener("resize", this.widthListener);
|
|
433
|
+
}
|
|
434
|
+
`,
|
|
435
|
+
beforeDestroy: /* js */`
|
|
436
|
+
function() {
|
|
437
|
+
// Remove the event listener when the component is destroyed
|
|
438
|
+
window.removeEventListener("resize", this.widthListener);
|
|
439
|
+
}
|
|
440
|
+
`,
|
|
441
|
+
methods: /* js */`
|
|
442
|
+
{
|
|
443
|
+
onElementSelect(chapter, element) {
|
|
444
|
+
if(!this.isNotMobile) {
|
|
445
|
+
this.currentMobileView = 'player';
|
|
446
|
+
}
|
|
447
|
+
if (!element) {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
if (element && element.id && this.currentElement && element.id == this.currentElement.id) {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
this.currentElement = element;
|
|
454
|
+
this.currentChapter = chapter;
|
|
455
|
+
},
|
|
456
|
+
bytesToHumanReadableSize(bytes) {
|
|
457
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
|
458
|
+
if (bytes == 0) return '0 Byte';
|
|
459
|
+
const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
|
|
460
|
+
return Math.round(bytes / Math.pow(1024, i), 2) + ' ' + sizes[i];
|
|
461
|
+
},
|
|
462
|
+
downloadFile(path) {
|
|
463
|
+
window.open(path, "_blank");
|
|
464
|
+
},
|
|
465
|
+
capitalizeText(str) {
|
|
466
|
+
return (str || '').split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
|
|
467
|
+
},
|
|
468
|
+
translateTitle(title) {
|
|
469
|
+
if (title && title.startsWith('i18n:')) {
|
|
470
|
+
return this.$t(title.replace('i18n:', ''));
|
|
471
|
+
}
|
|
472
|
+
return this.capitalizeText(title);
|
|
473
|
+
},
|
|
474
|
+
defaultSelectFirstElement() {
|
|
475
|
+
// Set default to first element of first chapter
|
|
476
|
+
if (this.course.chapters.length > 0 && this.course.chapters[0].elements.length > 0) {
|
|
477
|
+
this.selectedElement = this.course.chapters[0].elements[0];
|
|
478
|
+
this.currentChapter = this.course.chapters[0];
|
|
479
|
+
this.currentElement = this.selectedElement;
|
|
480
|
+
}
|
|
481
|
+
},
|
|
482
|
+
}
|
|
483
|
+
`,
|
|
484
|
+
style: /* css */`
|
|
485
|
+
.course-player {
|
|
486
|
+
width: calc(100% + 80px);
|
|
487
|
+
margin-top: -12px;
|
|
488
|
+
height: calc(100vh - 76px);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
.course-player .v-overlay--active {
|
|
492
|
+
backdrop-filter: blur(8px);
|
|
493
|
+
}
|
|
494
|
+
.course-player .v-expansion-panel {
|
|
495
|
+
background-color: #d7deff;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
.course-player .opacity-pointer-on-hover:hover {
|
|
499
|
+
cursor: pointer;
|
|
500
|
+
opacity: 0.5;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/* ------------- VIDEO -------------- */
|
|
504
|
+
.course-player .video-viewer {
|
|
505
|
+
max-width: 100%;
|
|
506
|
+
}
|
|
507
|
+
.course-player video {
|
|
508
|
+
width: 100%;
|
|
509
|
+
height: calc(100% - 100px);
|
|
510
|
+
background-color: #000;
|
|
511
|
+
border-radius: 7px;
|
|
512
|
+
border-top-right-radius: 0px;
|
|
513
|
+
border-top-left-radius: 0px;
|
|
514
|
+
}
|
|
515
|
+
.course-player video::-webkit-media-controls-play-button {
|
|
516
|
+
background-color: var(--v-primary-base);
|
|
517
|
+
color: white;
|
|
518
|
+
margin-right: 15px;
|
|
519
|
+
}
|
|
520
|
+
.course-player video::-webkit-media-controls-timeline {
|
|
521
|
+
background-color: rgba(255, 255, 255, 0.2);
|
|
522
|
+
}
|
|
523
|
+
.course-player video::-webkit-media-controls-volume-slider {
|
|
524
|
+
background-color: var(--v-primary-base);
|
|
525
|
+
border-radius: 6px;
|
|
526
|
+
padding: 10px 10px;
|
|
527
|
+
align-self: center;
|
|
528
|
+
}
|
|
529
|
+
.course-player video::-webkit-media-controls-fullscreen-button {
|
|
530
|
+
background-color: var(--v-primary-base);
|
|
531
|
+
color: white;
|
|
532
|
+
}
|
|
533
|
+
.course-player video::-webkit-media-controls-current-time-display {
|
|
534
|
+
color: white;
|
|
535
|
+
}
|
|
536
|
+
.course-player video::-webkit-media-controls-time-remaining-display {
|
|
537
|
+
color: var(--v-primary-base);
|
|
538
|
+
}
|
|
539
|
+
.course-player video::-webkit-media-controls-timeline {
|
|
540
|
+
width: calc(100% - 30px) !important;
|
|
541
|
+
padding: 0px;
|
|
542
|
+
position: absolute;
|
|
543
|
+
top: calc(100% - 70px) !important;
|
|
544
|
+
left: 15px;
|
|
545
|
+
border-radius: 0px !important;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/* ------------- SIDEBAR -------------- */
|
|
549
|
+
.course-player .course-sidebar {
|
|
550
|
+
height: calc(100vh - 64px);
|
|
551
|
+
width: 280px;
|
|
552
|
+
border-right: 1px solid #1f243b;
|
|
553
|
+
overflow-y: auto;
|
|
554
|
+
padding: 7px 7px;
|
|
555
|
+
flex-shrink: 0;
|
|
556
|
+
}
|
|
557
|
+
.course-player .course-sidebar {
|
|
558
|
+
overflow-y: scroll;
|
|
559
|
+
scrollbar-width: thin;
|
|
560
|
+
scrollbar-color: #6b7291 transparent !important;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
.course-player .course-sidebar:hover {
|
|
564
|
+
scrollbar-color: #98a1c7 transparent !important;
|
|
565
|
+
}
|
|
566
|
+
.course-player .course-sidebar .v-expansion-panel-header {
|
|
567
|
+
padding: 10px;
|
|
568
|
+
background-color: #23273d;
|
|
569
|
+
color: white;
|
|
570
|
+
}
|
|
571
|
+
.course-player .course-sidebar .v-expansion-panel-header:hover {
|
|
572
|
+
background-color: #394061;
|
|
573
|
+
}
|
|
574
|
+
.course-player .course-sidebar .v-expansion-panels .v-expansion-panel {
|
|
575
|
+
background-color: #00000000 !important;
|
|
576
|
+
}
|
|
577
|
+
.course-player .course-sidebar .v-expansion-panels {
|
|
578
|
+
border-radius: 7px;
|
|
579
|
+
overflow: hidden;
|
|
580
|
+
border: unset !important;
|
|
581
|
+
}
|
|
582
|
+
.course-player .course-sidebar .expansion-panel-content {
|
|
583
|
+
background-color: #12172b !important;
|
|
584
|
+
color: white;
|
|
585
|
+
font-weight: 500;
|
|
586
|
+
border-bottom-left-radius: 7px;
|
|
587
|
+
border-bottom-right-radius: 7px;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/* ------------- VIEWER -------------- */
|
|
591
|
+
.course-player .course-viewer {
|
|
592
|
+
display: block;
|
|
593
|
+
flex-grow: 1;
|
|
594
|
+
justify-content: flex-start;
|
|
595
|
+
flex-direction: column;
|
|
596
|
+
height: calc(100vh - 65px);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
.course-player .course-sidebar .expansion-panel-content > :first-child {
|
|
600
|
+
padding: 0px !important;
|
|
601
|
+
}
|
|
602
|
+
`
|
|
603
|
+
}
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
## Vue Component Format
|
|
607
|
+
|
|
608
|
+
### Basic Structure
|
|
609
|
+
|
|
610
|
+
```js
|
|
611
|
+
module.exports = {
|
|
612
|
+
name: "MyComponent",
|
|
613
|
+
|
|
614
|
+
props: {
|
|
615
|
+
title: { required: true },
|
|
616
|
+
count: { default: 0 }
|
|
617
|
+
},
|
|
618
|
+
// Or in array format:
|
|
619
|
+
// props: [
|
|
620
|
+
// 'title',
|
|
621
|
+
// 'count'
|
|
622
|
+
// ],
|
|
623
|
+
|
|
624
|
+
template: /* html */`
|
|
625
|
+
<div class="my-component">
|
|
626
|
+
<h1>{{ title }}</h1>
|
|
627
|
+
<button @click="increment">Count: {{ counter }}</button>
|
|
628
|
+
</div>
|
|
629
|
+
`,
|
|
630
|
+
|
|
631
|
+
style: /* css */`
|
|
632
|
+
.my-component { padding: 20px; }
|
|
633
|
+
.my-component h1 { color: blue; }
|
|
634
|
+
`,
|
|
635
|
+
|
|
636
|
+
data: /* js */`
|
|
637
|
+
function() {
|
|
638
|
+
return {
|
|
639
|
+
counter: 0
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
`,
|
|
643
|
+
|
|
644
|
+
computed: /* js */`
|
|
645
|
+
{
|
|
646
|
+
counterText() {
|
|
647
|
+
return this.counter + " Counts";
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
`,
|
|
651
|
+
|
|
652
|
+
methods: /* js */`
|
|
653
|
+
{
|
|
654
|
+
increment() {
|
|
655
|
+
this.counter++;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
`,
|
|
659
|
+
|
|
660
|
+
mounted: /* js */`
|
|
661
|
+
function() {
|
|
662
|
+
console.log('Component mounted!');
|
|
663
|
+
}
|
|
664
|
+
`
|
|
665
|
+
};
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
### Available Fields
|
|
669
|
+
|
|
670
|
+
| Field | Format | Description |
|
|
671
|
+
|-------|--------|-------------|
|
|
672
|
+
| `name` | String | Component name (required) |
|
|
673
|
+
| `props` | Object | Props definition (not a string) |
|
|
674
|
+
| `template` | Template literal | HTML template with Vue syntax |
|
|
675
|
+
| `style` | Template literal | CSS styles for the component |
|
|
676
|
+
| `data` | Template literal | Function returning initial state |
|
|
677
|
+
| `computed` | Template literal | Object with computed properties |
|
|
678
|
+
| `watch` | Template literal | Object with watchers |
|
|
679
|
+
| `methods` | Template literal | Object with methods |
|
|
680
|
+
| `mounted` | Template literal | Lifecycle hook function |
|
|
681
|
+
| `created`, `beforeMount`, `beforeUpdate`, `updated`, `beforeDestroy`, `destroyed` | Template literal | Other lifecycle hooks |
|
|
682
|
+
|
|
683
|
+
### Key Rules
|
|
684
|
+
|
|
685
|
+
1. **Use template literals** (backticks) for `template`, `style`, `data`, `methods`, etc.
|
|
686
|
+
2. **Props is an object**, not a template literal
|
|
687
|
+
3. **Comments are optional** but recommended: `/* html */`, `/* css */`, `/* js */`
|
|
688
|
+
4. **Unused fields** can be omitted or set to `null`
|
|
689
|
+
5. **Empty file marker**: `/* EMPTY FILE */` for placeholder files
|
|
690
|
+
|
|
691
|
+
### Minimal Example
|
|
692
|
+
|
|
693
|
+
```js
|
|
694
|
+
module.exports = {
|
|
695
|
+
name: "HelloWorld",
|
|
696
|
+
template: /* html */`
|
|
697
|
+
<div>Hello, World!</div>
|
|
698
|
+
`
|
|
699
|
+
};
|
|
700
|
+
```
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
- [Methods](#methods)
|
|
16
16
|
- [Page Sections](#page-sections)
|
|
17
17
|
- [Events to Emit](#events-to-emit)
|
|
18
|
-
- [Available translations](#available-translations)
|
|
18
|
+
- [Available translations (Use what you need)](#available-translations-use-what-you-need)
|
|
19
19
|
- [Example Student List Courses Page:](#example-student-list-courses-page)
|
|
20
20
|
- [Vue Component Format](#vue-component-format)
|
|
21
21
|
- [Basic Structure](#basic-structure)
|
|
@@ -137,16 +137,40 @@ Each course object contains:
|
|
|
137
137
|
|-------|--------------|-------------|
|
|
138
138
|
| `loadCoursesByCategoryId` | Category card click | Filter courses by category: `$emit('loadCoursesByCategoryId', category.id)` |
|
|
139
139
|
|
|
140
|
-
### Available translations
|
|
140
|
+
### Available translations (Use what you need)
|
|
141
141
|
|
|
142
|
-
`courses-page.no-grade-assigned` ->
|
|
142
|
+
`courses-page.no-grade-assigned` -> You are not assigned to any grade yet!
|
|
143
143
|
`courses-page.explore-the` -> Explore the
|
|
144
144
|
`courses-page.categories` -> Categories
|
|
145
|
-
`courses-page.featured-courses` -> Featured
|
|
146
|
-
`courses-page.explore-featured-courses` ->
|
|
147
|
-
`courses-page.courses-on-promotion` -> Courses on
|
|
145
|
+
`courses-page.featured-courses` -> Featured courses
|
|
146
|
+
`courses-page.explore-featured-courses` -> featured courses
|
|
147
|
+
`courses-page.courses-on-promotion` -> Courses on promotion
|
|
148
148
|
`courses-page.courses` -> Courses
|
|
149
|
-
`courses-page.on-promotion` ->
|
|
149
|
+
`courses-page.on-promotion` -> on promotion
|
|
150
|
+
`courses-page.courses-list` -> Courses List
|
|
151
|
+
`courses-page.course-description` -> Course Description
|
|
152
|
+
`courses-page.course-content` -> Course Content
|
|
153
|
+
`courses-page.free` -> FREE
|
|
154
|
+
`courses-page.lessons` -> Lessons
|
|
155
|
+
`courses-page.language` -> Language
|
|
156
|
+
`courses-page.english` -> English
|
|
157
|
+
`courses-page.get-course-free` -> Get the course for free
|
|
158
|
+
`courses-page.buy-now` -> Buy now
|
|
159
|
+
`courses-page.request-now` -> Request now
|
|
160
|
+
`courses-page.expires-on` -> Expires on
|
|
161
|
+
`courses-page.lifetime-access` -> Lifetime access
|
|
162
|
+
`courses-page.featured-course` -> Featured Course
|
|
163
|
+
`courses-page.certificate` -> Certificate
|
|
164
|
+
`courses-page.chapters` -> chapters
|
|
165
|
+
`courses-page.preview` -> Preview
|
|
166
|
+
`courses-page.included` -> Included
|
|
167
|
+
`courses-page.free-preview` -> Free Preview
|
|
168
|
+
`courses-page.secure-payment` -> Secure Payment
|
|
169
|
+
`courses-page.money-back-guarantee` -> Money Back Guarantee
|
|
170
|
+
`courses-page.ready-to-learn` -> Ready to learn?
|
|
171
|
+
`courses-page.start` -> Start
|
|
172
|
+
`courses-page.promo` -> PROMO
|
|
173
|
+
`courses-page.hot-deal` -> HOT DEAL
|
|
150
174
|
- If you want another text, just put it in English
|
|
151
175
|
|
|
152
176
|
### Example Student List Courses Page:
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
- [websiteInfo Object](#websiteinfo-object)
|
|
14
14
|
- [Page Sections](#page-sections)
|
|
15
15
|
- [Events to Emit](#events-to-emit)
|
|
16
|
-
- [Available translations](#available-translations)
|
|
16
|
+
- [Available translations (Use what you need)](#available-translations-use-what-you-need)
|
|
17
17
|
- [Example Student My Courses Page:](#example-student-my-courses-page)
|
|
18
18
|
- [Vue Component Format](#vue-component-format)
|
|
19
19
|
- [Basic Structure](#basic-structure)
|
|
@@ -107,7 +107,7 @@ Each course object contains:
|
|
|
107
107
|
| `openCourse` | Open button click | Open the course player: `$emit('openCourse', item)` |
|
|
108
108
|
| `openCertificate` | Certificate button click | Generate/view certificate: `$emit('openCertificate', item)` |
|
|
109
109
|
|
|
110
|
-
### Available translations
|
|
110
|
+
### Available translations (Use what you need)
|
|
111
111
|
|
|
112
112
|
`student-courses.my-courses-list` -> My Course List
|
|
113
113
|
`student-courses.no-courses-purchased` -> No courses purchased
|