From d80aacc180a225da45cd54f02ecdaf4b7d85d128 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Tue, 24 Mar 2026 11:08:51 +1100 Subject: [PATCH 1/2] Added a site picker plus options to create new sites --- assets/css/main.scss | 9 +++++ assets/js/main.js | 5 ++- cmds/server.go | 18 ++++++---- handlers/middleware/errlog.go | 2 +- handlers/middleware/site.go | 12 ++++++- handlers/sitesettings.go | 24 +++++++++++-- package-lock.json | 36 ++++++++++++++++--- package.json | 3 +- services/sites/services.go | 66 ++++++++++++++++++++++++++++++----- views/_common/nav.html | 20 ++++++++++- views/index/first-run.html | 2 +- views/pages/index.html | 2 +- views/sitesettings/new.html | 29 +++++++++++++++ views/uploads/index.html | 4 +++ 14 files changed, 203 insertions(+), 29 deletions(-) create mode 100644 views/sitesettings/new.html diff --git a/assets/css/main.scss b/assets/css/main.scss index dc6ad7d..2e0883a 100644 --- a/assets/css/main.scss +++ b/assets/css/main.scss @@ -10,6 +10,15 @@ $container-max-widths: ( @import "bootstrap/scss/bootstrap.scss"; +// Navbar + +.navbar-site-visit { + display: inline-block; + line-height: 2em; + margin-bottom: 4px; + margin-right: 10px; +} + // Post list .postlist .post img { diff --git a/assets/js/main.js b/assets/js/main.js index 28451fb..fcbe286 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -1,3 +1,4 @@ +import feather from "feather-icons/dist/feather.js"; import { Application } from "@hotwired/stimulus"; import ToastController from "./controllers/toast"; @@ -17,4 +18,6 @@ Stimulus.register("logout", LogoutController); Stimulus.register("first-run", FirstRunController); Stimulus.register("upload", UploadController); Stimulus.register("show-upload", ShowUploadController); -Stimulus.register("pagelist", PagelistController); \ No newline at end of file +Stimulus.register("pagelist", PagelistController); + +feather.replace(); \ No newline at end of file diff --git a/cmds/server.go b/cmds/server.go index 89310bd..36e5923 100644 --- a/cmds/server.go +++ b/cmds/server.go @@ -119,7 +119,17 @@ Starting weiro without any arguments will start the server. app.Post("/login", lh.DoLogin) app.Post("/logout", lh.Logout) - siteGroup := app.Group("/sites/:siteID", middleware.LogErrors(), middleware.RequireUser(svcs.Auth), middleware.RequiresSite(svcs.Sites)) + app.Get("/", middleware.OptionalUser(svcs.Auth), ih.Index) + app.Get("/first-run", ih.FirstRun) + app.Post("/first-run", ih.FirstRunSubmit) + + app.Get("/static/*", static.New("./static")) + + app.Use(middleware.LogErrors(), middleware.RequireUser(svcs.Auth)) + + app.Get("/sites/new", ssh.New) + app.Post("/sites", ssh.Create) + siteGroup := app.Group("/sites/:siteID", middleware.RequiresSite(svcs.Sites)) siteGroup.Get("/posts", ph.Index) siteGroup.Get("/posts/new", ph.New) @@ -158,12 +168,6 @@ Starting weiro without any arguments will start the server. siteGroup.Post("/pages/:pageID", pgh.Update) siteGroup.Post("/pages/:pageID/delete", pgh.Delete) - app.Get("/", middleware.OptionalUser(svcs.Auth), ih.Index) - app.Get("/first-run", ih.FirstRun) - app.Post("/first-run", ih.FirstRunSubmit) - - app.Get("/static/*", static.New("./static")) - if err := app.Listen(":3000"); err != nil { log.Println(err) } diff --git a/handlers/middleware/errlog.go b/handlers/middleware/errlog.go index 5b6dfa6..2acac04 100644 --- a/handlers/middleware/errlog.go +++ b/handlers/middleware/errlog.go @@ -9,7 +9,7 @@ import ( func LogErrors() func(c fiber.Ctx) error { return func(c fiber.Ctx) error { if err := c.Next(); err != nil { - log.Printf("error: %v\n", err) + log.Printf("%v: error: %v\n", c.Path(), err) return err } return nil diff --git a/handlers/middleware/site.go b/handlers/middleware/site.go index 54211bc..6f47430 100644 --- a/handlers/middleware/site.go +++ b/handlers/middleware/site.go @@ -32,9 +32,19 @@ func RequiresSite(sites *sites.Service) func(c fiber.Ctx) error { return err } } - c.Locals("site", site) c.SetContext(models.WithSite(c.Context(), site)) + + sitesOwnedByUser, err := sites.ListSites(c.Context()) + if err != nil { + return err + } + c.Locals("allSites", sitesOwnedByUser) + + if pubTargets, err := sites.BestPubTarget(c.Context(), site); err == nil { + c.Locals("pubTarget", pubTargets) + } + return c.Next() } } diff --git a/handlers/sitesettings.go b/handlers/sitesettings.go index 0fe2100..e61ced4 100644 --- a/handlers/sitesettings.go +++ b/handlers/sitesettings.go @@ -12,10 +12,28 @@ type SiteSettingsHandler struct { SiteService *sites.Service } -func (s *SiteSettingsHandler) General(ctx fiber.Ctx) error { - site := ctx.Locals("site").(models.Site) +func (s *SiteSettingsHandler) New(c fiber.Ctx) error { + return c.Render("sitesettings/new", fiber.Map{}, "layouts/bare_with_scripts") +} - return ctx.Render("sitesettings/general", fiber.Map{ +func (s *SiteSettingsHandler) Create(c fiber.Ctx) error { + var params sites.CreateSiteParams + if err := c.Bind().Body(¶ms); err != nil { + return err + } + + newSite, err := s.SiteService.CreateSite(c.Context(), params) + if err != nil { + return err + } + + return c.Redirect().To(fmt.Sprintf("/sites/%v/posts", newSite.ID)) +} + +func (s *SiteSettingsHandler) General(c fiber.Ctx) error { + site := c.Locals("site").(models.Site) + + return c.Render("sitesettings/general", fiber.Map{ "site": site, "tzones": sites.ListZones(), }) diff --git a/package-lock.json b/package-lock.json index c4f391c..2068fd3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,8 @@ "dependencies": { "@hotwired/stimulus": "^3.2.2", "bootstrap": "^5.3.8", - "esbuild-sass-plugin": "^3.6.0" + "esbuild-sass-plugin": "^3.6.0", + "feather-icons": "^4.29.2" }, "devDependencies": { "esbuild": "0.27.3" @@ -783,6 +784,12 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, "node_modules/colorjs.io": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz", @@ -790,6 +797,17 @@ "license": "MIT", "peer": true }, + "node_modules/core-js": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz", + "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -855,6 +873,16 @@ "sass-embedded": "^1.97.2" } }, + "node_modules/feather-icons": { + "version": "4.29.2", + "resolved": "https://registry.npmjs.org/feather-icons/-/feather-icons-4.29.2.tgz", + "integrity": "sha512-0TaCFTnBTVCz6U+baY2UJNKne5ifGh7sMG4ZC2LoBWCZdIyPa+y6UiR4lEYGws1JOFWdee8KAsAIvu0VcXqiqA==", + "license": "MIT", + "dependencies": { + "classnames": "^2.2.5", + "core-js": "^3.1.3" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -887,9 +915,9 @@ } }, "node_modules/immutable": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", - "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz", + "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==", "license": "MIT" }, "node_modules/is-core-module": { diff --git a/package.json b/package.json index 64e6fca..5a786ac 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "dependencies": { "@hotwired/stimulus": "^3.2.2", "bootstrap": "^5.3.8", - "esbuild-sass-plugin": "^3.6.0" + "esbuild-sass-plugin": "^3.6.0", + "feather-icons": "^4.29.2" } } diff --git a/services/sites/services.go b/services/sites/services.go index 86e34b2..4585d03 100644 --- a/services/sites/services.go +++ b/services/sites/services.go @@ -9,6 +9,7 @@ import ( "github.com/gofiber/fiber/v3" "lmika.dev/lmika/weiro/models" "lmika.dev/lmika/weiro/providers/db" + "lmika.dev/pkg/modash/moslice" ) type Service struct { @@ -25,6 +26,22 @@ func (s *Service) HasUsersAsSites(ctx context.Context) (bool, error) { return s.db.HasUsersAndSites(ctx) } +func (s *Service) ListSites(ctx context.Context) ([]models.Site, error) { + user, ok := models.GetUser(ctx) + if !ok { + return nil, models.UserRequiredError + } + + sites, err := s.db.SelectSitesOwnedByUser(ctx, user.ID) + if err != nil { + return nil, err + } else if len(sites) == 0 { + return nil, errors.New("no sites found") + } + + return sites, nil +} + func (s *Service) BestSite(ctx context.Context, user models.User) (models.Site, error) { sites, err := s.db.SelectSitesOwnedByUser(ctx, user.ID) if err != nil { @@ -36,16 +53,20 @@ func (s *Service) BestSite(ctx context.Context, user models.User) (models.Site, return sites[0], nil } -type FirstRunRequest struct { - Username string `form:"username"` - Password1 string `form:"password1"` - Password2 string `form:"password2"` +type CreateSiteParams struct { SiteName string `form:"siteName"` SiteURL string `form:"siteUrl"` NetlifySiteID string `form:"netlifySiteId"` NetlifyAPIKey string `form:"netlifyAPIToken"` } +type FirstRunRequest struct { + CreateSiteParams + Username string `form:"username"` + Password1 string `form:"password1"` + Password2 string `form:"password2"` +} + func (frr FirstRunRequest) Validate() error { return validation.ValidateStruct(&frr, validation.Field(&frr.Username, validation.Required, validation.Match(models.ValidUserName)), @@ -76,16 +97,31 @@ func (s *Service) FirstRun(ctx context.Context, req FirstRunRequest) (newUser mo return newUser, newSite, err } + ctx = models.WithUser(ctx, newUser) + newSite, err = s.CreateSite(ctx, req.CreateSiteParams) + if err != nil { + return newUser, newSite, err + } + + return newUser, newSite, nil +} + +func (s *Service) CreateSite(ctx context.Context, req CreateSiteParams) (newSite models.Site, _ error) { + user, ok := models.GetUser(ctx) + if !ok { + return newSite, models.UserRequiredError + } + newSite = models.Site{ Title: defaultIfEmpty(req.SiteName, "New Site"), GUID: models.NewNanoID(), - OwnerID: newUser.ID, + OwnerID: user.ID, Timezone: "UTC", PostsPerPage: 10, Created: time.Now(), } if err := s.db.SaveSite(ctx, &newSite); err != nil { - return newUser, newSite, err + return newSite, err } hasNetlifyConfig := req.SiteURL != "" && req.NetlifySiteID != "" && req.NetlifyAPIKey != "" @@ -100,11 +136,11 @@ func (s *Service) FirstRun(ctx context.Context, req FirstRunRequest) (newUser mo TargetKey: req.NetlifyAPIKey, } if err := s.db.SavePublishTarget(ctx, &target); err != nil { - return newUser, newSite, err + return newSite, err } } - return newUser, newSite, nil + return newSite, nil } func (s *Service) GetSiteByID(ctx context.Context, siteID int64) (models.Site, error) { @@ -166,3 +202,17 @@ func (s *Service) UpdateSiteSettings(ctx context.Context, params UpdateSiteSetti return site, nil } + +func (s *Service) BestPubTarget(ctx context.Context, site models.Site) (models.SitePublishTarget, error) { + pubTargets, err := s.db.SelectPublishTargetsOfSite(ctx, site.ID) + if err != nil { + return models.SitePublishTarget{}, err + } + + enabledPubTargets := moslice.Filter(pubTargets, func(pubTarget models.SitePublishTarget) bool { return pubTarget.Enabled }) + if len(enabledPubTargets) == 0 { + return models.SitePublishTarget{}, errors.New("no publish targets found") + } + + return enabledPubTargets[0], nil +} diff --git a/views/_common/nav.html b/views/_common/nav.html index e9c0de7..5005326 100644 --- a/views/_common/nav.html +++ b/views/_common/nav.html @@ -29,7 +29,25 @@ Publishing... --> - + +
+ {{ end }}
diff --git a/models/pubmodel/sites.go b/models/pubmodel/sites.go index 38ba614..9f25b2f 100644 --- a/models/pubmodel/sites.go +++ b/models/pubmodel/sites.go @@ -6,6 +6,7 @@ import ( "iter" "lmika.dev/lmika/weiro/models" + "lmika.dev/pkg/modash/moslice" ) type Site struct { @@ -20,3 +21,7 @@ type Site struct { CategoriesOfPost func(ctx context.Context, postID int64) ([]*models.Category, error) Pages []*models.Page } + +func (s Site) NavItems() []*models.Page { + return moslice.Filter(s.Pages, func(p *models.Page) bool { return p.ShowInNav }) +} -- 2.43.0