Complete transformation to htmx

This commit is contained in:
Caleb Gardner
2024-11-01 04:00:55 -05:00
parent 6965917e76
commit 1061a38c6a
9 changed files with 215 additions and 127 deletions
+1
View File
@@ -1 +1,2 @@
darkstorm-server darkstorm-server
test.sh
+8 -46
View File
@@ -1,28 +1,10 @@
package main package main
import ( import (
"context"
"fmt"
"log" "log"
"net/http" "net/http"
"time"
"github.com/CalebQ42/darkstorm-server/internal/backend" "github.com/CalebQ42/darkstorm-server/internal/backend"
"github.com/CalebQ42/darkstorm-server/internal/blog"
)
const (
blogTitle = "<h1 class='blog-title'><a href='https://darkstorm.tech/%v' onclick=\"return setToPath('%v')\" style='text-decoration:none'>%v</a></h1>"
blogAuthor = "<h4 class='blog-author'><i><b>By %v</b></i></h4>"
blogCreate = "<h5 class='blog-time'><i>Written on: %v</i></h5>"
blogMain = "<div class='blog'>%v</div>"
authorInfo = `
<h2 class='blog-author-info'>About the author:</h2>
<table><tr>
<td><img src="%v" alt="%v" class='author-pic'></td>
<td><h3 class="author-title">%v</h3>%v</td>
</tr></table>`
) )
func latestBlogsHandle(w http.ResponseWriter, r *http.Request) { func latestBlogsHandle(w http.ResponseWriter, r *http.Request) {
@@ -40,9 +22,13 @@ func latestBlogsHandle(w http.ResponseWriter, r *http.Request) {
} }
var out string var out string
for _, b := range latest { for _, b := range latest {
out += blogElement(r.Context(), b) out += b.HTMX(blogApp, r.Context())
} }
if r.Header.Get("Hx-Request") == "true" {
w.Write([]byte(out))
} else {
sendContent(w, r, out, "", "") sendContent(w, r, out, "", "")
}
} }
func blogHandle(w http.ResponseWriter, r *http.Request, blog string) { func blogHandle(w http.ResponseWriter, r *http.Request, blog string) {
@@ -58,33 +44,9 @@ func blogHandle(w http.ResponseWriter, r *http.Request, blog string) {
sendContent(w, r, "Error getting page", "", "") sendContent(w, r, "Error getting page", "", "")
return return
} }
sendContent(w, r, blogElement(r.Context(), bl), bl.Title, bl.Favicon) if r.Header.Get("Hx-Request") == "true" {
} w.Write([]byte(bl.HTMX(blogApp, r.Context())))
func blogElement(ctx context.Context, b *blog.Blog) (out string) {
if b.StaticPage {
return b.Blog
}
out = fmt.Sprintf(blogTitle, b.ID, b.ID, b.Title)
auth, err := blogApp.GetAuthor(ctx, b)
if err == nil {
out += fmt.Sprintf(blogAuthor, auth.Name)
} else { } else {
out += fmt.Sprintf(blogAuthor, "unknown") sendContent(w, r, bl.HTMX(blogApp, r.Context()), bl.Title, bl.Favicon)
} }
cTime := time.Unix(b.CreateTime, 0).Format(time.DateOnly)
if b.UpdateTime > b.CreateTime {
out += fmt.Sprintf(blogCreate, cTime+"; Last updated on: "+time.Unix(b.UpdateTime, 0).Format(time.DateOnly))
} else {
out += fmt.Sprintf(blogCreate, cTime)
}
out += fmt.Sprintf(blogMain, b.Blog)
if err == nil {
out += authorSection(auth)
}
return
}
func authorSection(a *blog.Author) string {
return fmt.Sprintf(authorInfo, a.PicURL, a.Name+"'s profile picture", a.Name, a.About)
} }
+5 -1
View File
@@ -21,7 +21,7 @@ func filesRequest(w http.ResponseWriter, r *http.Request) {
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
pageContent = "<p>404 Not Found</p>" pageContent = "<p>404 Not Found</p>"
w.WriteHeader(http.StatusNotFound) // w.WriteHeader(http.StatusNotFound)
} else { } else {
pageContent = "<p>Server error!</p>" pageContent = "<p>Server error!</p>"
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
@@ -52,5 +52,9 @@ func filesRequest(w http.ResponseWriter, r *http.Request) {
return return
} }
} }
if r.Header.Get("Hx-Request") == "true" {
w.Write([]byte(pageContent))
} else {
sendContent(w, r, pageContent, "Files", "") sendContent(w, r, pageContent, "Files", "")
}
} }
+1 -1
View File
@@ -104,7 +104,7 @@ func (b *Backend) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodOptions { if r.Method == http.MethodOptions {
w.Header().Set("Access-Control-Allow-Methods", "*") w.Header().Set("Access-Control-Allow-Methods", "*")
w.Header().Set("Access-Control-Allow-Credentials", "true") w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Allow-Headers", "Access-Control-Allow-Headers, Authorization, X-API-Key, Origin, Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers") w.Header().Set("Access-Control-Allow-Headers", "*")
} }
} }
b.m.ServeHTTP(w, r) b.m.ServeHTTP(w, r)
+11
View File
@@ -3,6 +3,7 @@ package blog
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"log" "log"
"net/http" "net/http"
"strconv" "strconv"
@@ -13,6 +14,12 @@ import (
"go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo"
) )
const authorInfo = `
<table><tr>
<td><img src="%v" alt="%v" class='author-pic'></td>
<td><h3 class="author-title">%v</h3>%v</td>
</tr></table>`
type Author struct { type Author struct {
ID string `json:"id" bson:"_id"` ID string `json:"id" bson:"_id"`
Name string `json:"name" bson:"name"` Name string `json:"name" bson:"name"`
@@ -20,6 +27,10 @@ type Author struct {
PicURL string `json:"picurl" bson:"picurl"` PicURL string `json:"picurl" bson:"picurl"`
} }
func (a Author) HTML() string {
return fmt.Sprintf(authorInfo, a.PicURL, a.Name+"'s profile picture", a.Name, a.About)
}
func (b *BlogApp) AboutMe(ctx context.Context) (*Author, error) { func (b *BlogApp) AboutMe(ctx context.Context) (*Author, error) {
res := b.authCol.FindOne(ctx, bson.M{"_id": "caleb_gardner"}) res := b.authCol.FindOne(ctx, bson.M{"_id": "caleb_gardner"})
if res.Err() != nil { if res.Err() != nil {
+36
View File
@@ -3,6 +3,7 @@ package blog
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"log" "log"
"net/http" "net/http"
"strconv" "strconv"
@@ -15,6 +16,13 @@ import (
"go.mongodb.org/mongo-driver/mongo/options" "go.mongodb.org/mongo-driver/mongo/options"
) )
const (
blogTitle = "<h1 class='blog-title'><a hx-push-url='true' hx-get='/%v' hx-target='#content' href='/%v' style='text-decoration:none'>%v</a></h1>"
blogAuthor = "<h4 class='blog-author'><i><b>By %v</b></i></h4>"
blogCreate = "<h5 class='blog-time'><i>Written on: %v</i></h5>"
blogMain = "<div class='blog'>%v</div>"
)
type Blog struct { type Blog struct {
ID string `json:"id" bson:"_id"` ID string `json:"id" bson:"_id"`
Author string `json:"author" bson:"author"` Author string `json:"author" bson:"author"`
@@ -27,6 +35,30 @@ type Blog struct {
UpdateTime int64 `json:"updateTime" bson:"updateTime"` UpdateTime int64 `json:"updateTime" bson:"updateTime"`
} }
func (b *Blog) HTMX(blogApp *BlogApp, ctx context.Context) string {
if b.StaticPage {
return b.Blog
}
out := fmt.Sprintf(blogTitle, b.ID, b.ID, b.Title)
auth, err := blogApp.GetAuthor(ctx, b)
if err == nil {
out += fmt.Sprintf(blogAuthor, auth.Name)
} else {
out += fmt.Sprintf(blogAuthor, "unknown")
}
cTime := time.Unix(b.CreateTime, 0).Format(time.DateOnly)
if b.UpdateTime > b.CreateTime {
out += fmt.Sprintf(blogCreate, cTime+"; Last updated on: "+time.Unix(b.UpdateTime, 0).Format(time.DateOnly))
} else {
out += fmt.Sprintf(blogCreate, cTime)
}
out += fmt.Sprintf(blogMain, b.Blog)
if err == nil {
out += "<h2 class='blog-author-info'>About the author:</h2>" + auth.HTML()
}
return out
}
func (b *BlogApp) ConvertBlog(blog *Blog) { func (b *BlogApp) ConvertBlog(blog *Blog) {
if !blog.StaticPage { if !blog.StaticPage {
blog.Blog = b.conv.HTMLConvert(blog.Blog) blog.Blog = b.conv.HTMLConvert(blog.Blog)
@@ -95,7 +127,11 @@ func (b *BlogApp) reqBlog(w http.ResponseWriter, r *http.Request) {
backend.ReturnError(w, http.StatusInternalServerError, "internal", "Server error") backend.ReturnError(w, http.StatusInternalServerError, "internal", "Server error")
return return
} }
if r.Header.Get("Hx-Request") == "true" {
w.Write([]byte(blog.HTMX(b, r.Context())))
} else {
json.NewEncoder(w).Encode(blog) json.NewEncoder(w).Encode(blog)
}
} }
func (b *BlogApp) createBlog(w http.ResponseWriter, r *http.Request) { func (b *BlogApp) createBlog(w http.ResponseWriter, r *http.Request) {
+93 -4
View File
@@ -3,13 +3,24 @@ package blog
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"net/http" "net/http"
"slices"
"strings"
"github.com/CalebQ42/darkstorm-server/internal/backend" "github.com/CalebQ42/darkstorm-server/internal/backend"
"go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo/options" "go.mongodb.org/mongo-driver/mongo/options"
) )
const (
portfolioTitle = "<h2 class='portfolio-title'>%v</h2>"
portfolioLink = "<p class='portfolio-link'><a href='%v'>%v</a>"
portfolioLanguage = "<p class='portfolio-language'><b>%v</b>: %v</p>"
portfolioTech = "<p class='portfolio-tech'><b>Tech: </b>%v</p>"
portfolioDesc = "<p class='portfolio-description'>%v</p>"
)
type PortfolioProject struct { type PortfolioProject struct {
Title string `json:"_id" bson:"_id"` Title string `json:"_id" bson:"_id"`
Order int `json:"order" bson:"order"` Order int `json:"order" bson:"order"`
@@ -22,10 +33,80 @@ type PortfolioProject struct {
} `json:"language" bson:"language"` } `json:"language" bson:"language"`
} }
func (b *BlogApp) Projects(ctx context.Context, languageFilter string) ([]PortfolioProject, error) { func (p PortfolioProject) HTMX() string {
out := fmt.Sprintf(portfolioTitle, p.Title)
out += fmt.Sprintf(portfolioLink, p.Repository, p.Repository)
for _, l := range p.Languages {
out += fmt.Sprintf(portfolioLanguage, l.Language, l.Dates)
}
out += fmt.Sprintf(portfolioTech, strings.Join(p.Technologies, ", "))
out += fmt.Sprintf(portfolioDesc, p.Description)
return out
}
type Portfolio []PortfolioProject
const (
portfolioSelector = `
<p>Tech Filter:
<select
id='techSelect'
name='tech'
hx-get='http://localhost:2323/blog/portfolio'
hx-target='#projects'>
%v
</select>
</p>`
portfolioSelectorOption = "<option value='%v'%v>%v</option>"
)
func (p Portfolio) FullHTMX(ctx context.Context, blogApp *BlogApp, selectedTech string) string {
aboutMe := "<h1 class='about-me-header'>About Me</h1>"
if me, err := blogApp.AboutMe(ctx); err != nil {
aboutMe += "Error getting info about me :("
} else {
aboutMe += me.HTML()
}
aboutMe += "<h1 class='my-projects-header' style='margin-bottom:15px'>My Projects</h1>"
tech := make(map[string]struct{})
for i := range p {
for _, t := range p[i].Technologies {
tech[t] = struct{}{}
}
}
techKeys := make([]string, 0, len(tech))
for k := range tech {
techKeys = append(techKeys, k)
}
slices.Sort(techKeys)
var out string
if selectedTech == "" {
out = fmt.Sprintf(portfolioSelectorOption, "", " selected=true", "All")
} else {
out = fmt.Sprintf(portfolioSelectorOption, "", "", "All")
}
for _, k := range techKeys {
if selectedTech == strings.ToLower(k) {
out += fmt.Sprintf(portfolioSelectorOption, k, " selected=true", k)
} else {
out += fmt.Sprintf(portfolioSelectorOption, k, "", k)
}
}
return aboutMe + fmt.Sprintf(portfolioSelector, out) + "<div id='projects'>" + p.HTMX() + "</div>"
}
func (p Portfolio) HTMX() string {
out := ""
for _, proj := range p {
out += proj.HTMX()
}
return out
}
func (b *BlogApp) Projects(ctx context.Context, techFilter string) (Portfolio, error) {
filter := bson.M{} filter := bson.M{}
if languageFilter != "" { if techFilter != "" {
filter["language.language"] = languageFilter filter = bson.M{"technologies": techFilter}
} }
res, err := b.portfolioCol.Find(ctx, filter, options.Find().SetSort(bson.M{"order": 1})) res, err := b.portfolioCol.Find(ctx, filter, options.Find().SetSort(bson.M{"order": 1}))
if err != nil { if err != nil {
@@ -37,10 +118,18 @@ func (b *BlogApp) Projects(ctx context.Context, languageFilter string) ([]Portfo
} }
func (b *BlogApp) reqPortfolio(w http.ResponseWriter, r *http.Request) { func (b *BlogApp) reqPortfolio(w http.ResponseWriter, r *http.Request) {
folio, err := b.Projects(r.Context(), r.URL.Query().Get("lang")) folio, err := b.Projects(r.Context(), r.URL.Query().Get("tech"))
if err != nil { if err != nil {
backend.ReturnError(w, http.StatusInternalServerError, "internal", "Server Error") backend.ReturnError(w, http.StatusInternalServerError, "internal", "Server Error")
return return
} }
if r.Header.Get("Hx-Request") == "true" {
if r.URL.Query().Has("tech") {
w.Write([]byte(folio.HTMX()))
} else {
w.Write([]byte(folio.FullHTMX(r.Context(), b, r.URL.Query().Get("tech"))))
}
} else {
json.NewEncoder(w).Encode(folio) json.NewEncoder(w).Encode(folio)
}
} }
+34 -2
View File
@@ -27,23 +27,30 @@ var (
back *backend.Backend back *backend.Backend
blogApp *blog.BlogApp blogApp *blog.BlogApp
webRoot *string webRoot *string
testing *bool
) )
func main() { func main() {
mongoURL := flag.String("mongo", "", "Enables MongoDB usage for Darkstorm backend.") mongoURL := flag.String("mongo", "", "Enables MongoDB usage for Darkstorm backend.")
webRoot = flag.String("web-root", "", "Sets root directory of web server.") webRoot = flag.String("web-root", "", "Sets root directory of web server.")
addr := flag.String("addr", ":443", "Set listen address. Defaults to \":443\"") addr := flag.String("addr", ":443", "Set listen address. Defaults to \":443\"")
testing = flag.Bool("testing", false, "Start in testing mode. If you don't know what this is, don't use it.")
flag.Parse() flag.Parse()
if flag.NArg() != 1 { if *testing {
*addr = ":4242"
}
if !*testing && flag.NArg() != 1 {
log.Fatal("You must specify key directory. ex: darkstorm-server /etc/web-keys") log.Fatal("You must specify key directory. ex: darkstorm-server /etc/web-keys")
} }
if *mongoURL == "" || *webRoot == "" { if *mongoURL == "" || *webRoot == "" {
log.Fatal("SPECIFY MONGO AND WEB-ROOT OR I WILL DIE, OH NO, THEY'RE COMING FOR ME.... **DEATH NOISES**") log.Fatal("SPECIFY MONGO AND WEB-ROOT OR I WILL DIE, OH NO, THEY'RE COMING FOR ME.... **DEATH NOISES**")
} }
if !*testing {
go func() { go func() {
log.Println("error redirecting http traffice:", log.Println("error redirecting http traffice:",
http.ListenAndServe(":80", http.RedirectHandler("https://darkstorm.tech", http.StatusPermanentRedirect))) http.ListenAndServe(":80", http.RedirectHandler("https://darkstorm.tech", http.StatusPermanentRedirect)))
}() }()
}
mux := http.NewServeMux() mux := http.NewServeMux()
setupMongo(*mongoURL) setupMongo(*mongoURL)
setupBackend(mux) setupBackend(mux)
@@ -52,11 +59,17 @@ func main() {
Addr: *addr, Addr: *addr,
Handler: mux, Handler: mux,
} }
err := serv.ListenAndServeTLS(filepath.Join(flag.Arg(0), "fullchain.pem"), filepath.Join(flag.Arg(0), "key.pem")) var err error
if *testing {
err = serv.ListenAndServe()
} else {
err = serv.ListenAndServeTLS(filepath.Join(flag.Arg(0), "fullchain.pem"), filepath.Join(flag.Arg(0), "key.pem"))
}
log.Println("webserver closed:", err) log.Println("webserver closed:", err)
} }
func setupMongo(uri string) { func setupMongo(uri string) {
if !*testing {
mongoCert, err := tls.LoadX509KeyPair(filepath.Join(flag.Arg(0), "mongo.pem"), filepath.Join(flag.Arg(0), "key.pem")) mongoCert, err := tls.LoadX509KeyPair(filepath.Join(flag.Arg(0), "mongo.pem"), filepath.Join(flag.Arg(0), "key.pem"))
if err != nil { if err != nil {
log.Fatal("error loading mongo keys:", err) log.Fatal("error loading mongo keys:", err)
@@ -67,6 +80,13 @@ func setupMongo(uri string) {
if err != nil { if err != nil {
log.Fatal("error connecting to mongo:", err) log.Fatal("error connecting to mongo:", err)
} }
} else {
var err error
mongoClient, err = mongo.Connect(context.Background(), options.Client().ApplyURI(uri))
if err != nil {
log.Fatal("error connecting to mongo:", err)
}
}
} }
func setupBackend(mux *http.ServeMux) { func setupBackend(mux *http.ServeMux) {
@@ -78,16 +98,28 @@ func setupBackend(mux *http.ServeMux) {
swassistant.NewSWBackend(mongoClient.Database("swassistant")), swassistant.NewSWBackend(mongoClient.Database("swassistant")),
cdr.NewBackend(mongoClient.Database("cdr")), cdr.NewBackend(mongoClient.Database("cdr")),
) )
if !*testing {
back.AddCorsAddress("https://darkstorm.tech") back.AddCorsAddress("https://darkstorm.tech")
} else {
back.AddCorsAddress("*")
}
if err != nil { if err != nil {
log.Fatal("error setting up backend:", err) log.Fatal("error setting up backend:", err)
} }
if !*testing {
mux.Handle("api.darkstorm.tech/", back) mux.Handle("api.darkstorm.tech/", back)
} else {
go func() {
http.ListenAndServe(":2323", back)
}()
}
} }
func setupWebsite(mux *http.ServeMux) { func setupWebsite(mux *http.ServeMux) {
if !*testing {
url, _ := url.Parse("https://localhost:30000") url, _ := url.Parse("https://localhost:30000")
mux.Handle("rpg.darkstorm.tech/", httputil.NewSingleHostReverseProxy(url)) mux.Handle("rpg.darkstorm.tech/", httputil.NewSingleHostReverseProxy(url))
}
mux.HandleFunc("GET /files/{w...}", filesRequest) mux.HandleFunc("GET /files/{w...}", filesRequest)
mux.HandleFunc("GET /portfolio", portfolioRequest) mux.HandleFunc("GET /portfolio", portfolioRequest)
mux.HandleFunc("/", mainHandle) mux.HandleFunc("/", mainHandle)
+5 -52
View File
@@ -1,69 +1,22 @@
package main package main
import ( import (
"fmt"
"log" "log"
"net/http" "net/http"
"slices"
"strings"
)
const (
portfolioSelector = "<p>Language Filter: <select id='langSelect' name='langSelect'>%v</select></p>"
portfolioSelectorOption = "<option value='%v'%v>%v</option>"
portfolioTitle = "<h2 class='portfolio-title'>%v</h2>"
portfolioLink = "<p class='portfolio-link'><a href='%v'>%v</a>"
portfolioLanguage = "<p class='portfolio-language'><b>%v</b>: %v</p>"
portfolioTech = "<p class='portfolio-tech'><b>Tech: </b>%v</p>"
portfolioDesc = "<p class='portfolio-description'>%v</p>"
) )
func portfolioRequest(w http.ResponseWriter, r *http.Request) { func portfolioRequest(w http.ResponseWriter, r *http.Request) {
selectedLang := r.URL.Query().Get("lang") selectedTech := r.URL.Query().Get("tech")
proj, err := blogApp.Projects(r.Context(), selectedLang) proj, err := blogApp.Projects(r.Context(), selectedTech)
if err != nil { if err != nil {
log.Println("error getting portfolio projects:", err) log.Println("error getting portfolio projects:", err)
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
sendContent(w, r, "Error getting portfolio", "", "") sendContent(w, r, "Error getting portfolio", "", "")
return return
} }
aboutMe := "<h1 class='about-me-header'>About Me</h1>" if r.Header.Get("Hx-Request") == "true" {
if me, err := blogApp.AboutMe(r.Context()); err != nil { w.Write([]byte(proj.FullHTMX(r.Context(), blogApp, selectedTech)))
aboutMe += "Error getting info about me :("
} else { } else {
aboutMe += authorSection(me) sendContent(w, r, proj.FullHTMX(r.Context(), blogApp, selectedTech), "Portfolio", "")
} }
aboutMe += "<h1 class='my-projects-header' style='margin-bottom:15px'>My Projects</h1>"
langs := make(map[string]struct{})
out := ""
for _, p := range proj {
out += fmt.Sprintf(portfolioTitle, p.Title)
out += fmt.Sprintf(portfolioLink, p.Repository, p.Repository)
for _, l := range p.Languages {
langs[l.Language] = struct{}{}
out += fmt.Sprintf(portfolioLanguage, l.Language, l.Dates)
}
out += fmt.Sprintf(portfolioTech, strings.Join(p.Technologies, ", "))
out += fmt.Sprintf(portfolioDesc, p.Description)
}
langKeys := make([]string, 0, len(langs))
for k := range langs {
langKeys = append(langKeys, k)
}
slices.Sort(langKeys)
var tmp string
if selectedLang == "" {
tmp = fmt.Sprintf(portfolioSelectorOption, "", " selected=true", "All")
} else {
tmp = fmt.Sprintf(portfolioSelectorOption, "", "", "All")
}
for _, k := range langKeys {
if selectedLang == strings.ToLower(k) {
tmp += fmt.Sprintf(portfolioSelectorOption, strings.ToLower(k), " selected=true", k)
} else {
tmp += fmt.Sprintf(portfolioSelectorOption, strings.ToLower(k), "", k)
}
}
out = aboutMe + fmt.Sprintf(portfolioSelector, tmp) + out
sendContent(w, r, out, "Portfolio", "")
} }