Skip to content

Commit 7eea712

Browse files
authored
Merge pull request #12 from git-calendar/month
Month View
2 parents fc84089 + ec60cec commit 7eea712

16 files changed

Lines changed: 774 additions & 88 deletions

File tree

src/assets/locales/cs.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"from": "Od",
2424
"to": "Do",
2525
"entireDay": "Celý den",
26+
"more": "další…",
2627
"repeat": {
2728
"repeat": "Opakování",
2829
"end": "Konec",

src/assets/locales/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"from": "From",
2424
"to": "To",
2525
"entireDay": "Entire day",
26+
"more": "more…",
2627
"repeat": {
2728
"repeat": "Repeat",
2829
"end": "End",

src/assets/locales/git.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
"new": "New Commit",
44
"title": "Message",
55
"description": "Message \\n",
6-
"location": "Origin"
6+
"location": "Origin",
7+
"more": "more…"
78
},
89
"saveBtn": "Commit",
910
"createBtn": "Init / Clone",

src/components/AllDayBar.vue

Lines changed: 9 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
<script setup lang="ts">
22
import { useEventModal } from '@/composables/modals/useEventModal';
33
import type { CalendarEvent } from '@/types/core';
4-
import { getAllDayEndDate, getCurrentViewDatetime, isWholeDay } from '@/utils';
4+
import { getCurrentViewDatetime } from '@/utils';
5+
import { getVisibleDayRange, layoutEventsByDay } from '@/utils/allDayEventLayout';
56
import { DateTime } from 'luxon';
67
import { computed, useTemplateRef } from 'vue';
78
import { useRoute } from 'vue-router';
@@ -36,23 +37,6 @@ const todayGridColumn = computed(() => {
3637
return '';
3738
});
3839
39-
function visibleRange(event: CalendarEvent) {
40-
const end = isWholeDay(event) ? getAllDayEndDate(event) : event.to;
41-
const startIndex = event.from.startOf('day').diff(currentDate.value, 'days').days;
42-
const endIndex = end.startOf('day').diff(currentDate.value, 'days').days;
43-
44-
if (endIndex < 0 || startIndex >= props.numOfDays) return null;
45-
46-
const firstDay = Math.max(0, startIndex);
47-
const lastDay = Math.min(props.numOfDays - 1, endIndex);
48-
49-
return {
50-
startIndex: firstDay,
51-
endIndex: lastDay,
52-
span: Math.max(1, lastDay - firstDay + 1),
53-
};
54-
}
55-
5640
function gridStyle(startIndex: number, span: number, row: number) {
5741
return {
5842
gridColumn: `${startIndex + 1} / span ${span}`,
@@ -69,42 +53,18 @@ function startCreate(event: PointerEvent) {
6953
props.drag.startCreateAllDay(event, element);
7054
}
7155
72-
const laidOutEvents = computed(() => {
73-
const visibleEvents = props.events.flatMap((event) => {
74-
const range = visibleRange(event);
75-
return range ? [{ event, ...range }] : [];
76-
});
77-
78-
visibleEvents.sort((a, b) => (a.startIndex !== b.startIndex ? a.startIndex - b.startIndex : b.span - a.span));
79-
80-
const rowEnds: number[] = [];
81-
return visibleEvents.map((item) => {
82-
let rowIndex = rowEnds.findIndex((endIndex) => endIndex < item.startIndex);
83-
84-
if (rowIndex === -1) {
85-
rowIndex = rowEnds.length;
86-
rowEnds.push(item.endIndex);
87-
} else {
88-
rowEnds[rowIndex] = item.endIndex;
89-
}
90-
91-
const row = rowIndex + 1;
92-
93-
return {
94-
event: item.event,
95-
startIndex: item.startIndex,
96-
endIndex: item.endIndex,
97-
row,
98-
gridStyle: gridStyle(item.startIndex, item.span, row),
99-
};
100-
});
101-
});
56+
const laidOutEvents = computed(() =>
57+
layoutEventsByDay(props.events, currentDate.value, props.numOfDays).map((item) => ({
58+
...item,
59+
gridStyle: gridStyle(item.startIndex, item.span, item.row),
60+
})),
61+
);
10262
10363
const previewEvent = computed(() => {
10464
const event = props.drag.active.value;
10565
if (props.drag.mode.value !== 'create-all-day' || !event) return null;
10666
107-
const range = visibleRange(event);
67+
const range = getVisibleDayRange(event, currentDate.value, props.numOfDays);
10868
if (!range) return null;
10969
11070
const occupiedRows = new Set(

src/components/MonthSideMap.vue

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const { monthNameLong } = useTranslation();
1212
const route = useRoute();
1313
1414
const currentDatetime = ref<DateTime>(DateTime.now());
15+
const isMonthView = computed(() => route.params.view === 'm');
1516
const monthTracker = ref(currentDatetime.value.month);
1617
const yearTracker = ref(currentDatetime.value.year);
1718
@@ -60,6 +61,12 @@ watch(
6061
const viewStart = getCurrentViewDatetime(route.params);
6162
currentDatetime.value = viewStart;
6263
64+
if (isMonthView.value) {
65+
monthTracker.value = viewStart.month;
66+
yearTracker.value = viewStart.year;
67+
return;
68+
}
69+
6370
const viewEnd = viewStart.plus({
6471
days: getViewLengthInDays(route.params),
6572
});
@@ -102,6 +109,11 @@ function changeMonthNum(up: boolean) {
102109
function jumpToInterval(clickedDay: DateTime) {
103110
if (route.params.view === 'w') {
104111
router.replace(getWeekAlignedRedirect(clickedDay));
112+
} else if (isMonthView.value) {
113+
router.replace({
114+
name: 'calendar',
115+
params: { view: 'm', year: clickedDay.year, month: clickedDay.month, day: clickedDay.day },
116+
});
105117
} else {
106118
router.replace({ params: { year: clickedDay.year, month: clickedDay.month, day: clickedDay.day } });
107119
}
@@ -131,11 +143,15 @@ function getIntervalFromDay(day: DateTime) {
131143
const hoverInterval = computed(() => {
132144
if (!hoveredDay.value) return null;
133145
146+
if (isMonthView.value) {
147+
return { start: hoveredDay.value, end: hoveredDay.value };
148+
}
149+
134150
return getIntervalFromDay(hoveredDay.value);
135151
});
136152
137153
const pressedInterval = computed(() => {
138-
if (!pressedDay.value) return null;
154+
if (isMonthView.value || !pressedDay.value) return null;
139155
140156
return getIntervalFromDay(pressedDay.value);
141157
});
@@ -172,10 +188,11 @@ const pressedInterval = computed(() => {
172188
class="day"
173189
:class="{
174190
today: d.hasSame(DateTime.now(), 'day'),
191+
'selected-day': isMonthView && d.hasSame(currentDatetime, 'day'),
175192
'not-this-month': d.month !== monthTracker,
176-
'in-range': d >= viewInterval.start && d <= viewInterval.end,
177-
'range-start': d.hasSame(viewInterval.start, 'day'),
178-
'range-end': d.hasSame(viewInterval.end, 'day'),
193+
'in-range': !isMonthView && d >= viewInterval.start && d <= viewInterval.end,
194+
'range-start': !isMonthView && d.hasSame(viewInterval.start, 'day'),
195+
'range-end': !isMonthView && d.hasSame(viewInterval.end, 'day'),
179196
'pressed-range': pressedInterval && d >= pressedInterval.start && d <= pressedInterval.end,
180197
}"
181198
@click="jumpToInterval(d)"
@@ -270,7 +287,7 @@ const pressedInterval = computed(() => {
270287
text-decoration-thickness: 0.15rem;
271288
text-underline-offset: 0.2rem;*/
272289
273-
&:not(.in-range) {
290+
&:not(.in-range):not(.selected-day) {
274291
color: var(--git-color);
275292
}
276293
@@ -306,6 +323,16 @@ const pressedInterval = computed(() => {
306323
}
307324
}
308325
326+
&.today.not-this-month:not(.selected-day) {
327+
color: color-mix(in srgb, var(--git-color) 45%, transparent);
328+
}
329+
330+
&.selected-day {
331+
border-radius: var(--small-border-radius);
332+
background-color: var(--git-bg-color);
333+
color: var(--text-color-hard);
334+
}
335+
309336
&.range-start {
310337
border-top-left-radius: var(--small-border-radius);
311338
border-bottom-left-radius: var(--small-border-radius);

src/components/TopBar.vue

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script setup lang="ts">
22
import { computed, ref, watch } from 'vue';
3+
import { DateTime } from 'luxon';
34
import { getCurrentViewDatetime, getWeekAlignedRedirect, moveView } from '@/utils';
45
56
import { FiChevronLeft, FiChevronRight } from 'vue-icons-plus/fi';
@@ -28,19 +29,33 @@ watch(
2829
{ immediate: true },
2930
);
3031
watch(view, (newView) => {
32+
const current = getCurrentViewDatetime(router.currentRoute.value.params);
33+
3134
if (newView === 'w') {
32-
// week view should be aligned
33-
const current = getCurrentViewDatetime(router.currentRoute.value.params);
3435
router.push(getWeekAlignedRedirect(current));
36+
} else if (newView === 'm') {
37+
router.push({
38+
name: 'calendar',
39+
params: { view: 'm', year: current.year, month: current.month, day: current.day },
40+
});
3541
} else {
3642
router.push({
3743
name: 'calendar',
38-
params: { ...router.currentRoute.value.params, view: newView },
44+
params: { view: newView, year: current.year, month: current.month, day: current.day },
3945
});
4046
}
4147
});
4248
4349
function jumpToToday() {
50+
if (view.value === 'm') {
51+
const today = DateTime.now();
52+
router.push({
53+
name: 'calendar',
54+
params: { view: 'm', year: today.year, month: today.month, day: today.day },
55+
});
56+
return;
57+
}
58+
4459
router.push({ name: 'calendar', params: { view: view.value } });
4560
}
4661
</script>
@@ -51,12 +66,10 @@ function jumpToToday() {
5166
<NewEventBtn v-if="!sidebar.isOpen.value" :large="!isMobile" />
5267

5368
<SyncStatus />
54-
<!-- TODO: remove disabled -->
5569
<MultiToggle
5670
v-model="view"
5771
:options="CALENDAR_VIEWS"
5872
:labels="CALENDAR_VIEWS.map((view) => $t(`views.${view}.${isMobile ? 'short' : 'long'}`))"
59-
:disabled="['m']"
6073
name="view-selector"
6174
/>
6275

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<script setup lang="ts">
2+
import { computed, useAttrs } from 'vue';
3+
import BaseEvent from '@/components/timeline/BaseEvent.vue';
4+
import { getTag } from '@/services/calendarCache';
5+
import type { CalendarEvent } from '@/types/core';
6+
import { isWholeDay, timeRangeFormat } from '@/utils';
7+
8+
defineOptions({ inheritAttrs: false });
9+
10+
const props = defineProps<{
11+
event: CalendarEvent;
12+
compact: boolean;
13+
interactive: boolean;
14+
temporary: boolean;
15+
}>();
16+
17+
const attrs = useAttrs();
18+
const showTime = computed(() => !isWholeDay(props.event));
19+
const color = computed(() => getTag(props.event.calendar, props.event.tagId ?? '')?.color);
20+
</script>
21+
22+
<template>
23+
<BaseEvent
24+
v-bind="attrs"
25+
variant="month"
26+
:title="event.title"
27+
:subtitle="showTime ? timeRangeFormat(event.from, event.to) : ''"
28+
:color="color"
29+
:temporary="temporary"
30+
:interactive="interactive"
31+
:compact="compact"
32+
:without-time="!showTime"
33+
:show-subtitle="showTime"
34+
/>
35+
</template>

0 commit comments

Comments
 (0)