mirror of
https://github.com/mentos1386/zdravko.git
synced 2024-11-21 23:33:34 +00:00
feat: basic settings style and 404 page
This commit is contained in:
parent
7085f0e4d6
commit
650a2d8a4c
13 changed files with 257 additions and 19 deletions
|
@ -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))
|
||||
}
|
||||
|
|
4
go.mod
4
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
|
||||
|
|
7
go.sum
7
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=
|
||||
|
|
33
internal/handlers/404.go
Normal file
33
internal/handlers/404.go
Normal file
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
11
web/templates/components/base.go
Normal file
11
web/templates/components/base.go
Normal file
|
@ -0,0 +1,11 @@
|
|||
package components
|
||||
|
||||
type Page struct {
|
||||
Path string
|
||||
Title string
|
||||
}
|
||||
|
||||
type Base struct {
|
||||
Page *Page
|
||||
Pages []*Page
|
||||
}
|
|
@ -1,18 +1,31 @@
|
|||
{{define "base"}}
|
||||
{{ $title := "" }}
|
||||
{{ $path := "" }}
|
||||
{{ if ne nil .Page }}
|
||||
{{ $title = .Page.Title }}
|
||||
{{ $path = .Page.Path }}
|
||||
{{ end }}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Zdravko - {{template "title" .}}</title>
|
||||
<title>Zdravko - {{$title}}</title>
|
||||
<link rel="stylesheet" href="/static/css/tailwind.css">
|
||||
</head>
|
||||
<body class="bg-gray-100">
|
||||
<nav class="justify-center flex space-x-2 mt-10">
|
||||
<a href="/" class="btn btn-active">Status</a>
|
||||
<a href="/incidents" class="btn">Incidents</a>
|
||||
<a href="/settings" class="btn">Settings</a>
|
||||
{{range .Pages}}
|
||||
<a
|
||||
{{$active := eq .Path $path }}
|
||||
{{if $active}}aria-current="true"{{end}}
|
||||
href="{{.Path}}"
|
||||
class="btn {{if $active}}btn-active{{end}}">
|
||||
{{.Title}}
|
||||
</a>
|
||||
{{end}}
|
||||
</nav>
|
||||
<div class="container max-w-screen-md flex flex-col mt-20">
|
||||
{{template "main" .}}
|
||||
|
|
7
web/templates/pages/404.tmpl
Normal file
7
web/templates/pages/404.tmpl
Normal file
|
@ -0,0 +1,7 @@
|
|||
{{define "main"}}
|
||||
<div class="text-center">
|
||||
<h1 class="text-3xl mb-4 font-bold"><span class="text-red-600">Error 404:</span> Page was not found!</h1>
|
||||
<p>We didn't find the page you were looking for. Please check the URL and try again.</p>
|
||||
<p>Or you can go back to the <a class="underline text-blue-600" href="/">homepage</a>.</p>
|
||||
</div>
|
||||
{{end}}
|
|
@ -1,9 +1,24 @@
|
|||
{{define "title"}}Settings{{end}}
|
||||
|
||||
{{define "main"}}
|
||||
<h1>The settings!</h1>
|
||||
<p>You are logged in as {{.Email}}.</p>
|
||||
<p>Your id is {{.ID}}.</p>
|
||||
<p>Your access expieres at {{.OAuth2Expiry}}.</p>
|
||||
<a href="/oauth2/logout">Logout</a>
|
||||
<div class="grid grid-cols-[1fr_auto] gap-8">
|
||||
<ul class="sidebar">
|
||||
<li>
|
||||
<a aria-current="true" class="active" href="/settings">Overview</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/settings/healthchecks">Healthchecks</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/settings/workers">Workers</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/oauth2/logout">Logout</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="shadow-inner bg-gray-200 p-5 rounded-lg">
|
||||
<h1 class="text-xl mb-4">Hi {{.User.Email}}!</h1>
|
||||
<p>Your id is {{.User.ID}}.</p>
|
||||
<p>Your access expieres at {{.User.OAuth2Expiry}}.</p>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
|
Loading…
Reference in a new issue