| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "html/template" |
| 5 | "net/http" |
| 6 | "strings" |
| 7 | "time" |
| 8 | ) |
| 9 | |
| 10 | func (a *App) indexHandler(w http.ResponseWriter, r *http.Request) { |
| 11 | wines, err := getCellarWines(a.DB) |
| 12 | if err != nil { |
| 13 | a.Log.Error("list wines", "err", err) |
| 14 | http.Error(w, "Server error", http.StatusInternalServerError) |
| 15 | return |
| 16 | } |
| 17 | out := make([]wineWithSVG, 0, len(wines)) |
| 18 | for _, wine := range wines { |
| 19 | svg := buildPentagonSVG(wine.Sweetness, wine.Acidity, wine.Tannin, wine.Alcohol, wine.Body, 80.0, false) |
| 20 | out = append(out, wineWithSVG{Wine: wine, PentagonSVG: template.HTML(svg)}) |
| 21 | } |
| 22 | a.renderPage(w, "index.html", indexPageData{Wines: out}) |
| 23 | } |
| 24 | |
| 25 | func (a *App) wineDetail(w http.ResponseWriter, r *http.Request) { |
| 26 | wine, err := getWineByShortID(a.DB, r.PathValue("short_id")) |
| 27 | if err != nil { |
| 28 | http.Error(w, "Server error", http.StatusInternalServerError) |
| 29 | return |
| 30 | } |
| 31 | if wine == nil { |
| 32 | http.Error(w, "Wine not found", http.StatusNotFound) |
| 33 | return |
| 34 | } |
| 35 | pentagon := buildPentagonSVG(wine.Sweetness, wine.Acidity, wine.Tannin, wine.Alcohol, wine.Body, 250.0, true) |
| 36 | bars := buildBarsSVG(wine.Clarity, wine.ColorIntensity, wine.AromaIntensity, wine.NoseComplexity, 250.0) |
| 37 | a.renderPage(w, "wine.html", wineDetailPageData{Wine: *wine, PentagonSVG: template.HTML(pentagon), BarsSVG: template.HTML(bars)}) |
| 38 | } |
| 39 | |
| 40 | func (a *App) wineImage(w http.ResponseWriter, r *http.Request) { |
| 41 | bytes, mime, err := getWineImage(a.DB, r.PathValue("short_id")) |
| 42 | if err != nil { |
| 43 | http.Error(w, "Server error", http.StatusInternalServerError) |
| 44 | return |
| 45 | } |
| 46 | if bytes == nil { |
| 47 | http.NotFound(w, r) |
| 48 | return |
| 49 | } |
| 50 | if mime == "" { |
| 51 | mime = "application/octet-stream" |
| 52 | } |
| 53 | w.Header().Set("Content-Type", mime) |
| 54 | _, _ = w.Write(bytes) |
| 55 | } |
| 56 | |
| 57 | func (a *App) wishlistHandler(w http.ResponseWriter, r *http.Request) { |
| 58 | wines, err := getWishlistWines(a.DB) |
| 59 | if err != nil { |
| 60 | http.Error(w, "Server error", http.StatusInternalServerError) |
| 61 | return |
| 62 | } |
| 63 | a.renderPage(w, "wishlist.html", wishlistPageData{Wines: wines, IsAdmin: a.Sessions.HasValid(r)}) |
| 64 | } |
| 65 | |
| 66 | func xmlEscape(s string) string { |
| 67 | r := strings.NewReplacer("&", "&", "<", "<", ">", ">", `"`, """, "'", "'") |
| 68 | return r.Replace(s) |
| 69 | } |
| 70 | |
| 71 | func toRFC2822(sqliteTS string) string { |
| 72 | if t, err := time.Parse("2006-01-02 15:04:05", sqliteTS); err == nil { |
| 73 | return t.UTC().Format(time.RFC1123Z) |
| 74 | } |
| 75 | return sqliteTS |
| 76 | } |
| 77 | |
| 78 | func (a *App) rssFeed(w http.ResponseWriter, r *http.Request) { |
| 79 | wines, err := getCellarWines(a.DB) |
| 80 | if err != nil { |
| 81 | http.Error(w, "Server error", http.StatusInternalServerError) |
| 82 | return |
| 83 | } |
| 84 | siteURL := a.SiteURL |
| 85 | |
| 86 | var items strings.Builder |
| 87 | for _, wine := range wines { |
| 88 | link := siteURL + "/wines/" + xmlEscape(wine.ShortID) |
| 89 | title := xmlEscape(wine.Name) |
| 90 | var parts []string |
| 91 | if wine.Origin != "" { |
| 92 | parts = append(parts, "Origin: "+wine.Origin) |
| 93 | } |
| 94 | if wine.Grape != "" { |
| 95 | parts = append(parts, "Grape: "+wine.Grape) |
| 96 | } |
| 97 | if wine.Notes != "" { |
| 98 | parts = append(parts, wine.Notes) |
| 99 | } |
| 100 | desc := xmlEscape(strings.Join(parts, " — ")) |
| 101 | pub := toRFC2822(wine.CreatedAt) |
| 102 | items.WriteString(" <item>\n <title>" + title + "</title>\n <link>" + link + "</link>\n <guid>" + link + "</guid>\n <description>" + desc + "</description>\n <pubDate>" + pub + "</pubDate>\n </item>\n") |
| 103 | } |
| 104 | |
| 105 | lastBuild := "" |
| 106 | if len(wines) > 0 { |
| 107 | lastBuild = toRFC2822(wines[0].CreatedAt) |
| 108 | } |
| 109 | |
| 110 | out := `<?xml version="1.0" encoding="UTF-8"?> |
| 111 | <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"> |
| 112 | <channel> |
| 113 | <title>` + xmlEscape(a.SiteTitle) + `</title> |
| 114 | <link>` + siteURL + `</link> |
| 115 | <description>` + xmlEscape(a.SiteDescription) + `</description> |
| 116 | <lastBuildDate>` + lastBuild + `</lastBuildDate> |
| 117 | <atom:link href="` + siteURL + `/feed.xml" rel="self" type="application/rss+xml"/> |
| 118 | ` + items.String() + ` </channel> |
| 119 | </rss>` |
| 120 | w.Header().Set("Content-Type", "application/rss+xml; charset=utf-8") |
| 121 | _, _ = w.Write([]byte(out)) |
| 122 | } |