memberships: modal for errors (#133)

* unpolished version of error modal rendering

* rework of html files. template implemented

* new html files

* minor fixes.

* minor fixes

* title changes
diff --git a/core/auth/memberships/.gitignore b/core/auth/memberships/.gitignore
index 6ce208d..fd1862e 100644
--- a/core/auth/memberships/.gitignore
+++ b/core/auth/memberships/.gitignore
@@ -1,2 +1,3 @@
 *.db
 memberships*
+!memberships-tmpl/
\ No newline at end of file
diff --git a/core/auth/memberships/main.go b/core/auth/memberships/main.go
index f8f2cff..7cb7538 100644
--- a/core/auth/memberships/main.go
+++ b/core/auth/memberships/main.go
@@ -9,6 +9,7 @@
 	"html/template"
 	"log"
 	"net/http"
+	"net/url"
 	"regexp"
 	"strings"
 
@@ -23,11 +24,8 @@
 var apiPort = flag.Int("api-port", 8081, "Port to listen on for API requests")
 var dbPath = flag.String("db-path", "memberships.db", "Path to SQLite file")
 
-//go:embed index.html
-var indexHTML string
-
-//go:embed group.html
-var groupHTML string
+//go:embed memberships-tmpl/*
+var tmpls embed.FS
 
 //go:embed static
 var staticResources embed.FS
@@ -563,6 +561,34 @@
 	return true, nil
 }
 
+type templates struct {
+	group *template.Template
+	user  *template.Template
+}
+
+func parseTemplates(fs embed.FS) (templates, error) {
+	base, err := template.ParseFS(fs, "memberships-tmpl/base.html")
+	if err != nil {
+		return templates{}, err
+	}
+	parse := func(path string) (*template.Template, error) {
+		if b, err := base.Clone(); err != nil {
+			return nil, err
+		} else {
+			return b.ParseFS(fs, path)
+		}
+	}
+	user, err := parse("memberships-tmpl/user.html")
+	if err != nil {
+		return templates{}, err
+	}
+	group, err := parse("memberships-tmpl/group.html")
+	if err != nil {
+		return templates{}, err
+	}
+	return templates{group, user}, nil
+}
+
 func (s *Server) homePageHandler(w http.ResponseWriter, r *http.Request) {
 	loggedInUser, err := getLoggedInUser(r)
 	if err != nil {
@@ -578,6 +604,7 @@
 		http.Error(w, "User Not Logged In", http.StatusUnauthorized)
 		return
 	}
+	errorMsg := r.URL.Query().Get("errorMessage")
 	vars := mux.Vars(r)
 	user := strings.ToLower(vars["username"])
 	// TODO(dtabidze): should check if username exists or not.
@@ -592,11 +619,6 @@
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
-	tmpl, err := template.New("index").Parse(indexHTML)
-	if err != nil {
-		http.Error(w, err.Error(), http.StatusInternalServerError)
-		return
-	}
 	transitiveGroups, err := s.store.GetAllTransitiveGroupsForUser(user)
 	if err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -608,15 +630,23 @@
 		TransitiveGroups []Group
 		LoggedInUserPage bool
 		CurrentUser      string
+		ErrorMessage     string
+		AdditionalCSS    bool
 	}{
 		OwnerGroups:      ownerGroups,
 		MembershipGroups: membershipGroups,
 		TransitiveGroups: transitiveGroups,
 		LoggedInUserPage: loggedInUserPage,
 		CurrentUser:      user,
+		ErrorMessage:     errorMsg,
+		AdditionalCSS:    false,
 	}
-	w.Header().Set("Content-Type", "text/html")
-	if err := tmpl.Execute(w, data); err != nil {
+	templates, err := parseTemplates(tmpls)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	if err := templates.user.Execute(w, data); err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
@@ -639,12 +669,16 @@
 	var group Group
 	group.Name = r.PostFormValue("group-name")
 	if err := isValidGroupName(group.Name); err != nil {
-		http.Error(w, err.Error(), http.StatusBadRequest)
+		// http.Error(w, err.Error(), http.StatusBadRequest)
+		redirectURL := fmt.Sprintf("/user/%s?errorMessage=%s", loggedInUser, url.QueryEscape(err.Error()))
+		http.Redirect(w, r, redirectURL, http.StatusFound)
 		return
 	}
 	group.Description = r.PostFormValue("description")
 	if err := s.store.CreateGroup(loggedInUser, group); err != nil {
-		http.Error(w, err.Error(), http.StatusInternalServerError)
+		// http.Error(w, err.Error(), http.StatusInternalServerError)
+		redirectURL := fmt.Sprintf("/user/%s?errorMessage=%s", loggedInUser, url.QueryEscape(err.Error()))
+		http.Redirect(w, r, redirectURL, http.StatusFound)
 		return
 	}
 	http.Redirect(w, r, "/", http.StatusSeeOther)
@@ -656,6 +690,7 @@
 		http.Error(w, "User Not Logged In", http.StatusUnauthorized)
 		return
 	}
+	errorMsg := r.URL.Query().Get("errorMessage")
 	vars := mux.Vars(r)
 	groupName := vars["group-name"]
 	exists, err := s.store.DoesGroupExist(groupName)
@@ -664,11 +699,11 @@
 		return
 	}
 	if !exists {
-		errorMsg := fmt.Sprintf("group with the name '%s' not found", groupName)
+		errorMsg = fmt.Sprintf("group with the name '%s' not found", groupName)
 		http.Error(w, errorMsg, http.StatusNotFound)
 		return
 	}
-	tmpl, err := template.New("group").Parse(groupHTML)
+	// tmpl, err := template.New("group").Parse(groupHTML)
 	if err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
@@ -711,6 +746,7 @@
 		AvailableGroups  []string
 		TransitiveGroups []Group
 		ChildGroups      []Group
+		ErrorMessage     string
 	}{
 		GroupName:        groupName,
 		Description:      description,
@@ -719,8 +755,14 @@
 		AvailableGroups:  availableGroups,
 		TransitiveGroups: transitiveGroups,
 		ChildGroups:      childGroups,
+		ErrorMessage:     errorMsg,
 	}
-	if err := tmpl.Execute(w, data); err != nil {
+	templates, err := parseTemplates(tmpls)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	if err := templates.group.Execute(w, data); err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
@@ -750,7 +792,8 @@
 		}
 		err := s.store.RemoveFromGroupToGroup(parentGroup, childGroup)
 		if err != nil {
-			http.Error(w, err.Error(), http.StatusInternalServerError)
+			redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", parentGroup, url.QueryEscape(err.Error()))
+			http.Redirect(w, r, redirectURL, http.StatusFound)
 			return
 		}
 		http.Redirect(w, r, "/group/"+parentGroup, http.StatusSeeOther)
@@ -778,7 +821,8 @@
 		}
 		err := s.store.RemoveUserFromTable(username, groupName, tableName)
 		if err != nil {
-			http.Error(w, err.Error(), http.StatusInternalServerError)
+			redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupName, url.QueryEscape(err.Error()))
+			http.Redirect(w, r, redirectURL, http.StatusFound)
 			return
 		}
 		http.Redirect(w, r, "/group/"+groupName, http.StatusSeeOther)
@@ -806,7 +850,8 @@
 		}
 		err := s.store.RemoveUserFromTable(username, groupName, tableName)
 		if err != nil {
-			http.Error(w, err.Error(), http.StatusInternalServerError)
+			redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupName, url.QueryEscape(err.Error()))
+			http.Redirect(w, r, redirectURL, http.StatusFound)
 			return
 		}
 		http.Redirect(w, r, "/group/"+groupName, http.StatusSeeOther)
@@ -853,7 +898,8 @@
 		return
 	}
 	if err != nil {
-		http.Error(w, err.Error(), http.StatusInternalServerError)
+		redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupName, url.QueryEscape(err.Error()))
+		http.Redirect(w, r, redirectURL, http.StatusFound)
 		return
 	}
 	http.Redirect(w, r, "/group/"+groupName, http.StatusSeeOther)
@@ -886,7 +932,8 @@
 		return
 	}
 	if err := s.store.AddChildGroup(parentGroup, childGroup); err != nil {
-		http.Error(w, err.Error(), http.StatusInternalServerError)
+		redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", parentGroup, url.QueryEscape(err.Error()))
+		http.Redirect(w, r, redirectURL, http.StatusFound)
 		return
 	}
 	http.Redirect(w, r, "/group/"+parentGroup, http.StatusSeeOther)
diff --git a/core/auth/memberships/memberships-tmpl/base.html b/core/auth/memberships/memberships-tmpl/base.html
new file mode 100644
index 0000000..39a7755
--- /dev/null
+++ b/core/auth/memberships/memberships-tmpl/base.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html lang="en" data-theme="light">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>{{ block "title" . }}{{ end }}</title>
+    <link rel="stylesheet" href="/static/pico.2.0.6.min.css">
+    <link rel="stylesheet" href="/static/main.css">
+</head>
+<body class="container">
+    {{- block "content" . }}
+    {{- end }}
+    {{ if ne .ErrorMessage "" }}
+    <dialog id="error-message" open>
+        <article>
+            <h2>Error</h2>
+            <p id="error-message-content">{{ .ErrorMessage }}</p>
+            <footer>
+                <button id="error-cancel-button" class="secondary error-cancel-button">Close</button>
+            </footer>
+        </article>
+    </dialog>
+    {{ end }}
+    <script src="/static/main.js"></script>
+</body>
+</html>
diff --git a/core/auth/memberships/group.html b/core/auth/memberships/memberships-tmpl/group.html
similarity index 89%
rename from core/auth/memberships/group.html
rename to core/auth/memberships/memberships-tmpl/group.html
index 42a2e38..2f5c89a 100644
--- a/core/auth/memberships/group.html
+++ b/core/auth/memberships/memberships-tmpl/group.html
@@ -1,14 +1,8 @@
-<!DOCTYPE html>
-<html lang="en" data-theme="light">
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>Group Membership Managment</title>
-    <link rel="stylesheet" href="/static/pico.2.0.6.min.css">
-    <link rel="stylesheet" href="/static/main.css">
-</head>
-<body class="container">
-    {{- $parentGroupName := .GroupName }}
+{{ define "title" }}
+    Group - {{ .GroupName }}
+{{ end }}
+{{ define "content" }}
+{{- $parentGroupName := .GroupName }}
     <div>
         <h2 class="headline">{{ .GroupName }} Group Management</h2>
         <p class="description">{{ .Description }}</p>
@@ -110,6 +104,4 @@
             </footer>
         </article>
     </dialog>
-    <script src="/static/main.js"></script>
-</body>
-</html>
+{{ end }}
diff --git a/core/auth/memberships/index.html b/core/auth/memberships/memberships-tmpl/user.html
similarity index 81%
rename from core/auth/memberships/index.html
rename to core/auth/memberships/memberships-tmpl/user.html
index cd3cf50..53a89be 100644
--- a/core/auth/memberships/index.html
+++ b/core/auth/memberships/memberships-tmpl/user.html
@@ -1,12 +1,7 @@
-<!DOCTYPE html>
-<html lang="en" data-theme="light">
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>Group Managment</title>
-    <link rel="stylesheet" href="/static/pico.2.0.6.min.css">
-</head>
-<body class="container">
+{{ define "title" }}
+    User - {{ .CurrentUser }}
+{{ end }}
+{{- define "content" -}}
     <h1>User: {{ .CurrentUser }}</h1>
     {{ if .LoggedInUserPage }}
     <form action="/create-group" method="post">
@@ -56,5 +51,4 @@
         </tr>
         {{- end -}}
     </table>
-</body>
-</html>
+{{- end }}
diff --git a/core/auth/memberships/static/main.js b/core/auth/memberships/static/main.js
index e4ea2ca..52dd1d8 100644
--- a/core/auth/memberships/static/main.js
+++ b/core/auth/memberships/static/main.js
@@ -45,3 +45,11 @@
         });
     });
 });
+
+document.addEventListener("DOMContentLoaded", function () {
+    var errorCancelButton = document.getElementById("error-cancel-button");
+    var errorMessageDialog = document.getElementById("error-message");
+    errorCancelButton.addEventListener("click", function () {
+        errorMessageDialog.close();
+    });
+});