239 lines
6.0 KiB
Go
239 lines
6.0 KiB
Go
package site
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"html/template"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/angelbeltran/templater"
|
|
|
|
"ruben/inventory2/internal/domains/accounts"
|
|
etsy_platform "ruben/inventory2/internal/domains/platforms/etsy"
|
|
"ruben/inventory2/internal/domains/raw_events"
|
|
)
|
|
|
|
func NewSiteHandler(
|
|
dir string,
|
|
rawEvents *raw_events.Store,
|
|
accts *accounts.Store,
|
|
etsy *etsy_platform.Platform,
|
|
) http.Handler {
|
|
mux := http.NewServeMux()
|
|
|
|
// api routes
|
|
|
|
mux.HandleFunc("POST /accounts", func(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
email := r.FormValue("email")
|
|
if email == "" {
|
|
http.Error(w, "no email provided", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
acct, err := accts.CreateAccount(ctx, email)
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("failed to create account: %w", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
http.Redirect(w, r, fmt.Sprintf("/site/accounts/%d", acct.ID), http.StatusSeeOther)
|
|
})
|
|
|
|
mux.HandleFunc("POST /log-in", func(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
email := r.FormValue("email")
|
|
if email == "" {
|
|
http.Error(w, "no email provided", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
acct, err := accts.GetAccountByEmail(ctx, email)
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("failed to create account: %w", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
http.Redirect(w, r, fmt.Sprintf("/site/accounts/%d", acct.ID), http.StatusSeeOther)
|
|
})
|
|
|
|
// non-html routes
|
|
|
|
scfs := http.FileServer(http.Dir(dir + "/scripts"))
|
|
mux.Handle("/scripts/", http.StripPrefix("/scripts", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/javascript")
|
|
if path.Ext(r.URL.Path) == ".gz" {
|
|
w.Header().Set("Content-Encoding", "gzip")
|
|
}
|
|
scfs.ServeHTTP(w, r)
|
|
})))
|
|
mux.Handle("/styles/", http.StripPrefix("/styles", http.FileServer(http.Dir(dir+"/styles"))))
|
|
|
|
// html page routes
|
|
|
|
tmplr := templater.NewTemplater(
|
|
dir+"/templates",
|
|
func() template.FuncMap {
|
|
return template.FuncMap{
|
|
"buildSitePath": func(parts ...any) string {
|
|
strParts := make([]string, len(parts))
|
|
for i, p := range parts {
|
|
strParts[i] = fmt.Sprint(p)
|
|
}
|
|
|
|
// TODO: make "/site" dynamic somehow
|
|
return path.Join(append([]string{"/site"}, strParts...)...)
|
|
},
|
|
"splitPath": func(p string) []string {
|
|
if p == "" {
|
|
return nil
|
|
}
|
|
return strings.Split(strings.TrimSuffix(strings.TrimPrefix(p, "/"), "/"), "/")
|
|
},
|
|
|
|
"prettyPrintJSON": func(j json.RawMessage) string {
|
|
b, err := json.MarshalIndent(j, " ", "")
|
|
if err != nil {
|
|
return string(j)
|
|
}
|
|
return string(b)
|
|
},
|
|
|
|
"parseInt64": func(s string) (int64, error) {
|
|
return strconv.ParseInt(s, 10, 64)
|
|
},
|
|
|
|
"addInt": func(a, b int) int {
|
|
return a + b
|
|
},
|
|
"subInt": func(a, b int) int {
|
|
return a - b
|
|
},
|
|
"multInt": func(a, b int) int {
|
|
return a * b
|
|
},
|
|
}
|
|
},
|
|
)
|
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
name, pathParams := getPageTemplateNameForURL(r.URL)
|
|
b, err := tmplr.ExecutePage(
|
|
name,
|
|
"Request",
|
|
r,
|
|
// add services here
|
|
"RawEvents",
|
|
rawEvents.WithContext(ctx),
|
|
"URLCalc",
|
|
newURLCalculator(r.URL),
|
|
"PathParams",
|
|
pathParams,
|
|
"Accounts",
|
|
accts.WithContext(ctx),
|
|
"Etsy",
|
|
etsy.WithContext(ctx),
|
|
)
|
|
if err != nil {
|
|
// TODO: handle 'not found' as a 404?
|
|
fmt.Println("[ERROR]: failed to load or parse layout template:", err)
|
|
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
|
return
|
|
}
|
|
|
|
w.Write(b)
|
|
})
|
|
|
|
return mux
|
|
}
|
|
|
|
// TODO: clean this up...
|
|
// TODO: somehow tell what the path params are and pass them up.
|
|
// - then consider pushing this functionality into the template library.
|
|
//
|
|
// getPageTemplateNameForURL eliminate any trailing .html or /, and checks for any
|
|
// file with path parameters in the name, eg '{abc}.html.tmpl', prefering exact filename matches.
|
|
func getPageTemplateNameForURL(u *url.URL) (name string, params map[string]string) {
|
|
fp := strings.TrimPrefix(strings.TrimSuffix(strings.TrimSuffix(u.Path, ".html"), "/"), "/")
|
|
if fp == "" {
|
|
// "/" maps to "/index"
|
|
fp = "index"
|
|
}
|
|
|
|
fpParts := strings.Split(fp, "/")
|
|
res := getMatchingGlobPatternsCapturingFilepathIncludingParametrizedFilepaths(fpParts)
|
|
for _, combs := range res {
|
|
const pageBodiesPrefix = "internal/site/templates/page_bodies"
|
|
pattern := path.Join(pageBodiesPrefix, path.Join(combs...)) + ".html.tmpl"
|
|
|
|
matches, _ := filepath.Glob(pattern)
|
|
if len(matches) == 0 {
|
|
pattern := path.Join(pageBodiesPrefix, path.Join(combs...), "index") + ".html.tmpl"
|
|
matches, _ = filepath.Glob(pattern)
|
|
}
|
|
if len(matches) > 0 {
|
|
match := matches[0]
|
|
name = strings.TrimPrefix(strings.TrimSuffix(match, ".html.tmpl"), pageBodiesPrefix+"/")
|
|
|
|
patternParts := strings.Split(name, "/")
|
|
params = make(map[string]string)
|
|
for i, pp := range patternParts {
|
|
if strings.HasPrefix(pp, "{") && strings.HasSuffix(pp, "}") {
|
|
params[pp[1:len(pp)-1]] = fpParts[i]
|
|
}
|
|
}
|
|
|
|
return name, params
|
|
}
|
|
}
|
|
|
|
return fp, nil
|
|
}
|
|
|
|
func getMatchingGlobPatternsCapturingFilepathIncludingParametrizedFilepaths(filepathParts []string) [][]string {
|
|
switch len(filepathParts) {
|
|
case 0:
|
|
return nil
|
|
case 1:
|
|
return [][]string{
|
|
[]string{filepathParts[0]},
|
|
[]string{"{*}"},
|
|
}
|
|
default:
|
|
tailCombs := getMatchingGlobPatternsCapturingFilepathIncludingParametrizedFilepaths(filepathParts[1:])
|
|
|
|
combs := make([][]string, 2*len(tailCombs))
|
|
for i, c := range tailCombs {
|
|
combs[i*2] = append([]string{filepathParts[0]}, c...)
|
|
combs[i*2+1] = append([]string{"{*}"}, c...)
|
|
}
|
|
|
|
return combs
|
|
}
|
|
}
|
|
|
|
type URLCalculator struct {
|
|
url *url.URL
|
|
}
|
|
|
|
func newURLCalculator(u *url.URL) URLCalculator {
|
|
cpy := *u
|
|
return URLCalculator{
|
|
url: &cpy,
|
|
}
|
|
}
|
|
|
|
func (c URLCalculator) SetQueryParam(k string, v any) string {
|
|
u := *c.url
|
|
q := u.Query()
|
|
q.Set(k, fmt.Sprint(v))
|
|
u.RawQuery = q.Encode()
|
|
|
|
return u.String()
|
|
}
|