init
This commit is contained in:
396
calendar/desktop/Events/DesktopEventDetails.js
Normal file
396
calendar/desktop/Events/DesktopEventDetails.js
Normal file
@@ -0,0 +1,396 @@
|
||||
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)
|
||||
1011
calendar/desktop/Events/DesktopEventForm.js
Normal file
1011
calendar/desktop/Events/DesktopEventForm.js
Normal file
File diff suppressed because it is too large
Load Diff
154
calendar/desktop/Events/FilePreview.js
Normal file
154
calendar/desktop/Events/FilePreview.js
Normal file
@@ -0,0 +1,154 @@
|
||||
class FilePreview extends Window {
|
||||
_visible = false
|
||||
_file = null
|
||||
_url = null
|
||||
|
||||
open(file, url) {
|
||||
this._file = file
|
||||
this._url = url
|
||||
this._visible = true
|
||||
this.rerender()
|
||||
}
|
||||
|
||||
close() {
|
||||
this._visible = false
|
||||
this.rerender()
|
||||
}
|
||||
|
||||
render() {
|
||||
this.style.position = "fixed"
|
||||
this.style.inset = "0"
|
||||
this.style.zIndex = "300"
|
||||
this.style.pointerEvents = this._visible ? "all" : "none"
|
||||
|
||||
if (!this._visible) return
|
||||
|
||||
const x = this.getX()
|
||||
const y = this.getY()
|
||||
const w = this.getWidth()
|
||||
const h = this.getHeight()
|
||||
|
||||
const type = this._file?.type ?? ""
|
||||
const isImage = type.startsWith("image/")
|
||||
const isPDF = type === "application/pdf"
|
||||
const isOffice = [
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
'application/msword',
|
||||
'application/vnd.ms-excel',
|
||||
'application/vnd.ms-powerpoint',
|
||||
'text/plain',
|
||||
].includes(type)
|
||||
const displayName = this._file?.original_name ?? this._file?.name ?? "File"
|
||||
|
||||
VStack(() => {
|
||||
const panel = VStack(() => {
|
||||
// Header
|
||||
HStack(() => {
|
||||
HStack(() => {
|
||||
div().attr({ class: "tl tl-close" }).onClick((done) => { if (done) this.close() })
|
||||
div().attr({ class: "tl tl-min" })
|
||||
div().attr({ class: "tl tl-max" })
|
||||
})
|
||||
.attr({ class: "traffic-lights" })
|
||||
.flexShrink(0)
|
||||
|
||||
p(displayName)
|
||||
.margin(0)
|
||||
.fontSize(0.88, em)
|
||||
.fontWeight("600")
|
||||
.color("var(--headertext)")
|
||||
.overflow("hidden")
|
||||
.whiteSpace("nowrap")
|
||||
.textOverflow("ellipsis")
|
||||
.flex(1)
|
||||
.textAlign("center")
|
||||
|
||||
div().width("52px").flexShrink(0)
|
||||
})
|
||||
.paddingHorizontal(1, em)
|
||||
.paddingVertical(0.75, em)
|
||||
.alignItems("center")
|
||||
.gap(1, em)
|
||||
.background("var(--darkaccent)")
|
||||
.borderBottom("1px solid var(--divider)")
|
||||
.width(100, pct)
|
||||
.boxSizing("border-box")
|
||||
.flexShrink(0)
|
||||
|
||||
// Content
|
||||
if (isImage) {
|
||||
img(this._url, "auto", "auto")
|
||||
.display("block")
|
||||
.maxWidth(100, pct)
|
||||
.maxHeight(h - 48, px)
|
||||
.margin("0 auto")
|
||||
} else if (isPDF) {
|
||||
const wrap = div()
|
||||
.flex(1)
|
||||
.width(100, pct)
|
||||
.overflow("hidden")
|
||||
wrap.innerHTML = `<iframe src="${this._url}" style="width:100%;height:100%;border:none;display:block;"></iframe>`
|
||||
} else {
|
||||
const icon = {
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': "📄",
|
||||
'application/msword': "📄",
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': "📊",
|
||||
'application/vnd.ms-excel': "📊",
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation': "📋",
|
||||
'application/vnd.ms-powerpoint': "📋",
|
||||
'text/plain': "📄",
|
||||
}[type] ?? "📎"
|
||||
|
||||
VStack(() => {
|
||||
p(icon).fontSize(3, em).margin(0)
|
||||
p(displayName)
|
||||
.margin(0)
|
||||
.fontSize(0.9, em)
|
||||
.color("var(--headertext)")
|
||||
.textAlign("center")
|
||||
|
||||
a(this._url, "Download")
|
||||
.attr({ download: displayName })
|
||||
.fontSize(0.85, em)
|
||||
.color("var(--quillred)")
|
||||
.fontWeight("600")
|
||||
.fontFamily("Arial")
|
||||
.textDecoration("none")
|
||||
.cursor("pointer")
|
||||
.marginTop(0.25, em)
|
||||
.onHover(function(hovering) {
|
||||
this.style.textDecoration = hovering ? "underline" : "none"
|
||||
})
|
||||
})
|
||||
.gap(0.5, em)
|
||||
.alignItems("center")
|
||||
.padding(2, em)
|
||||
}
|
||||
})
|
||||
.background("var(--window)")
|
||||
.backdropFilter("blur(18px)")
|
||||
.border("0.5px solid var(--window-border)")
|
||||
.borderRadius(12, px)
|
||||
.overflow("hidden")
|
||||
.width(100, pct)
|
||||
.boxSizing("border-box")
|
||||
|
||||
if (isPDF) panel.height(h, px)
|
||||
else panel.maxHeight(h, px)
|
||||
panel.onClick((done, e) => { e.stopPropagation() })
|
||||
})
|
||||
.position("fixed")
|
||||
.x(x, px)
|
||||
.y(y, px)
|
||||
.width(w, px)
|
||||
.height(h, px)
|
||||
.justifyContent(isPDF ? "flex-start" : "center")
|
||||
.alignItems("center")
|
||||
.onClick((done) => { if (done) this.close() })
|
||||
.onEvent("resize", () => this.rerender())
|
||||
}
|
||||
}
|
||||
|
||||
register(FilePreview)
|
||||
Reference in New Issue
Block a user