cyber-elx 1.0.7 → 1.1.0
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/LoginRegisterPagesDev.md +658 -0
- package/DEV_DOC/PaymentPageDev.md +576 -0
- package/package.json +3 -2
- package/src/api.js +52 -0
- package/src/cache.js +20 -4
- package/src/files.js +155 -2
- package/src/index.js +174 -25
- package/src/vue-utils.js +149 -0
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
# CyberOcean Custom Payment Page
|
|
2
|
+
|
|
3
|
+
## Summary
|
|
4
|
+
|
|
5
|
+
- [CyberOcean Custom Payment Page](#cyberocean-custom-payment-page)
|
|
6
|
+
- [Summary](#summary)
|
|
7
|
+
- [Overview](#overview)
|
|
8
|
+
- [Payment Page](#payment-page)
|
|
9
|
+
- [Component Structure](#component-structure)
|
|
10
|
+
- [Available Props](#available-props)
|
|
11
|
+
- [course Object](#course-object)
|
|
12
|
+
- [user Object](#user-object)
|
|
13
|
+
- [Page Sections](#page-sections)
|
|
14
|
+
- [Events to Emit](#events-to-emit)
|
|
15
|
+
- [Example Payment Page:](#example-payment-page)
|
|
16
|
+
- [Vue Component Format](#vue-component-format)
|
|
17
|
+
- [Basic Structure](#basic-structure)
|
|
18
|
+
- [Available Fields](#available-fields)
|
|
19
|
+
- [Key Rules](#key-rules)
|
|
20
|
+
- [Minimal Example](#minimal-example)
|
|
21
|
+
|
|
22
|
+
## Overview
|
|
23
|
+
|
|
24
|
+
The Payment Page handles course enrollment and payment processing. It displays course information, handles user authentication state, and provides payment options.
|
|
25
|
+
IMPORTANT: You will only handle UI and styles, all logic and functionality is handled by the parent component.
|
|
26
|
+
|
|
27
|
+
**Key Features:**
|
|
28
|
+
- Course information display with price breakdown
|
|
29
|
+
- User authentication check (login/register prompts for guests)
|
|
30
|
+
- Vuetify components for dialogs (Recommended)
|
|
31
|
+
|
|
32
|
+
## Payment Page
|
|
33
|
+
|
|
34
|
+
The payment page manages the course purchase flow:
|
|
35
|
+
1. **Guest User** - Prompts to login or register
|
|
36
|
+
2. **Logged-in User (Paid Course)** - Shows payment method dialog, just emit `payClick` event
|
|
37
|
+
3. **Logged-in User (Free Course)** - Direct enrollment, just emit `payClick` event
|
|
38
|
+
|
|
39
|
+
### Component Structure
|
|
40
|
+
|
|
41
|
+
The payment component must export a module with:
|
|
42
|
+
- `name` - Component name (`"PaymentPage"`)
|
|
43
|
+
- `props` - Required props (array or object format)
|
|
44
|
+
- `template` - HTML template string
|
|
45
|
+
- `style` - CSS styles string (optional but recommended)
|
|
46
|
+
|
|
47
|
+
### Available Props
|
|
48
|
+
|
|
49
|
+
| Prop | Type | Description |
|
|
50
|
+
|------|------|-------------|
|
|
51
|
+
| `loading` | Boolean | Whether a request is in progress |
|
|
52
|
+
| `isFreeCourse` | Boolean | Whether the course is free (`course.price == 0`) |
|
|
53
|
+
| `courseId` | String | The course ID |
|
|
54
|
+
| `course` | Object | Course data object (see below) |
|
|
55
|
+
| `paymentMethods` | Array | List of available payment method names |
|
|
56
|
+
| `showPaymentMethodDialog` | Boolean | Controls payment method dialog visibility |
|
|
57
|
+
| `showSuccessDialog` | Boolean | Controls success dialog visibility |
|
|
58
|
+
| `websiteLogo` | String | Path to the website logo |
|
|
59
|
+
| `user` | Object/null | Current user object or `null` if not logged in |
|
|
60
|
+
|
|
61
|
+
### course Object
|
|
62
|
+
|
|
63
|
+
The `course` prop contains course information:
|
|
64
|
+
|
|
65
|
+
| Field | Type | Description |
|
|
66
|
+
|-------|------|-------------|
|
|
67
|
+
| `name` | String | Course name |
|
|
68
|
+
| `description` | String | Course description |
|
|
69
|
+
| `price` | Number | Course price (0 if free) |
|
|
70
|
+
| `barredPrice` | String | Original price before discount |
|
|
71
|
+
| `logo.path` | String | Path to course image |
|
|
72
|
+
|
|
73
|
+
### user Object
|
|
74
|
+
|
|
75
|
+
The `user` prop (when logged in) contains:
|
|
76
|
+
|
|
77
|
+
| Field | Type | Description |
|
|
78
|
+
|-------|------|-------------|
|
|
79
|
+
| `email` | String | User's email address |
|
|
80
|
+
| `name` | String | User's name |
|
|
81
|
+
| `image.path` | String | User's profile image |
|
|
82
|
+
|
|
83
|
+
### Page Sections
|
|
84
|
+
|
|
85
|
+
1. **Course Info Section** - Displays course image, name, price, and description
|
|
86
|
+
2. **Auth Section** - Shows login/register buttons for guests, or user email for logged-in users
|
|
87
|
+
3. **Payment Methods Display** - Visual display of available payment methods
|
|
88
|
+
4. **Purchase Info Section** - Price breakdown and pay button
|
|
89
|
+
5. **Payment Method Dialog** - Modal to choose between online payment or course request
|
|
90
|
+
6. **Success Dialog** - Confirmation after successful course request
|
|
91
|
+
|
|
92
|
+
### Events to Emit
|
|
93
|
+
|
|
94
|
+
| Event | When to Emit | Description |
|
|
95
|
+
|-------|--------------|-------------|
|
|
96
|
+
| `redirectToAuth` | Login/Register click | Redirect to auth page: `$emit('redirectToAuth', '@PVP/login')` or `'@PVP/register'` |
|
|
97
|
+
| `payClick` | Pay button click | Initiate payment flow (opens payment method dialog) |
|
|
98
|
+
| `processOnlinePayment` | Online payment selected | Process online payment (redirects to payment gateway) |
|
|
99
|
+
| `requestCourse` | Request course selected | Submit course request (admin will contact user) |
|
|
100
|
+
| `closePaymentMethodDialog` | Dialog close | Close the payment method dialog |
|
|
101
|
+
| `closeSuccessDialog` | Success dialog close | Close the success confirmation dialog |
|
|
102
|
+
|
|
103
|
+
### Example Payment Page:
|
|
104
|
+
```js
|
|
105
|
+
module.exports = {
|
|
106
|
+
name: "PaymentPage",
|
|
107
|
+
props: [
|
|
108
|
+
'loading',
|
|
109
|
+
'isFreeCourse',
|
|
110
|
+
'courseId',
|
|
111
|
+
'course',
|
|
112
|
+
'paymentMethods',
|
|
113
|
+
'showPaymentMethodDialog', // Handled by parent component
|
|
114
|
+
'showSuccessDialog', // Handled by parent component
|
|
115
|
+
'websiteLogo',
|
|
116
|
+
'user'
|
|
117
|
+
],
|
|
118
|
+
template: /* html */`
|
|
119
|
+
<div class="enroll-page-class">
|
|
120
|
+
<v-overlay :value="loading">
|
|
121
|
+
<v-progress-circular indeterminate size="64"></v-progress-circular>
|
|
122
|
+
</v-overlay>
|
|
123
|
+
<div class="d-flex" style="padding: 15px 40px 0px;">
|
|
124
|
+
<img v-if="websiteLogo" :src="websiteLogo" alt="Logo" style="height: 50px;">
|
|
125
|
+
</div>
|
|
126
|
+
<div class="enroll-page-content">
|
|
127
|
+
<div class="course-info">
|
|
128
|
+
<!-- Course Info -->
|
|
129
|
+
<div class="course-holder">
|
|
130
|
+
<img :src="course.logo.path" alt="Course Image" class="course-image" />
|
|
131
|
+
<h2 class="course-title">{{ course.name }}</h2>
|
|
132
|
+
<v-spacer></v-spacer>
|
|
133
|
+
<p class="lesson-price">{{ course.price }} DT</p>
|
|
134
|
+
</div>
|
|
135
|
+
<span style="max-height: 100px; overflow: auto; border-bottom: 1px solid gray; margin-bottom: 20px; font-size: 12px; color: #474747;">
|
|
136
|
+
<b>{{ $t('public-pages.course-payment.description') }}</b>
|
|
137
|
+
{{ course.description }}
|
|
138
|
+
</span>
|
|
139
|
+
|
|
140
|
+
<!-- User Info or Auth -->
|
|
141
|
+
<div v-if="!user">
|
|
142
|
+
<span>{{ $t('public-pages.course-payment.login-to-continue') }}</span>
|
|
143
|
+
<div>
|
|
144
|
+
<v-btn @click="$emit('redirectToAuth', '@PVP/login')" class="normal-btn" color="primary" style="margin-top: 20px; width: 100%;">{{ $t('public-pages.course-payment.login') }}</v-btn>
|
|
145
|
+
<v-btn @click="$emit('redirectToAuth', '@PVP/register')" class="normal-btn" color="primary" style="margin-top: 20px; width: 100%;">{{ $t('public-pages.course-payment.create-account') }}</v-btn>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
<div v-else style="padding: 2px 7px; border: 2px solid var(--v-primary-base); border-radius: 7px; background-color: #ebf3ff; text-align: center; font-size: 17px;">
|
|
149
|
+
<p style="margin: 0px;">{{ $t('public-pages.course-payment.logged-in-as') }} <b style="color: var(--v-primary-base);">{{ user.email }}</b></p>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<!-- UI Decoration (Payment Methods) -->
|
|
153
|
+
<v-card style="margin-top: 15px; border-radius: 10px; box-shadow: 0px 0px 20px #00000012 !important; padding-bottom: 10px;" elevation="0">
|
|
154
|
+
<v-card-title style="font-size: 16px;">{{ $t('public-pages.course-payment.available-payment-methods') }}</v-card-title>
|
|
155
|
+
<v-card-text>
|
|
156
|
+
<v-row style="justify-content: center;">
|
|
157
|
+
<v-col
|
|
158
|
+
v-for="(method, index) in paymentMethods"
|
|
159
|
+
:key="index"
|
|
160
|
+
cols="2"
|
|
161
|
+
class="text-center"
|
|
162
|
+
style="min-width: 140px;"
|
|
163
|
+
>
|
|
164
|
+
<img
|
|
165
|
+
:src="'@PS/images/icons/bank-' + (index + 1) + '.png'"
|
|
166
|
+
style="width: 70px; height: 70px; object-fit: contain; border: 1px solid gray; padding: 3px; border-radius: 7px;"
|
|
167
|
+
/>
|
|
168
|
+
<div style="font-weight: 500; color: black;">{{ method }}</div>
|
|
169
|
+
</v-col>
|
|
170
|
+
</v-row>
|
|
171
|
+
</v-card-text>
|
|
172
|
+
</v-card>
|
|
173
|
+
|
|
174
|
+
</div>
|
|
175
|
+
<!-- Order Info and Price break down -->
|
|
176
|
+
<div class="purchase-info">
|
|
177
|
+
<p class="item-price"><span>{{ $t('public-pages.course-payment.course-price') }}</span> <span>{{ course.price.toFixed(3) }} DT</span></p>
|
|
178
|
+
<p v-if="isFreeCourse" class="item-price"><span>{{ $t('public-pages.course-payment.original-price') }}</span> <span>{{ parseFloat(course.barredPrice || "100").toFixed(3) }} DT</span></p>
|
|
179
|
+
<p class="total-price"><span>{{ $t('public-pages.course-payment.total') }} </span><span>{{ course.price.toFixed(3) }} DT</span></p>
|
|
180
|
+
<button
|
|
181
|
+
class="payment-button"
|
|
182
|
+
:style="!user ? 'filter: grayscale(1); opacity: 0.8;' : ''"
|
|
183
|
+
@click="$emit('payClick')"
|
|
184
|
+
>
|
|
185
|
+
<span v-if="isFreeCourse">
|
|
186
|
+
{{ $t('public-pages.course-payment.get-course-free') }}
|
|
187
|
+
</span>
|
|
188
|
+
<span v-else>
|
|
189
|
+
{{ $t('public-pages.course-payment.pay') }} {{course.price.toFixed(3)}} DT
|
|
190
|
+
</span>
|
|
191
|
+
</button>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
<!-- Payment Dialog -->
|
|
195
|
+
<v-dialog
|
|
196
|
+
:value="showPaymentMethodDialog"
|
|
197
|
+
@input="$event ? null : $emit('closePaymentMethodDialog')"
|
|
198
|
+
max-width="400px"
|
|
199
|
+
transition="dialog-bottom-transition"
|
|
200
|
+
>
|
|
201
|
+
<v-card class="rounded-lg">
|
|
202
|
+
<v-card-title class="text-center primary white--text">
|
|
203
|
+
<v-icon left color="white">mdi-credit-card-outline</v-icon>
|
|
204
|
+
{{ $t('public-pages.course-payment.choose-payment-method') }}
|
|
205
|
+
</v-card-title>
|
|
206
|
+
|
|
207
|
+
<v-card-text class="pt-6">
|
|
208
|
+
<v-row justify="center" class="mb-4">
|
|
209
|
+
<v-col cols="12" sm="10">
|
|
210
|
+
<v-btn
|
|
211
|
+
block
|
|
212
|
+
color="primary"
|
|
213
|
+
elevation="2"
|
|
214
|
+
class="mb-4 py-6"
|
|
215
|
+
@click="$emit('processOnlinePayment')"
|
|
216
|
+
>
|
|
217
|
+
<v-icon left>mdi-bank</v-icon>
|
|
218
|
+
{{ $t('public-pages.course-payment.online-payment') }}
|
|
219
|
+
<v-icon right>mdi-arrow-right</v-icon>
|
|
220
|
+
</v-btn>
|
|
221
|
+
|
|
222
|
+
<v-btn
|
|
223
|
+
block
|
|
224
|
+
color="secondary"
|
|
225
|
+
elevation="2"
|
|
226
|
+
class="py-6"
|
|
227
|
+
@click="$emit('requestCourse')"
|
|
228
|
+
>
|
|
229
|
+
<v-icon left>mdi-email-outline</v-icon>
|
|
230
|
+
{{ $t('public-pages.course-payment.request-course') }}
|
|
231
|
+
</v-btn>
|
|
232
|
+
</v-col>
|
|
233
|
+
</v-row>
|
|
234
|
+
</v-card-text>
|
|
235
|
+
|
|
236
|
+
<v-divider></v-divider>
|
|
237
|
+
|
|
238
|
+
<v-card-actions>
|
|
239
|
+
<v-spacer></v-spacer>
|
|
240
|
+
<v-btn
|
|
241
|
+
text
|
|
242
|
+
color="grey darken-1"
|
|
243
|
+
@click="$emit('closePaymentMethodDialog')"
|
|
244
|
+
>
|
|
245
|
+
<v-icon left>mdi-close</v-icon>
|
|
246
|
+
{{ $t('public-pages.course-payment.cancel') }}
|
|
247
|
+
</v-btn>
|
|
248
|
+
<v-spacer></v-spacer>
|
|
249
|
+
</v-card-actions>
|
|
250
|
+
</v-card>
|
|
251
|
+
</v-dialog>
|
|
252
|
+
|
|
253
|
+
<!-- Success Dialog -->
|
|
254
|
+
<v-dialog :value="showSuccessDialog" @input="$event ? null : $emit('closeSuccessDialog')" persistent max-width="400">
|
|
255
|
+
<v-card>
|
|
256
|
+
<v-card-title class="headline">{{ $t('public-pages.course-payment.request-sent') }}</v-card-title>
|
|
257
|
+
<v-card-text>
|
|
258
|
+
{{ $t('public-pages.course-payment.request-sent-message') }}
|
|
259
|
+
</v-card-text>
|
|
260
|
+
</v-card>
|
|
261
|
+
</v-dialog>
|
|
262
|
+
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
`,
|
|
266
|
+
style: /* css */`
|
|
267
|
+
.enroll-page-class {
|
|
268
|
+
overflow: auto;
|
|
269
|
+
max-height: calc(100vh - 7px);
|
|
270
|
+
max-width: 1100px;
|
|
271
|
+
margin: auto;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.enroll-page-content {
|
|
275
|
+
display: flex;
|
|
276
|
+
flex-direction: column;
|
|
277
|
+
justify-content: space-between;
|
|
278
|
+
gap: 20px;
|
|
279
|
+
padding: 20px;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.purchase-info {
|
|
283
|
+
justify-content: flex-start !important;
|
|
284
|
+
gap: 8px;
|
|
285
|
+
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.course-info,
|
|
289
|
+
.purchase-info {
|
|
290
|
+
background-color: white;
|
|
291
|
+
border-radius: 10px;
|
|
292
|
+
padding: 20px;
|
|
293
|
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); /* Adds shadow for a lifted look */
|
|
294
|
+
width: 100%;
|
|
295
|
+
display: flex;
|
|
296
|
+
flex-direction: column;
|
|
297
|
+
justify-content: space-between;
|
|
298
|
+
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.course-info {
|
|
302
|
+
flex-grow: 1;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.purchase-info {
|
|
306
|
+
flex-grow: 1;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.purchase-info p {
|
|
310
|
+
display: flex;
|
|
311
|
+
flex-direction: row;
|
|
312
|
+
justify-content: space-between;
|
|
313
|
+
}
|
|
314
|
+
.purchase-info p input {
|
|
315
|
+
width: 15%;
|
|
316
|
+
}
|
|
317
|
+
.course-holder {
|
|
318
|
+
display: flex;
|
|
319
|
+
flex-direction: row;
|
|
320
|
+
align-items: center;
|
|
321
|
+
gap: 10px;
|
|
322
|
+
width: 100%;
|
|
323
|
+
margin-bottom: 20px;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
@media (max-width: 768px) {
|
|
327
|
+
.course-holder {
|
|
328
|
+
flex-direction: column;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.course-image {
|
|
333
|
+
object-fit: contain;
|
|
334
|
+
width: 60px;
|
|
335
|
+
border: 1px solid black;
|
|
336
|
+
padding: 5px;
|
|
337
|
+
height: auto;
|
|
338
|
+
border-radius: 6px;
|
|
339
|
+
margin-bottom: 0px;
|
|
340
|
+
max-height: 110px;
|
|
341
|
+
}
|
|
342
|
+
@media (max-width: 768px) {
|
|
343
|
+
.course-image {
|
|
344
|
+
width: 80%;
|
|
345
|
+
margin: auto;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.course-title {
|
|
350
|
+
width: 50%;
|
|
351
|
+
max-width: 50%;
|
|
352
|
+
font-size: 1em;
|
|
353
|
+
text-wrap:wrap ;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.lesson-price {
|
|
357
|
+
width: fit-content;
|
|
358
|
+
font-size: 1.2em;
|
|
359
|
+
color: #333;
|
|
360
|
+
margin-bottom: 0px !important;
|
|
361
|
+
font-weight: 300;
|
|
362
|
+
font-size: 22px;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.payment-methods {
|
|
366
|
+
display: flex;
|
|
367
|
+
justify-content: space-between;
|
|
368
|
+
gap: 10px;
|
|
369
|
+
margin-top: 16px;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
.payment-option {
|
|
373
|
+
padding: 10px 20px;
|
|
374
|
+
background-color: #255ca838;
|
|
375
|
+
color: white;
|
|
376
|
+
border: none;
|
|
377
|
+
border-radius: 4px;
|
|
378
|
+
cursor: pointer;
|
|
379
|
+
color: #255ca8;
|
|
380
|
+
transition: background-color 0.3s;
|
|
381
|
+
width: 45%;
|
|
382
|
+
font-weight: 600;
|
|
383
|
+
border: 2px solid #255ca8;
|
|
384
|
+
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.payment-option:hover {
|
|
388
|
+
background-color: #255ca857;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
.item-price,
|
|
392
|
+
.quantity,
|
|
393
|
+
.total-price {
|
|
394
|
+
font-size: 1em;
|
|
395
|
+
margin: 8px 0;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
.payment-button {
|
|
399
|
+
width: 100%;
|
|
400
|
+
padding: 12px;
|
|
401
|
+
background-color: #255ca8;
|
|
402
|
+
color: white;
|
|
403
|
+
border: none;
|
|
404
|
+
border-radius: 6px;
|
|
405
|
+
cursor: pointer;
|
|
406
|
+
font-weight: bold;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
.payment-button:hover {
|
|
410
|
+
background-color: #0056b3;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/* Tablet and Desktop Styling */
|
|
414
|
+
@media (min-width: 768px) {
|
|
415
|
+
.enroll-page-content {
|
|
416
|
+
flex-direction: row;
|
|
417
|
+
gap: 20px;
|
|
418
|
+
padding: 40px;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
.course-info {
|
|
422
|
+
width: 60%;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
.purchase-info {
|
|
426
|
+
width: 40%;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
.payment-method-modal {
|
|
430
|
+
position: fixed;
|
|
431
|
+
top: 0;
|
|
432
|
+
left: 0;
|
|
433
|
+
width: 100%;
|
|
434
|
+
height: 100%;
|
|
435
|
+
background-color: rgba(0, 0, 0, 0.5);
|
|
436
|
+
display: flex;
|
|
437
|
+
justify-content: center;
|
|
438
|
+
align-items: center;
|
|
439
|
+
z-index: 1000;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
.payment-method-container {
|
|
443
|
+
background-color: white;
|
|
444
|
+
border-radius: 8px;
|
|
445
|
+
padding: 20px;
|
|
446
|
+
width: 90%;
|
|
447
|
+
max-width: 400px;
|
|
448
|
+
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
.payment-method-container h3 {
|
|
452
|
+
margin-top: 0;
|
|
453
|
+
text-align: center;
|
|
454
|
+
margin-bottom: 20px;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
.payment-options {
|
|
458
|
+
display: flex;
|
|
459
|
+
flex-direction: column;
|
|
460
|
+
gap: 12px;
|
|
461
|
+
margin-bottom: 20px;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
.payment-option {
|
|
465
|
+
padding: 12px;
|
|
466
|
+
border: 1px solid #ddd;
|
|
467
|
+
border-radius: 4px;
|
|
468
|
+
background-color: #f8f8f8;
|
|
469
|
+
cursor: pointer;
|
|
470
|
+
font-weight: 500;
|
|
471
|
+
transition: all 0.2s ease;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
.payment-option:hover {
|
|
475
|
+
background-color: #eaeaea;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
.close-button {
|
|
479
|
+
width: 100%;
|
|
480
|
+
padding: 10px;
|
|
481
|
+
background-color: #f0f0f0;
|
|
482
|
+
border: 1px solid #ddd;
|
|
483
|
+
border-radius: 4px;
|
|
484
|
+
cursor: pointer;
|
|
485
|
+
}
|
|
486
|
+
`
|
|
487
|
+
};
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
## Vue Component Format
|
|
491
|
+
|
|
492
|
+
### Basic Structure
|
|
493
|
+
|
|
494
|
+
```js
|
|
495
|
+
module.exports = {
|
|
496
|
+
name: "MyComponent",
|
|
497
|
+
|
|
498
|
+
props: {
|
|
499
|
+
title: { required: true },
|
|
500
|
+
count: { default: 0 }
|
|
501
|
+
},
|
|
502
|
+
// Or in array format:
|
|
503
|
+
// props: [
|
|
504
|
+
// 'title',
|
|
505
|
+
// 'count'
|
|
506
|
+
// ],
|
|
507
|
+
|
|
508
|
+
template: /* html */`
|
|
509
|
+
<div class="my-component">
|
|
510
|
+
<h1>{{ title }}</h1>
|
|
511
|
+
<button @click="increment">Count: {{ counter }}</button>
|
|
512
|
+
</div>
|
|
513
|
+
`,
|
|
514
|
+
|
|
515
|
+
style: /* css */`
|
|
516
|
+
.my-component { padding: 20px; }
|
|
517
|
+
.my-component h1 { color: blue; }
|
|
518
|
+
`,
|
|
519
|
+
|
|
520
|
+
data: /* js */`
|
|
521
|
+
function() {
|
|
522
|
+
return {
|
|
523
|
+
counter: 0
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
`,
|
|
527
|
+
|
|
528
|
+
methods: /* js */`
|
|
529
|
+
{
|
|
530
|
+
increment() {
|
|
531
|
+
this.counter++;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
`,
|
|
535
|
+
|
|
536
|
+
mounted: /* js */`
|
|
537
|
+
function() {
|
|
538
|
+
console.log('Component mounted!');
|
|
539
|
+
}
|
|
540
|
+
`
|
|
541
|
+
};
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
### Available Fields
|
|
545
|
+
|
|
546
|
+
| Field | Format | Description |
|
|
547
|
+
|-------|--------|-------------|
|
|
548
|
+
| `name` | String | Component name (required) |
|
|
549
|
+
| `props` | Object | Props definition (not a string) |
|
|
550
|
+
| `template` | Template literal | HTML template with Vue syntax |
|
|
551
|
+
| `style` | Template literal | CSS styles for the component |
|
|
552
|
+
| `data` | Template literal | Function returning initial state |
|
|
553
|
+
| `computed` | Template literal | Object with computed properties |
|
|
554
|
+
| `watch` | Template literal | Object with watchers |
|
|
555
|
+
| `methods` | Template literal | Object with methods |
|
|
556
|
+
| `mounted` | Template literal | Lifecycle hook function |
|
|
557
|
+
| `created`, `beforeMount`, `beforeUpdate`, `updated`, `beforeDestroy`, `destroyed` | Template literal | Other lifecycle hooks |
|
|
558
|
+
|
|
559
|
+
### Key Rules
|
|
560
|
+
|
|
561
|
+
1. **Use template literals** (backticks) for `template`, `style`, `data`, `methods`, etc.
|
|
562
|
+
2. **Props is an object**, not a template literal
|
|
563
|
+
3. **Comments are optional** but recommended: `/* html */`, `/* css */`, `/* js */`
|
|
564
|
+
4. **Unused fields** can be omitted or set to `null`
|
|
565
|
+
5. **Empty file marker**: `/* EMPTY FILE */` for placeholder files
|
|
566
|
+
|
|
567
|
+
### Minimal Example
|
|
568
|
+
|
|
569
|
+
```js
|
|
570
|
+
module.exports = {
|
|
571
|
+
name: "HelloWorld",
|
|
572
|
+
template: /* html */`
|
|
573
|
+
<div>Hello, World!</div>
|
|
574
|
+
`
|
|
575
|
+
};
|
|
576
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cyber-elx",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "CyberOcean CLI tool to upload/download ELX custom pages",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"chalk": "^4.1.2",
|
|
27
27
|
"commander": "^11.1.0",
|
|
28
28
|
"inquirer": "^8.2.6",
|
|
29
|
-
"jsonc-parser": "^3.2.0"
|
|
29
|
+
"jsonc-parser": "^3.2.0",
|
|
30
|
+
"vue-template-compiler": "^2.7.14"
|
|
30
31
|
}
|
|
31
32
|
}
|
package/src/api.js
CHANGED
|
@@ -28,6 +28,58 @@ function createApiClient(config) {
|
|
|
28
28
|
async getDefaultPages() {
|
|
29
29
|
const response = await client.get('/api/plugin_api/el-x/get_defaults_for_elx_pages');
|
|
30
30
|
return response.data;
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
// SPA General Pages
|
|
34
|
+
async getGeneralPages() {
|
|
35
|
+
const response = await client.get('/api/plugin_api/el-x/general_pages_elx_spa');
|
|
36
|
+
if(response.data?.debug) {
|
|
37
|
+
console.log(response.data.debug);
|
|
38
|
+
}
|
|
39
|
+
console.log(response.data);
|
|
40
|
+
return response.data;
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
async setGeneralPages(items) {
|
|
44
|
+
const response = await client.post('/api/plugin_api/el-x/general_pages_elx_spa', { items });
|
|
45
|
+
if(response.data?.debug) {
|
|
46
|
+
console.log(response.data.debug);
|
|
47
|
+
}
|
|
48
|
+
return response.data;
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
// SPA Teacher Dashboard
|
|
52
|
+
async getTeacherDashboard() {
|
|
53
|
+
const response = await client.get('/api/plugin_api/el-x/teacher_dashboard_elx_spa');
|
|
54
|
+
if(response.data?.debug) {
|
|
55
|
+
console.log(response.data.debug);
|
|
56
|
+
}
|
|
57
|
+
return response.data;
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
async setTeacherDashboard(items) {
|
|
61
|
+
const response = await client.post('/api/plugin_api/el-x/teacher_dashboard_elx_spa', { items });
|
|
62
|
+
if(response.data?.debug) {
|
|
63
|
+
console.log(response.data.debug);
|
|
64
|
+
}
|
|
65
|
+
return response.data;
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
// SPA Student Dashboard
|
|
69
|
+
async getStudentDashboard() {
|
|
70
|
+
const response = await client.get('/api/plugin_api/el-x/student_dashboard_elx_spa');
|
|
71
|
+
if(response.data?.debug) {
|
|
72
|
+
console.log(response.data.debug);
|
|
73
|
+
}
|
|
74
|
+
return response.data;
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
async setStudentDashboard(items) {
|
|
78
|
+
const response = await client.post('/api/plugin_api/el-x/student_dashboard_elx_spa', { items });
|
|
79
|
+
if(response.data?.debug) {
|
|
80
|
+
console.log(response.data.debug);
|
|
81
|
+
}
|
|
82
|
+
return response.data;
|
|
31
83
|
}
|
|
32
84
|
};
|
|
33
85
|
}
|
package/src/cache.js
CHANGED
|
@@ -10,13 +10,15 @@ function getCachePath(cwd = process.cwd()) {
|
|
|
10
10
|
function readCache(cwd = process.cwd()) {
|
|
11
11
|
const cachePath = getCachePath(cwd);
|
|
12
12
|
if (!fs.existsSync(cachePath)) {
|
|
13
|
-
return { pages: {} };
|
|
13
|
+
return { pages: {}, spa: {} };
|
|
14
14
|
}
|
|
15
15
|
try {
|
|
16
16
|
const content = fs.readFileSync(cachePath, 'utf-8');
|
|
17
|
-
|
|
17
|
+
const cache = JSON.parse(content);
|
|
18
|
+
if (!cache.spa) cache.spa = {};
|
|
19
|
+
return cache;
|
|
18
20
|
} catch (e) {
|
|
19
|
-
return { pages: {} };
|
|
21
|
+
return { pages: {}, spa: {} };
|
|
20
22
|
}
|
|
21
23
|
}
|
|
22
24
|
|
|
@@ -42,6 +44,18 @@ function setPageTimestamp(cache, type, key, updated_at) {
|
|
|
42
44
|
cache.pages[cacheKey] = { updated_at };
|
|
43
45
|
}
|
|
44
46
|
|
|
47
|
+
// SPA cache functions (per-folder caching)
|
|
48
|
+
function getSpaTimestamp(cache, spaKey) {
|
|
49
|
+
return cache.spa[spaKey]?.updated_at || null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function setSpaTimestamp(cache, spaKey, updated_at) {
|
|
53
|
+
if (!cache.spa) {
|
|
54
|
+
cache.spa = {};
|
|
55
|
+
}
|
|
56
|
+
cache.spa[spaKey] = { updated_at };
|
|
57
|
+
}
|
|
58
|
+
|
|
45
59
|
module.exports = {
|
|
46
60
|
CACHE_FILE,
|
|
47
61
|
getCachePath,
|
|
@@ -49,5 +63,7 @@ module.exports = {
|
|
|
49
63
|
writeCache,
|
|
50
64
|
getPageCacheKey,
|
|
51
65
|
getPageTimestamp,
|
|
52
|
-
setPageTimestamp
|
|
66
|
+
setPageTimestamp,
|
|
67
|
+
getSpaTimestamp,
|
|
68
|
+
setSpaTimestamp
|
|
53
69
|
};
|