package main import ( _ "embed" "encoding/gob" "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 // layout for how the date should be output in html var DateLayout string = "02/01/2006 on 15:04" // template for /ideas // //go:embed ideas.html var ideas_html string var Ideas []Idea // ToDo's // - [ ] Add a post // - [ ] Remove a post // - [ ] work with funcmaps in templates // - [ ] Put a reaction on a post // - [x] Store ideas to a file (encoder/gob) // - [x] Change the date format printing // Represents an idea // CreatedAt 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 CreatedAt string Comment []Comment } // Represents a comment on an idea type Comment struct { Text string Author string CreatedAt string } // Data passed to the ideas_html template type PageData struct { Ideas []Idea 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") // TODO: remove for testing p = "" if p == "" { p = "." } DataFilename = p + "/" + DataFilename f, err := os.OpenFile(DataFilename, os.O_RDONLY|os.O_CREATE, 0666) if err != nil { log.Fatalln("Error while opening data file:", err) } // Decode the data and import it into Ideas dec := gob.NewDecoder(f) 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) } 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() enc := gob.NewEncoder(f) if err := enc.Encode(Version); err != nil { log.Fatalln(err) } if err := enc.Encode(Ideas); err != nil { log.Fatalln("Error while saving ideas:", err) } log.Println("data saved.") os.Exit(0) }() // Handling http tmpl, err := template.New("ideas_html").Parse(ideas_html) if err != nil { log.Fatalln(err) } // TODO (Luca): Make the app more interactive by using websockets instead, // such that adding, editing or commenting on an idea does not require to // refresh the page. // Another approach would be to use htmx? http.HandleFunc("/ideas/", func(w http.ResponseWriter, r *http.Request) { tmpl.Execute(w, PageData{Ideas, ""}) }) http.HandleFunc("/create/", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Redirect(w, r, "/ideas/", http.StatusMovedPermanently) return } i := Idea{ Title: r.FormValue("title"), Author: r.FormValue("author"), CreatedAt: time.Now().Format(DateLayout), Text: r.FormValue("text"), } for _, v := range Ideas { if i.Title == v.Title { tmpl.Execute(w, PageData{Ideas, "An idea with title '" + v.Title + "' already exists!"}) return } } Ideas = append(Ideas, i) log.Println("Added new idea:", i.Title) http.Redirect(w, r, "/ideas/", http.StatusMovedPermanently) }) http.HandleFunc("/edit/", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Redirect(w, r, "/ideas/", http.StatusMovedPermanently) return } http.Redirect(w, r, "/ideas/", http.StatusMovedPermanently) }) http.HandleFunc("/delete/", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Redirect(w, r, "/ideas/", http.StatusMovedPermanently) return } t := r.FormValue("title") 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 } } tmpl.Execute(w, PageData{Ideas, "No idea with name '" + t + "'."}) }) http.HandleFunc("/comment/", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Redirect(w, r, "/ideas/", http.StatusMovedPermanently) return } http.Redirect(w, r, "/ideas/", http.StatusMovedPermanently) }) log.Println("Listening on http://localhost:8080") err = http.ListenAndServe(":8080", nil) if err != nil { log.Fatalln(err) } }