2020-11-08 17:00:23 +08:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2020-11-10 17:17:11 +08:00
|
|
|
"errors"
|
2020-11-08 17:00:23 +08:00
|
|
|
"html/template"
|
|
|
|
"net/http"
|
|
|
|
|
|
|
|
"github.com/serverwentdown/datetime.link/data"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
)
|
|
|
|
|
2020-11-10 17:17:11 +08:00
|
|
|
// ErrNoTemplate is returned when a template was not found on the server
|
|
|
|
var ErrNoTemplate = errors.New("missing template")
|
|
|
|
|
2020-11-08 17:00:23 +08:00
|
|
|
// Datetime is the main application server
|
|
|
|
type Datetime struct {
|
|
|
|
*http.ServeMux
|
|
|
|
tmpl *template.Template
|
|
|
|
cities map[string]*data.City
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewDatetime creates an application instance. It assumes certain resources
|
2020-11-09 00:27:16 +08:00
|
|
|
// like templates and data exist
|
2020-11-08 17:00:23 +08:00
|
|
|
func NewDatetime() (*Datetime, error) {
|
|
|
|
// Data
|
2020-11-10 17:17:11 +08:00
|
|
|
tmpl, err := template.New("templates").Funcs(templateFuncs).ParseGlob("templates/*")
|
2020-11-08 17:00:23 +08:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
cities, err := data.ReadCities()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Mux
|
|
|
|
mux := http.NewServeMux()
|
|
|
|
app := &Datetime{mux, tmpl, cities}
|
|
|
|
|
|
|
|
// Routes
|
|
|
|
mux.Handle("/data/", http.FileServer(http.Dir(".")))
|
|
|
|
mux.Handle("/js/", http.FileServer(http.Dir("assets")))
|
|
|
|
mux.Handle("/css/", http.FileServer(http.Dir("assets")))
|
|
|
|
mux.Handle("/favicon.ico", http.FileServer(http.Dir("assets")))
|
2020-11-22 16:42:35 +08:00
|
|
|
mux.HandleFunc("/search", app.search)
|
2020-11-08 17:00:23 +08:00
|
|
|
mux.HandleFunc("/", app.index)
|
|
|
|
|
|
|
|
return app, nil
|
|
|
|
}
|
|
|
|
|
2020-11-10 17:17:11 +08:00
|
|
|
type appRequest struct {
|
|
|
|
App Datetime
|
|
|
|
Req Request
|
|
|
|
}
|
|
|
|
|
2020-11-22 16:42:35 +08:00
|
|
|
type appSearch struct {
|
2020-11-22 16:59:41 +08:00
|
|
|
App Datetime
|
|
|
|
Search []*data.City
|
2020-11-22 16:42:35 +08:00
|
|
|
}
|
|
|
|
|
2020-11-08 17:00:23 +08:00
|
|
|
// index handles all incoming page requests
|
|
|
|
func (app Datetime) index(w http.ResponseWriter, req *http.Request) {
|
|
|
|
var err error
|
|
|
|
|
|
|
|
if req.Method != http.MethodGet && req.Method != http.MethodHead {
|
|
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-11-10 17:17:11 +08:00
|
|
|
tmpl := app.loadTemplate("index", w, req)
|
2020-11-08 17:00:23 +08:00
|
|
|
if tmpl == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if req.Method == http.MethodHead {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-11-10 17:17:11 +08:00
|
|
|
request := Request{}
|
|
|
|
if req.URL.Path != "/" {
|
|
|
|
request, err = ParseRequest(req.URL)
|
2020-11-29 09:16:41 +08:00
|
|
|
if errors.Is(err, ErrComponentsMismatch) {
|
|
|
|
l.Debug("not matching components", zap.Error(err))
|
|
|
|
app.error(HTTPError{http.StatusNotFound, err}, w, req)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if errors.Is(err, ErrInvalidTime) {
|
|
|
|
l.Debug("not matching components", zap.Error(err))
|
|
|
|
app.error(HTTPError{http.StatusNotFound, err}, w, req)
|
|
|
|
return
|
|
|
|
}
|
2020-11-10 17:17:11 +08:00
|
|
|
if err != nil {
|
2020-11-29 09:16:41 +08:00
|
|
|
l.Info("parse failed", zap.Error(err))
|
2020-11-10 17:17:11 +08:00
|
|
|
app.error(HTTPError{http.StatusBadRequest, err}, w, req)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
l.Debug("rendering template", zap.Reflect("request", request))
|
|
|
|
err = tmpl.Execute(w, appRequest{app, request})
|
2020-11-08 17:00:23 +08:00
|
|
|
if err != nil {
|
|
|
|
l.Error("templating failed", zap.Error(err))
|
2020-11-10 17:17:11 +08:00
|
|
|
app.templateError(HTTPError{http.StatusInternalServerError, err}, w, req)
|
2020-11-08 17:00:23 +08:00
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
2020-11-22 16:42:35 +08:00
|
|
|
|
|
|
|
// search handles zone search queries
|
|
|
|
func (app Datetime) search(w http.ResponseWriter, req *http.Request) {
|
|
|
|
var err error
|
|
|
|
|
|
|
|
if req.Method != http.MethodGet && req.Method != http.MethodHead {
|
|
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
tmpl := app.loadTemplate("search", w, req)
|
|
|
|
if tmpl == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if req.Method == http.MethodHead {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: do search
|
2020-11-22 16:59:41 +08:00
|
|
|
query := req.URL.Query()
|
|
|
|
search, err := FullSearchCities(app.cities, query.Get("zone"))
|
|
|
|
if err != nil {
|
|
|
|
l.Error("search failed", zap.Error(err))
|
|
|
|
app.error(HTTPError{http.StatusInternalServerError, err}, w, req)
|
|
|
|
return
|
2020-11-22 16:42:35 +08:00
|
|
|
}
|
|
|
|
|
2020-11-29 09:16:41 +08:00
|
|
|
//l.Debug("rendering template", zap.Reflect("search", search))
|
2020-11-22 16:42:35 +08:00
|
|
|
err = tmpl.Execute(w, appSearch{app, search})
|
|
|
|
if err != nil {
|
|
|
|
l.Error("templating failed", zap.Error(err))
|
|
|
|
app.templateError(HTTPError{http.StatusInternalServerError, err}, w, req)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|