import server from "/@server/server.js" import calendarUtil from "../calendarUtil.js" import "./EventForm.js" import "../../components/BottomSheet.js" import "../../components/BackButton.js" import "../../components/Avatar.js" css(` eventdetails- { display: flex; flex-direction: column; height: 100%; scrollbar-width: none; -ms-overflow-style: none; } eventdetails- ::-webkit-scrollbar { display: none; width: 0; height: 0; } eventdetails- ::-webkit-scrollbar-thumb { background: transparent; } eventdetails- ::-webkit-scrollbar-track { background: transparent; } #eventdetails-toast-wrap { transition: max-height 0.25s ease, opacity 0.22s ease, padding-top 0.25s ease; } `) class EventDetails extends Shadow { attachmentsOpen = false; selectedCalendars = []; _pendingCalendars = null; _prevCalendars = null; constructor(calendars, event = null, onEventEdited = null, onEventDeleted = null) { super() this.calendars = calendars; this.event = event; this.onEventEdited = onEventEdited; this.onEventDeleted = onEventDeleted; this.selectedCalendars = calendars.filter(c => event?.calendars?.includes(c.id)); } render() { this.editSheet = BottomSheet(100) // separate sheet for the edit form, layered above this one const isOwner = this.event?.creator_id === global.profile?.id; VStack(() => { this.renderHeader(isOwner) // ── Error toast ─────────────────────────────────────── VStack(() => { p("") .attr({ id: "eventdetails-toast" }) .margin(0) .padding("0.55em 1.1em") .background("var(--quillred)") .color("white") .fontSize(0.85, em) .fontWeight("500") .fontFamily("Arial") .borderRadius("0.5em") .boxShadow("0 2px 10px rgba(0,0,0,0.15)") .whiteSpace("nowrap") }) .attr({ id: "eventdetails-toast-wrap" }) .alignItems("center") .overflow("hidden") .maxHeight(0) .opacity(0) .flexShrink(0) // ── Scrollable body ─────────────────────────────────── VStack(() => { if (!this.event) return // VStack(() => {}).height(0.85, em).flexShrink(0) // ── Calendar / Location / Notes card ────────────── VStack(() => { // Calendar row — tappable to expand HStack(() => { p("Calendar") .margin(0).fontSize(0.92, em).color("var(--headertext)").flexShrink(0) HStack(() => { this.selectedCalendars.forEach(cal => { HStack(() => { p("").width(0.6, em).height(0.6, em) .background(cal.color) .borderRadius(50, pct) .flexShrink(0) p(cal.name) .margin(0) .fontSize(0.82, em) .fontWeight("600") .color("var(--text)") .whiteSpace("nowrap") }) .gap(0.3, em) .alignItems("center") }) }) .attr({ id: "calendar-display" }) .flex(1) .justifyContent("flex-end") .flexWrap("wrap") .gap(0.5, em) let chevron = p("›") chevron .attr({ id: "cal-chevron" }) .margin(0).opacity(0.3).fontSize(1.1, em).flexShrink(0) .transition("transform 0.25s ease") .state(chevron.parentElement, "open", function (value) { if(value === "true") { this.style.transform = "rotate(90deg)" } else { console.log("no trans") this.style.transform = "" } }) }) .attr({id: "calendar-row"}) .paddingHorizontal(1, em).paddingVertical(0.78, em) .alignItems("center").gap(0.5, em) .borderBottom("1px solid var(--divider)").cursor("pointer") .onTap(function () { const isOpen = this.getAttribute("open") === "true" if (isOpen) { this.setAttribute("open", "false") } else { this.setAttribute("open", "true") } }) // Calendar Expandable List VStack(() => { this.calendars.forEach(cal => { const isSelected = this.selectedCalendars.some(c => c.id === cal.id) HStack(() => { HStack(() => { p("").width(0.65, em).height(0.65, em) .background(cal.color).borderRadius(50, pct).flexShrink(0) p(cal.name) .margin(0).fontSize(0.9, em).color("var(--text)").fontFamily("Arial") }) .gap(0.45, em).alignItems("center").flex(1) p("✓") .attr({ id: `cal-check-${cal.id}` }) .margin(0).fontSize(0.88, em) .color("var(--quillred)").fontWeight("700") .display(isSelected ? "" : "none") }) .paddingHorizontal(1.25, em).paddingVertical(0.72, em) .alignItems("center") .borderBottom("1px solid var(--divider)") .cursor("pointer") .onTap(() => { const prevCalendars = [...this.selectedCalendars] const i = this.selectedCalendars.findIndex(c => c.id === cal.id) if (i >= 0) { if (this.selectedCalendars.length > 1) { this.selectedCalendars.splice(i, 1) const check = this.$(`#cal-check-${cal.id}`) if (check) check.style.display = "none" } } else { this.selectedCalendars.push(cal) const check = this.$(`#cal-check-${cal.id}`) if (check) check.style.display = "" } this.updateCalendarDisplay() this.saveCalendars(prevCalendars) }) }) }) .state(this.$("#calendar-row"), "open", function (value) { if(value === "false") { this.style.maxHeight = "0" } else { this.style.maxHeight = this.scrollHeight + "px" } }) .attr({ id: "cal-picker"}) .overflow("hidden").maxHeight(0) .transition("max-height 0.3s ease") // Location row if (this.event.location) { HStack(() => { p("📍").margin(0).fontSize(0.85, em).flexShrink(0) p(this.event.location) .margin(0).fontSize(0.9, em).color("var(--text)").fontFamily("Arial") }) .paddingHorizontal(1, em).paddingVertical(0.78, em) .alignItems("center").gap(0.65, em) .borderBottom("1px solid var(--divider)") } // Notes row if (this.event.description) { HStack(() => { p("📝").margin(0).fontSize(0.85, em).flexShrink(0).alignSelf("flex-start").paddingTop(0.1, em) p(this.event.description) .margin(0).fontSize(0.9, em).color("var(--text)").fontFamily("Arial") .whiteSpace("pre-wrap").lineHeight("1.45") }) .paddingHorizontal(1, em).paddingVertical(0.78, em) .alignItems("flex-start").gap(0.65, em) } }) .background("var(--darkaccent)").border("1px solid var(--divider)") .borderRadius(12, px).marginHorizontal(1, em).overflow("hidden").flexShrink(0) // ── Attachments card ────────────────────────────── if (this.event.attachments?.length > 0) { // VStack(() => {}).height(0.85, em).flexShrink(0) VStack(() => { HStack(() => { p("📎").margin(0).fontSize(0.85, em).flexShrink(0) p("Attachments") .margin(0).fontSize(0.92, em).color("var(--headertext)").flex(1) p("▼") .attr({ id: "attachments-chevron" }) .margin(0).fontSize(0.7, em).color("var(--text)").opacity(0.5) .display("inline-block") .transition("transform 0.3s ease") .transform(this.attachmentsOpen ? "rotate(180deg)" : "rotate(0deg)") }) .paddingHorizontal(1, em).paddingVertical(0.78, em) .alignItems("center").gap(0.65, em).cursor("pointer") .onTap(() => this.toggleAttachments()) VStack(() => { VStack(() => { this.event.attachments.forEach(file => this.renderFile(file)) }) .gap(0.75, em).width(100, pct) .padding("0 1em 0.75em").boxSizing("border-box") }) .attr({ id: "attachments-content" }) .overflow("hidden").maxHeight("0") .transition("max-height 0.5s ease") }) .background("var(--darkaccent)").border("1px solid var(--divider)") .borderRadius(12, px).marginHorizontal(1, em).overflow("hidden").flexShrink(0) } }) .overflowY("scroll").flex(1).minHeight(0).paddingTop(0.85, em).paddingBottom(1.5, em).gap(0.85, em) // ── Footer: creator avatar + timestamps ─────────────── if (this.event) { const members = global.currentNetwork.data?.members || [] const creator = members.find(m => m.id === this.event.creator_id) if (creator) { HStack(() => { Avatar(creator, 2) VStack(() => { p(`Created ${calendarUtil.timeAgo(this.event.created)} by ${creator.first_name}`) .margin(0).fontSize(0.9, 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.9, em).color("var(--headertext)").opacity(0.4) } }) .gap(0.15, em) }) .paddingHorizontal(1, em) .paddingVertical(0.65, em) .alignItems("center") .gap(0.5, em) .flexShrink(0) } } }) .height(100, pct) } renderHeader(isOwner) { VStack(() => { HStack(() => { BackButton(false, true, () => $("bottomsheet-").toggle()) if (isOwner) { HStack(() => { // ── Delete button ───────────────────────────────── button("Delete") .attr({ type: "button" }) .padding(0.4, rem) .fontSize(1.25, em) .boxSizing("border-box") .outline("none") .border("none") .background("transparent") .color("var(--quillred)") .onTap(() => this.handleDelete()) button("Edit") .padding(0.4, rem) .fontSize(1.25, em) .color("var(--darkaccent)") .boxSizing("border-box") .outline("none") .border("none") .zIndex(3) .onTap((e) => { e.preventDefault() let formEl const closeForm = () => { this.editSheet._closeOverride = null this.editSheet.setSheet(false) } const onSaveError = () => { this.editSheet._closeOverride = () => this.editSheet.forceClose() } this.editSheet.show(() => { // For override rows, attach template dates so scope='all' anchors correctly let eventForForm = 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) { eventForForm = { ...this.event, _templateStart: new Date(template.time_start), _templateEnd: new Date(template.time_end) }; } } formEl = EventForm( this.calendars, (updateResult) => { closeForm() const updatedEvent = updateResult?.scope ? updateResult.event : updateResult; this.event = { ...updatedEvent, time_start: new Date(updatedEvent.time_start), time_end: new Date(updatedEvent.time_end) } this.selectedCalendars = this.calendars.filter(c => updatedEvent.calendars?.includes(c.id)) setTimeout(() => { this.onEventEdited(updateResult) this.rerender() }, 300) }, eventForForm, closeForm, (deleteResult) => { closeForm() $("bottomsheet-").toggle() this.onEventDeleted(deleteResult) }, null, onSaveError ) }) this.editSheet._closeOverride = () => { this.editSheet.setSheet(true) formEl?.handleBack() } }) }) .fontFamily("Arial") .cursor("pointer") .paddingHorizontal(0.8, rem) .gap(0.4, rem) } }) .width(100, pct) .justifyContent("space-between") .alignItems("center") VStack(() => { h2(this.event?.title ?? "") .color("var(--headertext)") .fontFamily("Arial") .margin(0) .fontSize(1.4, em) p(this.event ? calendarUtil.formatEventTime(this.event) : "") .margin(0) .color("var(--headertext)") .opacity(0.7) .fontSize(0.85, em) }) .paddingHorizontal(1, em) .paddingBottom(1, em) .gap(0.3, em) .alignItems("flex-start") }) .width(100, pct) .background(util.darkMode() ? "var(--darkred)" : "var(--sidebottombars)") .borderTopLeftRadius("10px").borderTopRightRadius("10px") .border("1px solid var(--divider)") .boxSizing("border-box").flexShrink(0) .alignItems("flex-start") } updateCalendarDisplay() { const el = this.$("#calendar-display") if (!el) return el.innerHTML = this.selectedCalendars.map(cal => `

${cal.name}

`).join('') } showError(msg) { const wrap = this.$("#eventdetails-toast-wrap") const toast = this.$("#eventdetails-toast") if (!wrap || !toast) return clearTimeout(this._errorTimer) if (msg) { toast.innerText = msg wrap.style.maxHeight = "3em" wrap.style.opacity = "1" wrap.style.paddingTop = "0.85em" this._errorTimer = setTimeout(() => this.hideError(), 3500) } else { this.hideError() } } hideError() { const wrap = this.$("#eventdetails-toast-wrap") if (!wrap) return clearTimeout(this._errorTimer) wrap.style.maxHeight = "0" wrap.style.opacity = "0" wrap.style.paddingTop = "0" } saveCalendars(prevCalendars) { const event = this.event; const newCalendars = this.selectedCalendars.map(c => c.id); const isRecurring = !!(event._isOccurrence || event.recurrence_parent_id || event.recurrence_id); this._prevCalendars = prevCalendars; this._pendingCalendars = newCalendars; if (isRecurring) { $('actionsheetpopup-').show( "Edit Recurring Event", [ { label: "Edit just this event", onTap: () => this.performCalendarSave('single'), destructive: false }, { label: "Edit this and future events", onTap: () => this.performCalendarSave('future'), destructive: false }, { label: "Edit all events in series", onTap: () => this.performCalendarSave('all'), destructive: false }, ], () => { this._pendingCalendars = null; this._revertCalendars(prevCalendars); } ); return; } this.performCalendarSave(null); } _revertCalendars(prev) { this.selectedCalendars = prev; this.calendars.forEach(cal => { const check = this.$(`#cal-check-${cal.id}`); if (check) check.style.display = prev.some(c => c.id === cal.id) ? "" : "none"; }); this.updateCalendarDisplay(); } async performCalendarSave(scope) { const event = this.event; const newCalendars = this._pendingCalendars ?? this.selectedCalendars.map(c => c.id); this._pendingCalendars = null; const prevCalendars = this._prevCalendars; try { if (scope) { 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; const result = await server.editEvent(serverEventId, { title: event.title, description: event.description ?? null, location: event.location ?? null, time_start: event.time_start instanceof Date ? event.time_start.toISOString() : event.time_start, time_end: event.time_end instanceof Date ? event.time_end.toISOString() : event.time_end, all_day: event.all_day, calendars: newCalendars, scope, exception_date: occurrenceDate }, global.currentNetwork.id); if (result.status === 200) { const editResult = { scope, event: { ...result.event, calendars: newCalendars, attachments: event.attachments ?? [] }, templateId, occurrenceDate }; this.event = { ...editResult.event, time_start: new Date(result.event.time_start), time_end: new Date(result.event.time_end) }; this.selectedCalendars = this.calendars.filter(c => newCalendars.includes(c.id)); this._prevCalendars = null; $("bottomsheet-")._closeOverride = null; this.onEventEdited(editResult); } else { this._revertCalendars(prevCalendars); this.showError(result.error ?? "Failed to update calendars."); $("bottomsheet-")._closeOverride = () => $("bottomsheet-").forceClose(); } } else { const result = await server.editEvent(event.id, { ...event, calendars: newCalendars }, global.currentNetwork.id); if (result.status === 200) { this.event = { ...event, calendars: newCalendars }; this._prevCalendars = null; $("bottomsheet-")._closeOverride = null; this.onEventEdited(this.event); } else { this._revertCalendars(prevCalendars); this.showError(result.error ?? "Failed to update calendars."); $("bottomsheet-")._closeOverride = () => $("bottomsheet-").forceClose(); } } } catch (err) { console.error("Failed to update calendars:", err); this._revertCalendars(prevCalendars); this.showError("Failed to update calendars."); $("bottomsheet-")._closeOverride = () => $("bottomsheet-").forceClose(); } } 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) { $("bottomsheet-").toggle() const deleteResult = { scope: scope ?? 'all', templateId, occurrenceDate, overrideId: isOverride ? event.id : null }; setTimeout(() => this.onEventDeleted(deleteResult), 300) } else { this.showError(result.error ?? "Failed to delete event.") $("bottomsheet-")._closeOverride = () => $("bottomsheet-").forceClose() } } catch (err) { console.error("Failed to delete event:", err) this.showError("Failed to delete event.") $("bottomsheet-")._closeOverride = () => $("bottomsheet-").forceClose() } } renderFile(file) { const isImage = file.type?.startsWith("image/"); const url = `${config.SERVER}/db/images/events/${file.name}`; if (isImage) { img(url, "100%", "100%") .borderRadius(8, px).display("block").boxSizing("border-box") .cursor("pointer") .onTap(() => $("filepreview-")?.open(file, url)) } else { HStack(() => { p("📎").margin(0).fontSize(1, em) p(file.original_name ?? file.name) .margin(0).color("var(--text)").fontSize(0.9, em) .overflow("hidden").whiteSpace("nowrap").textOverflow("ellipsis") }) .gap(0.5, em).alignItems("center") .padding(0.5, em) .background("var(--searchbackground)") .borderRadius(8, px).boxSizing("border-box") .cursor("pointer") .onTap(() => $("filepreview-")?.open(file, url)) } } toggleAttachments() { this.attachmentsOpen = !this.attachmentsOpen; const content = this.$("#attachments-content"); const chevron = this.$("#attachments-chevron"); if (content) content.style.maxHeight = this.attachmentsOpen ? content.scrollHeight + "px" : "0"; if (chevron) chevron.style.transform = this.attachmentsOpen ? "rotate(180deg)" : "rotate(0deg)"; } } register(EventDetails)