diff --git a/framework/h/attribute.go b/framework/h/attribute.go
index 309aca5..9232776 100644
--- a/framework/h/attribute.go
+++ b/framework/h/attribute.go
@@ -117,6 +117,14 @@ func Href(path string) Ren {
return Attribute("href", path)
}
+func Target(target string) Ren {
+ return Attribute("target", target)
+}
+
+func D(value string) Ren {
+ return Attribute("d", value)
+}
+
func Type(name string) Ren {
return Attribute("type", name)
}
diff --git a/framework/h/tag.go b/framework/h/tag.go
index 8d6de4d..59ace19 100644
--- a/framework/h/tag.go
+++ b/framework/h/tag.go
@@ -25,6 +25,14 @@ func (node *Element) AppendChildren(children ...Ren) *Element {
return node
}
+func Svg(children ...Ren) *Element {
+ return Tag("svg", children...)
+}
+
+func Path(children ...Ren) *Element {
+ return Tag("path", children...)
+}
+
func TextF(format string, args ...interface{}) *TextContent {
return Text(fmt.Sprintf(format, args...))
}
diff --git a/htmgo-site/internal/cache/simplecache.go b/htmgo-site/internal/cache/simplecache.go
new file mode 100644
index 0000000..2131299
--- /dev/null
+++ b/htmgo-site/internal/cache/simplecache.go
@@ -0,0 +1,57 @@
+package cache
+
+import (
+ "sync"
+ "time"
+)
+
+type SimpleCache struct {
+ data map[string]Entry
+ lock sync.RWMutex
+}
+
+type Entry struct {
+ Value any
+ Expiration time.Time
+}
+
+func NewSimpleCache() *SimpleCache {
+ return &SimpleCache{
+ data: make(map[string]Entry),
+ lock: sync.RWMutex{},
+ }
+}
+
+func (c *SimpleCache) Get(key string) (any, bool) {
+ c.lock.RLock()
+ defer c.lock.RUnlock()
+ entry, ok := c.data[key]
+ if !ok {
+ return nil, false
+ }
+ if entry.Expiration.Before(time.Now()) {
+ delete(c.data, key)
+ return nil, false
+ }
+ return entry.Value, true
+}
+
+func (c *SimpleCache) Set(key string, value any, expiration time.Duration) {
+ c.lock.Lock()
+ defer c.lock.Unlock()
+ c.data[key] = Entry{
+ Value: value,
+ Expiration: time.Now().Add(expiration),
+ }
+}
+
+func GetOrSet[T any](cache *SimpleCache, key string, expiration time.Duration, cb func() (T, bool)) T {
+ if val, ok := cache.Get(key); ok {
+ return val.(T)
+ }
+ value, should := cb()
+ if should {
+ cache.Set(key, value, expiration)
+ }
+ return value
+}
diff --git a/htmgo-site/internal/httpjson/client.go b/htmgo-site/internal/httpjson/client.go
new file mode 100644
index 0000000..4a36bbd
--- /dev/null
+++ b/htmgo-site/internal/httpjson/client.go
@@ -0,0 +1,27 @@
+package httpjson
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+)
+
+// Get sends a GET request and decodes the response JSON into a generic type T
+func Get[T any](url string) (*T, error) {
+ resp, err := http.Get(url)
+ if err != nil {
+ return nil, fmt.Errorf("failed to make GET request: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("received non-OK status code: %d", resp.StatusCode)
+ }
+
+ var result T
+ if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
+ return nil, fmt.Errorf("failed to decode response: %w", err)
+ }
+
+ return &result, nil
+}
diff --git a/htmgo-site/main.go b/htmgo-site/main.go
index 6dbd38f..50b0b30 100644
--- a/htmgo-site/main.go
+++ b/htmgo-site/main.go
@@ -4,6 +4,7 @@ import (
"github.com/maddalax/htmgo/framework/h"
"github.com/maddalax/htmgo/framework/service"
"htmgo-site/__htmgo"
+ "htmgo-site/internal/cache"
"htmgo-site/internal/markdown"
"io/fs"
"net/http"
@@ -15,6 +16,7 @@ func main() {
markdownAssets := GetMarkdownAssets()
service.Set(locator, service.Singleton, markdown.NewRenderer)
+ service.Set(locator, service.Singleton, cache.NewSimpleCache)
h.Start(h.AppOpts{
ServiceLocator: locator,
diff --git a/htmgo-site/pages/base/root.go b/htmgo-site/pages/base/root.go
index 352593d..04b79a6 100644
--- a/htmgo-site/pages/base/root.go
+++ b/htmgo-site/pages/base/root.go
@@ -8,7 +8,7 @@ import (
var Version = uuid.NewString()[0:6]
-func RootPage(children ...h.Ren) *h.Element {
+func RootPage(ctx *h.RequestContext, children ...h.Ren) *h.Element {
title := "htmgo"
description := "build simple and scalable systems with go + htmx"
@@ -26,9 +26,6 @@ func RootPage(children ...h.Ren) *h.Element {
h.Meta("og:description", description),
h.LinkWithVersion("/public/main.css", "stylesheet", Version),
h.ScriptWithVersion("/public/htmgo.js", Version),
- h.Raw(`
-
- `),
h.Style(`
html {
scroll-behavior: smooth;
@@ -37,7 +34,7 @@ func RootPage(children ...h.Ren) *h.Element {
),
h.Body(
h.Class("bg-stone-50 min-h-screen overflow-x-hidden"),
- partials.NavBar(false),
+ partials.NavBar(ctx, false),
h.Fragment(children...),
),
)
diff --git a/htmgo-site/pages/docs.go b/htmgo-site/pages/docs.go
index c770336..4e2b1f2 100644
--- a/htmgo-site/pages/docs.go
+++ b/htmgo-site/pages/docs.go
@@ -13,6 +13,7 @@ func DocsPage(ctx *h.RequestContext) *h.Page {
pages := dirwalk.WalkPages("md/docs", assets)
return h.NewPage(base.RootPage(
+ ctx,
h.Div(
h.Class("flex flex-col md:flex-row gap-4 justify-center mb-12"),
partials.DocSidebar(pages),
diff --git a/htmgo-site/pages/examples.go b/htmgo-site/pages/examples.go
index 9bb39eb..5bbf9c2 100644
--- a/htmgo-site/pages/examples.go
+++ b/htmgo-site/pages/examples.go
@@ -29,7 +29,7 @@ var examples = []Example{
func ExamplesPage(ctx *h.RequestContext) *h.Page {
return h.NewPage(
- base.RootPage(h.Div(
+ base.RootPage(ctx, h.Div(
h.Class("flex items-center justify-center"),
h.Div(
h.Class("w-full px-4 flex flex-col prose max-w-[95vw] md:max-w-3xl mt-6"),
diff --git a/htmgo-site/pages/index.go b/htmgo-site/pages/index.go
index 3ee3c5a..d60283d 100644
--- a/htmgo-site/pages/index.go
+++ b/htmgo-site/pages/index.go
@@ -7,7 +7,7 @@ import (
func IndexPage(ctx *h.RequestContext) *h.Page {
return h.NewPage(
- base.RootPage(h.Div(
+ base.RootPage(ctx, h.Div(
h.Class("flex items-center justify-center "),
h.Div(
h.Class("w-full px-4 flex flex-col prose md:max-w-3xl mt-6 mx-auto"),
diff --git a/htmgo-site/pages/time.go b/htmgo-site/pages/time.go
index d8a02c7..d6349dd 100644
--- a/htmgo-site/pages/time.go
+++ b/htmgo-site/pages/time.go
@@ -9,6 +9,7 @@ import (
func CurrentTimePage(ctx *h.RequestContext) *h.Page {
return h.NewPage(
base.RootPage(
+ ctx,
h.GetPartial(
partials.CurrentTimePartial,
"load, every 1s"),
diff --git a/htmgo-site/partials/navbar.go b/htmgo-site/partials/navbar.go
index b957eeb..cd6ea8a 100644
--- a/htmgo-site/partials/navbar.go
+++ b/htmgo-site/partials/navbar.go
@@ -2,6 +2,10 @@ package partials
import (
"github.com/maddalax/htmgo/framework/h"
+ "github.com/maddalax/htmgo/framework/service"
+ "htmgo-site/internal/cache"
+ "htmgo-site/internal/httpjson"
+ "time"
)
type NavItem struct {
@@ -12,7 +16,7 @@ type NavItem struct {
func ToggleNavbar(ctx *h.RequestContext) *h.Partial {
return h.SwapManyPartial(
ctx,
- MobileNav(h.GetQueryParam(ctx, "expanded") == "true"),
+ MobileNav(ctx, h.GetQueryParam(ctx, "expanded") == "true"),
)
}
@@ -21,26 +25,46 @@ var navItems = []NavItem{
{Name: "Examples", Url: "/examples"},
}
-func Star() *h.Element {
+func Star(ctx *h.RequestContext) *h.Element {
- return h.Div(
- h.Script("https://buttons.github.io/buttons.js"),
- h.Id("github-star"),
- h.Class("min-w-[100px]"),
- h.Raw(`
- Star
- `),
+ type Repo struct {
+ StarCount int `json:"stargazers_count"`
+ }
+
+ simpleCache := service.Get[cache.SimpleCache](ctx.ServiceLocator())
+ count := cache.GetOrSet(simpleCache, "starCount", 10*time.Minute, func() (int, bool) {
+ response, err := httpjson.Get[Repo]("https://api.github.com/repos/maddalax/htmgo")
+ if err != nil {
+ return 0, false
+ }
+ return response.StarCount, true
+ })
+
+ return h.A(
+ h.Href("https://github.com/maddalax/htmgo"),
+ h.Target("_blank"),
+ h.Class("inline-flex items-center rounded overflow-hidden shadow-sm"),
+ h.Div(
+ h.Class("flex items-center px-2 py-1 bg-gray-800 text-white text-sm font-semibold hover:bg-gray-700 transition"),
+ h.Svg(
+ h.Class("w-4 h-4 -mt-0.5 mr-0.5 fill-current text-white"),
+ h.Attribute("xmlns", "http://www.w3.org/2000/svg"),
+ h.Attribute("viewBox", "0 0 24 24"),
+ h.Attribute("fill", "currentColor"),
+ h.Path(
+ h.D("M12 17.27l5.18 3.05-1.64-5.68 4.46-3.87-5.88-.5L12 3.5l-2.12 6.77-5.88.5 4.46 3.87-1.64 5.68L12 17.27z"),
+ ),
+ ),
+ h.Text("Star"),
+ ),
+ h.If(count > 0, h.Div(
+ h.Class("flex items-center px-3 py-1 bg-black text-white text-sm font-semibold"),
+ h.Pf("%d", count),
+ )),
)
}
-func NavBar(expanded bool) *h.Element {
+func NavBar(ctx *h.RequestContext, expanded bool) *h.Element {
prelease := h.A(h.Class("bg-yellow-200 text-yellow-800 text-center p-2 flex items-center justify-center"),
h.Href("https://github.com/maddalax/htmgo/issues"),
h.Attribute("target", "_blank"),
@@ -72,10 +96,7 @@ func NavBar(expanded bool) *h.Element {
),
)
}),
- h.Div(
- h.Class("ml-2 hidden md:block min-w-[99px]"),
- Star(),
- ),
+ Star(ctx),
),
),
),
@@ -84,12 +105,12 @@ func NavBar(expanded bool) *h.Element {
return h.Div(
h.Id("navbar"),
prelease,
- MobileNav(expanded),
+ MobileNav(ctx, expanded),
desktopNav,
)
}
-func MobileNav(expanded bool) *h.Element {
+func MobileNav(ctx *h.RequestContext, expanded bool) *h.Element {
return h.Nav(
h.Id("mobile-nav"),
h.Class("block sm:hidden bg-neutral-100 border border-b-slate-300 p-4 md:p-3"),
@@ -107,7 +128,7 @@ func MobileNav(expanded bool) *h.Element {
)),
h.Div(
h.Class("flex items-center gap-3"),
- h.Div(h.Class("mt-1"), Star()),
+ h.Div(h.Class("mt-1"), Star(ctx)),
h.Button(
h.Boost(),