Added a site picker #6
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
Stimulus.register("pagelist", PagelistController);
|
||||
|
||||
feather.replace();
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -13,6 +13,13 @@
|
|||
<header>
|
||||
<h1>{{ .Site.Title }}</h1>
|
||||
<p>{{ .Site.Tagline }}</p>
|
||||
{{ if .Site.NavItems }}
|
||||
<nav>
|
||||
{{ range .Site.NavItems }}
|
||||
{{ if .ShowInNav }}<a href="{{ url_abs .Slug }}">{{ .Title }}</a>{{ end }}
|
||||
{{ end }}
|
||||
</nav>
|
||||
{{ end }}
|
||||
</header>
|
||||
|
||||
<main>
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
}
|
||||
|
|
|
|||
36
package-lock.json
generated
36
package-lock.json
generated
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,25 @@
|
|||
<span class="visually-hidden">Publishing...</span>
|
||||
</div>
|
||||
-->
|
||||
|
||||
<div class="nav-item dropdown me-2">
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
{{ .site.Title }}
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
{{ range .allSites }}
|
||||
<li><a class="dropdown-item" href="/sites/{{.ID}}/posts">{{.Title}}</a></li>
|
||||
{{ end }}
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="/sites/new">New Site…</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="nav-item dropdown border-end me-3">
|
||||
{{ if .pubTarget }}
|
||||
<a href="{{.pubTarget.BaseURL}}" class="nav-link navbar-site-visit" target="_blank" title="Visit site">
|
||||
<i data-feather="external-link" width="18" height="18"></i>
|
||||
</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
{{ .user.Username }}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
</div>
|
||||
<div data-first-run-target="pages">
|
||||
<div class="text-center mb-4">
|
||||
<p>Enter the details of your blog, if you know them.<br>All fields are optional, and can be changed later.</p>
|
||||
<p>Enter the details of your blog if you know them.<br>All fields are optional and can be changed later.</p>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label for="siteName" class="form-label">Site Name</label>
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
</table>
|
||||
{{ else }}
|
||||
<div class="h4 m-3 text-center">
|
||||
<div class="position-absolute top-50 start-50 translate-middle">No pages yet.</div>
|
||||
<div class="position-absolute top-50 start-50 translate-middle">📄<br>No pages yet.</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</main>
|
||||
|
|
|
|||
29
views/sitesettings/new.html
Normal file
29
views/sitesettings/new.html
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<div class="mx-auto p-2" style="width: 400px; margin-block-start: 50px;" data-controller="first-run">
|
||||
<div class="text-center mb-4">
|
||||
<h1>New Site</h1>
|
||||
</div>
|
||||
<form action="/sites" method="post">
|
||||
<div class="text-center mb-4">
|
||||
<p>Enter the details of your blog if you know them.<br>All fields are optional and can be changed later.</p>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label for="siteName" class="form-label">Site Name</label>
|
||||
<input type="text" class="form-control" name="siteName" id="siteName">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="siteUrl" class="form-label">Site URL</label>
|
||||
<input type="text" class="form-control" name="siteUrl" id="siteUrl">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="netlifySiteId" class="form-label">Netlify Site ID</label>
|
||||
<input type="text" class="form-control" name="netlifySiteId" id="netlifySiteId">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="netlifyAPIToken" class="form-label">Netlify API Token</label>
|
||||
<input type="text" class="form-control" name="netlifyAPIToken" id="netlifyAPIToken">
|
||||
</div>
|
||||
<div class="mb-3 text-end">
|
||||
<input type="submit" class="btn btn-primary" value="Create Site">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -20,5 +20,9 @@
|
|||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ else }}
|
||||
<div class="h4 m-3 text-center">
|
||||
<div class="position-absolute top-50 start-50 translate-middle">🖼️<br>No uploads yet.</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</main>
|
||||
Loading…
Reference in a new issue