@eeplatform/nuxt-layer-common 1.2.11 → 1.3.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.
@@ -0,0 +1,452 @@
1
+ <template>
2
+ <div class="calendar month-view" :style="{ height: calendarHeight }">
3
+ <div v-for="day in weekdays" :key="day" class="header border">
4
+ <slot name="header" :day="day" :dayIndex="weekdays.indexOf(day)">
5
+ {{ day }}
6
+ </slot>
7
+ </div>
8
+
9
+ <div
10
+ v-for="cell in calendarCells"
11
+ :key="cell.type + '-' + cell.day"
12
+ class="day border"
13
+ :class="{
14
+ today: cell.type === 'current' && isToday(cell.day),
15
+ 'other-month': cell.type !== 'current',
16
+ }"
17
+ @click="onDayClick(cell)"
18
+ >
19
+ <slot
20
+ name="day"
21
+ :cell="cell"
22
+ :day="cell.day"
23
+ :date="cell.date"
24
+ :type="cell.type"
25
+ :monthAbbr="cell.monthAbbr"
26
+ :showMonthAbbr="cell.showMonthAbbr"
27
+ :isToday="cell.type === 'current' && isToday(cell.day)"
28
+ :isCurrentMonth="cell.type === 'current'"
29
+ :isPrevMonth="cell.type === 'prev'"
30
+ :isNextMonth="cell.type === 'next'"
31
+ :events="cell.events"
32
+ :eventPositions="cell.eventPositions"
33
+ :hasEvents="cell.events.length > 0"
34
+ >
35
+ <!-- Default slot content (fallback) -->
36
+ <div class="day-content">
37
+ <div class="day-number">{{ cell.day }}</div>
38
+ <div v-if="cell.showMonthAbbr" class="month-abbr">
39
+ {{ cell.monthAbbr }}
40
+ </div>
41
+ <!-- Default event indicators - Google Calendar style -->
42
+ <div v-if="cell.events.length > 0" class="event-indicators">
43
+ <div
44
+ v-for="(event, index) in cell.events.slice(0, 4)"
45
+ :key="index"
46
+ class="default-event-block"
47
+ :style="{ backgroundColor: event.color || '#2196f3' }"
48
+ :title="getEventTooltip(event)"
49
+ >
50
+ <div class="event-content">
51
+ <span class="default-event-text">{{ event.title }}</span>
52
+ <span
53
+ v-if="event.startTime || event.endTime"
54
+ class="event-time"
55
+ >
56
+ {{ formatEventTime(event) }}
57
+ </span>
58
+ </div>
59
+ </div>
60
+ <div v-if="cell.events.length > 4" class="more-events">
61
+ +{{ cell.events.length - 4 }} more
62
+ </div>
63
+ </div>
64
+ </div>
65
+ </slot>
66
+ </div>
67
+ </div>
68
+ </template>
69
+
70
+ <script setup>
71
+ import { computed } from "vue";
72
+
73
+ const props = defineProps({
74
+ current: {
75
+ type: Date,
76
+ required: true,
77
+ },
78
+ events: {
79
+ type: Array,
80
+ default: () => [],
81
+ },
82
+ calendarHeight: {
83
+ type: String,
84
+ default: "calc(100vh - 100px)",
85
+ },
86
+ });
87
+
88
+ const emit = defineEmits(["dayClick"]);
89
+
90
+ const weekdays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
91
+ const monthNames = [
92
+ "Jan",
93
+ "Feb",
94
+ "Mar",
95
+ "Apr",
96
+ "May",
97
+ "Jun",
98
+ "Jul",
99
+ "Aug",
100
+ "Sep",
101
+ "Oct",
102
+ "Nov",
103
+ "Dec",
104
+ ];
105
+
106
+ // Days in current month
107
+ const daysInMonth = computed(() => {
108
+ const year = props.current.getFullYear();
109
+ const month = props.current.getMonth();
110
+ return new Date(year, month + 1, 0).getDate();
111
+ });
112
+
113
+ // First day index of current month (0=Sun)
114
+ const firstDayOfMonth = computed(() => {
115
+ const year = props.current.getFullYear();
116
+ const month = props.current.getMonth();
117
+ return new Date(year, month, 1).getDay();
118
+ });
119
+
120
+ // Days in previous month
121
+ const prevMonthDays = computed(() => {
122
+ const year = props.current.getFullYear();
123
+ const month = props.current.getMonth();
124
+ return new Date(year, month, 0).getDate();
125
+ });
126
+
127
+ // Total cells in calendar (6 rows x 7 columns)
128
+ const totalCells = 42;
129
+
130
+ // Helper functions for event calculations
131
+ const isSameDay = (date1, date2) => {
132
+ return date1.toDateString() === date2.toDateString();
133
+ };
134
+
135
+ const isDateInRange = (date, startDate, endDate) => {
136
+ const checkDate = new Date(date);
137
+ checkDate.setHours(0, 0, 0, 0);
138
+ const start = new Date(startDate);
139
+ start.setHours(0, 0, 0, 0);
140
+ const end = new Date(endDate);
141
+ end.setHours(0, 0, 0, 0);
142
+
143
+ return checkDate >= start && checkDate <= end;
144
+ };
145
+
146
+ const getEventsForDate = (date) => {
147
+ return props.events.filter((event) =>
148
+ isDateInRange(date, event.startDate, event.endDate)
149
+ );
150
+ };
151
+
152
+ const getEventPosition = (event, date) => {
153
+ const isStart = isSameDay(date, event.startDate);
154
+ const isEnd = isSameDay(date, event.endDate);
155
+ const isMiddle =
156
+ !isStart && !isEnd && isDateInRange(date, event.startDate, event.endDate);
157
+
158
+ return {
159
+ isStart,
160
+ isEnd,
161
+ isMiddle,
162
+ isSingle: isStart && isEnd, // single day event
163
+ };
164
+ };
165
+
166
+ // Build calendar with prev, current, next month
167
+ const calendarCells = computed(() => {
168
+ const cells = [];
169
+ const year = props.current.getFullYear();
170
+ const month = props.current.getMonth();
171
+
172
+ // Previous month dates (for empty spots at start)
173
+ const prevMonth = month === 0 ? 11 : month - 1;
174
+ const prevYear = month === 0 ? year - 1 : year;
175
+ const prevMonthDaysArray = [];
176
+ for (let i = firstDayOfMonth.value - 1; i >= 0; i--) {
177
+ prevMonthDaysArray.push(prevMonthDays.value - i);
178
+ }
179
+
180
+ prevMonthDaysArray.forEach((day, index) => {
181
+ const cellDate = new Date(prevYear, prevMonth, day);
182
+ const eventsForDate = getEventsForDate(cellDate);
183
+
184
+ cells.push({
185
+ day: day,
186
+ type: "prev",
187
+ monthAbbr: monthNames[prevMonth],
188
+ showMonthAbbr: index === prevMonthDaysArray.length - 1,
189
+ date: cellDate,
190
+ events: eventsForDate,
191
+ eventPositions: eventsForDate.map((event) =>
192
+ getEventPosition(event, cellDate)
193
+ ),
194
+ });
195
+ });
196
+
197
+ // Current month dates
198
+ for (let i = 1; i <= daysInMonth.value; i++) {
199
+ const cellDate = new Date(year, month, i);
200
+ const eventsForDate = getEventsForDate(cellDate);
201
+
202
+ cells.push({
203
+ day: i,
204
+ type: "current",
205
+ monthAbbr: monthNames[month],
206
+ showMonthAbbr: false,
207
+ date: cellDate,
208
+ events: eventsForDate,
209
+ eventPositions: eventsForDate.map((event) =>
210
+ getEventPosition(event, cellDate)
211
+ ),
212
+ });
213
+ }
214
+
215
+ // Next month dates (to fill remaining cells)
216
+ const nextMonth = month === 11 ? 0 : month + 1;
217
+ const nextYear = month === 11 ? year + 1 : year;
218
+ let nextDays = totalCells - cells.length;
219
+ for (let i = 1; i <= nextDays; i++) {
220
+ const cellDate = new Date(nextYear, nextMonth, i);
221
+ const eventsForDate = getEventsForDate(cellDate);
222
+
223
+ cells.push({
224
+ day: i,
225
+ type: "next",
226
+ monthAbbr: monthNames[nextMonth],
227
+ showMonthAbbr: i === 1,
228
+ date: cellDate,
229
+ events: eventsForDate,
230
+ eventPositions: eventsForDate.map((event) =>
231
+ getEventPosition(event, cellDate)
232
+ ),
233
+ });
234
+ }
235
+
236
+ return cells;
237
+ });
238
+
239
+ // Check if a day is today
240
+ const isToday = (day) => {
241
+ const today = new Date();
242
+ return (
243
+ today.getDate() === day &&
244
+ today.getMonth() === props.current.getMonth() &&
245
+ today.getFullYear() === props.current.getFullYear()
246
+ );
247
+ };
248
+
249
+ // Format event time display
250
+ const formatEventTime = (event) => {
251
+ if (!event.startTime && !event.endTime) return "";
252
+
253
+ const formatTime = (timeStr) => {
254
+ if (!timeStr) return "";
255
+
256
+ // If it's already a formatted 12-hour string (contains AM/PM), return it as-is
257
+ if (typeof timeStr === "string" && /AM|PM/i.test(timeStr)) {
258
+ return timeStr;
259
+ }
260
+
261
+ // If it's a 24-hour format string, convert to 12-hour
262
+ if (typeof timeStr === "string" && /^\d{1,2}:\d{2}$/.test(timeStr)) {
263
+ const [hours, minutes] = timeStr.split(":");
264
+ const hour24 = parseInt(hours);
265
+ const hour12 = hour24 === 0 ? 12 : hour24 > 12 ? hour24 - 12 : hour24;
266
+ const period = hour24 >= 12 ? "PM" : "AM";
267
+ return `${hour12}:${minutes} ${period}`;
268
+ }
269
+
270
+ // If it's a Date object, format it to 12-hour
271
+ if (timeStr instanceof Date) {
272
+ return timeStr.toLocaleTimeString([], {
273
+ hour: "numeric",
274
+ minute: "2-digit",
275
+ hour12: true,
276
+ });
277
+ }
278
+
279
+ return timeStr;
280
+ };
281
+
282
+ const start = formatTime(event.startTime);
283
+ const end = formatTime(event.endTime);
284
+
285
+ if (start && end) {
286
+ return `${start} - ${end}`;
287
+ } else if (start) {
288
+ return start;
289
+ } else if (end) {
290
+ return `Until ${end}`;
291
+ }
292
+
293
+ return "";
294
+ };
295
+
296
+ // Get tooltip text for event
297
+ const getEventTooltip = (event) => {
298
+ let tooltip = event.title;
299
+
300
+ if (event.type) {
301
+ tooltip += ` (${event.type})`;
302
+ }
303
+
304
+ const timeStr = formatEventTime(event);
305
+ if (timeStr) {
306
+ tooltip += `\n${timeStr}`;
307
+ }
308
+
309
+ if (event.description) {
310
+ tooltip += `\n${event.description}`;
311
+ }
312
+
313
+ return tooltip;
314
+ };
315
+
316
+ // Handle day click
317
+ const onDayClick = (cell) => {
318
+ emit("dayClick", {
319
+ date: cell.date,
320
+ day: cell.day,
321
+ type: cell.type,
322
+ monthAbbr: cell.monthAbbr,
323
+ });
324
+ };
325
+ </script>
326
+
327
+ <style scoped>
328
+ .calendar {
329
+ display: grid;
330
+ grid-template-columns: repeat(7, 1fr);
331
+ grid-template-rows: auto repeat(6, 1fr);
332
+ width: 100%;
333
+ font-family: sans-serif;
334
+ }
335
+
336
+ .header {
337
+ font-weight: bold;
338
+ text-align: center;
339
+ padding: 10px 0;
340
+ background-color: #f0f0f0;
341
+ }
342
+
343
+ .day {
344
+ display: flex;
345
+ flex-direction: column;
346
+ justify-content: flex-start;
347
+ align-items: center;
348
+ text-align: center;
349
+ cursor: pointer;
350
+ transition: background-color 0.2s;
351
+ position: relative;
352
+ padding: 4px 2px;
353
+ }
354
+
355
+ .day-content {
356
+ width: 100%;
357
+ height: 100%;
358
+ display: flex;
359
+ flex-direction: column;
360
+ align-items: center;
361
+ }
362
+
363
+ .day-number {
364
+ font-size: 1.1em;
365
+ font-weight: 500;
366
+ margin-bottom: 2px;
367
+ }
368
+
369
+ .month-abbr {
370
+ font-size: 0.7em;
371
+ color: #888;
372
+ margin-top: 2px;
373
+ font-weight: normal;
374
+ }
375
+
376
+ .event-indicators {
377
+ width: 100%;
378
+ margin-top: auto;
379
+ padding-bottom: 2px;
380
+ }
381
+
382
+ .default-event-block {
383
+ margin: 1px 0;
384
+ border-radius: 4px;
385
+ font-size: 0.7em;
386
+ display: flex;
387
+ align-items: flex-start;
388
+ min-height: 18px;
389
+ padding: 2px 4px;
390
+ box-sizing: border-box;
391
+ border-bottom: 1px solid rgba(255, 255, 255, 0.3);
392
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
393
+ overflow: hidden;
394
+ }
395
+
396
+ .event-content {
397
+ display: flex;
398
+ flex-direction: column;
399
+ width: 100%;
400
+ line-height: 1.1;
401
+ }
402
+
403
+ .default-event-text {
404
+ color: white;
405
+ font-weight: 500;
406
+ font-size: 0.7em;
407
+ text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
408
+ white-space: nowrap;
409
+ overflow: hidden;
410
+ text-overflow: ellipsis;
411
+ width: 100%;
412
+ }
413
+
414
+ .event-time {
415
+ color: rgba(255, 255, 255, 0.9);
416
+ font-weight: 400;
417
+ font-size: 0.6em;
418
+ text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
419
+ white-space: nowrap;
420
+ overflow: hidden;
421
+ text-overflow: ellipsis;
422
+ margin-top: 1px;
423
+ }
424
+
425
+ .more-events {
426
+ font-size: 0.6em;
427
+ color: #666;
428
+ text-align: center;
429
+ margin-top: 1px;
430
+ font-weight: 500;
431
+ }
432
+
433
+ .day:hover {
434
+ background-color: #d0e8ff;
435
+ }
436
+
437
+ .day.other-month {
438
+ color: #aaa;
439
+ cursor: default;
440
+ }
441
+
442
+ .today {
443
+ background-color: #ffd54f;
444
+ border: 2px solid #ffa500;
445
+ }
446
+
447
+ /* Update month view */
448
+ .month-view {
449
+ grid-template-columns: repeat(7, 1fr);
450
+ grid-template-rows: auto repeat(6, 1fr);
451
+ }
452
+ </style>