cyber-elx 1.1.9 → 1.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/DEV_DOC/README.md CHANGED
@@ -10,6 +10,9 @@ Custom Login & Register pages using Vue.js components. Covers the multi-step log
10
10
  ### [PaymentPageDev.md](PaymentPageDev.md)
11
11
  Custom Payment page for course enrollment. Handles guest vs logged-in user states, displays course information with price breakdown, and provides payment method selection (online payment or course request). Documents the course object structure, user object, and all dialog interactions.
12
12
 
13
+ ### [StudentCourseDetailPageDev.md](StudentCourseDetailPageDev.md)
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
+
13
16
  ### [StudentCssDev.md](StudentCssDev.md)
14
17
  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.
15
18
 
@@ -0,0 +1,738 @@
1
+ # CyberOcean Custom Student Course Detail Page
2
+
3
+ ## Summary
4
+
5
+ This document describes how to customize the **Student Course Detail Page** using a Vue.js component. The page displays comprehensive course information including cover image, description, curriculum with chapters/lessons, and a sticky price card for enrollment. It supports free preview videos, accordion-style chapter navigation, and responsive layout.
6
+
7
+ > **IMPORTANT:** Your custom component handles **UI and styles only**. All logic and functionality (data fetching, payment processing, navigation) are handled by the parent component.
8
+
9
+ ## Overview
10
+
11
+ The Student Course Detail Page is rendered when a student views a specific course. The parent component provides the course data, payment mode settings, and primary color. Your custom component receives these as props and renders the UI.
12
+
13
+ **Key Features:**
14
+ - **Breadcrumb navigation** - Link back to courses list with current course name
15
+ - **Course header** - Cover image, logo, and course name
16
+ - **Description section** - Course description text
17
+ - **Curriculum section** - Accordion-style chapters with lessons, element type icons, duration, and free preview buttons
18
+ - **Price card** - Sticky sidebar with pricing (supports promo), course info, buy button, and expiration text
19
+ - **Video modal** - Fullscreen modal for free preview video playback
20
+ - **Responsive design** - 2-column layout on desktop, single column on mobile with price card on top
21
+
22
+ ## Student Course Detail Page
23
+
24
+ ### Available Props
25
+
26
+ | Prop | Type | Description |
27
+ |------|------|-------------|
28
+ | `course` | Object | The course object containing all course data (see structure below) |
29
+ | `courseId` | String | The unique identifier of the course |
30
+ | `onlineMode` | Boolean | Whether online payment is enabled |
31
+ | `offlineMode` | Boolean | Whether offline/request mode is enabled |
32
+ | `primaryColor` | String | The website's primary color (hex value) |
33
+
34
+ ### The `course` Object
35
+
36
+ ```js
37
+ {
38
+ id: "course-id",
39
+ name: "Course Name",
40
+ description: "Course description text...",
41
+ cover: { path: "/uploads/cover.jpg" }, // Cover image
42
+ logo: { path: "/uploads/logo.jpg" }, // Course logo/thumbnail
43
+ price: "99", // Current price (string)
44
+ barredPrice: "149", // Original price before discount
45
+ promo: true, // Whether course is on promotion
46
+ expirationType: "in-months", // "in-months", "fixed-date", or "lifetime"
47
+ expirationMonthsCount: "6", // Months until expiration (if type is "in-months")
48
+ expirationDate: "1735689600000", // Timestamp (if type is "fixed-date")
49
+ chapters: [ // Array of chapters (optional)
50
+ {
51
+ id: "chapter-1",
52
+ title: "Chapter 1: Introduction",
53
+ elementsIds: ["el-1", "el-2", "el-3"] // References to elements by ID
54
+ }
55
+ ],
56
+ elements: [ // Array of all course elements
57
+ {
58
+ id: "el-1",
59
+ title: "Welcome Video",
60
+ type: "video", // "video", "youtube", "pdf", "quiz", "iframe"
61
+ time: { minutes: 5, seconds: 30 }, // Duration
62
+ freePreview: true, // Whether element is free to preview
63
+ content: { path: "/uploads/video.mp4" } // Content URL
64
+ }
65
+ ]
66
+ }
67
+ ```
68
+
69
+ ### Element Types
70
+
71
+ | Type | Icon | Description |
72
+ |------|------|-------------|
73
+ | `video` | `mdi-youtube` | Uploaded video file |
74
+ | `youtube` | `mdi-youtube` | YouTube embedded video |
75
+ | `pdf` | `mdi-file-pdf-box` | PDF document |
76
+ | `quiz` | `mdi-clipboard-text-outline` | Quiz/assessment |
77
+ | `iframe` | `mdi-picture-in-picture-bottom-right-outline` | Embedded iframe content |
78
+ | (other) | `mdi-file` | Default file icon |
79
+
80
+ ### Local State (data)
81
+
82
+ | Property | Type | Default | Description |
83
+ |----------|------|---------|-------------|
84
+ | `showModal` | Boolean | `false` | Controls video modal visibility |
85
+ | `modalVideoUrl` | String | `''` | URL of the video to play in modal |
86
+ | `openChapters` | Array | `[]` | Array of chapter IDs that are currently expanded |
87
+
88
+ ### Computed Properties
89
+
90
+ | Property | Description |
91
+ |----------|-------------|
92
+ | `chapterWithElements` | Maps chapters with their full element objects (not just IDs) |
93
+ | `buttonText` | Returns appropriate button text: "Get Course Free", "Buy Now", or "Request Now" |
94
+ | `expirationText` | Returns expiration info: calculated date, fixed date, or "Lifetime Access" |
95
+
96
+ ### Methods
97
+
98
+ | Method | Parameters | Description |
99
+ |--------|------------|-------------|
100
+ | `getElementById(id)` | `id: String` | Finds and returns an element from `course.elements` by ID |
101
+ | `formatTime(time)` | `time: Object` | Formats `{ minutes, seconds }` to "MM:SS" string |
102
+ | `toggleChapter(id)` | `id: String` | Toggles chapter accordion open/closed state |
103
+ | `openVideoModal(url)` | `url: String` | Opens video modal and starts playback |
104
+ | `closeModal()` | - | Closes modal and stops video playback |
105
+ | `calculateExpirationDate()` | - | Calculates expiration date based on months from now |
106
+ | `formatDate(timestamp)` | `timestamp: String/Number` | Formats timestamp to locale date string |
107
+
108
+ ### Page Sections
109
+
110
+ 1. **Breadcrumb** - Navigation showing "Courses List / Course Name"
111
+ 2. **Cover Image** - Full-width course cover image
112
+ 3. **Layout Container** - Two-column grid (main content + price card)
113
+ 4. **Header** - Course logo and name in a card
114
+ 5. **Description** - Course description section
115
+ 6. **Curriculum** - Accordion chapters with lessons:
116
+ - If `course.chapters` exists: Show chapters with nested elements
117
+ - If no chapters: Show single "Course Content" accordion with all elements
118
+ - Each lesson shows: type icon, title, duration, and lock/preview button
119
+ 7. **Price Card** (sticky sidebar):
120
+ - Price display (with strikethrough for promo)
121
+ - Course info rows (lessons count, language)
122
+ - Buy/Request button (links to payment page)
123
+ - Expiration text
124
+ 8. **Video Modal** - Overlay with video player for free previews
125
+
126
+ ### Available translations
127
+
128
+ - `courses-page.courses-list` → "Courses List"
129
+ - `courses-page.course-description` → "Course Description"
130
+ - `courses-page.course-content` → "Course Content"
131
+ - `courses-page.free` → "Free"
132
+ - `courses-page.lessons` → "Lessons"
133
+ - `courses-page.language` → "Language"
134
+ - `courses-page.english` → "English"
135
+ - `courses-page.buy-now` → "Buy Now"
136
+ - `courses-page.request-now` → "Request Now"
137
+ - `courses-page.get-course-free` → "Get Course Free"
138
+ - `courses-page.expires-on` → "Expires on"
139
+ - `courses-page.lifetime-access` → "Lifetime Access"
140
+ - `courses-page.featured-course` → "Featured Course"
141
+ - `courses-page.certificate` → "Certificate"
142
+ - `courses-page.chapters` → "chapters"
143
+ - `courses-page.preview` → "Preview"
144
+ - `courses-page.included` → "Included"
145
+ - `courses-page.free-preview` → "Free Preview"
146
+ - `courses-page.secure-payment` → "Secure Payment"
147
+ - `courses-page.money-back-guarantee` → "Money Back Guarantee"
148
+
149
+ If you want another text, just put it in English.
150
+
151
+ ### Special URL Patterns
152
+
153
+ | Pattern | Description |
154
+ |---------|-------------|
155
+ | `@PV/courses/courses` | Link to courses list page (router-link) |
156
+ | `@PVP/course-payment/{id}` | Link to payment page (opens in new tab) |
157
+
158
+ ### Example Student Course Detail Page:
159
+ ```js
160
+ module.exports = {
161
+ name: "CourseDetail",
162
+ props: [
163
+ 'course',
164
+ 'courseId',
165
+ 'onlineMode',
166
+ 'offlineMode',
167
+ 'primaryColor',
168
+ ],
169
+ template: /* html */`
170
+ <div class="course-detail">
171
+ <!-- Breadcrumb -->
172
+ <nav class="breadcrumb">
173
+ <router-link to="@PV/courses/courses">{{ $t('courses-page.courses-list') }}</router-link>
174
+ <span :style="{ color: primaryColor }">{{ course.name }}</span>
175
+ </nav>
176
+
177
+ <!-- Cover -->
178
+ <img :src="course.cover?.path" alt="" class="cover-img" />
179
+
180
+ <!-- Layout -->
181
+ <div class="layout">
182
+ <!-- Main -->
183
+ <div class="main">
184
+ <header class="header">
185
+ <img :src="course.logo?.path" :alt="course.name" class="logo" />
186
+ <h1>{{ course.name }}</h1>
187
+ </header>
188
+
189
+ <section class="description">
190
+ <h2>{{ $t('courses-page.course-description') }}</h2>
191
+ <p>{{ course.description }}</p>
192
+ </section>
193
+
194
+ <!-- Curriculum -->
195
+ <section class="curriculum">
196
+ <template v-if="course.chapters?.length > 0">
197
+ <div v-for="chapter in chapterWithElements" :key="chapter.id" class="chapter">
198
+ <div class="chapter-head" @click="toggleChapter(chapter.id)">
199
+ <span>{{ chapter.title }}</span>
200
+ <span>{{ openChapters.includes(chapter.id) ? '−' : '+' }}</span>
201
+ </div>
202
+ <div v-if="openChapters.includes(chapter.id)" class="lessons">
203
+ <div v-for="elId in chapter.elementsIds" :key="elId" class="lesson">
204
+ <div class="lesson-left">
205
+ <v-icon v-if="getElementById(elId).type == 'quiz'">mdi-clipboard-text-outline</v-icon>
206
+ <v-icon v-else-if="getElementById(elId).type == 'youtube'">mdi-youtube</v-icon>
207
+ <v-icon v-else-if="getElementById(elId).type == 'video'">mdi-youtube</v-icon>
208
+ <v-icon v-else-if="getElementById(elId).type == 'pdf'">mdi-file-pdf-box</v-icon>
209
+ <v-icon v-else-if="getElementById(elId).type == 'iframe'">mdi-picture-in-picture-bottom-right-outline</v-icon>
210
+ <v-icon v-else>mdi-file</v-icon>
211
+ <span>{{ getElementById(elId).title }}</span>
212
+ </div>
213
+ <div class="lesson-right">
214
+ <span class="duration">{{ formatTime(getElementById(elId).time) }}</span>
215
+ <button v-if="getElementById(elId).freePreview" class="play-btn" @click="openVideoModal(getElementById(elId).content?.path)">
216
+ <v-icon small>mdi-play-circle</v-icon>
217
+ </button>
218
+ <v-icon v-else small color="#999">mdi-lock</v-icon>
219
+ </div>
220
+ </div>
221
+ </div>
222
+ </div>
223
+ </template>
224
+ <template v-else>
225
+ <div class="chapter">
226
+ <div class="chapter-head" @click="toggleChapter('default')">
227
+ <span>{{ $t('courses-page.course-content') }}</span>
228
+ <span>{{ openChapters.includes('default') ? '−' : '+' }}</span>
229
+ </div>
230
+ <div v-if="openChapters.includes('default')" class="lessons">
231
+ <div v-for="el in course.elements" :key="el.id" class="lesson">
232
+ <div class="lesson-left">
233
+ <v-icon small>mdi-play-circle</v-icon>
234
+ <span>{{ el.title }}</span>
235
+ </div>
236
+ <div class="lesson-right">
237
+ <span class="duration">{{ formatTime(el.time) }}</span>
238
+ <button v-if="el.freePreview" class="play-btn" @click="openVideoModal(el.content?.path)">
239
+ <v-icon small>mdi-play</v-icon>
240
+ </button>
241
+ <v-icon v-else small color="#999">mdi-lock</v-icon>
242
+ </div>
243
+ </div>
244
+ </div>
245
+ </div>
246
+ </template>
247
+ </section>
248
+ </div>
249
+
250
+ <!-- Price Card -->
251
+ <div class="price-card-container">
252
+ <div class="price-card">
253
+ <div class="price">
254
+ <span v-if="course.promo" class="old-price">{{ course.barredPrice }} dt</span>
255
+ <span class="current-price">{{ course.price === '0' ? $t('courses-page.free') : course.price + ' dt' }}</span>
256
+ </div>
257
+ <div class="info-container">
258
+ <div class="info-row">
259
+ <v-icon small :color="primaryColor">mdi-file-document-outline</v-icon>
260
+ <span>{{ $t('courses-page.lessons') }}</span>
261
+ <span>{{ course.elements?.length || 0 }}</span>
262
+ </div>
263
+ <div class="info-row">
264
+ <v-icon small :color="primaryColor">mdi-translate</v-icon>
265
+ <span>{{ $t('courses-page.language') }}</span>
266
+ <span>{{ $t('courses-page.english') }}</span>
267
+ </div>
268
+ </div>
269
+ <a :href="'@PVP/course-payment/' + course.id" target="_blank" class="buy-btn" :style="{ backgroundColor: primaryColor }">
270
+ {{ buttonText }}
271
+ </a>
272
+ <p class="expiry">{{ expirationText }}</p>
273
+ </div>
274
+ </div>
275
+ </div>
276
+
277
+ <!-- Video Modal -->
278
+ <div v-if="showModal" class="modal-overlay" @click="closeModal">
279
+ <div class="modal-box" @click.stop>
280
+ <button class="close-btn" @click="closeModal">✕</button>
281
+ <video ref="videoPlayer" controls class="video">
282
+ <source :src="modalVideoUrl" type="video/mp4" />
283
+ </video>
284
+ </div>
285
+ </div>
286
+ </div>
287
+ `,
288
+ data: /* js */`
289
+ function() {
290
+ return {
291
+ showModal: false,
292
+ modalVideoUrl: '',
293
+ openChapters: []
294
+ };
295
+ }
296
+ `,
297
+ computed: /* js */`
298
+ {
299
+ chapterWithElements() {
300
+ if (!this.course.chapters) return [];
301
+ return this.course.chapters.map(ch => ({
302
+ ...ch,
303
+ elements: ch.elementsIds.map(id => this.getElementById(id))
304
+ }));
305
+ },
306
+ buttonText() {
307
+ if (this.course.price === "0") return this.$t('courses-page.get-course-free');
308
+ return this.onlineMode ? this.$t('courses-page.buy-now') : this.$t('courses-page.request-now');
309
+ },
310
+ expirationText() {
311
+ if (this.course.expirationType === 'in-months') {
312
+ return this.$t('courses-page.expires-on') + ' : ' + this.calculateExpirationDate();
313
+ }
314
+ if (this.course.expirationType === 'fixed-date' && this.course.expirationDate) {
315
+ return this.$t('courses-page.expires-on') + ' : ' + this.formatDate(this.course.expirationDate);
316
+ }
317
+ return this.$t('courses-page.lifetime-access');
318
+ }
319
+ }
320
+ `,
321
+ mounted: /* js */`
322
+ function() {
323
+ if (this.course.chapters?.length > 0) {
324
+ this.openChapters.push(this.course.chapters[0].id);
325
+ } else {
326
+ this.openChapters.push('default');
327
+ }
328
+ }
329
+ `,
330
+ methods: /* js */`
331
+ {
332
+ getElementById(id) {
333
+ return this.course.elements?.find(el => el.id === id) || {};
334
+ },
335
+ formatTime(time) {
336
+ if (!time) return '';
337
+ const m = time.minutes || 0;
338
+ const s = time.seconds || 0;
339
+ return (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s);
340
+ },
341
+ toggleChapter(id) {
342
+ const idx = this.openChapters.indexOf(id);
343
+ idx > -1 ? this.openChapters.splice(idx, 1) : this.openChapters.push(id);
344
+ },
345
+ openVideoModal(url) {
346
+ this.modalVideoUrl = url;
347
+ this.showModal = true;
348
+ this.$nextTick(() => this.$refs.videoPlayer?.play());
349
+ },
350
+ closeModal() {
351
+ this.showModal = false;
352
+ this.modalVideoUrl = '';
353
+ if (this.$refs.videoPlayer) {
354
+ this.$refs.videoPlayer.pause();
355
+ this.$refs.videoPlayer.currentTime = 0;
356
+ }
357
+ },
358
+ calculateExpirationDate() {
359
+ const months = parseInt(this.course.expirationMonthsCount) || 0;
360
+ const ts = Date.now() + months * 2629746 * 1000;
361
+ return new Date(ts).toLocaleDateString('fr-FR');
362
+ },
363
+ formatDate(timestamp) {
364
+ if (!timestamp) return '';
365
+ const ts = typeof timestamp === 'string' ? parseInt(timestamp) : timestamp;
366
+ return new Date(ts).toLocaleDateString('fr-FR');
367
+ }
368
+ }
369
+ `,
370
+ style: /* css */`
371
+ .course-detail {
372
+ max-width: 1200px;
373
+ margin: 0 auto;
374
+ padding: 20px;
375
+ }
376
+
377
+ /* Breadcrumb */
378
+ .course-detail .breadcrumb {
379
+ display: flex;
380
+ gap: 8px;
381
+ margin-bottom: 20px;
382
+ font-size: 14px;
383
+ font-weight: bold;
384
+ }
385
+ .course-detail .breadcrumb a {
386
+ color: #333;
387
+ text-decoration: none;
388
+ }
389
+ .course-detail .breadcrumb a::after {
390
+ content: "/";
391
+ margin-left: 8px;
392
+ color: #999;
393
+ }
394
+
395
+ /* Cover */
396
+ .course-detail .cover-img {
397
+ width: 100%;
398
+ height: 280px;
399
+ object-fit: cover;
400
+ border-radius: 12px;
401
+ margin-bottom: -20px;
402
+ }
403
+
404
+ /* Layout */
405
+ .course-detail .layout {
406
+ display: grid;
407
+ grid-template-columns: 1fr 320px;
408
+ gap: 32px;
409
+ }
410
+
411
+ /* Header */
412
+ .course-detail .header {
413
+ display: flex;
414
+ align-items: center;
415
+ gap: 16px;
416
+ margin-bottom: 24px;
417
+ padding: 16px;
418
+ background: #fff;
419
+ border-radius: 12px;
420
+ box-shadow: 0 4px 20px rgba(0,0,0,0.08);
421
+ }
422
+ .course-detail .logo {
423
+ width: 56px;
424
+ height: 56px;
425
+ border-radius: 8px;
426
+ object-fit: cover;
427
+ }
428
+ .course-detail .header h1 {
429
+ font-size: 24px;
430
+ font-weight: 600;
431
+ margin: 0;
432
+ color: #333;
433
+ }
434
+
435
+ /* Description */
436
+ .course-detail .description {
437
+ margin-bottom: 32px;
438
+ }
439
+ .course-detail .description h2 {
440
+ font-size: 18px;
441
+ font-weight: 600;
442
+ margin: 0 0 12px;
443
+ color: #333;
444
+ }
445
+ .course-detail .description p {
446
+ font-size: 15px;
447
+ line-height: 1.6;
448
+ color: #666;
449
+ margin: 0;
450
+ }
451
+
452
+ /* Curriculum */
453
+ .course-detail .chapter {
454
+ border: 1px solid #e5e5e5;
455
+ border-radius: 8px;
456
+ margin-bottom: 8px;
457
+ overflow: hidden;
458
+ }
459
+ .course-detail .chapter-head {
460
+ display: flex;
461
+ justify-content: space-between;
462
+ padding: 14px 16px;
463
+ background: #f8f8f8;
464
+ cursor: pointer;
465
+ font-weight: 500;
466
+ transition: background 0.2s;
467
+ }
468
+ .course-detail .chapter-head:hover {
469
+ background: #f0f0f0;
470
+ }
471
+ .course-detail .lessons {
472
+ background: #fff;
473
+ }
474
+ .course-detail .lesson {
475
+ display: flex;
476
+ justify-content: space-between;
477
+ align-items: center;
478
+ padding: 12px 16px;
479
+ border-top: 1px solid #f0f0f0;
480
+ }
481
+ .course-detail .lesson-left {
482
+ display: flex;
483
+ align-items: center;
484
+ gap: 8px;
485
+ font-size: 14px;
486
+ color: #333;
487
+ }
488
+ .course-detail .lesson-right {
489
+ display: flex;
490
+ align-items: center;
491
+ gap: 10px;
492
+ }
493
+ .course-detail .duration {
494
+ font-size: 12px;
495
+ color: #888;
496
+ }
497
+ .course-detail .play-btn {
498
+ background: none;
499
+ border: none;
500
+ cursor: pointer;
501
+ color: #007bff;
502
+ }
503
+
504
+ /* Price Card */
505
+ .course-detail .price-card-container {
506
+ position: sticky;
507
+ top: 20px;
508
+ height: fit-content;
509
+ }
510
+ .course-detail .price-card {
511
+ background: #fff;
512
+ border: 1px solid #e5e5e5;
513
+ border-radius: 12px;
514
+ padding: 24px;
515
+ box-shadow: 0 4px 16px rgba(0,0,0,0.06);
516
+ }
517
+ .course-detail .price {
518
+ text-align: center;
519
+ margin-bottom: 20px;
520
+ }
521
+ .course-detail .old-price {
522
+ display: block;
523
+ font-size: 14px;
524
+ color: #999;
525
+ text-decoration: line-through;
526
+ margin-bottom: 4px;
527
+ }
528
+ .course-detail .current-price {
529
+ font-size: 28px;
530
+ font-weight: 700;
531
+ color: #333;
532
+ }
533
+ .course-detail .info-container {
534
+ margin-bottom: 20px;
535
+ }
536
+ .course-detail .info-row {
537
+ display: flex;
538
+ align-items: center;
539
+ gap: 8px;
540
+ padding: 10px 0;
541
+ border-bottom: 1px solid #f0f0f0;
542
+ font-size: 14px;
543
+ color: #666;
544
+ }
545
+ .course-detail .info-row span:nth-child(2) {
546
+ flex: 1;
547
+ }
548
+ .course-detail .info-row span:last-child {
549
+ font-weight: 500;
550
+ color: #333;
551
+ }
552
+ .course-detail .buy-btn {
553
+ display: block;
554
+ width: 100%;
555
+ padding: 14px;
556
+ text-align: center;
557
+ color: #fff;
558
+ text-decoration: none;
559
+ border-radius: 8px;
560
+ font-weight: 600;
561
+ margin-bottom: 12px;
562
+ transition: opacity 0.2s;
563
+ }
564
+ .course-detail .buy-btn:hover {
565
+ opacity: 0.9;
566
+ }
567
+ .course-detail .expiry {
568
+ font-size: 12px;
569
+ color: #888;
570
+ text-align: center;
571
+ margin: 0;
572
+ }
573
+
574
+ /* Modal */
575
+ .course-detail .modal-overlay {
576
+ position: fixed;
577
+ inset: 0;
578
+ background: rgba(0,0,0,0.85);
579
+ display: flex;
580
+ align-items: center;
581
+ justify-content: center;
582
+ z-index: 1000;
583
+ }
584
+ .course-detail .modal-box {
585
+ background: #fff;
586
+ border-radius: 12px;
587
+ max-width: 800px;
588
+ width: 90%;
589
+ overflow: hidden;
590
+ position: relative;
591
+ }
592
+ .course-detail .close-btn {
593
+ position: absolute;
594
+ top: 12px;
595
+ right: 12px;
596
+ background: rgba(0,0,0,0.5);
597
+ color: #fff;
598
+ border: none;
599
+ width: 32px;
600
+ height: 32px;
601
+ border-radius: 50%;
602
+ cursor: pointer;
603
+ font-size: 16px;
604
+ z-index: 10;
605
+ }
606
+ .course-detail .video {
607
+ width: 100%;
608
+ display: block;
609
+ }
610
+
611
+ /* Responsive */
612
+ @media (max-width: 900px) {
613
+ .course-detail .layout {
614
+ grid-template-columns: 1fr;
615
+ }
616
+ .course-detail .price-card-container {
617
+ position: static;
618
+ order: -1;
619
+ }
620
+ .course-detail .cover-img {
621
+ height: 200px;
622
+ }
623
+ }
624
+ @media (max-width: 480px) {
625
+ .course-detail {
626
+ padding: 12px;
627
+ }
628
+ .course-detail .header h1 {
629
+ font-size: 20px;
630
+ }
631
+ .course-detail .lesson {
632
+ flex-direction: column;
633
+ align-items: flex-start;
634
+ gap: 8px;
635
+ }
636
+ .course-detail .lesson-right {
637
+ align-self: flex-end;
638
+ }
639
+ }
640
+ `
641
+ }
642
+ ```
643
+
644
+ ## Vue Component Format
645
+
646
+ ### Basic Structure
647
+
648
+ ```js
649
+ module.exports = {
650
+ name: "MyComponent",
651
+
652
+ props: {
653
+ title: { required: true },
654
+ count: { default: 0 }
655
+ },
656
+ // Or in array format:
657
+ // props: [
658
+ // 'title',
659
+ // 'count'
660
+ // ],
661
+
662
+ template: /* html */`
663
+ <div class="my-component">
664
+ <h1>{{ title }}</h1>
665
+ <button @click="increment">Count: {{ counter }}</button>
666
+ </div>
667
+ `,
668
+
669
+ style: /* css */`
670
+ .my-component { padding: 20px; }
671
+ .my-component h1 { color: blue; }
672
+ `,
673
+
674
+ data: /* js */`
675
+ function() {
676
+ return {
677
+ counter: 0
678
+ };
679
+ }
680
+ `,
681
+
682
+ computed: /* js */`
683
+ {
684
+ counterText() {
685
+ return this.counter + " Counts";
686
+ }
687
+ }
688
+ `,
689
+
690
+ methods: /* js */`
691
+ {
692
+ increment() {
693
+ this.counter++;
694
+ }
695
+ }
696
+ `,
697
+
698
+ mounted: /* js */`
699
+ function() {
700
+ console.log('Component mounted!');
701
+ }
702
+ `
703
+ };
704
+ ```
705
+
706
+ ### Available Fields
707
+
708
+ | Field | Format | Description |
709
+ |-------|--------|-------------|
710
+ | `name` | String | Component name (required) |
711
+ | `props` | Object | Props definition (not a string) |
712
+ | `template` | Template literal | HTML template with Vue syntax |
713
+ | `style` | Template literal | CSS styles for the component |
714
+ | `data` | Template literal | Function returning initial state |
715
+ | `computed` | Template literal | Object with computed properties |
716
+ | `watch` | Template literal | Object with watchers |
717
+ | `methods` | Template literal | Object with methods |
718
+ | `mounted` | Template literal | Lifecycle hook function |
719
+ | `created`, `beforeMount`, `beforeUpdate`, `updated`, `beforeDestroy`, `destroyed` | Template literal | Other lifecycle hooks |
720
+
721
+ ### Key Rules
722
+
723
+ 1. **Use template literals** (backticks) for `template`, `style`, `data`, `methods`, etc.
724
+ 2. **Props is an object**, not a template literal
725
+ 3. **Comments are optional** but recommended: `/* html */`, `/* css */`, `/* js */`
726
+ 4. **Unused fields** can be omitted or set to `null`
727
+ 5. **Empty file marker**: `/* EMPTY FILE */` for placeholder files
728
+
729
+ ### Minimal Example
730
+
731
+ ```js
732
+ module.exports = {
733
+ name: "HelloWorld",
734
+ template: /* html */`
735
+ <div>Hello, World!</div>
736
+ `
737
+ };
738
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cyber-elx",
3
- "version": "1.1.9",
3
+ "version": "1.1.10",
4
4
  "description": "CyberOcean CLI tool to upload/download ELX custom pages",
5
5
  "main": "src/index.js",
6
6
  "bin": {