built create event UI
This commit is contained in:
42
ui/src/api/calendar.ts
Normal file
42
ui/src/api/calendar.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
export interface CalendarEvent {
|
||||
name: string,
|
||||
start: Date,
|
||||
end: Date,
|
||||
location: string,
|
||||
color: string,
|
||||
description: string,
|
||||
creator: any | null, // user object
|
||||
id: number | null
|
||||
}
|
||||
|
||||
export enum CalendarAttendance {
|
||||
Attending = "attending",
|
||||
NotAttending = "not_attending",
|
||||
Maybe = "maybe"
|
||||
}
|
||||
|
||||
export interface CalendarSignup {
|
||||
memberID: number,
|
||||
eventID: number,
|
||||
state: CalendarAttendance
|
||||
}
|
||||
|
||||
export async function createCalendarEvent(eventData: CalendarEvent) {
|
||||
|
||||
}
|
||||
|
||||
export async function editCalendarEvent(eventData: CalendarEvent) {
|
||||
|
||||
}
|
||||
|
||||
export async function cancelCalendarEvent(eventID: number) {
|
||||
|
||||
}
|
||||
|
||||
export async function adminCancelCalendarEvent(eventID: number) {
|
||||
|
||||
}
|
||||
|
||||
export async function setCalendarEventAttendance(eventID: number, state: CalendarAttendance) {
|
||||
|
||||
}
|
||||
257
ui/src/components/calendar/CreateCalendarEvent.vue
Normal file
257
ui/src/components/calendar/CreateCalendarEvent.vue
Normal file
@@ -0,0 +1,257 @@
|
||||
<script setup lang="ts">
|
||||
import { toTypedSchema } from "@vee-validate/zod"
|
||||
import { ref, defineExpose, watch } from "vue"
|
||||
import * as z from "zod"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import Textarea from "../ui/textarea/Textarea.vue"
|
||||
import { CalendarEvent } from "@/api/calendar"
|
||||
|
||||
// ---------- helpers ----------
|
||||
const dateRe = /^\d{4}-\d{2}-\d{2}$/ // YYYY-MM-DD
|
||||
const timeRe = /^(?:[01]\d|2[0-3]):[0-5]\d$/ // HH:mm (24h)
|
||||
|
||||
function toLocalDateString(d: Date) {
|
||||
// yyyy-MM-dd with local time zone
|
||||
const y = d.getFullYear()
|
||||
const m = String(d.getMonth() + 1).padStart(2, "0")
|
||||
const day = String(d.getDate()).padStart(2, "0")
|
||||
return `${y}-${m}-${day}`
|
||||
}
|
||||
function toLocalTimeString(d: Date) {
|
||||
const hh = String(d.getHours()).padStart(2, "0")
|
||||
const mm = String(d.getMinutes()).padStart(2, "0")
|
||||
return `${hh}:${mm}`
|
||||
}
|
||||
function roundToNextHour(d = new Date()) {
|
||||
const t = new Date(d)
|
||||
t.setMinutes(0, 0, 0)
|
||||
t.setHours(t.getHours() + 1)
|
||||
return t
|
||||
}
|
||||
function parseLocalDateTime(dateStr: string, timeStr: string) {
|
||||
// Construct a Date in the user's local timezone
|
||||
const [y, m, d] = dateStr.split("-").map(Number)
|
||||
const [hh, mm] = timeStr.split(":").map(Number)
|
||||
return new Date(y, m - 1, d, hh, mm, 0, 0)
|
||||
}
|
||||
|
||||
// ---------- schema ----------
|
||||
const zEvent = z.object({
|
||||
name: z.string().min(2, "Please enter at least 2 characters").max(100),
|
||||
startDate: z.string().regex(dateRe, "Use YYYY-MM-DD"),
|
||||
startTime: z.string().regex(timeRe, "Use HH:mm (24h)"),
|
||||
endDate: z.string().regex(dateRe, "Use YYYY-MM-DD"),
|
||||
endTime: z.string().regex(timeRe, "Use HH:mm (24h)"),
|
||||
location: z.string().max(200).default(""),
|
||||
color: z.string().regex(/^#([0-9A-Fa-f]{6})$/, "Use a hex color like #AABBCC"),
|
||||
description: z.string().max(2000).default(""),
|
||||
id: z.number().int().nonnegative().nullable().default(null),
|
||||
}).superRefine((vals, ctx) => {
|
||||
const start = parseLocalDateTime(vals.startDate, vals.startTime)
|
||||
const end = parseLocalDateTime(vals.endDate, vals.endTime)
|
||||
if (!(end > start)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "End must be after start",
|
||||
path: ["endTime"], // attach to a visible field
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const formSchema = toTypedSchema(zEvent)
|
||||
|
||||
// ---------- dialog state & defaults ----------
|
||||
const dialogOpen = ref(false)
|
||||
function openDialog() { dialogOpen.value = true }
|
||||
defineExpose({ openDialog })
|
||||
|
||||
function makeInitialValues() {
|
||||
const start = roundToNextHour()
|
||||
const end = new Date(start.getTime() + 60 * 60 * 1000)
|
||||
return {
|
||||
name: "",
|
||||
startDate: toLocalDateString(start),
|
||||
startTime: toLocalTimeString(start),
|
||||
endDate: toLocalDateString(end),
|
||||
endTime: toLocalTimeString(end),
|
||||
location: "",
|
||||
color: "#3b82f6",
|
||||
description: "",
|
||||
id: null as number | null,
|
||||
}
|
||||
}
|
||||
const initialValues = ref(makeInitialValues())
|
||||
|
||||
const formKey = ref(0)
|
||||
|
||||
watch(dialogOpen, (isOpen) => {
|
||||
if (!isOpen) {
|
||||
formKey.value++ // remounts the form -> picks up fresh initialValues
|
||||
}
|
||||
})
|
||||
// ---------- submit ----------
|
||||
function onSubmit(vals: z.infer<typeof zEvent>) {
|
||||
const start = parseLocalDateTime(vals.startDate, vals.startTime)
|
||||
const end = parseLocalDateTime(vals.endDate, vals.endTime)
|
||||
|
||||
const event: CalendarEvent = {
|
||||
name: vals.name,
|
||||
start,
|
||||
end,
|
||||
location: vals.location,
|
||||
color: vals.color,
|
||||
description: vals.description,
|
||||
id: null,
|
||||
creator: null
|
||||
}
|
||||
|
||||
console.log("Submitting CalendarEvent:", event)
|
||||
|
||||
// close after success
|
||||
dialogOpen.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Form :key="formKey" v-slot="{ handleSubmit, resetForm }" :validation-schema="formSchema"
|
||||
:initial-values="initialValues" keep-values as="">
|
||||
<Dialog v-model:open="dialogOpen">
|
||||
<DialogContent class="sm:max-w-[520px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Event</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<form id="dialogForm" class="grid grid-cols-1 gap-4"
|
||||
@submit="handleSubmit($event, (vals) => { onSubmit(vals); resetForm({ values: initialValues }); })">
|
||||
|
||||
<div class="flex gap-3 items-start w-full">
|
||||
<!-- Name -->
|
||||
<div class="flex-1">
|
||||
<FormField v-slot="{ componentField }" name="name">
|
||||
<FormItem>
|
||||
<FormLabel>Event Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="text" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<!-- Color -->
|
||||
<div class="w-[60px]">
|
||||
<FormField v-slot="{ componentField }" name="color">
|
||||
<FormItem>
|
||||
<FormLabel>Color</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="color" class="h-[38px] p-1 cursor-pointer"
|
||||
v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- Start: date + time -->
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<FormField v-slot="{ componentField }" name="startDate">
|
||||
<FormItem>
|
||||
<FormLabel>Start Date</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="date" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField v-slot="{ componentField }" name="startTime">
|
||||
<FormItem>
|
||||
<FormLabel>Start Time</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="text" v-bind="componentField" />
|
||||
<!-- If you ever want native picker: type="time" -->
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<!-- End: date + time -->
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<FormField v-slot="{ componentField }" name="endDate">
|
||||
<FormItem>
|
||||
<FormLabel>End Date</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="date" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField v-slot="{ componentField }" name="endTime">
|
||||
<FormItem>
|
||||
<FormLabel>End Time</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="text" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<!-- Location -->
|
||||
<FormField v-slot="{ componentField }" name="location">
|
||||
<FormItem>
|
||||
<FormLabel>Location</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="text" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
|
||||
<!-- Description -->
|
||||
<FormField v-slot="{ componentField }" name="description">
|
||||
<FormItem>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea class="resize-none h-32" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<!-- Hidden id -->
|
||||
<FormField v-slot="{ componentField }" name="id">
|
||||
<input type="hidden" v-bind="componentField" />
|
||||
</FormField>
|
||||
</form>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="submit" form="dialogForm">Create</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Form>
|
||||
</template>
|
||||
@@ -4,6 +4,7 @@ import FullCalendar from '@fullcalendar/vue3'
|
||||
import dayGridPlugin from '@fullcalendar/daygrid'
|
||||
import interactionPlugin from '@fullcalendar/interaction'
|
||||
import { X, Clock, MapPin, User, ListTodo, ChevronLeft, ChevronRight, Plus } from 'lucide-vue-next'
|
||||
import CreateCalendarEvent from '@/components/calendar/CreateCalendarEvent.vue'
|
||||
|
||||
const monthLabels = [
|
||||
'January', 'February', 'March', 'April', 'May', 'June',
|
||||
@@ -63,16 +64,19 @@ function onEventClick(arg: any) {
|
||||
panelOpen.value = true
|
||||
}
|
||||
|
||||
const dialogRef = ref<any>(null)
|
||||
|
||||
// NEW: handle day/time slot clicks to start creating an event
|
||||
function onDateClick(arg: { dateStr: string }) {
|
||||
dialogRef.value?.openDialog();
|
||||
// For now, just open the panel with a draft payload.
|
||||
activeEvent.value = {
|
||||
id: '__draft__',
|
||||
title: 'New event',
|
||||
start: arg.dateStr,
|
||||
extendedProps: { draft: true }
|
||||
}
|
||||
panelOpen.value = true
|
||||
// activeEvent.value = {
|
||||
// id: '__draft__',
|
||||
// title: 'New event',
|
||||
// start: arg.dateStr,
|
||||
// extendedProps: { draft: true }
|
||||
// }
|
||||
// panelOpen.value = true
|
||||
}
|
||||
|
||||
const calendarOptions = ref({
|
||||
@@ -127,6 +131,8 @@ const calendarOptions = ref({
|
||||
},
|
||||
})
|
||||
|
||||
//@ts-ignore (shhh)
|
||||
calendarOptions.value.datesSet = onDatesSet
|
||||
|
||||
|
||||
watch(panelOpen, async () => {
|
||||
@@ -165,6 +171,8 @@ const ext = computed(() => activeEvent.value?.extendedProps ?? {})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CreateCalendarEvent ref="dialogRef"></CreateCalendarEvent>
|
||||
|
||||
<div class="flex">
|
||||
<div class="flex-1 min-h-0 mt-5">
|
||||
<div class="h-[80vh] min-h-0">
|
||||
@@ -196,20 +204,20 @@ const ext = computed(() => activeEvent.value?.extendedProps ?? {})
|
||||
<!-- Right: nav + today + create -->
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md border hover:bg-muted/40"
|
||||
class="cursor-pointer inline-flex h-8 w-8 items-center justify-center rounded-md border hover:bg-muted/40"
|
||||
aria-label="Previous month" @click="goPrev">
|
||||
<ChevronLeft class="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md border hover:bg-muted/40"
|
||||
class="cursor-pointer inline-flex h-8 w-8 items-center justify-center rounded-md border hover:bg-muted/40"
|
||||
aria-label="Next month" @click="goNext">
|
||||
<ChevronRight class="h-4 w-4" />
|
||||
</button>
|
||||
<button class="ml-1 rounded-md border px-3 py-1.5 text-sm hover:bg-muted/40" @click="goToday">
|
||||
<button class="cursor-pointer ml-1 rounded-md border px-3 py-1.5 text-sm hover:bg-muted/40" @click="goToday">
|
||||
Today
|
||||
</button>
|
||||
<button
|
||||
class="ml-1 inline-flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-sm text-primary-foreground hover:opacity-90"
|
||||
class="cursor-pointer ml-1 inline-flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-sm text-primary-foreground hover:opacity-90"
|
||||
@click="onCreateEvent">
|
||||
<Plus class="h-4 w-4" />
|
||||
Create
|
||||
|
||||
Reference in New Issue
Block a user