diff --git a/cmd/server/main.go b/cmd/server/main.go index b7b17e4..d0b8716 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -53,6 +53,9 @@ func main() { r.HandleFunc("/oauth2/callback", h.OAuth2CallbackGET).Methods("GET") r.HandleFunc("/oauth2/logout", h.Authenticated(h.OAuth2LogoutGET)).Methods("GET") + // 404 + r.PathPrefix("/").HandlerFunc(h.Error404).Methods("GET") + log.Println("Server started on", config.PORT) log.Fatal(http.ListenAndServe(":"+config.PORT, r)) } diff --git a/go.mod b/go.mod index cd3c0b4..e682753 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,9 @@ go 1.21.6 require ( github.com/glebarez/sqlite v1.10.0 github.com/gorilla/mux v1.8.1 + github.com/gorilla/sessions v1.2.2 go.temporal.io/sdk v1.25.1 + golang.org/x/oauth2 v0.17.0 gorm.io/gen v0.3.25 gorm.io/gorm v1.25.7 gorm.io/plugin/dbresolver v1.5.0 @@ -24,7 +26,6 @@ require ( github.com/golang/protobuf v1.5.3 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/securecookie v1.1.2 // indirect - github.com/gorilla/sessions v1.2.2 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect @@ -41,7 +42,6 @@ require ( go.uber.org/atomic v1.9.0 // indirect golang.org/x/mod v0.15.0 // indirect golang.org/x/net v0.21.0 // indirect - golang.org/x/oauth2 v0.17.0 // indirect golang.org/x/sys v0.17.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.3.0 // indirect diff --git a/go.sum b/go.sum index eed271b..9aa166e 100644 --- a/go.sum +++ b/go.sum @@ -918,6 +918,8 @@ github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8 github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -1132,9 +1134,8 @@ golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= -golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1256,8 +1257,6 @@ golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= diff --git a/internal/handlers/404.go b/internal/handlers/404.go new file mode 100644 index 0000000..e7b492b --- /dev/null +++ b/internal/handlers/404.go @@ -0,0 +1,33 @@ +package handlers + +import ( + "fmt" + "net/http" + "text/template" + + "code.tjo.space/mentos1386/zdravko/web/templates" + "code.tjo.space/mentos1386/zdravko/web/templates/components" +) + +func (h *BaseHandler) Error404(w http.ResponseWriter, r *http.Request) { + ts, err := template.ParseFS(templates.Templates, + "components/base.tmpl", + "pages/404.tmpl", + ) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNotFound) + + err = ts.ExecuteTemplate(w, "base", &components.Base{ + Page: nil, + Pages: Pages, + }) + if err != nil { + fmt.Println("Error", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index e6a38ff..c673ecf 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -3,10 +3,26 @@ package handlers import ( "code.tjo.space/mentos1386/zdravko/internal" "code.tjo.space/mentos1386/zdravko/internal/models/query" + "code.tjo.space/mentos1386/zdravko/web/templates/components" "github.com/gorilla/sessions" "gorm.io/gorm" ) +var Pages = []*components.Page{ + {Path: "/", Title: "Status"}, + {Path: "/incidents", Title: "Incidents"}, + {Path: "/settings", Title: "Settings"}, +} + +func GetPageByTitle(title string) *components.Page { + for _, p := range Pages { + if p.Title == title { + return p + } + } + return nil +} + type BaseHandler struct { db *gorm.DB query *query.Query diff --git a/internal/handlers/index.go b/internal/handlers/index.go index 1501325..0dd8312 100644 --- a/internal/handlers/index.go +++ b/internal/handlers/index.go @@ -5,6 +5,7 @@ import ( "text/template" "code.tjo.space/mentos1386/zdravko/web/templates" + "code.tjo.space/mentos1386/zdravko/web/templates/components" ) func (h *BaseHandler) Index(w http.ResponseWriter, r *http.Request) { @@ -17,7 +18,10 @@ func (h *BaseHandler) Index(w http.ResponseWriter, r *http.Request) { return } - err = ts.ExecuteTemplate(w, "base", nil) + err = ts.ExecuteTemplate(w, "base", &components.Base{ + Page: GetPageByTitle("Status"), + Pages: Pages, + }) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } diff --git a/internal/handlers/settings.go b/internal/handlers/settings.go index ef8955f..5eb9c7d 100644 --- a/internal/handlers/settings.go +++ b/internal/handlers/settings.go @@ -5,8 +5,14 @@ import ( "text/template" "code.tjo.space/mentos1386/zdravko/web/templates" + "code.tjo.space/mentos1386/zdravko/web/templates/components" ) +type Settings struct { + *components.Base + User *AuthenticatedUser +} + func (h *BaseHandler) Settings(w http.ResponseWriter, r *http.Request, user *AuthenticatedUser) { ts, err := template.ParseFS(templates.Templates, "components/base.tmpl", @@ -17,7 +23,13 @@ func (h *BaseHandler) Settings(w http.ResponseWriter, r *http.Request, user *Aut return } - err = ts.ExecuteTemplate(w, "base", user) + err = ts.ExecuteTemplate(w, "base", &Settings{ + Base: &components.Base{ + Page: GetPageByTitle("Settings"), + Pages: Pages, + }, + User: user, + }) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } diff --git a/web/static/css/main.css b/web/static/css/main.css index 2a60876..9c0cae0 100644 --- a/web/static/css/main.css +++ b/web/static/css/main.css @@ -24,3 +24,23 @@ .btn-active:hover { @apply shadow; } + +.sidebar { + @apply w-48 h-fit text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg; +} + +.sidebar li:first-child a { + @apply rounded-t-lg; +} + +.sidebar li:last-child a { + @apply rounded-b-lg; +} + +.sidebar a { + @apply w-full block px-4 py-2 border-b border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-700 focus:text-blue-700; +} + +.sidebar a.active { + @apply bg-blue-700 text-white; +} diff --git a/web/static/css/tailwind.css b/web/static/css/tailwind.css index f4930fd..e1fc7f3 100644 --- a/web/static/css/tailwind.css +++ b/web/static/css/tailwind.css @@ -599,6 +599,10 @@ video { margin-right: auto; } +.mb-4 { + margin-bottom: 1rem; +} + .mr-1 { margin-right: 0.25rem; } @@ -659,6 +663,10 @@ video { grid-template-columns: repeat(2, minmax(0, 1fr)); } +.grid-cols-\[1fr_auto\] { + grid-template-columns: 1fr auto; +} + .flex-col { flex-direction: column; } @@ -683,6 +691,10 @@ video { gap: 0.5rem; } +.gap-8 { + gap: 2rem; +} + .gap-px { gap: 1px; } @@ -776,26 +788,54 @@ video { text-align: center; } +.text-3xl { + font-size: 1.875rem; + line-height: 2.25rem; +} + .text-sm { font-size: 0.875rem; line-height: 1.25rem; } +.text-xl { + font-size: 1.25rem; + line-height: 1.75rem; +} + .text-xs { font-size: 0.75rem; line-height: 1rem; } +.font-bold { + font-weight: 700; +} + +.text-blue-600 { + --tw-text-opacity: 1; + color: rgb(37 99 235 / var(--tw-text-opacity)); +} + .text-gray-500 { --tw-text-opacity: 1; color: rgb(107 114 128 / var(--tw-text-opacity)); } +.text-red-600 { + --tw-text-opacity: 1; + color: rgb(220 38 38 / var(--tw-text-opacity)); +} + .text-slate-500 { --tw-text-opacity: 1; color: rgb(100 116 139 / var(--tw-text-opacity)); } +.underline { + text-decoration-line: underline; +} + .shadow-inner { --tw-shadow: inset 0 2px 4px 0 rgb(0 0 0 / 0.05); --tw-shadow-colored: inset 0 2px 4px 0 var(--tw-shadow-color); @@ -835,6 +875,71 @@ video { box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); } +.sidebar { + height: -moz-fit-content; + height: fit-content; + width: 12rem; + border-radius: 0.5rem; + border-width: 1px; + --tw-border-opacity: 1; + border-color: rgb(229 231 235 / var(--tw-border-opacity)); + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity)); + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 500; + --tw-text-opacity: 1; + color: rgb(17 24 39 / var(--tw-text-opacity)); +} + +.sidebar li:first-child a { + border-top-left-radius: 0.5rem; + border-top-right-radius: 0.5rem; +} + +.sidebar li:last-child a { + border-bottom-right-radius: 0.5rem; + border-bottom-left-radius: 0.5rem; +} + +.sidebar a { + display: block; + width: 100%; + border-bottom-width: 1px; + --tw-border-opacity: 1; + border-color: rgb(229 231 235 / var(--tw-border-opacity)); + padding-left: 1rem; + padding-right: 1rem; + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} + +.sidebar a:hover { + --tw-bg-opacity: 1; + background-color: rgb(243 244 246 / var(--tw-bg-opacity)); + --tw-text-opacity: 1; + color: rgb(29 78 216 / var(--tw-text-opacity)); +} + +.sidebar a:focus { + --tw-text-opacity: 1; + color: rgb(29 78 216 / var(--tw-text-opacity)); + outline: 2px solid transparent; + outline-offset: 2px; + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); + --tw-ring-opacity: 1; + --tw-ring-color: rgb(29 78 216 / var(--tw-ring-opacity)); +} + +.sidebar a.active { + --tw-bg-opacity: 1; + background-color: rgb(29 78 216 / var(--tw-bg-opacity)); + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} + .hover\:bg-green-700:hover { --tw-bg-opacity: 1; background-color: rgb(21 128 61 / var(--tw-bg-opacity)); diff --git a/web/templates/components/base.go b/web/templates/components/base.go new file mode 100644 index 0000000..02f4fb9 --- /dev/null +++ b/web/templates/components/base.go @@ -0,0 +1,11 @@ +package components + +type Page struct { + Path string + Title string +} + +type Base struct { + Page *Page + Pages []*Page +} diff --git a/web/templates/components/base.tmpl b/web/templates/components/base.tmpl index 91e9084..5658616 100644 --- a/web/templates/components/base.tmpl +++ b/web/templates/components/base.tmpl @@ -1,18 +1,31 @@ {{define "base"}} +{{ $title := "" }} +{{ $path := "" }} +{{ if ne nil .Page }} + {{ $title = .Page.Title }} + {{ $path = .Page.Path }} +{{ end }} +
-We didn't find the page you were looking for. Please check the URL and try again.
+Or you can go back to the homepage.
+You are logged in as {{.Email}}.
-Your id is {{.ID}}.
-Your access expieres at {{.OAuth2Expiry}}.
- Logout +Your id is {{.User.ID}}.
+Your access expieres at {{.User.OAuth2Expiry}}.
+