diff options
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | assets/index.js | 28 | ||||
-rw-r--r-- | assets/index.ts | 32 | ||||
-rw-r--r-- | assets/main.css | 13 | ||||
-rw-r--r-- | doc.go | 40 | ||||
-rw-r--r-- | ideez_nginx.conf | 25 | ||||
-rw-r--r-- | main.go | 308 | ||||
-rw-r--r-- | server.go | 289 | ||||
-rw-r--r-- | templates/edit.html (renamed from t_idea/edit.html) | 15 | ||||
-rw-r--r-- | templates/ideas.html (renamed from t_idea/index.html) | 16 | ||||
-rw-r--r-- | templates/servers.html | 28 |
11 files changed, 549 insertions, 247 deletions
@@ -1,3 +1,3 @@ tmp/ -ideez ideas.data +ideez_servers diff --git a/assets/index.js b/assets/index.js index 6146c01..7905d9c 100644 --- a/assets/index.js +++ b/assets/index.js @@ -1,4 +1,9 @@ "use strict"; +// Get the routeprefix by finding the '/' after the ID +let url = window.location.pathname; +let end = url.indexOf("/", "/server/".length + 1); +let routePrefix = url.substring(0, end); +console.log("routePrefix:", routePrefix); let dels = document.querySelectorAll("form[action=\"/idea/delete/\"]"); for (let el of dels) { el.onsubmit = function (e) { @@ -11,8 +16,25 @@ for (let el of dels) { let eels = document.querySelectorAll("button.edit"); for (let el of eels) { el.onclick = function () { - console.log("clicked"); - let title = el.getAttribute("data-title"); - location.href = "/idea/edit?t=" + title; + let id = el.getAttribute("idea-id"); + location.href = routePrefix + "/idea/edit?id=" + id; }; } +let link = document.getElementById("link"); +if (link !== null) { + link.addEventListener("click", function () { + navigator.clipboard.writeText(window.location.href); + let old_text; + if (link !== null) { + old_text = link.innerHTML; + link.innerHTML = "(copied)"; + link.classList.remove("copied"); + } + setTimeout(function () { + if (link !== null) { + link.innerHTML = old_text; + link.classList.add("copied"); + } + }, 1000); + }); +} diff --git a/assets/index.ts b/assets/index.ts index 70b03b7..8081428 100644 --- a/assets/index.ts +++ b/assets/index.ts @@ -1,3 +1,9 @@ +// Get the routeprefix by finding the '/' after the ID +let url = window.location.pathname +let end = url.indexOf("/", "/server/".length +1) +let routePrefix = url.substring(0, end) +console.log("routePrefix:", routePrefix) + let dels:NodeListOf<HTMLFormElement> = document.querySelectorAll("form[action=\"/idea/delete/\"]"); for (let el of dels) { el.onsubmit = function(e) { @@ -8,11 +14,29 @@ for (let el of dels) { }; } -let eels:NodeListOf<HTMLElement> = document.querySelectorAll("button.edit"); +let eels:NodeListOf<HTMLButtonElement> = document.querySelectorAll("button.edit"); for (let el of eels) { el.onclick = function() { - console.log("clicked") - let title = el.getAttribute("data-title"); - location.href = "/idea/edit?t=" + title; + let id = el.getAttribute("idea-id"); + location.href = routePrefix + "/idea/edit?id=" + id; } } + +let link = document.getElementById("link"); +if (link !== null) { +link.addEventListener("click", function() { + navigator.clipboard.writeText(window.location.href) + let old_text: string; + if (link !== null) { + old_text = link.innerHTML; + link.innerHTML = "(copied)"; + link.classList.remove("copied"); + } + setTimeout(function() { + if (link !== null) { + link.innerHTML = old_text; + link.classList.add("copied"); + } + }, 1000); +}); +} diff --git a/assets/main.css b/assets/main.css index b3d6710..d551829 100644 --- a/assets/main.css +++ b/assets/main.css @@ -49,6 +49,19 @@ textarea[name="text"] { h3 { margin-bottom: 0; } +span#link { + color: #b38eac; +} +span#link:hover { + cursor: grab; + text-decoration: underline; +} +span#link.copied { + color: #b38eac; +} +span.link:hover { + cursor: grab; +} input[type="text"]{ border-radius: 3px; border: solid 1px black; @@ -0,0 +1,40 @@ +package main + +// ideez is a web application that allows people to post their ideas, it is meant to help +// brainstorming. + +// ToDo's +// - [ ] htmx or websockets +// - [ ] (server.go): update version number because data file is not compatible anymore +// - [ ] change name for server +// - [ ] use a hash instead of random hexes +// - [ ] create a module and a program +// - [x] change logger inside Ideez +// - [x] change og: properties for ideas.html +// - [x] import functionality for servers by walking the ideez_servers directory +// - [x] (server.go): fix cancel on edit +// - [x] pass mux to newServer, so we don't need to create 2 +// - [x] change CreatedAt to be Last Updated +// - [x] edit a post +// - [x] Store ideas to a file (encoder/gob) +// - [x] Change the date format printing +// - [x] outsource removing the posts to a separate cli tool +// - [x] Add a post +// - [x] Remove a post +// - [x] Create a Server out of this so you can run multiple instances +// - [x] check redirects + +// testing: +// - test for incompatible versions +// - test all routes +// - test importing + +// Every server is accessible at /server/ID, then all the routes should apply with these as the "root" +// route. eg. /server/ID/idea/edit +// +// When init server we +// - associate DataFilename with the ID +// - start it on the required address +// - append the id and process to a list +// - use the ID to create a path on which it will be served +// - the newServer() function returns a handler that we can associate with a route diff --git a/ideez_nginx.conf b/ideez_nginx.conf new file mode 100644 index 0000000..b22eb58 --- /dev/null +++ b/ideez_nginx.conf @@ -0,0 +1,25 @@ +server { + server_name ideez.website.com; + location / { + proxy_pass http://127.0.0.1:15118; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + } + + + listen 443 ssl; + ssl_certificate /etc/letsencrypt/live/ideez.website.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/ideez.website.com/privkey.pem; + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + +} + +server { + if ($host = ideez.website.com) { + return 301 https://$host$request_uri; + } + server_name ideez.website.com; + listen 80; + return 404; +} @@ -1,279 +1,133 @@ -package main - // ideez is a web application that allows people to post their ideas, it is meant to help // brainstorming. - -// ToDo's -// - [ ] Create a Server out of this so you can run multiple instances -// - [ ] change CreatedAt to be Last Updated -// - [x] edit a post -// - [x] Store ideas to a file (encoder/gob) -// - [x] Change the date format printing -// - [x] outsource removing the posts to a separate cli tool -// - [x] Add a post -// - [x] Remove a post +package main import ( - _ "embed" - "encoding/gob" + "crypto/rand" + "encoding/hex" "errors" - "fmt" "html/template" - "io" "log" "net/http" "os" - "os/signal" - "runtime/debug" - "time" -) - -// File for persistent storage -var DataFilename = "ideas.data" - -// Version number used for debugging compatibility with the .data file -var Version string + "strings" -// layout for how the date should be output in html -var DateLayout string = "02/01/2006 on 15:04" - -// template for ideas html -var ( - //go:embed t_idea/index.html - ideas_html string - //go:embed t_idea/edit.html - idea_edit_html string + _ "embed" ) -var Ideas []Idea - -// Represents an idea -// LastUpdated is a formatted date string -// out of 5 rating of the idea -// formatted time string with DateLayout -type Idea struct { - Title string - Text string - Author string - LastUpdated string -} - -// Data passed to the ideas_html template -type PageData struct { - Ideas []Idea - Error string +type ServersPageData struct { + Ideezes []Ideez + Address template.URL + Error string } -func GetVersion() string { - buildinfo, ok := debug.ReadBuildInfo() - if !ok { - log.Fatalln("Could not read buildinfo to know package version.") - } - return buildinfo.Main.Version -} - -func main() { - Version = GetVersion() - - // If the .data file does not exist, create it in the cache directory - { - p := os.Getenv("XDG_CACHE_HOME") - if p == "" { - p = "." - } - DataFilename = p + "/" + DataFilename - - f, err := os.Open(DataFilename) - if errors.Is(err, os.ErrNotExist) { - // The file does not exist, so create one with only Version encoded - log.Println("File does not exist, creating", DataFilename) - f, err = os.Create(DataFilename) - if err != nil { - log.Fatalln("Error while creating data file:", err) - } - - enc := gob.NewEncoder(f) - if err := enc.Encode(Version); err != nil { - log.Fatalln("Error while encoding Version:", err) - } - } else if err != nil { - log.Fatalln("Error while opening data file:", err) - } else { - dec := gob.NewDecoder(f) - - // Check the version - var v string - if err := dec.Decode(&v); err != io.EOF && err != nil { - log.Fatalln("Error while decoding version:", err) - } - if v != Version { - log.Fatalf("Version mismatch for datafile@%s != package@%s\n", v, Version) - } - - // Decode the data and import it into Ideas - err = dec.Decode(&Ideas) - if err != nil && err != io.EOF { - log.Fatalln("Error while decoding ideas:", err) - } - log.Printf("Imported @%s: %d ideas\n", v, len(Ideas)) - if err := f.Close(); err != nil { - log.Fatalln("Error while closing file:", err) - } - } - - } - - // Handle SIGINT - // Save all Ideas into the data file - // NOTE (Luca): This system might seem dumb, but it only adds overhead at startup - // and exit of the program which is fine. - go func() { - c := make(chan os.Signal, 1) - signal.Notify(c, os.Interrupt) - <-c - - f, err := os.Create(DataFilename) - if err != nil { - log.Fatalln(err) - } - defer f.Close() +const ( + // Name of the directory where the data files are stored + DATA_DIR = "ideez_servers" +) - enc := gob.NewEncoder(f) - if err := enc.Encode(Version); err != nil { - log.Fatalln(err) - } +var address string = "localhost:15118" - if err := enc.Encode(Ideas); err != nil { - log.Fatalln("Error while saving ideas:", err) - } +//go:embed templates/servers.html +var servers_html string - log.Println("data saved.") - os.Exit(0) - }() +// All servers that are currently managed +var Ideezes []Ideez - // Handling http +// Directory path where the data files are stored +var DataDir string - tmpl, err := template.New("ideas_html").Parse(ideas_html) - if err != nil { - log.Fatalln(err) +func getAddress(r *http.Request) template.URL { + p, ok := r.Header["X-Forwarded-Proto"] + if ok { + return template.URL(p[0] + "://" + r.Host) } - _, err = tmpl.New("edit").Parse(idea_edit_html) - if err != nil { - log.Fatalln(err) + if r.TLS == nil { + return template.URL("http://" + r.Host) + } else { + return template.URL("https://" + r.Host) } +} - // TODO (Luca): Make the app more interactive by using websockets instead, - // such that adding or editing does not require to - // refresh the page. - // Another approach would be to use htmx? - // +func main() { + var ( + logger = log.New(os.Stdout, "Ideez: ", log.Ldate) + ) mux := http.NewServeMux() fs := http.FileServer(http.Dir("assets/")) mux.Handle("/static/", http.StripPrefix("/static/", fs)) - mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, "/ideas/", http.StatusMovedPermanently) - }) + tmpl, err := template.New("servers").Parse(servers_html) - mux.HandleFunc("GET /ideas/", func(w http.ResponseWriter, r *http.Request) { - tmpl.Execute(w, PageData{Ideas, ""}) + // list servers + mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) { + tmpl.ExecuteTemplate(w, "servers", ServersPageData{Ideezes, getAddress(r), ""}) }) - mux.HandleFunc("POST /idea/create/", func(w http.ResponseWriter, r *http.Request) { - i := Idea{ - Title: r.FormValue("title"), - Author: r.FormValue("author"), - LastUpdated: time.Now().Format(DateLayout), - Text: r.FormValue("text"), - } - if i.Title == "" || i.Author == "" || i.Text == "" { - tmpl.Execute(w, PageData{Ideas, "All fields are required"}) + // create a new server + mux.HandleFunc("POST /server/create/", func(w http.ResponseWriter, r *http.Request) { + addr := getAddress(r) + name := r.FormValue("name") + logger.Println("Create server", name) + if name == "" { + tmpl.ExecuteTemplate(w, "servers", ServersPageData{Ideezes, addr, "You must provide a title"}) return } - for _, v := range Ideas { - if i.Title == v.Title { - tmpl.Execute(w, PageData{Ideas, "An idea with title '" + v.Title + "' already exists!"}) + for _, v := range Ideezes { + if v.Name == name { + tmpl.ExecuteTemplate(w, "servers", ServersPageData{Ideezes, addr, "A server with name '" + name + "' already exists!"}) return } } - Ideas = append(Ideas, i) - log.Println("Added new idea:", i.Title) - http.Redirect(w, r, "/ideas/", http.StatusMovedPermanently) - }) - - // A page to edit the idea, this page should lead to POST /idea/edit for confirming the edit. If - // the user cancels they should be redirected to the start page. This is done in the html. - mux.HandleFunc("GET /idea/edit/", func(w http.ResponseWriter, r *http.Request) { - t := r.URL.Query().Get("t") - if t == "" { - tmpl.Execute(w, PageData{Ideas, "You must provide a title."}) - return + bytes := make([]byte, SERVER_ID_BYTES) + _, err := rand.Read(bytes) + if err != nil { + panic(err) } + id := hex.EncodeToString(bytes) - for _, i := range Ideas { - if i.Title == t { - tmpl.ExecuteTemplate(w, "edit", i) - return - } - } - tmpl.Execute(w, PageData{Ideas, "No idea with title '" + t + "'."}) + server := NewIdeez(mux, name, id) + Ideezes = append(Ideezes, server) + http.Redirect(w, r, "/", http.StatusMovedPermanently) }) - // Perform the edit action on the idea - mux.HandleFunc("POST /idea/edit/", func(w http.ResponseWriter, r *http.Request) { - t := r.FormValue("title") - if t == "" { - tmpl.Execute(w, PageData{Ideas, "You must provide a title."}) - return - } - - var i *Idea - for j := range Ideas { - if Ideas[j].Title == t { - i = &Ideas[j] - break - } + // import the servers from the data files + { + p := os.Getenv("XDG_CACHE_HOME") + if p == "" { + p = "." } - if i.Title == "" { - tmpl.Execute(w, PageData{Ideas, "No idea with title '" + t + "'."}) + DataDir = p + "/" + DATA_DIR + if err := os.Mkdir(DataDir, 0755); err == nil { + logger.Println("Directory does not exist, creating " + DataDir) + } else if !errors.Is(err, os.ErrExist) { + panic(err) } - i.Title = r.FormValue("title") - i.Text = r.FormValue("text") - i.LastUpdated = time.Now().Format(DateLayout) + " (edit)" - log.Printf("Edited '%s'\n", i.Title) - - http.Redirect(w, r, "/ideas/", http.StatusMovedPermanently) - }) - - mux.HandleFunc("POST /idea/delete/", func(w http.ResponseWriter, r *http.Request) { - t := r.FormValue("title") - if t == "" { - tmpl.Execute(w, PageData{Ideas, "You must provide a title."}) - return + entries, err := os.ReadDir(DataDir) + if err != nil { + panic(err) } - for i, v := range Ideas { - if t == v.Title { - log.Println("Deleted:", v.Title) - Ideas = append(Ideas[:i], Ideas[i+1:]...) - http.Redirect(w, r, "/ideas/", http.StatusMovedPermanently) - return + counter := 0 + for _, file := range entries { + if !strings.HasSuffix(file.Name(), ".data") { + continue } + id := strings.TrimSuffix(file.Name(), ".data") + ideez := NewIdeez(mux, "", id) + Ideezes = append(Ideezes, ideez) + counter++ } - tmpl.Execute(w, PageData{Ideas, "No idea with title '" + t + "'."}) - }) - - mux.HandleFunc("POST /comment/create/", func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "Not implemented yet.") - }) + logger.Printf("Imported %d Ideezes\n", counter) + } - log.Println("Listening on http://localhost:8080/ideas") - err = http.ListenAndServe(":8080", mux) + logger.Println("Listening on http://" + address) + err = http.ListenAndServe(address, mux) if err != nil { - log.Fatalln(err) + panic(err) } } diff --git a/server.go b/server.go new file mode 100644 index 0000000..6eb46b6 --- /dev/null +++ b/server.go @@ -0,0 +1,289 @@ +package main + +// Ideez instance code. + +import ( + _ "embed" + "encoding/gob" + "errors" + "html/template" + "io" + "log" + "net/http" + "os" + "os/signal" + "runtime/debug" + "strconv" + "time" +) + +const ( + SERVER_ID_BYTES = 8 +) + +// This is the Object that should be paired to the Ideas. +type Ideez struct { + Id string + Name string +} + +// Represents an idea +// LastUpdated is a formatted date string +// out of 5 rating of the idea +// formatted time string with DateLayout +type Idea struct { + Id int + Title string + Text string + Author string + LastUpdated string +} + +// Data passed to the ideas_html template +type IdeasPageData struct { + Name string + Ideas []Idea + Error string + RoutePrefix string +} + +var ( + //go:embed templates/ideas.html + ideas_html string + //go:embed templates/edit.html + idea_edit_html string + // layout for how the date should be output in html + DateLayout string = "02/01/2006 on 15:04" +) + +func GetVersion() string { + buildinfo, ok := debug.ReadBuildInfo() + if !ok { + panic("Could not read buildinfo to know package version.") + } + return buildinfo.Main.Version +} + +// This function intializes a server for use it +// 1. Imports the data file for its ID, or creates one if it does not exist +// 2. Creates a SIGINT handler to save the data +// 3. Creates the html templates +// 4. Registers the routes to mux passed +// Returns a new server so that the manager can add it to the list of servers +func NewIdeez(mux *http.ServeMux, name, id string) Ideez { + var logger = log.New(os.Stdout, "NewIdeez: ", log.Ldate) + logger.Printf("New server %s[%s]\n", name, id) + + // Ideas for this server + var Ideas []Idea + // version number used for debugging compatibility with the .data file + version := GetVersion() + // Prefix that should be prepended before each route + routePrefix := "/server/" + id + // File for persistent storage + dataFilename := DataDir + "/" + id + ".data" + // Counter that goes up each time a new idea is created + idCounter := 0 + + // If the .data file does not exist, create it in the cache directory + { + f, err := os.Open(dataFilename) + if errors.Is(err, os.ErrNotExist) { + // The file does not exist, so create one with only Version encoded + logger.Println("Datafile does not exist. Creating", dataFilename) + f, err = os.Create(dataFilename) + if err != nil { + logger.Fatalln("Error while creating data file:", err) + } + + enc := gob.NewEncoder(f) + if err := enc.Encode(version); err != nil { + logger.Fatalln("Error while encoding Version:", err) + } + } else if err != nil { + logger.Fatalln("Error while opening data file:", err) + } else { + dec := gob.NewDecoder(f) + + // Check the version + var v string + if err := dec.Decode(&v); err != io.EOF && err != nil { + logger.Fatalln("Error while decoding version:", err) + } + if v != version { + logger.Fatalf("Version mismatch for datafile@%s != package@%s\n", v, version) + } + + // Decode the data and import it into Ideas + if err := dec.Decode(&name); err != nil && err != io.EOF { + logger.Fatalln("Error while decoding name:", err) + } + if err := dec.Decode(&Ideas); err != nil && err != io.EOF { + logger.Fatalln("Error while decoding ideas:", err) + } + logger.Printf("Imported @%s: %d ideas\n", v, len(Ideas)) + if err := f.Close(); err != nil { + logger.Fatalln("Error while closing file:", err) + } + } + + } + + // Data was succesfully imported + logger.SetPrefix("Ideez(" + name + ") ") + + // Handle SIGINT + // Save all Ideas into the data file + // NOTE (Luca): This system might seem dumb, but it only adds overhead at startup + // and exit of the program which is fine. + go func() { + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + <-c + + f, err := os.Create(dataFilename) + if err != nil { + logger.Fatalln(err) + } + defer f.Close() + + enc := gob.NewEncoder(f) + if err := enc.Encode(version); err != nil { + logger.Fatalln(err) + } + if err := enc.Encode(name); err != nil { + logger.Fatalln(err) + } + if err := enc.Encode(Ideas); err != nil { + logger.Fatalln("Error while saving ideas:", err) + } + + logger.Println("data saved.") + os.Exit(0) + }() + + // Creating templates + tmpl, err := template.New("ideas").Parse(ideas_html) + if err != nil { + logger.Fatalln(err) + } + _, err = tmpl.New("edit").Parse(idea_edit_html) + if err != nil { + logger.Fatalln(err) + } + + // Register http handlers + { + mux.HandleFunc("GET "+routePrefix+"/{$}", func(w http.ResponseWriter, r *http.Request) { + tmpl.ExecuteTemplate(w, "ideas", IdeasPageData{name, Ideas, "", routePrefix}) + }) + + mux.HandleFunc("POST "+routePrefix+"/idea/create/", func(w http.ResponseWriter, r *http.Request) { + i := Idea{ + Title: r.FormValue("title"), + Author: r.FormValue("author"), + LastUpdated: time.Now().Format(DateLayout), + Text: r.FormValue("text"), + Id: idCounter, + } + if i.Title == "" || i.Author == "" || i.Text == "" { + tmpl.ExecuteTemplate(w, "ideas", IdeasPageData{name, Ideas, "All fields are required", routePrefix}) + return + } + + Ideas = append(Ideas, i) + idCounter++ + + logger.Printf("Added new idea: %#v\n", i) + http.Redirect(w, r, routePrefix+"/", http.StatusMovedPermanently) + }) + + // A page to edit the idea, this page should lead to POST /idea/edit for confirming the edit. If + // the user cancels they should be redirected to the start page. This is done in the html. + mux.HandleFunc("GET "+routePrefix+"/idea/edit/", func(w http.ResponseWriter, r *http.Request) { + id := r.URL.Query().Get("id") + if id == "" { + tmpl.ExecuteTemplate(w, "ideas", IdeasPageData{name, Ideas, "You must provide an Idea ID.", routePrefix}) + return + } + idInt, err := strconv.Atoi(id) + if err != nil { + tmpl.ExecuteTemplate(w, "ideas", IdeasPageData{name, Ideas, id + " is not a valid Idea ID.", routePrefix}) + return + } + + for _, i := range Ideas { + if i.Id == idInt { + tmpl.ExecuteTemplate(w, "edit", struct { + Idea Idea + RoutePrefix string + }{i, routePrefix}) + return + } + } + tmpl.ExecuteTemplate(w, "ideas", IdeasPageData{name, Ideas, "No idea with id '" + id + "'.", routePrefix}) + }) + + // Perform the edit action on the idea + mux.HandleFunc("POST "+routePrefix+"/idea/edit/", func(w http.ResponseWriter, r *http.Request) { + id := r.FormValue("id") + if id == "" { + tmpl.ExecuteTemplate(w, "ideas", IdeasPageData{name, Ideas, "You must provide an Idea ID.", routePrefix}) + return + } + + idInt, err := strconv.Atoi(id) + if err != nil { + tmpl.ExecuteTemplate(w, "ideas", IdeasPageData{name, Ideas, id + " is not a valid Idea ID.", routePrefix}) + return + } + + var i *Idea + for j := range Ideas { + if Ideas[j].Id == idInt { + i = &Ideas[j] + break + } + } + + if i == nil { + tmpl.ExecuteTemplate(w, "ideas", IdeasPageData{name, Ideas, "No idea with id '" + id + "'.", routePrefix}) + return + } + + i.Title = r.FormValue("title") + i.Text = r.FormValue("text") + i.LastUpdated = time.Now().Format(DateLayout) + " (edit)" + + logger.Printf("Edited %#v\n", i) + + http.Redirect(w, r, routePrefix+"/", http.StatusMovedPermanently) + }) + + mux.HandleFunc("POST "+routePrefix+"/idea/delete/", func(w http.ResponseWriter, r *http.Request) { + id := r.FormValue("id") + if id == "" { + tmpl.ExecuteTemplate(w, "ideas", IdeasPageData{name, Ideas, "You must provide an Idea ID.", routePrefix}) + return + } + + idInt, err := strconv.Atoi(id) + if err != nil { + tmpl.ExecuteTemplate(w, "ideas", IdeasPageData{name, Ideas, id + " is not a valid Idea ID.", routePrefix}) + return + } + + for i, v := range Ideas { + if idInt == v.Id { + Ideas = append(Ideas[:i], Ideas[i+1:]...) + logger.Printf("Deleted: %#v\n", v) + http.Redirect(w, r, routePrefix+"/", http.StatusMovedPermanently) + return + } + } + tmpl.ExecuteTemplate(w, "ideas", IdeasPageData{name, Ideas, "No idea with id '" + id + "'.", routePrefix}) + }) + } + + return Ideez{Name: name, Id: id} +} diff --git a/t_idea/edit.html b/templates/edit.html index 7af6b0b..fef6846 100644 --- a/t_idea/edit.html +++ b/templates/edit.html @@ -1,3 +1,4 @@ +{{ with .Idea}} <!DOCTYPE html> <html lang="en"> <head> @@ -31,15 +32,21 @@ input[name="title"] { <h3>Editing "{{.Title}}"</h3> <hr> <div class="idea"> - <form action="/idea/edit/" method="post"> + <form action="{{$.RoutePrefix}}/idea/edit/" method="post"> <input name="title" type="text" value="{{.Title}}"><br> <textarea name="text" rows=6 required>{{.Text}}</textarea> <p class="creation">by <span class="author">{{.Author}}</span> on <span class="date">{{.LastUpdated}}</span></p> + <input type="hidden" name="id" value="{{.Id}}"}}> <input type="submit" value="confirm"> </form> - <form action="/ideas/" method="get"> - <input type="submit" value="cancel"> - </form> + <button id="cancel">cancel</button> </div> </body> </html> +<script defer> + let cancelButton = document.getElementById("cancel") + cancelButton.onclick = function() { + window.location = "{{$.RoutePrefix}}" + } +</script> +{{end}} diff --git a/t_idea/index.html b/templates/ideas.html index 691f8be..e233000 100644 --- a/t_idea/index.html +++ b/templates/ideas.html @@ -3,20 +3,20 @@ <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <meta property="og:title" content="Ideez"/> - <meta property="og:description" content="Ideez is an app that you can storm with ideas, hoping to facilitate plans and brainstorms."/> + <meta property="og:title" content="{{.Name}} Ideez"/> + <meta property="og:description" content="Find all {{.Name}} ideez here!"/> <meta property="og:image" content="/static/favicon.ico" /> <link rel="stylesheet" href="/static/main.css"> <script src="/static/index.js" defer></script> <link rel="icon" type="image/x-icon" href="/static/favicon.ico"> - <title>Ideas</title> + <title>{{.Name}} Ideez</title> </head> <body> {{ with .Error }} <p class="error">{{.}}</p> {{ end}} - <h3>Add an idea:</h3> - <form action="/idea/create/" method="post"> + <h3>Add an idea to <span title="copy link to Ideez" id="link">{{.Name}}</span>:</h3> + <form action="{{.RoutePrefix}}/idea/create/" method="post"> <input name="title" type="text" placeholder="Title" required></br> <textarea name="text" cols=50 rows=4 placeholder="Write what is in your lightbulb here." required></textarea> </br> @@ -30,11 +30,11 @@ <h2 class="title">{{.Title}}</h2> <pre class="text">{{.Text}}</pre> <p class="creation">by <span class="author">{{.Author}}</span> on <span class="date">{{.LastUpdated}}</span></p> - <form action="/idea/delete/" method="post"> - <input type="hidden" name="title" value="{{.Title}}"> + <form action="{{$.RoutePrefix}}/idea/delete/" method="post"> + <input type="hidden" name="id" value="{{.Id}}"> <input type="submit" value="delete"> </form> - <button class="edit" data-title="{{.Title}}">edit</button> + <button class="edit" idea-id="{{.Id}}">edit</button> </div> {{ else }} <p><i>No ideas here... Be the first one to think!</i></p> diff --git a/templates/servers.html b/templates/servers.html new file mode 100644 index 0000000..f35f4e9 --- /dev/null +++ b/templates/servers.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta property="og:title" content="Ideez"/> + <meta property="og:description" content="Ideez is an app that you can storm with ideas, hoping to facilitate plans and brainstorms."/> + <meta property="og:image" content="/static/favicon.ico" /> + <link rel="stylesheet" href="/static/main.css"> + <link rel="icon" type="image/x-icon" href="/static/favicon.ico"> + <title>Servers</title> + </head> + <body> + {{ with .Error }} + <p class="error">{{.}}</p> + {{ end}} + <form method="post" action="/server/create/"> + <input name="name" type="text" placeholder="Ideez Server Name" required> + <button>Create Server</button> + </form> + <hr> + <ul> + {{range .Ideezes}} + <li><a href="{{$.Address}}/server/{{.ID}}/">{{.Name}}</a></li> + {{end}} + <ul> + </body> +</html> |