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.
- package/DEV_DOC/LoginRegisterPagesDev.md +12 -0
- package/DEV_DOC/PaymentPageDev.md +5 -0
- package/DEV_DOC/README.md +3 -0
- package/DEV_DOC/StudentCourseDetailPageDev.md +5 -1
- package/DEV_DOC/StudentCoursePlayerDev.md +5 -0
- package/DEV_DOC/StudentListCoursesPageDev.md +6 -0
- package/DEV_DOC/StudentMyCoursesPageDev.md +6 -0
- package/DEV_DOC/StudentProfileDev.md +19 -0
- package/DEV_DOC/StudentSessionsPageDev.md +939 -0
- package/package.json +1 -1
|
@@ -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
|
+
```
|