Files
apps/calendar/desktop/Events/DesktopEventDetails.js
metacryst 0d6c7683ff init
2026-04-28 20:05:00 -05:00

397 lines
15 KiB
JavaScript

import server from "/@server/server.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 isOwner = this.event.creator_id === global.profile?.id
VStack(() => {
this.renderHeader(isOwner)
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(isOwner) {
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")
if (isOwner) {
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)