init
This commit is contained in:
437
calendar/CalendarForm.js
Normal file
437
calendar/CalendarForm.js
Normal file
@@ -0,0 +1,437 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user