Files
apps/calendar/CalendarForm.js
metacryst 0d6c7683ff init
2026-04-28 20:05:00 -05:00

437 lines
17 KiB
JavaScript

import server from "/@server/server.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)