added edit modal. info popover. create modal. delete functionality. delete modal
This commit is contained in:
@@ -21,6 +21,7 @@
|
|||||||
"typescript": "^5.6.3",
|
"typescript": "^5.6.3",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
"vue-router": "^4.5.1",
|
"vue-router": "^4.5.1",
|
||||||
|
"zod": "^3.25.64",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -2045,7 +2046,7 @@
|
|||||||
|
|
||||||
"zip-stream": ["zip-stream@6.0.1", "", { "dependencies": { "archiver-utils": "^5.0.0", "compress-commons": "^6.0.2", "readable-stream": "^4.0.0" } }, "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA=="],
|
"zip-stream": ["zip-stream@6.0.1", "", { "dependencies": { "archiver-utils": "^5.0.0", "compress-commons": "^6.0.2", "readable-stream": "^4.0.0" } }, "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA=="],
|
||||||
|
|
||||||
"zod": ["zod@3.24.4", "", {}, "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg=="],
|
"zod": ["zod@3.25.64", "", {}, "sha512-hbP9FpSZf7pkS7hRVUrOjhwKJNyampPgtXKc3AN6DsWtoHsg2Sb4SQaS4Tcay380zSwd2VPo9G9180emBACp5g=="],
|
||||||
|
|
||||||
"zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="],
|
"zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="],
|
||||||
|
|
||||||
@@ -2077,6 +2078,8 @@
|
|||||||
|
|
||||||
"@mapbox/node-pre-gyp/detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
|
"@mapbox/node-pre-gyp/detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
|
||||||
|
|
||||||
|
"@modelcontextprotocol/sdk/zod": ["zod@3.24.4", "", {}, "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg=="],
|
||||||
|
|
||||||
"@netlify/functions/is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="],
|
"@netlify/functions/is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="],
|
||||||
|
|
||||||
"@netlify/zip-it-and-ship-it/@babel/types": ["@babel/types@7.26.10", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ=="],
|
"@netlify/zip-it-and-ship-it/@babel/types": ["@babel/types@7.26.10", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ=="],
|
||||||
@@ -2099,6 +2102,8 @@
|
|||||||
|
|
||||||
"@netlify/zip-it-and-ship-it/resolve": ["resolve@2.0.0-next.5", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA=="],
|
"@netlify/zip-it-and-ship-it/resolve": ["resolve@2.0.0-next.5", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA=="],
|
||||||
|
|
||||||
|
"@netlify/zip-it-and-ship-it/zod": ["zod@3.24.4", "", {}, "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg=="],
|
||||||
|
|
||||||
"@nodelib/fs.scandir/@nodelib/fs.stat": ["@nodelib/fs.stat@4.0.0", "", {}, "sha512-ctr6bByzksKRCV0bavi8WoQevU6plSp2IkllIsEqaiKe2mwNNnaluhnRhcsgGZHrrHk57B3lf95MkLMO3STYcg=="],
|
"@nodelib/fs.scandir/@nodelib/fs.stat": ["@nodelib/fs.stat@4.0.0", "", {}, "sha512-ctr6bByzksKRCV0bavi8WoQevU6plSp2IkllIsEqaiKe2mwNNnaluhnRhcsgGZHrrHk57B3lf95MkLMO3STYcg=="],
|
||||||
|
|
||||||
"@nuxt/cli/pkg-types": ["pkg-types@2.1.0", "", { "dependencies": { "confbox": "^0.2.1", "exsolve": "^1.0.1", "pathe": "^2.0.3" } }, "sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A=="],
|
"@nuxt/cli/pkg-types": ["pkg-types@2.1.0", "", { "dependencies": { "confbox": "^0.2.1", "exsolve": "^1.0.1", "pathe": "^2.0.3" } }, "sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A=="],
|
||||||
@@ -2191,6 +2196,8 @@
|
|||||||
|
|
||||||
"eslint/find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
|
"eslint/find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
|
||||||
|
|
||||||
|
"eslint/zod": ["zod@3.24.4", "", {}, "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg=="],
|
||||||
|
|
||||||
"eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
"eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
||||||
|
|
||||||
"eslint-plugin-import-x/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
"eslint-plugin-import-x/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||||
|
|||||||
122
web/components/ui/EventFormModal.vue
Normal file
122
web/components/ui/EventFormModal.vue
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { type SimpleEvent } from '#imports';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
import * as z from 'zod';
|
||||||
|
|
||||||
|
const open = defineModel<boolean>('open', { required: true })
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'submnitted', event: SimpleEvent): void
|
||||||
|
(e: 'canceled'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
action: 'create' | 'edit'
|
||||||
|
input: Partial<SimpleEvent>,
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
const titleField = ref('')
|
||||||
|
const descriptionField = ref('')
|
||||||
|
const fromTimeField = ref('')
|
||||||
|
const fromDateField = ref('')
|
||||||
|
const toTimeField = ref('')
|
||||||
|
const toDateField = ref('')
|
||||||
|
|
||||||
|
const modalTitle = computed(() => {
|
||||||
|
return props.action === 'create' ? 'Create Event' : 'Edit Event'
|
||||||
|
})
|
||||||
|
|
||||||
|
const modalDescription = computed(() => {
|
||||||
|
return props.action === 'create' ? 'Create event and set name, description and time' : 'Edit name, description and time'
|
||||||
|
})
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
titleField.value = props.input.title ?? ''
|
||||||
|
descriptionField.value = props.input.description ?? ''
|
||||||
|
fromTimeField.value = props.input.from?.toFormat('HH:mm') ?? ''
|
||||||
|
fromDateField.value = props.input.from?.toFormat('yyyy-MM-dd') ?? ''
|
||||||
|
toTimeField.value = props.input.to?.toFormat('HH:mm') ?? ''
|
||||||
|
toDateField.value = props.input.to?.toFormat('yyyy-MM-dd') ?? ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
title: z.string().trim().min(1, { message: 'Title is required' }),
|
||||||
|
description: z.string().default(''),
|
||||||
|
from: z.string().datetime({ message: 'Invalid start date', local: true }),
|
||||||
|
to: z.string().datetime({ message: 'Invalid end date', local: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
function submit() {
|
||||||
|
console.log({
|
||||||
|
from: dateStringFromFields(fromDateField.value, fromTimeField.value),
|
||||||
|
to: dateStringFromFields(toDateField.value, toTimeField.value)
|
||||||
|
})
|
||||||
|
const form = formSchema.safeParse({
|
||||||
|
title: titleField.value,
|
||||||
|
description: descriptionField.value,
|
||||||
|
from: dateStringFromFields(fromDateField.value, fromTimeField.value),
|
||||||
|
to: dateStringFromFields(toDateField.value, toTimeField.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (form.error) {
|
||||||
|
for (const error of form.error.errors) {
|
||||||
|
toast.add({
|
||||||
|
title: error.message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('submnitted', {
|
||||||
|
title: form.data.title,
|
||||||
|
description: form.data.description,
|
||||||
|
from: DateTime.fromISO(form.data.from),
|
||||||
|
to: DateTime.fromISO(form.data.to)
|
||||||
|
})
|
||||||
|
open.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function dateStringFromFields(date: string, time: string) {
|
||||||
|
return `${date}T${time}:00`
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancel() {
|
||||||
|
emit('canceled')
|
||||||
|
open.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UModal v-model:open="open" :title="modalTitle" :description="modalDescription">
|
||||||
|
<template #body>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<UInput type="text" placeholder="Name" v-model="titleField" required />
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<UInput class="grow" placeholder="2025-06-16" v-model="fromDateField" icon="i-lucide-calendar"
|
||||||
|
required />
|
||||||
|
<UInput class="grow" placeholder="15:34" v-model="fromTimeField" icon="i-lucide-clock" required />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<UInput class="grow" placeholder="2025-06-16" v-model="toDateField" icon="i-lucide-calendar"
|
||||||
|
required />
|
||||||
|
<UInput class="grow" placeholder="15:34" v-model="toTimeField" icon="i-lucide-clock" required />
|
||||||
|
</div>
|
||||||
|
<UTextarea type="text" placeholder="Description" v-model="descriptionField" required />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<UButton variant="solid" @click="submit">
|
||||||
|
{{ action === 'create' ? 'Create' : 'Edit' }}
|
||||||
|
</UButton>
|
||||||
|
<UButton variant="soft" @click="cancel">
|
||||||
|
Cancel
|
||||||
|
</UButton>
|
||||||
|
</template>
|
||||||
|
</UModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -2,34 +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 { Event } from '~/utils/event';
|
import { Event, type SimpleEvent } from '~/utils/event';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { UTextarea } from '#components';
|
import EventFormModal from '../EventFormModal.vue';
|
||||||
|
|
||||||
const events = defineModel<Event[]>('events', { required: true })
|
const events = defineModel<Event[]>('events', { required: true })
|
||||||
const date = defineModel<DateTime>('date', { required: true })
|
const date = defineModel<DateTime>('date', { required: true })
|
||||||
const draggedEvent = ref<DraggedEvent | undefined>()
|
const draggedEvent = ref<DraggedEvent | undefined>()
|
||||||
|
const createInput = ref<Partial<SimpleEvent>>({})
|
||||||
const createModalOpened = ref(false)
|
const createModalOpened = ref(false)
|
||||||
const createContext = ref<{ date: DateTime, timespan: Timespan } | undefined>(undefined)
|
const editInput = ref<Partial<SimpleEvent>>({})
|
||||||
const createModal = {
|
const editContext = ref<{ event: Event }>()
|
||||||
open: ref(false),
|
const editModalOpened = ref(false)
|
||||||
fromTimeField: ref<string>(''),
|
const deleteModalOpened = ref(false)
|
||||||
toTimeField: ref<string>(''),
|
const deleteContext = ref<{ event: Event }>()
|
||||||
toDateField: ref<string>(''),
|
|
||||||
fromDateField: ref<string>(''),
|
|
||||||
nameField: ref<string>(''),
|
|
||||||
descriptionField: ref<string>(''),
|
|
||||||
clear: () => {
|
|
||||||
createModal.nameField.value = ''
|
|
||||||
createModal.fromDateField.value = ''
|
|
||||||
createModal.toDateField.value = ''
|
|
||||||
createModal.fromTimeField.value = ''
|
|
||||||
createModal.toTimeField.value = ''
|
|
||||||
createModal.descriptionField.value = ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const toast = useToast()
|
|
||||||
|
|
||||||
type Day = {
|
type Day = {
|
||||||
date: DateTime
|
date: DateTime
|
||||||
@@ -93,6 +79,8 @@ const days = computed<Day[]>(() => {
|
|||||||
|
|
||||||
const emits = defineEmits<{
|
const emits = defineEmits<{
|
||||||
(e: 'create', event: Event): void
|
(e: 'create', event: Event): void
|
||||||
|
(e: 'edit', event: Event): void
|
||||||
|
(e: 'delete', event: Event): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const hour = (num: number) => {
|
const hour = (num: number) => {
|
||||||
@@ -110,93 +98,74 @@ const seperators = ref<Seperator[]>([
|
|||||||
])
|
])
|
||||||
|
|
||||||
function openCreateModal(date: DateTime, timespan: Timespan) {
|
function openCreateModal(date: DateTime, timespan: Timespan) {
|
||||||
createContext.value = {
|
createInput.value = {
|
||||||
date: date,
|
from: date.startOf('day').plus({ minutes: timespan.from * 24 * 60 }),
|
||||||
timespan: timespan
|
to: date.startOf('day').plus({ minutes: timespan.to * 24 * 60 }),
|
||||||
}
|
}
|
||||||
|
|
||||||
const from = date.startOf('day').plus({ minutes: timespan.from * 24 * 60 })
|
createModalOpened.value = true
|
||||||
const to = date.startOf('day').plus({ minutes: timespan.to * 24 * 60 })
|
|
||||||
|
|
||||||
createModal.fromDateField.value = from.toISODate() ?? ''
|
|
||||||
createModal.toDateField.value = to.toISODate() ?? ''
|
|
||||||
createModal.fromTimeField.value = from.toFormat('HH:mm')
|
|
||||||
createModal.toTimeField.value = to.toFormat('HH:mm')
|
|
||||||
createModal.open.value = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function create() {
|
function create(simple: SimpleEvent) {
|
||||||
const from = dateFromFields(createModal.fromDateField.value, createModal.fromTimeField.value)
|
const event = Event.fromSimple(simple)
|
||||||
const to = dateFromFields(createModal.toDateField.value, createModal.toTimeField.value)
|
|
||||||
|
|
||||||
if (!from.isValid) {
|
|
||||||
toast.add({
|
|
||||||
title: 'Invalid `from` date format'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!to.isValid) {
|
|
||||||
toast.add({
|
|
||||||
title: 'Invalid `to` date format'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (createModal.nameField.value.trim() === '') {
|
|
||||||
toast.add({
|
|
||||||
title: 'Name is required'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const event = new Event(createModal.nameField.value, from, to, createModal.descriptionField.value)
|
|
||||||
emits('create', event)
|
|
||||||
createModal.clear()
|
|
||||||
createModal.open.value = false
|
|
||||||
events.value.push(event)
|
events.value.push(event)
|
||||||
|
emits('create', event)
|
||||||
}
|
}
|
||||||
|
|
||||||
function dateFromFields(date: string, time: string) {
|
function openEditModal(event: Event) {
|
||||||
return DateTime.fromFormat(`${date} ${time}`, 'yyyy-MM-dd HH:mm')
|
editInput.value = event.toSimple()
|
||||||
|
editContext.value = { event: event }
|
||||||
|
editModalOpened.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function edit(simple: SimpleEvent) {
|
||||||
|
editContext.value?.event.updateWithSimple(simple)
|
||||||
|
if (editContext.value === undefined) return
|
||||||
|
emits('edit', editContext.value.event)
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDeleteModal(event: Event) {
|
||||||
|
deleteContext.value = { event: event }
|
||||||
|
deleteModalOpened.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteEvent() {
|
||||||
|
if (deleteContext.value === undefined) return
|
||||||
|
emits('delete', deleteContext.value?.event)
|
||||||
|
events.value = events.value.filter(e => e.title !== deleteContext.value?.event.title)
|
||||||
|
deleteModalOpened.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveEvent(event: Event) {
|
||||||
|
emits('edit', event)
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full flex flex-col">
|
<div class="w-full h-full flex flex-col">
|
||||||
<UModal v-model:open="createModal.open.value" title="Create Event"
|
<EventFormModal action="create" @submnitted="event => create(event)" :input="createInput"
|
||||||
description="Set name and change time of event before creation">
|
v-model:open="createModalOpened" />
|
||||||
<template #body>
|
<EventFormModal action="edit" @submnitted="event => edit(event)" :input="editInput"
|
||||||
<div class="flex flex-col gap-2">
|
v-model:open="editModalOpened" />
|
||||||
<UInput type="text" placeholder="Name" v-model="createModal.nameField.value" required />
|
|
||||||
<div class="flex flex-row gap-2">
|
<UModal v-model:open="deleteModalOpened" title="Delete Event" description="Are you sure you want to delete this event?">
|
||||||
<UInput class="grow" placeholder="2025-06-16" v-model="createModal.fromDateField.value"
|
|
||||||
icon="i-lucide-calendar" required />
|
|
||||||
<UInput class="grow" placeholder="15:34" v-model="createModal.fromTimeField.value"
|
|
||||||
icon="i-lucide-clock" required />
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-row gap-2">
|
|
||||||
<UInput class="grow" placeholder="2025-06-16" v-model="createModal.toDateField.value"
|
|
||||||
icon="i-lucide-calendar" required />
|
|
||||||
<UInput class="grow" placeholder="15:34" v-model="createModal.toTimeField.value"
|
|
||||||
icon="i-lucide-clock" required />
|
|
||||||
</div>
|
|
||||||
<UTextarea type="text" placeholder="Description" v-model="createModal.descriptionField.value"
|
|
||||||
required />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<UButton variant="solid" @click="create">
|
<UButton variant="solid" @click="deleteEvent">
|
||||||
Create
|
Delete
|
||||||
</UButton>
|
</UButton>
|
||||||
<UButton variant="soft">
|
<UButton variant="solid" @click="deleteModalOpened = false">
|
||||||
Cancel
|
Cancel
|
||||||
</UButton>
|
</UButton>
|
||||||
</template>
|
</template>
|
||||||
</UModal>
|
</UModal>
|
||||||
|
|
||||||
<div class="calendar flex flex-row w-full flex-1 items-stretch divide-x divide-muted">
|
<div class="calendar flex flex-row w-full flex-1 items-stretch divide-x divide-muted">
|
||||||
<CalendarHeader :seperators="seperators" />
|
<CalendarHeader :seperators="seperators" />
|
||||||
|
|
||||||
<CalendarCollumn v-for="day in days" :seperators="seperators" :day="day.date" :events="day.events"
|
<CalendarCollumn v-for="day in days" :seperators="seperators" :day="day.date" :events="day.events"
|
||||||
:date="date" v-model:draggedEvent="draggedEvent" @quick-create="openCreateModal" />
|
:date="date" v-model:draggedEvent="draggedEvent" @quick-create="openCreateModal"
|
||||||
|
@edit="openEditModal" @delete="openDeleteModal" @moved="moveEvent" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'quick-create', day: DateTime, event: EventDimensions): void,
|
(e: 'quick-create', day: DateTime, event: EventDimensions): void,
|
||||||
|
(e: 'edit', event: Event): void
|
||||||
|
(e: 'moved', event: Event): void
|
||||||
|
(e: 'delete', event: Event): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const isDragging = ref(false)
|
const isDragging = ref(false)
|
||||||
@@ -87,6 +90,13 @@ function dragover(e: DragEvent) {
|
|||||||
|
|
||||||
function dragDrop(_: DragEvent) {
|
function dragDrop(_: DragEvent) {
|
||||||
draggedEvent.value?.target.updateWithDraggedEvent(draggedEvent.value, column.value?.offsetHeight ?? 0)
|
draggedEvent.value?.target.updateWithDraggedEvent(draggedEvent.value, column.value?.offsetHeight ?? 0)
|
||||||
|
|
||||||
|
if (draggedEvent.value === undefined){
|
||||||
|
draggedEvent.value = undefined
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('moved', draggedEvent.value.target)
|
||||||
draggedEvent.value = undefined
|
draggedEvent.value = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +106,8 @@ function dragDrop(_: DragEvent) {
|
|||||||
<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.toFormat('ccc').toUpperCase() }}</div>
|
<div>{{ props.day.toFormat('ccc').toUpperCase() }}</div>
|
||||||
<UBadge class="rounded-full" v-if="date.startOf('day').equals(day.startOf('day'))">{{ props.day.day }}</UBadge>
|
<UBadge class="rounded-full" v-if="date.startOf('day').equals(day.startOf('day'))">{{ props.day.day }}
|
||||||
|
</UBadge>
|
||||||
<div v-else>{{ props.day.day }}</div>
|
<div v-else>{{ props.day.day }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -109,7 +120,8 @@ function dragDrop(_: DragEvent) {
|
|||||||
:style="{ height: `${height}px`, top: `${top}px` }"></div>
|
: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">
|
<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" />
|
<CalendarEvent v-for="event in column" :event="event" :columnIndex="index" @move="eventMove"
|
||||||
|
@edit="event => emit(`edit`, event)" @delete="event => emit(`delete`, event)" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="draggedEvent !== undefined && draggedEvent.date.equals(props.day)"
|
<div v-if="draggedEvent !== undefined && draggedEvent.date.equals(props.day)"
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'move', mouseEvent: MouseEvent, event: Event): void,
|
(e: 'move', mouseEvent: MouseEvent, event: Event): void,
|
||||||
|
(e: 'edit', event: Event): void
|
||||||
|
(e: 'delete', event: Event): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const visible = ref(true)
|
const visible = ref(true)
|
||||||
@@ -42,10 +44,31 @@ function dragStart(e: DragEvent) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<UPopover :content="{ side: 'right' }" arrow>
|
||||||
<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.toFormat('HH:mm') }} - {{ event.event.to.toFormat('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 #content>
|
||||||
|
<UCard class="w-xl">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex flex-row justify-between items-center">
|
||||||
|
<h1>{{ event.event.title }}</h1>
|
||||||
|
<nav class="flex flex-row gap-2">
|
||||||
|
<UButton icon="i-lucide-pencil" @click="emit('edit', event.event)"></UButton>
|
||||||
|
<UButton icon="i-lucide-trash" @click="emit('delete', event.event)"></UButton>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #default>
|
||||||
|
<div>{{ event.event.from.toFormat('HH:mm') }} - {{ event.event.to.toFormat('HH:mm') }}</div>
|
||||||
|
<div class="overflow-scroll pb-5">
|
||||||
|
{{ event.event.description }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UCard>
|
||||||
|
</template>
|
||||||
|
</UPopover>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
"nuxt-app": "file:",
|
"nuxt-app": "file:",
|
||||||
"typescript": "^5.6.3",
|
"typescript": "^5.6.3",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
"vue-router": "^4.5.1"
|
"vue-router": "^4.5.1",
|
||||||
|
"zod": "^3.25.64"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,15 @@ export class Event {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateWithSimple(simple: SimpleEvent): Event {
|
||||||
|
this.title = simple.title
|
||||||
|
this.from = simple.from
|
||||||
|
this.to = simple.to
|
||||||
|
this.description = simple.description
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
static fromSimple(event: SimpleEvent): Event {
|
static fromSimple(event: SimpleEvent): Event {
|
||||||
return new Event(event.title, event.from, event.to, event.description)
|
return new Event(event.title, event.from, event.to, event.description)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user