Files
apps/calendar/desktop/DesktopCalendarForm.js
2026-04-28 21:25:54 -05:00

335 lines
11 KiB
JavaScript

import server from "/calendar/@server/calendar.js"
class DesktopCalendarForm extends Shadow {
static COLORS = [
"#9E1C29", "#3D6FAD", "#2A8636", "#B38A1E",
"#B85A1F", "#7A3FA3", "#B23D6B", "#2D8A87",
"#3C9A5F", "#6E9A23", "#7A8428", "#9A2F7D",
"#4F54A8", "#8A5A32", "#546B86", "#A67A1F",
]
constructor(calendars, onSaved, editCalendar = null, onDelete = null, onBack = null) {
super()
this.calendars = calendars
this.onSaved = onSaved
this.editCalendar = editCalendar
this.onDelete = onDelete
this.onBack = onBack
this.selectedColor = editCalendar?.color ?? DesktopCalendarForm.COLORS[0]
if (editCalendar) {
this.originalFormData = {
name: editCalendar.name ?? "",
description: editCalendar.description || "",
color: editCalendar.color ?? ""
}
} else {
this.originalFormData = {
name: "",
description: "",
color: DesktopCalendarForm.COLORS[0]
}
}
}
fieldStyles(el) {
return el
.border("1px solid var(--divider)")
.borderRadius(0.35, em)
.outline("none")
.background("transparent")
.color("var(--headertext)")
.fontSize(0.88, em)
.padding(0.4, em)
.boxSizing("border-box")
.onHover(function(hovering) {
this.style.border = `1px solid ${hovering ? "var(--lightDivider)" : "var(--divider)"}`
})
}
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)
}
render() {
const isEdit = !!this.editCalendar
form(() => {
VStack(() => {
this.renderHeader(isEdit)
this.renderBody(isEdit)
})
.height(100, pct)
.boxSizing("border-box")
})
.height(100, pct)
.onSubmit(e => { e.preventDefault(); this.handleSave() })
.onKeyDown(e => {
if (e.key === "Enter" && e.target.tagName !== "TEXTAREA" && e.target.tagName !== "BUTTON") e.preventDefault()
})
}
renderHeader(isEdit) {
HStack(() => {
VStack(() => {
input("", "100%")
.attr({ name: "name", type: "text", placeholder: "Enter calendar name...", value: this.editCalendar?.name ?? "" })
.border("none")
.outline("none")
.background("transparent")
.color("var(--headertext)")
.fontSize(1.45, em)
.fontWeight("700")
.padding(0)
.onHover(function(hovering) {
this.style.opacity = hovering ? 0.82 : 1;
})
})
.flex(1)
.paddingHorizontal(1.4, em)
.paddingTop(2.5, em)
.paddingBottom(0.5, em)
.justifyContent("center")
if (isEdit) {
button("Delete")
.attr({ type: "button" })
.fontSize(0.8, em)
.fontWeight("600")
.background("transparent")
.color("var(--quillred)")
.paddingVertical(0.34, em)
.paddingHorizontal(0.85, em)
.marginRight(1.4, em)
.marginBottom(1, em)
.border("1px solid var(--quillred)")
.borderRadius(0.45, em)
.flexShrink(0)
.cursor("pointer")
.boxSizing("border-box")
.onClick((done) => { if (done) this.handleDelete() })
.onHover(function(hovering) {
this.style.background = hovering ? "var(--quillred)" : "transparent";
this.style.color = hovering ? "white" : "var(--quillred)"
this.style.opacity = hovering ? 0.82 : 1;
})
}
button("Save")
.attr({ type: "submit" })
.paddingVertical(0.34, em)
.paddingHorizontal(0.85, em)
.border("none")
.borderRadius(0.45, em)
.background("var(--quillred)")
.color("white")
.cursor("pointer")
.fontSize(0.8, em)
.fontWeight("600")
.marginRight(1.4, em)
.marginTop("auto")
.marginBottom(1, em)
.flexShrink(0)
.onHover(function(hovering) {
this.style.opacity = hovering ? 0.82 : 1;
})
})
.width(100, pct)
.alignItems("flex-end")
.background("var(--darkaccent)")
.borderBottom("1px solid var(--divider)")
.boxSizing("border-box")
.flexShrink(0)
}
renderBody() {
VStack(() => {
this.prop("COLOR", () => {
const renderSwatch = (color) => {
const selected = this.selectedColor === color
p("")
.flex(1)
.height(1.6, em)
.background(color)
.borderRadius(5, px)
.border(`3px solid ${selected ? "var(--quillred)" : color}`)
.boxSizing("border-box")
.cursor("pointer")
.attr({ "data-color": color })
.onClick((done) => {
if (!done) return
const prev = this.$(`[data-color="${this.selectedColor}"]`)
if (prev) prev.style.border = `3px solid ${this.selectedColor}`
this.selectedColor = color
const next = this.$(`[data-color="${color}"]`)
if (next) next.style.border = `3px solid var(--quillred)`
})
.onHover(function(hovering) {
this.style.opacity = hovering ? 0.82 : 1;
})
}
VStack(() => {
HStack(() => {
DesktopCalendarForm.COLORS.slice(0, 8).forEach(renderSwatch)
})
.gap(0.55, em)
HStack(() => {
DesktopCalendarForm.COLORS.slice(8, 16).forEach(renderSwatch)
})
.gap(0.55, em)
})
.gap(0.55, em)
})
this.prop("DESCRIPTION", () => {
textarea(this.editCalendar?.description ?? "")
.attr({ name: "description" })
.styles(this.fieldStyles)
.lineHeight("1.65")
.width(100, pct)
.minHeight("3em")
.resize("none")
.fieldSizing("content")
.fontFamily("Arial")
.onAppear(function() {
this.value = this.placeholder;
})
})
})
.flex(1)
.overflowY("scroll")
.width(100, pct)
.boxSizing("border-box")
}
showError(msg) {
$("modal-")?.showError(msg)
}
getFormData() {
const val = name => this.$(`[name="${name}"]`).value
return {
name: val("name"),
description: val("description") || null,
color: this.selectedColor
}
}
isUnchanged(data) {
const o = this.originalFormData
return (
data.name === o.name &&
data.color === o.color &&
(data.description || "") === o.description
)
}
isNewCalendarDirty() {
const data = this.getFormData()
const o = this.originalFormData
return (
data.name !== o.name ||
(data.description || "") !== o.description ||
data.color !== o.color
)
}
async trySave() {
if (!this.editCalendar) {
// New calendar: only save if the user made edits
if (!this.isNewCalendarDirty()) return false
const data = this.getFormData()
const payload = {
name: data.name || "New calendar",
description: data.description,
color: data.color
}
const result = await server.addCalendar(payload, global.currentNetwork.id)
if (result.status === 200) return result.calendar
this.showError(result.error ?? "Failed to save calendar.")
return null
}
const data = this.getFormData()
if (this.isUnchanged(data)) return this.editCalendar
const result = await server.editCalendar(
this.editCalendar.id,
{ ...data, name: data.name || "New calendar" },
global.currentNetwork.id
)
if (result.status === 200) return result.calendar
this.showError(result.error ?? "Failed to save calendar.")
return null
}
async handleSave() {
$("modal-")?.showError("")
const data = this.getFormData()
if (this.editCalendar) {
if (this.isUnchanged(data)) {
if (this.onBack) this.onBack()
return
}
}
const payload = {
name: data.name || "New calendar",
description: data.description,
color: data.color
}
const result = this.editCalendar
? await server.editCalendar(this.editCalendar.id, payload, global.currentNetwork.id)
: await server.addCalendar(payload, global.currentNetwork.id)
if (result.status === 200) {
if (this.editCalendar) {
this.onSaved(result.calendar)
} else {
// Use forceClose so _closeOverride doesn't re-trigger trySave
$("modal-").forceClose()
this.onSaved(result.calendar)
}
} else {
this.showError(result.error ?? "Failed to save calendar.")
}
}
async handleDelete() {
const result = await server.deleteCalendar(this.editCalendar.id, global.currentNetwork.id)
if (result.status === 200) {
$("modal-").forceClose()
if (this.onDelete) this.onDelete(this.editCalendar.id)
} else {
this.showError(result.error ?? "Failed to delete calendar.")
}
}
}
register(DesktopCalendarForm)