added calendar and rough structure of calendar
This commit is contained in:
20
bun.lock
Normal file
20
bun.lock
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"moment": "^2.30.1",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/lucide": "^1.2.42",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@iconify-json/lucide": ["@iconify-json/lucide@1.2.42", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-exkRygF4yd6e5q966TXJQc/b+MAu3iQb8LeExCjl2JoP4/RlpudkYpg1AIZVCVCjAiElDYmVJYDWiJXPLkFN/g=="],
|
||||
|
||||
"@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="],
|
||||
|
||||
"moment": ["moment@2.30.1", "", {}, "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="],
|
||||
}
|
||||
}
|
||||
8
package.json
Normal file
8
package.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"moment": "^2.30.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/lucide": "^1.2.42"
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@
|
||||
"@nuxt/ui": "3.1.1",
|
||||
"eslint": "^9.0.0",
|
||||
"nuxt": "^3.17.2",
|
||||
"nuxt-app": "file:",
|
||||
"typescript": "^5.6.3",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.1",
|
||||
@@ -1445,6 +1446,8 @@
|
||||
|
||||
"nuxt": ["nuxt@3.17.2", "", { "dependencies": { "@nuxt/cli": "^3.25.0", "@nuxt/devalue": "^2.0.2", "@nuxt/devtools": "^2.4.0", "@nuxt/kit": "3.17.2", "@nuxt/schema": "3.17.2", "@nuxt/telemetry": "^2.6.6", "@nuxt/vite-builder": "3.17.2", "@unhead/vue": "^2.0.8", "@vue/shared": "^3.5.13", "c12": "^3.0.3", "chokidar": "^4.0.3", "compatx": "^0.2.0", "consola": "^3.4.2", "cookie-es": "^2.0.0", "defu": "^6.1.4", "destr": "^2.0.5", "devalue": "^5.1.1", "errx": "^0.1.0", "esbuild": "^0.25.3", "escape-string-regexp": "^5.0.0", "estree-walker": "^3.0.3", "exsolve": "^1.0.5", "globby": "^14.1.0", "h3": "^1.15.3", "hookable": "^5.5.3", "ignore": "^7.0.4", "impound": "^1.0.0", "jiti": "^2.4.2", "klona": "^2.0.6", "knitwork": "^1.2.0", "magic-string": "^0.30.17", "mlly": "^1.7.4", "mocked-exports": "^0.1.1", "nanotar": "^0.2.0", "nitropack": "^2.11.11", "nypm": "^0.6.0", "ofetch": "^1.4.1", "ohash": "^2.0.11", "on-change": "^5.0.1", "oxc-parser": "^0.68.1", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.1.0", "radix3": "^1.1.2", "scule": "^1.3.0", "semver": "^7.7.1", "std-env": "^3.9.0", "strip-literal": "^3.0.0", "tinyglobby": "0.2.13", "ufo": "^1.6.1", "ultrahtml": "^1.6.0", "uncrypto": "^0.1.3", "unctx": "^2.4.1", "unimport": "^5.0.1", "unplugin": "^2.3.2", "unplugin-vue-router": "^0.12.0", "unstorage": "^1.16.0", "untyped": "^2.0.0", "vue": "^3.5.13", "vue-bundle-renderer": "^2.1.1", "vue-devtools-stub": "^0.1.0", "vue-router": "^4.5.1" }, "peerDependencies": { "@parcel/watcher": "^2.1.0", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "optionalPeers": ["@parcel/watcher", "@types/node"], "bin": { "nuxi": "bin/nuxt.mjs", "nuxt": "bin/nuxt.mjs" } }, "sha512-zPEGeGlHoMCFf+Y9I7iEZKhdfsRq0Zf2qE8wEEcjP9T6omzm776h9KVzoj3+qPL1v0rGzSyCFslFtxk+Ey6neA=="],
|
||||
|
||||
"nuxt-app": ["nuxt-app@file:", { "dependencies": { "@internationalized/date": "^3.8.0", "@nuxt/eslint": "1.3.0", "@nuxt/test-utils": "3.18.0", "@nuxt/ui": "3.1.1", "eslint": "^9.0.0", "nuxt": "^3.17.2", "nuxt-app": "file:", "typescript": "^5.6.3", "vue": "^3.5.13", "vue-router": "^4.5.1" } }],
|
||||
|
||||
"nypm": ["nypm@0.6.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "pathe": "^2.0.3", "pkg-types": "^2.0.0", "tinyexec": "^0.3.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-mn8wBFV9G9+UFHIrq+pZ2r2zL4aPau/by3kJb3cM7+5tQHMt6HGQB8FDIeKFYp8o0D2pnH6nVsO88N4AmUxIWg=="],
|
||||
|
||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||
@@ -2197,6 +2200,8 @@
|
||||
|
||||
"nuxt/unimport": ["unimport@5.0.1", "", { "dependencies": { "acorn": "^8.14.1", "escape-string-regexp": "^5.0.0", "estree-walker": "^3.0.3", "local-pkg": "^1.1.1", "magic-string": "^0.30.17", "mlly": "^1.7.4", "pathe": "^2.0.3", "picomatch": "^4.0.2", "pkg-types": "^2.1.0", "scule": "^1.3.0", "strip-literal": "^3.0.0", "tinyglobby": "^0.2.13", "unplugin": "^2.3.2", "unplugin-utils": "^0.2.4" } }, "sha512-1YWzPj6wYhtwHE+9LxRlyqP4DiRrhGfJxdtH475im8ktyZXO3jHj/3PZ97zDdvkYoovFdi0K4SKl3a7l92v3sQ=="],
|
||||
|
||||
"nuxt-app/nuxt-app": ["nuxt-app@file:.", {}],
|
||||
|
||||
"nypm/tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
|
||||
|
||||
"p-event/p-timeout": ["p-timeout@5.1.0", "", {}, "sha512-auFDyzzzGZZZdHz3BtET9VEz0SE/uMEAx7uWfGPucfzEwwe/xH0iVeZibQmANYE/hp9T2+UUZT5m+BKyrDp3Ew=="],
|
||||
|
||||
@@ -5,6 +5,10 @@ import Title1 from './Title1.vue';
|
||||
|
||||
const selectedDate = shallowRef(new CalendarDate(2024, 6, 29));
|
||||
|
||||
const dropDownItems = ref<DropDownMenuItems>([
|
||||
{label: "Profile", icon: "i-lucide-user"}
|
||||
])
|
||||
|
||||
defineProps<{
|
||||
todos: string[]
|
||||
}>()
|
||||
@@ -12,20 +16,39 @@ defineProps<{
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UCard class="w-64 h-full">
|
||||
<template #header class="flex flex-col gap-2">
|
||||
<Title1>Calendar</Title1>
|
||||
<UCalendar v-model="selectedDate" />
|
||||
</template>
|
||||
<template #default class="flex-col gap-2">
|
||||
<Title1>Todos</Title1>
|
||||
<div class="flex gap-2 flex-col">
|
||||
<ListItem v-for="todo in todos">{{ todo }}</ListItem>
|
||||
<UCard class="flex w-64 h-full">
|
||||
<div class="flex flex-col h-full w-full">
|
||||
<header>
|
||||
<Title1>Calendar</Title1>
|
||||
<UCalendar v-model="selectedDate" />
|
||||
</header>
|
||||
<div class="flex flex-col grow justify-between">
|
||||
<div>
|
||||
<Title1>Todos</Title1>
|
||||
<div class="flex gap-2 flex-col">
|
||||
<ListItem v-for="todo in todos">{{ todo }}</ListItem>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<UButton class="w-full flex justify-center">
|
||||
+
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
<UButton class="flex w-full justify-center ga">
|
||||
+
|
||||
</UButton>
|
||||
</template>
|
||||
<footer class="pt-4">
|
||||
<UDropdownMenu
|
||||
:items="dropDownItems"
|
||||
:ui="{
|
||||
content: 'w-full'
|
||||
}"
|
||||
>
|
||||
<UButton class="flex gap-1 items-center">
|
||||
<UAvatar src="https://github.com/benjamincanac.png" />
|
||||
Sebastian Peinbauer
|
||||
</Ubutton>
|
||||
</UDropdownMenu>
|
||||
</footer>
|
||||
</div>
|
||||
</UCard>
|
||||
</template>
|
||||
|
||||
|
||||
113
web/components/ui/calendar/Calendar.vue
Normal file
113
web/components/ui/calendar/Calendar.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import CalendarHeader from './CalendarHeader.vue';
|
||||
import CalendarCollumn from './CalendarCollumn.vue';
|
||||
import moment, { type Moment } from 'moment';
|
||||
|
||||
const events = defineModel<Event[]>('events', { required: true })
|
||||
const date = ref(moment())
|
||||
const draggedEvent = ref<DraggedEvent | undefined>()
|
||||
|
||||
type Day = {
|
||||
date: Moment
|
||||
events: CollissionWrapper[][]
|
||||
}
|
||||
|
||||
const week = computed(() => {
|
||||
return moment(date.value).startOf('isoWeek')
|
||||
})
|
||||
|
||||
function pushEventWithCollisionUpdate(array: CollissionWrapper[], event: Event, collisions: CollissionWrapper[], collisionCount: number) {
|
||||
array.push({event: event, collisions: collisionCount })
|
||||
|
||||
for (let collision of collisions) {
|
||||
collision.collisions = collisionCount
|
||||
}
|
||||
}
|
||||
|
||||
const days = computed<Day[]>(() => {
|
||||
return [1, 2, 3, 4, 5, 6, 7].map((i) => {
|
||||
const filteredEvents = events.value.filter(
|
||||
(event) => event.from >= moment(week.value).weekday(i).startOf('day') && event.to <= moment(week.value).weekday(i).endOf('day')
|
||||
)
|
||||
|
||||
const sortedEvents = filteredEvents.sort((a, b) => a.from.valueOf() - b.from.valueOf())
|
||||
|
||||
const columns: CollissionWrapper[][] = [[]]
|
||||
|
||||
for (let event of sortedEvents) {
|
||||
let collisions: CollissionWrapper[] = []
|
||||
for (let i = 0; i < columns.length; i++) {
|
||||
const column = columns[i]
|
||||
if (column.length === 0) {
|
||||
pushEventWithCollisionUpdate(column, event, collisions, collisions.length)
|
||||
break
|
||||
}
|
||||
|
||||
if (event.from.valueOf() > column[column.length - 1].event.to.valueOf()) {
|
||||
pushEventWithCollisionUpdate(column, event, collisions, collisions.length)
|
||||
break
|
||||
} else {
|
||||
collisions.push(column[column.length - 1])
|
||||
}
|
||||
|
||||
if (columns.length === i + 1) {
|
||||
columns.push([])
|
||||
pushEventWithCollisionUpdate(columns[i + 1], event, collisions, collisions.length)
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
date: moment(week.value).weekday(i),
|
||||
events: columns
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: 'create', timespan: Event): void
|
||||
}>()
|
||||
|
||||
const seperators = ref<Seperator[]>([
|
||||
{ text: '3 AM', time: moment().hour(3) },
|
||||
{ text: '6 AM', time: moment().hour(6) },
|
||||
{ text: '9 AM', time: moment().hour(9) },
|
||||
{ text: '12 PM', time: moment().hour(12) },
|
||||
{ text: '3 PM', time: moment().hour(15) },
|
||||
{ text: '6 PM', time: moment().hour(18) },
|
||||
{ text: '9 PM', time: moment().hour(21) },
|
||||
])
|
||||
|
||||
function quickCreate(date: Moment, timespan: Timespan) {
|
||||
const eventTitle = prompt("Event title")
|
||||
|
||||
if (eventTitle === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const newEvent: Event = new Event(
|
||||
eventTitle,
|
||||
moment(date).startOf('day').minutes(timespan.from * 24 * 60),
|
||||
moment(date).startOf('day').minutes(timespan.to * 24 * 60)
|
||||
)
|
||||
|
||||
emits('create', newEvent)
|
||||
events.value.push(newEvent)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full h-full flex flex-col">
|
||||
<div class="calendar flex flex-row w-full flex-1 items-stretch">
|
||||
<CalendarHeader :seperators="seperators" />
|
||||
|
||||
<CalendarCollumn v-for="day in days" :seperators="seperators" :day="day.date" :events="day.events" v-model:draggedEvent="draggedEvent"
|
||||
@quick-create="quickCreate" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
119
web/components/ui/calendar/CalendarCollumn.vue
Normal file
119
web/components/ui/calendar/CalendarCollumn.vue
Normal file
@@ -0,0 +1,119 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, useTemplateRef } from 'vue';
|
||||
import CalendarSeperator from './CalendarSeperator.vue';
|
||||
import type { Moment } from 'moment';
|
||||
import CalendarEvent from './CalendarEvent.vue';
|
||||
import moment from 'moment';
|
||||
|
||||
const props = defineProps<{
|
||||
seperators: Seperator[],
|
||||
day: Moment
|
||||
events: CollissionWrapper[][]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'quick-create', day: Moment, event: EventDimensions): void,
|
||||
}>()
|
||||
|
||||
const isDragging = ref(false)
|
||||
const startY = ref(0)
|
||||
const endY = ref(0)
|
||||
const column = useTemplateRef('column')
|
||||
const draggedEvent = defineModel<DraggedEvent | undefined>('draggedEvent')
|
||||
|
||||
const height = computed(() => {
|
||||
return Math.abs(endY.value - startY.value)
|
||||
})
|
||||
|
||||
const top = computed(() => {
|
||||
return Math.min(startY.value, endY.value)
|
||||
})
|
||||
|
||||
function mousedown(e: MouseEvent) {
|
||||
startY.value = absoluteToRelativeY(e.clientY)
|
||||
endY.value = absoluteToRelativeY(e.clientY)
|
||||
isDragging.value = true
|
||||
}
|
||||
|
||||
function mouseover(e: MouseEvent) {
|
||||
if (!isDragging.value) {
|
||||
return
|
||||
}
|
||||
|
||||
endY.value = absoluteToRelativeY(e.clientY)
|
||||
}
|
||||
|
||||
function absoluteToRelativeY(n: number) {
|
||||
return n - (column.value?.getBoundingClientRect().top ?? 0)
|
||||
}
|
||||
|
||||
function mouseup(_: MouseEvent) {
|
||||
isDragging.value = false
|
||||
|
||||
if (column.value === null) {
|
||||
console.error('column element is undefined')
|
||||
return
|
||||
}
|
||||
|
||||
const timeFrom = Math.min(endY.value, startY.value) / column.value.offsetHeight
|
||||
const timeTo = Math.max(endY.value, startY.value) / column.value.offsetHeight
|
||||
emit('quick-create', moment(props.day), {
|
||||
from: timeFrom,
|
||||
to: timeTo
|
||||
})
|
||||
|
||||
startY.value = 0
|
||||
endY.value = 0
|
||||
}
|
||||
|
||||
function eventMove(mouseEvent: MouseEvent, event: Event) {
|
||||
draggedEvent.value = event.toDraggedEvent(column.value?.offsetHeight ?? 0, absoluteToRelativeY(mouseEvent.clientY))
|
||||
}
|
||||
|
||||
function dragover(e: DragEvent) {
|
||||
if (draggedEvent.value === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!draggedEvent.value.date.isSame(props.day)) {
|
||||
draggedEvent.value.date = props.day
|
||||
}
|
||||
|
||||
|
||||
draggedEvent.value.top = absoluteToRelativeY(e.clientY) - draggedEvent.value.offset
|
||||
}
|
||||
|
||||
function dragDrop(_: DragEvent) {
|
||||
draggedEvent.value?.target.updateWithDraggedEvent(draggedEvent.value, column.value?.offsetHeight ?? 0)
|
||||
draggedEvent.value = undefined
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col h-full grow">
|
||||
<div class="flex justify-center items-center flex-col bg-gray-600 h-18 text-white border-b-2 border-white">
|
||||
<div>{{ props.day.format('dd').toUpperCase() }}</div>
|
||||
<div>{{ props.day.date() }}</div>
|
||||
</div>
|
||||
|
||||
<div id="col" ref="column" @mousedown="mousedown" @mouseup="mouseup" @mousemove="mouseover" @dragover="dragover"
|
||||
@dragend="dragDrop" class="bg-gray-600 text-white relative flex flex-col grow items-center">
|
||||
<CalendarSeperator v-for="sep in seperators" :seperator="sep">
|
||||
<hr class="w-full">
|
||||
</CalendarSeperator>
|
||||
<div class="absolute w-11/12 top-20 bg-black opacity-45 rounded-lg"
|
||||
:style="{ height: `${height}px`, top: `${top}px` }"></div>
|
||||
|
||||
<div v-for="[index, column] in events.entries()" class="flex flex-row w-11/12 h-full absolute top-0">
|
||||
<CalendarEvent v-for="event in column" :event="event" :columnIndex="index" @move="eventMove" />
|
||||
</div>
|
||||
|
||||
<div v-if="draggedEvent !== undefined && draggedEvent.date.isSame(props.day)" class="absolute w-11/12 top-20 bg-black opacity-45 rounded-lg"
|
||||
:style="{ height: `${draggedEvent.height}px`, top: `${draggedEvent.top}px` }"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<style scoped></style>
|
||||
50
web/components/ui/calendar/CalendarEvent.vue
Normal file
50
web/components/ui/calendar/CalendarEvent.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
event: CollissionWrapper
|
||||
columnIndex: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'move', mouseEvent: MouseEvent, event: Event): void,
|
||||
}>()
|
||||
|
||||
const visible = ref(true)
|
||||
|
||||
const dimensions = computed<EventDimensions>(() => {
|
||||
return props.event.event.getPercentDimensions()
|
||||
})
|
||||
|
||||
const left = computed(() => {
|
||||
return (100 / (props.event.collisions + 1)) * props.columnIndex
|
||||
})
|
||||
|
||||
const widht = computed(() => {
|
||||
return (100 / (props.event.collisions + 1))
|
||||
})
|
||||
|
||||
const height = computed(() => {
|
||||
return Math.abs(dimensions.value.from - dimensions.value.to)
|
||||
})
|
||||
|
||||
const top = computed(() => {
|
||||
return Math.min(dimensions.value.from, dimensions.value.to)
|
||||
})
|
||||
|
||||
function dragStart(e: DragEvent) {
|
||||
console.log("start drag")
|
||||
emit('move', e, props.event.event)
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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"
|
||||
: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.title }}</div>
|
||||
</div>
|
||||
</template>
|
||||
24
web/components/ui/calendar/CalendarHeader.vue
Normal file
24
web/components/ui/calendar/CalendarHeader.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import CalendarSeperator from './CalendarSeperator.vue';
|
||||
|
||||
defineProps<{
|
||||
seperators: Seperator[],
|
||||
}>()
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col h-full grow">
|
||||
<div class="flex justify-center items-center bg-gray-600 h-18 text-white border-b-white border-b-2">vue-calendar
|
||||
</div>
|
||||
<div class="calendar-legend bg-gray-600 text-white relative flex flex-col grow justify-evenly">
|
||||
<CalendarSeperator v-for="sep in seperators" :seperator="sep">
|
||||
{{ sep.text }}
|
||||
</CalendarSeperator>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
20
web/components/ui/calendar/CalendarSeperator.vue
Normal file
20
web/components/ui/calendar/CalendarSeperator.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import type { Moment } from 'moment';
|
||||
|
||||
defineProps<{
|
||||
seperator: Seperator
|
||||
}>();
|
||||
|
||||
const relativePositionOf = function (time: Moment) {
|
||||
return `${(time.hours() / 24) * 100}%`
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :style="{top: relativePositionOf(seperator.time)}" class="h-10 w-full flex justify-center items-center text-white border-white absolute -translate-y-1/2">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -16,6 +16,7 @@
|
||||
"@nuxt/ui": "3.1.1",
|
||||
"eslint": "^9.0.0",
|
||||
"nuxt": "^3.17.2",
|
||||
"nuxt-app": "file:",
|
||||
"typescript": "^5.6.3",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.1"
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import Sidebar from '~/components/ui/Sidebar.vue';
|
||||
import Calendar from '~/components/ui/calendar/Calendar.vue'
|
||||
import moment, { type Moment } from 'moment';
|
||||
|
||||
const todos = ["Staistics", "Computer Graphics", "Webdev"]
|
||||
const events = ref([])
|
||||
const date = ref(moment())
|
||||
|
||||
onMounted(() => {
|
||||
date.value.subtract(10,"days")
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-screen w-screen p-4">
|
||||
<div class="h-screen w-screen p-4 flex flex-row gap-5">
|
||||
<Sidebar :todos="todos" />
|
||||
<div class="flex grow">
|
||||
<Calendar v-model:events="events" v-model:date="date"></Calendar>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
142
web/utils/event.ts
Normal file
142
web/utils/event.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import type { Moment } from "moment"
|
||||
import moment from "moment"
|
||||
|
||||
export class Event {
|
||||
private static readonly MINUTES_IN_DAY = 24 * 60
|
||||
|
||||
constructor(
|
||||
public readonly title: string,
|
||||
public from: Moment,
|
||||
public to: Moment
|
||||
) { }
|
||||
|
||||
|
||||
getPercentDimensions(): EventDimensions {
|
||||
const start_of_day = moment(this.from).startOf('day')
|
||||
const from_percentage = this.from.diff(start_of_day, 'minutes') / Event.MINUTES_IN_DAY
|
||||
const to_percentage = this.to.diff(start_of_day, 'minutes') / Event.MINUTES_IN_DAY
|
||||
return {
|
||||
from: from_percentage * 100,
|
||||
to: to_percentage * 100
|
||||
}
|
||||
}
|
||||
|
||||
getPixelDimensions(maxHeight: number): EventDimensions {
|
||||
const percentDimensions = this.getPercentDimensions()
|
||||
return {
|
||||
from: (percentDimensions.from / 100) * maxHeight,
|
||||
to: (percentDimensions.to / 100) * maxHeight
|
||||
}
|
||||
}
|
||||
|
||||
static fromSimple(event: SimpleEvent): Event {
|
||||
return new Event(event.title, event.from, event.to)
|
||||
}
|
||||
|
||||
static fromSerializable(event: SerializableEvent) {
|
||||
return new Event(event.title, moment(event.from), moment(event.to))
|
||||
}
|
||||
|
||||
static fromPercentDimensions(title: string, dimensions: EventDimensions, date: Moment): Event {
|
||||
return new Event(
|
||||
title,
|
||||
moment(date).startOf('day').minutes((dimensions.from / 100) * Event.MINUTES_IN_DAY),
|
||||
moment(date).startOf('day').minutes((dimensions.to / 100) * Event.MINUTES_IN_DAY)
|
||||
)
|
||||
}
|
||||
|
||||
static fromPixelDimensions(title: string, dimensions: EventDimensions, height: number, date: Moment): Event {
|
||||
const percentDimensions: EventDimensions = {
|
||||
from: dimensions.from * 100 / height,
|
||||
to: dimensions.to * 100 / height
|
||||
}
|
||||
|
||||
return Event.fromPercentDimensions(title, percentDimensions, date)
|
||||
}
|
||||
|
||||
static fromDraggedEvent(draggedEvent: DraggedEvent, height: number): Event {
|
||||
const pixelDimensions: EventDimensions = {
|
||||
from: draggedEvent.top,
|
||||
to: draggedEvent.top + draggedEvent.height
|
||||
}
|
||||
|
||||
return Event.fromPixelDimensions(draggedEvent.target.title, pixelDimensions, height, draggedEvent.date)
|
||||
}
|
||||
|
||||
updateWithDraggedEvent(draggedEvent: DraggedEvent, height: number): Event {
|
||||
const newEventData = Event.fromDraggedEvent(draggedEvent, height)
|
||||
|
||||
this.from = newEventData.from
|
||||
this.to = newEventData.to
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
|
||||
toSimple(): SimpleEvent {
|
||||
return {
|
||||
title: this.title,
|
||||
from: this.from,
|
||||
to: this.to
|
||||
}
|
||||
}
|
||||
|
||||
toSerializable(): SerializableEvent {
|
||||
return {
|
||||
title: this.title,
|
||||
from: this.from.toISOString(),
|
||||
to: this.to.toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
withCollisions(collisions: number = 0): CollissionWrapper {
|
||||
return {
|
||||
event: this,
|
||||
collisions
|
||||
}
|
||||
}
|
||||
|
||||
toDraggedEvent(height: number, mouseY: number): DraggedEvent {
|
||||
const pixelDimensions = this.getPixelDimensions(height)
|
||||
const offset = mouseY - pixelDimensions.from
|
||||
|
||||
return {
|
||||
date: moment(this.from).startOf('day'),
|
||||
top: pixelDimensions.from,
|
||||
height: pixelDimensions.to - pixelDimensions.from,
|
||||
target: this,
|
||||
offset: offset
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export type EventDimensions = {
|
||||
from: number,
|
||||
to: number
|
||||
}
|
||||
|
||||
export type SimpleEvent = {
|
||||
title: string,
|
||||
from: Moment,
|
||||
to: Moment
|
||||
}
|
||||
|
||||
export type SerializableEvent = {
|
||||
title: string,
|
||||
from: string,
|
||||
to: string
|
||||
}
|
||||
|
||||
export type CollissionWrapper = {
|
||||
event: Event,
|
||||
collisions: number
|
||||
}
|
||||
|
||||
export type DraggedEvent = {
|
||||
date: Moment,
|
||||
top: number,
|
||||
height: number,
|
||||
target: Event,
|
||||
offset: number
|
||||
}
|
||||
42
web/utils/lib.ts
Normal file
42
web/utils/lib.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { Moment } from "moment"
|
||||
|
||||
export type Seperator = {
|
||||
text: string,
|
||||
time: Moment
|
||||
}
|
||||
|
||||
export type Timespan = {
|
||||
from: number,
|
||||
to: number
|
||||
}
|
||||
|
||||
export type Event = AnonymousEvent & {
|
||||
title: string
|
||||
}
|
||||
|
||||
export type AnonymousEvent = {
|
||||
from: Moment,
|
||||
to: Moment
|
||||
}
|
||||
|
||||
export type SerializableEvent = {
|
||||
title: string,
|
||||
from: string,
|
||||
to: string
|
||||
}
|
||||
|
||||
export type EventWithCollisions = Event & {
|
||||
collisions: number
|
||||
}
|
||||
|
||||
export type EventDimensions = {
|
||||
from: number,
|
||||
to: number
|
||||
}
|
||||
|
||||
export function percentToPixelDimensions(dimensions: EventDimensions, totalHeight: number): EventDimensions {
|
||||
return {
|
||||
from: (dimensions.from / 100) * totalHeight,
|
||||
to: (dimensions.to / 100) * totalHeight
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user