cyber-elx 1.1.12 → 1.2.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.
@@ -7,12 +7,14 @@
7
7
  - [Overview](#overview)
8
8
  - [Login Page](#login-page)
9
9
  - [Component Structure](#component-structure)
10
+ - [Project Colors](#project-colors)
10
11
  - [Available Props](#available-props)
11
12
  - [Login Flow Steps](#login-flow-steps)
12
13
  - [Events to Emit](#events-to-emit)
13
14
  - [Example Login Page:](#example-login-page)
14
15
  - [Register Page](#register-page)
15
16
  - [Component Structure](#component-structure-1)
17
+ - [Project Colors](#project-colors-1)
16
18
  - [Available Props](#available-props-1)
17
19
  - [inputsData Object](#inputsdata-object)
18
20
  - [Register Flow States](#register-flow-states)
@@ -49,6 +51,11 @@ The login component must export a module with:
49
51
  - `template` - HTML template string
50
52
  - `style` - CSS styles string (optional but recommended)
51
53
 
54
+ ### Project Colors
55
+
56
+ - Primary: `--elx-primary-color`
57
+ - Secondary: `--elx-secondary-color`
58
+
52
59
  ### Available Props
53
60
 
54
61
  | Prop | Type | Description |
@@ -287,6 +294,11 @@ The register component must export a module with:
287
294
  - `template` - HTML template string
288
295
  - `style` - CSS styles string (optional but recommended)
289
296
 
297
+ ### Project Colors
298
+
299
+ - Primary: `--elx-primary-color`
300
+ - Secondary: `--elx-secondary-color`
301
+
290
302
  ### Available Props
291
303
 
292
304
  | Prop | Type | Description |
@@ -7,6 +7,7 @@
7
7
  - [Overview](#overview)
8
8
  - [Payment Page](#payment-page)
9
9
  - [Component Structure](#component-structure)
10
+ - [Project Colors](#project-colors)
10
11
  - [Available Props](#available-props)
11
12
  - [course Object](#course-object)
12
13
  - [user Object](#user-object)
@@ -44,6 +45,10 @@ The payment component must export a module with:
44
45
  - `template` - HTML template string
45
46
  - `style` - CSS styles string (optional but recommended)
46
47
 
48
+ ### Project Colors
49
+
50
+ - Primary: `--elx-primary-color`
51
+ - Secondary: `--elx-secondary-color`
47
52
  ### Available Props
48
53
 
49
54
  | Prop | Type | Description |
package/DEV_DOC/README.md CHANGED
@@ -28,6 +28,9 @@ Custom Student My Courses page (student dashboard home). Shows statistics cards
28
28
  ### [StudentProfileDev.md](StudentProfileDev.md)
29
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
30
 
31
+ ### [StudentSessionsPageDev.md](StudentSessionsPageDev.md)
32
+ Custom Student Sessions page (live sessions calendar). Self-contained component with no props — fetches sessions and teachers via API. Displays monthly calendar with color-coded session indicators, navigation arrows, and month dropdown. Session detail dialog shows teacher, time, duration, attached files, and join button with status logic (Join Now, Upcoming, Not Started, Ended).
33
+
31
34
  ### [StudentStartupDev.md](StudentStartupDev.md)
32
35
  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.
33
36
 
@@ -21,6 +21,11 @@ The Student Course Detail Page is rendered when a student views a specific cours
21
21
 
22
22
  ## Student Course Detail Page
23
23
 
24
+ ### Project Colors
25
+
26
+ - Primary: `--elx-primary-color`
27
+ - Secondary: `--elx-secondary-color`
28
+
24
29
  ### Available Props
25
30
 
26
31
  | Prop | Type | Description |
@@ -29,7 +34,6 @@ The Student Course Detail Page is rendered when a student views a specific cours
29
34
  | `courseId` | String | The unique identifier of the course |
30
35
  | `onlineMode` | Boolean | Whether online payment is enabled |
31
36
  | `offlineMode` | Boolean | Whether offline/request mode is enabled |
32
- | `primaryColor` | String | The website's primary color (hex value) |
33
37
 
34
38
  ### The `course` Object
35
39
 
@@ -50,6 +50,11 @@ The Student Course Player is rendered when a student opens a course they are enr
50
50
  └─────────────────────────────────────────────────────────────────┘
51
51
  ```
52
52
 
53
+ ### Project Colors
54
+
55
+ - Primary: `--elx-primary-color`
56
+ - Secondary: `--elx-secondary-color`
57
+
53
58
  ### Available Props
54
59
 
55
60
  | Prop | Type | Description |
@@ -7,6 +7,7 @@
7
7
  - [Overview](#overview)
8
8
  - [Student List Courses Page](#student-list-courses-page)
9
9
  - [Component Structure](#component-structure)
10
+ - [Project Colors](#project-colors)
10
11
  - [Available Props](#available-props)
11
12
  - [allCategories Array](#allcategories-array)
12
13
  - [coursesList / promotedCourses Arrays](#courseslist--promotedcourses-arrays)
@@ -59,6 +60,11 @@ The component must export a module with:
59
60
  - `mounted` - Initialize slider width and resize listener
60
61
  - `beforeDestroy` - Clean up resize listener
61
62
 
63
+ ### Project Colors
64
+
65
+ - Primary: `--elx-primary-color`
66
+ - Secondary: `--elx-secondary-color`
67
+
62
68
  ### Available Props
63
69
 
64
70
  | Prop | Type | Description |
@@ -7,6 +7,7 @@
7
7
  - [Overview](#overview)
8
8
  - [Student My Courses Page](#student-my-courses-page)
9
9
  - [Component Structure](#component-structure)
10
+ - [Project Colors](#project-colors)
10
11
  - [Available Props](#available-props)
11
12
  - [cardsInfos Array](#cardsinfos-array)
12
13
  - [courses Array](#courses-array)
@@ -50,6 +51,11 @@ The component must export a module with:
50
51
  - `data` - Local state (search field)
51
52
  - `computed` - Computed properties (filteredCourses, headers)
52
53
 
54
+ ### Project Colors
55
+
56
+ - Primary: `--elx-primary-color`
57
+ - Secondary: `--elx-secondary-color`
58
+
53
59
  ### Available Props
54
60
 
55
61
  | Prop | Type | Description |
@@ -56,6 +56,11 @@ The Student Profile Page displays the current user's information and allows edit
56
56
  └─────────────────────────────────────────────────────────────┘
57
57
  ```
58
58
 
59
+ ### Project Colors
60
+
61
+ - Primary: `--elx-primary-color`
62
+ - Secondary: `--elx-secondary-color`
63
+
59
64
  ### Available Props
60
65
 
61
66
  | Prop | Type | Description |
@@ -239,6 +244,20 @@ The `isFormValid` computed property performs real-time validation:
239
244
  - `profile.save` → "Save"
240
245
  - `toast.profile-updated-successfully` → "Profile updated successfully"
241
246
  - `toast.error-saving-profile` → "Error saving profile"
247
+ - `profile.student` → "Student"
248
+ - `profile.courses` → "Courses"
249
+ - `profile.personal-info` → "Personal Info"
250
+ - `profile.enter-first-name` → "Enter first name"
251
+ - `profile.enter-last-name` → "Enter last name"
252
+ - `profile.not-specified` → "Not specified"
253
+ - `profile.select-grade` → "Select grade"
254
+ - `profile.enter-new-password` → "Enter new password"
255
+ - `profile.confirm-password-placeholder` → "Confirm password"
256
+ - `profile.profile-updated` → "Profile Updated!"
257
+ - `profile.changes-saved` → "Your changes have been saved"
258
+ - `profile.error-confirm-password` → "Please confirm your password"
259
+ - `profile.error-enter-password` → "Please enter your new password"
260
+ - `profile.error-passwords-not-match` → "Passwords do not match"
242
261
 
243
262
  If you want another text, just put it in English.
244
263
 
@@ -0,0 +1,939 @@
1
+ # CyberOcean Custom Student Sessions Page
2
+
3
+ ## Summary
4
+
5
+ This document describes how to customize the **Student Sessions Page** (live sessions calendar). The component has no props and handles all data fetching internally via API calls. The calendar displays live/scheduled sessions organized by month with color-coded indicators, session detail dialogs with join buttons, and attached file viewing.
6
+
7
+ ## Overview
8
+
9
+ **Key Features:**
10
+ - **Monthly calendar view** with navigation arrows and month dropdown selector
11
+ - **Session indicators** on calendar days with color-coded dots and truncated names
12
+ - **Session detail dialog** showing teacher, time, duration, files, and join button
13
+ - **Session status logic** - Join Now (today), Upcoming (future), Not Started (no link), Ended (past)
14
+ - **Files dialog** for viewing and opening attached session files
15
+ - **i18n support** for day names and month names based on locale
16
+ - **API integration** - fetches sessions and teachers data on mount and month change
17
+
18
+ ## Student Sessions Page
19
+
20
+ ### Component Structure
21
+
22
+ ```
23
+ ┌─────────────────────────────────────────────────────────────────┐
24
+ │ Page Title: [icon] Live Sessions │
25
+ ├─────────────────────────────────────────────────────────────────┤
26
+ │ ┌───────────────────────────────────────────────────────────┐ │
27
+ │ │ Calendar Card │ │
28
+ │ │ ┌─────────────────────────────────────────────────────┐ │ │
29
+ │ │ │ [<] February 2025 [>] (navigation header) │ │ │
30
+ │ │ ├─────────────────────────────────────────────────────┤ │ │
31
+ │ │ │ [Select Month ▼] (month dropdown) │ │ │
32
+ │ │ ├─────────────────────────────────────────────────────┤ │ │
33
+ │ │ │ Sun | Mon | Tue | Wed | Thu | Fri | Sat │ │ │
34
+ │ │ ├─────────────────────────────────────────────────────┤ │ │
35
+ │ │ │ | | | | | | 1 | │ │ │
36
+ │ │ │ 2 | 3 | 4 | 5 | 6 | 7 | 8 | │ │ │
37
+ │ │ │ | | ● Session Name | | │ │ │
38
+ │ │ │ 9 | 10 | 11 | 12 | 13 | 14 | 15 | │ │ │
39
+ │ │ │ | | | ● Session 1 | | │ │ │
40
+ │ │ │ | | | +2 more | | │ │ │
41
+ │ │ │ ... │ │ │
42
+ │ │ └─────────────────────────────────────────────────────┘ │ │
43
+ │ └───────────────────────────────────────────────────────────┘ │
44
+ └─────────────────────────────────────────────────────────────────┘
45
+
46
+ Session Detail Dialog:
47
+ ┌─────────────────────────────────────────────────────────────────┐
48
+ │ Sessions for February 10, 2025 [X] │
49
+ ├─────────────────────────────────────────────────────────────────┤
50
+ │ ┌───────────────────────────────────────────────────────────┐ │
51
+ │ │ │ Session Name │ │
52
+ │ │ │ Teacher: John Doe │ │
53
+ │ │ │ Starts at: 10:00 AM │ │
54
+ │ │ │ Duration: 2 hours [Join Now] │ │
55
+ │ │ │ [View Attached Files (3)] │ │
56
+ │ └───────────────────────────────────────────────────────────┘ │
57
+ └─────────────────────────────────────────────────────────────────┘
58
+ ```
59
+
60
+ ### Project Colors
61
+
62
+ - Primary: `--elx-primary-color`
63
+ - Secondary: `--elx-secondary-color`
64
+
65
+ ### Available Props
66
+
67
+ This component has **no props** — it is self-contained and fetches all data internally.
68
+
69
+ ```js
70
+ props: []
71
+ ```
72
+
73
+ ### The `session` Object
74
+
75
+ Each session returned from the API has the following structure:
76
+
77
+ ```js
78
+ {
79
+ id: "abc123", // String - Unique session ID
80
+ name: "Math Lesson 1", // String - Session display name
81
+ formatted_date: 1707570000000, // Number - Timestamp (milliseconds)
82
+ duration: 2, // Number - Duration in hours
83
+ link: "https://meet.google.com/xyz", // String|null - Meeting link (Google Meet, Zoom, etc.)
84
+ teacherId: "teacher-456", // String - Teacher ID for lookup
85
+ files: [ // Array - Attached files (Could be empty)
86
+ { name: "notes.pdf", url: "https://..." },
87
+ { filename: "slides.pptx", path: "/files/..." }
88
+ ]
89
+ }
90
+ ```
91
+
92
+ ### The `teacher` Object
93
+
94
+ Teachers are fetched separately for name lookup:
95
+
96
+ ```js
97
+ {
98
+ id: "teacher-456",
99
+ user: {
100
+ name: "John Doe"
101
+ }
102
+ }
103
+ ```
104
+
105
+ ### Local State
106
+
107
+ | Property | Type | Default | Description |
108
+ |----------|------|---------|-------------|
109
+ | `loading` | Boolean | `false` | Shows loading overlay during API calls |
110
+ | `sessions` | Array | `[]` | Sessions for the selected month |
111
+ | `teachers` | Array | `[]` | All teachers for name lookup |
112
+ | `currentDate` | Date | `new Date()` | Reference to current date |
113
+ | `selectedMonth` | Number | Current month (0-11) | Selected month index |
114
+ | `selectedYear` | Number | Current year | Selected year |
115
+ | `sessionDialog` | Boolean | `false` | Controls session details dialog visibility |
116
+ | `selectedDate` | String | `null` | Formatted date string for dialog title |
117
+ | `selectedDaySessions` | Array | `[]` | Sessions for the clicked day |
118
+ | `daysOfWeek` | Array | `[]` | Translated day names (initialized in `mounted`) |
119
+ | `filesDialog` | Boolean | `false` | Controls files dialog visibility |
120
+ | `selectedFiles` | Array | `[]` | Files for the selected session |
121
+
122
+ ### Computed Properties
123
+
124
+ | Property | Returns | Description |
125
+ |----------|---------|-------------|
126
+ | `currentMonthName` | String | Localized month name using `$i18n.locale` |
127
+ | `currentYear` | Number | The `selectedYear` value |
128
+ | `daysInMonth` | Number | Number of days in selected month |
129
+ | `firstDayOfMonth` | Number | Day of week (0-6) for 1st day — used for empty cells |
130
+ | `emptyCellsAfterMonth` | Number | Empty cells needed to complete 5-row grid |
131
+ | `availableMonths` | Array | 13 months: 6 previous, current, 6 next with `{text, value}` |
132
+
133
+ ### Methods
134
+
135
+ | Method | Description |
136
+ |--------|-------------|
137
+ | `loadSessions()` | Async. Fetches sessions for selected month from API with date filters |
138
+ | `loadTeachers()` | Async. Fetches all teachers for name lookup |
139
+ | `navigateMonth(direction)` | Navigate to previous (-1) or next (+1) month and reload sessions |
140
+ | `selectMonth(monthData)` | Select specific month/year from dropdown and reload sessions |
141
+ | `isToday(day)` | Returns true if day matches today's date |
142
+ | `isCurrentMonthSelected(monthValue)` | Returns true if monthValue matches selected month/year |
143
+ | `isSessionInPast(timestamp)` | Returns true if session date is before today |
144
+ | `isSessionInFuture(timestamp)` | Returns true if session date is after today |
145
+ | `hasSessions(day)` | Returns true if day has any sessions |
146
+ | `getDaySessions(day)` | Returns array of sessions for the given day |
147
+ | `showDaySessions(day)` | Opens session dialog with clicked day's sessions |
148
+ | `getTeacherName(session)` | Looks up teacher name from `teachers` array using `teacherId` |
149
+ | `formatTime(timestamp)` | Formats timestamp to localized time string (HH:MM) |
150
+ | `getSessionColor(session)` | Returns consistent color based on session ID hash |
151
+ | `showFilesDialog(session)` | Opens files dialog with session's attached files |
152
+ | `openFile(file)` | Opens file URL/path in new browser window |
153
+
154
+ ### Session Status Logic
155
+
156
+ The join button displays different states based on session timing:
157
+
158
+ | Condition | Button State | Description |
159
+ |-----------|--------------|-------------|
160
+ | `link && !past && !future` | **Join Now** (green) | Session is today with link available |
161
+ | `link && future` | **Upcoming** (disabled) | Session is in future with link |
162
+ | `!link && !past` | **Not Started** (disabled) | Session not past, no link yet |
163
+ | `past` | **Ended** (disabled) | Session date has passed |
164
+
165
+ ### Session Indicators
166
+
167
+ Calendar days show session indicators with smart truncation:
168
+
169
+ - **≤2 sessions**: Show all with colored dots and truncated names (max 15 chars)
170
+ - **>2 sessions**: Show first session + "+N more" indicator in grey
171
+
172
+ Colors are generated from a 6-color palette using a hash of the session ID for consistency.
173
+
174
+ ### API Integration
175
+
176
+ **Load Sessions:**
177
+ ```js
178
+ GET @PA/student-sessions?date_filter_start={startTimestamp}&date_filter_end={endTimestamp}
179
+ // Returns: { items: [...sessions] }
180
+ ```
181
+
182
+ **Load Teachers:**
183
+ ```js
184
+ GET @PA/get-all-teachers-for-students
185
+ // Returns: [...teachers]
186
+ ```
187
+
188
+ Both APIs are called on `mounted` and sessions are reloaded on month navigation.
189
+
190
+ ### Page Sections
191
+
192
+ 1. **Page Header** - Title with video icon and "Live Sessions" text
193
+ 2. **Calendar Card** - White card with shadow containing:
194
+ - **Loading Overlay** - `v-overlay` with spinner during API calls
195
+ - **Calendar Header** - Previous/Next buttons with month/year display
196
+ - **Month Selector** - Dropdown menu with 13 available months
197
+ - **Days of Week Header** - Localized day abbreviations (Sun-Sat)
198
+ - **Calendar Grid** - 7-column grid with day cells, session indicators
199
+ 3. **Session Details Dialog** - List of sessions for clicked day with:
200
+ - Session name, teacher, start time, duration
201
+ - Attached files button (if files exist)
202
+ - Join/Status button based on session state
203
+ 4. **Files Dialog** - List of clickable file items with icons
204
+
205
+ ### Available Translations
206
+
207
+ - Page
208
+ `global.live-sessions` -> Live Sessions
209
+ `global.upcoming` -> upcoming
210
+ `global.loading-sessions` -> Loading sessions...
211
+ `global.no-sessions-this-month` -> No sessions this month
212
+ `global.check-back-later` -> Check back later or explore other months!
213
+ `global.today` -> TODAY
214
+ `global.completed` -> Completed
215
+ - Calendar
216
+ `global.select-month` -> Select month
217
+ `global.more` -> more
218
+ - Days of week
219
+ `global.sun` -> Sun
220
+ `global.mon` -> Mon
221
+ `global.tue` -> Tue
222
+ `global.wed` -> Wed
223
+ `global.thu` -> Thu
224
+ `global.fri` -> Fri
225
+ `global.sat` -> Sat
226
+ - Session dialog
227
+ `global.sessions-for` -> Sessions for
228
+ `global.teacher` -> Teacher
229
+ `global.starts-at` -> Starts at
230
+ `global.duration` -> Duration
231
+ `global.hours` -> hours
232
+ - Session buttons
233
+ `global.join-now` -> Join now
234
+ `global.upcoming-session` -> Upcoming session
235
+ `global.not-started-yet` -> Not started yet
236
+ `global.session-ended` -> Session ended
237
+ - Files
238
+ `global.attached-files` -> Attached files
239
+ `global.view-attached-files` -> View attached files
240
+ `global.no-attached-files` -> No attached files
241
+ `global.unnamed-file` -> Unnamed file
242
+ - Toast
243
+ `global.file-url-not-available` -> File URL not available
244
+ - If you want another text, just put it in English
245
+
246
+ ### Example Student Sessions Page:
247
+ ```js
248
+ module.exports = {
249
+ name: "SessionsCalendar",
250
+ props: [],
251
+ template: /* html */`
252
+ <div class="student-calendar-page">
253
+ <h1><v-icon>mdi-video-outline</v-icon>{{ $t("global.live-sessions") }}</h1>
254
+ <div class="sessions-calendar">
255
+ <!-- Loader overlay -->
256
+ <v-overlay :value="loading">
257
+ <v-progress-circular indeterminate color="white" size="40"></v-progress-circular>
258
+ </v-overlay>
259
+
260
+ <!-- Calendar header with navigation -->
261
+ <div class="calendar-header">
262
+ <v-btn icon @click="navigateMonth(-1)">
263
+ <v-icon>mdi-chevron-left</v-icon>
264
+ </v-btn>
265
+ <h2>{{ currentMonthName }} {{ currentYear }}</h2>
266
+ <v-btn icon @click="navigateMonth(1)">
267
+ <v-icon>mdi-chevron-right</v-icon>
268
+ </v-btn>
269
+ </div>
270
+
271
+ <!-- Month selector -->
272
+ <div class="month-selector mb-4" style="justify-self: center;">
273
+ <v-menu offset-y>
274
+ <template v-slot:activator="{ on, attrs }">
275
+ <v-btn color="primary" v-bind="attrs" v-on="on">
276
+ {{ $t("global.select-month") }}
277
+ <v-icon right>mdi-calendar-month</v-icon>
278
+ </v-btn>
279
+ </template>
280
+ <v-list>
281
+ <v-list-item v-for="(month, i) in availableMonths" :key="i" @click="selectMonth(month.value)">
282
+ <v-list-item-title :class="{ 'primary--text font-weight-bold': isCurrentMonthSelected(month.value) }">
283
+ {{ month.text }}
284
+ </v-list-item-title>
285
+ </v-list-item>
286
+ </v-list>
287
+ </v-menu>
288
+ </div>
289
+
290
+ <!-- Days of week header -->
291
+ <div class="calendar-grid-header">
292
+ <div class="day-name" v-for="day in daysOfWeek" :key="day">{{ day }}</div>
293
+ </div>
294
+
295
+ <!-- Calendar grid -->
296
+ <div class="calendar-grid">
297
+ <!-- Empty cells for days before start of month -->
298
+ <div class="calendar-day empty" v-for="n in firstDayOfMonth" :key="'empty-start-'+n"></div>
299
+
300
+ <!-- Calendar days -->
301
+ <div class="calendar-day" v-for="day in daysInMonth" :key="day"
302
+ :class="{ today: isToday(day), 'has-sessions': hasSessions(day) }" @click="showDaySessions(day)">
303
+ <div class="day-number">{{ day }}</div>
304
+
305
+ <!-- Session indicators - Updated style to match image -->
306
+ <div class="sessions-container">
307
+ <template v-if="getDaySessions(day).length <= 2">
308
+ <div class="session-indicator" v-for="(session, index) in getDaySessions(day)"
309
+ :key="'session-'+day+'-'+index">
310
+ <div class="session-dot" :style="{ backgroundColor: getSessionColor(session) }"></div>
311
+ <span class="session-name">{{ session.name.length > 22 ? session.name.substring(0, 15) + '...' : session.name }}</span>
312
+ </div>
313
+ </template>
314
+ <template v-else>
315
+ <div class="session-indicator">
316
+ <div class="session-dot" :style="{ backgroundColor: getSessionColor(getDaySessions(day)[0]) }"></div>
317
+ <span class="session-name">{{ getDaySessions(day)[0].name.length > 22 ? getDaySessions(day)[0].name.substring(0, 15) + '...' : getDaySessions(day)[0].name }}</span>
318
+ </div>
319
+ <div class="session-indicator more-sessions">
320
+ <div class="session-dot" style="backgroundColor: #9e9e9e"></div>
321
+ <span class="session-name">+{{ getDaySessions(day).length - 1 }} {{ $t("global.more") }}</span>
322
+ </div>
323
+ </template>
324
+ </div>
325
+ </div>
326
+
327
+ <!-- Empty cells for days after end of month -->
328
+ <div class="calendar-day empty" v-for="n in emptyCellsAfterMonth" :key="'empty-end-'+n"></div>
329
+ </div>
330
+
331
+ <!-- Files dialog -->
332
+ <v-dialog v-model="filesDialog" max-width="500">
333
+ <v-card>
334
+ <v-card-title style="font-weight: 300">
335
+ {{ $t("global.attached-files") }}
336
+ <v-spacer></v-spacer>
337
+ <v-btn icon @click="filesDialog = false">
338
+ <v-icon>mdi-close</v-icon>
339
+ </v-btn>
340
+ </v-card-title>
341
+ <v-card-text>
342
+ <v-list>
343
+ <v-list-item
344
+ v-for="(file, index) in selectedFiles"
345
+ :key="index"
346
+ @click="openFile(file)"
347
+ style="cursor: pointer; border: 1px solid #e0e0e0; margin-bottom: 8px; border-radius: 4px;"
348
+ >
349
+ <v-list-item-avatar>
350
+ <v-icon color="primary">mdi-file-document</v-icon>
351
+ </v-list-item-avatar>
352
+ <v-list-item-content>
353
+ <v-list-item-title>{{ file.name || file.filename || $t("global.unnamed-file") }}</v-list-item-title>
354
+ </v-list-item-content>
355
+ <v-list-item-action>
356
+ <v-icon>mdi-open-in-new</v-icon>
357
+ </v-list-item-action>
358
+ </v-list-item>
359
+ </v-list>
360
+ <div v-if="!selectedFiles || selectedFiles.length === 0" class="text-center pa-4 grey--text">
361
+ {{ $t("global.no-attached-files") }}
362
+ </div>
363
+ </v-card-text>
364
+ </v-card>
365
+ </v-dialog>
366
+
367
+ <!-- Session details dialog -->
368
+ <v-dialog v-model="sessionDialog" max-width="600">
369
+ <v-card>
370
+ <v-card-title style="font-weight: 300">
371
+ {{ $t("global.sessions-for") }} {{ selectedDate }}
372
+ <v-spacer></v-spacer>
373
+ <v-btn icon @click="sessionDialog = false">
374
+ <v-icon>mdi-close</v-icon>
375
+ </v-btn>
376
+ </v-card-title>
377
+ <v-card-text>
378
+ <v-list two-line>
379
+ <v-list-item v-for="(session, index) in selectedDaySessions" :key="'detail-'+index"
380
+ class="popup-session-card">
381
+ <v-list-item-content>
382
+ <v-list-item-title>{{ session.name }}</v-list-item-title>
383
+ <v-list-item-subtitle>
384
+ <template>
385
+ {{ $t("global.teacher") }}: {{ getTeacherName(session) }}
386
+ </template>
387
+ <br>
388
+ <template>
389
+ {{ $t("global.starts-at") }}: {{ formatTime(session.formatted_date) }}
390
+ </template>
391
+ <br>
392
+ {{ $t("global.duration") }}: {{ session.duration }} {{ $t("global.hours") }}
393
+ <br>
394
+ <v-btn
395
+ color="primary"
396
+ style="padding: 0 5px;"
397
+ v-if="session.files && session.files.length > 0"
398
+ @click.stop="showFilesDialog(session)"
399
+ >
400
+ {{ $t("global.view-attached-files") }} ({{ session.files.length }})
401
+ </v-btn>
402
+ <span v-else>
403
+ {{ $t("global.attached-files") }}: 0
404
+ </span>
405
+ </v-list-item-subtitle>
406
+ </v-list-item-content>
407
+ <v-list-item-action>
408
+ <!-- Button to Join Now -->
409
+ <v-btn
410
+ v-if="session.link && !isSessionInPast(session.formatted_date) && !isSessionInFuture(session.formatted_date)"
411
+ color="green"
412
+ dark
413
+ :href="session.link"
414
+ target="_blank"
415
+ >
416
+ <v-icon left>mdi-open-in-new</v-icon>
417
+ {{ $t("global.join-now") }}
418
+ </v-btn>
419
+
420
+ <!-- Button for Upcoming Session -->
421
+ <v-btn
422
+ v-else-if="session.link && isSessionInFuture(session.formatted_date)"
423
+ disabled
424
+ color="grey"
425
+ >
426
+ <v-icon left>mdi-circle-off-outline</v-icon>
427
+ {{ $t("global.upcoming-session") }}
428
+ </v-btn>
429
+
430
+ <!-- Button for Session Not Started (No Link Yet) -->
431
+ <v-btn
432
+ v-else-if="!session.link && !isSessionInPast(session.formatted_date)"
433
+ disabled
434
+ color="grey"
435
+ >
436
+ <v-icon left>mdi-circle-off-outline</v-icon>
437
+ {{ $t("global.not-started-yet") }}
438
+ </v-btn>
439
+
440
+ <!-- Button for Past Session -->
441
+ <v-btn
442
+ v-else-if="isSessionInPast(session.formatted_date)"
443
+ disabled
444
+ color="grey"
445
+ >
446
+ <v-icon left>mdi-circle-off-outline</v-icon>
447
+ {{ $t("global.session-ended") }}
448
+ </v-btn>
449
+ </v-list-item-action>
450
+ </v-list-item>
451
+ </v-list>
452
+ </v-card-text>
453
+ </v-card>
454
+ </v-dialog>
455
+ </div>
456
+ </div>
457
+ `,
458
+
459
+ data: /* js */`
460
+ function() {
461
+ return {
462
+ loading: false,
463
+ sessions: [],
464
+ teachers: [],
465
+ currentDate: new Date(),
466
+ selectedMonth: new Date().getMonth(),
467
+ selectedYear: new Date().getFullYear(),
468
+ sessionDialog: false,
469
+ selectedDate: null,
470
+ selectedDaySessions: [],
471
+ daysOfWeek: [],
472
+ filesDialog: false,
473
+ selectedFiles: []
474
+ };
475
+ }
476
+ `,
477
+
478
+ mounted: /* js */`
479
+ async function() {
480
+ this.loading = true;
481
+
482
+ // Initialize days of week with i18n
483
+ this.daysOfWeek = [
484
+ this.$t("global.sun"),
485
+ this.$t("global.mon"),
486
+ this.$t("global.tue"),
487
+ this.$t("global.wed"),
488
+ this.$t("global.thu"),
489
+ this.$t("global.fri"),
490
+ this.$t("global.sat")
491
+ ];
492
+
493
+ // Load sessions for the current month
494
+ await this.loadSessions();
495
+
496
+ // Load teachers data for displaying teacher names
497
+ await this.loadTeachers();
498
+
499
+ this.loading = false;
500
+ }
501
+ `,
502
+
503
+ computed: /* js */`
504
+ {
505
+ currentMonthName() {
506
+ return new Date(this.selectedYear, this.selectedMonth).toLocaleString(this.$i18n.locale, { month: 'long' });
507
+ },
508
+ currentYear() {
509
+ return this.selectedYear;
510
+ },
511
+ daysInMonth() {
512
+ return new Date(this.selectedYear, this.selectedMonth + 1, 0).getDate();
513
+ },
514
+ firstDayOfMonth() {
515
+ return new Date(this.selectedYear, this.selectedMonth, 1).getDay();
516
+ },
517
+ emptyCellsAfterMonth() {
518
+ const totalCells = 35; // 5 rows of 7 days
519
+ return totalCells - this.daysInMonth - this.firstDayOfMonth;
520
+ },
521
+ availableMonths() {
522
+ const today = new Date();
523
+ const currentMonth = today.getMonth();
524
+ const currentYear = today.getFullYear();
525
+
526
+ const months = [];
527
+
528
+ // Previous 6 months
529
+ for (let i = 6; i > 0; i--) {
530
+ let month = currentMonth - i;
531
+ let year = currentYear;
532
+
533
+ if (month < 0) {
534
+ month += 12;
535
+ year--;
536
+ }
537
+
538
+ const date = new Date(year, month);
539
+ months.push({
540
+ text: date.toLocaleString(this.$i18n.locale, { month: 'long' }) + " " + year,
541
+ value: { month, year }
542
+ });
543
+ }
544
+
545
+ // Current month
546
+ months.push({
547
+ text: today.toLocaleString(this.$i18n.locale, { month: 'long' }) + " " + currentYear,
548
+ value: { month: currentMonth, year: currentYear }
549
+ });
550
+
551
+ // Next 6 months
552
+ for (let i = 1; i <= 6; i++) {
553
+ let month = currentMonth + i;
554
+ let year = currentYear;
555
+
556
+ if (month > 11) {
557
+ month -= 12;
558
+ year++;
559
+ }
560
+
561
+ const date = new Date(year, month);
562
+ months.push({
563
+ text: date.toLocaleString(this.$i18n.locale, { month: 'long' }) + " " + year,
564
+ value: { month, year }
565
+ });
566
+ }
567
+
568
+ return months;
569
+ }
570
+ }
571
+ `,
572
+
573
+ methods: /* js */`
574
+ {
575
+ async loadSessions() {
576
+ this.loading = true;
577
+
578
+ try {
579
+ // Calculate date range for the selected month
580
+ const startDate = new Date(this.selectedYear, this.selectedMonth, 1).getTime();
581
+ const endDate = new Date(this.selectedYear, this.selectedMonth + 1, 0, 23, 59, 59).getTime();
582
+
583
+ const response = await this.$dataCaller("get", "@PA/student-sessions?date_filter_start="+startDate+"&date_filter_end="+endDate);
584
+
585
+ this.sessions = response.items || [];
586
+ } catch (error) {
587
+ console.error("Error loading sessions:", error);
588
+ this.sessions = [];
589
+ }
590
+
591
+ this.loading = false;
592
+ },
593
+ async loadTeachers() {
594
+ try {
595
+ const response = await this.$dataCaller("get", "@PA/get-all-teachers-for-students");
596
+ this.teachers = response || [];
597
+ } catch (error) {
598
+ console.error("Error loading teachers:", error);
599
+ this.teachers = [];
600
+ }
601
+ },
602
+ navigateMonth(direction) {
603
+ let newMonth = this.selectedMonth + direction;
604
+ let newYear = this.selectedYear;
605
+ if (newMonth < 0) {
606
+ newMonth = 11;
607
+ newYear--;
608
+ } else if (newMonth > 11) {
609
+ newMonth = 0;
610
+ newYear++;
611
+ }
612
+ this.selectedMonth = newMonth;
613
+ this.selectedYear = newYear;
614
+ this.loadSessions();
615
+ },
616
+ selectMonth(monthData) {
617
+ this.selectedMonth = monthData.month;
618
+ this.selectedYear = monthData.year;
619
+ this.loadSessions();
620
+ },
621
+ isToday(day) {
622
+ const today = new Date();
623
+ return day === today.getDate() &&
624
+ this.selectedMonth === today.getMonth() &&
625
+ this.selectedYear === today.getFullYear();
626
+ },
627
+ isCurrentMonthSelected(monthValue) {
628
+ return monthValue.month === this.selectedMonth && monthValue.year === this.selectedYear;
629
+ },
630
+ isSessionInPast(sessionTimestamp) {
631
+ if (!sessionTimestamp) return true; // Consider it past if no timestamp
632
+ const sessionDate = new Date(sessionTimestamp);
633
+ const now = new Date();
634
+
635
+ // Set time to 00:00:00 for both dates to only compare dates, not time
636
+ sessionDate.setHours(0, 0, 0, 0);
637
+ now.setHours(0, 0, 0, 0);
638
+ return sessionTimestamp < now;
639
+ },
640
+ isSessionInFuture(sessionTimestamp) {
641
+ if (!sessionTimestamp) return true; // Consider it past if no timestamp
642
+
643
+ const sessionDate = new Date(sessionTimestamp);
644
+ const now = new Date();
645
+
646
+ // Set time to 00:00:00 for both dates to only compare dates, not time
647
+ sessionDate.setHours(0, 0, 0, 0);
648
+ now.setHours(0, 0, 0, 0);
649
+
650
+ return sessionDate > now; // true if session is after today
651
+ },
652
+ hasSessions(day) {
653
+ return this.getDaySessions(day).length > 0;
654
+ },
655
+ getDaySessions(day) {
656
+ return this.sessions.filter(session => {
657
+ const sessionDate = new Date(session.formatted_date);
658
+ const isSameDay = sessionDate.getDate().toString() == day.toString();
659
+ return isSameDay;
660
+ });
661
+ },
662
+ showDaySessions(day) {
663
+ this.selectedDaySessions = this.getDaySessions(day);
664
+ this.selectedDate = this.currentMonthName + " " + day + ", " + this.currentYear;
665
+ this.sessionDialog = true;
666
+ },
667
+ getTeacherName(session) {
668
+ if (!session.teacherId) return "N/A";
669
+
670
+ const teacher = this.teachers.find(t => t.id === session.teacherId);
671
+ return teacher ? teacher.user.name : "Unknown";
672
+ },
673
+ formatTime(timestamp) {
674
+ if (!timestamp) return "";
675
+ const date = new Date(timestamp);
676
+ return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
677
+ },
678
+ getSessionColor(session) {
679
+ // Generate consistent colors based on session ID or name
680
+ const colors = [
681
+ "#4285F4", // Blue
682
+ "#EA4335", // Red
683
+ "#FBBC05", // Yellow
684
+ "#34A853", // Green
685
+ "#8F00FF", // Purple
686
+ "#FF6D01", // Orange
687
+ ];
688
+ // Simple hash function to get a consistent index
689
+ const hash = session.id.split('').reduce((acc, char) => {
690
+ return acc + char.charCodeAt(0);
691
+ }, 0);
692
+ return colors[hash % colors.length];
693
+ },
694
+ showFilesDialog(session) {
695
+ this.selectedFiles = session.files || [];
696
+ this.filesDialog = true;
697
+ },
698
+ openFile(file) {
699
+ // Open file in new window
700
+ if (file.url) {
701
+ window.open(file.url, '_blank');
702
+ } else if (file.path) {
703
+ window.open(file.path, '_blank');
704
+ } else {
705
+ this.$toast.error(this.$t('global.file-url-not-available'));
706
+ }
707
+ }
708
+ }
709
+ `,
710
+
711
+ style: /* css */`
712
+ .student-calendar-page {
713
+ padding: 20px;
714
+ }
715
+
716
+ .student-calendar-page h1 {
717
+ font-weight: 300;
718
+ margin-bottom: 7px;
719
+ }
720
+ .student-calendar-page h1 i {
721
+ font-size: 60px;
722
+ }
723
+
724
+ .student-calendar-page .sessions-calendar {
725
+ padding: 20px;
726
+ background-color: #fff;
727
+ border-radius: 8px;
728
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
729
+ }
730
+
731
+ .student-calendar-page .calendar-header {
732
+ display: flex;
733
+ align-items: center;
734
+ justify-content: space-between;
735
+ margin-bottom: 20px;
736
+ }
737
+
738
+ .student-calendar-page .calendar-grid-header {
739
+ display: grid;
740
+ grid-template-columns: repeat(7, 1fr);
741
+ text-align: center;
742
+ font-weight: bold;
743
+ margin-bottom: 10px;
744
+ }
745
+
746
+ .student-calendar-page .day-name {
747
+ padding: 10px;
748
+ background-color: #f5f5f5;
749
+ border-radius: 4px;
750
+ }
751
+
752
+ .student-calendar-page .calendar-grid {
753
+ display: grid;
754
+ grid-template-columns: repeat(7, 1fr);
755
+ grid-gap: 5px;
756
+ max-width: 100%;
757
+ overflow: auto;
758
+ }
759
+
760
+ .student-calendar-page .calendar-day {
761
+ min-height: 100px;
762
+ border: 1px solid #e0e0e0;
763
+ border-radius: 4px;
764
+ padding: 5px;
765
+ position: relative;
766
+ transition: background-color 0.2s;
767
+ max-width: 20vw;
768
+ }
769
+
770
+ .student-calendar-page .calendar-day:hover {
771
+ background-color: #f9f9f9;
772
+ cursor: pointer;
773
+ }
774
+
775
+ .student-calendar-page .calendar-day.empty {
776
+ background-color: #f7f7f7;
777
+ cursor: default;
778
+ }
779
+
780
+ .student-calendar-page .calendar-day.today {
781
+ border: 2px solid #4285F4;
782
+ }
783
+
784
+ .student-calendar-page .day-number {
785
+ font-weight: bold;
786
+ margin-bottom: 5px;
787
+ }
788
+
789
+ .student-calendar-page .sessions-container {
790
+ display: flex;
791
+ flex-direction: column;
792
+ gap: 3px;
793
+ }
794
+
795
+ .student-calendar-page .popup-session-card {
796
+ border: 1px solid #818181;
797
+ border-radius: 5px;
798
+ border-left-width: 4px;
799
+ margin-bottom: 7px;
800
+ }
801
+
802
+ /* Updated session indicator styles to match the image */
803
+ .student-calendar-page .session-indicator {
804
+ display: flex;
805
+ align-items: center;
806
+ font-size: 12px;
807
+ padding: 2px 4px;
808
+ border-radius: 3px;
809
+ white-space: nowrap;
810
+ overflow: hidden;
811
+ text-overflow: ellipsis;
812
+ }
813
+
814
+ .student-calendar-page .session-dot {
815
+ width: 8px;
816
+ height: 8px;
817
+ border-radius: 50%;
818
+ margin-right: 5px;
819
+ flex-shrink: 0;
820
+ }
821
+
822
+ .student-calendar-page .session-name {
823
+ overflow: hidden;
824
+ text-overflow: ellipsis;
825
+ }
826
+
827
+ .student-calendar-page .more-sessions {
828
+ color: #757575;
829
+ font-size: 11px;
830
+ }
831
+
832
+ .student-calendar-page .has-sessions .day-number::after {
833
+ content: "";
834
+ display: inline-block;
835
+ width: 6px;
836
+ height: 6px;
837
+ background-color: #4285F4;
838
+ border-radius: 50%;
839
+ margin-left: 3px;
840
+ }
841
+ `
842
+ }
843
+ ```
844
+
845
+ ## Vue Component Format
846
+
847
+ ### Basic Structure
848
+
849
+ ```js
850
+ module.exports = {
851
+ name: "MyComponent",
852
+
853
+ props: {
854
+ title: { required: true },
855
+ count: { default: 0 }
856
+ },
857
+ // Or in array format:
858
+ // props: [
859
+ // 'title',
860
+ // 'count'
861
+ // ],
862
+
863
+ template: /* html */`
864
+ <div class="my-component">
865
+ <h1>{{ title }}</h1>
866
+ <button @click="increment">Count: {{ counter }}</button>
867
+ </div>
868
+ `,
869
+
870
+ style: /* css */`
871
+ .my-component { padding: 20px; }
872
+ .my-component h1 { color: blue; }
873
+ `,
874
+
875
+ data: /* js */`
876
+ function() {
877
+ return {
878
+ counter: 0
879
+ };
880
+ }
881
+ `,
882
+
883
+ computed: /* js */`
884
+ {
885
+ counterText() {
886
+ return this.counter + " Counts";
887
+ }
888
+ }
889
+ `,
890
+
891
+ methods: /* js */`
892
+ {
893
+ increment() {
894
+ this.counter++;
895
+ }
896
+ }
897
+ `,
898
+
899
+ mounted: /* js */`
900
+ function() {
901
+ console.log('Component mounted!');
902
+ }
903
+ `
904
+ };
905
+ ```
906
+
907
+ ### Available Fields
908
+
909
+ | Field | Format | Description |
910
+ |-------|--------|-------------|
911
+ | `name` | String | Component name (required) |
912
+ | `props` | Object | Props definition (not a string) |
913
+ | `template` | Template literal | HTML template with Vue syntax |
914
+ | `style` | Template literal | CSS styles for the component |
915
+ | `data` | Template literal | Function returning initial state |
916
+ | `computed` | Template literal | Object with computed properties |
917
+ | `watch` | Template literal | Object with watchers |
918
+ | `methods` | Template literal | Object with methods |
919
+ | `mounted` | Template literal | Lifecycle hook function |
920
+ | `created`, `beforeMount`, `beforeUpdate`, `updated`, `beforeDestroy`, `destroyed` | Template literal | Other lifecycle hooks |
921
+
922
+ ### Key Rules
923
+
924
+ 1. **Use template literals** (backticks) for `template`, `style`, `data`, `methods`, etc.
925
+ 2. **Props is an object**, not a template literal
926
+ 3. **Comments are optional** but recommended: `/* html */`, `/* css */`, `/* js */`
927
+ 4. **Unused fields** can be omitted or set to `null`
928
+ 5. **Empty file marker**: `/* EMPTY FILE */` for placeholder files
929
+
930
+ ### Minimal Example
931
+
932
+ ```js
933
+ module.exports = {
934
+ name: "HelloWorld",
935
+ template: /* html */`
936
+ <div>Hello, World!</div>
937
+ `
938
+ };
939
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cyber-elx",
3
- "version": "1.1.12",
3
+ "version": "1.2.0",
4
4
  "description": "CyberOcean CLI tool to upload/download ELX custom pages",
5
5
  "main": "src/index.js",
6
6
  "bin": {