aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRaymaekers Luca <raymaekers.luca@gmail.com>2024-10-09 16:54:52 +0200
committerRaymaekers Luca <raymaekers.luca@gmail.com>2024-10-12 12:01:51 +0200
commit2d498443031c1831925d9ba91f8795b7f994677f (patch)
tree5952a1ae36d41ce20a8257f34c7f9487131203db
parent1e0ae41f1ec06379a6602613612c085ad053c0fa (diff)
Split server code and http code
Create multiple instances of Ideez by running NewIdeez(), after creating they can be added to the Ideezes slice to manage them. NewIdeez() imports the data and registers the routes. main.go has the code for managing the instances and server.go has the code coupled to a single instance. For eg. main.go has the code which checks files with .data extension in data directory, and imports servers on startup. - Add import functionality - Add nginx configuration - Add style and copy animation - Check for https with "X-Forwarded-Proto" - Change assets and templates to reflect the new id change - Change port to 15118 - Add Id field to Idea - Do not check for duplicate titles anymore - Extract ideez_servers into const variable - Extract documentation in doc.go - Rename Server -> Ideez - Rename t_idea/ -> templates/ - Fix cancel not working with form by using javascript instead - Set logger prefix in different contexts
-rw-r--r--.gitignore2
-rw-r--r--assets/index.js28
-rw-r--r--assets/index.ts32
-rw-r--r--assets/main.css13
-rw-r--r--doc.go40
-rw-r--r--ideez_nginx.conf25
-rw-r--r--main.go308
-rw-r--r--server.go289
-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.html28
11 files changed, 549 insertions, 247 deletions
diff --git a/.gitignore b/.gitignore
index 17fc593..b142eb9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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;
diff --git a/doc.go b/doc.go
new file mode 100644
index 0000000..6d73b92
--- /dev/null
+++ b/doc.go
@@ -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;
+}
diff --git a/main.go b/main.go
index d4c2c15..ddc8728 100644
--- a/main.go
+++ b/main.go
@@ -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>