diff --git a/internal/app/database/database.go b/internal/app/database/database.go index 43bd256..feb7116 100644 --- a/internal/app/database/database.go +++ b/internal/app/database/database.go @@ -60,6 +60,11 @@ type Service interface { SearchLists(searchQuery string) ([]*List, error) ListTasksSearch(id int, searchQuery string) ([]*Task, error) + + Groups() ([]*ListGroup, error) + CreateGroup(name string, position int) error + UpdateGroup(id int, name string) error + DeleteGroup(id int) error } type cacheEntry struct { @@ -730,6 +735,7 @@ func (s *service) Lists(filter string) ([]*List, error) { &data.Position, &data.CreatedAt, &data.UpdatedAt, + &data.GroupId, ) if err != nil { return nil, fmt.Errorf("DB.Lists - result scan failed: %v", err) @@ -926,7 +932,8 @@ func (s *service) List(id string) (*List, error) { &list.Base, &list.Position, &list.CreatedAt, - &list.UpdatedAt) + &list.UpdatedAt, + &list.GroupId) if err != nil { return nil, fmt.Errorf("DB.List - query result failed: %v", err) } @@ -1273,3 +1280,114 @@ func (s *service) ListTasksSearch(id int, searchQuery string) ([]*Task, error) { return tasks, nil } + +func (s *service) Groups() ([]*ListGroup, error) { + const key = "groups" + if cached, ok := s.cache.get(key); ok { + return cached.([]*ListGroup), nil + } + + query, err := s.db.Prepare("SELECT * FROM list_group ORDER BY position ASC, created_at ASC") + if err != nil { + return nil, fmt.Errorf("DB.Groups - prepare query failed: %v", err) + } + defer query.Close() + + result, err := query.Query() + if err != nil { + return nil, fmt.Errorf("DB.Groups - query result failed: %v", err) + } + + groups := make([]*ListGroup, 0) + for result.Next() { + data := new(ListGroup) + err := result.Scan( + &data.ID, + &data.Name, + &data.Position, + &data.CreatedAt, + &data.UpdatedAt, + ) + if err != nil { + return nil, fmt.Errorf("DB.Groups - result scan failed: %v", err) + } + groups = append(groups, data) + } + + // Populate each group with its lists + lists, err := s.Lists("") + if err != nil { + return nil, fmt.Errorf("DB.Groups - get lists failed: %v", err) + } + for _, g := range groups { + g.Lists = make([]*List, 0) + for _, l := range lists { + if l.GroupId == g.ID { + g.Lists = append(g.Lists, l) + } + } + } + + s.cache.set(key, groups) + return groups, nil +} + +func (s *service) CreateGroup(name string, position int) error { + query, err := s.db.Prepare("INSERT INTO list_group (name, position) VALUES (?, ?)") + if err != nil { + return fmt.Errorf("DB.CreateGroup - prepare query failed: %v", err) + } + defer query.Close() + + _, err = query.Exec(name, position) + if err != nil { + return fmt.Errorf("DB.CreateGroup - exec failed: %v", err) + } + + s.cache.invalidate() + return nil +} + +func (s *service) UpdateGroup(id int, name string) error { + query, err := s.db.Prepare("UPDATE list_group SET name = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?") + if err != nil { + return fmt.Errorf("DB.UpdateGroup - prepare query failed: %v", err) + } + defer query.Close() + + _, err = query.Exec(name, id) + if err != nil { + return fmt.Errorf("DB.UpdateGroup - exec failed: %v", err) + } + + s.cache.invalidate() + return nil +} + +func (s *service) DeleteGroup(id int) error { + // Unassign lists from this group first + unassign, err := s.db.Prepare("UPDATE list SET group_id = 0 WHERE group_id = ?") + if err != nil { + return fmt.Errorf("DB.DeleteGroup - prepare unassign query failed: %v", err) + } + defer unassign.Close() + + _, err = unassign.Exec(id) + if err != nil { + return fmt.Errorf("DB.DeleteGroup - unassign exec failed: %v", err) + } + + query, err := s.db.Prepare("DELETE FROM list_group WHERE id = ?") + if err != nil { + return fmt.Errorf("DB.DeleteGroup - prepare delete query failed: %v", err) + } + defer query.Close() + + _, err = query.Exec(id) + if err != nil { + return fmt.Errorf("DB.DeleteGroup - delete exec failed: %v", err) + } + + s.cache.invalidate() + return nil +} diff --git a/internal/app/database/migrations/004_add_list_groups.sql b/internal/app/database/migrations/004_add_list_groups.sql new file mode 100644 index 0000000..2b11e10 --- /dev/null +++ b/internal/app/database/migrations/004_add_list_groups.sql @@ -0,0 +1,11 @@ +-- Add list_group table and group_id to list + +CREATE TABLE IF NOT EXISTS list_group ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + position INTEGER DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +ALTER TABLE list ADD COLUMN group_id INTEGER DEFAULT 0; diff --git a/internal/app/database/schema.go b/internal/app/database/schema.go index 3c3105d..16022f8 100644 --- a/internal/app/database/schema.go +++ b/internal/app/database/schema.go @@ -30,11 +30,21 @@ type List struct { Pinned bool `json:"pinned"` Base bool `json:"base"` Position int `json:"position"` + GroupId int `json:"group_id"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` Tasks []*Task } +type ListGroup struct { + ID int `json:"id"` + Name string `json:"name"` + Position int `json:"position"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + Lists []*List +} + type Total struct { Tasks int `json:"total_tasks"` Completed int `json:"total_completed"` diff --git a/internal/handlers/tasks/groups.go b/internal/handlers/tasks/groups.go new file mode 100644 index 0000000..310b037 --- /dev/null +++ b/internal/handlers/tasks/groups.go @@ -0,0 +1,130 @@ +package tasks + +import ( + "rminder/internal/app" + "rminder/web" + "strconv" + + "github.com/gin-gonic/gin" +) + +func CreateGroup(ctx *gin.Context) { + db := app.GetUserDatabase(ctx) + log := app.GetLogger(ctx) + + err := ctx.Request.ParseForm() + if err != nil { + log.Error("error parsing form", "error", err) + app.ErrorBadRequestHTML(ctx, "Invalid form data.") + return + } + + name := ctx.Request.FormValue("name") + if name == "" || len(name) < 3 || len(name) > 255 { + log.Error("error form field validation failed", "name", name) + app.ErrorBadRequestHTML(ctx, "Name must be between 3 and 255 characters.") + return + } + + groups, err := db.Groups() + if err != nil { + log.Error("error loading groups for position", "error", err) + app.ErrorInternalHTML(ctx, "Failed to load groups.") + return + } + + if err := db.CreateGroup(name, len(groups)+1); err != nil { + log.Error("error creating group", "error", err) + app.ErrorInternalHTML(ctx, "Failed to create group.") + return + } + log.Info("group created", "name", name) + + renderSidebarLists(ctx) +} + +func UpdateGroup(ctx *gin.Context) { + db := app.GetUserDatabase(ctx) + log := app.GetLogger(ctx) + + groupID := ctx.Param("groupID") + id, _ := strconv.Atoi(groupID) + + err := ctx.Request.ParseForm() + if err != nil { + log.Error("error parsing form", "error", err) + app.ErrorBadRequestHTML(ctx, "Invalid form data.") + return + } + + name := ctx.Request.FormValue("name") + if name == "" || len(name) < 3 || len(name) > 255 { + log.Error("error form field validation failed", "name", name) + app.ErrorBadRequestHTML(ctx, "Name must be between 3 and 255 characters.") + return + } + + if err := db.UpdateGroup(id, name); err != nil { + log.Error("error updating group", "groupID", groupID, "error", err) + app.ErrorInternalHTML(ctx, "Failed to update group.") + return + } + log.Info("group updated", "groupID", groupID, "name", name) + + renderSidebarLists(ctx) +} + +func DeleteGroup(ctx *gin.Context) { + db := app.GetUserDatabase(ctx) + log := app.GetLogger(ctx) + + groupID := ctx.Param("groupID") + id, _ := strconv.Atoi(groupID) + + if err := db.DeleteGroup(id); err != nil { + log.Error("error deleting group", "groupID", groupID, "error", err) + app.ErrorInternalHTML(ctx, "Failed to delete group.") + return + } + log.Info("group deleted", "groupID", groupID) + + renderSidebarLists(ctx) +} + +// renderSidebarLists is a shared helper to re-render the sidebar lists after group mutations. +func renderSidebarLists(ctx *gin.Context) { + db := app.GetUserDatabase(ctx) + log := app.GetLogger(ctx) + + lists, err := db.Lists("") + if err != nil { + log.Error("error loading lists", "error", err) + app.ErrorInternalHTML(ctx, "Failed to load lists.") + return + } + + groups, err := db.Groups() + if err != nil { + log.Error("error loading groups", "error", err) + app.ErrorInternalHTML(ctx, "Failed to load groups.") + return + } + + persistence, err := db.Persistence() + if err != nil { + log.Error("error loading persistence", "error", err) + app.ErrorInternalHTML(ctx, "Failed to load state.") + return + } + + ctx.Writer.Header().Set("Content-Type", "text/html; charset=utf-8") + err = web.Render(ctx.Writer, "sidebar-lists", map[string]any{ + "Lists": lists, + "Groups": groups, + "Persistence": persistence, + }) + if err != nil { + log.Error("error rendering sidebar-lists", "error", err) + app.ErrorInternalHTML(ctx, "Failed to render lists.") + } +} diff --git a/internal/handlers/tasks/lists.go b/internal/handlers/tasks/lists.go index ca7b85c..9077e01 100644 --- a/internal/handlers/tasks/lists.go +++ b/internal/handlers/tasks/lists.go @@ -16,33 +16,7 @@ import ( ) func GetLists(ctx *gin.Context) { - db := app.GetUserDatabase(ctx) - log := app.GetLogger(ctx) - - lists, err := db.Lists("") - if err != nil { - log.Error("error handling GetLists", "error", err) - app.ErrorInternalHTML(ctx, "Failed to load lists.") - return - } - - persistence, err := db.Persistence() - if err != nil { - log.Error("error handling GetLists Persistence", "error", err) - app.ErrorInternalHTML(ctx, "Failed to load lists.") - return - } - - ctx.Writer.Header().Set("Content-Type", "text/html; charset=utf-8") - - err = web.Render(ctx.Writer, "sidebar-lists", map[string]any{ - "Lists": lists, - "Persistence": persistence, - }) - if err != nil { - log.Error("error rendering in GetLists", "error", err) - app.ErrorInternalHTML(ctx, "Failed to render lists.") - } + renderSidebarLists(ctx) } func GetList(ctx *gin.Context) { @@ -167,29 +141,7 @@ func CreateList(ctx *gin.Context) { return } - lists, err := db.Lists("") - if err != nil { - log.Error("error handling lists", "error", err) - app.ErrorInternalHTML(ctx, "Failed to load lists.") - return - } - - persistence, err := db.Persistence() - if err != nil { - log.Error("error handling CreateList Persistence", "error", err) - app.ErrorInternalHTML(ctx, "Failed to load list state.") - return - } - - ctx.Writer.Header().Set("Content-Type", "text/html; charset=utf-8") - err = web.Render(ctx.Writer, "sidebar-lists", map[string]any{ - "Lists": lists, - "Persistence": persistence, - }) - if err != nil { - log.Error("error rendering in Lists", "error", err) - app.ErrorInternalHTML(ctx, "Failed to render lists.") - } + renderSidebarLists(ctx) } func DeleteList(ctx *gin.Context) { @@ -216,13 +168,6 @@ func DeleteList(ctx *gin.Context) { return } - lists, err := db.Lists("") - if err != nil { - log.Error("error handling DeleteList lists", "error", err) - app.ErrorInternalHTML(ctx, "Failed to load lists.") - return - } - persistence, err := db.Persistence() if err != nil { log.Error("error handling DeleteList Persistence", "error", err) @@ -237,18 +182,9 @@ func DeleteList(ctx *gin.Context) { app.ErrorInternalHTML(ctx, "Failed to update list state.") return } - persistence.ListId = 0 } - ctx.Writer.Header().Set("Content-Type", "text/html; charset=utf-8") - err = web.Render(ctx.Writer, "sidebar-lists", map[string]any{ - "Lists": lists, - "Persistence": persistence, - }) - if err != nil { - log.Error("error rendering in Lists", "error", err) - app.ErrorInternalHTML(ctx, "Failed to render lists.") - } + renderSidebarLists(ctx) } func UpdateList(ctx *gin.Context) { @@ -301,29 +237,7 @@ func UpdateList(ctx *gin.Context) { return } - lists, err := db.Lists("") - if err != nil { - log.Error("error handling lists", "error", err) - app.ErrorInternalHTML(ctx, "Failed to load lists.") - return - } - - persistence, err := db.Persistence() - if err != nil { - log.Error("error handling UpdateList Persistence", "error", err) - app.ErrorInternalHTML(ctx, "Failed to load list state.") - return - } - - ctx.Writer.Header().Set("Content-Type", "text/html; charset=utf-8") - err = web.Render(ctx.Writer, "sidebar-lists", map[string]any{ - "Lists": lists, - "Persistence": persistence, - }) - if err != nil { - log.Error("error rendering in Lists", "error", err) - app.ErrorInternalHTML(ctx, "Failed to render lists.") - } + renderSidebarLists(ctx) } func SearchLists(ctx *gin.Context) { diff --git a/internal/handlers/tasks/tasks.go b/internal/handlers/tasks/tasks.go index e77e783..5be8b0d 100644 --- a/internal/handlers/tasks/tasks.go +++ b/internal/handlers/tasks/tasks.go @@ -23,6 +23,12 @@ func Load(ctx *gin.Context) { return } + groups, err := db.Groups() + if err != nil { + log.Error("error handling Tasks app Load Groups", "error", err) + return + } + persistence, err := db.Persistence() if err != nil { log.Error("error handling Tasks app Load Persistence", "error", err) @@ -52,6 +58,7 @@ func Load(ctx *gin.Context) { err = web.Render(ctx.Writer, "tasks-page", map[string]any{ "Lists": lists, + "Groups": groups, "MultiList": multiList, "Persistence": persistence, "CSRFToken": app.GetCSRFToken(ctx), diff --git a/internal/router/tasks.go b/internal/router/tasks.go index c9e9503..80efdf1 100644 --- a/internal/router/tasks.go +++ b/internal/router/tasks.go @@ -34,6 +34,11 @@ func TasksRoutes(router *gin.Engine, application *app.App) { lists.DELETE("/:listID", taskhandlers.DeleteList) lists.PUT("/:listID", taskhandlers.UpdateList) + groups := partials.Group("/groups") + groups.POST("/create", taskhandlers.CreateGroup) + groups.PUT("/:groupID", taskhandlers.UpdateGroup) + groups.DELETE("/:groupID", taskhandlers.DeleteGroup) + // API: returns JSON api := router.Group("/api", middleware.UserMiddleware(application), middleware.CSRFMiddleware()) diff --git a/web/assets/js/apps/tasks/tasks.entry.js b/web/assets/js/apps/tasks/tasks.entry.js index c9f8be0..1ffce06 100644 --- a/web/assets/js/apps/tasks/tasks.entry.js +++ b/web/assets/js/apps/tasks/tasks.entry.js @@ -1,6 +1,7 @@ import "../../components/details.js"; import "../../components/sidebar.js"; import "../../components/listNavigation.js"; +import "../../components/groupNavigation.js"; import "../../components/appSwitcher.js"; import "../../components/settingsSwitcher.js"; import "./tasks.js"; diff --git a/web/assets/js/components/groupNavigation.js b/web/assets/js/components/groupNavigation.js new file mode 100644 index 0000000..7b107b0 --- /dev/null +++ b/web/assets/js/components/groupNavigation.js @@ -0,0 +1,56 @@ +class GroupNavigationElement extends HTMLElement { + /** @type string */ + groupId; + /** @type string */ + groupName; + /** @type HTMLSpanElement */ + settings; + /** @type HTMLDialogElement */ + formContainer; + + #editGroupHandler; + + /** + * @param {MouseEvent} e + */ + edit(e) { + e.preventDefault(); + + const form = this.formContainer.querySelector("form"); + const nameInput = form.querySelector("input.new-group"); + const updateBtn = form.querySelector(".btn.add-new-group"); + const removeBtn = form.querySelector(".remove-group"); + + form.removeAttribute("hx-post"); + form.setAttribute("hx-put", `/partials/groups/${this.groupId}`); + removeBtn.setAttribute("hx-delete", `/partials/groups/${this.groupId}`); + + nameInput.value = this.groupName; + updateBtn.innerHTML = "Update"; + removeBtn.classList.remove("hidden"); + + htmx.process(form); + htmx.process(removeBtn); + + this.formContainer.showModal(); + nameInput.focus(); + } + + connectedCallback() { + this.formContainer = document.querySelector(".group-form-container"); + this.settings = this.querySelector(".group-settings"); + + this.groupId = this.dataset.groupId; + this.groupName = this.querySelector(".group-name")?.textContent?.trim() ?? ""; + + this.#editGroupHandler = (e) => this.edit(e); + + this.settings?.addEventListener("click", this.#editGroupHandler); + } + + disconnectedCallback() { + this.settings?.removeEventListener("click", this.#editGroupHandler); + } +} + +customElements.define("rm-group-nav", GroupNavigationElement); diff --git a/web/assets/js/components/sidebar.js b/web/assets/js/components/sidebar.js index a073d79..8a15318 100644 --- a/web/assets/js/components/sidebar.js +++ b/web/assets/js/components/sidebar.js @@ -10,18 +10,31 @@ class SidebarElement extends HTMLElement { /** @type HTMLInputElement */ searchInput; + /** @type HTMLButtonElement */ + addGroup; + /** @type HTMLDivElement */ + cancelGroupBtn; + /** @type HTMLDialogElement */ + groupFormContainer; + #closePopoverHandler; #formFocusHandler; #searchClearHandler; + #closeGroupPopoverHandler; + #groupFormFocusHandler; addEvents() { this.#closePopoverHandler = () => this.closePopover(); this.#formFocusHandler = () => this.formFocus(); this.#searchClearHandler = () => this.clearSearch(); + this.#closeGroupPopoverHandler = () => this.closeGroupPopover(); + this.#groupFormFocusHandler = () => this.groupFormFocus(); this.addList.addEventListener("click", this.#formFocusHandler); this.cancelBtn.addEventListener("click", this.#closePopoverHandler); this.clearIcon.addEventListener("click", this.#searchClearHandler); + this.addGroup?.addEventListener("click", this.#groupFormFocusHandler); + this.cancelGroupBtn?.addEventListener("click", this.#closeGroupPopoverHandler); } closePopover() { @@ -55,6 +68,27 @@ class SidebarElement extends HTMLElement { this.formContainer.querySelector(".new-list").focus(); } + closeGroupPopover() { + this.groupFormContainer.close(); + const form = this.groupFormContainer.querySelector("form"); + const btn = form.querySelector(".btn.add-new-group"); + const removeBtn = form.querySelector(".remove-group"); + form.reset(); + + form.removeAttribute("hx-put"); + form.setAttribute("hx-post", "/partials/groups/create"); + btn.innerHTML = "Add"; + removeBtn?.classList.add("hidden"); + removeBtn?.removeAttribute("hx-delete"); + + htmx.process(form); + } + + groupFormFocus() { + this.groupFormContainer.showModal(); + this.groupFormContainer.querySelector(".new-group").focus(); + } + clearSearch() { this.searchInput.value = ""; } @@ -66,6 +100,10 @@ class SidebarElement extends HTMLElement { this.clearIcon = this.querySelector(".searchbox__container i.clear-icon"); this.searchInput = this.querySelector(".searchbox"); + this.addGroup = this.querySelector(".add-group"); + this.cancelGroupBtn = this.querySelector(".cancel-new-group"); + this.groupFormContainer = this.querySelector(".group-form-container"); + this.addEvents(); } @@ -73,6 +111,8 @@ class SidebarElement extends HTMLElement { this.addList.removeEventListener("click", this.#formFocusHandler); this.cancelBtn.removeEventListener("click", this.#closePopoverHandler); this.clearIcon.removeEventListener("click", this.#searchClearHandler); + this.addGroup?.removeEventListener("click", this.#groupFormFocusHandler); + this.cancelGroupBtn?.removeEventListener("click", this.#closeGroupPopoverHandler); } } diff --git a/web/assets/js/dist/tasks.min.js b/web/assets/js/dist/tasks.min.js index 597eaf2..da76145 100644 --- a/web/assets/js/dist/tasks.min.js +++ b/web/assets/js/dist/tasks.min.js @@ -1 +1 @@ -var Z=Object.defineProperty;var C=r=>{throw TypeError(r)};var ee=(r,n,e)=>n in r?Z(r,n,{enumerable:!0,configurable:!0,writable:!0,value:e}):r[n]=e;var a=(r,n,e)=>ee(r,typeof n!="symbol"?n+"":n,e),x=(r,n,e)=>n.has(r)||C("Cannot "+e);var u=(r,n,e)=>(x(r,n,"read from private field"),e?e.call(r):n.get(r)),m=(r,n,e)=>n.has(r)?C("Cannot add the same private member more than once"):n instanceof WeakSet?n.add(r):n.set(r,e),E=(r,n,e,t)=>(x(r,n,"write to private field"),t?t.call(r,e):n.set(r,e),e),h=(r,n,e)=>(x(r,n,"access private method"),e);var q,P;class te extends HTMLElement{constructor(){super(...arguments);m(this,q);a(this,"quill")}get description(){return this.quill.getSemanticHTML()}async connectedCallback(){await h(this,q,P).call(this),this.getAttribute("task-id");const e=this.querySelector("button.close"),t=this.querySelector("button.remove"),s=this.querySelector("dialog.modal"),i=this.querySelector("dialog button.default"),o=this.querySelector("dialog button.warning");this.quill=new Quill("#note-editor",{modules:{toolbar:[[{header:[1,2,3,!1]}],["bold","italic","underline","strike"],["blockquote","code-block"],["link","image"],[{list:"ordered"},{list:"bullet"}]]},bounds:this,placeholder:"Add description...",theme:"snow"}),e.addEventListener("click",()=>{const l=document.querySelector("input[name='task-detail']:checked");l&&(l.checked=!1)}),t.addEventListener("click",()=>{s==null||s.showModal()}),i.addEventListener("click",()=>{s==null||s.close()}),o.addEventListener("click",()=>{s==null||s.close()})}}q=new WeakSet,P=async function(){if(!window.Quill){if(!document.querySelector('link[href="/assets/css/libs/quill.snow.css"]')){const e=document.createElement("link");e.rel="stylesheet",e.href="/assets/css/libs/quill.snow.css",document.head.appendChild(e)}return new Promise((e,t)=>{const s=document.createElement("script");s.src="/assets/js/libs/quill.js",s.onload=e,s.onerror=t,document.head.appendChild(s)})}};customElements.define("rm-task-details",te);var k,y,v;class se extends HTMLElement{constructor(){super(...arguments);a(this,"cancelBtn");a(this,"addList");a(this,"formContainer");a(this,"clearIcon");a(this,"searchInput");m(this,k);m(this,y);m(this,v)}addEvents(){E(this,k,()=>this.closePopover()),E(this,y,()=>this.formFocus()),E(this,v,()=>this.clearSearch()),this.addList.addEventListener("click",u(this,y)),this.cancelBtn.addEventListener("click",u(this,k)),this.clearIcon.addEventListener("click",u(this,v))}closePopover(){var i,o,l,p;this.formContainer.close();const e=this.formContainer.querySelector("form"),t=e.querySelector(".btn.add-new-list"),s=e.querySelector(".remove-list");e.reset(),e.removeAttribute("hx-put"),e.setAttribute("hx-post","/partials/lists/create"),(i=e.querySelectorAll("input:checked"))==null||i.forEach(f=>{f.removeAttribute("checked")}),(o=e.querySelectorAll("option"))==null||o.forEach(f=>{f.removeAttribute("selected")}),(l=e.querySelector('input[name="swatch"]'))==null||l.setAttribute("checked",""),(p=e.querySelector('input[name="icon"]'))==null||p.setAttribute("checked",""),t.innerHTML="Add",s==null||s.classList.add("hidden"),s==null||s.removeAttribute("hx-delete"),htmx.process(e)}formFocus(){this.formContainer.showModal(),this.formContainer.querySelector(".new-list").focus()}clearSearch(){this.searchInput.value=""}connectedCallback(){this.cancelBtn=this.querySelector(".cancel-new-list"),this.addList=this.querySelector(".add-list"),this.formContainer=this.querySelector(".list-form-container"),this.clearIcon=this.querySelector(".searchbox__container i.clear-icon"),this.searchInput=this.querySelector(".searchbox"),this.addEvents()}disconnectedCallback(){this.addList.removeEventListener("click",u(this,y)),this.cancelBtn.removeEventListener("click",u(this,k)),this.clearIcon.removeEventListener("click",u(this,v))}}k=new WeakMap,y=new WeakMap,v=new WeakMap;customElements.define("rm-sidebar",se);var b;class ie extends HTMLElement{constructor(){super(...arguments);a(this,"listId");a(this,"listName");a(this,"colour");a(this,"icon");a(this,"position");a(this,"pinned");a(this,"filter");a(this,"settings");a(this,"formContainer");m(this,b)}edit(e){var T;e.preventDefault();const t=this.formContainer.querySelector("form");(T=t.querySelectorAll("input:checked"))==null||T.forEach(w=>{w.removeAttribute("checked")});const s=t.querySelector("input.new-list"),i=t.querySelector('input[name="pin"]'),o=t.querySelector(`input[name="swatch"][value="${this.colour}"]`),l=t.querySelector(`input[name="icon"][value="${this.icon}"]`),p=t.querySelector(".btn.add-new-list"),f=t.querySelector(".remove-list"),K=t.querySelector('select[name="include"]'),M=t.querySelector('select[name="completed"]'),F=t.querySelector('select[name="important"]'),$=t.querySelector('select[name="priority"]'),j=t.querySelector('select[name="date"]'),J=t.querySelector('input[name="from"]'),z=t.querySelector('input[name="to"]'),[U,Q,X,Y,G,V,W]=this.filter.split(";").map(w=>w.split("=")[1]);t.removeAttribute("hx-post"),t.setAttribute("hx-put",`/partials/lists/${this.listId}`),f.setAttribute("hx-delete",`/partials/lists/${this.listId}`),s.value=this.listName,i.toggleAttribute("checked",this.pinned===""),o.setAttribute("checked",""),l.setAttribute("checked",""),p.innerHTML="Update",f.classList.remove("hidden"),K.value=U,M.value=Q,F.value=X,$.value=Y,j.value=G,J.value=V,z.value=W,htmx.process(t),this.formContainer.showModal()}connectedCallback(){var e;this.formContainer=document.querySelector(".list-form-container"),this.settings=this.querySelector(".settings"),this.listId=this.dataset.id,this.listName=this.dataset.name,this.colour=this.dataset.colour,this.icon=this.dataset.icon,this.pinned=this.dataset.pinned,this.position=this.dataset.position,this.filter=this.dataset.filter,E(this,b,t=>this.edit(t)),this.style.setProperty("--list-colour",`var(${this.colour})`),(e=this.settings)==null||e.addEventListener("click",u(this,b))}disconnectedCallback(){var e;(e=this.settings)==null||e.removeEventListener("click",u(this,b))}}b=new WeakMap;customElements.define("rm-list-nav",ie);class re extends HTMLElement{connectedCallback(){const n=this.getBoundingClientRect(),e=document.body.getBoundingClientRect();this.style.setProperty("--popover-top",`${n.bottom}px`),this.style.setProperty("--popover-right",`${e.right-n.right+10}px`)}}customElements.define("rm-app-switcher",re);async function L(r){try{return[null,await r]}catch(n){return[n]}}var S,N,_;class ne extends HTMLElement{constructor(){super(...arguments);m(this,S);a(this,"formContainer");a(this,"fileElem");a(this,"messages");a(this,"maxFileSize",10*1024*1024);a(this,"allowedTypes",["application/json"])}saveJSON(e,t){var s=JSON.stringify(e,null,2),i=new Blob([s],{type:"application/json"}),o=URL.createObjectURL(i),l=document.createElement("a");l.download=t+".json",l.href=o,l.id=t,document.body.appendChild(l),l.click(),document.querySelector("#"+l.id).remove()}validateFile(e){return this.messages.textContent="",this.allowedTypes.includes(e.type)?e.size>this.maxFileSize?(this.showError(`${e.name} exceeds the maximum file size of 10MB.`),!1):!0:(this.showError(`${e.name} is not an allowed file type.`),!1)}showError(e){this.messages.textContent=e}connectedCallback(){const e=this.getBoundingClientRect(),t=document.body.getBoundingClientRect();this.formContainer=this.querySelector(".file-form-container"),this.inputElem=this.querySelector("#file"),this.messages=this.querySelector("#messages"),this.style.setProperty("--popover-top",`${e.bottom}px`),this.style.setProperty("--popover-right",`${t.right-e.right+10}px`),this.handleClick=h(this,S,N).bind(this),this.handleFiles=h(this,S,_).bind(this),this.addEventListener("click",this.handleClick,!1),this.inputElem.addEventListener("change",this.handleFiles,!1)}disconnectedCallback(){this.removeEventListener("click",this.handleClick,!1),this.inputElem.removeEventListener("change",this.handleFiles,!1)}}S=new WeakSet,N=async function(e){if(e.target.dataset.action){if(e.target.dataset.action==="export"){const[t,s]=await L(fetch("/api/tasks/export"));t&&console.error("POST: Export lists: ",t);const[i,o]=await L(s.json());i&&console.error("JSON: Export lists response: ",i),this.saveJSON(o,"rminder_data")}e.target.dataset.action==="import"&&this.formContainer.showModal()}},_=function(){this.validateFile(this.inputElem.files[0])};customElements.define("rm-settings-switcher",ne);const c={Next:0,Previous:1},g={Task:0,List:1};function A(){var r;return((r=document.querySelector('meta[name="csrf-token"]'))==null?void 0:r.content)||""}var d,I,B,H,D,R,O;class oe extends HTMLElement{constructor(){super(...arguments);m(this,d);a(this,"observer");a(this,"isEditing",!1);a(this,"tasksList");a(this,"draggedTask",null)}setEdit(e,t){(e.tagName.toLowerCase()==="input"||e.tagName.toLowerCase()==="div"&&e.classList.contains("ql-editor"))&&(this.editing=t)}navigation(e,t){var l;const s=e===g.Task?this.querySelectorAll(".tasks__list > li input"):this.querySelectorAll("rm-list-nav input");if(s.length===0)return;const i=s.length-1,o=[...s].findIndex(p=>p.checked);t===c.Next&&o===i||t===c.Previous&&o===0||(o<0?s[t===c.Next?0:i].click():s[t===c.Next?o+1:o-1].click(),e===g.Task&&((l=this.querySelector(".tasks"))==null||l.scrollTo({top:o>=i/2?(o||1)*44:0,behavior:"smooth"})))}toggleTaskCompletion(){const e=[...this.querySelectorAll(".tasks__list > li input")].findIndex(t=>t.checked);e>=0&&this.querySelectorAll(".tasks__list > li .completed-check")[e].click()}toggleTaskImportance(){const e=[...this.querySelectorAll(".tasks__list > li input")].findIndex(t=>t.checked);e>=0&&this.querySelectorAll(".tasks__list > li .importance-check")[e].click()}switchPriority(){if([...this.querySelectorAll(".tasks__list > li input")].findIndex(t=>t.checked)>=0){const t=this.querySelector(".details__priority select[name='priority']");t.value=t.value==="4"?0:Number(t.value)+1,t.dispatchEvent(new Event("change"))}}reorderTask(e){var i;window.getSelection().removeAllRanges();const t=this.querySelectorAll(".tasks__list > li input");if(t.length===0)return;const s=(i=[...t].find(o=>o.checked))==null?void 0:i.closest(".drag-item");s&&(e===c.Next&&s.nextSibling&&s.parentNode.insertBefore(s,s.nextSibling.nextSibling),e===c.Previous&&s.previousSibling&&s.parentNode.insertBefore(s,s.previousSibling),this.updateTaskPosition())}async updateTaskPosition(){var s;const e=[];if((s=this.querySelectorAll(".tasks__list > li"))==null||s.forEach((i,o)=>{+i.dataset.position!==o&&(e.push({id:+i.dataset.id,position:o}),i.dataset.position=o)}),e.length===0)return;const[t]=await L(fetch("/api/tasks/reorder",{method:"POST",headers:{"Content-Type":"application/json","X-CSRF-Token":A()},body:JSON.stringify(e)}));t&&console.error("POST: Tasks reorder: ",t)}reorderList(e){var i;window.getSelection().removeAllRanges();const t=this.querySelectorAll(".lists-holder > rm-list-nav input");if(t.length===0)return;const s=(i=[...t].find(o=>o.checked))==null?void 0:i.closest("rm-list-nav");s&&(e===c.Next&&s.nextSibling&&s.parentNode.insertBefore(s,s.nextSibling.nextSibling),e===c.Previous&&s.previousSibling&&s.parentNode.insertBefore(s,s.previousSibling),this.updateListPosition())}async updateListPosition(){var s;const e=[];if((s=this.querySelectorAll(".lists-holder > rm-list-nav"))==null||s.forEach((i,o)=>{+i.dataset.position!==o&&(e.push({id:+i.dataset.id,position:o}),i.dataset.position=o)}),e.length===0)return;const[t]=await L(fetch("/api/lists/reorder",{method:"POST",headers:{"Content-Type":"application/json","X-CSRF-Token":A()},body:JSON.stringify(e)}));t&&console.error("POST: Lists reorder: ",t)}connectedCallback(){var e,t,s;this.tasksList=this.querySelector(".tasks__list"),this.observer=new ResizeObserver(i=>{document.body.style.setProperty("--vh",`${document.body.clientHeight}px`)}),this.observer.observe(document.body),this.keybidings=h(this,d,I).bind(this),this.focusInHandler=h(this,d,B).bind(this),this.focusOutHandler=h(this,d,H).bind(this),this.handleDragStart=h(this,d,D).bind(this),this.handleDragOver=h(this,d,R).bind(this),this.handleDrop=h(this,d,O).bind(this),document.addEventListener("keydown",this.keybidings,!1),document.addEventListener("focusin",this.focusInHandler,!1),document.addEventListener("focusout",this.focusOutHandler,!1),(e=this.tasksList)==null||e.addEventListener("dragstart",this.handleDragStart,!1),(t=this.tasksList)==null||t.addEventListener("dragover",this.handleDragOver,!1),(s=this.tasksList)==null||s.addEventListener("drop",this.handleDrop,!1)}disconnectedCallback(){var e,t,s;this.observer.disconnect(),document.removeEventListener("keydown",this.keybidings,!1),document.removeEventListener("focusin",this.focusInHandler,!1),document.removeEventListener("focusout",this.focusOutHandler,!1),(e=this.tasksList)==null||e.removeEventListener("dragstart",this.handleDragStart,!1),(t=this.tasksList)==null||t.removeEventListener("dragover",this.handleDragOver,!1),(s=this.tasksList)==null||s.removeEventListener("drop",this.handleDrop,!1)}}d=new WeakSet,I=function(e){var t,s,i;if(!this.editing)switch(!0){case(e.shiftKey===!1&&(e.code==="ArrowDown"||e.code==="KeyJ")):this.navigation(g.Task,c.Next);break;case(e.shiftKey===!1&&(e.code==="ArrowUp"||e.code==="KeyK")):this.navigation(g.Task,c.Previous);break;case(e.shiftKey===!1&&e.code==="BracketLeft"):this.navigation(g.List,c.Next);break;case(e.shiftKey===!1&&e.code==="BracketRight"):this.navigation(g.List,c.Previous);break;case e.code==="KeyA":e.preventDefault(),(t=this.querySelector("#task"))==null||t.focus();break;case e.code==="KeyN":e.preventDefault(),(s=this.querySelector(".add-list"))==null||s.click();break;case e.code==="KeyF":e.preventDefault(),(i=this.querySelector(".searchbox"))==null||i.focus();break;case e.code==="KeyX":this.toggleTaskCompletion();break;case e.code==="KeyI":this.toggleTaskImportance();break;case e.code==="KeyP":this.switchPriority();break;case(e.shiftKey===!0&&(e.code==="ArrowDown"||e.code==="KeyJ")):this.reorderTask(c.Next);break;case(e.shiftKey===!0&&(e.code==="ArrowUp"||e.code==="KeyK")):this.reorderTask(c.Previous);break;case(e.shiftKey===!0&&e.code==="BracketLeft"):this.reorderList(c.Next);break;case(e.shiftKey===!0&&e.code==="BracketRight"):this.reorderList(c.Previous);break}},B=function(e){this.setEdit(e.target,!0)},H=function(e){this.setEdit(e.target,!1)},D=function(e){this.draggedTask=e.target,e.dataTransfer.effectAllowed="move",e.dataTransfer.setData("text/html",this.draggedTask.innerHTML),e.target.style.opacity="0.5"},R=function(e){e.preventDefault(),e.dataTransfer.dropEffect="move";const t=e.target.classList.contains("drag-item")?e.target:e.target.closest(".drag-item");if(t!==this.draggedTask&&t){const s=t.getBoundingClientRect(),i=s.top+s.height/2;e.clientY-i>0?(t.style.borderBottom="solid 2px transparent",t.style.borderTop=""):(t.style.borderTop="solid 2px transparent",t.style.borderBottom="")}},O=function(e){e.preventDefault();const t=e.target.classList.contains("drag-item")?e.target:e.target.closest(".drag-item");t!==this.draggedTask&&t&&(e.clientY>t.getBoundingClientRect().top+t.offsetHeight/2?t.parentNode.insertBefore(this.draggedTask,t.nextSibling):t.parentNode.insertBefore(this.draggedTask,t)),this.querySelectorAll(".drag-item").forEach(s=>{s.style.borderTop="",s.style.borderBottom=""}),this.draggedTask.style.opacity="",this.draggedTask=null,this.updateTaskPosition()};customElements.define("rm-tasks-app",oe); +var se=Object.defineProperty;var P=r=>{throw TypeError(r)};var ie=(r,o,e)=>o in r?se(r,o,{enumerable:!0,configurable:!0,writable:!0,value:e}):r[o]=e;var a=(r,o,e)=>ie(r,typeof o!="symbol"?o+"":o,e),A=(r,o,e)=>o.has(r)||P("Cannot "+e);var d=(r,o,e)=>(A(r,o,"read from private field"),e?e.call(r):o.get(r)),u=(r,o,e)=>o.has(r)?P("Cannot add the same private member more than once"):o instanceof WeakSet?o.add(r):o.set(r,e),m=(r,o,e,t)=>(A(r,o,"write to private field"),t?t.call(r,e):o.set(r,e),e),p=(r,o,e)=>(A(r,o,"access private method"),e);var C,H;class re extends HTMLElement{constructor(){super(...arguments);u(this,C);a(this,"quill")}get description(){return this.quill.getSemanticHTML()}async connectedCallback(){await p(this,C,H).call(this),this.getAttribute("task-id");const e=this.querySelector("button.close"),t=this.querySelector("button.remove"),s=this.querySelector("dialog.modal"),i=this.querySelector("dialog button.default"),n=this.querySelector("dialog button.warning");this.quill=new Quill("#note-editor",{modules:{toolbar:[[{header:[1,2,3,!1]}],["bold","italic","underline","strike"],["blockquote","code-block"],["link","image"],[{list:"ordered"},{list:"bullet"}]]},bounds:this,placeholder:"Add description...",theme:"snow"}),e.addEventListener("click",()=>{const l=document.querySelector("input[name='task-detail']:checked");l&&(l.checked=!1)}),t.addEventListener("click",()=>{s==null||s.showModal()}),i.addEventListener("click",()=>{s==null||s.close()}),n.addEventListener("click",()=>{s==null||s.close()})}}C=new WeakSet,H=async function(){if(!window.Quill){if(!document.querySelector('link[href="/assets/css/libs/quill.snow.css"]')){const e=document.createElement("link");e.rel="stylesheet",e.href="/assets/css/libs/quill.snow.css",document.head.appendChild(e)}return new Promise((e,t)=>{const s=document.createElement("script");s.src="/assets/js/libs/quill.js",s.onload=e,s.onerror=t,document.head.appendChild(s)})}};customElements.define("rm-task-details",re);var y,k,b,S,E;class oe extends HTMLElement{constructor(){super(...arguments);a(this,"cancelBtn");a(this,"addList");a(this,"formContainer");a(this,"clearIcon");a(this,"searchInput");a(this,"addGroup");a(this,"cancelGroupBtn");a(this,"groupFormContainer");u(this,y);u(this,k);u(this,b);u(this,S);u(this,E)}addEvents(){var e,t;m(this,y,()=>this.closePopover()),m(this,k,()=>this.formFocus()),m(this,b,()=>this.clearSearch()),m(this,S,()=>this.closeGroupPopover()),m(this,E,()=>this.groupFormFocus()),this.addList.addEventListener("click",d(this,k)),this.cancelBtn.addEventListener("click",d(this,y)),this.clearIcon.addEventListener("click",d(this,b)),(e=this.addGroup)==null||e.addEventListener("click",d(this,E)),(t=this.cancelGroupBtn)==null||t.addEventListener("click",d(this,S))}closePopover(){var i,n,l,f;this.formContainer.close();const e=this.formContainer.querySelector("form"),t=e.querySelector(".btn.add-new-list"),s=e.querySelector(".remove-list");e.reset(),e.removeAttribute("hx-put"),e.setAttribute("hx-post","/partials/lists/create"),(i=e.querySelectorAll("input:checked"))==null||i.forEach(g=>{g.removeAttribute("checked")}),(n=e.querySelectorAll("option"))==null||n.forEach(g=>{g.removeAttribute("selected")}),(l=e.querySelector('input[name="swatch"]'))==null||l.setAttribute("checked",""),(f=e.querySelector('input[name="icon"]'))==null||f.setAttribute("checked",""),t.innerHTML="Add",s==null||s.classList.add("hidden"),s==null||s.removeAttribute("hx-delete"),htmx.process(e)}formFocus(){this.formContainer.showModal(),this.formContainer.querySelector(".new-list").focus()}closeGroupPopover(){this.groupFormContainer.close();const e=this.groupFormContainer.querySelector("form"),t=e.querySelector(".btn.add-new-group"),s=e.querySelector(".remove-group");e.reset(),e.removeAttribute("hx-put"),e.setAttribute("hx-post","/partials/groups/create"),t.innerHTML="Add",s==null||s.classList.add("hidden"),s==null||s.removeAttribute("hx-delete"),htmx.process(e)}groupFormFocus(){this.groupFormContainer.showModal(),this.groupFormContainer.querySelector(".new-group").focus()}clearSearch(){this.searchInput.value=""}connectedCallback(){this.cancelBtn=this.querySelector(".cancel-new-list"),this.addList=this.querySelector(".add-list"),this.formContainer=this.querySelector(".list-form-container"),this.clearIcon=this.querySelector(".searchbox__container i.clear-icon"),this.searchInput=this.querySelector(".searchbox"),this.addGroup=this.querySelector(".add-group"),this.cancelGroupBtn=this.querySelector(".cancel-new-group"),this.groupFormContainer=this.querySelector(".group-form-container"),this.addEvents()}disconnectedCallback(){var e,t;this.addList.removeEventListener("click",d(this,k)),this.cancelBtn.removeEventListener("click",d(this,y)),this.clearIcon.removeEventListener("click",d(this,b)),(e=this.addGroup)==null||e.removeEventListener("click",d(this,E)),(t=this.cancelGroupBtn)==null||t.removeEventListener("click",d(this,S))}}y=new WeakMap,k=new WeakMap,b=new WeakMap,S=new WeakMap,E=new WeakMap;customElements.define("rm-sidebar",oe);var L;class ne extends HTMLElement{constructor(){super(...arguments);a(this,"listId");a(this,"listName");a(this,"colour");a(this,"icon");a(this,"position");a(this,"pinned");a(this,"filter");a(this,"settings");a(this,"formContainer");u(this,L)}edit(e){var N;e.preventDefault();const t=this.formContainer.querySelector("form");(N=t.querySelectorAll("input:checked"))==null||N.forEach(T=>{T.removeAttribute("checked")});const s=t.querySelector("input.new-list"),i=t.querySelector('input[name="pin"]'),n=t.querySelector(`input[name="swatch"][value="${this.colour}"]`),l=t.querySelector(`input[name="icon"][value="${this.icon}"]`),f=t.querySelector(".btn.add-new-list"),g=t.querySelector(".remove-list"),$=t.querySelector('select[name="include"]'),G=t.querySelector('select[name="completed"]'),j=t.querySelector('select[name="important"]'),J=t.querySelector('select[name="priority"]'),U=t.querySelector('select[name="date"]'),z=t.querySelector('input[name="from"]'),Q=t.querySelector('input[name="to"]'),[X,Y,V,W,Z,ee,te]=this.filter.split(";").map(T=>T.split("=")[1]);t.removeAttribute("hx-post"),t.setAttribute("hx-put",`/partials/lists/${this.listId}`),g.setAttribute("hx-delete",`/partials/lists/${this.listId}`),s.value=this.listName,i.toggleAttribute("checked",this.pinned===""),n.setAttribute("checked",""),l.setAttribute("checked",""),f.innerHTML="Update",g.classList.remove("hidden"),$.value=X,G.value=Y,j.value=V,J.value=W,U.value=Z,z.value=ee,Q.value=te,htmx.process(t),this.formContainer.showModal()}connectedCallback(){var e;this.formContainer=document.querySelector(".list-form-container"),this.settings=this.querySelector(".settings"),this.listId=this.dataset.id,this.listName=this.dataset.name,this.colour=this.dataset.colour,this.icon=this.dataset.icon,this.pinned=this.dataset.pinned,this.position=this.dataset.position,this.filter=this.dataset.filter,m(this,L,t=>this.edit(t)),this.style.setProperty("--list-colour",`var(${this.colour})`),(e=this.settings)==null||e.addEventListener("click",d(this,L))}disconnectedCallback(){var e;(e=this.settings)==null||e.removeEventListener("click",d(this,L))}}L=new WeakMap;customElements.define("rm-list-nav",ne);var q;class ae extends HTMLElement{constructor(){super(...arguments);a(this,"groupId");a(this,"groupName");a(this,"settings");a(this,"formContainer");u(this,q)}edit(e){e.preventDefault();const t=this.formContainer.querySelector("form"),s=t.querySelector("input.new-group"),i=t.querySelector(".btn.add-new-group"),n=t.querySelector(".remove-group");t.removeAttribute("hx-post"),t.setAttribute("hx-put",`/partials/groups/${this.groupId}`),n.setAttribute("hx-delete",`/partials/groups/${this.groupId}`),s.value=this.groupName,i.innerHTML="Update",n.classList.remove("hidden"),htmx.process(t),htmx.process(n),this.formContainer.showModal(),s.focus()}connectedCallback(){var e,t,s;this.formContainer=document.querySelector(".group-form-container"),this.settings=this.querySelector(".group-settings"),this.groupId=this.dataset.groupId,this.groupName=((t=(e=this.querySelector(".group-name"))==null?void 0:e.textContent)==null?void 0:t.trim())??"",m(this,q,i=>this.edit(i)),(s=this.settings)==null||s.addEventListener("click",d(this,q))}disconnectedCallback(){var e;(e=this.settings)==null||e.removeEventListener("click",d(this,q))}}q=new WeakMap;customElements.define("rm-group-nav",ae);class le extends HTMLElement{connectedCallback(){const o=this.getBoundingClientRect(),e=document.body.getBoundingClientRect();this.style.setProperty("--popover-top",`${o.bottom}px`),this.style.setProperty("--popover-right",`${e.right-o.right+10}px`)}}customElements.define("rm-app-switcher",le);async function w(r){try{return[null,await r]}catch(o){return[o]}}var x,B,_;class ce extends HTMLElement{constructor(){super(...arguments);u(this,x);a(this,"formContainer");a(this,"fileElem");a(this,"messages");a(this,"maxFileSize",10*1024*1024);a(this,"allowedTypes",["application/json"])}saveJSON(e,t){var s=JSON.stringify(e,null,2),i=new Blob([s],{type:"application/json"}),n=URL.createObjectURL(i),l=document.createElement("a");l.download=t+".json",l.href=n,l.id=t,document.body.appendChild(l),l.click(),document.querySelector("#"+l.id).remove()}validateFile(e){return this.messages.textContent="",this.allowedTypes.includes(e.type)?e.size>this.maxFileSize?(this.showError(`${e.name} exceeds the maximum file size of 10MB.`),!1):!0:(this.showError(`${e.name} is not an allowed file type.`),!1)}showError(e){this.messages.textContent=e}connectedCallback(){const e=this.getBoundingClientRect(),t=document.body.getBoundingClientRect();this.formContainer=this.querySelector(".file-form-container"),this.inputElem=this.querySelector("#file"),this.messages=this.querySelector("#messages"),this.style.setProperty("--popover-top",`${e.bottom}px`),this.style.setProperty("--popover-right",`${t.right-e.right+10}px`),this.handleClick=p(this,x,B).bind(this),this.handleFiles=p(this,x,_).bind(this),this.addEventListener("click",this.handleClick,!1),this.inputElem.addEventListener("change",this.handleFiles,!1)}disconnectedCallback(){this.removeEventListener("click",this.handleClick,!1),this.inputElem.removeEventListener("change",this.handleFiles,!1)}}x=new WeakSet,B=async function(e){if(e.target.dataset.action){if(e.target.dataset.action==="export"){const[t,s]=await w(fetch("/api/tasks/export"));t&&console.error("POST: Export lists: ",t);const[i,n]=await w(s.json());i&&console.error("JSON: Export lists response: ",i),this.saveJSON(n,"rminder_data")}e.target.dataset.action==="import"&&this.formContainer.showModal()}},_=function(){this.validateFile(this.inputElem.files[0])};customElements.define("rm-settings-switcher",ce);const c={Next:0,Previous:1},v={Task:0,List:1};function I(){var r;return((r=document.querySelector('meta[name="csrf-token"]'))==null?void 0:r.content)||""}var h,F,D,R,O,K,M;class de extends HTMLElement{constructor(){super(...arguments);u(this,h);a(this,"observer");a(this,"isEditing",!1);a(this,"tasksList");a(this,"draggedTask",null)}setEdit(e,t){(e.tagName.toLowerCase()==="input"||e.tagName.toLowerCase()==="div"&&e.classList.contains("ql-editor"))&&(this.editing=t)}navigation(e,t){var l;const s=e===v.Task?this.querySelectorAll(".tasks__list > li input"):this.querySelectorAll("rm-list-nav input");if(s.length===0)return;const i=s.length-1,n=[...s].findIndex(f=>f.checked);t===c.Next&&n===i||t===c.Previous&&n===0||(n<0?s[t===c.Next?0:i].click():s[t===c.Next?n+1:n-1].click(),e===v.Task&&((l=this.querySelector(".tasks"))==null||l.scrollTo({top:n>=i/2?(n||1)*44:0,behavior:"smooth"})))}toggleTaskCompletion(){const e=[...this.querySelectorAll(".tasks__list > li input")].findIndex(t=>t.checked);e>=0&&this.querySelectorAll(".tasks__list > li .completed-check")[e].click()}toggleTaskImportance(){const e=[...this.querySelectorAll(".tasks__list > li input")].findIndex(t=>t.checked);e>=0&&this.querySelectorAll(".tasks__list > li .importance-check")[e].click()}switchPriority(){if([...this.querySelectorAll(".tasks__list > li input")].findIndex(t=>t.checked)>=0){const t=this.querySelector(".details__priority select[name='priority']");t.value=t.value==="4"?0:Number(t.value)+1,t.dispatchEvent(new Event("change"))}}reorderTask(e){var i;window.getSelection().removeAllRanges();const t=this.querySelectorAll(".tasks__list > li input");if(t.length===0)return;const s=(i=[...t].find(n=>n.checked))==null?void 0:i.closest(".drag-item");s&&(e===c.Next&&s.nextSibling&&s.parentNode.insertBefore(s,s.nextSibling.nextSibling),e===c.Previous&&s.previousSibling&&s.parentNode.insertBefore(s,s.previousSibling),this.updateTaskPosition())}async updateTaskPosition(){var s;const e=[];if((s=this.querySelectorAll(".tasks__list > li"))==null||s.forEach((i,n)=>{+i.dataset.position!==n&&(e.push({id:+i.dataset.id,position:n}),i.dataset.position=n)}),e.length===0)return;const[t]=await w(fetch("/api/tasks/reorder",{method:"POST",headers:{"Content-Type":"application/json","X-CSRF-Token":I()},body:JSON.stringify(e)}));t&&console.error("POST: Tasks reorder: ",t)}reorderList(e){var i;window.getSelection().removeAllRanges();const t=this.querySelectorAll(".lists-holder > rm-list-nav input");if(t.length===0)return;const s=(i=[...t].find(n=>n.checked))==null?void 0:i.closest("rm-list-nav");s&&(e===c.Next&&s.nextSibling&&s.parentNode.insertBefore(s,s.nextSibling.nextSibling),e===c.Previous&&s.previousSibling&&s.parentNode.insertBefore(s,s.previousSibling),this.updateListPosition())}async updateListPosition(){var s;const e=[];if((s=this.querySelectorAll(".lists-holder > rm-list-nav"))==null||s.forEach((i,n)=>{+i.dataset.position!==n&&(e.push({id:+i.dataset.id,position:n}),i.dataset.position=n)}),e.length===0)return;const[t]=await w(fetch("/api/lists/reorder",{method:"POST",headers:{"Content-Type":"application/json","X-CSRF-Token":I()},body:JSON.stringify(e)}));t&&console.error("POST: Lists reorder: ",t)}connectedCallback(){var e,t,s;this.tasksList=this.querySelector(".tasks__list"),this.observer=new ResizeObserver(i=>{document.body.style.setProperty("--vh",`${document.body.clientHeight}px`)}),this.observer.observe(document.body),this.keybidings=p(this,h,F).bind(this),this.focusInHandler=p(this,h,D).bind(this),this.focusOutHandler=p(this,h,R).bind(this),this.handleDragStart=p(this,h,O).bind(this),this.handleDragOver=p(this,h,K).bind(this),this.handleDrop=p(this,h,M).bind(this),document.addEventListener("keydown",this.keybidings,!1),document.addEventListener("focusin",this.focusInHandler,!1),document.addEventListener("focusout",this.focusOutHandler,!1),(e=this.tasksList)==null||e.addEventListener("dragstart",this.handleDragStart,!1),(t=this.tasksList)==null||t.addEventListener("dragover",this.handleDragOver,!1),(s=this.tasksList)==null||s.addEventListener("drop",this.handleDrop,!1)}disconnectedCallback(){var e,t,s;this.observer.disconnect(),document.removeEventListener("keydown",this.keybidings,!1),document.removeEventListener("focusin",this.focusInHandler,!1),document.removeEventListener("focusout",this.focusOutHandler,!1),(e=this.tasksList)==null||e.removeEventListener("dragstart",this.handleDragStart,!1),(t=this.tasksList)==null||t.removeEventListener("dragover",this.handleDragOver,!1),(s=this.tasksList)==null||s.removeEventListener("drop",this.handleDrop,!1)}}h=new WeakSet,F=function(e){var t,s,i;if(!this.editing)switch(!0){case(e.shiftKey===!1&&(e.code==="ArrowDown"||e.code==="KeyJ")):this.navigation(v.Task,c.Next);break;case(e.shiftKey===!1&&(e.code==="ArrowUp"||e.code==="KeyK")):this.navigation(v.Task,c.Previous);break;case(e.shiftKey===!1&&e.code==="BracketLeft"):this.navigation(v.List,c.Next);break;case(e.shiftKey===!1&&e.code==="BracketRight"):this.navigation(v.List,c.Previous);break;case e.code==="KeyA":e.preventDefault(),(t=this.querySelector("#task"))==null||t.focus();break;case e.code==="KeyN":e.preventDefault(),(s=this.querySelector(".add-list"))==null||s.click();break;case e.code==="KeyF":e.preventDefault(),(i=this.querySelector(".searchbox"))==null||i.focus();break;case e.code==="KeyX":this.toggleTaskCompletion();break;case e.code==="KeyI":this.toggleTaskImportance();break;case e.code==="KeyP":this.switchPriority();break;case(e.shiftKey===!0&&(e.code==="ArrowDown"||e.code==="KeyJ")):this.reorderTask(c.Next);break;case(e.shiftKey===!0&&(e.code==="ArrowUp"||e.code==="KeyK")):this.reorderTask(c.Previous);break;case(e.shiftKey===!0&&e.code==="BracketLeft"):this.reorderList(c.Next);break;case(e.shiftKey===!0&&e.code==="BracketRight"):this.reorderList(c.Previous);break}},D=function(e){this.setEdit(e.target,!0)},R=function(e){this.setEdit(e.target,!1)},O=function(e){this.draggedTask=e.target,e.dataTransfer.effectAllowed="move",e.dataTransfer.setData("text/html",this.draggedTask.innerHTML),e.target.style.opacity="0.5"},K=function(e){e.preventDefault(),e.dataTransfer.dropEffect="move";const t=e.target.classList.contains("drag-item")?e.target:e.target.closest(".drag-item");if(t!==this.draggedTask&&t){const s=t.getBoundingClientRect(),i=s.top+s.height/2;e.clientY-i>0?(t.style.borderBottom="solid 2px transparent",t.style.borderTop=""):(t.style.borderTop="solid 2px transparent",t.style.borderBottom="")}},M=function(e){e.preventDefault();const t=e.target.classList.contains("drag-item")?e.target:e.target.closest(".drag-item");t!==this.draggedTask&&t&&(e.clientY>t.getBoundingClientRect().top+t.offsetHeight/2?t.parentNode.insertBefore(this.draggedTask,t.nextSibling):t.parentNode.insertBefore(this.draggedTask,t)),this.querySelectorAll(".drag-item").forEach(s=>{s.style.borderTop="",s.style.borderBottom=""}),this.draggedTask.style.opacity="",this.draggedTask=null,this.updateTaskPosition()};customElements.define("rm-tasks-app",de); diff --git a/web/templates/apps/tasks/components/sidebar.html b/web/templates/apps/tasks/components/sidebar.html index 9017986..2d4afe9 100644 --- a/web/templates/apps/tasks/components/sidebar.html +++ b/web/templates/apps/tasks/components/sidebar.html @@ -2,8 +2,8 @@ {{template "sidebar-header"}} {{template "search"}} - {{template "sidebar-lists" (dict "Lists" .Lists "Persistence" .Persistence)}} - {{template "sidebar-footer" .Lists}} + {{template "sidebar-lists" (dict "Lists" .Lists "Groups" .Groups "Persistence" .Persistence)}} + {{template "sidebar-footer" (dict "Lists" .Lists "Groups" .Groups)}} {{end}} @@ -55,12 +55,16 @@ {{end}} {{end}} - + + {{range .Groups}} + {{template "group-item" (dict "Group" . "SelectedList" $.Persistence.ListId)}} + {{end}} +

My lists

{{range .Lists}} - {{if not .Pinned}} + {{if and (not .Pinned) (eq .GroupId 0)}} {{template "list-item" (dict "List" . "SelectedList" $.Persistence.ListId)}} {{end}} {{end}} @@ -102,18 +106,71 @@ {{end}} +{{define "group-item"}} + +
+ {{.Group.Name}} +
+ +
+
+
+ {{range .Group.Lists}} + {{template "list-item" (dict "List" . "SelectedList" $.SelectedList)}} + {{end}} +
+
+{{end}} + {{define "sidebar-footer"}}
- {{template "list-modal" .}} + {{template "list-modal" .Lists}} + {{template "group-modal" .Groups}}
{{end}} +{{define "group-modal"}} + +
+ +
+
+
Cancel
+ +
+ +
+
+
+{{end}} + {{define "list-modal"}}