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... --> - +
+Enter the details of your blog, if you know them.
All fields are optional, and can be changed later.
Enter the details of your blog if you know them.
All fields are optional and can be changed later.