cyber-elx 1.1.11 → 1.1.12

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
@@ -25,6 +25,9 @@ Custom Student List Courses page (course catalog). Displays categories grid, fea
25
25
  ### [StudentMyCoursesPageDev.md](StudentMyCoursesPageDev.md)
26
26
  Custom Student My Courses page (student dashboard home). Shows statistics cards (total courses, completed, certificates), course list with progress bars, and certificate generation for completed courses. Documents the cardsInfos structure, courses array with progress tracking, and available translation keys.
27
27
 
28
+ ### [StudentProfileDev.md](StudentProfileDev.md)
29
+ Custom Student Profile page for viewing and editing user information. Features view/edit mode toggle, profile image upload via CLoader component, name fields, grade selection (if enabled), and password change with real-time validation. Handles API calls to `/api/users/update_my_profile` and updates VueX store. Documents the `additionalFields` object for extensible user data.
30
+
28
31
  ### [StudentStartupDev.md](StudentStartupDev.md)
29
32
  Available JavaScript variables for student-related customizations. Documents the student user object accessible via `$nuxt.$store.state.auth.user` including fields like id, name, email, phone, grade, and profile image.
30
33
 
@@ -0,0 +1,835 @@
1
+ # CyberOcean Custom Student Profile Page
2
+
3
+ ## Summary
4
+
5
+ This document describes how to customize the **Student Profile Page** using a Vue.js component. The page allows students to view and edit their profile information including name, profile image, grade (if enabled), and password. It features a view/edit mode toggle, form validation, and updates the VueX store after saving.
6
+
7
+ > **IMPORTANT:** This component handles both **UI and logic** including API calls to save the profile and VueX store updates. The `additionalFields` object is used for extensible user data like grade.
8
+
9
+ ## Overview
10
+
11
+ The Student Profile Page displays the current user's information and allows editing. The parent component provides the user object, grade options, and grade list. Your component manages the edit state, form validation, and API submission.
12
+
13
+ **Key Features:**
14
+ - **View/Edit mode toggle** - Switch between viewing and editing profile
15
+ - **Profile header** - Avatar with user image or default icon
16
+ - **Profile image upload** - CLoader component for image upload (edit mode only)
17
+ - **Email display** - Read-only email field (view mode only)
18
+ - **Name fields** - Editable first name and last name
19
+ - **Grade selection** - Dropdown for grade (only if `gradeOption` is true and user is a student)
20
+ - **Password change** - New password and confirm password with real-time validation
21
+ - **Form validation** - Validates changes and password matching before save
22
+ - **API integration** - Calls `/api/users/update_my_profile` to save changes
23
+ - **VueX store update** - Updates `auth/setUser` after successful save
24
+
25
+ ## Student Profile Page
26
+
27
+ ### Component Structure
28
+
29
+ ```
30
+ ┌─────────────────────────────────────────────────────────────┐
31
+ │ .profile-component │
32
+ │ ┌─────────────────────────────────────────────────────────┐ │
33
+ │ │ .profile-header │ │
34
+ │ │ [Avatar] "My Profile" │ │
35
+ │ └─────────────────────────────────────────────────────────┘ │
36
+ │ ┌─────────────────────────────────────────────────────────┐ │
37
+ │ │ .profile-image-uploader (edit mode only) │ │
38
+ │ │ [CLoader FilesInput] │ │
39
+ │ └─────────────────────────────────────────────────────────┘ │
40
+ │ ┌─────────────────────────────────────────────────────────┐ │
41
+ │ │ .profile-content │ │
42
+ │ │ ┌─────────────────────────────────────────────────────┐ │ │
43
+ │ │ │ .info-section │ │ │
44
+ │ │ │ Email: user@example.com (view mode only) │ │ │
45
+ │ │ │ First Name: [___________] or "John" │ │ │
46
+ │ │ │ Last Name: [___________] or "Doe" │ │ │
47
+ │ │ │ Grade: [Dropdown___] (if gradeOption) │ │ │
48
+ │ │ │ Password: [___________] (edit mode only) │ │ │
49
+ │ │ │ Confirm: [___________] (edit mode only) │ │ │
50
+ │ │ └─────────────────────────────────────────────────────┘ │ │
51
+ │ └─────────────────────────────────────────────────────────┘ │
52
+ │ ┌─────────────────────────────────────────────────────────┐ │
53
+ │ │ .button-row │ │
54
+ │ │ [Edit Profile] or [Cancel] [Save] │ │
55
+ │ └─────────────────────────────────────────────────────────┘ │
56
+ └─────────────────────────────────────────────────────────────┘
57
+ ```
58
+
59
+ ### Available Props
60
+
61
+ | Prop | Type | Description |
62
+ |------|------|-------------|
63
+ | `user` | Object | The current logged-in user object (see structure below) |
64
+ | `gradeOption` | Boolean | Whether grade selection is enabled for this website |
65
+ | `itemsTunisiaGrades` | Array | List of available grades (see structure below) |
66
+
67
+ ### The `user` Object
68
+
69
+ ```js
70
+ {
71
+ id: "user-id",
72
+ email: "user@example.com", // Read-only, displayed in view mode
73
+ first_name: "John",
74
+ last_name: "Doe",
75
+ customer_locale: "en", // User's language preference
76
+ image: { // Profile image (optional)
77
+ id: "image-id",
78
+ path: "/uploads/profile.jpg"
79
+ },
80
+ role: { // User role
81
+ key: "student" // "student" or "teacher"
82
+ },
83
+ additionalFields: { // Dynamic extensible fields
84
+ grade: "grade-9" // Grade key (if gradeOption is true)
85
+ // Can contain other custom fields in the future
86
+ }
87
+ }
88
+ ```
89
+
90
+ ### The `additionalFields` Object
91
+
92
+ This is a **dynamic object** used to store extensible user data. Currently used for:
93
+
94
+ | Field | Type | Description |
95
+ |-------|------|-------------|
96
+ | `grade` | String | Grade key (e.g., "grade-9") - only used when `gradeOption` is true |
97
+
98
+ **Why `additionalFields`?**
99
+ - Allows adding new user properties without changing the core user schema
100
+ - Grade is stored here because it's specific to educational platforms
101
+ - Future extensions (country, school, etc.) can be added here
102
+
103
+ ### The `itemsTunisiaGrades` Array
104
+
105
+ ```js
106
+ [
107
+ { key: "grade-7", title: "7th Grade" },
108
+ { key: "grade-8", title: "8th Grade" },
109
+ { key: "grade-9", title: "9th Grade" },
110
+ // ... more grades
111
+ ]
112
+ ```
113
+
114
+ ### Local State (data)
115
+
116
+ | Property | Type | Default | Description |
117
+ |----------|------|---------|-------------|
118
+ | `loading` | Boolean | `false` | Loading state during API call |
119
+ | `isEditing` | Boolean | `false` | Whether in edit mode |
120
+ | `newData` | Object | `{}` | Object holding modified field values |
121
+ | `originalData` | Object | `{}` | Original user data for reset |
122
+ | `newPassword` | String | `''` | New password input |
123
+ | `confirmPassword` | String | `''` | Confirm password input |
124
+ | `passwordError` | String | `''` | Password validation error message |
125
+
126
+ ### Computed Properties
127
+
128
+ | Property | Description |
129
+ |----------|-------------|
130
+ | `grade` | Returns grade from `newData.additionalFields.grade` or `user.additionalFields.grade` |
131
+ | `first_name` | Returns first name from `newData` or `user` |
132
+ | `last_name` | Returns last name from `newData` or `user` |
133
+ | `customer_locale` | Returns locale from `newData` or `user` |
134
+ | `isFormValid` | Returns true if form has changes and password validation passes |
135
+
136
+ ### Methods
137
+
138
+ | Method | Parameters | Description |
139
+ |--------|------------|-------------|
140
+ | `toggleEditMode()` | - | Toggles edit mode on/off, resets form when exiting |
141
+ | `cancelEdit()` | - | Resets form and exits edit mode |
142
+ | `resetForm()` | - | Clears `newData` (except password), resets password fields |
143
+ | `update(key, val)` | `key: String, val: any` | Updates a field in `newData` using `$set` |
144
+ | `updateCountry(val)` | `val: String` | Updates `additionalFields.countryKey` (for future use) |
145
+ | `updateGrade(val)` | `val: String` | Updates `additionalFields.grade` |
146
+ | `validatePassword()` | - | Validates password fields, returns true if valid |
147
+ | `saveProfile()` | - | Validates form, calls API, updates VueX store |
148
+ | `getGradeText(value)` | `value: String` | Converts grade key to display text |
149
+
150
+ ### Page Sections
151
+
152
+ 1. **Profile Header** (`.profile-header`)
153
+ - User avatar (image or default icon)
154
+ - "My Profile" title
155
+
156
+ 2. **Profile Image Uploader** (`.profile-image-uploader`) - Edit mode only
157
+ - Uses `CLoader` component with `cpn="FilesInput"`
158
+ - Props: `v-model`, `:value`, `@input`, `SingleFile`, `tag="picture"`
159
+
160
+ 3. **Info Section** (`.info-section`)
161
+ - **Email** - Display only (view mode only, read-only)
162
+ - **First Name** - Text field or display
163
+ - **Last Name** - Text field or display
164
+ - **Grade** - Select dropdown (only if `user.role.key === 'student'` AND `gradeOption === true`)
165
+ - **Password Section** - Two password fields (edit mode only)
166
+
167
+ 4. **Button Row** (`.button-row`)
168
+ - View mode: "Edit Profile" button
169
+ - Edit mode: "Cancel" and "Save" buttons (Save disabled if `!isFormValid`)
170
+
171
+ ### CLoader Component
172
+
173
+ The `CLoader` component is a platform-provided file uploader:
174
+
175
+ ```html
176
+ <CLoader
177
+ cpn="FilesInput"
178
+ v-model="user.image"
179
+ :value="user.image.id"
180
+ @input="update('image', $event)"
181
+ label="Profile Photo"
182
+ SingleFile
183
+ tag="picture"
184
+ class="file-uploader"
185
+ />
186
+ ```
187
+
188
+ | Prop | Description |
189
+ |------|-------------|
190
+ | `cpn` | Component type: `"FilesInput"` for file upload |
191
+ | `v-model` | Bound to user image object |
192
+ | `:value` | Current image ID |
193
+ | `@input` | Handler to update newData |
194
+ | `SingleFile` | Only allow one file |
195
+ | `tag` | File category: `"picture"` |
196
+
197
+ ### API Integration
198
+
199
+ **Endpoint:** `POST /api/users/update_my_profile`
200
+
201
+ **Request body:**
202
+ ```js
203
+ {
204
+ first_name: "John",
205
+ last_name: "Doe",
206
+ image: { id: "image-id", path: "/uploads/new.jpg" },
207
+ password: "newpassword123", // Only if changing password
208
+ additionalFields: {
209
+ grade: "grade-10" // Only if gradeOption is true
210
+ }
211
+ }
212
+ ```
213
+
214
+ **After successful save:**
215
+ ```js
216
+ this.$store.dispatch('auth/setUser', updatedUser);
217
+ ```
218
+
219
+ ### Password Validation
220
+
221
+ The `isFormValid` computed property performs real-time validation:
222
+
223
+ 1. If neither password field is filled → Valid (no password change)
224
+ 2. If only one field is filled → Invalid, show appropriate error
225
+ 3. If both filled but don't match → Invalid, show "Passwords do not match"
226
+ 4. If both filled and match → Valid
227
+
228
+ ### Available translations (Use what you need)
229
+
230
+ - `profile.my-profile` → "My Profile"
231
+ - `profile.first-name` → "First Name"
232
+ - `profile.last-name` → "Last Name"
233
+ - `profile.grade` → "Grade"
234
+ - `profile.change-password` → "Change Password"
235
+ - `profile.new-password` → "New Password"
236
+ - `profile.confirm-password` → "Confirm Password"
237
+ - `profile.edit-profile` → "Edit Profile"
238
+ - `profile.cancel` → "Cancel"
239
+ - `profile.save` → "Save"
240
+ - `toast.profile-updated-successfully` → "Profile updated successfully"
241
+ - `toast.error-saving-profile` → "Error saving profile"
242
+
243
+ If you want another text, just put it in English.
244
+
245
+ ### Example Student Profile Page:
246
+ ```js
247
+ module.exports = {
248
+ name: "ProfilePage",
249
+ props: [
250
+ 'itemsTunisiaGrades',
251
+ 'gradeOption',
252
+ 'user',
253
+ ],
254
+ template: /* html */`
255
+ <div class="profile-component">
256
+ <div class="profile-header">
257
+ <div class="user-avatar">
258
+ <img v-if="user.image" :src="user.image.path" alt="Profile" class="avatar-img" />
259
+ <v-icon v-else size="24" color="white">mdi-account</v-icon>
260
+ </div>
261
+ <h2 class="profile-title">{{ $t("profile.my-profile") }}</h2>
262
+ </div>
263
+ <!-- Profile Image Section (Only visible in edit mode) -->
264
+ <div v-if="isEditing" class="profile-image-uploader">
265
+ <CLoader
266
+ cpn="FilesInput"
267
+ v-model="user.image"
268
+ :value="user.image.id"
269
+ @input="update('image', $event)"
270
+ label="Profile Photo" SingleFile tag="picture" class="file-uploader"></CLoader>
271
+ </div>
272
+
273
+ <div class="profile-content">
274
+
275
+ <div class="info-section">
276
+ <!-- Email -->
277
+ <div v-if="!isEditing" class="info-field">
278
+ <label class="field-label"> E-mail</label>
279
+ <div class="field-container">
280
+ <div class="field-display" style="font-weight: bold;">{{ user.email }}</div>
281
+ </div>
282
+ </div>
283
+
284
+ <!-- First Name -->
285
+ <div class="info-field">
286
+ <label class="field-label">{{ $t("profile.first-name") }}</label>
287
+ <div class="field-container">
288
+ <div v-if="!isEditing" class="field-display">{{ first_name }}</div>
289
+ <v-text-field v-else :value="first_name" @input="update('first_name', $event)" outlined hide-details dense rounded class="profile-input-field"
290
+ />
291
+ </div>
292
+ </div>
293
+
294
+ <!-- Last Name -->
295
+ <div class="info-field">
296
+ <label class="field-label">{{ $t("profile.last-name") }}</label>
297
+ <div class="field-container">
298
+ <div v-if="!isEditing" class="field-display">{{ last_name }}</div>
299
+ <v-text-field v-else :value="last_name" @input="update('last_name', $event)" outlined hide-details dense rounded class="profile-input-field"
300
+ />
301
+ </div>
302
+ </div>
303
+
304
+ <!-- Grade - Only visible for students when gradeOption is true -->
305
+ <div class="info-field" v-if="user.role && user.role.key === 'student' && gradeOption">
306
+ <label class="field-label">{{ $t("profile.grade") }}</label>
307
+ <div class="field-container">
308
+ <div v-if="!isEditing" class="field-display">
309
+ {{ getGradeText(grade) || 'Not specified' }}
310
+ </div>
311
+ <v-select v-else :value="grade" @input="updateGrade($event)" :items="itemsTunisiaGrades"
312
+ outlined hide-details dense rounded class="profile-input-field" item-text="title" item-value="key" />
313
+ </div>
314
+ </div>
315
+
316
+ <!-- Password Section -->
317
+ <div class="info-field password-field" v-if="isEditing">
318
+
319
+
320
+ <label class="field-label">{{ $t("profile.change-password") }}</label>
321
+ <div class="password-section">
322
+ <v-text-field v-model="newPassword" type="password" outlined hide-details dense rounded class="profile-input-field"
323
+ :placeholder="$t('profile.new-password')" />
324
+ <v-text-field v-model="confirmPassword" type="password" outlined hide-details dense rounded class="profile-input-field"
325
+ :placeholder="$t('profile.confirm-password')"
326
+ :error-messages="passwordError ? [passwordError] : []" />
327
+
328
+ <div class="password-error" v-if="passwordError">
329
+ {{ passwordError }}
330
+ </div>
331
+ </div>
332
+ </div>
333
+ </div>
334
+
335
+ <div class="button-row">
336
+ <v-btn v-if="!isEditing"
337
+ color="primary"
338
+ @click="toggleEditMode"
339
+ depressed
340
+ rounded
341
+ class="action-button"
342
+ >
343
+ <v-icon left size="18">mdi-pencil</v-icon>
344
+ {{ $t("profile.edit-profile") }}
345
+ </v-btn>
346
+ <template v-else>
347
+ <v-btn
348
+ class="mr-4 action-button"
349
+ text
350
+ @click="cancelEdit"
351
+ rounded
352
+ >
353
+ {{ $t("profile.cancel") }}
354
+ </v-btn>
355
+ <v-btn
356
+ color="primary"
357
+ @click="saveProfile"
358
+ depressed
359
+ rounded
360
+ :disabled="!isFormValid"
361
+ class="action-button"
362
+ >
363
+ <v-icon left size="18">mdi-content-save</v-icon>
364
+ {{ $t("profile.save") }}
365
+ </v-btn>
366
+ </template>
367
+ </div>
368
+ </div>
369
+ </div>
370
+ `,
371
+
372
+ data: /* js */`
373
+ function() {
374
+ return {
375
+ loading: false,
376
+ isEditing: false,
377
+ newData: {},
378
+ originalData: {},
379
+ newPassword: '',
380
+ confirmPassword: '',
381
+ passwordError: '',
382
+ };
383
+ }
384
+ `,
385
+ mounted: /* js */`
386
+ function() {
387
+ this.originalData = {
388
+ first_name: this.user.first_name,
389
+ last_name: this.user.last_name,
390
+ customer_locale: this.user.customer_locale,
391
+ image: this.user.image,
392
+ additionalFields: this.user.additionalFields || {}
393
+ };
394
+ }
395
+ `,
396
+ computed: /* js */`
397
+ {
398
+ grade() {
399
+ return this.newData.additionalFields?.grade
400
+ ? this.newData.additionalFields.grade
401
+ : this.user?.additionalFields?.grade || '';
402
+ },
403
+ first_name() {
404
+ return this.newData.first_name
405
+ ? this.newData.first_name
406
+ : this.user.first_name;
407
+ },
408
+ last_name() {
409
+ return this.newData.last_name
410
+ ? this.newData.last_name
411
+ : this.user.last_name;
412
+ },
413
+ customer_locale() {
414
+ return this.newData.customer_locale
415
+ ? this.newData.customer_locale
416
+ : this.user.customer_locale;
417
+ },
418
+ isFormValid() {
419
+ // Only require changes in edit mode
420
+ if (!this.isEditing) return false;
421
+
422
+ // Check if form has changes
423
+ const hasChanges = Object.keys(this.newData).length > 0 || this.newPassword || this.confirmPassword;
424
+
425
+ // Real-time password validation
426
+ let passwordValid = true;
427
+ if (this.newPassword || this.confirmPassword) {
428
+ // If either password field is filled, both must be filled and match
429
+ passwordValid = this.newPassword && this.confirmPassword && this.newPassword === this.confirmPassword;
430
+
431
+ // Update error message for immediate feedback
432
+ if (this.newPassword && !this.confirmPassword) {
433
+ this.passwordError = 'Please confirm your password';
434
+ } else if (!this.newPassword && this.confirmPassword) {
435
+ this.passwordError = 'Please enter your new password';
436
+ } else if (this.newPassword !== this.confirmPassword) {
437
+ this.passwordError = 'Passwords do not match';
438
+ } else {
439
+ this.passwordError = '';
440
+ }
441
+ }
442
+
443
+ return hasChanges && passwordValid;
444
+ }
445
+ }
446
+ `,
447
+ methods: /* js */`
448
+ {
449
+ toggleEditMode() {
450
+ this.isEditing = !this.isEditing;
451
+ if (!this.isEditing) {
452
+ this.resetForm();
453
+ }
454
+ },
455
+ cancelEdit() {
456
+ this.resetForm();
457
+ this.isEditing = false;
458
+ },
459
+ resetForm() {
460
+ // Create a new object without the password property
461
+ const newDataWithoutPassword = { ...this.newData };
462
+ if (newDataWithoutPassword.hasOwnProperty('password')) {
463
+ delete newDataWithoutPassword.password;
464
+ }
465
+
466
+ this.newData = newDataWithoutPassword;
467
+ this.newPassword = '';
468
+ this.confirmPassword = '';
469
+ this.passwordError = '';
470
+ },
471
+ update(key, val) {
472
+ this.$set(this.newData, key, val);
473
+ },
474
+ updateCountry(val) {
475
+ if (!this.newData.additionalFields) {
476
+ this.$set(this.newData, 'additionalFields', {});
477
+ }
478
+ this.$set(this.newData.additionalFields, 'countryKey', val);
479
+ },
480
+ updateGrade(val) {
481
+ if (!this.newData.additionalFields) {
482
+ this.$set(this.newData, 'additionalFields', {});
483
+ }
484
+ this.$set(this.newData.additionalFields, 'grade', val);
485
+ },
486
+ // Modify the validatePassword method to only add password to newData if both fields are filled
487
+ validatePassword() {
488
+ // Clear previous error
489
+ this.passwordError = '';
490
+
491
+ // If both fields are empty, no password change is intended
492
+ if (!this.newPassword && !this.confirmPassword) {
493
+ // Remove password from newData if it exists
494
+ if (this.newData.hasOwnProperty('password')) {
495
+ delete this.newData.password;
496
+ }
497
+ return true;
498
+ }
499
+
500
+ // If only one field is filled
501
+ if (this.newPassword && !this.confirmPassword) {
502
+ this.passwordError = 'Please confirm your password';
503
+ return false;
504
+ }
505
+
506
+ if (!this.newPassword && this.confirmPassword) {
507
+ this.passwordError = 'Please enter your new password';
508
+ return false;
509
+ }
510
+
511
+ // Check if passwords match
512
+ if (this.newPassword !== this.confirmPassword) {
513
+ this.passwordError = 'Passwords do not match';
514
+ return false;
515
+ }
516
+
517
+ // Password is valid, update newData only if both fields are filled
518
+ if (this.newPassword && this.confirmPassword) {
519
+ this.update('password', this.newPassword);
520
+ }
521
+ return true;
522
+ },
523
+ saveProfile() {
524
+ if (!this.isFormValid) return;
525
+
526
+ // Validate password before saving
527
+ if (!this.validatePassword()) return;
528
+
529
+ // Create a copy of the data to send
530
+ const dataToSend = { ...this.newData };
531
+
532
+ // Always include first_name and last_name
533
+ dataToSend.first_name = this.first_name;
534
+ dataToSend.last_name = this.last_name;
535
+
536
+ // Only include password if both fields are filled
537
+ if (!this.newPassword && !this.confirmPassword) {
538
+ delete dataToSend.password;
539
+ }
540
+
541
+ this.loading = true;
542
+ this.$dataCaller("post", "/api/users/update_my_profile" ,dataToSend)
543
+ .then((response) => {
544
+ this.loading = false;
545
+ this.isEditing = false;
546
+ this.resetForm();
547
+ this.$toast.success(this.$t('toast.profile-updated-successfully'));
548
+
549
+ // IMPORTANT: Update the VueX Store with the new user data
550
+ const updatedUser = {
551
+ ...this.user,
552
+ ...dataToSend,
553
+ additionalFields: {
554
+ ...this.user.additionalFields,
555
+ ...(dataToSend.additionalFields || {})
556
+ }
557
+ };
558
+ this.$store.dispatch('auth/setUser', updatedUser);
559
+ })
560
+ .catch((error) => {
561
+ this.loading = false;
562
+ this.$toast.error(error?.message || this.$t('toast.error-saving-profile'));
563
+ });
564
+ },
565
+ getGradeText(value) {
566
+ const grade = this.itemsTunisiaGrades.find(item => item.key === value);
567
+ return grade ? grade.title : value;
568
+ }
569
+ }
570
+ `,
571
+ style: /* css */`
572
+ .profile-component .profile-input-field>.v-input__control>.v-input__slot>.v-text-field__slot {
573
+ padding-left: 10px !important;
574
+ }
575
+
576
+ /* Base Styles - Apply to both themes */
577
+ .profile-component {
578
+ padding: 20px;
579
+ transition: all 0.3s ease;
580
+ min-height: 100%;
581
+ max-width: 800px;
582
+ margin: 0 auto;
583
+ }
584
+
585
+ /* Profile Image Section */
586
+ .profile-component .profile-image-uploader {
587
+ max-width: 220px;
588
+ margin: 20px auto;
589
+ }
590
+
591
+ /* Profile Header with User Avatar */
592
+ .profile-component .profile-header {
593
+ display: flex;
594
+ align-items: center;
595
+ margin-bottom: 24px;
596
+ padding-bottom: 16px;
597
+ border-bottom: 1px solid rgba(0, 0, 0, 0.1);
598
+ }
599
+
600
+ .profile-component .theme--dark .profile-header {
601
+ border-bottom-color: rgba(255, 255, 255, 0.1);
602
+ }
603
+
604
+ .profile-component .user-avatar {
605
+ width: 40px;
606
+ height: 40px;
607
+ background: var(--v-primary-base);
608
+ border-radius: 50%;
609
+ display: flex;
610
+ align-items: center;
611
+ justify-content: center;
612
+ margin-right: 16px;
613
+ flex-shrink: 0;
614
+ overflow: hidden;
615
+ }
616
+
617
+ .profile-component .avatar-img {
618
+ width: 100%;
619
+ height: 100%;
620
+ object-fit: cover;
621
+ }
622
+
623
+ .profile-component .profile-title {
624
+ font-size: 20px;
625
+ font-weight: 600;
626
+ margin: 0;
627
+ }
628
+
629
+ /* Content Section */
630
+ .profile-component .profile-content {
631
+ width: 100%;
632
+ }
633
+
634
+ .profile-component .info-section {
635
+ display: flex;
636
+ flex-direction: column;
637
+ gap: 20px;
638
+ }
639
+
640
+ .profile-component .info-field {
641
+ display: flex;
642
+ flex-direction: column;
643
+ gap: 8px;
644
+ }
645
+
646
+ .profile-component .field-label {
647
+ font-size: 14px;
648
+ font-weight: 500;
649
+ color: #666;
650
+ letter-spacing: 0.3px;
651
+ }
652
+
653
+ .profile-component .field-container {
654
+ width: 100%;
655
+ }
656
+
657
+ .profile-component .field-display {
658
+ font-size: 16px;
659
+ padding: 6px 0;
660
+ border-bottom: 1px solid #eee;
661
+ }
662
+
663
+ .profile-component .profile-input-field {
664
+ margin-top: 0;
665
+ border-radius: 8px !important;
666
+ }
667
+
668
+ /* Password Section */
669
+ .profile-component .password-section {
670
+ display: flex;
671
+ flex-direction: column;
672
+ gap: 16px;
673
+ padding: 10px 0;
674
+ margin-top: 5px;
675
+ }
676
+
677
+ .profile-component .password-error {
678
+ color: #ff5252;
679
+ font-size: 14px;
680
+ margin-top: 4px;
681
+ }
682
+
683
+ /* Button Styles */
684
+ .profile-component .button-row {
685
+ display: flex;
686
+ justify-content: flex-end;
687
+ gap: 16px;
688
+ margin-top: 40px;
689
+ }
690
+
691
+ .profile-component .action-button {
692
+ text-transform: none;
693
+ font-weight: 500;
694
+ letter-spacing: 0.3px;
695
+ }
696
+
697
+
698
+
699
+ /* Responsive Styles */
700
+ @media (min-width: 768px) {
701
+ .profile-component .info-field {
702
+ flex-direction: row;
703
+ align-items: center;
704
+ }
705
+
706
+ .profile-component .field-label {
707
+ width: 150px;
708
+ text-align: right;
709
+ padding-right: 20px;
710
+ }
711
+
712
+ .profile-component .field-container {
713
+ flex: 1;
714
+ }
715
+ }
716
+
717
+ @media (max-width: 767px) {
718
+ .profile-component .profile-card {
719
+ padding: 30px 20px;
720
+ border-radius: 15px;
721
+ }
722
+
723
+ .profile-component .section-title {
724
+ font-size: 20px;
725
+ }
726
+
727
+ .profile-component .action-button {
728
+ padding: 10px 20px;
729
+ min-width: 100px;
730
+ }
731
+
732
+ .profile-component .button-row {
733
+ justify-content: center;
734
+ }
735
+ }
736
+ `
737
+ }
738
+ ```
739
+
740
+
741
+ ## Vue Component Format
742
+
743
+ ### Basic Structure
744
+
745
+ ```js
746
+ module.exports = {
747
+ name: "MyComponent",
748
+
749
+ props: {
750
+ title: { required: true },
751
+ count: { default: 0 }
752
+ },
753
+ // Or in array format:
754
+ // props: [
755
+ // 'title',
756
+ // 'count'
757
+ // ],
758
+
759
+ template: /* html */`
760
+ <div class="my-component">
761
+ <h1>{{ title }}</h1>
762
+ <button @click="increment">Count: {{ counter }}</button>
763
+ </div>
764
+ `,
765
+
766
+ style: /* css */`
767
+ .my-component { padding: 20px; }
768
+ .my-component h1 { color: blue; }
769
+ `,
770
+
771
+ data: /* js */`
772
+ function() {
773
+ return {
774
+ counter: 0
775
+ };
776
+ }
777
+ `,
778
+
779
+ computed: /* js */`
780
+ {
781
+ counterText() {
782
+ return this.counter + " Counts";
783
+ }
784
+ }
785
+ `,
786
+
787
+ methods: /* js */`
788
+ {
789
+ increment() {
790
+ this.counter++;
791
+ }
792
+ }
793
+ `,
794
+
795
+ mounted: /* js */`
796
+ function() {
797
+ console.log('Component mounted!');
798
+ }
799
+ `
800
+ };
801
+ ```
802
+
803
+ ### Available Fields
804
+
805
+ | Field | Format | Description |
806
+ |-------|--------|-------------|
807
+ | `name` | String | Component name (required) |
808
+ | `props` | Object | Props definition (not a string) |
809
+ | `template` | Template literal | HTML template with Vue syntax |
810
+ | `style` | Template literal | CSS styles for the component |
811
+ | `data` | Template literal | Function returning initial state |
812
+ | `computed` | Template literal | Object with computed properties |
813
+ | `watch` | Template literal | Object with watchers |
814
+ | `methods` | Template literal | Object with methods |
815
+ | `mounted` | Template literal | Lifecycle hook function |
816
+ | `created`, `beforeMount`, `beforeUpdate`, `updated`, `beforeDestroy`, `destroyed` | Template literal | Other lifecycle hooks |
817
+
818
+ ### Key Rules
819
+
820
+ 1. **Use template literals** (backticks) for `template`, `style`, `data`, `methods`, etc.
821
+ 2. **Props is an object**, not a template literal
822
+ 3. **Comments are optional** but recommended: `/* html */`, `/* css */`, `/* js */`
823
+ 4. **Unused fields** can be omitted or set to `null`
824
+ 5. **Empty file marker**: `/* EMPTY FILE */` for placeholder files
825
+
826
+ ### Minimal Example
827
+
828
+ ```js
829
+ module.exports = {
830
+ name: "HelloWorld",
831
+ template: /* html */`
832
+ <div>Hello, World!</div>
833
+ `
834
+ };
835
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cyber-elx",
3
- "version": "1.1.11",
3
+ "version": "1.1.12",
4
4
  "description": "CyberOcean CLI tool to upload/download ELX custom pages",
5
5
  "main": "src/index.js",
6
6
  "bin": {