Compare commits
2 commits
740cf8979a
...
d9aec4af2c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9aec4af2c | ||
|
|
f45bdcd83c |
|
|
@ -10,21 +10,7 @@ $container-max-widths: (
|
|||
|
||||
@import "bootstrap/scss/bootstrap.scss";
|
||||
|
||||
// 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%;
|
||||
}
|
||||
// Post list
|
||||
|
||||
.postlist .post img {
|
||||
max-width: 300px;
|
||||
|
|
@ -32,6 +18,49 @@ $container-max-widths: (
|
|||
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 {
|
||||
max-width: 100vw;
|
||||
height: auto;
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ func (ph PostsHandler) New(c fiber.Ctx) error {
|
|||
"post": p,
|
||||
"categories": cats,
|
||||
"selectedCategories": map[int64]bool{},
|
||||
"bodyClass": "post-edit-page",
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -93,6 +94,7 @@ func (ph PostsHandler) Edit(c fiber.Ctx) error {
|
|||
"post": post,
|
||||
"categories": cats,
|
||||
"selectedCategories": selectedCategories,
|
||||
"bodyClass": "post-edit-page",
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
<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>
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
<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 }}
|
||||
|
|
@ -2,5 +2,6 @@ package simplecss
|
|||
|
||||
import "embed"
|
||||
|
||||
//go:embed *.html
|
||||
//go:embed templates/*.html
|
||||
//go:embed static/*
|
||||
var FS embed.FS
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
{{ 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 }}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
{{ 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 }}
|
||||
55
layouts/simplecss/static/style.css
Normal file
55
layouts/simplecss/static/style.css
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
.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;
|
||||
}
|
||||
10
layouts/simplecss/templates/_post_meta.html
Normal file
10
layouts/simplecss/templates/_post_meta.html
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<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>
|
||||
9
layouts/simplecss/templates/categories_list.html
Normal file
9
layouts/simplecss/templates/categories_list.html
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<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>
|
||||
11
layouts/simplecss/templates/categories_single.html
Normal file
11
layouts/simplecss/templates/categories_single.html
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<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 }}
|
||||
|
|
@ -7,6 +7,7 @@
|
|||
<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="stylesheet" href="https://cdn.simplecss.org/simple.min.css">
|
||||
<link rel="stylesheet" href="{{ url_abs "/static/style.css" }}">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
8
layouts/simplecss/templates/posts_list.html
Normal file
8
layouts/simplecss/templates/posts_list.html
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{{ range .Posts }}
|
||||
<div class="h-entry">
|
||||
{{ if .Post.Title }}<h3>{{ .Post.Title }}</h3>{{ end }}
|
||||
{{ .HTML }}
|
||||
|
||||
{{ template "_post_meta.html" . }}
|
||||
</div>
|
||||
{{ end }}
|
||||
5
layouts/simplecss/templates/posts_single.html
Normal file
5
layouts/simplecss/templates/posts_single.html
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<div class="h-entry">
|
||||
{{ if .Post.Title }}<h3>{{ .Post.Title }}</h3>{{ end }}
|
||||
{{ .HTML }}
|
||||
{{ template "_post_meta.html" . }}
|
||||
</div>
|
||||
|
|
@ -6,7 +6,9 @@ import (
|
|||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/fs"
|
||||
"iter"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
|
@ -31,11 +33,15 @@ type Builder struct {
|
|||
func New(site pubmodel.Site, opts Options) (*Builder, error) {
|
||||
tmpls, err := template.New("").
|
||||
Funcs(templateFns(site, opts)).
|
||||
ParseFS(opts.TemplatesFS, tmplNamePostSingle, tmplNamePostList, tmplNameLayoutMain, tmplNameCategoryList, tmplNameCategorySingle)
|
||||
ParseFS(opts.TemplatesFS, "*.html")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, t := range tmpls.Templates() {
|
||||
log.Printf("Loaded template %s", t.Name())
|
||||
}
|
||||
|
||||
return &Builder{
|
||||
site: site,
|
||||
opts: opts,
|
||||
|
|
@ -109,6 +115,9 @@ func (b *Builder) BuildSite(outDir string) error {
|
|||
return b.writeUploads(buildCtx, b.site.Uploads)
|
||||
})
|
||||
|
||||
// Build static assets
|
||||
eg.Go(func() error { return b.writeStaticAssets(buildCtx) })
|
||||
|
||||
return eg.Wait()
|
||||
}
|
||||
|
||||
|
|
@ -180,6 +189,7 @@ func (b *Builder) renderFeeds(ctx buildContext, postIter iter.Seq[models.Maybe[*
|
|||
Title: postTitle,
|
||||
Link: &feedhub.Link{Href: renderedPost.PostURL},
|
||||
Content: string(renderedPost.HTML),
|
||||
// TO FIX: Why the heck does this only include the first category?
|
||||
Category: catName,
|
||||
// TO FIX: Created should be first published
|
||||
Created: post.PublishedAt,
|
||||
|
|
@ -431,7 +441,7 @@ func (b *Builder) renderTemplate(w io.Writer, name string, data interface{}) err
|
|||
|
||||
func (b *Builder) writeUploads(ctx buildContext, uploads []models.Upload) error {
|
||||
for _, u := range uploads {
|
||||
fullPath := filepath.Join(ctx.outDir, "uploads", u.Slug)
|
||||
fullPath := filepath.Join(ctx.outDir, b.opts.BaseUploads, u.Slug)
|
||||
if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -459,3 +469,37 @@ func (b *Builder) writeUploads(ctx buildContext, uploads []models.Upload) error
|
|||
}
|
||||
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
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,12 +29,17 @@ const (
|
|||
)
|
||||
|
||||
type Options struct {
|
||||
// BasePosts is the base path for posts.
|
||||
BasePosts string
|
||||
BasePosts string // BasePosts is the base path for posts.
|
||||
BaseUploads string // BaseUploads is the base path for uploads.
|
||||
BaseStatic string // BaseStatic is the base path for static assets.
|
||||
|
||||
// TemplatesFS provides the raw templates for rendering the site.
|
||||
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 int
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package publisher
|
|||
import (
|
||||
"context"
|
||||
"io"
|
||||
"io/fs"
|
||||
"iter"
|
||||
"log"
|
||||
"os"
|
||||
|
|
@ -102,9 +103,22 @@ func (p *Publisher) publishSite(ctx context.Context, pubSite pubmodel.Site, targ
|
|||
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{
|
||||
BasePosts: "/posts",
|
||||
TemplatesFS: simplecss.FS,
|
||||
BaseUploads: "/uploads",
|
||||
BaseStatic: "/static",
|
||||
TemplatesFS: templateFS,
|
||||
StaticFS: staticFS,
|
||||
FeedItems: 30,
|
||||
RenderTZ: renderTZ,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<main class="container">
|
||||
<div class="my-4">
|
||||
<h4>{{ if .isNew }}New Category{{ else }}Edit Category{{ end }}</h4>
|
||||
<h5>{{ if .isNew }}New Category{{ else }}Edit Category{{ end }}</h5>
|
||||
</div>
|
||||
|
||||
{{ if .isNew }}
|
||||
|
|
@ -10,27 +10,27 @@
|
|||
{{ end }}
|
||||
<input type="hidden" name="guid" value="{{ .category.GUID }}">
|
||||
<div class="row mb-3">
|
||||
<label for="catName" class="col-sm-2 col-form-label">Name</label>
|
||||
<label for="catName" class="col-sm-3 col-form-label text-end">Name</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" class="form-control" id="catName" name="name" value="{{ .category.Name }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<label for="catSlug" class="col-sm-2 col-form-label">Slug</label>
|
||||
<label for="catSlug" class="col-sm-3 col-form-label text-end">Slug</label>
|
||||
<div class="col-sm-6">
|
||||
<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>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<label for="catDesc" class="col-sm-2 col-form-label">Description</label>
|
||||
<label for="catDesc" class="col-sm-3 col-form-label text-end">Description</label>
|
||||
<div class="col-sm-9">
|
||||
<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>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-sm-2"></div>
|
||||
<div class="col-sm-3"></div>
|
||||
<div class="col-sm-9">
|
||||
<button type="submit" class="btn btn-primary">{{ if .isNew }}Create{{ else }}Save{{ end }}</button>
|
||||
{{ if not .isNew }}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,17 @@
|
|||
<main class="container">
|
||||
<div class="my-4 d-flex justify-content-between align-items-baseline">
|
||||
<h4>Categories</h4>
|
||||
<div>
|
||||
<a href="/sites/{{ .site.ID }}/categories/new" class="btn btn-success">New Category</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ if .categories }}
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Slug</th>
|
||||
<th>Posts</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
@ -21,15 +20,13 @@
|
|||
<td><a href="/sites/{{ $.site.ID }}/categories/{{ .ID }}">{{ .Name }}</a></td>
|
||||
<td><code>{{ .Slug }}</code></td>
|
||||
<td>{{ .PostCount }}</td>
|
||||
<td>
|
||||
<a href="/sites/{{ $.site.ID }}/categories/{{ .ID }}" class="btn btn-outline-secondary btn-sm">Edit</a>
|
||||
</td>
|
||||
</tr>
|
||||
{{ else }}
|
||||
<tr>
|
||||
<td colspan="4" class="text-center text-muted py-4">No categories yet.</td>
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
<link rel="stylesheet" href="/static/assets/main.css">
|
||||
<script src="/static/assets/main.js" type="module"></script>
|
||||
</head>
|
||||
<body class="min-vh-100 d-flex flex-column">
|
||||
<body class="d-flex flex-column {{.bodyClass}}">
|
||||
{{ template "_common/nav" . }}
|
||||
|
||||
{{ embed }}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{{ $isPublished := ne .post.State 1 }}
|
||||
<main class="flex-grow-1 position-relative">
|
||||
<form action="/sites/{{.site.ID}}/posts" method="post" class="container-fluid post-form p-2"
|
||||
<form action="/sites/{{.site.ID}}/posts" method="post" class="container-fluid post-form py-2"
|
||||
data-controller="postedit"
|
||||
data-action="keydown.meta+s->postedit#save keydown.meta+enter->postedit#publish"
|
||||
data-postedit-save-action-value="{{ if $isPublished }}Update{{ else }}Save Draft{{ end }}">
|
||||
|
|
@ -10,9 +10,7 @@
|
|||
<div class="mb-2">
|
||||
<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" rows="3">{{.post.Body}}</textarea>
|
||||
</div>
|
||||
<textarea data-postedit-target="bodyTextEdit" name="body" class="form-control flex-grow-1" rows="3">{{.post.Body}}</textarea>
|
||||
<div>
|
||||
{{ if $isPublished }}
|
||||
<input type="submit" name="action" class="btn btn-primary mt-2" value="Update">
|
||||
|
|
|
|||
|
|
@ -28,12 +28,9 @@
|
|||
|
||||
<div class="mb-3 d-flex align-items-center flex-wrap gap-1">
|
||||
{{ if eq $p.State 1 }}
|
||||
<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>
|
||||
<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>
|
||||
{{ else }}
|
||||
<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>
|
||||
<span class="text-muted post-date">{{ $.user.FormatTime $p.PublishedAt }}</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue