import server from "/calendar/@server/calendar.js" import calendarUtil from "../../calendarUtil.js" import "../../../components/Avatar.js" class DesktopEventDetails extends Shadow { attachmentsOpen = false constructor(calendars, event, onUpdated = null, onDeleted = null, onEdit = null) { super() this.calendars = calendars this.event = event this.attachmentsOpen = (event?.attachments?.length > 0) this.onUpdated = onUpdated this.onDeleted = onDeleted this.onEdit = onEdit } render() { if (!this.event) return const eventCals = this.calendars.filter(c => this.event.calendars?.includes(c.id)) const canEdit = global.currentNetwork.permissions.includes("events.edit") VStack(() => { this.renderHeader(canEdit) this.renderBody(eventCals) HStack(() => { const members = global.currentNetwork.data?.members || [] const creator = members.find(m => m.id === this.event.creator_id) if (creator) { Avatar(creator, 1.6) VStack(() => { p(`Created ${calendarUtil.timeAgo(this.event.created)} by ${creator.first_name}`) .margin(0).fontSize(0.7, em).color("var(--headertext)").opacity(0.5) if (this.event.updated_at && this.event.updated_at !== this.event.created) { p(`Last updated ${calendarUtil.timeAgo(this.event.updated_at)}`) .margin(0).fontSize(0.7, em).color("var(--headertext)").opacity(0.4) } }) .gap(0.15, em) } }) .paddingHorizontal(1, em) .paddingVertical(0.65, em) .boxSizing("border-box") .alignItems("center") .gap(0.5, em) .flexShrink(0) }) .height(100, pct) .boxSizing("border-box") } // ── Header ──────────────────────────────────────────────────────────────── renderHeader(canEdit) { HStack(() => { VStack(() => { h2(this.event.title || "Untitled") .margin(0) .fontSize(1.45, em) .fontWeight("700") .color("var(--headertext)") .lineHeight("1.2") }) .flex(1) .paddingHorizontal(1.4, em) .paddingTop(2.5, em) .paddingBottom(0.5, em) .justifyContent("center") // Non-owners never see the controls that open the edit form or delete flow. if (canEdit) { button("Delete") .paddingVertical(0.34, em) .paddingHorizontal(0.85, em) .border("1px solid var(--quillred)") .borderRadius(0.45, em) .background("transparent") .color("var(--quillred)") .cursor("pointer") .fontSize(0.8, em) .fontWeight("600") .marginRight(0.5, em) .marginTop("auto") .marginBottom(1, em) .flexShrink(0) .onClick((done) => { if (done) this.handleDelete() }) .onHover(function(hovering) { this.style.background = hovering ? "var(--quillred)" : "transparent"; this.style.color = hovering ? "white" : "var(--quillred)"; }) button("Edit") .paddingVertical(0.34, em) .paddingHorizontal(0.85, em) .border("1px solid var(--divider)") .borderRadius(0.45, em) .background("transparent") .color("var(--headertext)") .cursor("pointer") .fontSize(0.8, em) .marginRight(1.4, em) .marginTop("auto") .marginBottom(1, em) .flexShrink(0) .onClick((done) => { if (!done) return // Attach template dates for override events so scope='all' anchors correctly let eventForEdit = this.event if (this.event.recurrence_parent_id && !this.event._templateStart) { const template = global.currentNetwork.data.events.find(e => e.id === this.event.recurrence_parent_id) if (template) { eventForEdit = { ...this.event, _templateStart: new Date(template.time_start), _templateEnd: new Date(template.time_end) } } } this.onEdit(eventForEdit) }) .onHover(function(hovering) { this.style.background = hovering ? "var(--divider)" : "transparent"; }) } }) .width(100, pct) .alignItems("stretch") .background("var(--darkaccent)") .borderBottom("1px solid var(--divider)") .boxSizing("border-box") .flexShrink(0) } // ── Body ───────────────────────────────────────────────────────────────── renderBody(eventCals) { VStack(() => { VStack(() => { this.prop("WHEN", () => { p(calendarUtil.formatEventTime(this.event)) .margin(0) .fontSize(0.88, em) .color("var(--headertext)") }) if (this.event.recurrence) { this.prop("REPEATS", () => { p(this._recurrenceLabel()) .margin(0) .fontSize(0.88, em) .color("var(--headertext)") }) } this.prop("CALENDARS", () => { HStack(() => { eventCals.forEach(cal => { p(cal.name) .margin(0) .fontSize(0.78, em) .fontWeight("600") .color("white") .paddingHorizontal(0.65, em) .paddingVertical(0.28, em) .background(cal.color) .borderRadius(0.45, em) }) }) .flexWrap("wrap") .gap(0.45, em) }) if (this.event.location) { this.prop("LOCATION", () => { p(this.event.location) .margin(0) .fontSize(0.88, em) .color("var(--headertext)") .lineHeight("1.5") }) } if (this.event.description) { this.prop("DESCRIPTION", () => { p(this.event.description) .margin(0) .fontSize(0.88, em) .color("var(--headertext)") .lineHeight("1.65") .whiteSpace("pre-wrap") }) } }) if (this.event.attachments?.length > 0) { this.renderAttachments() } }) .flex(1) .overflowY("scroll") .width(100, pct) .boxSizing("border-box") } prop(label, contentFn) { VStack(() => { p(label) .margin(0) .marginBottom(1, em) .fontSize(0.67, em) .fontWeight("600") .letterSpacing("0.06em") .color("var(--headertext)") .opacity(0.38) VStack(() => { contentFn() }) .width(100, pct) }) .paddingHorizontal(1.5, em) .paddingTop(0.75, em) .paddingBottom(0.4, em) .boxSizing("border-box") .width(100, pct) } _recurrenceLabel() { const r = this.event.recurrence if (!r) return "" if (r.frequency === 'daily') return "Daily" if (r.frequency === 'weekly' && r.interval === 2) return "Every 2 weeks" if (r.frequency === 'weekly') return "Weekly" if (r.frequency === 'monthly') return "Monthly" if (r.frequency === 'yearly') return "Yearly" return "" } // ── Attachments ─────────────────────────────────────────────────────────── renderAttachments() { VStack(() => { HStack(() => { p("Attachments") .margin(0) .fontSize(0.82, em) .fontWeight("600") .color("var(--headertext)") .opacity(0.4) p("▼") .attr({ id: "desktop-attachments-chevron" }) .margin(0) .fontSize(0.65, em) .color("var(--headertext)") .opacity(0.4) .display("inline-block") .transition("transform 0.22s ease") .transform(this.attachmentsOpen ? "rotate(180deg)" : "rotate(0deg)") .userSelect("none") }) .gap(0.5, em) .alignItems("center") .cursor("pointer") .onClick((done) => { if (!done) return; this.toggleAttachments() }) VStack(() => { const images = this.event.attachments.filter(f => f.type?.startsWith("image/")) const files = this.event.attachments.filter(f => !f.type?.startsWith("image/")) if (images.length > 0) { HStack(() => { images.forEach(file => { const url = `${config.SERVER}/db/images/events/${file.name}` VStack(() => { img(url, "100%", "100%") .objectFit("cover") .display("block") }) .width("6.5em") .height("6.5em") .flexShrink(0) .border("1px solid var(--divider)") .borderRadius(6, px) .overflow("hidden") .cursor("pointer") .onClick((done) => { if (!done) return; $("filepreview-").open(file, url) }) }) }) .flexWrap("wrap") .gap(0.5, em) .boxSizing("border-box") .width(100, pct) } if (files.length > 0) { VStack(() => { files.forEach(file => this.renderFile(file)) }) .gap(0.5, em) .width("max-content") .boxSizing("border-box") } }) .attr({ id: "desktop-attachments-content" }) .width(100, pct) .display(this.attachmentsOpen ? "" : "none") .gap(1, em) }) .width(100, pct) .boxSizing("border-box") .paddingHorizontal(1.5, em) .paddingVertical(0.85, em) .gap(1, em) } renderFile(file) { const url = `${config.SERVER}/db/images/events/${file.name}` HStack(() => { p("📎") .margin(0) .fontSize(0.9, em) p(file.original_name ?? file.name) .margin(0) .color("var(--headertext)") .fontSize(0.85, em) .overflow("hidden") .whiteSpace("nowrap") .textOverflow("ellipsis") }) .gap(0.5, em) .alignItems("center") .padding(0.55, em) .background("var(--darkaccent)") .border("1px solid var(--divider)") .borderRadius(0.45, em) .boxSizing("border-box") .cursor("pointer") .onClick((done) => { if (!done) return; $("filepreview-").open(file, url) }) } toggleAttachments() { this.attachmentsOpen = !this.attachmentsOpen const content = this.$("#desktop-attachments-content") const chevron = this.$("#desktop-attachments-chevron") if (content) content.style.display = this.attachmentsOpen ? "" : "none" if (chevron) chevron.style.transform = this.attachmentsOpen ? "rotate(180deg)" : "rotate(0deg)" } handleDelete() { const event = this.event const isRecurring = !!(event?._isOccurrence || event?.recurrence_parent_id || event?.recurrence_id) if (isRecurring) { $('actionsheetpopup-').show( "Delete Recurring Event", [ { label: "Delete just this event", onTap: () => this.performDelete('single') }, { label: "Delete this and future events", onTap: () => this.performDelete('future') }, { label: "Delete all events in series", onTap: () => this.performDelete('all') }, ], () => {} ) return } this.performDelete(null) } async performDelete(scope) { const event = this.event const isOverride = !!event.recurrence_parent_id const templateId = isOverride ? event.recurrence_parent_id : event.id const occurrenceDate = isOverride ? (event.recurrence_exception_date instanceof Date ? event.recurrence_exception_date.toISOString() : event.recurrence_exception_date) ?? null : event._occurrenceDate?.toISOString() ?? null const serverEventId = (scope === 'single' && isOverride) ? event.id : templateId try { const result = await server.deleteEvent(serverEventId, global.currentNetwork.id, scope, occurrenceDate) if (result.status === 200) { $("modal-").forceClose() const deleteResult = { scope: scope ?? 'all', templateId, occurrenceDate, overrideId: isOverride ? event.id : null } if (this.onDeleted) this.onDeleted(deleteResult) } else { $("modal-")?.showError(result.error ?? "Failed to delete event.") } } catch (err) { console.error("Failed to delete event:", err) $("modal-")?.showError("Failed to delete event.") } } } register(DesktopEventDetails)