aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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>