Started working on the frontend
- Added the new post frontend - Hooked up publishing of posts to the site publisher - Added an site exporter as a publishing target
This commit is contained in:
parent
a59008b3e8
commit
e77cac2fd5
40 changed files with 1427 additions and 84 deletions
|
|
@ -3,8 +3,10 @@ package models
|
|||
import "context"
|
||||
|
||||
type userKeyType struct{}
|
||||
type siteKeyType struct{}
|
||||
|
||||
var userKey = userKeyType{}
|
||||
var siteKey = userKeyType{}
|
||||
|
||||
func WithUser(ctx context.Context, user User) context.Context {
|
||||
return context.WithValue(ctx, userKey, user)
|
||||
|
|
@ -14,3 +16,12 @@ func GetUser(ctx context.Context) (User, bool) {
|
|||
user, ok := ctx.Value(userKey).(User)
|
||||
return user, ok
|
||||
}
|
||||
|
||||
func WithSite(ctx context.Context, site Site) context.Context {
|
||||
return context.WithValue(ctx, siteKey, site)
|
||||
}
|
||||
|
||||
func GetSite(ctx context.Context) (Site, bool) {
|
||||
site, ok := ctx.Value(siteKey).(Site)
|
||||
return site, ok
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,3 +4,5 @@ import "emperror.dev/errors"
|
|||
|
||||
var UserRequiredError = errors.New("user required")
|
||||
var PermissionError = errors.New("permission denied")
|
||||
var NotFoundError = errors.New("not found")
|
||||
var SiteRequiredError = errors.New("site required")
|
||||
|
|
|
|||
8
models/ids.go
Normal file
8
models/ids.go
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
package models
|
||||
|
||||
import "github.com/matoous/go-nanoid/v2"
|
||||
|
||||
func NewNanoID() string {
|
||||
id, _ := gonanoid.New(12)
|
||||
return id
|
||||
}
|
||||
34
models/ids_test.go
Normal file
34
models/ids_test.go
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewNanoID(t *testing.T) {
|
||||
id := NewNanoID()
|
||||
|
||||
if len(id) != 12 {
|
||||
t.Errorf("Expected ID length of 12, got %d", len(id))
|
||||
}
|
||||
|
||||
if id == "" {
|
||||
t.Error("Expected non-empty ID")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewNanoID_Uniqueness(t *testing.T) {
|
||||
ids := make(map[string]bool)
|
||||
iterations := 1000
|
||||
|
||||
for i := 0; i < iterations; i++ {
|
||||
id := NewNanoID()
|
||||
if ids[id] {
|
||||
t.Errorf("Duplicate ID generated: %s", id)
|
||||
}
|
||||
ids[id] = true
|
||||
}
|
||||
|
||||
if len(ids) != iterations {
|
||||
t.Errorf("Expected %d unique IDs, got %d", iterations, len(ids))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,12 @@
|
|||
package models
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
type Post struct {
|
||||
ID int64
|
||||
|
|
@ -12,3 +18,61 @@ type Post struct {
|
|||
CreatedAt time.Time
|
||||
PublishedAt time.Time
|
||||
}
|
||||
|
||||
func (p *Post) BestSlug() string {
|
||||
if p.Slug != "" {
|
||||
return p.Slug
|
||||
}
|
||||
|
||||
bestDateToUse := p.PublishedAt
|
||||
if bestDateToUse.IsZero() {
|
||||
bestDateToUse = p.CreatedAt
|
||||
}
|
||||
|
||||
slugPath := firstNWords(p.Title, 3, wordForSlug)
|
||||
if slugPath == "" {
|
||||
slugPath = firstNWords(p.Body, 3, wordForSlug)
|
||||
}
|
||||
if slugPath != "" {
|
||||
slugPath = strings.Replace(strings.ToLower(slugPath), " ", "-", -1)
|
||||
} else {
|
||||
slugPath = p.GUID
|
||||
if slugPath == "" {
|
||||
slugPath = bestDateToUse.Format("150405")
|
||||
}
|
||||
}
|
||||
|
||||
datePart := fmt.Sprintf("%04d/%02d/%02d", bestDateToUse.Year(), bestDateToUse.Month(), bestDateToUse.Day())
|
||||
return fmt.Sprintf("/%s/%s", datePart, slugPath)
|
||||
}
|
||||
|
||||
func wordForSlug(word string) string {
|
||||
var sb strings.Builder
|
||||
for _, c := range word {
|
||||
if unicode.IsLetter(c) || unicode.IsNumber(c) {
|
||||
sb.WriteRune(c)
|
||||
}
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func firstNWords(s string, n int, keepWord func(word string) string) string {
|
||||
if n == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
suitableWords := make([]string, 0, n)
|
||||
|
||||
scnr := bufio.NewScanner(strings.NewReader(s))
|
||||
scnr.Split(bufio.ScanWords)
|
||||
for scnr.Scan() {
|
||||
word := scnr.Text()
|
||||
if w := keepWord(word); w != "" {
|
||||
suitableWords = append(suitableWords, w)
|
||||
if len(suitableWords) >= n {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return strings.Join(suitableWords, " ")
|
||||
}
|
||||
|
|
|
|||
78
models/posts_test.go
Normal file
78
models/posts_test.go
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFirstNWords(t *testing.T) {
|
||||
tests := []struct {
|
||||
words int
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{words: 3, input: "This is a test string with multiple words", expected: "This is a"},
|
||||
{words: 5, input: "Short string", expected: "Short string"},
|
||||
{words: 0, input: "Empty string", expected: ""},
|
||||
{words: 3, input: " The rain in Spain etc.", expected: "The rain in"},
|
||||
{words: 3, input: " The? rain! in$ Spain etc.", expected: "The rain in"},
|
||||
{words: 3, input: " !!! The 23123 rain ++_+_+ in Spain etc.", expected: "The 23123 rain"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.input, func(t *testing.T) {
|
||||
result := firstNWords(test.input, test.words, wordForSlug)
|
||||
assert.Equal(t, test.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPost_BestSlug(t *testing.T) {
|
||||
postDate := time.Date(2023, time.January, 1, 15, 12, 11, 0, time.UTC)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
post Post
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "returns slug when slug is set",
|
||||
post: Post{Slug: "my-custom-slug", Title: "My Title", PublishedAt: postDate},
|
||||
expected: "my-custom-slug",
|
||||
},
|
||||
{
|
||||
name: "use title when slug is empty",
|
||||
post: Post{Slug: "", Title: "My Title", PublishedAt: postDate},
|
||||
expected: "/2023/01/01/my-title",
|
||||
},
|
||||
{
|
||||
name: "use body when slug is empty",
|
||||
post: Post{Slug: "", Body: "My body", PublishedAt: postDate},
|
||||
expected: "/2023/01/01/my-body",
|
||||
},
|
||||
{
|
||||
name: "use guid when body is empty",
|
||||
post: Post{GUID: "abc123", PublishedAt: postDate},
|
||||
expected: "/2023/01/01/abc123",
|
||||
},
|
||||
{
|
||||
name: "use time component when guid is empty",
|
||||
post: Post{Slug: "", Title: "", PublishedAt: postDate},
|
||||
expected: "/2023/01/01/151211",
|
||||
},
|
||||
{
|
||||
name: "use created date if publish date is unset",
|
||||
post: Post{Slug: "", Title: "a title", CreatedAt: postDate},
|
||||
expected: "/2023/01/01/a-title",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
result := test.post.BestSlug()
|
||||
assert.Equal(t, test.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue