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)