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
|
+
```
|