Merge pull request #3 from quirinecker/feature/luxon

Ported Calendar to Luxon
This commit is contained in:
2025-05-13 16:31:54 +02:00
committed by GitHub
11 changed files with 70 additions and 61 deletions

View File

@@ -10,8 +10,10 @@
"@nuxt/test-utils": "3.18.0", "@nuxt/test-utils": "3.18.0",
"@nuxt/ui": "3.1.1", "@nuxt/ui": "3.1.1",
"@nuxtjs/color-mode": "3.5.2", "@nuxtjs/color-mode": "3.5.2",
"@types/luxon": "^3.6.2",
"@types/moment": "^2.13.0", "@types/moment": "^2.13.0",
"eslint": "^9.0.0", "eslint": "^9.0.0",
"luxon": "^3.6.1",
"moment": "^2.30.1", "moment": "^2.30.1",
"nuxt": "^3.17.2", "nuxt": "^3.17.2",
"nuxt-app": "file:", "nuxt-app": "file:",
@@ -474,6 +476,8 @@
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/luxon": ["@types/luxon@3.6.2", "", {}, "sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw=="],
"@types/moment": ["@types/moment@2.13.0", "", { "dependencies": { "moment": "*" } }, "sha512-DyuyYGpV6r+4Z1bUznLi/Y7HpGn4iQ4IVcGn8zrr1P4KotKLdH0sbK1TFR6RGyX6B+G8u83wCzL+bpawKU/hdQ=="], "@types/moment": ["@types/moment@2.13.0", "", { "dependencies": { "moment": "*" } }, "sha512-DyuyYGpV6r+4Z1bUznLi/Y7HpGn4iQ4IVcGn8zrr1P4KotKLdH0sbK1TFR6RGyX6B+G8u83wCzL+bpawKU/hdQ=="],
"@types/node": ["@types/node@22.15.12", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-K0fpC/ZVeb8G9rm7bH7vI0KAec4XHEhBam616nVJCV51bKzJ6oA3luG4WdKoaztxe70QaNjS/xBmcDLmr4PiGw=="], "@types/node": ["@types/node@22.15.12", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-K0fpC/ZVeb8G9rm7bH7vI0KAec4XHEhBam616nVJCV51bKzJ6oA3luG4WdKoaztxe70QaNjS/xBmcDLmr4PiGw=="],

View File

@@ -1,11 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Moment } from 'moment';
import Calendar from './calendar/Calendar.vue' import Calendar from './calendar/Calendar.vue'
import { Event } from '~/utils/event'; import { Event } from '~/utils/event';
import { UCard } from '#components'; import { UCard } from '#components';
import type { DateTime } from 'luxon';
const events = defineModel<Event[]>('events', { required: true }) const events = defineModel<Event[]>('events', { required: true })
const date = defineModel<Moment>('date', { required: true }) const date = defineModel<DateTime>('date', { required: true })
</script> </script>

View File

@@ -3,8 +3,7 @@ import { CalendarDate } from '@internationalized/date';
import ListItem from './ListItem.vue'; import ListItem from './ListItem.vue';
import Title1 from './Title1.vue'; import Title1 from './Title1.vue';
import type { DropdownMenuItem } from '@nuxt/ui'; import type { DropdownMenuItem } from '@nuxt/ui';
import type { Moment } from 'moment'; import { DateTime } from 'luxon';
import moment from 'moment';
const colorMode = useColorMode(); const colorMode = useColorMode();
const currentTheme = ref<'dark' | 'system' | 'light'>(colorMode.preference as 'dark' | 'system' | 'light'); const currentTheme = ref<'dark' | 'system' | 'light'>(colorMode.preference as 'dark' | 'system' | 'light');
@@ -56,17 +55,17 @@ const dropDownItems = computed<DropdownMenuItem[][]>(() => [
] ]
]) ])
const date = defineModel<Moment>('date', { required: true }) const date = defineModel<DateTime>('date', { required: true })
const selectedDate = computed({ const selectedDate = computed({
get() { get() {
return new CalendarDate(date.value.year(), date.value.month() + 1, date.value.date()) return new CalendarDate(date.value.year, date.value.month, date.value.day)
}, },
set(value) { set(value) {
if (value === undefined) { if (value === undefined) {
return return
} }
date.value = moment(value.toString()); date.value = DateTime.fromISO(value.toString());
} }
}) })

View File

@@ -2,20 +2,20 @@
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import CalendarHeader from './CalendarHeader.vue'; import CalendarHeader from './CalendarHeader.vue';
import CalendarCollumn from './CalendarCollumn.vue'; import CalendarCollumn from './CalendarCollumn.vue';
import moment, { type Moment } from 'moment';
import { Event } from '~/utils/event'; import { Event } from '~/utils/event';
import { DateTime } from 'luxon';
const events = defineModel<Event[]>('events', { required: true }) const events = defineModel<Event[]>('events', { required: true })
const date = defineModel<Moment>('date', { required: true }) const date = defineModel<DateTime>('date', { required: true })
const draggedEvent = ref<DraggedEvent | undefined>() const draggedEvent = ref<DraggedEvent | undefined>()
type Day = { type Day = {
date: Moment date: DateTime
events: CollissionWrapper[][] events: CollissionWrapper[][]
} }
const week = computed(() => { const week = computed(() => {
return moment(date.value).startOf('isoWeek') return date.value.startOf('week')
}) })
function pushEventWithCollisionUpdate(array: CollissionWrapper[], event: Event, collisions: CollissionWrapper[], collisionCount: number) { function pushEventWithCollisionUpdate(array: CollissionWrapper[], event: Event, collisions: CollissionWrapper[], collisionCount: number) {
@@ -28,8 +28,9 @@ function pushEventWithCollisionUpdate(array: CollissionWrapper[], event: Event,
const days = computed<Day[]>(() => { const days = computed<Day[]>(() => {
return [1, 2, 3, 4, 5, 6, 7].map((i) => { return [1, 2, 3, 4, 5, 6, 7].map((i) => {
const currentDate = date.value.startOf('week').plus({ day: i - 1 })
const filteredEvents = events.value.filter( const filteredEvents = events.value.filter(
(event) => event.from >= moment(week.value).weekday(i).startOf('day') && event.to <= moment(week.value).weekday(i).endOf('day') (event) => event.from >= currentDate.startOf('day') && event.to <= currentDate.endOf('day')
) )
const sortedEvents = filteredEvents.sort((a, b) => a.from.valueOf() - b.from.valueOf()) const sortedEvents = filteredEvents.sort((a, b) => a.from.valueOf() - b.from.valueOf())
@@ -62,7 +63,7 @@ const days = computed<Day[]>(() => {
} }
return { return {
date: moment(week.value).weekday(i), date: currentDate,
events: columns events: columns
} }
}) })
@@ -72,17 +73,22 @@ const emits = defineEmits<{
(e: 'create', timespan: Event): void (e: 'create', timespan: Event): void
}>() }>()
const hour = (num: number) => {
return DateTime.now().startOf('day').plus({ hours: num })
}
const seperators = ref<Seperator[]>([ const seperators = ref<Seperator[]>([
{ text: '3 AM', time: moment().hour(3) }, { text: '3 AM', time: hour(3) },
{ text: '6 AM', time: moment().hour(6) }, { text: '6 AM', time: hour(6) },
{ text: '9 AM', time: moment().hour(9) }, { text: '9 AM', time: hour(9) },
{ text: '12 PM', time: moment().hour(12) }, { text: '12 PM', time: hour(12) },
{ text: '3 PM', time: moment().hour(15) }, { text: '3 PM', time: hour(15) },
{ text: '6 PM', time: moment().hour(18) }, { text: '6 PM', time: hour(18) },
{ text: '9 PM', time: moment().hour(21) }, { text: '9 PM', time: hour(21) },
]) ])
function quickCreate(date: Moment, timespan: Timespan) {
function quickCreate(date: DateTime, timespan: Timespan) {
const eventTitle = prompt("Event title") const eventTitle = prompt("Event title")
if (eventTitle === null) { if (eventTitle === null) {
@@ -91,8 +97,8 @@ function quickCreate(date: Moment, timespan: Timespan) {
const newEvent: Event = new Event( const newEvent: Event = new Event(
eventTitle, eventTitle,
moment(date).startOf('day').minutes(timespan.from * 24 * 60), date.startOf('day').plus({ minutes: timespan.from * 24 * 60 }),
moment(date).startOf('day').minutes(timespan.to * 24 * 60) date.startOf('day').plus({ minutes: timespan.to * 24 * 60 })
) )
emits('create', newEvent) emits('create', newEvent)

View File

@@ -1,19 +1,18 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, useTemplateRef } from 'vue'; import { computed, ref, useTemplateRef } from 'vue';
import CalendarSeperator from './CalendarSeperator.vue'; import CalendarSeperator from './CalendarSeperator.vue';
import type { Moment } from 'moment';
import CalendarEvent from './CalendarEvent.vue'; import CalendarEvent from './CalendarEvent.vue';
import { Event } from '~/utils/event'; import { Event } from '~/utils/event';
import moment from 'moment'; import type { DateTime } from 'luxon';
const props = defineProps<{ const props = defineProps<{
seperators: Seperator[], seperators: Seperator[],
day: Moment day: DateTime
events: CollissionWrapper[][] events: CollissionWrapper[][]
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'quick-create', day: Moment, event: EventDimensions): void, (e: 'quick-create', day: DateTime, event: EventDimensions): void,
}>() }>()
const isDragging = ref(false) const isDragging = ref(false)
@@ -58,7 +57,7 @@ function mouseup(_: MouseEvent) {
const timeFrom = Math.min(endY.value, startY.value) / column.value.offsetHeight const timeFrom = Math.min(endY.value, startY.value) / column.value.offsetHeight
const timeTo = Math.max(endY.value, startY.value) / column.value.offsetHeight const timeTo = Math.max(endY.value, startY.value) / column.value.offsetHeight
emit('quick-create', moment(props.day), { emit('quick-create', props.day, {
from: timeFrom, from: timeFrom,
to: timeTo to: timeTo
}) })
@@ -76,7 +75,7 @@ function dragover(e: DragEvent) {
return return
} }
if (!draggedEvent.value.date.isSame(props.day)) { if (!draggedEvent.value.date.equals(props.day)) {
draggedEvent.value.date = props.day draggedEvent.value.date = props.day
} }
@@ -94,12 +93,12 @@ function dragDrop(_: DragEvent) {
<template> <template>
<div class="flex flex-col h-full grow"> <div class="flex flex-col h-full grow">
<div class="flex justify-center items-center flex-col h-18 border-b-1 border-muted"> <div class="flex justify-center items-center flex-col h-18 border-b-1 border-muted">
<div>{{ props.day.format('dd').toUpperCase() }}</div> <div>{{ props.day.toFormat('ccc').toUpperCase() }}</div>
<div>{{ props.day.date() }}</div> <div>{{ props.day.day }}</div>
</div> </div>
<div id="col" ref="column" @mousedown="mousedown" @mouseup="mouseup" @mousemove="mouseover" @dragover="dragover" <div id="col" ref="column" @mousedown="mousedown" @mouseup="mouseup" @mousemove="mouseover" @dragover="dragover"
@dragend="dragDrop" class="relative flex flex-col grow items-center"> @dragend="dragDrop" class="relative flex flex-col grow items-center select-none">
<CalendarSeperator v-for="sep in seperators" :seperator="sep"> <CalendarSeperator v-for="sep in seperators" :seperator="sep">
<hr class="w-full border-muted"> <hr class="w-full border-muted">
</CalendarSeperator> </CalendarSeperator>
@@ -110,7 +109,7 @@ function dragDrop(_: DragEvent) {
<CalendarEvent v-for="event in column" :event="event" :columnIndex="index" @move="eventMove" /> <CalendarEvent v-for="event in column" :event="event" :columnIndex="index" @move="eventMove" />
</div> </div>
<div v-if="draggedEvent !== undefined && draggedEvent.date.isSame(props.day)" <div v-if="draggedEvent !== undefined && draggedEvent.date.equals(props.day)"
class="absolute w-11/12 top-20 bg-black opacity-45 rounded-lg" class="absolute w-11/12 top-20 bg-black opacity-45 rounded-lg"
:style="{ height: `${draggedEvent.height}px`, top: `${draggedEvent.top}px` }"></div> :style="{ height: `${draggedEvent.height}px`, top: `${draggedEvent.top}px` }"></div>
</div> </div>

View File

@@ -45,7 +45,7 @@ function dragStart(e: DragEvent) {
<div class="absolute rounded-lg h-0 top-20 bg-black opacity-45 p-2 flex flex-col z-10" @mousedown.stop <div class="absolute rounded-lg h-0 top-20 bg-black opacity-45 p-2 flex flex-col z-10" @mousedown.stop
@mouseover.stop @mouseup.stop draggable="true" @dragstart="dragStart" @mouseover.stop @mouseup.stop draggable="true" @dragstart="dragStart"
:style="{ top: `${top}%`, height: `${height}%`, left: `${left}%`, width: `${widht}%` }"> :style="{ top: `${top}%`, height: `${height}%`, left: `${left}%`, width: `${widht}%` }">
<div>{{ event.event.from.format('HH:mm') }} - {{ event.event.to.format('HH:mm') }}</div> <div>{{ event.event.from.toFormat('HH:mm') }} - {{ event.event.to.toFormat('HH:mm') }}</div>
<div>{{ event.event.title }}</div> <div>{{ event.event.title }}</div>
</div> </div>
</template> </template>

View File

@@ -1,12 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Moment } from 'moment'; import type { DateTime } from 'luxon';
defineProps<{ defineProps<{
seperator: Seperator seperator: Seperator
}>(); }>();
const relativePositionOf = function (time: Moment) { const relativePositionOf = function (time: DateTime) {
return `${(time.hours() / 24) * 100}%` return `${((time.hour) / 24) * 100}%`
} }
</script> </script>

View File

@@ -16,8 +16,10 @@
"@nuxt/test-utils": "3.18.0", "@nuxt/test-utils": "3.18.0",
"@nuxt/ui": "3.1.1", "@nuxt/ui": "3.1.1",
"@nuxtjs/color-mode": "3.5.2", "@nuxtjs/color-mode": "3.5.2",
"@types/luxon": "^3.6.2",
"@types/moment": "^2.13.0", "@types/moment": "^2.13.0",
"eslint": "^9.0.0", "eslint": "^9.0.0",
"luxon": "^3.6.1",
"moment": "^2.30.1", "moment": "^2.30.1",
"nuxt": "^3.17.2", "nuxt": "^3.17.2",
"nuxt-app": "file:", "nuxt-app": "file:",

View File

@@ -1,11 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import moment from 'moment'; import { DateTime } from 'luxon';
import MainContent from '~/components/ui/MainContent.vue'; import MainContent from '~/components/ui/MainContent.vue';
import Sidebar from '~/components/ui/Sidebar.vue'; import Sidebar from '~/components/ui/Sidebar.vue';
const todos = ["Staistics", "Computer Graphics", "Webdev"] const todos = ["Staistics", "Computer Graphics", "Webdev"]
const events = ref([]) const events = ref([])
const date = ref(moment()) const date = ref<DateTime>(DateTime.now())
</script> </script>

View File

@@ -1,20 +1,19 @@
import type { Moment } from "moment" import { DateTime } from "luxon"
import moment from "moment"
export class Event { export class Event {
private static readonly MINUTES_IN_DAY = 24 * 60 private static readonly MINUTES_IN_DAY = 24 * 60
constructor( constructor(
public readonly title: string, public readonly title: string,
public from: Moment, public from: DateTime,
public to: Moment public to: DateTime
) { } ) { }
getPercentDimensions(): EventDimensions { getPercentDimensions(): EventDimensions {
const start_of_day = moment(this.from).startOf('day') const start_of_day = this.from.startOf('day')
const from_percentage = this.from.diff(start_of_day, 'minutes') / Event.MINUTES_IN_DAY const from_percentage = (this.from.diff(start_of_day, 'minutes').minutes) / Event.MINUTES_IN_DAY
const to_percentage = this.to.diff(start_of_day, 'minutes') / Event.MINUTES_IN_DAY const to_percentage = (this.to.diff(start_of_day, 'minutes').minutes) / Event.MINUTES_IN_DAY
return { return {
from: from_percentage * 100, from: from_percentage * 100,
to: to_percentage * 100 to: to_percentage * 100
@@ -34,18 +33,18 @@ export class Event {
} }
static fromSerializable(event: SerializableEvent) { static fromSerializable(event: SerializableEvent) {
return new Event(event.title, moment(event.from), moment(event.to)) return new Event(event.title, DateTime.fromISO(event.from), DateTime.fromISO(event.to))
} }
static fromPercentDimensions(title: string, dimensions: EventDimensions, date: Moment): Event { static fromPercentDimensions(title: string, dimensions: EventDimensions, date: DateTime): Event {
return new Event( return new Event(
title, title,
moment(date).startOf('day').minutes((dimensions.from / 100) * Event.MINUTES_IN_DAY), date.startOf('day').plus({ minutes: (dimensions.from / 100) * Event.MINUTES_IN_DAY }),
moment(date).startOf('day').minutes((dimensions.to / 100) * Event.MINUTES_IN_DAY) date.startOf('day').plus({ minutes: (dimensions.to / 100) * Event.MINUTES_IN_DAY })
) )
} }
static fromPixelDimensions(title: string, dimensions: EventDimensions, height: number, date: Moment): Event { static fromPixelDimensions(title: string, dimensions: EventDimensions, height: number, date: DateTime): Event {
const percentDimensions: EventDimensions = { const percentDimensions: EventDimensions = {
from: dimensions.from * 100 / height, from: dimensions.from * 100 / height,
to: dimensions.to * 100 / height to: dimensions.to * 100 / height
@@ -84,8 +83,8 @@ export class Event {
toSerializable(): SerializableEvent { toSerializable(): SerializableEvent {
return { return {
title: this.title, title: this.title,
from: this.from.toISOString(), from: this.from.toISO() ?? '',
to: this.to.toISOString() to: this.to.toISO() ?? ''
} }
} }
@@ -101,7 +100,7 @@ export class Event {
const offset = mouseY - pixelDimensions.from const offset = mouseY - pixelDimensions.from
return { return {
date: moment(this.from).startOf('day'), date: this.from.startOf('day'),
top: pixelDimensions.from, top: pixelDimensions.from,
height: pixelDimensions.to - pixelDimensions.from, height: pixelDimensions.to - pixelDimensions.from,
target: this, target: this,
@@ -118,8 +117,8 @@ export type EventDimensions = {
export type SimpleEvent = { export type SimpleEvent = {
title: string, title: string,
from: Moment, from: DateTime,
to: Moment to: DateTime
} }
export type SerializableEvent = { export type SerializableEvent = {
@@ -134,7 +133,7 @@ export type CollissionWrapper = {
} }
export type DraggedEvent = { export type DraggedEvent = {
date: Moment, date: DateTime,
top: number, top: number,
height: number, height: number,
target: Event, target: Event,

View File

@@ -1,8 +1,8 @@
import type { Moment } from "moment" import type { DateTime } from "luxon"
export type Seperator = { export type Seperator = {
text: string, text: string,
time: Moment time: DateTime
} }
export type Timespan = { export type Timespan = {
@@ -15,8 +15,8 @@ export type Event = AnonymousEvent & {
} }
export type AnonymousEvent = { export type AnonymousEvent = {
from: Moment, from: DateTime,
to: Moment to: DateTime
} }
export type EventWithCollisions = Event & { export type EventWithCollisions = Event & {