init
This commit is contained in:
334
calendar/desktop/DesktopCalendarForm.js
Normal file
334
calendar/desktop/DesktopCalendarForm.js
Normal file
@@ -0,0 +1,334 @@
|
||||
import server from "/@server/server.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)
|
||||
Reference in New Issue
Block a user