Compare commits

..

No commits in common. "d9aec4af2c98abf0fc0f96f50b4b5f24f0ed2fb2" and "740cf8979ac025aa94f0daf7ee8ddf7184e405f2" have entirely different histories.

22 changed files with 112 additions and 251 deletions

View file

@ -10,7 +10,21 @@ $container-max-widths: (
@import "bootstrap/scss/bootstrap.scss"; @import "bootstrap/scss/bootstrap.scss";
// Post list // Local classes
.post-form {
display: grid;
grid-template-rows: min-content auto min-content;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.post-form textarea {
height: 100%;
}
.postlist .post img { .postlist .post img {
max-width: 300px; max-width: 300px;
@ -18,49 +32,6 @@ $container-max-widths: (
max-height: 300px; max-height: 300px;
} }
.postlist .post-date {
font-size: 0.9rem;
}
// Post form
// Post edit page styling
.post-edit-page {
height: 100vh;
}
.post-edit-page main {
display: flex;
flex-direction: column;
overflow: hidden;
}
.post-edit-page .post-form {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.post-edit-page .post-form .row {
flex: 1;
display: flex;
min-height: 0;
}
.post-edit-page .post-form .col-md-9 {
display: flex;
flex-direction: column;
}
.post-edit-page .post-form textarea {
flex: 1;
resize: vertical;
min-height: 300px;
}
.show-upload figure img { .show-upload figure img {
max-width: 100vw; max-width: 100vw;
height: auto; height: auto;

View file

@ -53,7 +53,6 @@ func (ph PostsHandler) New(c fiber.Ctx) error {
"post": p, "post": p,
"categories": cats, "categories": cats,
"selectedCategories": map[int64]bool{}, "selectedCategories": map[int64]bool{},
"bodyClass": "post-edit-page",
}) })
} }
@ -94,7 +93,6 @@ func (ph PostsHandler) Edit(c fiber.Ctx) error {
"post": post, "post": post,
"categories": cats, "categories": cats,
"selectedCategories": selectedCategories, "selectedCategories": selectedCategories,
"bodyClass": "post-edit-page",
}) })
})) }))
} }

View file

@ -0,0 +1,9 @@
<h2>Categories</h2>
<ul>
{{ range .Categories }}
<li>
<a href="{{ url_abs .Path }}">{{ .Name }}</a> ({{ .PostCount }})
{{ if .DescriptionBrief }}<br><small>{{ .DescriptionBrief }}</small>{{ end }}
</li>
{{ end }}
</ul>

View file

@ -0,0 +1,16 @@
<h2>{{ .Category.Name }}</h2>
{{ if .DescriptionHTML }}
<div>{{ .DescriptionHTML }}</div>
{{ end }}
{{ range .Posts }}
{{ if .Post.Title }}<h3>{{ .Post.Title }}</h3>{{ end }}
{{ .HTML }}
<a href="{{ url_abs .Path }}">{{ format_date .Post.PublishedAt }}</a>
{{ if .Categories }}
<p>
{{ range .Categories }}
<a href="{{ url_abs (printf "/categories/%s" .Slug) }}">{{ .Name }}</a>
{{ end }}
</p>
{{ end }}
{{ end }}

View file

@ -2,6 +2,5 @@ package simplecss
import "embed" import "embed"
//go:embed templates/*.html //go:embed *.html
//go:embed static/*
var FS embed.FS var FS embed.FS

View file

@ -7,7 +7,6 @@
<link rel="alternate" type="application/rss+xml" title="RSS Feed" href="{{ url_abs "/feed.xml" }}"/> <link rel="alternate" type="application/rss+xml" title="RSS Feed" href="{{ url_abs "/feed.xml" }}"/>
<link rel="alternate" type="application/json" title="JSON feed" href="{{ url_abs "/feed.json" }}"/> <link rel="alternate" type="application/json" title="JSON feed" href="{{ url_abs "/feed.json" }}"/>
<link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css"> <link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css">
<link rel="stylesheet" href="{{ url_abs "/static/style.css" }}">
</head> </head>
<body> <body>
<header> <header>

View file

@ -0,0 +1,12 @@
{{ range .Posts }}
{{ if .Post.Title }}<h3>{{ .Post.Title }}</h3>{{ end }}
{{ .HTML }}
<a href="{{ url_abs .Path }}">{{ format_date .Post.PublishedAt }}</a>
{{ if .Categories }}
<p>
{{ range .Categories }}
<a href="{{ url_abs (printf "/categories/%s" .Slug) }}">{{ .Name }}</a>
{{ end }}
</p>
{{ end }}
{{ end }}

View file

@ -0,0 +1,10 @@
{{ if .Post.Title }}<h3>{{ .Post.Title }}</h3>{{ end }}
{{ .HTML }}
<a href="{{ url_abs .Path }}">{{ format_date .Post.PublishedAt }}</a>
{{ if .Categories }}
<p>
{{ range .Categories }}
<a href="{{ url_abs (printf "/categories/%s" .Slug) }}">{{ .Name }}</a>
{{ end }}
</p>
{{ end }}

View file

@ -1,55 +0,0 @@
.h-entry {
margin-block-start: 1.5rem;
margin-block-end: 2.5rem;
}
.post-meta {
display: flex;
flex-direction: row;
justify-content: space-between;
font-size: 0.95rem;
}
.post-meta a {
color: var(--text-light);
text-decoration: none;
}
.post-meta a:hover {
text-decoration: underline;
}
.post-categories {
display: inline-flex;
gap: 0.5rem;
}
.post-categories a:before {
content: "#";
}
/* Category list */
ul.category-list {
list-style: none;
padding-inline-start: 0;
}
ul.category-list li {
display: flex;
flex-direction: row;
justify-content: start;
gap: 4rem;
}
ul.category-list span.category-list-name {
min-width: 15vw;
}
/* Category single */
.category-description {
margin-block-start: 1.5rem;
margin-block-end: 2.5rem;
}

View file

@ -1,10 +0,0 @@
<div class="post-meta">
<a href="{{ url_abs .Path }}">{{ format_date .Post.PublishedAt }}</a>
{{ if .Categories }}
<div class="post-categories">
{{ range .Categories }}
<a href="{{ url_abs (printf "/categories/%s" .Slug) }}">{{ .Name }}</a>
{{ end }}
</div>
{{ end }}
</div>

View file

@ -1,9 +0,0 @@
<h2>Categories</h2>
<ul class="category-list">
{{ range .Categories }}
<li>
<span class="category-list-name"><a href="{{ url_abs .Path }}">{{ .Name }}</a> ({{ .PostCount }})</span>
{{ if .DescriptionBrief }}<small>{{ .DescriptionBrief }}</small>{{ end }}
</li>
{{ end }}
</ul>

View file

@ -1,11 +0,0 @@
<h2>{{ .Category.Name }}</h2>
{{ if .DescriptionHTML }}
<div class="notice category-description">{{ .DescriptionHTML }}</div>
{{ end }}
{{ range .Posts }}
<div class="h-entry">
{{ if .Post.Title }}<h3>{{ .Post.Title }}</h3>{{ end }}
{{ .HTML }}
{{ template "_post_meta.html" . }}
</div>
{{ end }}

View file

@ -1,8 +0,0 @@
{{ range .Posts }}
<div class="h-entry">
{{ if .Post.Title }}<h3>{{ .Post.Title }}</h3>{{ end }}
{{ .HTML }}
{{ template "_post_meta.html" . }}
</div>
{{ end }}

View file

@ -1,5 +0,0 @@
<div class="h-entry">
{{ if .Post.Title }}<h3>{{ .Post.Title }}</h3>{{ end }}
{{ .HTML }}
{{ template "_post_meta.html" . }}
</div>

View file

@ -6,9 +6,7 @@ import (
"fmt" "fmt"
"html/template" "html/template"
"io" "io"
"io/fs"
"iter" "iter"
"log"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -33,15 +31,11 @@ type Builder struct {
func New(site pubmodel.Site, opts Options) (*Builder, error) { func New(site pubmodel.Site, opts Options) (*Builder, error) {
tmpls, err := template.New(""). tmpls, err := template.New("").
Funcs(templateFns(site, opts)). Funcs(templateFns(site, opts)).
ParseFS(opts.TemplatesFS, "*.html") ParseFS(opts.TemplatesFS, tmplNamePostSingle, tmplNamePostList, tmplNameLayoutMain, tmplNameCategoryList, tmplNameCategorySingle)
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, t := range tmpls.Templates() {
log.Printf("Loaded template %s", t.Name())
}
return &Builder{ return &Builder{
site: site, site: site,
opts: opts, opts: opts,
@ -115,9 +109,6 @@ func (b *Builder) BuildSite(outDir string) error {
return b.writeUploads(buildCtx, b.site.Uploads) return b.writeUploads(buildCtx, b.site.Uploads)
}) })
// Build static assets
eg.Go(func() error { return b.writeStaticAssets(buildCtx) })
return eg.Wait() return eg.Wait()
} }
@ -185,11 +176,10 @@ func (b *Builder) renderFeeds(ctx buildContext, postIter iter.Seq[models.Maybe[*
} }
feed.Items = append(feed.Items, &feedhub.Item{ feed.Items = append(feed.Items, &feedhub.Item{
Id: filepath.Join(b.site.BaseURL, post.GUID), Id: filepath.Join(b.site.BaseURL, post.GUID),
Title: postTitle, Title: postTitle,
Link: &feedhub.Link{Href: renderedPost.PostURL}, Link: &feedhub.Link{Href: renderedPost.PostURL},
Content: string(renderedPost.HTML), Content: string(renderedPost.HTML),
// TO FIX: Why the heck does this only include the first category?
Category: catName, Category: catName,
// TO FIX: Created should be first published // TO FIX: Created should be first published
Created: post.PublishedAt, Created: post.PublishedAt,
@ -441,7 +431,7 @@ func (b *Builder) renderTemplate(w io.Writer, name string, data interface{}) err
func (b *Builder) writeUploads(ctx buildContext, uploads []models.Upload) error { func (b *Builder) writeUploads(ctx buildContext, uploads []models.Upload) error {
for _, u := range uploads { for _, u := range uploads {
fullPath := filepath.Join(ctx.outDir, b.opts.BaseUploads, u.Slug) fullPath := filepath.Join(ctx.outDir, "uploads", u.Slug)
if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil { if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil {
return err return err
} }
@ -469,37 +459,3 @@ func (b *Builder) writeUploads(ctx buildContext, uploads []models.Upload) error
} }
return nil return nil
} }
func (b *Builder) writeStaticAssets(ctx buildContext) error {
return fs.WalkDir(b.opts.StaticFS, ".", func(path string, d os.DirEntry, err error) error {
if err != nil {
return err
} else if d.IsDir() {
return nil
}
fullPath := filepath.Join(ctx.outDir, b.opts.BaseStatic, path)
if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil {
return err
}
return func() error {
r, err := b.opts.StaticFS.Open(path)
if err != nil {
return err
}
defer r.Close()
w, err := os.Create(fullPath)
if err != nil {
return err
}
defer w.Close()
if _, err := io.Copy(w, r); err != nil {
return err
}
return nil
}()
})
}

View file

@ -29,17 +29,12 @@ const (
) )
type Options struct { type Options struct {
BasePosts string // BasePosts is the base path for posts. // BasePosts is the base path for posts.
BaseUploads string // BaseUploads is the base path for uploads. BasePosts string
BaseStatic string // BaseStatic is the base path for static assets.
// TemplatesFS provides the raw templates for rendering the site. // TemplatesFS provides the raw templates for rendering the site.
TemplatesFS fs.FS TemplatesFS fs.FS
// StaticFS provides the raw assets for the site. This will be written as is
// from the BaseStatic dir.
StaticFS fs.FS
// FeedItems holds the number of posts to show in the feed. // FeedItems holds the number of posts to show in the feed.
FeedItems int FeedItems int

View file

@ -3,7 +3,6 @@ package publisher
import ( import (
"context" "context"
"io" "io"
"io/fs"
"iter" "iter"
"log" "log"
"os" "os"
@ -103,22 +102,9 @@ func (p *Publisher) publishSite(ctx context.Context, pubSite pubmodel.Site, targ
renderTZ = time.UTC renderTZ = time.UTC
} }
templateFS, err := fs.Sub(simplecss.FS, "templates")
if err != nil {
return err
}
staticFS, err := fs.Sub(simplecss.FS, "static")
if err != nil {
return err
}
sb, err := sitebuilder.New(pubSite, sitebuilder.Options{ sb, err := sitebuilder.New(pubSite, sitebuilder.Options{
BasePosts: "/posts", BasePosts: "/posts",
BaseUploads: "/uploads", TemplatesFS: simplecss.FS,
BaseStatic: "/static",
TemplatesFS: templateFS,
StaticFS: staticFS,
FeedItems: 30, FeedItems: 30,
RenderTZ: renderTZ, RenderTZ: renderTZ,
}) })

View file

@ -1,6 +1,6 @@
<main class="container"> <main class="container">
<div class="my-4"> <div class="my-4">
<h5>{{ if .isNew }}New Category{{ else }}Edit Category{{ end }}</h5> <h4>{{ if .isNew }}New Category{{ else }}Edit Category{{ end }}</h4>
</div> </div>
{{ if .isNew }} {{ if .isNew }}
@ -10,27 +10,27 @@
{{ end }} {{ end }}
<input type="hidden" name="guid" value="{{ .category.GUID }}"> <input type="hidden" name="guid" value="{{ .category.GUID }}">
<div class="row mb-3"> <div class="row mb-3">
<label for="catName" class="col-sm-3 col-form-label text-end">Name</label> <label for="catName" class="col-sm-2 col-form-label">Name</label>
<div class="col-sm-6"> <div class="col-sm-6">
<input type="text" class="form-control" id="catName" name="name" value="{{ .category.Name }}"> <input type="text" class="form-control" id="catName" name="name" value="{{ .category.Name }}">
</div> </div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<label for="catSlug" class="col-sm-3 col-form-label text-end">Slug</label> <label for="catSlug" class="col-sm-2 col-form-label">Slug</label>
<div class="col-sm-6"> <div class="col-sm-6">
<input type="text" class="form-control" id="catSlug" name="slug" value="{{ .category.Slug }}"> <input type="text" class="form-control" id="catSlug" name="slug" value="{{ .category.Slug }}">
<div class="form-text">Auto-generated from name if left blank.</div> <div class="form-text">Auto-generated from name if left blank.</div>
</div> </div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<label for="catDesc" class="col-sm-3 col-form-label text-end">Description</label> <label for="catDesc" class="col-sm-2 col-form-label">Description</label>
<div class="col-sm-9"> <div class="col-sm-9">
<textarea class="form-control" id="catDesc" name="description" rows="5">{{ .category.Description }}</textarea> <textarea class="form-control" id="catDesc" name="description" rows="5">{{ .category.Description }}</textarea>
<div class="form-text">Markdown supported. Displayed on the category archive page.</div> <div class="form-text">Markdown supported. Displayed on the category archive page.</div>
</div> </div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-sm-3"></div> <div class="col-sm-2"></div>
<div class="col-sm-9"> <div class="col-sm-9">
<button type="submit" class="btn btn-primary">{{ if .isNew }}Create{{ else }}Save{{ end }}</button> <button type="submit" class="btn btn-primary">{{ if .isNew }}Create{{ else }}Save{{ end }}</button>
{{ if not .isNew }} {{ if not .isNew }}

View file

@ -1,32 +1,35 @@
<main class="container"> <main class="container">
<div class="my-4 d-flex justify-content-between align-items-baseline"> <div class="my-4 d-flex justify-content-between align-items-baseline">
<h4>Categories</h4>
<div> <div>
<a href="/sites/{{ .site.ID }}/categories/new" class="btn btn-success">New Category</a> <a href="/sites/{{ .site.ID }}/categories/new" class="btn btn-success">New Category</a>
</div> </div>
</div> </div>
{{ if .categories }} <table class="table">
<table class="table"> <thead>
<thead> <tr>
<th>Name</th>
<th>Slug</th>
<th>Posts</th>
<th></th>
</tr>
</thead>
<tbody>
{{ range .categories }}
<tr> <tr>
<th>Name</th> <td><a href="/sites/{{ $.site.ID }}/categories/{{ .ID }}">{{ .Name }}</a></td>
<th>Slug</th> <td><code>{{ .Slug }}</code></td>
<th>Posts</th> <td>{{ .PostCount }}</td>
<td>
<a href="/sites/{{ $.site.ID }}/categories/{{ .ID }}" class="btn btn-outline-secondary btn-sm">Edit</a>
</td>
</tr> </tr>
</thead> {{ else }}
<tbody> <tr>
{{ range .categories }} <td colspan="4" class="text-center text-muted py-4">No categories yet.</td>
<tr> </tr>
<td><a href="/sites/{{ $.site.ID }}/categories/{{ .ID }}">{{ .Name }}</a></td> {{ end }}
<td><code>{{ .Slug }}</code></td> </tbody>
<td>{{ .PostCount }}</td> </table>
</tr>
{{ end }}
</tbody>
</table>
{{ else }}
<div class="h4 m-3 text-center">
<div class="position-absolute top-50 start-50 translate-middle">📚<br>No categories yet.</div>
</div>
{{ end }}
</main> </main>

View file

@ -7,7 +7,7 @@
<link rel="stylesheet" href="/static/assets/main.css"> <link rel="stylesheet" href="/static/assets/main.css">
<script src="/static/assets/main.js" type="module"></script> <script src="/static/assets/main.js" type="module"></script>
</head> </head>
<body class="d-flex flex-column {{.bodyClass}}"> <body class="min-vh-100 d-flex flex-column">
{{ template "_common/nav" . }} {{ template "_common/nav" . }}
{{ embed }} {{ embed }}

View file

@ -1,6 +1,6 @@
{{ $isPublished := ne .post.State 1 }} {{ $isPublished := ne .post.State 1 }}
<main class="flex-grow-1 position-relative"> <main class="flex-grow-1 position-relative">
<form action="/sites/{{.site.ID}}/posts" method="post" class="container-fluid post-form py-2" <form action="/sites/{{.site.ID}}/posts" method="post" class="container-fluid post-form p-2"
data-controller="postedit" data-controller="postedit"
data-action="keydown.meta+s->postedit#save keydown.meta+enter->postedit#publish" data-action="keydown.meta+s->postedit#save keydown.meta+enter->postedit#publish"
data-postedit-save-action-value="{{ if $isPublished }}Update{{ else }}Save Draft{{ end }}"> data-postedit-save-action-value="{{ if $isPublished }}Update{{ else }}Save Draft{{ end }}">
@ -10,7 +10,9 @@
<div class="mb-2"> <div class="mb-2">
<input type="text" name="title" class="form-control" placeholder="Title" value="{{ .post.Title }}"> <input type="text" name="title" class="form-control" placeholder="Title" value="{{ .post.Title }}">
</div> </div>
<textarea data-postedit-target="bodyTextEdit" name="body" class="form-control flex-grow-1" rows="3">{{.post.Body}}</textarea> <div>
<textarea data-postedit-target="bodyTextEdit" name="body" class="form-control" rows="3">{{.post.Body}}</textarea>
</div>
<div> <div>
{{ if $isPublished }} {{ if $isPublished }}
<input type="submit" name="action" class="btn btn-primary mt-2" value="Update"> <input type="submit" name="action" class="btn btn-primary mt-2" value="Update">

View file

@ -28,9 +28,12 @@
<div class="mb-3 d-flex align-items-center flex-wrap gap-1"> <div class="mb-3 d-flex align-items-center flex-wrap gap-1">
{{ if eq $p.State 1 }} {{ if eq $p.State 1 }}
<span class="text-muted post-date">{{ $.user.FormatTime $p.UpdatedAt }}</span> <span class="ms-3 badge text-primary-emphasis bg-primary-subtle border border-primary-subtle">Draft</span> <span class="text-muted">{{ $.user.FormatTime $p.UpdatedAt }}</span> <span class="ms-3 badge text-primary-emphasis bg-primary-subtle border border-primary-subtle">Draft</span>
{{ else }} {{ else }}
<span class="text-muted post-date">{{ $.user.FormatTime $p.PublishedAt }}</span> <span class="text-muted">{{ $.user.FormatTime $p.PublishedAt }}</span>
{{ end }}
{{ range $p.Categories }}
<span class="ms-1 badge bg-secondary-subtle text-secondary-emphasis border border-secondary-subtle">{{ .Name }}</span>
{{ end }} {{ end }}
</div> </div>