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` -> 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 Courses
146
- `courses-page.explore-featured-courses` -> Featured Courses
147
- `courses-page.courses-on-promotion` -> Courses on Promotion
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` -> 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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cyber-elx",
3
- "version": "1.1.10",
3
+ "version": "1.1.11",
4
4
  "description": "CyberOcean CLI tool to upload/download ELX custom pages",
5
5
  "main": "src/index.js",
6
6
  "bin": {