Finished
This commit is contained in:
parent
161cc95538
commit
f8ce4e3b48
43 changed files with 1614 additions and 21 deletions
46
.air.toml
Normal file
46
.air.toml
Normal file
|
@ -0,0 +1,46 @@
|
|||
root = "."
|
||||
testdata_dir = "testdata"
|
||||
tmp_dir = "tmp"
|
||||
|
||||
[build]
|
||||
args_bin = []
|
||||
bin = "./tmp/main"
|
||||
cmd = "go build -o ./tmp/main ."
|
||||
delay = 1000
|
||||
exclude_dir = ["assets", "tmp", "vendor", "testdata", "stylegen"]
|
||||
exclude_file = []
|
||||
exclude_regex = ["_test.go"]
|
||||
exclude_unchanged = false
|
||||
follow_symlink = false
|
||||
full_bin = ""
|
||||
include_dir = []
|
||||
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||
include_file = []
|
||||
kill_delay = "0s"
|
||||
log = "build-errors.log"
|
||||
poll = false
|
||||
poll_interval = 0
|
||||
post_cmd = []
|
||||
pre_cmd = []
|
||||
rerun = false
|
||||
rerun_delay = 500
|
||||
send_interrupt = false
|
||||
stop_on_error = false
|
||||
|
||||
[color]
|
||||
app = ""
|
||||
build = "yellow"
|
||||
main = "magenta"
|
||||
runner = "green"
|
||||
watcher = "cyan"
|
||||
|
||||
[log]
|
||||
main_only = false
|
||||
time = false
|
||||
|
||||
[misc]
|
||||
clean_on_exit = false
|
||||
|
||||
[screen]
|
||||
clear_on_rebuild = false
|
||||
keep_scroll = true
|
2
.env.example
Normal file
2
.env.example
Normal file
|
@ -0,0 +1,2 @@
|
|||
REDIS_HOST=""
|
||||
REDIS_PASSWORD=""
|
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
*.css linguist-vendored
|
||||
*.js linguist-vendored
|
15
.github/workflows/fly.yml
vendored
Normal file
15
.github/workflows/fly.yml
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
name: Fly Deploy
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy app
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: superfly/flyctl-actions/setup-flyctl@master
|
||||
- run: flyctl deploy --remote-only
|
||||
env:
|
||||
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
|
27
.gitignore
vendored
27
.gitignore
vendored
|
@ -1,21 +1,6 @@
|
|||
# If you prefer the allow list template instead of the deny list, see community template:
|
||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||
#
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
node_modules/
|
||||
atri.dad
|
||||
.env
|
||||
airbin
|
||||
tmp/
|
||||
*.rdb
|
17
Dockerfile
Normal file
17
Dockerfile
Normal file
|
@ -0,0 +1,17 @@
|
|||
FROM golang:1.21.6 as build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN go mod download
|
||||
RUN CGO_ENABLED=0 go build -o /go/bin/app
|
||||
|
||||
FROM gcr.io/distroless/base-debian12
|
||||
|
||||
COPY --from=build /go/bin/app /
|
||||
COPY --from=build /app/content /content
|
||||
COPY --from=build /app/pages /pages
|
||||
COPY --from=build /app/public /public
|
||||
|
||||
CMD [ "/app" ]
|
11
api/ping.go
Normal file
11
api/ping.go
Normal file
|
@ -0,0 +1,11 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func Ping(c echo.Context) error {
|
||||
return c.String(http.StatusOK, "Pong!")
|
||||
}
|
58
api/sse.go
Normal file
58
api/sse.go
Normal file
|
@ -0,0 +1,58 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"goth.stack/lib"
|
||||
)
|
||||
|
||||
func SSEDemo(c echo.Context) error {
|
||||
channel := c.QueryParam("channel")
|
||||
if channel == "" {
|
||||
channel = "default"
|
||||
}
|
||||
|
||||
// Use the request context, which is cancelled when the client disconnects
|
||||
ctx := c.Request().Context()
|
||||
|
||||
pubsub, _ := lib.Subscribe(lib.RedisClient, channel)
|
||||
|
||||
c.Response().Header().Set(echo.HeaderContentType, "text/event-stream")
|
||||
c.Response().Header().Set(echo.HeaderConnection, "keep-alive")
|
||||
c.Response().Header().Set(echo.HeaderCacheControl, "no-cache")
|
||||
|
||||
// Create a ticker that fires every 15 seconds
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// If the client has disconnected, stop the loop
|
||||
return nil
|
||||
case <-ticker.C:
|
||||
// Every 30 seconds, send a comment to keep the connection alive
|
||||
if _, err := c.Response().Write([]byte(": keep-alive\n\n")); err != nil {
|
||||
return err
|
||||
}
|
||||
c.Response().Flush()
|
||||
default:
|
||||
// Handle incoming messages as before
|
||||
msg, err := pubsub.ReceiveMessage(ctx)
|
||||
if err != nil {
|
||||
log.Printf("Failed to receive message: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
data := fmt.Sprintf("data: %s\n\n", msg.Payload)
|
||||
if _, err := c.Response().Write([]byte(data)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Response().Flush()
|
||||
}
|
||||
}
|
||||
}
|
37
api/ssedemosend.go
Normal file
37
api/ssedemosend.go
Normal file
|
@ -0,0 +1,37 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"goth.stack/lib"
|
||||
)
|
||||
|
||||
func SSEDemoSend(c echo.Context) error {
|
||||
channel := c.QueryParam("channel")
|
||||
if channel == "" {
|
||||
channel = "default"
|
||||
}
|
||||
|
||||
// Get message from query parameters, form value, or request body
|
||||
message := c.QueryParam("message")
|
||||
if message == "" {
|
||||
message = c.FormValue("message")
|
||||
if message == "" {
|
||||
var body map[string]string
|
||||
if err := c.Bind(&body); err != nil {
|
||||
return err
|
||||
}
|
||||
message = body["message"]
|
||||
}
|
||||
}
|
||||
|
||||
if message == "" {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "message parameter is required"})
|
||||
}
|
||||
|
||||
// Send message
|
||||
lib.SendSSE("default", message)
|
||||
|
||||
return c.JSON(http.StatusOK, map[string]string{"status": "message sent"})
|
||||
}
|
13
content/code-blocks.md
Normal file
13
content/code-blocks.md
Normal file
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
name: "Code Blocks"
|
||||
date: "April 20 1337"
|
||||
tags: ["article","demo"]
|
||||
---
|
||||
# Markdown is cool!
|
||||
|
||||
Here is an example of a code block!:
|
||||
```go
|
||||
func onePlusOne() int {
|
||||
return 1 + 1
|
||||
}
|
||||
```
|
7
content/welcome.md
Normal file
7
content/welcome.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
name: "Welcome!"
|
||||
date: "April 20 1337"
|
||||
tags: ["meta", "demo"]
|
||||
---
|
||||
|
||||
Hello and welcome to this demo of the GOTH Stack! This file is just regular markdown... feel free to edit it! **Thanks for stopping by**!
|
17
fly.toml
Normal file
17
fly.toml
Normal file
|
@ -0,0 +1,17 @@
|
|||
app = "goth-stack"
|
||||
primary_region = "sea"
|
||||
|
||||
[build]
|
||||
|
||||
[http_service]
|
||||
internal_port = 3000
|
||||
force_https = true
|
||||
auto_stop_machines = true
|
||||
auto_start_machines = true
|
||||
min_machines_running = 1
|
||||
processes = ["app"]
|
||||
|
||||
[[vm]]
|
||||
cpu_kind = "shared"
|
||||
cpus = 1
|
||||
memory_mb = 256
|
42
go.mod
Normal file
42
go.mod
Normal file
|
@ -0,0 +1,42 @@
|
|||
module goth.stack
|
||||
|
||||
go 1.21.6
|
||||
|
||||
require (
|
||||
github.com/alecthomas/chroma/v2 v2.12.0
|
||||
github.com/labstack/echo/v4 v4.11.4
|
||||
github.com/stretchr/testify v1.8.4
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/alecthomas/repr v0.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/dlclark/regexp2 v1.10.0 // indirect
|
||||
github.com/hexops/gotextdiff v1.0.3 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/alecthomas/assert/v2 v2.4.1
|
||||
github.com/go-redis/redismock/v9 v9.2.0
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/labstack/gommon v0.4.2 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/redis/go-redis/v9 v9.4.0
|
||||
github.com/resendlabs/resend-go v1.7.0
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
github.com/yuin/goldmark v1.6.0
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||
golang.org/x/crypto v0.18.0 // indirect
|
||||
golang.org/x/net v0.20.0 // indirect
|
||||
golang.org/x/sys v0.16.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
90
go.sum
Normal file
90
go.sum
Normal file
|
@ -0,0 +1,90 @@
|
|||
github.com/alecthomas/assert/v2 v2.4.1 h1:mwPZod/d35nlaCppr6sFP0rbCL05WH9fIo7lvsf47zo=
|
||||
github.com/alecthomas/assert/v2 v2.4.1/go.mod h1:fw5suVxB+wfYJ3291t0hRTqtGzFYdSwstnRQdaQx2DM=
|
||||
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
|
||||
github.com/alecthomas/chroma/v2 v2.12.0 h1:Wh8qLEgMMsN7mgyG8/qIpegky2Hvzr4By6gEF7cmWgw=
|
||||
github.com/alecthomas/chroma/v2 v2.12.0/go.mod h1:4TQu7gdfuPjSh76j78ietmqh9LiurGF0EpseFXdKMBw=
|
||||
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
|
||||
github.com/alecthomas/repr v0.3.0 h1:NeYzUPfjjlqHY4KtzgKJiWd6sVq2eNUPTi34PiFGjY8=
|
||||
github.com/alecthomas/repr v0.3.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
|
||||
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/go-redis/redismock/v9 v9.2.0 h1:ZrMYQeKPECZPjOj5u9eyOjg8Nnb0BS9lkVIZ6IpsKLw=
|
||||
github.com/go-redis/redismock/v9 v9.2.0/go.mod h1:18KHfGDK4Y6c2R0H38EUGWAdc7ZQS9gfYxc94k7rWT0=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8=
|
||||
github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8=
|
||||
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/gomega v1.25.0 h1:Vw7br2PCDYijJHSfBOWhov+8cAnUf8MfMaIOV323l6Y=
|
||||
github.com/onsi/gomega v1.25.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/redis/go-redis/v9 v9.4.0 h1:Yzoz33UZw9I/mFhx4MNrB6Fk+XHO1VukNcCa1+lwyKk=
|
||||
github.com/redis/go-redis/v9 v9.4.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
|
||||
github.com/resendlabs/resend-go v1.7.0 h1:DycOqSXtw2q7aB+Nt9DDJUDtaYcrNPGn1t5RFposas0=
|
||||
github.com/resendlabs/resend-go v1.7.0/go.mod h1:yip1STH7Bqfm4fD0So5HgyNbt5taG5Cplc4xXxETyLI=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68=
|
||||
github.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
|
||||
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
31
lib/email.go
Normal file
31
lib/email.go
Normal file
|
@ -0,0 +1,31 @@
|
|||
package lib
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/resendlabs/resend-go"
|
||||
)
|
||||
|
||||
var client *resend.Client
|
||||
|
||||
// init function
|
||||
func init() {
|
||||
client = resend.NewClient(os.Getenv("RESEND_API_KEY"))
|
||||
}
|
||||
|
||||
func SendEmail(to_email string, from_email string, from_name string, html string, subject string) {
|
||||
params := &resend.SendEmailRequest{
|
||||
From: from_name + "<" + from_email + ">",
|
||||
To: []string{to_email},
|
||||
Html: html,
|
||||
Subject: subject,
|
||||
}
|
||||
|
||||
sent, err := client.Emails.Send(params)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
return
|
||||
}
|
||||
fmt.Println(sent.Id)
|
||||
}
|
59
lib/markdown.go
Normal file
59
lib/markdown.go
Normal file
|
@ -0,0 +1,59 @@
|
|||
package lib
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func ExtractFrontMatter(file os.DirEntry, dir string) (CardLink, error) {
|
||||
f, err := os.Open(dir + file.Name())
|
||||
if err != nil {
|
||||
return CardLink{}, fmt.Errorf("failed to open file: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
scanner := bufio.NewScanner(f)
|
||||
var lines []string
|
||||
for scanner.Scan() {
|
||||
lines = append(lines, scanner.Text())
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return CardLink{}, fmt.Errorf("failed to read file: %w", err)
|
||||
}
|
||||
|
||||
content := strings.Join(lines, "\n")
|
||||
splitContent := strings.SplitN(content, "---", 3)
|
||||
if len(splitContent) < 3 {
|
||||
return CardLink{}, fmt.Errorf("invalid file format: %s", file.Name())
|
||||
}
|
||||
|
||||
frontMatter := CardLink{}
|
||||
if err := yaml.Unmarshal([]byte(splitContent[1]), &frontMatter); err != nil {
|
||||
return CardLink{}, fmt.Errorf("failed to unmarshal frontmatter: %w", err)
|
||||
}
|
||||
|
||||
md := goldmark.New(goldmark.WithExtensions())
|
||||
var buf bytes.Buffer
|
||||
if err := md.Convert([]byte(splitContent[2]), &buf); err != nil {
|
||||
return CardLink{}, fmt.Errorf("failed to convert markdown: %w", err)
|
||||
}
|
||||
|
||||
return frontMatter, nil
|
||||
}
|
||||
|
||||
func SplitFrontmatter(md []byte) (frontmatter []byte, content []byte, err error) {
|
||||
parts := bytes.SplitN(md, []byte("---"), 3)
|
||||
|
||||
if len(parts) < 3 {
|
||||
return nil, nil, errors.New("invalid or missing frontmatter")
|
||||
}
|
||||
|
||||
return parts[1], parts[2], nil
|
||||
}
|
57
lib/markdown_test.go
Normal file
57
lib/markdown_test.go
Normal file
|
@ -0,0 +1,57 @@
|
|||
package lib_test
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/alecthomas/assert/v2"
|
||||
"goth.stack/lib"
|
||||
)
|
||||
|
||||
func TestExtractFrontMatter(t *testing.T) {
|
||||
// Create a temporary file with some front matter
|
||||
tmpfile, err := os.CreateTemp("../content", "example.*.md")
|
||||
println(tmpfile.Name())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer os.Remove(tmpfile.Name()) // clean up
|
||||
|
||||
text := `---
|
||||
name: "Test Title"
|
||||
description: "Test Description"
|
||||
---
|
||||
|
||||
# Test Content
|
||||
|
||||
`
|
||||
if _, err := tmpfile.Write([]byte(text)); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err := tmpfile.Close(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Get the directory entry for the temporary file
|
||||
dirEntry, err := os.ReadDir(filepath.Dir(tmpfile.Name()))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var tmpFileEntry fs.DirEntry
|
||||
for _, entry := range dirEntry {
|
||||
if entry.Name() == filepath.Base(tmpfile.Name()) {
|
||||
tmpFileEntry = entry
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Now we can test ExtractFrontMatter
|
||||
frontMatter, err := lib.ExtractFrontMatter(tmpFileEntry, "../content/")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Test Title", frontMatter.Name)
|
||||
assert.Equal(t, "Test Description", frontMatter.Description)
|
||||
}
|
48
lib/redis.go
Normal file
48
lib/redis.go
Normal file
|
@ -0,0 +1,48 @@
|
|||
package lib
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
var ctx = context.Background()
|
||||
|
||||
var RedisClient *redis.Client
|
||||
|
||||
func NewClient() *redis.Client {
|
||||
godotenv.Load(".env")
|
||||
redis_host := os.Getenv("REDIS_HOST")
|
||||
redis_password := os.Getenv("REDIS_PASSWORD")
|
||||
|
||||
log.Printf("Connecting to Redis at %s", redis_host)
|
||||
return redis.NewClient(&redis.Options{
|
||||
Addr: redis_host,
|
||||
Password: redis_password,
|
||||
DB: 0,
|
||||
})
|
||||
}
|
||||
|
||||
func Publish(client *redis.Client, channel string, message string) error {
|
||||
if client == nil {
|
||||
client = NewClient()
|
||||
}
|
||||
|
||||
return client.Publish(ctx, channel, message).Err()
|
||||
}
|
||||
|
||||
func Subscribe(client *redis.Client, channel string) (*redis.PubSub, string) {
|
||||
if client == nil {
|
||||
client = NewClient()
|
||||
}
|
||||
|
||||
pubsub := client.Subscribe(ctx, channel)
|
||||
_, err := pubsub.Receive(ctx)
|
||||
if err != nil {
|
||||
log.Fatalf("Error receiving subscription: %v", err)
|
||||
}
|
||||
return pubsub, channel
|
||||
}
|
27
lib/redis_test.go
Normal file
27
lib/redis_test.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package lib_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/go-redis/redismock/v9"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"goth.stack/lib"
|
||||
)
|
||||
|
||||
func TestPublish(t *testing.T) {
|
||||
db, mock := redismock.NewClientMock()
|
||||
mock.ExpectPublish("mychannel", "mymessage").SetVal(1)
|
||||
|
||||
err := lib.Publish(db, "mychannel", "mymessage")
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, mock.ExpectationsWereMet())
|
||||
}
|
||||
|
||||
// Then you can check the channel name in your test
|
||||
func TestSubscribe(t *testing.T) {
|
||||
db, _ := redismock.NewClientMock()
|
||||
|
||||
pubsub, channel := lib.Subscribe(db, "mychannel")
|
||||
assert.NotNil(t, pubsub)
|
||||
assert.Equal(t, "mychannel", channel)
|
||||
}
|
20
lib/sse.go
Normal file
20
lib/sse.go
Normal file
|
@ -0,0 +1,20 @@
|
|||
package lib
|
||||
|
||||
func SendSSE(channel string, message string) error {
|
||||
// Create a channel to receive an error from the goroutine
|
||||
errCh := make(chan error, 1)
|
||||
|
||||
// Use a goroutine to send the message asynchronously
|
||||
go func() {
|
||||
err := Publish(RedisClient, channel, message)
|
||||
errCh <- err // Send the error to the channel
|
||||
}()
|
||||
|
||||
// Wait for the goroutine to finish and check for errors
|
||||
err := <-errCh
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
37
lib/types.go
Normal file
37
lib/types.go
Normal file
|
@ -0,0 +1,37 @@
|
|||
package lib
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
)
|
||||
|
||||
type IconLink struct {
|
||||
Name string
|
||||
Href string
|
||||
Icon template.HTML
|
||||
}
|
||||
|
||||
type CardLink struct {
|
||||
Name string
|
||||
Href string
|
||||
Description string
|
||||
Date string
|
||||
Tags []string
|
||||
Internal bool
|
||||
}
|
||||
|
||||
type Post struct {
|
||||
Content template.HTML
|
||||
Name string
|
||||
Date string
|
||||
Tags []string
|
||||
}
|
||||
type FrontMatter struct {
|
||||
Name string
|
||||
Date string
|
||||
Tags []string
|
||||
}
|
||||
|
||||
type PubSubMessage struct {
|
||||
Channel string `json:"channel"`
|
||||
Data string `json:"data"`
|
||||
}
|
63
main.go
Normal file
63
main.go
Normal file
|
@ -0,0 +1,63 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"text/template"
|
||||
|
||||
"goth.stack/api"
|
||||
"goth.stack/pages"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
)
|
||||
|
||||
// Template Type
|
||||
type Template struct {
|
||||
templates *template.Template
|
||||
}
|
||||
|
||||
// Template Render function
|
||||
func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
|
||||
return t.templates.ExecuteTemplate(w, name, data)
|
||||
}
|
||||
|
||||
func main() {
|
||||
godotenv.Load(".env")
|
||||
|
||||
// Initialize router
|
||||
e := echo.New()
|
||||
|
||||
// Middlewares
|
||||
e.Pre(middleware.RemoveTrailingSlash())
|
||||
e.Use(middleware.Logger())
|
||||
e.Use(middleware.RequestID())
|
||||
e.Use(middleware.Secure())
|
||||
e.Use(middleware.GzipWithConfig(middleware.GzipConfig{
|
||||
Level: 5,
|
||||
}))
|
||||
e.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(69)))
|
||||
|
||||
// Template Parsing
|
||||
t := &Template{
|
||||
templates: template.Must(template.ParseGlob("pages/**/*.html")),
|
||||
}
|
||||
|
||||
e.Renderer = t
|
||||
|
||||
// // Static server
|
||||
e.Static("/public", "public")
|
||||
|
||||
// Page routes
|
||||
e.GET("/", pages.Home)
|
||||
e.GET("/blog", pages.Blog)
|
||||
e.GET("/post/:post", pages.Post)
|
||||
e.GET("/sse", pages.SSEDemo)
|
||||
|
||||
// API Routes:
|
||||
e.GET("/api/ping", api.Ping)
|
||||
e.GET("/api/ssedemo", api.SSEDemo)
|
||||
e.POST("/api/sendsse", api.SSEDemoSend)
|
||||
|
||||
e.Logger.Fatal(e.Start(":3000"))
|
||||
}
|
75
pages/blog.go
Normal file
75
pages/blog.go
Normal file
|
@ -0,0 +1,75 @@
|
|||
package pages
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"goth.stack/lib"
|
||||
)
|
||||
|
||||
type BlogProps struct {
|
||||
Posts []lib.CardLink
|
||||
}
|
||||
|
||||
func Blog(c echo.Context) error {
|
||||
var posts []lib.CardLink
|
||||
|
||||
files, err := os.ReadDir("./content/")
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "There was an finding posts!")
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
frontMatter, err := lib.ExtractFrontMatter(file, "./content/")
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "There was an issue rendering the posts!")
|
||||
}
|
||||
|
||||
frontMatter.Href = "post/" + strings.TrimSuffix(file.Name(), ".md")
|
||||
frontMatter.Internal = true
|
||||
|
||||
posts = append(posts, frontMatter)
|
||||
}
|
||||
|
||||
const layout = "January 2 2006"
|
||||
|
||||
sort.Slice(posts, func(i, j int) bool {
|
||||
iDate, err := time.Parse(layout, posts[i].Date)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
jDate, err := time.Parse(layout, posts[j].Date)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
return iDate.Before(jDate)
|
||||
})
|
||||
|
||||
props := BlogProps{
|
||||
Posts: posts,
|
||||
}
|
||||
|
||||
templates := []string{
|
||||
"./pages/templates/layouts/base.html",
|
||||
"./pages/templates/partials/header.html",
|
||||
"./pages/templates/partials/navitems.html",
|
||||
"./pages/templates/partials/cardlinks.html",
|
||||
"./pages/templates/blog.html",
|
||||
}
|
||||
|
||||
ts, err := template.ParseFiles(templates...)
|
||||
if err != nil {
|
||||
log.Print(err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
return ts.ExecuteTemplate(c.Response().Writer, "base", props)
|
||||
}
|
78
pages/home.go
Normal file
78
pages/home.go
Normal file
|
@ -0,0 +1,78 @@
|
|||
package pages
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"log"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"goth.stack/lib"
|
||||
)
|
||||
|
||||
type HomeProps struct {
|
||||
Socials []lib.IconLink
|
||||
Tech []lib.IconLink
|
||||
ContractLink string
|
||||
ResumeURL string
|
||||
SupportLink string
|
||||
}
|
||||
|
||||
func Home(c echo.Context) error {
|
||||
socials := []lib.IconLink{
|
||||
{
|
||||
Name: "Email",
|
||||
Href: "mailto:me@atri.dad",
|
||||
Icon: template.HTML(`<svg xmlns="http://www.w3.org/2000/svg" height="32" width="32" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path d="M48 64C21.5 64 0 85.5 0 112c0 15.1 7.1 29.3 19.2 38.4L236.8 313.6c11.4 8.5 27 8.5 38.4 0L492.8 150.4c12.1-9.1 19.2-23.3 19.2-38.4c0-26.5-21.5-48-48-48H48zM0 176V384c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V176L294.4 339.2c-22.8 17.1-54 17.1-76.8 0L0 176z"/></svg>`),
|
||||
},
|
||||
{
|
||||
Name: "GitHub",
|
||||
Href: "https://github.com/atridadl/goth-stack",
|
||||
Icon: template.HTML(`<svg xmlns="http://www.w3.org/2000/svg" height="32" width="32" viewBox="0 0 496 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3 .3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5 .3-6.2 2.3zm44.2-1.7c-2.9 .7-4.9 2.6-4.6 4.9 .3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3 .7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3 .3 2.9 2.3 3.9 1.6 1 3.6 .7 4.3-.7 .7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3 .7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3 .7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"/></svg>`),
|
||||
},
|
||||
}
|
||||
|
||||
tech := []lib.IconLink{
|
||||
{
|
||||
Name: "Go",
|
||||
Href: "https://golang.org",
|
||||
Icon: template.HTML(`<svg xmlns="http://www.w3.org/2000/svg" role="img" height="32" width="32" viewBox="0 0 24 24"><title>Go</title><path d="M1.811 10.231c-.047 0-.058-.023-.035-.059l.246-.315c.023-.035.081-.058.128-.058h4.172c.046 0 .058.035.035.07l-.199.303c-.023.036-.082.07-.117.07zM.047 11.306c-.047 0-.059-.023-.035-.058l.245-.316c.023-.035.082-.058.129-.058h5.328c.047 0 .07.035.058.07l-.093.28c-.012.047-.058.07-.105.07zm2.828 1.075c-.047 0-.059-.035-.035-.07l.163-.292c.023-.035.07-.07.117-.07h2.337c.047 0 .07.035.07.082l-.023.28c0 .047-.047.082-.082.082zm12.129-2.36c-.736.187-1.239.327-1.963.514-.176.046-.187.058-.34-.117-.174-.199-.303-.327-.548-.444-.737-.362-1.45-.257-2.115.175-.795.514-1.204 1.274-1.192 2.22.011.935.654 1.706 1.577 1.835.795.105 1.46-.175 1.987-.77.105-.13.198-.27.315-.434H10.47c-.245 0-.304-.152-.222-.35.152-.362.432-.97.596-1.274a.315.315 0 01.292-.187h4.253c-.023.316-.023.631-.07.947a4.983 4.983 0 01-.958 2.29c-.841 1.11-1.94 1.8-3.33 1.986-1.145.152-2.209-.07-3.143-.77-.865-.655-1.356-1.52-1.484-2.595-.152-1.274.222-2.419.993-3.424.83-1.086 1.928-1.776 3.272-2.02 1.098-.2 2.15-.07 3.096.571.62.41 1.063.97 1.356 1.648.07.105.023.164-.117.2m3.868 6.461c-1.064-.024-2.034-.328-2.852-1.029a3.665 3.665 0 01-1.262-2.255c-.21-1.32.152-2.489.947-3.529.853-1.122 1.881-1.706 3.272-1.95 1.192-.21 2.314-.095 3.33.595.923.63 1.496 1.484 1.648 2.605.198 1.578-.257 2.863-1.344 3.962-.771.783-1.718 1.273-2.805 1.495-.315.06-.63.07-.934.106zm2.78-4.72c-.011-.153-.011-.27-.034-.387-.21-1.157-1.274-1.81-2.384-1.554-1.087.245-1.788.935-2.045 2.033-.21.912.234 1.835 1.075 2.21.643.28 1.285.244 1.905-.07.923-.48 1.425-1.228 1.484-2.233z"/></svg>`),
|
||||
},
|
||||
{
|
||||
Name: "Redis",
|
||||
Href: "https://redis.io",
|
||||
Icon: template.HTML(`<svg xmlns="http://www.w3.org/2000/svg" role="img" height="32" width="32" viewBox="0 0 24 24"><title>Redis</title><path d="M10.5 2.661l.54.997-1.797.644 2.409.218.748 1.246.467-1.121 2.077-.208-1.61-.613.426-1.017-1.578.519zm6.905 2.077L13.76 6.182l3.292 1.298.353-.146 3.293-1.298zm-10.51.312a2.97 1.153 0 0 0-2.97 1.152 2.97 1.153 0 0 0 2.97 1.153 2.97 1.153 0 0 0 2.97-1.153 2.97 1.153 0 0 0-2.97-1.152zM24 6.805s-8.983 4.278-10.395 4.953c-1.226.561-1.901.561-3.261.094C8.318 11.022 0 7.241 0 7.241v1.038c0 .24.332.499.966.8 1.277.613 8.34 3.677 9.45 4.206 1.112.53 1.9.54 3.313-.197 1.412-.738 8.049-3.905 9.326-4.57.654-.342.945-.602.945-.84zm-10.042.602L8.39 8.26l3.884 1.61zM24 10.637s-8.983 4.279-10.395 4.954c-1.226.56-1.901.56-3.261.093C8.318 14.854 0 11.074 0 11.074v1.038c0 .238.332.498.966.8 1.277.612 8.34 3.676 9.45 4.205 1.112.53 1.9.54 3.313-.197 1.412-.737 8.049-3.905 9.326-4.57.654-.332.945-.602.945-.84zm0 3.842l-10.395 4.954c-1.226.56-1.901.56-3.261.094C8.318 18.696 0 14.916 0 14.916v1.038c0 .239.332.499.966.8 1.277.613 8.34 3.676 9.45 4.206 1.112.53 1.9.54 3.313-.198 1.412-.737 8.049-3.904 9.326-4.569.654-.343.945-.613.945-.841z"/></svg>`),
|
||||
},
|
||||
{
|
||||
Name: "Fly.io",
|
||||
Href: "https://fly.io",
|
||||
Icon: template.HTML(`<svg xmlns="http://www.w3.org/2000/svg" height="32" width="32" viewBox="0 0 384 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path d="M197.8 427.8c12.9 11.7 33.7 33.3 33.2 50.7 0 .8-.1 1.6-.1 2.5-1.8 19.8-18.8 31.1-39.1 31-25-.1-39.9-16.8-38.7-35.8 1-16.2 20.5-36.7 32.4-47.6 2.3-2.1 2.7-2.7 5.6-3.6 3.4 0 3.9 .3 6.7 2.8zM331.9 67.3c-16.3-25.7-38.6-40.6-63.3-52.1C243.1 4.5 214-.2 192 0c-44.1 0-71.2 13.2-81.1 17.3C57.3 45.2 26.5 87.2 28 158.6c7.1 82.2 97 176 155.8 233.8 1.7 1.6 4.5 4.5 6.2 5.1l3.3 .1c2.1-.7 1.8-.5 3.5-2.1 52.3-49.2 140.7-145.8 155.9-215.7 7-39.2 3.1-72.5-20.8-112.5zM186.8 351.9c-28-51.1-65.2-130.7-69.3-189-3.4-47.5 11.4-131.2 69.3-136.7v325.7zM328.7 180c-16.4 56.8-77.3 128-118.9 170.3C237.6 298.4 275 217 277 158.4c1.6-45.9-9.8-105.8-48-131.4 88.8 18.3 115.5 98.1 99.7 153z"/></svg>`),
|
||||
},
|
||||
{
|
||||
Name: "Docker",
|
||||
Href: "https://docker.com",
|
||||
Icon: template.HTML(`<svg xmlns="http://www.w3.org/2000/svg" role="img" height="32" width="32" viewBox="0 0 24 24"><title>Docker</title><path d="M13.983 11.078h2.119a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.119a.185.185 0 00-.185.185v1.888c0 .102.083.185.185.185m-2.954-5.43h2.118a.186.186 0 00.186-.186V3.574a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m0 2.716h2.118a.187.187 0 00.186-.186V6.29a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.887c0 .102.082.185.185.186m-2.93 0h2.12a.186.186 0 00.184-.186V6.29a.185.185 0 00-.185-.185H8.1a.185.185 0 00-.185.185v1.887c0 .102.083.185.185.186m-2.964 0h2.119a.186.186 0 00.185-.186V6.29a.185.185 0 00-.185-.185H5.136a.186.186 0 00-.186.185v1.887c0 .102.084.185.186.186m5.893 2.715h2.118a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m-2.93 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.083.185.185.185m-2.964 0h2.119a.185.185 0 00.185-.185V9.006a.185.185 0 00-.184-.186h-2.12a.186.186 0 00-.186.186v1.887c0 .102.084.185.186.185m-2.92 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.082.185.185.185M23.763 9.89c-.065-.051-.672-.51-1.954-.51-.338.001-.676.03-1.01.087-.248-1.7-1.653-2.53-1.716-2.566l-.344-.199-.226.327c-.284.438-.49.922-.612 1.43-.23.97-.09 1.882.403 2.661-.595.332-1.55.413-1.744.42H.751a.751.751 0 00-.75.748 11.376 11.376 0 00.692 4.062c.545 1.428 1.355 2.48 2.41 3.124 1.18.723 3.1 1.137 5.275 1.137.983.003 1.963-.086 2.93-.266a12.248 12.248 0 003.823-1.389c.98-.567 1.86-1.288 2.61-2.136 1.252-1.418 1.998-2.997 2.553-4.4h.221c1.372 0 2.215-.549 2.68-1.009.309-.293.55-.65.707-1.046l.098-.288Z"/></svg>`),
|
||||
},
|
||||
}
|
||||
|
||||
props := HomeProps{
|
||||
Socials: socials,
|
||||
Tech: tech,
|
||||
ContractLink: "mailto:contract@atri.dad",
|
||||
ResumeURL: "https://srv.atri.dad/Atridad_Lahiji_Resume.pdf",
|
||||
SupportLink: "https://donate.stripe.com/8wMeVF25c78L0V2288",
|
||||
}
|
||||
|
||||
templates := []string{
|
||||
"./pages/templates/layouts/base.html",
|
||||
"./pages/templates/partials/header.html",
|
||||
"./pages/templates/partials/navitems.html",
|
||||
"./pages/templates/home.html",
|
||||
}
|
||||
|
||||
ts, err := template.ParseFiles(templates...)
|
||||
if err != nil {
|
||||
log.Print(err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
return ts.ExecuteTemplate(c.Response().Writer, "base", props)
|
||||
}
|
80
pages/post.go
Normal file
80
pages/post.go
Normal file
|
@ -0,0 +1,80 @@
|
|||
package pages
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/yuin/goldmark"
|
||||
highlighting "github.com/yuin/goldmark-highlighting/v2"
|
||||
"gopkg.in/yaml.v2"
|
||||
"goth.stack/lib"
|
||||
)
|
||||
|
||||
type PostProps struct {
|
||||
Content template.HTML
|
||||
Name string
|
||||
Date string
|
||||
Tags []string
|
||||
}
|
||||
|
||||
func Post(c echo.Context) error {
|
||||
postName := c.ParamValues()[0]
|
||||
|
||||
filePath := "content/" + postName + ".md"
|
||||
|
||||
md, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusNotFound, "This post does not exist!")
|
||||
}
|
||||
|
||||
frontmatterBytes, content, err := lib.SplitFrontmatter(md)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "There was an issue rendering this post!")
|
||||
}
|
||||
|
||||
var frontmatter lib.FrontMatter
|
||||
if err := yaml.Unmarshal(frontmatterBytes, &frontmatter); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "There was an issue rendering this post!")
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
markdown := goldmark.New(
|
||||
goldmark.WithExtensions(
|
||||
highlighting.NewHighlighting(
|
||||
highlighting.WithStyle("dracula"),
|
||||
highlighting.WithFormatOptions(
|
||||
chromahtml.WithLineNumbers(true),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
if err := markdown.Convert(content, &buf); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "There was an issue rendering this post!")
|
||||
}
|
||||
|
||||
props := PostProps{
|
||||
Content: template.HTML(buf.String()),
|
||||
Name: frontmatter.Name,
|
||||
Date: frontmatter.Date,
|
||||
Tags: frontmatter.Tags,
|
||||
}
|
||||
|
||||
templates := []string{
|
||||
"./pages/templates/layouts/post.html",
|
||||
"./pages/templates/partials/header.html",
|
||||
"./pages/templates/partials/navitems.html",
|
||||
"./pages/templates/post.html",
|
||||
}
|
||||
|
||||
ts, err := template.ParseFiles(templates...)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "There was an issue rendering this post!")
|
||||
}
|
||||
|
||||
return ts.ExecuteTemplate(c.Response().Writer, "post", props)
|
||||
}
|
25
pages/ssedemo.go
Normal file
25
pages/ssedemo.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
package pages
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"log"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func SSEDemo(c echo.Context) error {
|
||||
templates := []string{
|
||||
"./pages/templates/layouts/base.html",
|
||||
"./pages/templates/partials/header.html",
|
||||
"./pages/templates/partials/navitems.html",
|
||||
"./pages/templates/ssedemo.html",
|
||||
}
|
||||
|
||||
ts, err := template.ParseFiles(templates...)
|
||||
if err != nil {
|
||||
log.Print(err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
return ts.ExecuteTemplate(c.Response().Writer, "base", nil)
|
||||
}
|
18
pages/templates/blog.html
Normal file
18
pages/templates/blog.html
Normal file
|
@ -0,0 +1,18 @@
|
|||
{{define "title"}}
|
||||
GOTH // Blog
|
||||
{{end}}
|
||||
|
||||
{{define "headercontent"}}
|
||||
GOTH // Blog
|
||||
{{end}}
|
||||
|
||||
{{define "head"}}
|
||||
{{end}}
|
||||
|
||||
{{define "main"}}
|
||||
<section class="flex flex-row flex-wrap gap-2 justify-center align-middle">
|
||||
{{range .Posts}}
|
||||
{{template "cardlinks" .}}
|
||||
{{end}}
|
||||
</section>
|
||||
{{end}}
|
51
pages/templates/home.html
Normal file
51
pages/templates/home.html
Normal file
|
@ -0,0 +1,51 @@
|
|||
{{define "title"}}
|
||||
GOTH // Home
|
||||
{{end}}
|
||||
|
||||
{{define "headercontent"}}
|
||||
GOTH // Home
|
||||
{{end}}
|
||||
|
||||
{{define "head"}}
|
||||
{{end}}
|
||||
|
||||
{{define "main"}}
|
||||
<h1 class="text-4xl font-extrabold text-white sm:text-8xl">
|
||||
<span
|
||||
class="bg-gradient-to-r from-pink-500 to-blue-500 bg-clip-text text-transparent"
|
||||
>GOTH Stack</span
|
||||
>
|
||||
</h1>
|
||||
|
||||
<h2 class="text-2xl font-extrabold tracking-tight text-white sm:text-[2rem]">
|
||||
A full-stack template for building modern web applications with Go and HTMX.
|
||||
</h2>
|
||||
|
||||
<span>
|
||||
<h2 class="mb-2 text-xl text-white sm:text-[1.5rem]">Links:</h2>
|
||||
<div
|
||||
class="flex flex-row flex-wrap items-center justify-center gap-4 text-center"
|
||||
>
|
||||
{{range .Socials}}
|
||||
<a class="fill-white hover:fill-pink-500"
|
||||
href={{.Href}} target="_blank" rel="me" aria-label={{.Name}}>
|
||||
{{.Icon}}
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<h2 class="mb-2 text-xl text-white sm:text-[1.5rem]">Technologies Used:</h2>
|
||||
<div
|
||||
class="flex flex-row flex-wrap items-center justify-center gap-4 text-center"
|
||||
>
|
||||
{{range .Tech}}
|
||||
<a class="fill-white hover:fill-pink-500"
|
||||
href={{.Href}} target="_blank" rel="me" aria-label={{.Name}}>
|
||||
{{.Icon}}
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</span>
|
||||
{{end}}
|
22
pages/templates/layouts/base.html
Normal file
22
pages/templates/layouts/base.html
Normal file
|
@ -0,0 +1,22 @@
|
|||
{{define "base"}}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="night">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" href="/public/favicon.ico" />
|
||||
<title>{{template "title" .}}</title>
|
||||
<meta name="description" content="Just here for the vibes...">
|
||||
<link rel="stylesheet" href="/public/css/styles.css" />
|
||||
{{template "head" .}}
|
||||
</head>
|
||||
|
||||
<body class="block h-[100%]">
|
||||
{{template "header" .}}
|
||||
|
||||
<main class="container flex flex-col items-center justify-center gap-3 sm:gap-6 p-4 text-center mx-auto min-h-[calc(100%-64px)]">
|
||||
{{template "main" .}}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
43
pages/templates/layouts/post.html
Normal file
43
pages/templates/layouts/post.html
Normal file
|
@ -0,0 +1,43 @@
|
|||
{{define "post"}}
|
||||
<html lang="en" data-theme="night">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" href="/public/favicon.ico" />
|
||||
<title>{{.Name}}</title>
|
||||
<meta name="description" content="Just here for the vibes...">
|
||||
<link rel="stylesheet" href="/public/css/styles.css" />
|
||||
</head>
|
||||
|
||||
<body class="block h-[100%]">
|
||||
{{template "header" .}}
|
||||
|
||||
<main class="prose prose-invert mx-auto p-4">
|
||||
<article>
|
||||
<h1 class="title">{{.Name}}</h1>
|
||||
<div class="flex flex-row flex-wrap gap-4">
|
||||
{{if .Date}}
|
||||
<p>
|
||||
<div class="flex flex-row flex-wrap items-center gap-1 text-md">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-clock-4"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
||||
{{.Date}}
|
||||
</div>
|
||||
</p>
|
||||
{{end}}
|
||||
|
||||
{{if .Tags}}
|
||||
<div class="flex flex-row flex-wrap text-center items-center justify-center gap-1">
|
||||
{{range .Tags}}
|
||||
<div class="badge badge-accent">#{{.}}</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<hr />
|
||||
{{template "main" .}}
|
||||
|
||||
</article>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
45
pages/templates/partials/cardlinks.html
Normal file
45
pages/templates/partials/cardlinks.html
Normal file
|
@ -0,0 +1,45 @@
|
|||
{{define "cardlinks"}}
|
||||
<div class="card card-compact w-64 bg-secondary shadow-xl">
|
||||
<div class="card-body text-base-100 flex flex-col">
|
||||
<h2 class="card-title text-base-100">{{.Name}}</h2>
|
||||
|
||||
{{if .Description}}
|
||||
<p>{{.Description}}</p>
|
||||
{{end}}
|
||||
|
||||
{{if .Date}}
|
||||
<p>
|
||||
<div class="flex flex-row flex-wrap items-center gap-1 text-md">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-clock-4"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
||||
{{.Date}}
|
||||
</div>
|
||||
</p>
|
||||
{{end}}
|
||||
|
||||
{{if .Tags}}
|
||||
<div class="flex flex-row flex-wrap text-center items-center justify-center gap-1">
|
||||
{{range .Tags}}
|
||||
<div class="badge badge-accent">#{{.}}</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .Href}}
|
||||
<div class="card-actions justify-end">
|
||||
<a
|
||||
role="button"
|
||||
href={{.Href}}
|
||||
aria-label={{.Name}}
|
||||
class="btn btn-circle btn-base-100 text-primary hover:btn-accent hover:text-neutral"
|
||||
>
|
||||
{{if eq true .Internal}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-right"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>
|
||||
{{else}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
|
||||
{{end}}
|
||||
</a>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
20
pages/templates/partials/header.html
Normal file
20
pages/templates/partials/header.html
Normal file
|
@ -0,0 +1,20 @@
|
|||
{{define "header"}}
|
||||
<header class="navbar bg-base-100">
|
||||
<div class="navbar-start">
|
||||
<a class="btn btn-ghost normal-case text-lg sm:text-xl" href="/">{{template "headercontent".}}</a>
|
||||
</div>
|
||||
<div class="navbar-end z-50">
|
||||
<div class="dropdown dropdown-end">
|
||||
<label tabindex="0" class="btn btn-sm btn-ghost">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-menu"><line x1="4" x2="20" y1="12" y2="12"/><line x1="4" x2="20" y1="6" y2="6"/><line x1="4" x2="20" y1="18" y2="18"/></svg>
|
||||
</label>
|
||||
<ul
|
||||
tabindex="0"
|
||||
class="menu menu-compact dropdown-content gap-2 mt-3 p-2 shadow bg-base-100 rounded-box"
|
||||
>
|
||||
{{template "navitems" .}}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
{{end}}
|
17
pages/templates/partials/navitems.html
Normal file
17
pages/templates/partials/navitems.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
{{define "navitems"}}
|
||||
<li>
|
||||
<a class="no-underline" href="/">
|
||||
Home
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="no-underline" href="/ssedemo">
|
||||
SSE Demo
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="no-underline" href="/blog">
|
||||
Blog Demo
|
||||
</a>
|
||||
</li>
|
||||
{{end}}
|
14
pages/templates/post.html
Normal file
14
pages/templates/post.html
Normal file
|
@ -0,0 +1,14 @@
|
|||
{{define "title"}}
|
||||
GOTH // Post
|
||||
{{end}}
|
||||
|
||||
{{define "headercontent"}}
|
||||
GOTH // Post
|
||||
{{end}}
|
||||
|
||||
{{define "head"}}
|
||||
{{end}}
|
||||
|
||||
{{define "main"}}
|
||||
{{.Content}}
|
||||
{{end}}
|
29
pages/templates/ssedemo.html
Normal file
29
pages/templates/ssedemo.html
Normal file
|
@ -0,0 +1,29 @@
|
|||
{{define "title"}}GOTH // SSE{{end}}
|
||||
|
||||
{{define "headercontent"}}
|
||||
GOTH // SSE <div class="badge badge-accent">DEMO</div>
|
||||
{{end}}
|
||||
|
||||
{{define "head"}}
|
||||
<script src="/public/js/htmx.min.js"></script>
|
||||
<script src="/public/js/htmx.sse.js"></script>
|
||||
{{end}}
|
||||
|
||||
{{define "main"}}
|
||||
<h1 class="text-4xl">Server Sent Events</h1>
|
||||
<h2 class="text-xl">This page demonstrates the use of the <a href="https://htmx.org/extensions/sse/">HTMX SSE Extention</a> to receive Server Sent Events on the "default" channel.</h2>
|
||||
<p class="text-lg">Any events received on the "default" channel will appear below:</p>
|
||||
<div hx-ext="sse" sse-connect="/api/ssedemo" sse-swap="message">
|
||||
Waiting for SSE Message...
|
||||
</div>
|
||||
|
||||
<p class="text-lg">Here you can send messages on the default channel:</p>
|
||||
<form hx-post="/api/sendsse" hx-trigger="submit" hx-swap="none" class="flex-col flex gap-2">
|
||||
<div class="label">
|
||||
<span class="label-text">Message</span>
|
||||
</div>
|
||||
<input type="text" name="message" value="Hello world!" placeholder="Enter your message here" class="input input-bordered input-primary w-full max-w-xs" />
|
||||
|
||||
<button type="submit" class="btn btn-primary">Send Event</button>
|
||||
</form>
|
||||
{{end}}
|
1
public/css/styles.css
vendored
Normal file
1
public/css/styles.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
1
public/js/htmx.min.js
vendored
Normal file
1
public/js/htmx.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
355
public/js/htmx.sse.js
vendored
Normal file
355
public/js/htmx.sse.js
vendored
Normal file
|
@ -0,0 +1,355 @@
|
|||
/*
|
||||
Server Sent Events Extension
|
||||
============================
|
||||
This extension adds support for Server Sent Events to htmx. See /www/extensions/sse.md for usage instructions.
|
||||
|
||||
*/
|
||||
|
||||
(function() {
|
||||
|
||||
/** @type {import("../htmx").HtmxInternalApi} */
|
||||
var api;
|
||||
|
||||
htmx.defineExtension("sse", {
|
||||
|
||||
/**
|
||||
* Init saves the provided reference to the internal HTMX API.
|
||||
*
|
||||
* @param {import("../htmx").HtmxInternalApi} api
|
||||
* @returns void
|
||||
*/
|
||||
init: function(apiRef) {
|
||||
// store a reference to the internal API.
|
||||
api = apiRef;
|
||||
|
||||
// set a function in the public API for creating new EventSource objects
|
||||
if (htmx.createEventSource == undefined) {
|
||||
htmx.createEventSource = createEventSource;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* onEvent handles all events passed to this extension.
|
||||
*
|
||||
* @param {string} name
|
||||
* @param {Event} evt
|
||||
* @returns void
|
||||
*/
|
||||
onEvent: function(name, evt) {
|
||||
|
||||
switch (name) {
|
||||
|
||||
case "htmx:beforeCleanupElement":
|
||||
var internalData = api.getInternalData(evt.target)
|
||||
// Try to remove remove an EventSource when elements are removed
|
||||
if (internalData.sseEventSource) {
|
||||
internalData.sseEventSource.close();
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
// Try to create EventSources when elements are processed
|
||||
case "htmx:afterProcessNode":
|
||||
ensureEventSourceOnElement(evt.target);
|
||||
registerSSE(evt.target);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
///////////////////////////////////////////////
|
||||
// HELPER FUNCTIONS
|
||||
///////////////////////////////////////////////
|
||||
|
||||
|
||||
/**
|
||||
* createEventSource is the default method for creating new EventSource objects.
|
||||
* it is hoisted into htmx.config.createEventSource to be overridden by the user, if needed.
|
||||
*
|
||||
* @param {string} url
|
||||
* @returns EventSource
|
||||
*/
|
||||
function createEventSource(url) {
|
||||
return new EventSource(url, { withCredentials: true });
|
||||
}
|
||||
|
||||
function splitOnWhitespace(trigger) {
|
||||
return trigger.trim().split(/\s+/);
|
||||
}
|
||||
|
||||
function getLegacySSEURL(elt) {
|
||||
var legacySSEValue = api.getAttributeValue(elt, "hx-sse");
|
||||
if (legacySSEValue) {
|
||||
var values = splitOnWhitespace(legacySSEValue);
|
||||
for (var i = 0; i < values.length; i++) {
|
||||
var value = values[i].split(/:(.+)/);
|
||||
if (value[0] === "connect") {
|
||||
return value[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getLegacySSESwaps(elt) {
|
||||
var legacySSEValue = api.getAttributeValue(elt, "hx-sse");
|
||||
var returnArr = [];
|
||||
if (legacySSEValue != null) {
|
||||
var values = splitOnWhitespace(legacySSEValue);
|
||||
for (var i = 0; i < values.length; i++) {
|
||||
var value = values[i].split(/:(.+)/);
|
||||
if (value[0] === "swap") {
|
||||
returnArr.push(value[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return returnArr;
|
||||
}
|
||||
|
||||
/**
|
||||
* registerSSE looks for attributes that can contain sse events, right
|
||||
* now hx-trigger and sse-swap and adds listeners based on these attributes too
|
||||
* the closest event source
|
||||
*
|
||||
* @param {HTMLElement} elt
|
||||
*/
|
||||
function registerSSE(elt) {
|
||||
// Find closest existing event source
|
||||
var sourceElement = api.getClosestMatch(elt, hasEventSource);
|
||||
if (sourceElement == null) {
|
||||
// api.triggerErrorEvent(elt, "htmx:noSSESourceError")
|
||||
return null; // no eventsource in parentage, orphaned element
|
||||
}
|
||||
|
||||
// Set internalData and source
|
||||
var internalData = api.getInternalData(sourceElement);
|
||||
var source = internalData.sseEventSource;
|
||||
|
||||
// Add message handlers for every `sse-swap` attribute
|
||||
queryAttributeOnThisOrChildren(elt, "sse-swap").forEach(function(child) {
|
||||
|
||||
var sseSwapAttr = api.getAttributeValue(child, "sse-swap");
|
||||
if (sseSwapAttr) {
|
||||
var sseEventNames = sseSwapAttr.split(",");
|
||||
} else {
|
||||
var sseEventNames = getLegacySSESwaps(child);
|
||||
}
|
||||
|
||||
for (var i = 0; i < sseEventNames.length; i++) {
|
||||
var sseEventName = sseEventNames[i].trim();
|
||||
var listener = function(event) {
|
||||
|
||||
// If the source is missing then close SSE
|
||||
if (maybeCloseSSESource(sourceElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the body no longer contains the element, remove the listener
|
||||
if (!api.bodyContains(child)) {
|
||||
source.removeEventListener(sseEventName, listener);
|
||||
}
|
||||
|
||||
// swap the response into the DOM and trigger a notification
|
||||
swap(child, event.data);
|
||||
api.triggerEvent(elt, "htmx:sseMessage", event);
|
||||
};
|
||||
|
||||
// Register the new listener
|
||||
api.getInternalData(child).sseEventListener = listener;
|
||||
source.addEventListener(sseEventName, listener);
|
||||
}
|
||||
});
|
||||
|
||||
// Add message handlers for every `hx-trigger="sse:*"` attribute
|
||||
queryAttributeOnThisOrChildren(elt, "hx-trigger").forEach(function(child) {
|
||||
|
||||
var sseEventName = api.getAttributeValue(child, "hx-trigger");
|
||||
if (sseEventName == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only process hx-triggers for events with the "sse:" prefix
|
||||
if (sseEventName.slice(0, 4) != "sse:") {
|
||||
return;
|
||||
}
|
||||
|
||||
// remove the sse: prefix from here on out
|
||||
sseEventName = sseEventName.substr(4);
|
||||
|
||||
var listener = function() {
|
||||
if (maybeCloseSSESource(sourceElement)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!api.bodyContains(child)) {
|
||||
source.removeEventListener(sseEventName, listener);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ensureEventSourceOnElement creates a new EventSource connection on the provided element.
|
||||
* If a usable EventSource already exists, then it is returned. If not, then a new EventSource
|
||||
* is created and stored in the element's internalData.
|
||||
* @param {HTMLElement} elt
|
||||
* @param {number} retryCount
|
||||
* @returns {EventSource | null}
|
||||
*/
|
||||
function ensureEventSourceOnElement(elt, retryCount) {
|
||||
|
||||
if (elt == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// handle extension source creation attribute
|
||||
queryAttributeOnThisOrChildren(elt, "sse-connect").forEach(function(child) {
|
||||
var sseURL = api.getAttributeValue(child, "sse-connect");
|
||||
if (sseURL == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ensureEventSource(child, sseURL, retryCount);
|
||||
});
|
||||
|
||||
// handle legacy sse, remove for HTMX2
|
||||
queryAttributeOnThisOrChildren(elt, "hx-sse").forEach(function(child) {
|
||||
var sseURL = getLegacySSEURL(child);
|
||||
if (sseURL == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ensureEventSource(child, sseURL, retryCount);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function ensureEventSource(elt, url, retryCount) {
|
||||
var source = htmx.createEventSource(url);
|
||||
|
||||
source.onerror = function(err) {
|
||||
|
||||
// Log an error event
|
||||
api.triggerErrorEvent(elt, "htmx:sseError", { error: err, source: source });
|
||||
|
||||
// If parent no longer exists in the document, then clean up this EventSource
|
||||
if (maybeCloseSSESource(elt)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, try to reconnect the EventSource
|
||||
if (source.readyState === EventSource.CLOSED) {
|
||||
retryCount = retryCount || 0;
|
||||
var timeout = Math.random() * (2 ^ retryCount) * 500;
|
||||
window.setTimeout(function() {
|
||||
ensureEventSourceOnElement(elt, Math.min(7, retryCount + 1));
|
||||
}, timeout);
|
||||
}
|
||||
};
|
||||
|
||||
source.onopen = function(evt) {
|
||||
api.triggerEvent(elt, "htmx:sseOpen", { source: source });
|
||||
}
|
||||
|
||||
api.getInternalData(elt).sseEventSource = source;
|
||||
}
|
||||
|
||||
/**
|
||||
* maybeCloseSSESource confirms that the parent element still exists.
|
||||
* If not, then any associated SSE source is closed and the function returns true.
|
||||
*
|
||||
* @param {HTMLElement} elt
|
||||
* @returns boolean
|
||||
*/
|
||||
function maybeCloseSSESource(elt) {
|
||||
if (!api.bodyContains(elt)) {
|
||||
var source = api.getInternalData(elt).sseEventSource;
|
||||
if (source != undefined) {
|
||||
source.close();
|
||||
// source = null
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* queryAttributeOnThisOrChildren returns all nodes that contain the requested attributeName, INCLUDING THE PROVIDED ROOT ELEMENT.
|
||||
*
|
||||
* @param {HTMLElement} elt
|
||||
* @param {string} attributeName
|
||||
*/
|
||||
function queryAttributeOnThisOrChildren(elt, attributeName) {
|
||||
|
||||
var result = [];
|
||||
|
||||
// If the parent element also contains the requested attribute, then add it to the results too.
|
||||
if (api.hasAttribute(elt, attributeName)) {
|
||||
result.push(elt);
|
||||
}
|
||||
|
||||
// Search all child nodes that match the requested attribute
|
||||
elt.querySelectorAll("[" + attributeName + "], [data-" + attributeName + "]").forEach(function(node) {
|
||||
result.push(node);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} elt
|
||||
* @param {string} content
|
||||
*/
|
||||
function swap(elt, content) {
|
||||
|
||||
api.withExtensions(elt, function(extension) {
|
||||
content = extension.transformResponse(content, null, elt);
|
||||
});
|
||||
|
||||
var swapSpec = api.getSwapSpecification(elt);
|
||||
var target = api.getTarget(elt);
|
||||
var settleInfo = api.makeSettleInfo(elt);
|
||||
|
||||
api.selectAndSwap(swapSpec.swapStyle, target, elt, content, settleInfo);
|
||||
|
||||
settleInfo.elts.forEach(function(elt) {
|
||||
if (elt.classList) {
|
||||
elt.classList.add(htmx.config.settlingClass);
|
||||
}
|
||||
api.triggerEvent(elt, 'htmx:beforeSettle');
|
||||
});
|
||||
|
||||
// Handle settle tasks (with delay if requested)
|
||||
if (swapSpec.settleDelay > 0) {
|
||||
setTimeout(doSettle(settleInfo), swapSpec.settleDelay);
|
||||
} else {
|
||||
doSettle(settleInfo)();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* doSettle mirrors much of the functionality in htmx that
|
||||
* settles elements after their content has been swapped.
|
||||
* TODO: this should be published by htmx, and not duplicated here
|
||||
* @param {import("../htmx").HtmxSettleInfo} settleInfo
|
||||
* @returns () => void
|
||||
*/
|
||||
function doSettle(settleInfo) {
|
||||
|
||||
return function() {
|
||||
settleInfo.tasks.forEach(function(task) {
|
||||
task.call();
|
||||
});
|
||||
|
||||
settleInfo.elts.forEach(function(elt) {
|
||||
if (elt.classList) {
|
||||
elt.classList.remove(htmx.config.settlingClass);
|
||||
}
|
||||
api.triggerEvent(elt, 'htmx:afterSettle');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function hasEventSource(node) {
|
||||
return api.getInternalData(node).sseEventSource != null;
|
||||
}
|
||||
|
||||
})();
|
12
stylegen/base.css
vendored
Normal file
12
stylegen/base.css
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
html,
|
||||
container,
|
||||
body {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
position: fixed;
|
||||
}
|
BIN
stylegen/bun.lockb
Executable file
BIN
stylegen/bun.lockb
Executable file
Binary file not shown.
11
stylegen/package.json
Normal file
11
stylegen/package.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"scripts": {
|
||||
"gen": "tailwindcss build ./base.css -o ../public/css/styles.css --minify",
|
||||
"watch": "tailwindcss build ./base.css -o ../public/css/styles.css --watch --minify"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"daisyui": "^4.4.24",
|
||||
"tailwindcss": "^3.4.0"
|
||||
}
|
||||
}
|
12
stylegen/tailwind.config.js
vendored
Normal file
12
stylegen/tailwind.config.js
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ["../pages/**/*.{html,go}"],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
daisyui: {
|
||||
themes: ["night"],
|
||||
},
|
||||
plugins: [require("daisyui"), require('@tailwindcss/typography')],
|
||||
}
|
||||
|
Loading…
Add table
Reference in a new issue