From cdb75d3fcbc9339b897f8c6ff4d69a577f017393 Mon Sep 17 00:00:00 2001 From: Leonardo Bishop Date: Tue, 8 Jul 2025 23:26:05 +0100 Subject: Rewrite in Go --- web/command/handler/create.go | 53 ++++++++++++++ web/command/handler/home.go | 17 +++++ web/command/handler/site.go | 27 +++++++ web/command/handler/upload.go | 49 +++++++++++++ web/command/html/create.go | 74 +++++++++++++++++++ web/command/html/home.go | 53 ++++++++++++++ web/command/html/site.go | 73 +++++++++++++++++++ web/command/html/skeleton.go | 57 +++++++++++++++ web/command/html/style.css | 165 ++++++++++++++++++++++++++++++++++++++++++ web/command/html/upload.go | 53 ++++++++++++++ 10 files changed, 621 insertions(+) create mode 100644 web/command/handler/create.go create mode 100644 web/command/handler/home.go create mode 100644 web/command/handler/site.go create mode 100644 web/command/handler/upload.go create mode 100644 web/command/html/create.go create mode 100644 web/command/html/home.go create mode 100644 web/command/html/site.go create mode 100644 web/command/html/skeleton.go create mode 100644 web/command/html/style.css create mode 100644 web/command/html/upload.go (limited to 'web/command') diff --git a/web/command/handler/create.go b/web/command/handler/create.go new file mode 100644 index 0000000..21c6b1a --- /dev/null +++ b/web/command/handler/create.go @@ -0,0 +1,53 @@ +package handler + +import ( + "fmt" + "net/http" + "path" + + "github.com/LMBishop/scrapbook/pkg/config" + "github.com/LMBishop/scrapbook/pkg/constants" + "github.com/LMBishop/scrapbook/pkg/index" + "github.com/LMBishop/scrapbook/pkg/site" + "github.com/LMBishop/scrapbook/web/command/html" + . "maragu.dev/gomponents" + ghttp "maragu.dev/gomponents/http" +) + +func GetCreate() func(http.ResponseWriter, *http.Request) { + return ghttp.Adapt(func(w http.ResponseWriter, r *http.Request) (Node, error) { + return html.CreatePage("", "", html.CreatePageForm{}), nil + }) +} + +func PostCreate(mainConfig *config.MainConfig, index *index.SiteIndex) func(http.ResponseWriter, *http.Request) { + return ghttp.Adapt(func(w http.ResponseWriter, r *http.Request) (Node, error) { + err := r.ParseForm() + if err != nil { + return html.CreatePage("", err.Error(), html.CreatePageForm{}), nil + } + + name := r.Form.Get("name") + host := r.Form.Get("host") + + formValues := html.CreatePageForm{Name: name, Host: host} + + if name == "" { + return html.CreatePage("", "A name must be specified", formValues), nil + } + + if host == "" { + return html.CreatePage("", "A host must be specified", formValues), nil + } + + site, err := site.CreateNewSite(name, path.Join(constants.SysDataDir, "sites"), host) + + if err != nil { + return html.CreatePage("", fmt.Errorf("Unexpected error: %w", err).Error(), formValues), nil + } + + index.AddSite(site) + + return html.CreatePage("Successfully created new site", "", formValues), nil + }) +} diff --git a/web/command/handler/home.go b/web/command/handler/home.go new file mode 100644 index 0000000..a46d5ed --- /dev/null +++ b/web/command/handler/home.go @@ -0,0 +1,17 @@ +package handler + +import ( + "net/http" + + "github.com/LMBishop/scrapbook/pkg/config" + "github.com/LMBishop/scrapbook/pkg/index" + "github.com/LMBishop/scrapbook/web/command/html" + . "maragu.dev/gomponents" + ghttp "maragu.dev/gomponents/http" +) + +func GetHome(mainConfig *config.MainConfig, index *index.SiteIndex) func(http.ResponseWriter, *http.Request) { + return ghttp.Adapt(func(w http.ResponseWriter, r *http.Request) (Node, error) { + return html.HomePage(index), nil + }) +} diff --git a/web/command/handler/site.go b/web/command/handler/site.go new file mode 100644 index 0000000..73ea1a0 --- /dev/null +++ b/web/command/handler/site.go @@ -0,0 +1,27 @@ +package handler + +import ( + "fmt" + "net/http" + + "github.com/LMBishop/scrapbook/pkg/config" + "github.com/LMBishop/scrapbook/pkg/index" + "github.com/LMBishop/scrapbook/web/command/html" + . "maragu.dev/gomponents" + ghttp "maragu.dev/gomponents/http" +) + +func GetSite(mainConfig *config.MainConfig, index *index.SiteIndex) func(http.ResponseWriter, *http.Request) { + return ghttp.Adapt(func(w http.ResponseWriter, r *http.Request) (Node, error) { + siteName := r.PathValue("site") + if siteName == "" { + return nil, fmt.Errorf("unknown site") + } + site := index.GetSite(siteName) + if site == nil { + return nil, fmt.Errorf("unknown site") + } + + return html.SitePage(mainConfig, site), nil + }) +} diff --git a/web/command/handler/upload.go b/web/command/handler/upload.go new file mode 100644 index 0000000..0e4928c --- /dev/null +++ b/web/command/handler/upload.go @@ -0,0 +1,49 @@ +package handler + +import ( + "fmt" + "net/http" + + "github.com/LMBishop/scrapbook/pkg/config" + "github.com/LMBishop/scrapbook/pkg/index" + "github.com/LMBishop/scrapbook/pkg/upload" + "github.com/LMBishop/scrapbook/web/command/html" + . "maragu.dev/gomponents" + ghttp "maragu.dev/gomponents/http" +) + +func GetUpload(index *index.SiteIndex) func(http.ResponseWriter, *http.Request) { + return ghttp.Adapt(func(w http.ResponseWriter, r *http.Request) (Node, error) { + siteName := r.PathValue("site") + if siteName == "" { + return nil, fmt.Errorf("unknown site") + } + site := index.GetSite(siteName) + if site == nil { + return nil, fmt.Errorf("unknown site") + } + + return html.UploadPage("", "", siteName), nil + }) +} + +func PostUpload(mainConfig *config.MainConfig, index *index.SiteIndex) func(http.ResponseWriter, *http.Request) { + return ghttp.Adapt(func(w http.ResponseWriter, r *http.Request) (Node, error) { + siteName := r.PathValue("site") + if siteName == "" { + return nil, fmt.Errorf("unknown site") + } + + reader, err := r.MultipartReader() + if err != nil { + return html.UploadPage("", fmt.Errorf("Unexpected error: could not read stream: %w", err).Error(), siteName), nil + } + + version, err := upload.HandleUpload(siteName, reader, index) + if err != nil { + return html.UploadPage("", fmt.Errorf("Unexpected error: %w", err).Error(), siteName), nil + } + + return html.UploadPage(fmt.Sprintf("Version %s created successfully", version), "", siteName), nil + }) +} diff --git a/web/command/html/create.go b/web/command/html/create.go new file mode 100644 index 0000000..a0b77d1 --- /dev/null +++ b/web/command/html/create.go @@ -0,0 +1,74 @@ +package html + +import ( + . "maragu.dev/gomponents" + . "maragu.dev/gomponents/html" +) + +type CreatePageForm struct { + Name string + Host string +} + +func CreatePage(success, err string, formValues CreatePageForm) Node { + return page("Create site", + H1(Text("Create site")), + + If(success != "", Group{ + alertSuccess(success), + Div( + Class("control-group group-right"), + navButton("OK", "/"), + ), + }), + + If(success == "", Group{ + If(err != "", alertError(err)), + + Form( + Action("/create"), + Method("post"), + + FieldSet( + Legend(Text("Site details")), + Label( + For("name"), + Text("Name"), + ), + Input( + ID("name"), + Name("name"), + Value(formValues.Name), + ), + Span( + Class("form-help"), + Text("The unique identifier for this site. This must be a valid directory name, and should be lower case with no spaces."), + ), + + Label( + For("host"), + Text("Host"), + ), + Input( + ID("host"), + Name("host"), + Value(formValues.Host), + ), + Span( + Class("form-help"), + Text("The fully qualified domain name for which this site is to be served on."), + ), + ), + + Div( + Class("control-group group-right"), + navButton("Go back", "/"), + Input( + Type("submit"), + Value("Submit"), + ), + ), + ), + }), + ) +} diff --git a/web/command/html/home.go b/web/command/html/home.go new file mode 100644 index 0000000..f0e783d --- /dev/null +++ b/web/command/html/home.go @@ -0,0 +1,53 @@ +package html + +import ( + "fmt" + + "github.com/LMBishop/scrapbook/pkg/index" + "github.com/LMBishop/scrapbook/pkg/site" + . "maragu.dev/gomponents" + . "maragu.dev/gomponents/html" +) + +func HomePage(siteIndex *index.SiteIndex) Node { + return page("All sites", + H1(Text("All sites")), + + Div( + Class("sites-table"), + Group{ + Span( + Class("header name"), + Text("Site"), + ), + Span( + Class("header status"), + Text("Status"), + ), + Span( + Class("header actions"), + Text("Actions"), + ), + }, + Map(siteIndex.GetSites(), func(site *site.Site) Node { + return Group{ + Span( + Class("name"), + Span(Text(site.Name)), + Span(Text(fmt.Sprintf("on %s", site.SiteConfig.Host))), + ), + Span( + Class("status"), + Text(site.EvaluateSiteStatus()), + ), + Span( + Class("actions"), + navButton("Details", fmt.Sprintf("/site/%s/", site.Name)), + ), + } + }), + ), + + navButton("Create new", "/create"), + ) +} diff --git a/web/command/html/site.go b/web/command/html/site.go new file mode 100644 index 0000000..b4239b1 --- /dev/null +++ b/web/command/html/site.go @@ -0,0 +1,73 @@ +package html + +import ( + "fmt" + + "github.com/LMBishop/scrapbook/pkg/config" + "github.com/LMBishop/scrapbook/pkg/site" + . "maragu.dev/gomponents" + . "maragu.dev/gomponents/html" +) + +func SitePage(mainConfig *config.MainConfig, site *site.Site) Node { + versions, err := site.GetAllVersions() + currentVersion, _ := site.GetCurrentVersion() + + return page("Site "+site.Name, + H1(Text("Site "+site.Name)), + + If(site.EvaluateSiteStatus() != "live", alertError(site.EvaluateSiteStatusReason())), + + FieldSet( + Legend(Text("Site actions")), + + Div( + Class("control-group"), + + navButton("Upload new version", "upload"), + navButton("Disable site", "disable"), + navButton("Delete site", "delete"), + ), + ), + + H2(Text("Version history")), + + If(len(versions) == 0, Span(Class("span"), alert("There are no versions to display", ""))), + If(err != nil, Span(Class("span"), alertError(fmt.Errorf("Cannot show site versions: %w", err).Error()))), + If(len(versions) > 0 && err == nil, Group{ + Div( + Class("versions-table"), + Group{ + Span( + Class("header date"), + Text("Date"), + ), + Span( + Class("header actions"), + Text("Actions"), + ), + }, + + Map(versions, func(version string) Node { + return Group{ + Span( + Class("date"), + Span(Text(version)), + If(currentVersion == version, Span(Class("current"), Text("current"))), + ), + Span( + Class("actions"), + If(currentVersion != version, navButton("Set current", fmt.Sprintf("/site/%s/", site.Name))), + navButton("Details", fmt.Sprintf("version/%s/", version)), + ), + } + }), + ), + }), + + H2(Text("API endpoints")), + P(Code(Text(fmt.Sprintf("http://%s/api/site/%s/upload", mainConfig.Command.Host, site.Name)))), + + navButton("Go back", "/"), + ) +} diff --git a/web/command/html/skeleton.go b/web/command/html/skeleton.go new file mode 100644 index 0000000..9e17475 --- /dev/null +++ b/web/command/html/skeleton.go @@ -0,0 +1,57 @@ +package html + +import ( + _ "embed" + + . "maragu.dev/gomponents" + . "maragu.dev/gomponents/components" + . "maragu.dev/gomponents/html" +) + +//go:embed style.css +var styles string + +func page(title string, children ...Node) Node { + return HTML5(HTML5Props{ + Title: title, + Language: "en", + Head: []Node{ + StyleEl(Raw(styles)), + }, + Body: []Node{ + Div(Class("container"), + Group(children), + footer(), + ), + }, + }) +} + +func footer() Node { + return Footer( + Hr(), + Text("scrapbook"), + ) +} + +func navButton(label string, dest string) Node { + return A( + Href(dest), + Text(label), + ) +} + +func alert(label string, class string) Node { + return Div( + Class("alert "+class), + Text(label), + ) +} + +func alertError(label string) Node { + return alert(label, "error") +} + +func alertSuccess(label string) Node { + return alert(label, "success") +} diff --git a/web/command/html/style.css b/web/command/html/style.css new file mode 100644 index 0000000..7e40ad4 --- /dev/null +++ b/web/command/html/style.css @@ -0,0 +1,165 @@ +html, body { + font-family: sans-serif; + +} +table { + border-collapse: collapse; +} + +th, td { + text-align: left; + padding: 8px; + border: black 1px solid; +} + +th { + background-color: #f2f2f2; +} + +a, input[type=submit] { + /* font-weight: bold; */ + border: 1px solid #000!important; + color: #000; + /* border: 2px solid #0074D9!important; + color: #0074D9; */ + padding: 3px 6px!important; + text-decoration: none; +} + +a :visited{ + color: #fff; +} + +form { + display: flex; + flex-direction: column; +} + +fieldset { + display: flex; + flex-direction: column; + gap: 0.5rem; + margin-bottom: 1rem;; +} + +legend { + background-color: #000; + color: #fff; + padding: 3px 6px; + font-style: italic; +} + +label { + font-weight: bold; +} + +footer { + margin-top: 2rem; + color: gray; + font-size: smaller; +} + +button, input[type=submit] { + background: none!important; + font-family: arial, sans-serif; + font-size: medium; + cursor: pointer; +} + +a:hover, button:hover, input[type=submit]:hover, button:active, a:active, input[type=submit]:active { + background: #000!important; + color: #fff!important; +} + +.form-help { + font-size: smaller; +} + +.container { + max-width: 800px; + margin: 0 auto; +} + +.control-group { + display: flex; + gap: 0.5rem; + flex-direction: row; +} + +.control-group.group-right { + justify-content: flex-end; +} + +.control-group a, .control-group button, .control-group input[type=submit] { + text-decoration: none; + width: initial; +} + +.sites-table { + width: 100%; + display: grid; + grid-template-columns: 50% 1fr 1fr; + align-items: center; + gap: 1rem; + margin-bottom: 1rem; +} + +.sites-table > .header, .versions-table > .header { + font-weight: bold; +} + +.sites-table > .name { + display: flex; + flex-direction: column; +} + +.sites-table > .name > span:nth-child(2) { + font-size: smaller; +} + +.sites-table > .actions, .versions-table > .actions { + justify-self: flex-end; +} + +.versions-table { + width: 100%; + display: grid; + grid-template-columns: 1fr 1fr; + align-items: center; + gap: 1rem; + margin-bottom: 1rem; +} + +.versions-table > .span { + grid-column: 1 / span 2; +} + +.versions-table > .date { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.versions-table > .date > .current { + color: #2ECC40; + font-size: smaller; + font-style: italic; +} + +.alert { + background-color: #f2f2f2; + padding: 0.5rem 1rem; + border: 1px solid #e7e7e7; + margin-bottom: 1rem; + font-style: italic; +} + +.alert.success { + background-color: #aaffaa; + border: 1px solid #90ee90; +} + +.alert.error { + background-color: #ffcccb; + border: 1px solid #ff6666; +} \ No newline at end of file diff --git a/web/command/html/upload.go b/web/command/html/upload.go new file mode 100644 index 0000000..1225d0f --- /dev/null +++ b/web/command/html/upload.go @@ -0,0 +1,53 @@ +package html + +import ( + "fmt" + + . "maragu.dev/gomponents" + . "maragu.dev/gomponents/html" +) + +func UploadPage(success, err string, siteName string) Node { + return page("Upload new version to "+siteName, + H1(Text("Upload new version to "+siteName)), + + If(success != "", Group{ + alertSuccess(success), + Div( + Class("control-group group-right"), + navButton("OK", fmt.Sprintf("/site/%s/", siteName)), + ), + }), + + If(success == "", Group{ + If(err != "", alertError(err)), + + Form( + Method("post"), + EncType("multipart/form-data"), + + FieldSet( + Legend(Text("Upload")), + Input( + ID("upload"), + Name("upload"), + Type("file"), + ), + Span( + Class("form-help"), + Text("A zip file with the contents of the site."), + ), + ), + + Div( + Class("control-group group-right"), + navButton("Go back", fmt.Sprintf("/site/%s/", siteName)), + Input( + Type("submit"), + Value("Submit"), + ), + ), + ), + }), + ) +} -- cgit v1.2.3-70-g09d2