picocrank 1.4.0 → 1.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "picocrank",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "A set of zero dependency Vue components built on the Femtocrank global CSS theme.",
5
5
  "main": "vite.config.js",
6
6
  "scripts": {
package/router.js CHANGED
@@ -4,6 +4,7 @@ import { HomeIcon } from '@hugeicons/core-free-icons';
4
4
  import { TableIcon } from '@hugeicons/core-free-icons';
5
5
  import { ViewIcon } from '@hugeicons/core-free-icons';
6
6
  import { SecurityValidationIcon } from '@hugeicons/core-free-icons';
7
+ import { CalendarIcon } from '@hugeicons/core-free-icons';
7
8
 
8
9
  const routes = [
9
10
  {
@@ -43,6 +44,16 @@ const routes = [
43
44
  title: 'Admin',
44
45
  icon: SecurityValidationIcon,
45
46
  }
47
+ },
48
+ {
49
+ name: 'CalendarExample',
50
+ path: '/calendar-example',
51
+ component: () => import('./vue/views/CalendarExample.vue'),
52
+ props: true,
53
+ meta: {
54
+ title: 'Calendar Example',
55
+ icon: CalendarIcon,
56
+ }
46
57
  }
47
58
  ];
48
59
 
@@ -0,0 +1,632 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed } from 'vue'
3
+
4
+ export interface CalendarEvent {
5
+ id: string | number
6
+ title: string
7
+ startDate?: Date | string | null
8
+ endDate?: Date | string | null
9
+ date?: Date | string | null
10
+ [key: string]: any
11
+ }
12
+
13
+ export interface CalendarProps {
14
+ events: CalendarEvent[]
15
+ monthNames?: string[]
16
+ dayNames?: string[]
17
+ loading?: boolean
18
+ error?: string | null
19
+ // Formatter functions
20
+ getEventDate?: (event: CalendarEvent) => Date | null
21
+ getEventDateRange?: (event: CalendarEvent) => { start: Date | null; end: Date | null }
22
+ formatEventTime?: (event: CalendarEvent, date: Date) => string
23
+ // Customization
24
+ showNavigation?: boolean
25
+ currentMonth?: number
26
+ currentYear?: number
27
+ }
28
+
29
+ const props = withDefaults(defineProps<CalendarProps>(), {
30
+ section: false,
31
+ monthNames: () => [
32
+ 'January', 'February', 'March', 'April', 'May', 'June',
33
+ 'July', 'August', 'September', 'October', 'November', 'December'
34
+ ],
35
+ dayNames: () => ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
36
+ loading: false,
37
+ error: null,
38
+ showNavigation: true,
39
+ })
40
+
41
+ const emit = defineEmits<{
42
+ 'event-click': [event: CalendarEvent]
43
+ 'date-click': [date: Date]
44
+ 'month-change': [month: number, year: number]
45
+ 'event-context-menu': [event: CalendarEvent, mouseEvent: MouseEvent]
46
+ }>()
47
+
48
+ // Internal calendar state
49
+ const internalCurrentDate = ref(new Date())
50
+ const currentDate = computed(() => {
51
+ if (props.currentMonth !== undefined && props.currentYear !== undefined) {
52
+ return new Date(props.currentYear, props.currentMonth, 1)
53
+ }
54
+ return internalCurrentDate.value
55
+ })
56
+
57
+ const currentMonth = computed(() => currentDate.value.getMonth())
58
+ const currentYear = computed(() => currentDate.value.getFullYear())
59
+
60
+ // Helper function to check if a time is midnight (00:00)
61
+ function isMidnight(dateValue: any): boolean {
62
+ if (!dateValue) return false
63
+ const date = new Date(dateValue)
64
+ return date.getHours() === 0 && date.getMinutes() === 0
65
+ }
66
+
67
+ // Helper function to format time, returning "No time" if midnight
68
+ function formatTimeOrNoTime(dateValue: any): string {
69
+ if (!dateValue) return 'No time'
70
+ if (isMidnight(dateValue)) return 'No time'
71
+ const date = new Date(dateValue)
72
+ return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
73
+ }
74
+
75
+ // Get event date range
76
+ function getEventDateRange(event: CalendarEvent): { start: Date | null; end: Date | null } {
77
+ if (props.getEventDateRange) {
78
+ return props.getEventDateRange(event)
79
+ }
80
+
81
+ // Default implementation
82
+ let start: Date | null = null
83
+ let end: Date | null = null
84
+
85
+ if (event.startDate) {
86
+ start = new Date(event.startDate)
87
+ } else if (event.date) {
88
+ start = new Date(event.date)
89
+ }
90
+
91
+ if (event.endDate) {
92
+ end = new Date(event.endDate)
93
+ }
94
+
95
+ return { start, end }
96
+ }
97
+
98
+ // Get events for a specific date
99
+ function getEventsForDate(date: Date): CalendarEvent[] {
100
+ const targetDate = new Date(date)
101
+ targetDate.setHours(0, 0, 0, 0)
102
+
103
+ return props.events.filter(event => {
104
+ const { start, end } = getEventDateRange(event)
105
+
106
+ if (!start) return false
107
+
108
+ const startDateOnly = new Date(start)
109
+ startDateOnly.setHours(0, 0, 0, 0)
110
+
111
+ if (end) {
112
+ const endDateOnly = new Date(end)
113
+ endDateOnly.setHours(0, 0, 0, 0)
114
+ return targetDate >= startDateOnly && targetDate <= endDateOnly
115
+ } else {
116
+ return targetDate.getTime() === startDateOnly.getTime()
117
+ }
118
+ })
119
+ }
120
+
121
+ // Check if an event is a multi-day event
122
+ function isMultiDayEvent(event: CalendarEvent): boolean {
123
+ const { start, end } = getEventDateRange(event)
124
+
125
+ if (!start || !end) return false
126
+
127
+ const startDate = new Date(start)
128
+ const endDate = new Date(end)
129
+
130
+ startDate.setHours(0, 0, 0, 0)
131
+ endDate.setHours(0, 0, 0, 0)
132
+
133
+ return startDate.getTime() !== endDate.getTime()
134
+ }
135
+
136
+ // Get the position of an event within a multi-day range for a specific date
137
+ function getMultiDayPosition(event: CalendarEvent, date: Date): 'start' | 'middle' | 'end' | 'single' {
138
+ const { start, end } = getEventDateRange(event)
139
+
140
+ if (!start || !end) return 'single'
141
+
142
+ const startDate = new Date(start)
143
+ const endDate = new Date(end)
144
+ const targetDate = new Date(date)
145
+
146
+ startDate.setHours(0, 0, 0, 0)
147
+ endDate.setHours(0, 0, 0, 0)
148
+ targetDate.setHours(0, 0, 0, 0)
149
+
150
+ if (startDate.getTime() === endDate.getTime()) return 'single'
151
+ if (targetDate.getTime() === startDate.getTime()) return 'start'
152
+ if (targetDate.getTime() === endDate.getTime()) return 'end'
153
+ if (targetDate > startDate && targetDate < endDate) return 'middle'
154
+
155
+ return 'single'
156
+ }
157
+
158
+ // Format event time based on position in multi-day event
159
+ function formatEventTimeDefault(event: CalendarEvent, date: Date): string {
160
+ if (props.formatEventTime) {
161
+ return props.formatEventTime(event, date)
162
+ }
163
+
164
+ const { start, end } = getEventDateRange(event)
165
+ const position = getMultiDayPosition(event, date)
166
+
167
+ if (!start || !end) return 'No time'
168
+
169
+ if (position === 'start') {
170
+ return formatTimeOrNoTime(start)
171
+ } else if (position === 'end') {
172
+ return formatTimeOrNoTime(end)
173
+ } else if (position === 'middle') {
174
+ return 'All day'
175
+ } else if (position === 'single') {
176
+ return formatTimeOrNoTime(start)
177
+ }
178
+
179
+ return 'All day'
180
+ }
181
+
182
+ // Calendar generation
183
+ const calendarDays = computed(() => {
184
+ const year = currentYear.value
185
+ const month = currentMonth.value
186
+
187
+ const firstDay = new Date(year, month, 1)
188
+ const lastDay = new Date(year, month + 1, 0)
189
+ const daysInMonth = lastDay.getDate()
190
+
191
+ // Get starting day of week (Monday as first day)
192
+ const startDay = (firstDay.getDay() + 6) % 7
193
+
194
+ const days = []
195
+
196
+ // Add days from previous month to fill the first row
197
+ const prevMonth = month === 0 ? 11 : month - 1
198
+ const prevYear = month === 0 ? year - 1 : year
199
+ const prevMonthLastDay = new Date(prevYear, prevMonth + 1, 0).getDate()
200
+
201
+ for (let i = 0; i < startDay; i++) {
202
+ const day = prevMonthLastDay - startDay + i + 1
203
+ const date = new Date(prevYear, prevMonth, day)
204
+ days.push({
205
+ date,
206
+ events: getEventsForDate(date)
207
+ })
208
+ }
209
+
210
+ // Add days of the month
211
+ for (let day = 1; day <= daysInMonth; day++) {
212
+ const date = new Date(year, month, day)
213
+ days.push({
214
+ date,
215
+ events: getEventsForDate(date)
216
+ })
217
+ }
218
+
219
+ // Add days from next month to complete the last row
220
+ const totalCells = 42
221
+ const remainingCells = totalCells - days.length
222
+
223
+ for (let day = 1; day <= remainingCells; day++) {
224
+ const date = new Date(year, month + 1, day)
225
+ days.push({
226
+ date,
227
+ events: getEventsForDate(date)
228
+ })
229
+ }
230
+
231
+ return days
232
+ })
233
+
234
+ // Navigation functions
235
+ function previousMonth() {
236
+ if (props.currentMonth !== undefined && props.currentYear !== undefined) {
237
+ const newMonth = currentMonth.value === 0 ? 11 : currentMonth.value - 1
238
+ const newYear = currentMonth.value === 0 ? currentYear.value - 1 : currentYear.value
239
+ emit('month-change', newMonth, newYear)
240
+ } else {
241
+ internalCurrentDate.value = new Date(currentYear.value, currentMonth.value - 1, 1)
242
+ }
243
+ }
244
+
245
+ function nextMonth() {
246
+ if (props.currentMonth !== undefined && props.currentYear !== undefined) {
247
+ const newMonth = currentMonth.value === 11 ? 0 : currentMonth.value + 1
248
+ const newYear = currentMonth.value === 11 ? currentYear.value + 1 : currentYear.value
249
+ emit('month-change', newMonth, newYear)
250
+ } else {
251
+ internalCurrentDate.value = new Date(currentYear.value, currentMonth.value + 1, 1)
252
+ }
253
+ }
254
+
255
+ function goToToday() {
256
+ if (props.currentMonth !== undefined && props.currentYear !== undefined) {
257
+ const today = new Date()
258
+ emit('month-change', today.getMonth(), today.getFullYear())
259
+ } else {
260
+ internalCurrentDate.value = new Date()
261
+ }
262
+ }
263
+
264
+ // Event handlers
265
+ function handleEventClick(event: CalendarEvent) {
266
+ emit('event-click', event)
267
+ }
268
+
269
+ function handleDateClick(date: Date) {
270
+ emit('date-click', date)
271
+ }
272
+
273
+ function handleContextMenu(event: CalendarEvent, mouseEvent: MouseEvent) {
274
+ mouseEvent.preventDefault()
275
+ mouseEvent.stopPropagation()
276
+ emit('event-context-menu', event, mouseEvent)
277
+ }
278
+ </script>
279
+
280
+ <template>
281
+ <div class="calendar-wrapper">
282
+ <div v-if="showNavigation" class="calendar-header-nav">
283
+ <h2 class="calendar-title">{{ monthNames[currentMonth] }} {{ currentYear }}</h2>
284
+ <div class="calendar-nav-buttons">
285
+ <slot name="nav-buttons">
286
+ <button @click="previousMonth" class="button neutral">‹</button>
287
+ <button @click="goToToday" class="button neutral">Today</button>
288
+ <button @click="nextMonth" class="button neutral">›</button>
289
+ </slot>
290
+ </div>
291
+ </div>
292
+
293
+ <div v-if="error" class="calendar-error">{{ error }}</div>
294
+ <div v-else-if="loading" class="calendar-loading">Loading…</div>
295
+ <div v-else class="calendar-container">
296
+ <div class="calendar-grid">
297
+ <!-- Day headers -->
298
+ <div v-for="day in dayNames" :key="day" class="day-header">{{ day }}</div>
299
+
300
+ <!-- Calendar days -->
301
+ <div
302
+ v-for="(day, index) in calendarDays"
303
+ :key="index"
304
+ class="calendar-day"
305
+ :class="{
306
+ 'today': day && day.date.toDateString() === new Date().toDateString(),
307
+ 'weekend': day && (day.date.getDay() === 0 || day.date.getDay() === 6),
308
+ 'prev-month': day && day.date.getMonth() !== currentMonth && day.date.getMonth() !== (currentMonth + 1) % 12,
309
+ 'next-month': day && day.date.getMonth() !== currentMonth && day.date.getMonth() === (currentMonth + 1) % 12
310
+ }"
311
+ >
312
+ <div v-if="day" class="day-content" @click="handleDateClick(day.date)">
313
+ <div class="day-number clickable">
314
+ {{ day.date.getDate() }}
315
+ </div>
316
+ <div class="day-events">
317
+ <div
318
+ v-for="event in day.events.sort((a, b) => {
319
+ const aMultiDay = isMultiDayEvent(a);
320
+ const bMultiDay = isMultiDayEvent(b);
321
+ if (aMultiDay && !bMultiDay) return -1;
322
+ if (!aMultiDay && bMultiDay) return 1;
323
+ return 0;
324
+ }).slice(0, 3)"
325
+ :key="event.id"
326
+ class="calendar-event"
327
+ :class="{
328
+ 'multi-day': isMultiDayEvent(event),
329
+ 'multi-day-start': isMultiDayEvent(event) && getMultiDayPosition(event, day.date) === 'start',
330
+ 'multi-day-middle': isMultiDayEvent(event) && getMultiDayPosition(event, day.date) === 'middle',
331
+ 'multi-day-end': isMultiDayEvent(event) && getMultiDayPosition(event, day.date) === 'end'
332
+ }"
333
+ @click.stop="handleEventClick(event)"
334
+ @contextmenu.stop="handleContextMenu(event, $event)"
335
+ >
336
+ <slot name="event" :event="event" :date="day.date" :position="getMultiDayPosition(event, day.date)">
337
+ <div class="event-content">
338
+ <div class="event-title">
339
+ {{ event.title }}
340
+ <span v-if="isMultiDayEvent(event)" class="multi-day-indicator">
341
+ {{ getMultiDayPosition(event, day.date) === 'start' ? '▶' :
342
+ getMultiDayPosition(event, day.date) === 'end' ? '◀' :
343
+ getMultiDayPosition(event, day.date) === 'middle' ? '▬' : '' }}
344
+ </span>
345
+ </div>
346
+ <div class="event-time">
347
+ {{ formatEventTimeDefault(event, day.date) }}
348
+ </div>
349
+ </div>
350
+ </slot>
351
+ </div>
352
+ <div v-if="day.events.length > 3" class="more-events">
353
+ +{{ day.events.length - 3 }} more
354
+ </div>
355
+ </div>
356
+ </div>
357
+ </div>
358
+ </div>
359
+ </div>
360
+ </div>
361
+ </template>
362
+
363
+ <style scoped>
364
+ .calendar-wrapper {
365
+ width: 100%;
366
+ }
367
+
368
+ .calendar-header-nav {
369
+ display: flex;
370
+ justify-content: space-between;
371
+ align-items: center;
372
+ margin-bottom: 1rem;
373
+ padding: 1rem;
374
+ background: #f8f9fa;
375
+ border-radius: 8px;
376
+ }
377
+
378
+ .calendar-title {
379
+ margin: 0;
380
+ font-size: 1.5rem;
381
+ font-weight: 600;
382
+ }
383
+
384
+ .calendar-nav-buttons {
385
+ display: flex;
386
+ align-items: center;
387
+ gap: 0.5rem;
388
+ }
389
+
390
+ .calendar-error {
391
+ color: #b00020;
392
+ padding: 1rem;
393
+ }
394
+
395
+ .calendar-loading {
396
+ padding: 1rem;
397
+ text-align: center;
398
+ }
399
+
400
+ .calendar-container {
401
+ background: white;
402
+ border: 1px solid #e0e0e0;
403
+ overflow: hidden;
404
+ border-radius: 8px;
405
+ }
406
+
407
+ .calendar-grid {
408
+ display: grid;
409
+ grid-template-columns: repeat(7, minmax(0, 1fr));
410
+ min-height: 400px;
411
+ }
412
+
413
+ .day-header {
414
+ padding: 1rem;
415
+ text-align: center;
416
+ font-weight: 600;
417
+ color: #666;
418
+ border-right: 1px solid #e0e0e0;
419
+ border-bottom: 1px solid #e0e0e0;
420
+ background: #f8f9fa;
421
+ }
422
+
423
+ .day-header:last-child {
424
+ border-right: none;
425
+ }
426
+
427
+ .calendar-day {
428
+ border-right: 1px solid #e0e0e0;
429
+ border-bottom: 1px solid #e0e0e0;
430
+ min-height: 120px;
431
+ position: relative;
432
+ transition: background-color 0.2s ease;
433
+ cursor: pointer;
434
+ }
435
+
436
+ .calendar-day:hover {
437
+ background-color: #f0f8ff;
438
+ }
439
+
440
+ .calendar-day:nth-child(7n) {
441
+ border-right: none;
442
+ }
443
+
444
+ .calendar-day.weekend {
445
+ background: #f8f9fa;
446
+ }
447
+
448
+ .calendar-day.weekend:hover {
449
+ background: #e9ecef;
450
+ }
451
+
452
+ .calendar-day.next-month,
453
+ .calendar-day.prev-month {
454
+ background: #f8f9fa;
455
+ opacity: 0.6;
456
+ }
457
+
458
+ .calendar-day.next-month:hover,
459
+ .calendar-day.prev-month:hover {
460
+ background: #e9ecef;
461
+ opacity: 0.8;
462
+ }
463
+
464
+ .calendar-day.today {
465
+ background: #f7f8d7;
466
+ font-weight: bold;
467
+ }
468
+
469
+ .calendar-day.today:hover {
470
+ background: #bbdefb;
471
+ }
472
+
473
+
474
+ .day-content {
475
+ height: 100%;
476
+ display: flex;
477
+ flex-direction: column;
478
+ }
479
+
480
+ .day-number {
481
+ font-size: 0.9rem;
482
+ margin-bottom: 0.25rem;
483
+ color: #333;
484
+ text-decoration: none;
485
+ display: inline-block;
486
+ padding: 0.1rem;
487
+ border-radius: 4px;
488
+ transition: all 0.2s;
489
+ min-width: 1.5rem;
490
+ text-align: center;
491
+ }
492
+
493
+ .day-number.clickable {
494
+ cursor: pointer;
495
+ padding: 0.25rem;
496
+ border-radius: 4px;
497
+ transition: background-color 0.2s;
498
+ }
499
+
500
+ .day-content:hover .day-number.clickable {
501
+ color: #007bff;
502
+ }
503
+
504
+ .day-events {
505
+ flex: 1;
506
+ overflow: hidden;
507
+ }
508
+
509
+ .calendar-event {
510
+ background: #d6f1aa;
511
+ border: 1px solid #c4db96;
512
+ margin-bottom: 0.25rem;
513
+ cursor: pointer;
514
+ transition: all 0.2s;
515
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
516
+ padding: 0.1rem 0.1rem;
517
+ }
518
+
519
+ .calendar-event:hover {
520
+ background: #b3de6e;
521
+ border-color: #c4db96;
522
+ transform: translateY(-1px);
523
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
524
+ }
525
+
526
+ .calendar-event.multi-day-start {
527
+ border-top-left-radius: 4px;
528
+ border-bottom-left-radius: 4px;
529
+ }
530
+
531
+ .calendar-event.multi-day-middle {
532
+ border-radius: 0;
533
+ }
534
+
535
+ .calendar-event.multi-day-end {
536
+ border-top-right-radius: 4px;
537
+ border-bottom-right-radius: 4px;
538
+ }
539
+
540
+ .event-content {
541
+ cursor: pointer;
542
+ }
543
+
544
+ .event-title {
545
+ font-weight: bold;
546
+ font-size: 0.85rem;
547
+ color: #333;
548
+ white-space: normal;
549
+ overflow: hidden;
550
+ text-overflow: ellipsis;
551
+ display: flex;
552
+ align-items: center;
553
+ gap: 0.25rem;
554
+ }
555
+
556
+ .multi-day-indicator {
557
+ font-size: 0.7rem;
558
+ color: #007bff;
559
+ font-weight: bold;
560
+ }
561
+
562
+ .event-time {
563
+ font-size: 0.75rem;
564
+ color: #666;
565
+ margin-top: 0.125rem;
566
+ }
567
+
568
+ .more-events {
569
+ font-size: 0.75rem;
570
+ color: #666;
571
+ text-align: center;
572
+ padding: 0.25rem;
573
+ background: #f8f9fa;
574
+ border-radius: 4px;
575
+ margin-top: 0.25rem;
576
+ }
577
+
578
+ /* Responsive design */
579
+ @media (max-width: 768px) {
580
+ .calendar-header-nav {
581
+ flex-wrap: wrap;
582
+ gap: 0.5rem;
583
+ }
584
+
585
+ .calendar-title {
586
+ font-size: 1.2rem;
587
+ }
588
+
589
+ .calendar-day {
590
+ min-height: 80px;
591
+ }
592
+
593
+ .day-number {
594
+ font-size: 1rem;
595
+ }
596
+
597
+ .event-title {
598
+ font-size: 0.8rem;
599
+ }
600
+
601
+ .event-time {
602
+ font-size: 0.7rem;
603
+ }
604
+ }
605
+
606
+ @media (max-width: 480px) {
607
+ .calendar-grid {
608
+ min-height: 300px;
609
+ }
610
+
611
+ .calendar-day {
612
+ min-height: 60px;
613
+ }
614
+
615
+ .day-header {
616
+ padding: 0.5rem 0.25rem;
617
+ font-size: 0.8rem;
618
+ }
619
+ }
620
+
621
+ @media (min-width: 768px) {
622
+ .day-content {
623
+ padding: 0.45rem;
624
+ }
625
+
626
+ .calendar-event {
627
+ border-radius: 4px;
628
+ padding: 0.25rem 0.5rem;
629
+ }
630
+ }
631
+ </style>
632
+
package/vue/views/App.vue CHANGED
@@ -45,6 +45,7 @@
45
45
  onMounted(() => {
46
46
  sidebar.value.addRouterLink('Welcome')
47
47
  sidebar.value.addRouterLink('TableExample')
48
+ sidebar.value.addRouterLink('CalendarExample')
48
49
  sidebar.value.addSeparator('separator-1');
49
50
  sidebar.value.addRouterLink('ViewItem', { id: 1 })
50
51
  sidebar.value.addSeparator('separator-2');
@@ -0,0 +1,328 @@
1
+ <template>
2
+ <Section
3
+ title="Example Calendar"
4
+ padding
5
+ >
6
+ <template #toolbar>
7
+ <button @click="goToToday">Today</button>
8
+ <button @click="previousMonth">‹</button>
9
+ <div class="date-picker-container">
10
+ <input
11
+ id="goto-date"
12
+ type="month"
13
+ v-model="selectedDate"
14
+ @change="goToSelectedDate"
15
+ class="date-picker-input"
16
+ />
17
+ </div>
18
+ <button @click="nextMonth">›</button>
19
+ </template>
20
+ <Calendar
21
+ :section="true"
22
+ :events="events"
23
+ :loading="loading"
24
+ :error="error"
25
+ :show-navigation="false"
26
+ :current-month="currentMonthIndex"
27
+ :current-year="currentYear"
28
+ @event-click="handleEventClick"
29
+ @date-click="handleDateClick"
30
+ @month-change="handleMonthChange"
31
+ />
32
+ </Section>
33
+ </template>
34
+
35
+ <script setup>
36
+ import { ref, onMounted } from 'vue'
37
+ import Calendar from '../components/Calendar.vue'
38
+ import Section from '../components/Section.vue'
39
+
40
+ const events = ref([])
41
+ const loading = ref(false)
42
+ const error = ref(null)
43
+ const currentMonth = ref('?')
44
+ const currentMonthIndex = ref(new Date().getMonth())
45
+ const currentYear = ref(new Date().getFullYear())
46
+
47
+ // Date picker state - using native HTML5 month input
48
+ const selectedDate = ref('')
49
+
50
+ // Generate dummy events for the current month
51
+ function generateDummyEvents() {
52
+ const now = new Date()
53
+ const currentMonth = now.getMonth()
54
+ const currentYear = now.getFullYear()
55
+
56
+ // Get the number of days in the current month
57
+ const daysInMonth = new Date(currentYear, currentMonth + 1, 0).getDate()
58
+
59
+ const dummyEvents = []
60
+
61
+ // Add some recurring events
62
+ for (let day = 1; day <= daysInMonth; day++) {
63
+ const date = new Date(currentYear, currentMonth, day)
64
+ const dayOfWeek = date.getDay()
65
+
66
+ // Add weekend events (Saturday and Sunday)
67
+ if (dayOfWeek === 0 || dayOfWeek === 6) {
68
+ dummyEvents.push({
69
+ id: `weekend-${day}`,
70
+ title: 'Weekend Activity',
71
+ date: new Date(currentYear, currentMonth, day, 10, 0),
72
+ description: 'Relaxing weekend activity'
73
+ })
74
+ }
75
+
76
+ // Add some random events
77
+ if (Math.random() > 0.7) {
78
+ const eventTypes = [
79
+ { title: 'Team Meeting', time: [9, 0], duration: 1 },
80
+ { title: 'Client Call', time: [14, 30], duration: 0.5 },
81
+ { title: 'Project Review', time: [16, 0], duration: 2 },
82
+ { title: 'Lunch Break', time: [12, 0], duration: 1 },
83
+ { title: 'Code Review', time: [10, 0], duration: 1.5 }
84
+ ]
85
+
86
+ const eventType = eventTypes[Math.floor(Math.random() * eventTypes.length)]
87
+ const [hours, minutes] = eventType.time
88
+
89
+ dummyEvents.push({
90
+ id: `event-${day}-${Math.random().toString(36).substr(2, 9)}`,
91
+ title: eventType.title,
92
+ date: new Date(currentYear, currentMonth, day, hours, minutes),
93
+ description: `Scheduled ${eventType.title.toLowerCase()}`
94
+ })
95
+ }
96
+ }
97
+
98
+ // Add some multi-day events
99
+ dummyEvents.push({
100
+ id: 'conference-1',
101
+ title: 'Tech Conference',
102
+ startDate: new Date(currentYear, currentMonth, 5, 9, 0),
103
+ endDate: new Date(currentYear, currentMonth, 7, 17, 0),
104
+ description: 'Annual technology conference'
105
+ })
106
+
107
+ dummyEvents.push({
108
+ id: 'vacation-1',
109
+ title: 'Vacation',
110
+ startDate: new Date(currentYear, currentMonth, 15, 0, 0),
111
+ endDate: new Date(currentYear, currentMonth, 18, 23, 59),
112
+ description: 'Family vacation time'
113
+ })
114
+
115
+ // Add some all-day events
116
+ dummyEvents.push({
117
+ id: 'holiday-1',
118
+ title: 'Public Holiday',
119
+ date: new Date(currentYear, currentMonth, 1, 0, 0),
120
+ description: 'National holiday - office closed'
121
+ })
122
+
123
+ if (daysInMonth >= 25) {
124
+ dummyEvents.push({
125
+ id: 'deadline-1',
126
+ title: 'Project Deadline',
127
+ date: new Date(currentYear, currentMonth, 25, 0, 0),
128
+ description: 'Important project deadline'
129
+ })
130
+ }
131
+
132
+ return dummyEvents
133
+ }
134
+
135
+ function handleEventClick(event) {
136
+ console.log('Event clicked:', event)
137
+ alert(`Event: ${event.title}\nDescription: ${event.description || 'No description'}`)
138
+ }
139
+
140
+ function handleDateClick(date) {
141
+ console.log('Date clicked:', date)
142
+ alert(`Date selected: ${date.toLocaleDateString()}`)
143
+ }
144
+
145
+ // Helper function to update the date picker value
146
+ function updateDatePicker(monthIndex, year) {
147
+ const month = String(monthIndex + 1).padStart(2, '0')
148
+ selectedDate.value = `${year}-${month}`
149
+ }
150
+
151
+ // Navigation functions - using the same fast path as internal calendar navigation
152
+ function previousMonth() {
153
+ const newMonth = currentMonthIndex.value === 0 ? 11 : currentMonthIndex.value - 1
154
+ const newYear = currentMonthIndex.value === 0 ? currentYear.value - 1 : currentYear.value
155
+
156
+ // Update state directly (fast path)
157
+ currentMonthIndex.value = newMonth
158
+ currentYear.value = newYear
159
+
160
+ // Update title
161
+ const date = new Date(newYear, newMonth, 1)
162
+ currentMonth.value = date.toLocaleDateString('en-US', {
163
+ month: 'long',
164
+ year: 'numeric'
165
+ })
166
+
167
+ // Update date picker
168
+ updateDatePicker(newMonth, newYear)
169
+ }
170
+
171
+ function nextMonth() {
172
+ const newMonth = currentMonthIndex.value === 11 ? 0 : currentMonthIndex.value + 1
173
+ const newYear = currentMonthIndex.value === 11 ? currentYear.value + 1 : currentYear.value
174
+
175
+ // Update state directly (fast path)
176
+ currentMonthIndex.value = newMonth
177
+ currentYear.value = newYear
178
+
179
+ // Update title
180
+ const date = new Date(newYear, newMonth, 1)
181
+ currentMonth.value = date.toLocaleDateString('en-US', {
182
+ month: 'long',
183
+ year: 'numeric'
184
+ })
185
+
186
+ // Update date picker
187
+ updateDatePicker(newMonth, newYear)
188
+ }
189
+
190
+ function goToToday() {
191
+ const now = new Date()
192
+ const newMonth = now.getMonth()
193
+ const newYear = now.getFullYear()
194
+
195
+ // Update state directly (fast path)
196
+ currentMonthIndex.value = newMonth
197
+ currentYear.value = newYear
198
+
199
+ // Update title
200
+ currentMonth.value = now.toLocaleDateString('en-US', {
201
+ month: 'long',
202
+ year: 'numeric'
203
+ })
204
+
205
+ // Update date picker
206
+ updateDatePicker(newMonth, newYear)
207
+ }
208
+
209
+ function goToSelectedDate() {
210
+ if (!selectedDate.value) return
211
+
212
+ // Parse the YYYY-MM format from the native date picker
213
+ const [year, month] = selectedDate.value.split('-').map(Number)
214
+ const monthIndex = month - 1 // Convert to 0-based month index
215
+
216
+ // Update state directly (fast path)
217
+ currentMonthIndex.value = monthIndex
218
+ currentYear.value = year
219
+
220
+ // Update title
221
+ const date = new Date(year, monthIndex, 1)
222
+ currentMonth.value = date.toLocaleDateString('en-US', {
223
+ month: 'long',
224
+ year: 'numeric'
225
+ })
226
+ }
227
+
228
+ function handleMonthChange(month, year) {
229
+ console.log('Month changed:', month, year)
230
+ // In a real app, you might fetch events for the new month
231
+ loading.value = true
232
+
233
+ // Update current month and year state
234
+ currentMonthIndex.value = month
235
+ currentYear.value = year
236
+
237
+ // Use native JavaScript to format month and year
238
+ const date = new Date(year, month, 1)
239
+ currentMonth.value = date.toLocaleDateString('en-US', {
240
+ month: 'long',
241
+ year: 'numeric'
242
+ })
243
+
244
+ // Update date picker to reflect the new month
245
+ updateDatePicker(month, year)
246
+
247
+ setTimeout(() => {
248
+ // Simulate loading delay
249
+ events.value = generateDummyEvents()
250
+ loading.value = false
251
+ }, 500)
252
+ }
253
+
254
+ onMounted(() => {
255
+ // Generate initial events for current month
256
+ events.value = generateDummyEvents()
257
+
258
+ // Set initial month title using native JavaScript
259
+ const now = new Date()
260
+ currentMonthIndex.value = now.getMonth()
261
+ currentYear.value = now.getFullYear()
262
+ currentMonth.value = now.toLocaleDateString('en-US', {
263
+ month: 'long',
264
+ year: 'numeric'
265
+ })
266
+
267
+ // Initialize the native date picker with current month/year
268
+ const year = now.getFullYear()
269
+ const month = String(now.getMonth() + 1).padStart(2, '0')
270
+ selectedDate.value = `${year}-${month}`
271
+ })
272
+ </script>
273
+
274
+ <style scoped>
275
+ .date-picker-container {
276
+ display: flex;
277
+ align-items: center;
278
+ gap: 0.5rem;
279
+ }
280
+
281
+ .date-picker-label {
282
+ font-size: 0.875rem;
283
+ font-weight: 500;
284
+ color: #374151;
285
+ }
286
+
287
+ .date-picker-input {
288
+ padding: 0.5rem;
289
+ border: 1px solid #d1d5db;
290
+ border-radius: 4px;
291
+ font-size: 0.875rem;
292
+ background: white;
293
+ cursor: pointer;
294
+ transition: all 0.2s;
295
+ }
296
+
297
+ .date-picker-input:hover {
298
+ border-color: #9ca3af;
299
+ }
300
+
301
+ .date-picker-input:focus {
302
+ outline: none;
303
+ border-color: #3b82f6;
304
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
305
+ }
306
+
307
+ /* Dark theme support */
308
+ @media (prefers-color-scheme: dark) {
309
+ .date-picker-label {
310
+ color: #d1d5db;
311
+ }
312
+
313
+ .date-picker-input {
314
+ background: #374151;
315
+ border-color: #4b5563;
316
+ color: #f9fafb;
317
+ }
318
+
319
+ .date-picker-input:hover {
320
+ border-color: #6b7280;
321
+ }
322
+
323
+ .date-picker-input:focus {
324
+ border-color: #60a5fa;
325
+ box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.1);
326
+ }
327
+ }
328
+ </style>