Merge pull request #2 from CalebQ42/htmx
Complete transformation to htmx
This commit is contained in:
@@ -1 +1,2 @@
|
||||
darkstorm-server
|
||||
test.sh
|
||||
|
||||
@@ -1,28 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"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) {
|
||||
@@ -40,9 +22,13 @@ func latestBlogsHandle(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
var out string
|
||||
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, "", "")
|
||||
}
|
||||
}
|
||||
|
||||
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", "", "")
|
||||
return
|
||||
}
|
||||
sendContent(w, r, blogElement(r.Context(), bl), bl.Title, bl.Favicon)
|
||||
}
|
||||
|
||||
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)
|
||||
if r.Header.Get("Hx-Request") == "true" {
|
||||
w.Write([]byte(bl.HTMX(blogApp, r.Context())))
|
||||
} 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)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ func filesRequest(w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
pageContent = "<p>404 Not Found</p>"
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
// w.WriteHeader(http.StatusNotFound)
|
||||
} else {
|
||||
pageContent = "<p>Server error!</p>"
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
@@ -52,5 +52,9 @@ func filesRequest(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if r.Header.Get("Hx-Request") == "true" {
|
||||
w.Write([]byte(pageContent))
|
||||
} else {
|
||||
sendContent(w, r, pageContent, "Files", "")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ func (b *Backend) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodOptions {
|
||||
w.Header().Set("Access-Control-Allow-Methods", "*")
|
||||
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)
|
||||
|
||||
@@ -3,6 +3,7 @@ package blog
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -13,6 +14,12 @@ import (
|
||||
"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 {
|
||||
ID string `json:"id" bson:"_id"`
|
||||
Name string `json:"name" bson:"name"`
|
||||
@@ -20,6 +27,10 @@ type Author struct {
|
||||
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) {
|
||||
res := b.authCol.FindOne(ctx, bson.M{"_id": "caleb_gardner"})
|
||||
if res.Err() != nil {
|
||||
|
||||
@@ -3,6 +3,7 @@ package blog
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -15,6 +16,13 @@ import (
|
||||
"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 {
|
||||
ID string `json:"id" bson:"_id"`
|
||||
Author string `json:"author" bson:"author"`
|
||||
@@ -27,6 +35,30 @@ type Blog struct {
|
||||
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) {
|
||||
if !blog.StaticPage {
|
||||
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")
|
||||
return
|
||||
}
|
||||
if r.Header.Get("Hx-Request") == "true" {
|
||||
w.Write([]byte(blog.HTMX(b, r.Context())))
|
||||
} else {
|
||||
json.NewEncoder(w).Encode(blog)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BlogApp) createBlog(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -3,13 +3,24 @@ package blog
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/CalebQ42/darkstorm-server/internal/backend"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"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 {
|
||||
Title string `json:"_id" bson:"_id"`
|
||||
Order int `json:"order" bson:"order"`
|
||||
@@ -22,10 +33,80 @@ type PortfolioProject struct {
|
||||
} `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{}
|
||||
if languageFilter != "" {
|
||||
filter["language.language"] = languageFilter
|
||||
if techFilter != "" {
|
||||
filter = bson.M{"technologies": techFilter}
|
||||
}
|
||||
res, err := b.portfolioCol.Find(ctx, filter, options.Find().SetSort(bson.M{"order": 1}))
|
||||
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) {
|
||||
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 {
|
||||
backend.ReturnError(w, http.StatusInternalServerError, "internal", "Server Error")
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,23 +27,30 @@ var (
|
||||
back *backend.Backend
|
||||
blogApp *blog.BlogApp
|
||||
webRoot *string
|
||||
testing *bool
|
||||
)
|
||||
|
||||
func main() {
|
||||
mongoURL := flag.String("mongo", "", "Enables MongoDB usage for Darkstorm backend.")
|
||||
webRoot = flag.String("web-root", "", "Sets root directory of web server.")
|
||||
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()
|
||||
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")
|
||||
}
|
||||
if *mongoURL == "" || *webRoot == "" {
|
||||
log.Fatal("SPECIFY MONGO AND WEB-ROOT OR I WILL DIE, OH NO, THEY'RE COMING FOR ME.... **DEATH NOISES**")
|
||||
}
|
||||
if !*testing {
|
||||
go func() {
|
||||
log.Println("error redirecting http traffice:",
|
||||
http.ListenAndServe(":80", http.RedirectHandler("https://darkstorm.tech", http.StatusPermanentRedirect)))
|
||||
}()
|
||||
}
|
||||
mux := http.NewServeMux()
|
||||
setupMongo(*mongoURL)
|
||||
setupBackend(mux)
|
||||
@@ -52,11 +59,17 @@ func main() {
|
||||
Addr: *addr,
|
||||
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)
|
||||
}
|
||||
|
||||
func setupMongo(uri string) {
|
||||
if !*testing {
|
||||
mongoCert, err := tls.LoadX509KeyPair(filepath.Join(flag.Arg(0), "mongo.pem"), filepath.Join(flag.Arg(0), "key.pem"))
|
||||
if err != nil {
|
||||
log.Fatal("error loading mongo keys:", err)
|
||||
@@ -67,6 +80,13 @@ func setupMongo(uri string) {
|
||||
if err != nil {
|
||||
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) {
|
||||
@@ -78,16 +98,28 @@ func setupBackend(mux *http.ServeMux) {
|
||||
swassistant.NewSWBackend(mongoClient.Database("swassistant")),
|
||||
cdr.NewBackend(mongoClient.Database("cdr")),
|
||||
)
|
||||
if !*testing {
|
||||
back.AddCorsAddress("https://darkstorm.tech")
|
||||
} else {
|
||||
back.AddCorsAddress("*")
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatal("error setting up backend:", err)
|
||||
}
|
||||
if !*testing {
|
||||
mux.Handle("api.darkstorm.tech/", back)
|
||||
} else {
|
||||
go func() {
|
||||
http.ListenAndServe(":2323", back)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func setupWebsite(mux *http.ServeMux) {
|
||||
if !*testing {
|
||||
url, _ := url.Parse("https://localhost:30000")
|
||||
mux.Handle("rpg.darkstorm.tech/", httputil.NewSingleHostReverseProxy(url))
|
||||
}
|
||||
mux.HandleFunc("GET /files/{w...}", filesRequest)
|
||||
mux.HandleFunc("GET /portfolio", portfolioRequest)
|
||||
mux.HandleFunc("/", mainHandle)
|
||||
|
||||
+5
-52
@@ -1,69 +1,22 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"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) {
|
||||
selectedLang := r.URL.Query().Get("lang")
|
||||
proj, err := blogApp.Projects(r.Context(), selectedLang)
|
||||
selectedTech := r.URL.Query().Get("tech")
|
||||
proj, err := blogApp.Projects(r.Context(), selectedTech)
|
||||
if err != nil {
|
||||
log.Println("error getting portfolio projects:", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
sendContent(w, r, "Error getting portfolio", "", "")
|
||||
return
|
||||
}
|
||||
aboutMe := "<h1 class='about-me-header'>About Me</h1>"
|
||||
if me, err := blogApp.AboutMe(r.Context()); err != nil {
|
||||
aboutMe += "Error getting info about me :("
|
||||
if r.Header.Get("Hx-Request") == "true" {
|
||||
w.Write([]byte(proj.FullHTMX(r.Context(), blogApp, selectedTech)))
|
||||
} 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", "")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user