438 lines
17 KiB
JavaScript
438 lines
17 KiB
JavaScript
import server from "/calendar/@server/calendar.js"
|
|
|
|
css(`
|
|
calendarform- {
|
|
scrollbar-width: none;
|
|
-ms-overflow-style: none;
|
|
}
|
|
calendarform- ::-webkit-scrollbar { display: none; width: 0; height: 0; }
|
|
calendarform- ::-webkit-scrollbar-thumb { background: transparent; }
|
|
calendarform- ::-webkit-scrollbar-track { background: transparent; }
|
|
calendarform- input::placeholder,
|
|
calendarform- textarea::placeholder {
|
|
color: var(--headertext);
|
|
opacity: 0.35;
|
|
}
|
|
#calendarform-toast-wrap {
|
|
transition: max-height 0.25s ease, opacity 0.22s ease, padding-top 0.25s ease;
|
|
}
|
|
`)
|
|
|
|
class CalendarForm extends Shadow {
|
|
static COLORS = [
|
|
"#9E1C29", "#3D6FAD", "#2A8636", "#B38A1E",
|
|
"#B85A1F", "#7A3FA3", "#B23D6B", "#2D8A87",
|
|
"#3C9A5F", "#6E9A23", "#7A8428", "#9A2F7D",
|
|
"#4F54A8", "#8A5A32", "#546B86", "#A67A1F",
|
|
]
|
|
|
|
cardInputStyles(el) {
|
|
return el
|
|
.border("none")
|
|
.outline("none")
|
|
.background("transparent")
|
|
.color("var(--text)")
|
|
.fontSize(0.9, em)
|
|
.fontFamily("Arial")
|
|
.padding(0)
|
|
.boxSizing("border-box")
|
|
}
|
|
|
|
constructor(onBack = null, onCreated = null, editCalendar = null, onDelete = null, onSaveError = null) {
|
|
super()
|
|
this.onBack = onBack;
|
|
this.onCreated = onCreated;
|
|
this.editCalendar = editCalendar;
|
|
this.onDelete = onDelete;
|
|
this.onSaveError = onSaveError;
|
|
this.selectedColor = editCalendar ? editCalendar.color : CalendarForm.COLORS[0];
|
|
|
|
if (editCalendar) {
|
|
this.originalFormData = {
|
|
name: editCalendar.name ?? "",
|
|
description: editCalendar.description || "",
|
|
color: editCalendar.color ?? ""
|
|
}
|
|
} else {
|
|
this.originalFormData = {
|
|
name: "",
|
|
description: "",
|
|
color: CalendarForm.COLORS[0]
|
|
}
|
|
}
|
|
}
|
|
|
|
render() {
|
|
const isEditMode = !!this.editCalendar;
|
|
|
|
form(() => {
|
|
VStack(() => {
|
|
// ── Header ────────────────────────────────────────────
|
|
HStack(() => {
|
|
button("Cancel")
|
|
.attr({ type: "button" })
|
|
.background("none")
|
|
.border("none")
|
|
.padding(0)
|
|
.color("var(--quillred)")
|
|
.fontSize(0.95, em)
|
|
.fontFamily("Arial")
|
|
.cursor("pointer")
|
|
.flexShrink(0)
|
|
.onTap(() => this.handleBack())
|
|
|
|
p(isEditMode ? "Edit Calendar" : "New Calendar")
|
|
.margin(0)
|
|
.fontSize(1, em)
|
|
.fontWeight("700")
|
|
.color("var(--headertext)")
|
|
.fontFamily("Arial")
|
|
.flex(1)
|
|
.textAlign("center")
|
|
|
|
button("Save")
|
|
.attr({ type: "submit" })
|
|
.background("none")
|
|
.border("none")
|
|
.padding(0)
|
|
.color("var(--quillred)")
|
|
.fontSize(0.95, em)
|
|
.fontWeight("600")
|
|
.fontFamily("Arial")
|
|
.cursor("pointer")
|
|
.flexShrink(0)
|
|
})
|
|
.paddingHorizontal(1.25, em)
|
|
.paddingVertical(0.9, em)
|
|
.alignItems("center")
|
|
.borderBottom("1px solid var(--divider)")
|
|
.flexShrink(0)
|
|
.background(util.darkMode() ? "transparent" : "var(--sidebottombars)")
|
|
|
|
// ── Error toast ───────────────────────────────────────
|
|
VStack(() => {
|
|
p("")
|
|
.attr({ id: "calendarform-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: "calendarform-toast-wrap" })
|
|
.alignItems("center")
|
|
.overflow("hidden")
|
|
.maxHeight(0)
|
|
.opacity(0)
|
|
.flexShrink(0)
|
|
|
|
// ── Scrollable body ───────────────────────────────────
|
|
VStack(() => {
|
|
// ── Name card ─────────────────────────────────────
|
|
VStack(() => {
|
|
input("Name", "100%")
|
|
.attr({ name: "name", type: "text", value: this.editCalendar?.name ?? "" })
|
|
.border("none")
|
|
.outline("none")
|
|
.background("transparent")
|
|
.color("var(--text)")
|
|
.fontSize(1.1, em)
|
|
.fontFamily("Arial")
|
|
.fontWeight("500")
|
|
.padding("0.9em 1em")
|
|
.boxSizing("border-box")
|
|
})
|
|
.background("var(--darkaccent)")
|
|
.border("1px solid var(--divider)")
|
|
.borderRadius(12, px)
|
|
.marginHorizontal(1, em)
|
|
.overflow("hidden")
|
|
|
|
VStack(() => {}).height(0.85, em)
|
|
|
|
// ── Description card ──────────────────────────────
|
|
VStack(() => {
|
|
HStack(() => {
|
|
p("📝")
|
|
.margin(0)
|
|
.fontSize(0.85, em)
|
|
.flexShrink(0)
|
|
.alignSelf("flex-start")
|
|
.paddingTop(0.08, em)
|
|
|
|
const editDesc = this.editCalendar?.description
|
|
textarea(editDesc ?? "Description")
|
|
.attr({ name: "description" })
|
|
.styles(this.cardInputStyles)
|
|
.flex(1)
|
|
.minHeight(3, em)
|
|
.resize("none")
|
|
.fieldSizing("content")
|
|
.lineHeight("1.45")
|
|
.onAppear(function() {
|
|
if (editDesc) this.value = editDesc
|
|
})
|
|
})
|
|
.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")
|
|
|
|
VStack(() => {}).height(0.85, em)
|
|
|
|
// ── Color card ────────────────────────────────────
|
|
VStack(() => {
|
|
HStack(() => {
|
|
p("Color")
|
|
.margin(0)
|
|
.fontSize(0.92, em)
|
|
.color("var(--headertext)")
|
|
.flex(1)
|
|
})
|
|
.paddingHorizontal(1, em)
|
|
.paddingVertical(0.78, em)
|
|
.alignItems("center")
|
|
.borderBottom("1px solid var(--divider)")
|
|
|
|
VStack(() => {
|
|
HStack(() => {
|
|
CalendarForm.COLORS.slice(0, 8).forEach(color => {
|
|
const selected = this.selectedColor === color
|
|
p("")
|
|
.flex(1)
|
|
.height(1.8, em)
|
|
.background(color)
|
|
.borderRadius(8, px)
|
|
.border(`3px solid ${selected ? "var(--quillred)" : color}`)
|
|
.boxSizing("border-box")
|
|
.cursor("pointer")
|
|
.attr({ "data-color": color })
|
|
.onTap(() => {
|
|
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)`
|
|
})
|
|
})
|
|
})
|
|
.gap(0.55, em)
|
|
|
|
HStack(() => {
|
|
CalendarForm.COLORS.slice(8, 16).forEach(color => {
|
|
const selected = this.selectedColor === color
|
|
p("")
|
|
.flex(1)
|
|
.height(1.8, em)
|
|
.background(color)
|
|
.borderRadius(8, px)
|
|
.border(`3px solid ${selected ? "var(--quillred)" : color}`)
|
|
.boxSizing("border-box")
|
|
.cursor("pointer")
|
|
.attr({ "data-color": color })
|
|
.onTap(() => {
|
|
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)`
|
|
})
|
|
})
|
|
})
|
|
.gap(0.55, em)
|
|
})
|
|
.padding(1, em)
|
|
.gap(0.55, em)
|
|
})
|
|
.background("var(--darkaccent)")
|
|
.border("1px solid var(--divider)")
|
|
.borderRadius(12, px)
|
|
.marginHorizontal(1, em)
|
|
.overflow("hidden")
|
|
|
|
if (isEditMode) {
|
|
VStack(() => {}).height(0.85, em)
|
|
|
|
button("Delete Calendar")
|
|
.attr({ type: "button" })
|
|
.width("calc(100% - 2em)")
|
|
.marginHorizontal(1, em)
|
|
.padding(0.85, em)
|
|
.boxSizing("border-box")
|
|
.background("transparent")
|
|
.color("var(--quillred)")
|
|
.border("1.5px solid var(--quillred)")
|
|
.borderRadius(12, px)
|
|
.fontSize(0.95, em)
|
|
.fontFamily("Arial")
|
|
.fontWeight("600")
|
|
.cursor("pointer")
|
|
.onTap(() => this.handleDelete())
|
|
}
|
|
|
|
VStack(() => {}).height(1.5, em)
|
|
})
|
|
.overflowY("scroll")
|
|
.flex(1)
|
|
.paddingTop(0.85, em)
|
|
})
|
|
.height(100, pct)
|
|
.onSubmit((e) => {
|
|
e.preventDefault()
|
|
this.handleSubmit(this.getFormData())
|
|
})
|
|
.onKeyDown(e => {
|
|
if (e.key === "Enter" && e.target.tagName !== "TEXTAREA" && e.target.tagName !== "BUTTON") e.preventDefault()
|
|
})
|
|
})
|
|
.height(100, pct)
|
|
}
|
|
|
|
showError(msg) {
|
|
const wrap = this.$("#calendarform-toast-wrap")
|
|
const toast = this.$("#calendarform-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.$("#calendarform-toast-wrap")
|
|
if (!wrap) return
|
|
clearTimeout(this._errorTimer)
|
|
wrap.style.maxHeight = "0"
|
|
wrap.style.opacity = "0"
|
|
wrap.style.paddingTop = "0"
|
|
}
|
|
|
|
getFormData() {
|
|
return {
|
|
name: this.$('[name="name"]').value,
|
|
color: this.selectedColor,
|
|
description: this.$('[name="description"]').value
|
|
}
|
|
}
|
|
|
|
isNewCalendarDirty() {
|
|
const data = this.getFormData()
|
|
const o = this.originalFormData
|
|
return (
|
|
data.name !== o.name ||
|
|
(data.description || "") !== o.description ||
|
|
data.color !== o.color
|
|
)
|
|
}
|
|
|
|
async handleBack() {
|
|
this.hideError()
|
|
if (this.onBack) this.onBack()
|
|
}
|
|
|
|
async trySave() {
|
|
if (!this.editCalendar) {
|
|
if (!this.isNewCalendarDirty()) return false
|
|
const data = this.getFormData()
|
|
const payload = {
|
|
name: data.name || "New calendar",
|
|
description: data.description || null,
|
|
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()
|
|
const unchanged =
|
|
data.name === this.originalFormData.name &&
|
|
data.color === this.originalFormData.color &&
|
|
(data.description || "") === this.originalFormData.description
|
|
|
|
if (unchanged) return this.editCalendar
|
|
|
|
const result = await server.editCalendar(
|
|
this.editCalendar.id,
|
|
{
|
|
name: data.name || "New calendar",
|
|
description: data.description || null,
|
|
color: data.color
|
|
},
|
|
global.currentNetwork.id
|
|
)
|
|
|
|
if (result.status === 200) return result.calendar
|
|
|
|
this.showError(result.error ?? "Failed to save calendar.")
|
|
return null
|
|
}
|
|
|
|
async handleSubmit(data) {
|
|
this.hideError()
|
|
|
|
if (this.editCalendar && !data.name) {
|
|
this.showError("Calendars must have a name.")
|
|
this.onSaveError?.()
|
|
return;
|
|
}
|
|
|
|
const payload = {
|
|
name: data.name || "New calendar",
|
|
description: data.description || null,
|
|
color: data.color
|
|
}
|
|
|
|
if (this.editCalendar) {
|
|
const result = await server.editCalendar(this.editCalendar.id, payload, global.currentNetwork.id);
|
|
if (result.status === 200) {
|
|
if (this.onCreated) this.onCreated(result.calendar);
|
|
} else {
|
|
this.showError(result.error ?? "Failed to update calendar.")
|
|
this.onSaveError?.()
|
|
}
|
|
} else {
|
|
const result = await server.addCalendar(payload, global.currentNetwork.id);
|
|
if (result.status === 200) {
|
|
if (this.onCreated) this.onCreated(result.calendar);
|
|
} else {
|
|
this.showError(result.error ?? "Failed to create calendar.")
|
|
this.onSaveError?.()
|
|
}
|
|
}
|
|
}
|
|
|
|
async handleDelete() {
|
|
this.hideError()
|
|
const result = await server.deleteCalendar(
|
|
this.editCalendar.id,
|
|
global.currentNetwork.id
|
|
);
|
|
if (result.status === 200) {
|
|
if (this.onDelete) this.onDelete(this.editCalendar.id);
|
|
} else {
|
|
this.showError(result.error ?? "Failed to delete calendar.");
|
|
}
|
|
}
|
|
}
|
|
|
|
register(CalendarForm)
|