refactor: code clean up

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-07-06 21:09:07 -04:00
parent dea5a61811
commit df531908f3
19 changed files with 594 additions and 643 deletions
+53 -145
View File
@@ -1,79 +1,30 @@
<template>
<div class="agenda-view">
<div class="view-controls">
<div class="view-navigation">
<v-btn size="x-small" variant="text" icon="mdi-chevron-left" @click="previousPeriod" />
<v-btn size="x-small" variant="tonal" @click="goToToday">Today</v-btn>
<v-btn size="x-small" variant="text" icon="mdi-chevron-right" @click="nextPeriod" />
<span class="current-period">{{ dateRangeLabel }}</span>
</div>
<v-spacer />
<div class="view-selector">
<v-btn
v-for="span in AGENDA_VIEW_SPANS"
:key="span"
size="x-small"
variant="text"
@click="setSpan(span)"
:color="selectedSpan === span ? 'primary' : undefined"
>
{{ span.toUpperCase() }}
</v-btn>
</div>
</div>
<v-list class="agenda-list">
<template v-if="Object.keys(groupedEvents).length === 0">
<v-list-item>
<v-list-item-title class="text-center text-medium-emphasis">No events in this period</v-list-item-title>
</v-list-item>
</template>
<template v-for="(dayInstances, date) in groupedEvents" :key="date">
<v-list-subheader>{{ formatAgendaDate(date) }}</v-list-subheader>
<v-list-item
v-for="instance in dayInstances"
:key="instance.key"
@click="$emit('event-click', instance.entity)"
>
<template #prepend>
<v-icon :color="getEventColor(instance)">mdi-circle</v-icon>
</template>
<v-list-item-title>{{ instance.label || 'Untitled' }}</v-list-item-title>
<v-list-item-subtitle>
{{ instance.timeless ? 'All day' : `${formatInstanceTime(instance.startMs)} - ${formatInstanceTime(instance.endMs)}` }}
</v-list-item-subtitle>
</v-list-item>
</template>
</v-list>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import { AGENDA_VIEW_SPANS, spanToDays, type AgendaViewSpan } from '@/types/spans';
import type { EntityObject } from '@ChronoManager/models/entity';
import type { CollectionObject } from '@ChronoManager/models/collection';
import { daysVisibleRange, shiftLocalDays } from '@/utils/date';
import { daysVisibleRange } from '@/utils/date';
import { useChronoInstancesStore } from '@/stores/chronoInstancesStore';
import type { CalendarInstance } from '@/types/instance';
type CalendarEntity = EntityObject;
type CalendarCollection = CollectionObject;
import ViewControls from './ViewControls.vue';
import { formatEpochTime, formatFullDate } from '@/utils/format';
const props = defineProps<{
calendars: CalendarCollection[];
calendars: CollectionObject[];
currentDate: Date;
initialSpan?: AgendaViewSpan;
}>();
const emit = defineEmits<{
'event-click': [event: EntityObject];
'update:span': [span: AgendaViewSpan];
'update:current-date': [date: Date];
}>();
const instancesStore = useChronoInstancesStore();
const selectedSpan = ref<AgendaViewSpan>(props.initialSpan ?? '1w');
const emit = defineEmits<{
'event-click': [event: CalendarEntity];
'update:span': [span: AgendaViewSpan];
'update:current-date': [date: Date];
}>();
const selectedSpanDays = computed(() => spanToDays(selectedSpan.value));
watch(() => props.initialSpan, (newSpan) => {
if (newSpan && newSpan !== selectedSpan.value) {
@@ -86,58 +37,25 @@ function setSpan(span: AgendaViewSpan) {
emit('update:span', span);
}
function getSpanDays(): number {
return spanToDays(selectedSpan.value);
}
function previousPeriod() {
emit('update:current-date', shiftLocalDays(props.currentDate, -getSpanDays()));
}
function nextPeriod() {
emit('update:current-date', shiftLocalDays(props.currentDate, getSpanDays()));
}
function goToToday() {
emit('update:current-date', new Date());
}
const dateRange = computed(() => {
const range = daysVisibleRange(props.currentDate, getSpanDays());
const range = daysVisibleRange(props.currentDate, selectedSpanDays.value);
return {
start: new Date(range.startMs),
end: new Date(range.endMs),
start: new Date(range.start),
end: new Date(range.end),
};
});
const dateRangeLabel = computed(() => {
const { start, end: exclusiveEnd } = dateRange.value;
const end = new Date(exclusiveEnd);
end.setDate(end.getDate() - 1);
const formatOptions: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric' };
if (getSpanDays() === 1) {
return start.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
}
if (start.getMonth() === end.getMonth()) {
return `${start.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} - ${end.getDate()}, ${start.getFullYear()}`;
}
return `${start.toLocaleDateString('en-US', formatOptions)} - ${end.toLocaleDateString('en-US', formatOptions)}, ${end.getFullYear()}`;
});
const groupedEvents = computed(() => {
const grouped: Record<string, CalendarInstance[]> = {};
const { start, end } = dateRange.value;
const instances = instancesStore.instancesForRange({
startMs: start.getTime(),
endMs: end.getTime(),
start: start.getTime(),
end: end.getTime(),
});
instances.forEach(instance => {
const dateKey = new Date(instance.startMs).toDateString();
const dateKey = new Date(instance.start).toDateString();
if (!grouped[dateKey]) {
grouped[dateKey] = [];
}
@@ -153,26 +71,44 @@ function getEventColor(instance: CalendarInstance): string {
const calendar = props.calendars.find(cal => cal.identifier === entity.collection);
return calendar?.properties?.color || '#1976D2';
}
function formatTime(date: Date): string {
return date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
}
function formatInstanceTime(timestamp: number): string {
return formatTime(new Date(timestamp));
}
function formatAgendaDate(dateString: string): string {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
});
}
</script>
<template>
<div class="agenda-view">
<ViewControls
mode="days"
:current-date="currentDate"
:spans="AGENDA_VIEW_SPANS"
:selected-span="selectedSpan"
@update:current-date="emit('update:current-date', $event)"
@update:span="setSpan"
/>
<v-list class="agenda-list">
<template v-if="Object.keys(groupedEvents).length === 0">
<v-list-item>
<v-list-item-title class="text-center text-medium-emphasis">No events in this period</v-list-item-title>
</v-list-item>
</template>
<template v-for="(dayInstances, date) in groupedEvents" :key="date">
<v-list-subheader>{{ formatFullDate(date) }}</v-list-subheader>
<v-list-item
v-for="instance in dayInstances"
:key="instance.key"
@click="$emit('event-click', instance.entity)"
>
<template #prepend>
<v-icon :color="getEventColor(instance)">mdi-circle</v-icon>
</template>
<v-list-item-title>{{ instance.label || 'Untitled' }}</v-list-item-title>
<v-list-item-subtitle>
{{ instance.timeless ? 'All day' : `${formatEpochTime(instance.start)} - ${formatEpochTime(instance.end)}` }}
</v-list-item-subtitle>
</v-list-item>
</template>
</v-list>
</div>
</template>
<style scoped>
.agenda-view {
display: flex;
@@ -180,34 +116,6 @@ function formatAgendaDate(dateString: string): string {
height: 100%;
}
.view-controls {
display: flex;
align-items: center;
padding: 4px 8px;
border-bottom: 1px solid rgb(var(--v-border-color));
background-color: rgb(var(--v-theme-surface));
flex-shrink: 0;
}
.view-navigation {
display: flex;
align-items: center;
gap: 4px;
}
.current-period {
font-size: 14px;
font-weight: 500;
margin-left: 8px;
white-space: nowrap;
}
.view-selector {
display: flex;
align-items: center;
gap: 2px;
}
.agenda-list {
flex: 1;
overflow-y: auto;
+56 -72
View File
@@ -1,3 +1,59 @@
<script setup lang="ts">
import { ref } from 'vue';
import MonthView from './MonthView.vue';
import DaysView from './DaysView.vue';
import AgendaView from './AgendaView.vue';
import EventViewerPopup from './EventViewerPopup.vue';
import {
type AgendaViewSpan,
type DaysViewSpan
} from '@/types/spans';
import type { EntityObject } from '@ChronoManager/models/entity';
import type { CollectionObject } from '@ChronoManager/models/collection';
const props = defineProps<{
view: 'days' | 'month' | 'agenda';
currentDate: Date;
calendars: CollectionObject[];
initialDaysSpan?: DaysViewSpan;
initialAgendaViewSpan?: AgendaViewSpan;
}>();
const emit = defineEmits<{
'event-click': [event: EntityObject];
'date-click': [date: Date];
'update:current-date': [date: Date];
'update:days-span': [span: DaysViewSpan];
'update:agenda-span': [span: AgendaViewSpan];
}>();
// Popup state
const hoveredEvent = ref<EntityObject | null>(null);
const popupPosition = ref({ x: 0, y: 0 });
const showPopup = ref(false);
let hideTimeout: ReturnType<typeof setTimeout> | null = null;
function handleEventHover(data: { event: MouseEvent; entity: EntityObject }) {
if (hideTimeout) {
clearTimeout(hideTimeout);
hideTimeout = null;
}
hoveredEvent.value = data.entity;
popupPosition.value = {
x: data.event.clientX,
y: data.event.clientY
};
showPopup.value = true;
}
function hidePopup() {
hideTimeout = setTimeout(() => {
showPopup.value = false;
hoveredEvent.value = null;
}, 200);
}
</script>
<template>
<div class="calendar-view-container">
<EventViewerPopup
@@ -40,78 +96,6 @@
</div>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import MonthView from './MonthView.vue';
import DaysView from './DaysView.vue';
import AgendaView from './AgendaView.vue';
import EventViewerPopup from './EventViewerPopup.vue';
import { spanToDays, type AgendaViewSpan, type DaysViewSpan } from '@/types/spans';
import type { EntityObject } from '@ChronoManager/models/entity';
import type { CollectionObject } from '@ChronoManager/models/collection';
import type { VisibleDateRange } from '@/types';
import { daysVisibleRange, monthGridVisibleRange } from '@/utils/date.ts';
const props = defineProps<{
view: 'days' | 'month' | 'agenda';
currentDate: Date;
calendars: CollectionObject[];
initialDaysSpan?: DaysViewSpan;
initialAgendaViewSpan?: AgendaViewSpan;
}>();
const emit = defineEmits<{
'event-click': [event: EntityObject];
'date-click': [date: Date];
'update:days-span': [span: DaysViewSpan];
'update:agenda-span': [span: AgendaViewSpan];
'update:current-date': [date: Date];
'update:visible-range': [range: VisibleDateRange];
}>();
const visibleRange = computed(() => {
if (props.view === 'month') {
return monthGridVisibleRange(props.currentDate);
}
const span = props.view === 'agenda'
? props.initialAgendaViewSpan ?? '1w'
: props.initialDaysSpan ?? '7d';
return daysVisibleRange(props.currentDate, spanToDays(span));
});
watch(visibleRange, range => {
emit('update:visible-range', range);
}, { immediate: true });
// Popup state
const hoveredEvent = ref<EntityObject | null>(null);
const popupPosition = ref({ x: 0, y: 0 });
const showPopup = ref(false);
let hideTimeout: ReturnType<typeof setTimeout> | null = null;
function handleEventHover(data: { event: MouseEvent; entity: EntityObject }) {
if (hideTimeout) {
clearTimeout(hideTimeout);
hideTimeout = null;
}
hoveredEvent.value = data.entity;
popupPosition.value = {
x: data.event.clientX,
y: data.event.clientY
};
showPopup.value = true;
}
function hidePopup() {
hideTimeout = setTimeout(() => {
showPopup.value = false;
hoveredEvent.value = null;
}, 200);
}
</script>
<style scoped>
.calendar-view-container {
height: 100%;
+142 -233
View File
@@ -1,26 +1,146 @@
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import { layoutMultiDayLanes, type MultiDaySegment } from '@/utils/multiDayLanes';
import { shiftLocalDays, startOfLocalDay } from '@/utils/date';
import { DAYS_VIEW_SPANS, spanToDays, type DaysViewSpan } from '@/types/spans';
import type { EntityObject } from '@ChronoManager/models/entity';
import type { CollectionObject } from '@ChronoManager/models/collection';
import { useChronoInstancesStore } from '@/stores/chronoInstancesStore';
import type { CalendarInstance } from '@/types/instance';
import ViewControls from './ViewControls.vue';
import { formatEpochTime, formatHour, formatWeekDay } from '@/utils/format';
const ALL_DAY_EVENT_HEIGHT = 24;
const props = defineProps<{
currentDate: Date;
calendars: CollectionObject[];
initialSpan?: DaysViewSpan;
}>();
const instancesStore = useChronoInstancesStore();
const selectedSpan = ref<DaysViewSpan>(props.initialSpan ?? '7d');
const daysCount = computed(() => spanToDays(selectedSpan.value));
const emit = defineEmits<{
'event-click': [event: EntityObject];
'event-hover': [data: { event: MouseEvent; entity: EntityObject }];
'event-hover-end': [];
'date-click': [date: Date];
'update:span': [span: DaysViewSpan];
'update:current-date': [date: Date];
}>();
watch(() => props.initialSpan, (newSpan) => {
if (newSpan && newSpan !== selectedSpan.value) {
selectedSpan.value = newSpan;
}
});
function setSpan(span: DaysViewSpan) {
selectedSpan.value = span;
emit('update:span', span);
}
const visibleDates = computed(() => {
const dates: Date[] = [];
const start = new Date(props.currentDate);
for (let i = 0; i < daysCount.value; i++) {
dates.push(new Date(start));
start.setDate(start.getDate() + 1);
}
return dates;
});
// Compute all-day/multi-day event segments
const allDaySegments = computed(() => {
if (visibleDates.value.length === 0) {
return { segments: [], laneCount: 0 };
}
const rangeStart = startOfLocalDay(visibleDates.value[0]);
const rangeEnd = startOfLocalDay(visibleDates.value[visibleDates.value.length - 1]);
const getColumnIndex = (date: Date): number => {
const targetDay = startOfLocalDay(date);
return visibleDates.value.findIndex(d => startOfLocalDay(d).getTime() === targetDay.getTime());
};
const instances = instancesStore.instancesForRange({
start: rangeStart.getTime(),
end: shiftLocalDays(rangeEnd, 1).getTime(),
});
return layoutMultiDayLanes(instances, rangeStart, rangeEnd, daysCount.value, getColumnIndex);
});
function isToday(date: Date): boolean {
const today = new Date();
return date.toDateString() === today.toDateString();
}
function getTimedEvents(date: Date): CalendarInstance[] {
return instancesStore.instancesForDay(date).filter(instance => !instance.timeless);
}
function getEventColor(instance: CalendarInstance): string {
if (instance.color) return instance.color;
const calendar = props.calendars.find(cal => cal.identifier === instance.entity.collection);
return calendar?.properties?.color || '#1976D2';
}
function getAllDayEventStyle(segment: MultiDaySegment) {
return {
backgroundColor: getEventColor(segment.instance),
left: `calc(${segment.startCol} / ${daysCount.value} * 100% + 4px)`,
width: `calc(${segment.span} / ${daysCount.value} * 100% - 8px)`,
top: `${segment.lane * ALL_DAY_EVENT_HEIGHT}px`,
};
}
function getEventStyle(instance: CalendarInstance) {
const startTime = new Date(instance.start);
const start = startTime.getHours() * 60 + startTime.getMinutes();
const duration = instance.duration / (1000 * 60);
return {
top: `${(start / 1440) * 100}%`,
height: `${(duration / 1440) * 100}%`,
backgroundColor: getEventColor(instance),
};
}
function handleDayClick(event: MouseEvent, day: Date) {
const target = event.currentTarget as HTMLElement;
const rect = target.getBoundingClientRect();
const clickY = event.clientY - rect.top;
// Calculate the hour based on click position (60px per hour)
const totalMinutes = Math.floor((clickY / 60) * 60);
const hours = Math.floor(totalMinutes / 60);
const minutes = Math.round((totalMinutes % 60) / 15) * 15; // Round to nearest 15 min
// Create a new date with the clicked time
const clickedDate = new Date(day);
clickedDate.setHours(hours, minutes, 0, 0);
emit('date-click', clickedDate);
}
</script>
<template>
<div class="days-view">
<div class="view-controls">
<div class="view-navigation">
<v-btn size="x-small" variant="text" icon="mdi-chevron-left" @click="previousPeriod" />
<v-btn size="x-small" variant="tonal" @click="goToToday">Today</v-btn>
<v-btn size="x-small" variant="text" icon="mdi-chevron-right" @click="nextPeriod" />
<span class="current-period">{{ dateRangeLabel }}</span>
</div>
<v-spacer />
<div class="view-selector">
<v-btn
v-for="span in DAYS_VIEW_SPANS"
:key="span"
size="x-small"
variant="text"
@click="setSpan(span)"
:color="selectedSpan === span ? 'primary' : undefined"
>
{{ span.toUpperCase() }}
</v-btn>
</div>
</div>
<ViewControls
mode="days"
:current-date="currentDate"
:spans="DAYS_VIEW_SPANS"
:selected-span="selectedSpan"
@update:current-date="emit('update:current-date', $event)"
@update:span="setSpan"
/>
<div class="days-grid">
<!-- Fixed Headers Row -->
<div class="day-headers">
@@ -83,7 +203,7 @@
>
<template v-if="daysCount <= 3">
<div class="event-time">
{{ formatInstanceTime(instance.startMs) }} - {{ formatInstanceTime(instance.endMs) }}
{{ formatEpochTime(instance.start) }} - {{ formatEpochTime(instance.end) }}
</div>
<div class="event-title">{{ instance.label || 'Untitled' }}</div>
</template>
@@ -99,189 +219,6 @@
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import { layoutMultiDayLanes, type MultiDaySegment } from '@/utils/multiDayLanes';
import { shiftLocalDays, startOfLocalDay } from '@/utils/date';
import { DAYS_VIEW_SPANS, spanToDays, type DaysViewSpan } from '@/types/spans';
import type { EntityObject } from '@ChronoManager/models/entity';
import type { CollectionObject } from '@ChronoManager/models/collection';
import { useChronoInstancesStore } from '@/stores/chronoInstancesStore';
import type { CalendarInstance } from '@/types/instance';
const ALL_DAY_EVENT_HEIGHT = 24;
type CalendarEntity = EntityObject;
type CalendarCollection = CollectionObject;
const props = defineProps<{
currentDate: Date;
calendars: CalendarCollection[];
initialSpan?: DaysViewSpan;
}>();
const instancesStore = useChronoInstancesStore();
const selectedSpan = ref<DaysViewSpan>(props.initialSpan ?? '7d');
const daysCount = computed(() => spanToDays(selectedSpan.value));
const emit = defineEmits<{
'event-click': [event: CalendarEntity];
'event-hover': [data: { event: MouseEvent; entity: CalendarEntity }];
'event-hover-end': [];
'date-click': [date: Date];
'update:span': [span: DaysViewSpan];
'update:current-date': [date: Date];
}>();
watch(() => props.initialSpan, (newSpan) => {
if (newSpan && newSpan !== selectedSpan.value) {
selectedSpan.value = newSpan;
}
});
function setSpan(span: DaysViewSpan) {
selectedSpan.value = span;
emit('update:span', span);
}
function previousPeriod() {
emit('update:current-date', shiftLocalDays(props.currentDate, -daysCount.value));
}
function nextPeriod() {
emit('update:current-date', shiftLocalDays(props.currentDate, daysCount.value));
}
function goToToday() {
emit('update:current-date', new Date());
}
const dateRangeLabel = computed(() => {
if (visibleDates.value.length === 0) return '';
const first = visibleDates.value[0];
const last = visibleDates.value[visibleDates.value.length - 1];
const formatOptions: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric' };
if (daysCount.value === 1) {
return first.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
}
if (first.getMonth() === last.getMonth()) {
return `${first.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} - ${last.getDate()}, ${first.getFullYear()}`;
}
return `${first.toLocaleDateString('en-US', formatOptions)} - ${last.toLocaleDateString('en-US', formatOptions)}, ${last.getFullYear()}`;
});
const visibleDates = computed(() => {
const dates: Date[] = [];
const start = new Date(props.currentDate);
for (let i = 0; i < daysCount.value; i++) {
dates.push(new Date(start));
start.setDate(start.getDate() + 1);
}
return dates;
});
// Compute all-day/multi-day event segments
const allDaySegments = computed(() => {
if (visibleDates.value.length === 0) {
return { segments: [], laneCount: 0 };
}
const rangeStart = startOfLocalDay(visibleDates.value[0]);
const rangeEnd = startOfLocalDay(visibleDates.value[visibleDates.value.length - 1]);
const getColumnIndex = (date: Date): number => {
const targetDay = startOfLocalDay(date);
return visibleDates.value.findIndex(d => startOfLocalDay(d).getTime() === targetDay.getTime());
};
const instances = instancesStore.instancesForRange({
startMs: rangeStart.getTime(),
endMs: shiftLocalDays(rangeEnd, 1).getTime(),
});
return layoutMultiDayLanes(instances, rangeStart, rangeEnd, daysCount.value, getColumnIndex);
});
function isToday(date: Date): boolean {
const today = new Date();
return date.toDateString() === today.toDateString();
}
function getTimedEvents(date: Date): CalendarInstance[] {
return instancesStore.instancesForDay(date).filter(instance => !instance.timeless);
}
function getEventColor(instance: CalendarInstance): string {
if (instance.color) return instance.color;
const calendar = props.calendars.find(cal => cal.identifier === instance.entity.collection);
return calendar?.properties?.color || '#1976D2';
}
function getAllDayEventStyle(segment: MultiDaySegment) {
return {
backgroundColor: getEventColor(segment.instance),
left: `calc(${segment.startCol} / ${daysCount.value} * 100% + 4px)`,
width: `calc(${segment.span} / ${daysCount.value} * 100% - 8px)`,
top: `${segment.lane * ALL_DAY_EVENT_HEIGHT}px`,
};
}
function getEventStyle(instance: CalendarInstance) {
const startTime = new Date(instance.startMs);
const start = startTime.getHours() * 60 + startTime.getMinutes();
const duration = instance.durationMs / (1000 * 60);
return {
top: `${(start / 1440) * 100}%`,
height: `${(duration / 1440) * 100}%`,
backgroundColor: getEventColor(instance),
};
}
function formatHour(hour: number): string {
const ampm = hour >= 12 ? 'PM' : 'AM';
const displayHour = hour % 12 || 12;
return `${displayHour} ${ampm}`;
}
function formatTime(date: Date): string {
return date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
}
function formatInstanceTime(timestamp: number): string {
return formatTime(new Date(timestamp));
}
function formatWeekDay(date: Date): string {
return date.toLocaleDateString('en-US', { weekday: 'short' });
}
function handleDayClick(event: MouseEvent, day: Date) {
const target = event.currentTarget as HTMLElement;
const rect = target.getBoundingClientRect();
const clickY = event.clientY - rect.top;
// Calculate the hour based on click position (60px per hour)
const totalMinutes = Math.floor((clickY / 60) * 60);
const hours = Math.floor(totalMinutes / 60);
const minutes = Math.round((totalMinutes % 60) / 15) * 15; // Round to nearest 15 min
// Create a new date with the clicked time
const clickedDate = new Date(day);
clickedDate.setHours(hours, minutes, 0, 0);
emit('date-click', clickedDate);
}
</script>
<style scoped>
.days-view {
display: flex;
@@ -290,34 +227,6 @@ function handleDayClick(event: MouseEvent, day: Date) {
min-height: 600px;
}
.view-controls {
display: flex;
align-items: center;
padding: 4px 8px;
border-bottom: 1px solid rgb(var(--v-border-color));
background-color: rgb(var(--v-theme-surface));
flex-shrink: 0;
}
.view-navigation {
display: flex;
align-items: center;
gap: 4px;
}
.current-period {
font-size: 14px;
font-weight: 500;
margin-left: 8px;
white-space: nowrap;
}
.view-selector {
display: flex;
align-items: center;
gap: 2px;
}
.days-grid {
display: flex;
flex-direction: column;
+2 -6
View File
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { computed } from 'vue'
import { parseCalendarDate } from '@/utils/date'
import { formatTime as formatClockTime } from '@/utils/format'
const props = defineProps<{
event: any | null
@@ -13,12 +14,7 @@ const emit = defineEmits<{
}>()
const formatTime = (isoString: string | null | undefined): string => {
if (!isoString) return 'Not set'
const date = new Date(isoString)
return date.toLocaleTimeString('en-US', {
hour: 'numeric',
minute: '2-digit'
})
return isoString ? formatClockTime(new Date(isoString)) : 'Not set'
}
const formatAllDayDate = (isoString: string | null | undefined): string => {
+11 -57
View File
@@ -1,9 +1,11 @@
<script setup lang="ts">
import { computed, ref, onMounted, onUnmounted } from 'vue';
import { layoutMultiDayLanes, type MultiDaySegment } from '@/utils/multiDayLanes';
import { shiftLocalMonths, startOfLocalDay } from '@/utils/date';
import { startOfLocalDay } from '@/utils/date';
import { useChronoInstancesStore } from '@/stores/chronoInstancesStore';
import type { CalendarInstance } from '@/types/instance';
import ViewControls from './ViewControls.vue';
import { formatEpochTime } from '@/utils/format';
const EVENT_HEIGHT = 22; // Height of each event row in pixels
const DATE_ROW_HEIGHT = 34; // Space reserved at the top of each cell for the date label
@@ -39,23 +41,6 @@ const emit = defineEmits<{
const weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
// Navigation functions
function previousMonth() {
emit('update:current-date', shiftLocalMonths(props.currentDate, -1));
}
function nextMonth() {
emit('update:current-date', shiftLocalMonths(props.currentDate, 1));
}
function goToToday() {
emit('update:current-date', new Date());
}
const monthLabel = computed(() => {
return props.currentDate.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
});
// Track the calendar body size so each week row shows as many event rows as fit
const bodyRef = ref<HTMLElement | null>(null);
const bodyHeight = ref(600);
@@ -123,8 +108,8 @@ const weeks = computed<WeekData[]>(() => {
// Get multiday segments for this week
const instances = instancesStore.instancesForRange({
startMs: weekStart.getTime(),
endMs: new Date(
start: weekStart.getTime(),
end: new Date(
weekEnd.getFullYear(),
weekEnd.getMonth(),
weekEnd.getDate() + 1,
@@ -189,12 +174,6 @@ function morePopoverLabel(date: Date): string {
});
}
function formatStartTime(instance: CalendarInstance): string {
return new Date(instance.startMs).toLocaleTimeString('en-US', {
hour: 'numeric',
minute: '2-digit',
});
}
function isToday(date: Date): boolean {
const today = new Date();
@@ -211,14 +190,11 @@ function getEventColor(instance: CalendarInstance): string {
<template>
<div class="month-view">
<!-- Navigation Controls -->
<div class="view-controls">
<div class="view-navigation">
<v-btn size="x-small" variant="text" icon="mdi-chevron-left" @click="previousMonth" />
<v-btn size="x-small" variant="tonal" @click="goToToday">Today</v-btn>
<v-btn size="x-small" variant="text" icon="mdi-chevron-right" @click="nextMonth" />
<span class="current-period">{{ monthLabel }}</span>
</div>
</div>
<ViewControls
mode="month"
:current-date="currentDate"
@update:current-date="emit('update:current-date', $event)"
/>
<!-- Header row -->
<div class="day-headers">
@@ -292,7 +268,7 @@ function getEventColor(instance: CalendarInstance): string {
</template>
<v-list-item-title>
<span v-if="!instance.timeless" class="more-popover-time">
{{ formatStartTime(instance) }}
{{ formatEpochTime(instance.start) }}
</span>
{{ instance.label || 'Untitled' }}
</v-list-item-title>
@@ -345,28 +321,6 @@ function getEventColor(instance: CalendarInstance): string {
border: 1px solid rgb(var(--v-border-color));
}
.view-controls {
display: flex;
align-items: center;
padding: 4px 8px;
border-bottom: 1px solid rgb(var(--v-border-color));
background-color: rgb(var(--v-theme-surface));
flex-shrink: 0;
}
.view-navigation {
display: flex;
align-items: center;
gap: 4px;
}
.current-period {
font-size: 14px;
font-weight: 500;
margin-left: 8px;
white-space: nowrap;
}
.day-headers {
display: grid;
grid-template-columns: repeat(7, 1fr);
+110
View File
@@ -0,0 +1,110 @@
<script setup lang="ts" generic="S extends TimeSpanLabel">
import { computed } from 'vue';
import { spanToDays, type TimeSpanLabel } from '@/types/spans';
import { daysVisibleRange, shiftLocalDays, shiftLocalMonths } from '@/utils/date';
const props = defineProps<{
mode: 'days' | 'month';
currentDate: Date;
spans?: readonly S[];
selectedSpan?: S;
}>();
const emit = defineEmits<{
'update:current-date': [date: Date];
'update:span': [span: S];
}>();
const spanDays = computed(() => props.selectedSpan ? spanToDays(props.selectedSpan) : 1);
function previousPeriod() {
emit('update:current-date', props.mode === 'month'
? shiftLocalMonths(props.currentDate, -1)
: shiftLocalDays(props.currentDate, -spanDays.value));
}
function nextPeriod() {
emit('update:current-date', props.mode === 'month'
? shiftLocalMonths(props.currentDate, 1)
: shiftLocalDays(props.currentDate, spanDays.value));
}
function goToToday() {
emit('update:current-date', new Date());
}
const periodLabel = computed(() => {
if (props.mode === 'month') {
return props.currentDate.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
}
const range = daysVisibleRange(props.currentDate, spanDays.value);
const start = new Date(range.start);
const end = shiftLocalDays(new Date(range.end), -1);
const formatOptions: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric' };
if (spanDays.value === 1) {
return start.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
}
if (start.getMonth() === end.getMonth()) {
return `${start.toLocaleDateString('en-US', formatOptions)} - ${end.getDate()}, ${start.getFullYear()}`;
}
return `${start.toLocaleDateString('en-US', formatOptions)} - ${end.toLocaleDateString('en-US', formatOptions)}, ${end.getFullYear()}`;
});
</script>
<template>
<div class="view-controls">
<div class="view-navigation">
<v-btn size="x-small" variant="text" icon="mdi-chevron-left" @click="previousPeriod" />
<v-btn size="x-small" variant="tonal" @click="goToToday">Today</v-btn>
<v-btn size="x-small" variant="text" icon="mdi-chevron-right" @click="nextPeriod" />
<span class="current-period">{{ periodLabel }}</span>
</div>
<v-spacer />
<div v-if="spans?.length" class="view-selector">
<v-btn
v-for="span in spans"
:key="span"
size="x-small"
variant="text"
:color="selectedSpan === span ? 'primary' : undefined"
@click="emit('update:span', span)"
>
{{ span.toUpperCase() }}
</v-btn>
</div>
</div>
</template>
<style scoped>
.view-controls {
display: flex;
align-items: center;
padding: 4px 8px;
border-bottom: 1px solid rgb(var(--v-border-color));
background-color: rgb(var(--v-theme-surface));
flex-shrink: 0;
}
.view-navigation {
display: flex;
align-items: center;
gap: 4px;
}
.current-period {
font-size: 14px;
font-weight: 500;
margin-left: 8px;
white-space: nowrap;
}
.view-selector {
display: flex;
align-items: center;
gap: 2px;
}
</style>
+11 -39
View File
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { parseCalendarDate } from '@/utils/date'
import { formatDisplayDate, formatDisplayTime, formatLocalIsoDate, formatLocalIsoTime } from '@/utils/format'
interface Props {
mode: 'edit' | 'view'
@@ -29,18 +30,6 @@ const timeZoneValue = computed({
set: (value) => emit('update:timeZone', value || null)
})
// Format helpers
const formatDate = (date: Date): string => {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
const formatTime = (date: Date): string => {
return date.toTimeString().slice(0, 5)
}
const formatDateTime = (isoString: string | null | undefined): string => {
if (!isoString) return 'Not set'
if (timelessValue.value) {
@@ -64,24 +53,7 @@ const serializeDateTime = (date: string, time: string): string => {
}
const inputDate = (value: string, timeless: boolean): string => {
return timeless ? value.slice(0, 10) : formatDate(new Date(value))
}
const formatDisplayDate = (value: string): string => {
if (!value) return ''
return new Date(`${value}T00:00:00`).toLocaleDateString(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric',
})
}
const formatDisplayTime = (value: string): string => {
if (!value) return ''
const [hour, minute] = value.split(':').map(Number)
const date = new Date()
date.setHours(hour, minute, 0, 0)
return date.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' })
return timeless ? value.slice(0, 10) : formatLocalIsoDate(new Date(value))
}
const pickerDate = (value: string): Date | null => value ? new Date(`${value}T00:00:00`) : null
@@ -89,7 +61,7 @@ const pickerDate = (value: string): Date | null => value ? new Date(`${value}T00
const normalizePickerDate = (value: unknown): string => {
const selected = Array.isArray(value) ? value[0] : value
const date = selected instanceof Date ? selected : new Date(String(selected))
return Number.isNaN(date.getTime()) ? '' : formatDate(date)
return Number.isNaN(date.getTime()) ? '' : formatLocalIsoDate(date)
}
// Date/time fields
@@ -119,11 +91,11 @@ watch([() => props.startsOn, () => props.timeless], ([newValue, timeless]) => {
if (newValue) {
const start = new Date(newValue)
startDate.value = inputDate(newValue, timeless === true)
startTime.value = formatTime(start)
startTime.value = formatLocalIsoTime(start)
} else {
const now = new Date()
startDate.value = formatDate(now)
startTime.value = formatTime(now)
startDate.value = formatLocalIsoDate(now)
startTime.value = formatLocalIsoTime(now)
}
}, { immediate: true })
@@ -131,11 +103,11 @@ watch([() => props.endsOn, () => props.timeless], ([newValue, timeless]) => {
if (newValue) {
const end = new Date(newValue)
endDate.value = inputDate(newValue, timeless === true)
endTime.value = formatTime(end)
endTime.value = formatLocalIsoTime(end)
} else {
const later = new Date(Date.now() + 60 * 60 * 1000)
endDate.value = formatDate(later)
endTime.value = formatTime(later)
endDate.value = formatLocalIsoDate(later)
endTime.value = formatLocalIsoTime(later)
}
}, { immediate: true })
@@ -151,8 +123,8 @@ const ensureValidRange = (): boolean => {
const correctedEnd = new Date(start)
if (timelessValue.value) correctedEnd.setDate(correctedEnd.getDate() + 1)
else correctedEnd.setHours(correctedEnd.getHours() + 1)
endDate.value = formatDate(correctedEnd)
endTime.value = formatTime(correctedEnd)
endDate.value = formatLocalIsoDate(correctedEnd)
endTime.value = formatLocalIsoTime(correctedEnd)
return true
}
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import type { EventOccurrence } from '@ChronoManager/types/event'
import { formatDisplayDate, formatLocalIsoDate } from '@/utils/format'
type Frequency = 'daily' | 'weekly' | 'monthly' | 'yearly'
type EndMode = 'never' | 'date' | 'count'
@@ -170,19 +171,11 @@ const pickerDate = (value: string | null | undefined): Date | null => {
return value ? new Date(`${value.slice(0, 10)}T00:00:00`) : null
}
const formatDisplayDate = (value: string | null | undefined): string => {
const date = pickerDate(value)
return date ? date.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }) : ''
}
const selectConclusionDate = (value: unknown) => {
const selected = Array.isArray(value) ? value[0] : value
const date = selected instanceof Date ? selected : new Date(String(selected))
if (!Number.isNaN(date.getTime())) {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
updatePattern({ concludes: `${year}-${month}-${day}`, iterations: null })
updatePattern({ concludes: formatLocalIsoDate(date), iterations: null })
}
endDateMenu.value = false
}