Compare commits

...

5 Commits

Author SHA1 Message Date
Niko Abeler c39d8ea01d http router 2022-08-01 21:06:48 +02:00
Niko Abeler 9d14e1a8fc creating media dir 2022-08-01 19:50:29 +02:00
Niko Abeler 03a27f890c render page with user config 2022-07-27 21:53:56 +02:00
Niko Abeler 09a084e5fa a bit more style 2022-07-27 21:26:37 +02:00
Niko Abeler ec1c18bc54 list user index 2022-07-24 20:29:49 +02:00
15 changed files with 350 additions and 82 deletions

View File

@ -2,72 +2,70 @@ package main
import ( import (
"h4kor/kiss-social" "h4kor/kiss-social"
"h4kor/kiss-social/cmd/kiss-web/static"
"net/http" "net/http"
"os" "os"
"strconv" "strconv"
"strings"
"github.com/julienschmidt/httprouter"
) )
func handler(repo kiss.Repository) func(http.ResponseWriter, *http.Request) { // func handler(repo kiss.Repository) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) { // return func(w http.ResponseWriter, r *http.Request) {
// normalize the path // // normalize the path
path := r.URL.Path // path := r.URL.Path
// remove leading '/' // // remove leading '/'
if len(path) > 0 && path[0] == '/' { // if len(path) > 0 && path[0] == '/' {
path = path[1:] // path = path[1:]
} // }
// remove trailing '/' // // remove trailing '/'
if len(path) > 0 && path[len(path)-1] == '/' { // if len(path) > 0 && path[len(path)-1] == '/' {
path = path[:len(path)-1] // path = path[:len(path)-1]
} // }
// index page // // index page
if path == "" { // if path == "" {
println("Index page") // println("Index page")
indexHandler(repo)(w, r) // indexHandler(repo)(w, r)
return // return
} // }
// parse the path // // parse the path
parts := strings.Split(path, "/") // parts := strings.Split(path, "/")
userName := parts[0] // userName := parts[0]
// only one part -> user page // // only one part -> user page
if len(parts) == 1 { // if len(parts) == 1 {
println("User page") // println("User page")
userHandler(repo, userName)(w, r) // userHandler(repo, userName)(w, r)
return // return
} // }
// multiple parts -> post page // // multiple parts -> post page
println("Post page") // println("Post page")
postId := strings.Join(parts[1:], "/") // postId := strings.Join(parts[1:], "/")
postHandler(repo, userName, postId)(w, r) // postHandler(repo, userName, postId)(w, r)
} // }
} // }
func indexHandler(repo kiss.Repository) func(http.ResponseWriter, *http.Request, httprouter.Params) {
return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
html, err := kiss.RenderUserList(repo)
func indexHandler(repo kiss.Repository) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
users, err := repo.Users()
if err != nil { if err != nil {
println("Error getting users: ", err.Error()) println("Error rendering index: ", err.Error())
w.Write([]byte("Error getting users")) w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Internal server error"))
return return
} }
w.Write([]byte("Index")) println("Rendering index")
w.Write([]byte("<ul>")) w.Write([]byte(html))
for _, user := range users {
w.Write([]byte("<li>"))
w.Write([]byte(user.Name()))
w.Write([]byte("</li>"))
}
} }
} }
func userHandler(repo kiss.Repository, userName string) func(http.ResponseWriter, *http.Request) { func userHandler(repo kiss.Repository) func(http.ResponseWriter, *http.Request, httprouter.Params) {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
userName := ps.ByName("user")
user, err := repo.GetUser(userName) user, err := repo.GetUser(userName)
if err != nil { if err != nil {
println("Error getting user: ", err.Error()) println("Error getting user: ", err.Error())
@ -87,8 +85,10 @@ func userHandler(repo kiss.Repository, userName string) func(http.ResponseWriter
} }
} }
func postHandler(repo kiss.Repository, userName string, postId string) func(http.ResponseWriter, *http.Request) { func postHandler(repo kiss.Repository) func(http.ResponseWriter, *http.Request, httprouter.Params) {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
userName := ps.ByName("user")
postId := ps.ByName("post")
user, err := repo.GetUser(userName) user, err := repo.GetUser(userName)
if err != nil { if err != nil {
println("Error getting user: ", err.Error()) println("Error getting user: ", err.Error())
@ -144,10 +144,13 @@ func main() {
os.Exit(1) os.Exit(1)
} }
http.Handle("/static/", static.StaticHandler(repo)) router := httprouter.New()
http.HandleFunc("/", handler(repo)) router.ServeFiles("/static/*filepath", http.Dir(repo.StaticDir()))
router.GET("/", indexHandler(repo))
router.GET("/user/:user", userHandler(repo))
router.GET("/user/:user/posts/*post", postHandler(repo))
println("Listening on port", port) println("Listening on port", port)
http.ListenAndServe(":"+strconv.Itoa(port), nil) http.ListenAndServe(":"+strconv.Itoa(port), router)
} }

View File

@ -7,10 +7,32 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ .Title }}</title> <title>{{ .Title }}</title>
<link rel="stylesheet" href="/static/pico.min.css"> <link rel="stylesheet" href="/static/pico.min.css">
<style>
header {
background-color: {{.HeaderColor}};
}
</style>
</head> </head>
<body> <body>
{{ .Content }} <header>
<nav class="container">
<ul>
<li>
<hgroup>
<h2>{{ .UserTitle }}</h2>
<h3>{{ .UserSubtitle }}</h3>
</hgroup>
</li>
</ul>
</nav>
</header>
<main class="container">
{{ .Content }}
</main>
<footer class="container">
</footer>
</body> </body>
</html> </html>

View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ .Title }}</title>
<link rel="stylesheet" href="/static/pico.min.css">
</head>
<body>
{{ .Content }}
</body>
</html>

View File

@ -1,8 +1,8 @@
{{range .Users}} {{range .}}
<ul> <ul>
<li> <li>
<a href="{{ .Path() }}"> <a href="{{ .Path }}">
{{ .Name() }} {{ .Name }}
</a> </a>
</li> </li>
</ul> </ul>

1
go.mod
View File

@ -3,6 +3,7 @@ module h4kor/kiss-social
go 1.18 go 1.18
require ( require (
github.com/julienschmidt/httprouter v1.3.0 // indirect
github.com/yuin/goldmark v1.4.13 // indirect github.com/yuin/goldmark v1.4.13 // indirect
github.com/yuin/goldmark-meta v1.1.0 // indirect github.com/yuin/goldmark-meta v1.1.0 // indirect
gopkg.in/yaml.v2 v2.3.0 // indirect gopkg.in/yaml.v2 v2.3.0 // indirect

2
go.sum
View File

@ -1,3 +1,5 @@
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc= github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc=

View File

@ -30,6 +30,11 @@ func getTestUser() kiss.User {
return user return user
} }
func getTestRepo() kiss.Repository {
repo, _ := kiss.CreateRepository(testRepoName())
return repo
}
func contains(s []string, e string) bool { func contains(s []string, e string) bool {
for _, a := range s { for _, a := range s {
if a == e { if a == e {

View File

@ -7,6 +7,7 @@ import (
"github.com/yuin/goldmark" "github.com/yuin/goldmark"
meta "github.com/yuin/goldmark-meta" meta "github.com/yuin/goldmark-meta"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser" "github.com/yuin/goldmark/parser"
) )
@ -20,8 +21,12 @@ func (post Post) Dir() string {
return path.Join(post.user.Dir(), "public", post.id) return path.Join(post.user.Dir(), "public", post.id)
} }
func (post Post) MediaDir() string {
return path.Join(post.Dir(), "media")
}
func (post Post) UrlPath() string { func (post Post) UrlPath() string {
return post.user.Path() + "/" + post.id return post.user.Path() + "/posts/" + post.id
} }
func (post Post) Title() string { func (post Post) Title() string {
@ -43,6 +48,7 @@ func (post Post) MarkdownData() (bytes.Buffer, map[string]interface{}) {
markdown := goldmark.New( markdown := goldmark.New(
goldmark.WithExtensions( goldmark.WithExtensions(
meta.Meta, meta.Meta,
extension.GFM,
), ),
) )
var buf bytes.Buffer var buf bytes.Buffer

View File

@ -1,6 +1,9 @@
package kiss_test package kiss_test
import "testing" import (
"path"
"testing"
)
func TestCanGetPostTitle(t *testing.T) { func TestCanGetPostTitle(t *testing.T) {
user := getTestUser() user := getTestUser()
@ -10,3 +13,12 @@ func TestCanGetPostTitle(t *testing.T) {
t.Error("Wrong Title. Got: " + result) t.Error("Wrong Title. Got: " + result)
} }
} }
func TestMediaDir(t *testing.T) {
user := getTestUser()
post, _ := user.CreateNewPost("testpost")
result := post.MediaDir()
if result != path.Join(post.Dir(), "media") {
t.Error("Wrong MediaDir. Got: " + result)
}
}

View File

@ -22,8 +22,35 @@ type PostRenderData struct {
Post template.HTML Post template.HTML
} }
func renderIntoBaseTemplate(user User, data PageContent) (string, error) {
baseTemplate, _ := user.Template()
t, err := template.New("index").Parse(baseTemplate)
if err != nil {
return "", err
}
user_config, _ := user.Config()
full_data := struct {
Title string
Content template.HTML
UserTitle string
UserSubtitle string
HeaderColor string
}{
Title: data.Title,
Content: data.Content,
UserTitle: user_config.Title,
UserSubtitle: user_config.SubTitle,
HeaderColor: user_config.HeaderColor,
}
var html bytes.Buffer
t.Execute(&html, full_data)
return html.String(), nil
}
func RenderPost(post Post) (string, error) { func RenderPost(post Post) (string, error) {
baseTemplate, _ := post.user.Template()
buf, _ := post.MarkdownData() buf, _ := post.MarkdownData()
postTemplate, _ := template.New("post").Parse(postTemplateStr) postTemplate, _ := template.New("post").Parse(postTemplateStr)
@ -41,16 +68,10 @@ func RenderPost(post Post) (string, error) {
Content: template.HTML(postHtml.String()), Content: template.HTML(postHtml.String()),
} }
var html bytes.Buffer return renderIntoBaseTemplate(post.user, data)
t, err := template.New("page").Parse(baseTemplate)
t.Execute(&html, data)
return html.String(), err
} }
func RenderIndexPage(user User) (string, error) { func RenderIndexPage(user User) (string, error) {
baseTemplate, _ := user.Template()
posts, _ := user.Posts() posts, _ := user.Posts()
postHtml := "" postHtml := ""
@ -64,18 +85,34 @@ func RenderIndexPage(user User) (string, error) {
Content: template.HTML(postHtml), Content: template.HTML(postHtml),
} }
var html bytes.Buffer return renderIntoBaseTemplate(user, data)
t, err := template.New("index").Parse(baseTemplate)
t.Execute(&html, data)
return html.String(), err
} }
// func RenderUserList(user User) (string, error) { func RenderUserList(repo Repository) (string, error) {
// base_template, _ := user.Template() baseTemplate, _ := repo.Template()
// users, _ := user.repo.Users() users, _ := repo.Users()
// template.New("user_list").Parse() t, err := template.New("user_list").Parse(userListTemplateStr)
// return strings.Replace(template, "{{content}}", userHtml, -1), nil if err != nil {
// } return "", err
}
var userHtml bytes.Buffer
t.Execute(&userHtml, users)
data := PageContent{
Title: "Index",
Content: template.HTML(userHtml.String()),
}
var html bytes.Buffer
t, err = template.New("index").Parse(baseTemplate)
if err != nil {
return "", err
}
t.Execute(&html, data)
return html.String(), nil
}

View File

@ -2,6 +2,8 @@ package kiss_test
import ( import (
"h4kor/kiss-social" "h4kor/kiss-social"
"os"
"path"
"strings" "strings"
"testing" "testing"
) )
@ -49,3 +51,55 @@ func TestCanRenderIndexPage(t *testing.T) {
t.Error("Post title not rendered. Got: " + result) t.Error("Post title not rendered. Got: " + result)
} }
} }
func TestRenderIndexPageWithBrokenBaseTemplate(t *testing.T) {
user := getTestUser()
user.CreateNewPost("testpost1")
user.CreateNewPost("testpost2")
os.WriteFile(path.Join(user.Dir(), "meta/base.html"), []byte("{{content}}"), 0644)
_, err := kiss.RenderIndexPage(user)
if err == nil {
t.Error("Expected error rendering index page, got nil")
}
}
func TestRenderUserList(t *testing.T) {
repo := getTestRepo()
repo.CreateUser("user1")
repo.CreateUser("user2")
result, err := kiss.RenderUserList(repo)
if err != nil {
t.Error("Error rendering user list: " + err.Error())
}
if !strings.Contains(result, "user1") {
t.Error("Post title not rendered. Got: " + result)
}
if !strings.Contains(result, "user2") {
t.Error("Post title not rendered. Got: " + result)
}
}
func TestRendersHeaderTitle(t *testing.T) {
user := getTestUser()
user.SetConfig(kiss.UserConfig{
Title: "Test Title",
SubTitle: "Test SubTitle",
HeaderColor: "#ff1337",
})
post, _ := user.CreateNewPost("testpost")
result, _ := kiss.RenderPost(post)
if !strings.Contains(result, "Test Title") {
t.Error("Header title not rendered. Got: " + result)
}
if !strings.Contains(result, "Test SubTitle") {
t.Error("Header subtitle not rendered. Got: " + result)
}
if !strings.Contains(result, "#ff1337") {
t.Error("Header color not rendered. Got: " + result)
}
}

View File

@ -4,14 +4,17 @@ import (
"embed" "embed"
_ "embed" _ "embed"
"fmt" "fmt"
"io/ioutil"
"os" "os"
"path" "path"
"gopkg.in/yaml.v2"
) )
//go:embed embed/initial/base.html //go:embed embed/initial/base.html
var base_template string var base_template string
//go:embed embed/initial/static/* //go:embed embed/*
var static_files embed.FS var static_files embed.FS
var VERSION = "0.0.1" var VERSION = "0.0.1"
@ -40,6 +43,10 @@ func CreateRepository(name string) (Repository, error) {
src_data, _ := static_files.ReadFile(file.Name()) src_data, _ := static_files.ReadFile(file.Name())
os.WriteFile(newRepo.StaticDir()+"/"+file.Name(), src_data, 0644) os.WriteFile(newRepo.StaticDir()+"/"+file.Name(), src_data, 0644)
} }
// copy repo_base.html to base.html
src_data, _ := static_files.ReadFile("embed/initial/repo_base.html")
os.WriteFile(newRepo.Dir()+"/base.html", src_data, 0644)
return newRepo, nil return newRepo, nil
} }
@ -66,6 +73,16 @@ func (repo Repository) UsersDir() string {
return path.Join(repo.Dir(), "users") return path.Join(repo.Dir(), "users")
} }
func (repo Repository) Template() (string, error) {
// load base.html
path := path.Join(repo.Dir(), "base.html")
base_html, err := ioutil.ReadFile(path)
if err != nil {
return "", err
}
return string(base_html), nil
}
func (repo Repository) Users() ([]User, error) { func (repo Repository) Users() ([]User, error) {
userNames := listDir(repo.UsersDir()) userNames := listDir(repo.UsersDir())
users := make([]User, len(userNames)) users := make([]User, len(userNames))
@ -93,6 +110,13 @@ func (repo Repository) CreateUser(name string) (User, error) {
os.WriteFile(path.Join(user_dir, "meta", "VERSION"), []byte(VERSION), 0644) os.WriteFile(path.Join(user_dir, "meta", "VERSION"), []byte(VERSION), 0644)
os.WriteFile(path.Join(user_dir, "meta", "base.html"), []byte(base_template), 0644) os.WriteFile(path.Join(user_dir, "meta", "base.html"), []byte(base_template), 0644)
meta, _ := yaml.Marshal(UserConfig{
Title: name,
SubTitle: "",
HeaderColor: "#bdd6be",
})
os.WriteFile(new_user.ConfigFile(), meta, 0644)
return new_user, nil return new_user, nil
} }

View File

@ -63,6 +63,15 @@ func TestCreateUserAddsBaseHtmlFile(t *testing.T) {
} }
} }
func TestCreateUserAddConfigYml(t *testing.T) {
// Create a new user
repo, _ := kiss.CreateRepository(testRepoName())
user, _ := repo.CreateUser(randomUserName())
if _, err := os.Stat(path.Join(user.Dir(), "/meta/config.yml")); err != nil {
t.Error("Config file not created")
}
}
func TestCreateUserAddsPublicFolder(t *testing.T) { func TestCreateUserAddsPublicFolder(t *testing.T) {
// Create a new user // Create a new user
repo, _ := kiss.CreateRepository(testRepoName()) repo, _ := kiss.CreateRepository(testRepoName())
@ -157,3 +166,24 @@ func TestNewRepoGetsStaticFiles(t *testing.T) {
t.Error("No static files found") t.Error("No static files found")
} }
} }
func TestNewRepoGetsBaseHtml(t *testing.T) {
// Create a new user
repo, _ := kiss.CreateRepository(testRepoName())
if _, err := os.Stat(path.Join(repo.Dir(), "/base.html")); err != nil {
t.Error("Base html file not found")
}
}
func TestCanGetRepoTemplate(t *testing.T) {
// Create a new user
repo, _ := kiss.CreateRepository(testRepoName())
// Get the user
template, err := repo.Template()
if err != nil {
t.Error("Error getting template: ", err.Error())
}
if template == "" {
t.Error("Template not returned")
}
}

47
user.go
View File

@ -7,6 +7,8 @@ import (
"path" "path"
"strings" "strings"
"time" "time"
"gopkg.in/yaml.v2"
) )
type User struct { type User struct {
@ -14,18 +16,32 @@ type User struct {
name string name string
} }
type UserConfig struct {
Title string `yaml:"title"`
SubTitle string `yaml:"subtitle"`
HeaderColor string `yaml:"header_color"`
}
func (user User) Dir() string { func (user User) Dir() string {
return path.Join(user.repo.UsersDir(), user.name) return path.Join(user.repo.UsersDir(), user.name)
} }
func (user User) Path() string { func (user User) Path() string {
return "/" + user.name return "/user/" + user.name
} }
func (user User) PostDir() string { func (user User) PostDir() string {
return path.Join(user.Dir(), "public") return path.Join(user.Dir(), "public")
} }
func (user User) MetaDir() string {
return path.Join(user.Dir(), "meta")
}
func (user User) ConfigFile() string {
return path.Join(user.MetaDir(), "config.yml")
}
func (user User) Name() string { func (user User) Name() string {
return user.name return user.name
} }
@ -78,6 +94,8 @@ func (user User) CreateNewPost(title string) (Post, error) {
// create post file // create post file
os.Mkdir(post_dir, 0755) os.Mkdir(post_dir, 0755)
os.WriteFile(post.ContentFile(), []byte(initial_content), 0644) os.WriteFile(post.ContentFile(), []byte(initial_content), 0644)
// create media dir
os.Mkdir(post.MediaDir(), 0755)
return post, nil return post, nil
} }
@ -90,3 +108,30 @@ func (user User) Template() (string, error) {
} }
return string(base_html), nil return string(base_html), nil
} }
func (user User) Config() (UserConfig, error) {
config_path := user.ConfigFile()
config_data, err := ioutil.ReadFile(config_path)
if err != nil {
return UserConfig{}, err
}
var meta UserConfig
err = yaml.Unmarshal(config_data, &meta)
if err != nil {
return UserConfig{}, err
}
return meta, nil
}
func (user User) SetConfig(new_config UserConfig) error {
config_path := user.ConfigFile()
config_data, err := yaml.Marshal(new_config)
if err != nil {
return err
}
err = ioutil.WriteFile(config_path, config_data, 0644)
if err != nil {
return err
}
return nil
}

View File

@ -24,6 +24,17 @@ func TestCreateNewPostCreatesEntryInPublic(t *testing.T) {
} }
} }
func TestCreateNewPostCreatesMediaDir(t *testing.T) {
// Create a new user
repo, _ := kiss.CreateRepository(testRepoName())
user, _ := repo.CreateUser(randomUserName())
// Create a new post
post, _ := user.CreateNewPost("testpost")
if _, err := os.Stat(post.MediaDir()); os.IsNotExist(err) {
t.Error("Media directory not created")
}
}
func TestCreateNewPostMultipleCalls(t *testing.T) { func TestCreateNewPostMultipleCalls(t *testing.T) {
// Create a new user // Create a new user
repo, _ := kiss.CreateRepository(testRepoName()) repo, _ := kiss.CreateRepository(testRepoName())