Compare commits
No commits in common. "master" and "v1.0.0" have entirely different histories.
281 changed files with 2324 additions and 12385 deletions
3
.github/workflows/release-chat-example.yml
vendored
3
.github/workflows/release-chat-example.yml
vendored
|
|
@ -5,6 +5,9 @@ on:
|
||||||
workflows: [ "Update HTMGO Framework Dependency" ] # The name of the first workflow
|
workflows: [ "Update HTMGO Framework Dependency" ] # The name of the first workflow
|
||||||
types:
|
types:
|
||||||
- completed
|
- completed
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
workflow_dispatch: # Trigger on manual workflow_dispatch
|
workflow_dispatch: # Trigger on manual workflow_dispatch
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
|
|
|
||||||
48
.github/workflows/release-ws-test.yml
vendored
48
.github/workflows/release-ws-test.yml
vendored
|
|
@ -1,48 +0,0 @@
|
||||||
name: Build and Deploy ws-test
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_run:
|
|
||||||
workflows: [ "Update HTMGO Framework Dependency" ] # The name of the first workflow
|
|
||||||
types:
|
|
||||||
- completed
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- ws-testing
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-push:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v2
|
|
||||||
|
|
||||||
- name: Log in to GitHub Container Registry
|
|
||||||
uses: docker/login-action@v2
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Get short commit hash
|
|
||||||
id: vars
|
|
||||||
run: echo "::set-output name=short_sha::$(echo $GITHUB_SHA | cut -c1-7)"
|
|
||||||
|
|
||||||
- name: Build Docker image
|
|
||||||
run: |
|
|
||||||
cd ./examples/ws-example && docker build -t ghcr.io/${{ github.repository_owner }}/ws-example:${{ steps.vars.outputs.short_sha }} .
|
|
||||||
|
|
||||||
- name: Tag as latest Docker image
|
|
||||||
run: |
|
|
||||||
docker tag ghcr.io/${{ github.repository_owner }}/ws-example:${{ steps.vars.outputs.short_sha }} ghcr.io/${{ github.repository_owner }}/ws-example:latest
|
|
||||||
|
|
||||||
- name: Log in to GitHub Container Registry
|
|
||||||
run: echo "${{ secrets.CR_PAT }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
|
|
||||||
|
|
||||||
- name: Push Docker image
|
|
||||||
run: |
|
|
||||||
docker push ghcr.io/${{ github.repository_owner }}/ws-example:latest
|
|
||||||
33
.github/workflows/run-cli-tests.yml
vendored
33
.github/workflows/run-cli-tests.yml
vendored
|
|
@ -1,33 +0,0 @@
|
||||||
name: CLI Tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- '**' # Runs on any pull request to any branch
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Set up Go
|
|
||||||
uses: actions/setup-go@v4
|
|
||||||
with:
|
|
||||||
go-version: '1.23' # Specify the Go version you need
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: cd ./cli/htmgo && go mod download
|
|
||||||
|
|
||||||
- name: Run Go tests
|
|
||||||
run: cd ./cli/htmgo/tasks/astgen && go test ./... -coverprofile=coverage.txt
|
|
||||||
|
|
||||||
- name: Upload results to Codecov
|
|
||||||
uses: codecov/codecov-action@v4
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
|
||||||
9
.github/workflows/run-framework-tests.yml
vendored
9
.github/workflows/run-framework-tests.yml
vendored
|
|
@ -3,7 +3,7 @@ name: Framework Tests
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- '**' # Runs on any branch push
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- '**' # Runs on any pull request to any branch
|
- '**' # Runs on any pull request to any branch
|
||||||
|
|
@ -25,9 +25,4 @@ jobs:
|
||||||
run: cd ./framework && go mod download
|
run: cd ./framework && go mod download
|
||||||
|
|
||||||
- name: Run Go tests
|
- name: Run Go tests
|
||||||
run: cd ./framework && go test ./... -coverprofile=coverage.txt
|
run: cd ./framework && go test ./...
|
||||||
|
|
||||||
- name: Upload results to Codecov
|
|
||||||
uses: codecov/codecov-action@v4
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
|
||||||
|
|
|
||||||
3
.github/workflows/update-framework-dep.yml
vendored
3
.github/workflows/update-framework-dep.yml
vendored
|
|
@ -6,8 +6,7 @@ on:
|
||||||
branches:
|
branches:
|
||||||
- master # Trigger on pushes to master
|
- master # Trigger on pushes to master
|
||||||
paths:
|
paths:
|
||||||
- 'framework/**'
|
- 'framework/**' # Trigger only if files in this directory change
|
||||||
- 'tools/html-to-htmgo/**'
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
update-htmgo-dep:
|
update-htmgo-dep:
|
||||||
|
|
|
||||||
|
|
@ -5,16 +5,8 @@
|
||||||
-------
|
-------
|
||||||
[](https://goreportcard.com/report/github.com/maddalax/htmgo)
|
[](https://goreportcard.com/report/github.com/maddalax/htmgo)
|
||||||

|

|
||||||
[](https://htmgo.dev/docs)
|
|
||||||
[](https://codecov.io/github/maddalax/htmgo)
|
|
||||||
[](https://htmgo.dev/discord)
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<sup>looking for a python version? check out: https://fastht.ml</sup>
|
|
||||||
|
|
||||||
**introduction:**
|
**introduction:**
|
||||||
|
|
||||||
htmgo is a lightweight pure go way to build interactive websites / web applications using go & htmx.
|
htmgo is a lightweight pure go way to build interactive websites / web applications using go & htmx.
|
||||||
|
|
|
||||||
|
|
@ -3,25 +3,13 @@ module github.com/maddalax/htmgo/cli/htmgo
|
||||||
go 1.23.0
|
go 1.23.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/dave/jennifer v1.7.1
|
||||||
github.com/fsnotify/fsnotify v1.7.0
|
github.com/fsnotify/fsnotify v1.7.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/maddalax/htmgo/framework v1.0.7-0.20250703190716-06f01b3d7c1b
|
|
||||||
github.com/maddalax/htmgo/tools/html-to-htmgo v0.0.0-20250703190716-06f01b3d7c1b
|
|
||||||
github.com/stretchr/testify v1.9.0
|
|
||||||
golang.org/x/mod v0.21.0
|
golang.org/x/mod v0.21.0
|
||||||
golang.org/x/sys v0.26.0
|
golang.org/x/net v0.29.0
|
||||||
|
golang.org/x/sys v0.25.0
|
||||||
golang.org/x/tools v0.25.0
|
golang.org/x/tools v0.25.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require github.com/bmatcuk/doublestar/v4 v4.7.1 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/bmatcuk/doublestar/v4 v4.7.1
|
|
||||||
github.com/go-chi/chi/v5 v5.1.0 // indirect
|
|
||||||
golang.org/x/net v0.30.0 // indirect
|
|
||||||
golang.org/x/text v0.19.0 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,16 @@
|
||||||
github.com/bmatcuk/doublestar/v4 v4.7.1 h1:fdDeAqgT47acgwd9bd9HxJRDmc9UAmPpc+2m0CXv75Q=
|
github.com/bmatcuk/doublestar/v4 v4.7.1 h1:fdDeAqgT47acgwd9bd9HxJRDmc9UAmPpc+2m0CXv75Q=
|
||||||
github.com/bmatcuk/doublestar/v4 v4.7.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
github.com/bmatcuk/doublestar/v4 v4.7.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/dave/jennifer v1.7.1 h1:B4jJJDHelWcDhlRQxWeo0Npa/pYKBLrirAQoTN45txo=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/dave/jennifer v1.7.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc=
|
||||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||||
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
|
|
||||||
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/maddalax/htmgo/framework v1.0.7-0.20250703190716-06f01b3d7c1b h1:m+xI+HBEQdie/Rs+mYI0HTFTMlYQSCv0l/siPDoywA4=
|
|
||||||
github.com/maddalax/htmgo/framework v1.0.7-0.20250703190716-06f01b3d7c1b/go.mod h1:NGGzWVXWksrQJ9kV9SGa/A1F1Bjsgc08cN7ZVb98RqY=
|
|
||||||
github.com/maddalax/htmgo/tools/html-to-htmgo v0.0.0-20250703190716-06f01b3d7c1b h1:jvfp35fig2TzBjAgw82fe8+7cvaLX9EbipZUlj8FDDY=
|
|
||||||
github.com/maddalax/htmgo/tools/html-to-htmgo v0.0.0-20250703190716-06f01b3d7c1b/go.mod h1:FraJsj3NRuLBQDk83ZVa+psbNRNLe+rajVtVhYMEme4=
|
|
||||||
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/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
|
||||||
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
||||||
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||||
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
|
||||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
|
||||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
|
||||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
|
||||||
golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE=
|
golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE=
|
||||||
golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg=
|
golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg=
|
||||||
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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,11 @@ package dirutil
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/bmatcuk/doublestar/v4"
|
"github.com/bmatcuk/doublestar/v4"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func matchesAny(patterns []string, path string) bool {
|
func matchesAny(patterns []string, path string) bool {
|
||||||
for _, pattern := range patterns {
|
for _, pattern := range patterns {
|
||||||
matched, err :=
|
matched, err := doublestar.Match(pattern, path)
|
||||||
doublestar.Match(strings.ReplaceAll(pattern, `\`, "/"), strings.ReplaceAll(path, `\`, "/"))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error matching pattern: %v\n", err)
|
fmt.Printf("Error matching pattern: %v\n", err)
|
||||||
return false
|
return false
|
||||||
|
|
|
||||||
|
|
@ -9,22 +9,20 @@ import (
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/tasks/copyassets"
|
"github.com/maddalax/htmgo/cli/htmgo/tasks/copyassets"
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/tasks/css"
|
"github.com/maddalax/htmgo/cli/htmgo/tasks/css"
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/tasks/downloadtemplate"
|
"github.com/maddalax/htmgo/cli/htmgo/tasks/downloadtemplate"
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/tasks/formatter"
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/tasks/process"
|
"github.com/maddalax/htmgo/cli/htmgo/tasks/process"
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/tasks/reloader"
|
"github.com/maddalax/htmgo/cli/htmgo/tasks/reloader"
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/tasks/run"
|
"github.com/maddalax/htmgo/cli/htmgo/tasks/run"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
const version = "1.0.6"
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
needsSignals := true
|
done := RegisterSignals()
|
||||||
|
|
||||||
commandMap := make(map[string]*flag.FlagSet)
|
commandMap := make(map[string]*flag.FlagSet)
|
||||||
commands := []string{"template", "run", "watch", "build", "setup", "css", "schema", "generate", "format", "version"}
|
commands := []string{"template", "run", "watch", "build", "setup", "css", "schema", "generate"}
|
||||||
|
|
||||||
for _, command := range commands {
|
for _, command := range commands {
|
||||||
commandMap[command] = flag.NewFlagSet(command, flag.ExitOnError)
|
commandMap[command] = flag.NewFlagSet(command, flag.ExitOnError)
|
||||||
|
|
@ -58,15 +56,6 @@ func main() {
|
||||||
slog.Debug("Running task:", slog.String("task", taskName))
|
slog.Debug("Running task:", slog.String("task", taskName))
|
||||||
slog.Debug("working dir:", slog.String("dir", process.GetWorkingDir()))
|
slog.Debug("working dir:", slog.String("dir", process.GetWorkingDir()))
|
||||||
|
|
||||||
if taskName == "format" {
|
|
||||||
needsSignals = false
|
|
||||||
}
|
|
||||||
|
|
||||||
done := make(chan bool, 1)
|
|
||||||
if needsSignals {
|
|
||||||
done = RegisterSignals()
|
|
||||||
}
|
|
||||||
|
|
||||||
if taskName == "watch" {
|
if taskName == "watch" {
|
||||||
fmt.Printf("Running in watch mode\n")
|
fmt.Printf("Running in watch mode\n")
|
||||||
os.Setenv("ENV", "development")
|
os.Setenv("ENV", "development")
|
||||||
|
|
@ -78,9 +67,21 @@ func main() {
|
||||||
fmt.Printf("Generating CSS...\n")
|
fmt.Printf("Generating CSS...\n")
|
||||||
css.GenerateCss(process.ExitOnError)
|
css.GenerateCss(process.ExitOnError)
|
||||||
|
|
||||||
// generate ast needs to be run after css generation
|
wg := sync.WaitGroup{}
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
astgen.GenAst(process.ExitOnError)
|
astgen.GenAst(process.ExitOnError)
|
||||||
|
}()
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
run.EntGenerate()
|
run.EntGenerate()
|
||||||
|
}()
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
fmt.Printf("Starting server...\n")
|
fmt.Printf("Starting server...\n")
|
||||||
process.KillAll()
|
process.KillAll()
|
||||||
|
|
@ -89,22 +90,7 @@ func main() {
|
||||||
}()
|
}()
|
||||||
startWatcher(reloader.OnFileChange)
|
startWatcher(reloader.OnFileChange)
|
||||||
} else {
|
} else {
|
||||||
if taskName == "version" {
|
if taskName == "schema" {
|
||||||
fmt.Printf("htmgo cli version %s\n", version)
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
if taskName == "format" {
|
|
||||||
if len(os.Args) < 3 {
|
|
||||||
fmt.Println(fmt.Sprintf("Usage: htmgo format <file>"))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
file := os.Args[2]
|
|
||||||
if file == "." {
|
|
||||||
formatter.FormatDir(process.GetWorkingDir())
|
|
||||||
} else {
|
|
||||||
formatter.FormatFile(os.Args[2])
|
|
||||||
}
|
|
||||||
} else if taskName == "schema" {
|
|
||||||
reader := bufio.NewReader(os.Stdin)
|
reader := bufio.NewReader(os.Stdin)
|
||||||
fmt.Print("Enter entity name:")
|
fmt.Print("Enter entity name:")
|
||||||
text, _ := reader.ReadString('\n')
|
text, _ := reader.ReadString('\n')
|
||||||
|
|
@ -118,10 +104,10 @@ func main() {
|
||||||
} else if taskName == "css" {
|
} else if taskName == "css" {
|
||||||
_ = css.GenerateCss(process.ExitOnError)
|
_ = css.GenerateCss(process.ExitOnError)
|
||||||
} else if taskName == "ast" {
|
} else if taskName == "ast" {
|
||||||
css.GenerateCss(process.ExitOnError)
|
|
||||||
_ = astgen.GenAst(process.ExitOnError)
|
_ = astgen.GenAst(process.ExitOnError)
|
||||||
} else if taskName == "run" {
|
} else if taskName == "run" {
|
||||||
run.MakeBuildable()
|
_ = astgen.GenAst(process.ExitOnError)
|
||||||
|
_ = css.GenerateCss(process.ExitOnError)
|
||||||
_ = run.Server(process.ExitOnError)
|
_ = run.Server(process.ExitOnError)
|
||||||
} else if taskName == "template" {
|
} else if taskName == "template" {
|
||||||
name := ""
|
name := ""
|
||||||
|
|
|
||||||
|
|
@ -2,21 +2,17 @@ package astgen
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/maddalax/htmgo/cli/htmgo/internal/dirutil"
|
||||||
|
"github.com/maddalax/htmgo/cli/htmgo/tasks/process"
|
||||||
|
"github.com/maddalax/htmgo/framework/h"
|
||||||
"go/ast"
|
"go/ast"
|
||||||
"go/parser"
|
"go/parser"
|
||||||
"go/token"
|
"go/token"
|
||||||
"io/fs"
|
"golang.org/x/mod/modfile"
|
||||||
"log/slog"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
|
||||||
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/internal/dirutil"
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/tasks/process"
|
|
||||||
"github.com/maddalax/htmgo/framework/h"
|
|
||||||
"golang.org/x/mod/modfile"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Page struct {
|
type Page struct {
|
||||||
|
|
@ -41,36 +37,6 @@ const ModuleName = "github.com/maddalax/htmgo/framework/h"
|
||||||
var PackageName = fmt.Sprintf("package %s", GeneratedDirName)
|
var PackageName = fmt.Sprintf("package %s", GeneratedDirName)
|
||||||
var GeneratedFileLine = fmt.Sprintf("// Package %s THIS FILE IS GENERATED. DO NOT EDIT.", GeneratedDirName)
|
var GeneratedFileLine = fmt.Sprintf("// Package %s THIS FILE IS GENERATED. DO NOT EDIT.", GeneratedDirName)
|
||||||
|
|
||||||
func toPascaleCase(input string) string {
|
|
||||||
words := strings.Split(input, "_")
|
|
||||||
for i := range words {
|
|
||||||
words[i] = strings.Title(strings.ToLower(words[i]))
|
|
||||||
}
|
|
||||||
return strings.Join(words, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
func isValidGoVariableName(name string) bool {
|
|
||||||
// Variable name must not be empty
|
|
||||||
if name == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// First character must be a letter or underscore
|
|
||||||
if !unicode.IsLetter(rune(name[0])) && name[0] != '_' {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// Remaining characters must be letters, digits, or underscores
|
|
||||||
for _, char := range name[1:] {
|
|
||||||
if !unicode.IsLetter(char) && !unicode.IsDigit(char) && char != '_' {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizePath(path string) string {
|
|
||||||
return strings.ReplaceAll(path, `\`, "/")
|
|
||||||
}
|
|
||||||
|
|
||||||
func sliceCommonPrefix(dir1, dir2 string) string {
|
func sliceCommonPrefix(dir1, dir2 string) string {
|
||||||
// Use filepath.Clean to normalize the paths
|
// Use filepath.Clean to normalize the paths
|
||||||
dir1 = filepath.Clean(dir1)
|
dir1 = filepath.Clean(dir1)
|
||||||
|
|
@ -96,35 +62,9 @@ func sliceCommonPrefix(dir1, dir2 string) string {
|
||||||
|
|
||||||
// Return the longer one
|
// Return the longer one
|
||||||
if len(slicedDir1) > len(slicedDir2) {
|
if len(slicedDir1) > len(slicedDir2) {
|
||||||
return normalizePath(slicedDir1)
|
return slicedDir1
|
||||||
}
|
}
|
||||||
return normalizePath(slicedDir2)
|
return slicedDir2
|
||||||
}
|
|
||||||
|
|
||||||
func hasOnlyReqContextParam(funcType *ast.FuncType) bool {
|
|
||||||
if len(funcType.Params.List) != 1 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if funcType.Params.List[0].Names == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if len(funcType.Params.List[0].Names) != 1 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
t := funcType.Params.List[0].Type
|
|
||||||
name, ok := t.(*ast.StarExpr)
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
selectorExpr, ok := name.X.(*ast.SelectorExpr)
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
ident, ok := selectorExpr.X.(*ast.Ident)
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return ident.Name == "h" && selectorExpr.Sel.Name == "RequestContext"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func findPublicFuncsReturningHPartial(dir string, predicate func(partial Partial) bool) ([]Partial, error) {
|
func findPublicFuncsReturningHPartial(dir string, predicate func(partial Partial) bool) ([]Partial, error) {
|
||||||
|
|
@ -163,11 +103,11 @@ func findPublicFuncsReturningHPartial(dir string, predicate func(partial Partial
|
||||||
if selectorExpr, ok := starExpr.X.(*ast.SelectorExpr); ok {
|
if selectorExpr, ok := starExpr.X.(*ast.SelectorExpr); ok {
|
||||||
// Check if the package name is 'h' and type is 'Partial'.
|
// Check if the package name is 'h' and type is 'Partial'.
|
||||||
if ident, ok := selectorExpr.X.(*ast.Ident); ok && ident.Name == "h" {
|
if ident, ok := selectorExpr.X.(*ast.Ident); ok && ident.Name == "h" {
|
||||||
if selectorExpr.Sel.Name == "Partial" && hasOnlyReqContextParam(funcDecl.Type) {
|
if selectorExpr.Sel.Name == "Partial" {
|
||||||
p := Partial{
|
p := Partial{
|
||||||
Package: node.Name.Name,
|
Package: node.Name.Name,
|
||||||
Path: normalizePath(sliceCommonPrefix(cwd, path)),
|
Path: sliceCommonPrefix(cwd, path),
|
||||||
Import: sliceCommonPrefix(cwd, normalizePath(filepath.Dir(path))),
|
Import: sliceCommonPrefix(cwd, strings.ReplaceAll(filepath.Dir(path), `\`, `/`)),
|
||||||
FuncName: funcDecl.Name.Name,
|
FuncName: funcDecl.Name.Name,
|
||||||
}
|
}
|
||||||
if predicate(p) {
|
if predicate(p) {
|
||||||
|
|
@ -230,11 +170,11 @@ func findPublicFuncsReturningHPage(dir string) ([]Page, error) {
|
||||||
if selectorExpr, ok := starExpr.X.(*ast.SelectorExpr); ok {
|
if selectorExpr, ok := starExpr.X.(*ast.SelectorExpr); ok {
|
||||||
// Check if the package name is 'h' and type is 'Partial'.
|
// Check if the package name is 'h' and type is 'Partial'.
|
||||||
if ident, ok := selectorExpr.X.(*ast.Ident); ok && ident.Name == "h" {
|
if ident, ok := selectorExpr.X.(*ast.Ident); ok && ident.Name == "h" {
|
||||||
if selectorExpr.Sel.Name == "Page" && hasOnlyReqContextParam(funcDecl.Type) {
|
if selectorExpr.Sel.Name == "Page" {
|
||||||
pages = append(pages, Page{
|
pages = append(pages, Page{
|
||||||
Package: node.Name.Name,
|
Package: node.Name.Name,
|
||||||
Import: normalizePath(filepath.Dir(path)),
|
Import: strings.ReplaceAll(filepath.Dir(path), `\`, `/`),
|
||||||
Path: normalizePath(path),
|
Path: path,
|
||||||
FuncName: funcDecl.Name.Name,
|
FuncName: funcDecl.Name.Name,
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
|
|
@ -260,34 +200,59 @@ func findPublicFuncsReturningHPage(dir string) ([]Page, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildGetPartialFromContext(builder *CodeBuilder, partials []Partial) {
|
func buildGetPartialFromContext(builder *CodeBuilder, partials []Partial) {
|
||||||
moduleName := GetModuleName()
|
fName := "GetPartialFromContext"
|
||||||
|
|
||||||
var routerHandlerMethod = func(path string, caller string) string {
|
body := `
|
||||||
return fmt.Sprintf(`
|
path := r.URL.Path
|
||||||
router.Handle("%s", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
`
|
||||||
|
|
||||||
|
if len(partials) == 0 {
|
||||||
|
body = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
moduleName := GetModuleName()
|
||||||
|
for _, f := range partials {
|
||||||
|
if f.FuncName == fName {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
caller := fmt.Sprintf("%s.%s", f.Package, f.FuncName)
|
||||||
|
path := fmt.Sprintf("/%s/%s.%s", moduleName, f.Import, f.FuncName)
|
||||||
|
|
||||||
|
body += fmt.Sprintf(`
|
||||||
|
if path == "%s" || path == "%s" {
|
||||||
cc := r.Context().Value(h.RequestContextKey).(*h.RequestContext)
|
cc := r.Context().Value(h.RequestContextKey).(*h.RequestContext)
|
||||||
partial := %s(cc)
|
return %s(cc)
|
||||||
|
}
|
||||||
|
`, f.FuncName, path, caller)
|
||||||
|
}
|
||||||
|
|
||||||
|
body += "return nil"
|
||||||
|
|
||||||
|
f := Function{
|
||||||
|
Name: fName,
|
||||||
|
Parameters: []NameType{
|
||||||
|
{Name: "r", Type: "*http.Request"},
|
||||||
|
},
|
||||||
|
Return: []ReturnType{
|
||||||
|
{Type: "*h.Partial"},
|
||||||
|
},
|
||||||
|
Body: body,
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Append(builder.BuildFunction(f))
|
||||||
|
|
||||||
|
registerFunction := fmt.Sprintf(`
|
||||||
|
func RegisterPartials(router *chi.Mux) {
|
||||||
|
router.Handle("/%s/partials*", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
partial := GetPartialFromContext(r)
|
||||||
if partial == nil {
|
if partial == nil {
|
||||||
w.WriteHeader(404)
|
w.WriteHeader(404)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
h.PartialView(w, partial)
|
h.PartialView(w, partial)
|
||||||
}))`, path, caller)
|
}))
|
||||||
}
|
}
|
||||||
|
`, moduleName)
|
||||||
handlerMethods := make([]string, 0)
|
|
||||||
|
|
||||||
for _, f := range partials {
|
|
||||||
caller := fmt.Sprintf("%s.%s", f.Package, f.FuncName)
|
|
||||||
path := fmt.Sprintf("/%s/%s.%s", moduleName, f.Import, f.FuncName)
|
|
||||||
handlerMethods = append(handlerMethods, routerHandlerMethod(path, caller))
|
|
||||||
}
|
|
||||||
|
|
||||||
registerFunction := fmt.Sprintf(`
|
|
||||||
func RegisterPartials(router *chi.Mux) {
|
|
||||||
%s
|
|
||||||
}
|
|
||||||
`, strings.Join(handlerMethods, "\n"))
|
|
||||||
|
|
||||||
builder.AppendLine(registerFunction)
|
builder.AppendLine(registerFunction)
|
||||||
}
|
}
|
||||||
|
|
@ -296,7 +261,7 @@ func writePartialsFile() {
|
||||||
config := dirutil.GetConfig()
|
config := dirutil.GetConfig()
|
||||||
|
|
||||||
cwd := process.GetWorkingDir()
|
cwd := process.GetWorkingDir()
|
||||||
partialPath := filepath.Join(cwd)
|
partialPath := filepath.Join(cwd, "partials")
|
||||||
partials, err := findPublicFuncsReturningHPartial(partialPath, func(partial Partial) bool {
|
partials, err := findPublicFuncsReturningHPartial(partialPath, func(partial Partial) bool {
|
||||||
return partial.FuncName != "GetPartialFromContext"
|
return partial.FuncName != "GetPartialFromContext"
|
||||||
})
|
})
|
||||||
|
|
@ -313,12 +278,9 @@ func writePartialsFile() {
|
||||||
builder := NewCodeBuilder(nil)
|
builder := NewCodeBuilder(nil)
|
||||||
builder.AppendLine(GeneratedFileLine)
|
builder.AppendLine(GeneratedFileLine)
|
||||||
builder.AppendLine(PackageName)
|
builder.AppendLine(PackageName)
|
||||||
builder.AddImport(ChiModuleName)
|
|
||||||
|
|
||||||
if len(partials) > 0 {
|
|
||||||
builder.AddImport(ModuleName)
|
builder.AddImport(ModuleName)
|
||||||
builder.AddImport(HttpModuleName)
|
builder.AddImport(HttpModuleName)
|
||||||
}
|
builder.AddImport(ChiModuleName)
|
||||||
|
|
||||||
moduleName := GetModuleName()
|
moduleName := GetModuleName()
|
||||||
for _, partial := range partials {
|
for _, partial := range partials {
|
||||||
|
|
@ -424,96 +386,9 @@ func writePagesFile() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeAssetsFile() {
|
|
||||||
cwd := process.GetWorkingDir()
|
|
||||||
config := dirutil.GetConfig()
|
|
||||||
|
|
||||||
slog.Debug("writing assets file", slog.String("cwd", cwd), slog.String("config", config.PublicAssetPath))
|
|
||||||
|
|
||||||
distAssets := filepath.Join(cwd, "assets", "dist")
|
|
||||||
hasAssets := false
|
|
||||||
|
|
||||||
builder := strings.Builder{}
|
|
||||||
|
|
||||||
builder.WriteString(`package assets`)
|
|
||||||
builder.WriteString("\n")
|
|
||||||
|
|
||||||
filepath.WalkDir(distAssets, func(path string, d fs.DirEntry, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if d.IsDir() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(d.Name(), ".") {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
path = strings.ReplaceAll(path, distAssets, "")
|
|
||||||
httpUrl := normalizePath(fmt.Sprintf("%s%s", config.PublicAssetPath, path))
|
|
||||||
|
|
||||||
path = normalizePath(path)
|
|
||||||
path = strings.ReplaceAll(path, "/", "_")
|
|
||||||
path = strings.ReplaceAll(path, "//", "_")
|
|
||||||
|
|
||||||
name := strings.ReplaceAll(path, ".", "_")
|
|
||||||
name = strings.ReplaceAll(name, "-", "_")
|
|
||||||
|
|
||||||
name = toPascaleCase(name)
|
|
||||||
|
|
||||||
if isValidGoVariableName(name) {
|
|
||||||
builder.WriteString(fmt.Sprintf(`const %s = "%s"`, name, httpUrl))
|
|
||||||
builder.WriteString("\n")
|
|
||||||
hasAssets = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
builder.WriteString("\n")
|
|
||||||
|
|
||||||
str := builder.String()
|
|
||||||
|
|
||||||
if hasAssets {
|
|
||||||
WriteFile(filepath.Join(GeneratedDirName, "assets", "assets-generated.go"), func(content *ast.File) string {
|
|
||||||
return str
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func HasModuleFile(path string) bool {
|
|
||||||
_, err := os.Stat(path)
|
|
||||||
return !os.IsNotExist(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func CheckPagesDirectory(path string) error {
|
|
||||||
pagesPath := filepath.Join(path, "pages")
|
|
||||||
_, err := os.Stat(pagesPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("The directory pages does not exist.")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetModuleName() string {
|
func GetModuleName() string {
|
||||||
wd := process.GetWorkingDir()
|
wd := process.GetWorkingDir()
|
||||||
modPath := filepath.Join(wd, "go.mod")
|
modPath := filepath.Join(wd, "go.mod")
|
||||||
|
|
||||||
if HasModuleFile(modPath) == false {
|
|
||||||
fmt.Fprintf(os.Stderr, "Module not found: go.mod file does not exist.")
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
checkDir := CheckPagesDirectory(wd)
|
|
||||||
if checkDir != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, checkDir.Error())
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
goModBytes, err := os.ReadFile(modPath)
|
goModBytes, err := os.ReadFile(modPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "error reading go.mod: %v\n", err)
|
fmt.Fprintf(os.Stderr, "error reading go.mod: %v\n", err)
|
||||||
|
|
@ -532,7 +407,6 @@ func GenAst(flags ...process.RunFlag) error {
|
||||||
}
|
}
|
||||||
writePartialsFile()
|
writePartialsFile()
|
||||||
writePagesFile()
|
writePagesFile()
|
||||||
writeAssetsFile()
|
|
||||||
|
|
||||||
WriteFile("__htmgo/setup-generated.go", func(content *ast.File) string {
|
WriteFile("__htmgo/setup-generated.go", func(content *ast.File) string {
|
||||||
|
|
||||||
|
|
|
||||||
82
cli/htmgo/tasks/astgen/map.go
Normal file
82
cli/htmgo/tasks/astgen/map.go
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
package astgen
|
||||||
|
|
||||||
|
// OrderedMap is a generic data structure that maintains the order of keys.
|
||||||
|
type OrderedMap[K comparable, V any] struct {
|
||||||
|
keys []K
|
||||||
|
values map[K]V
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entries returns the key-value pairs in the order they were added.
|
||||||
|
func (om *OrderedMap[K, V]) Entries() []struct {
|
||||||
|
Key K
|
||||||
|
Value V
|
||||||
|
} {
|
||||||
|
entries := make([]struct {
|
||||||
|
Key K
|
||||||
|
Value V
|
||||||
|
}, len(om.keys))
|
||||||
|
for i, key := range om.keys {
|
||||||
|
entries[i] = struct {
|
||||||
|
Key K
|
||||||
|
Value V
|
||||||
|
}{
|
||||||
|
Key: key,
|
||||||
|
Value: om.values[key],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOrderedMap creates a new OrderedMap.
|
||||||
|
func NewOrderedMap[K comparable, V any]() *OrderedMap[K, V] {
|
||||||
|
return &OrderedMap[K, V]{
|
||||||
|
keys: []K{},
|
||||||
|
values: make(map[K]V),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set adds or updates a key-value pair in the OrderedMap.
|
||||||
|
func (om *OrderedMap[K, V]) Set(key K, value V) {
|
||||||
|
// Check if the key already exists
|
||||||
|
if _, exists := om.values[key]; !exists {
|
||||||
|
om.keys = append(om.keys, key) // Append key to the keys slice if it's a new key
|
||||||
|
}
|
||||||
|
om.values[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get retrieves a value by key.
|
||||||
|
func (om *OrderedMap[K, V]) Get(key K) (V, bool) {
|
||||||
|
value, exists := om.values[key]
|
||||||
|
return value, exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keys returns the keys in the order they were added.
|
||||||
|
func (om *OrderedMap[K, V]) Keys() []K {
|
||||||
|
return om.keys
|
||||||
|
}
|
||||||
|
|
||||||
|
// Values returns the values in the order of their keys.
|
||||||
|
func (om *OrderedMap[K, V]) Values() []V {
|
||||||
|
values := make([]V, len(om.keys))
|
||||||
|
for i, key := range om.keys {
|
||||||
|
values[i] = om.values[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete removes a key-value pair from the OrderedMap.
|
||||||
|
func (om *OrderedMap[K, V]) Delete(key K) {
|
||||||
|
if _, exists := om.values[key]; exists {
|
||||||
|
// Remove the key from the map
|
||||||
|
delete(om.values, key)
|
||||||
|
|
||||||
|
// Remove the key from the keys slice
|
||||||
|
for i, k := range om.keys {
|
||||||
|
if k == key {
|
||||||
|
om.keys = append(om.keys[:i], om.keys[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
/assets/dist
|
|
||||||
tmp
|
|
||||||
node_modules
|
|
||||||
.idea
|
|
||||||
__htmgo
|
|
||||||
dist
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
//go:build !prod
|
|
||||||
// +build !prod
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"astgen-project-sample/internal/embedded"
|
|
||||||
"io/fs"
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetStaticAssets() fs.FS {
|
|
||||||
return embedded.NewOsFs()
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
//go:build prod
|
|
||||||
// +build prod
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"embed"
|
|
||||||
"io/fs"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed assets/dist/*
|
|
||||||
var staticAssets embed.FS
|
|
||||||
|
|
||||||
func GetStaticAssets() fs.FS {
|
|
||||||
return staticAssets
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
module astgen-project-sample
|
|
||||||
|
|
||||||
go 1.23.0
|
|
||||||
|
|
||||||
require github.com/maddalax/htmgo/framework v1.0.7-0.20250703190716-06f01b3d7c1b
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/go-chi/chi/v5 v5.1.0 // indirect
|
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
)
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
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/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
|
|
||||||
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/maddalax/htmgo/framework v1.0.7-0.20250703190716-06f01b3d7c1b h1:m+xI+HBEQdie/Rs+mYI0HTFTMlYQSCv0l/siPDoywA4=
|
|
||||||
github.com/maddalax/htmgo/framework v1.0.7-0.20250703190716-06f01b3d7c1b/go.mod h1:NGGzWVXWksrQJ9kV9SGa/A1F1Bjsgc08cN7ZVb98RqY=
|
|
||||||
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/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
|
||||||
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
|
|
||||||
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
|
|
||||||
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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
package embedded
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io/fs"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
type OsFs struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (receiver OsFs) Open(name string) (fs.File, error) {
|
|
||||||
return os.Open(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewOsFs() OsFs {
|
|
||||||
return OsFs{}
|
|
||||||
}
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"astgen-project-sample/__htmgo"
|
|
||||||
"fmt"
|
|
||||||
"github.com/maddalax/htmgo/framework/config"
|
|
||||||
"github.com/maddalax/htmgo/framework/h"
|
|
||||||
"github.com/maddalax/htmgo/framework/service"
|
|
||||||
"io/fs"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
locator := service.NewLocator()
|
|
||||||
cfg := config.Get()
|
|
||||||
|
|
||||||
h.Start(h.AppOpts{
|
|
||||||
ServiceLocator: locator,
|
|
||||||
LiveReload: true,
|
|
||||||
Register: func(app *h.App) {
|
|
||||||
sub, err := fs.Sub(GetStaticAssets(), "assets/dist")
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
http.FileServerFS(sub)
|
|
||||||
|
|
||||||
// change this in htmgo.yml (public_asset_path)
|
|
||||||
app.Router.Handle(fmt.Sprintf("%s/*", cfg.PublicAssetPath),
|
|
||||||
http.StripPrefix(cfg.PublicAssetPath, http.FileServerFS(sub)))
|
|
||||||
|
|
||||||
__htmgo.Register(app.Router)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
package pages
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/maddalax/htmgo/framework/h"
|
|
||||||
)
|
|
||||||
|
|
||||||
func IndexPage(ctx *h.RequestContext) *h.Page {
|
|
||||||
return RootPage(
|
|
||||||
h.Div(
|
|
||||||
h.Class("flex flex-col gap-4 items-center pt-24 min-h-screen bg-neutral-100"),
|
|
||||||
h.H3(
|
|
||||||
h.Id("intro-text"),
|
|
||||||
h.Text("hello htmgo"),
|
|
||||||
h.Class("text-5xl"),
|
|
||||||
),
|
|
||||||
h.Div(
|
|
||||||
h.Class("mt-3"),
|
|
||||||
),
|
|
||||||
h.Div(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPartial(ctx *h.RequestContext) *h.Partial {
|
|
||||||
return h.NewPartial(
|
|
||||||
h.Div(
|
|
||||||
h.Text("Hello World"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
package pages
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/maddalax/htmgo/framework/h"
|
|
||||||
)
|
|
||||||
|
|
||||||
func RootPage(children ...h.Ren) *h.Page {
|
|
||||||
title := "htmgo template"
|
|
||||||
description := "an example of the htmgo template"
|
|
||||||
author := "htmgo"
|
|
||||||
url := "https://htmgo.dev"
|
|
||||||
|
|
||||||
return h.NewPage(
|
|
||||||
h.Html(
|
|
||||||
h.HxExtensions(
|
|
||||||
h.BaseExtensions(),
|
|
||||||
),
|
|
||||||
h.Head(
|
|
||||||
h.Title(
|
|
||||||
h.Text(title),
|
|
||||||
),
|
|
||||||
h.Meta("viewport", "width=device-width, initial-scale=1"),
|
|
||||||
h.Meta("title", title),
|
|
||||||
h.Meta("charset", "utf-8"),
|
|
||||||
h.Meta("author", author),
|
|
||||||
h.Meta("description", description),
|
|
||||||
h.Meta("og:title", title),
|
|
||||||
h.Meta("og:url", url),
|
|
||||||
h.Link("canonical", url),
|
|
||||||
h.Meta("og:description", description),
|
|
||||||
),
|
|
||||||
h.Body(
|
|
||||||
h.Div(
|
|
||||||
h.Class("flex flex-col gap-2 bg-white h-full"),
|
|
||||||
h.Fragment(children...),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
package partials
|
|
||||||
|
|
||||||
import "github.com/maddalax/htmgo/framework/h"
|
|
||||||
|
|
||||||
func CountersPartial(ctx *h.RequestContext) *h.Partial {
|
|
||||||
return h.NewPartial(
|
|
||||||
h.Div(
|
|
||||||
h.Text("my counter"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func SwapFormError(ctx *h.RequestContext, error string) *h.Partial {
|
|
||||||
return h.SwapPartial(
|
|
||||||
ctx,
|
|
||||||
h.Div(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
package astgen
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/internal/dirutil"
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/tasks/process"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestAstGen(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
workingDir, err := filepath.Abs("./project-sample")
|
|
||||||
|
|
||||||
assert.NoError(t, err)
|
|
||||||
process.SetWorkingDir(workingDir)
|
|
||||||
assert.NoError(t, os.Chdir(workingDir))
|
|
||||||
|
|
||||||
err = dirutil.DeleteDir(filepath.Join(process.GetWorkingDir(), "__htmgo"))
|
|
||||||
assert.NoError(t, err)
|
|
||||||
err = process.Run(process.NewRawCommand("", "go build ."))
|
|
||||||
assert.Error(t, err)
|
|
||||||
err = GenAst()
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
// project was buildable after astgen, confirmed working
|
|
||||||
err = process.Run(process.NewRawCommand("server", "go run ."))
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}()
|
|
||||||
|
|
||||||
time.Sleep(time.Second * 1)
|
|
||||||
|
|
||||||
urls := []string{
|
|
||||||
"/astgen-project-sample/partials.CountersPartial",
|
|
||||||
"/",
|
|
||||||
"/astgen-project-sample/pages.TestPartial",
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
serverProcess := process.GetProcessByName("server")
|
|
||||||
assert.NotNil(t, serverProcess)
|
|
||||||
process.KillProcess(*serverProcess)
|
|
||||||
}()
|
|
||||||
|
|
||||||
wg := sync.WaitGroup{}
|
|
||||||
|
|
||||||
for _, url := range urls {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
// ensure we can get a 200 response on the partials
|
|
||||||
resp, e := http.Get(fmt.Sprintf("http://localhost:3000%s", url))
|
|
||||||
assert.NoError(t, e)
|
|
||||||
assert.Equal(t, http.StatusOK, resp.StatusCode, fmt.Sprintf("%s was not a 200 response", url))
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
@ -7,3 +7,16 @@ import (
|
||||||
func PanicF(format string, args ...interface{}) {
|
func PanicF(format string, args ...interface{}) {
|
||||||
panic(fmt.Sprintf(format, args...))
|
panic(fmt.Sprintf(format, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Unique[T any](slice []T, key func(item T) string) []T {
|
||||||
|
var result []T
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
for _, v := range slice {
|
||||||
|
k := key(v)
|
||||||
|
if _, ok := seen[k]; !ok {
|
||||||
|
seen[k] = true
|
||||||
|
result = append(result, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ func downloadTailwindCli() {
|
||||||
log.Fatal(fmt.Sprintf("Unsupported OS/ARCH: %s/%s", os, arch))
|
log.Fatal(fmt.Sprintf("Unsupported OS/ARCH: %s/%s", os, arch))
|
||||||
}
|
}
|
||||||
fileName := fmt.Sprintf(`tailwindcss-%s`, distro)
|
fileName := fmt.Sprintf(`tailwindcss-%s`, distro)
|
||||||
url := fmt.Sprintf(`https://github.com/tailwindlabs/tailwindcss/releases/download/v3.4.16/%s`, fileName)
|
url := fmt.Sprintf(`https://github.com/tailwindlabs/tailwindcss/releases/latest/download/%s`, fileName)
|
||||||
|
|
||||||
cmd := fmt.Sprintf(`curl -LO %s`, url)
|
cmd := fmt.Sprintf(`curl -LO %s`, url)
|
||||||
process.Run(process.NewRawCommand("tailwind-cli-download", cmd, process.ExitOnError))
|
process.Run(process.NewRawCommand("tailwind-cli-download", cmd, process.ExitOnError))
|
||||||
|
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
package formatter
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/maddalax/htmgo/tools/html-to-htmgo/htmltogo"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func FormatDir(dir string) {
|
|
||||||
files, err := os.ReadDir(dir)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("error reading dir: %s\n", err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, file := range files {
|
|
||||||
if file.IsDir() {
|
|
||||||
FormatDir(filepath.Join(dir, file.Name()))
|
|
||||||
} else {
|
|
||||||
FormatFile(filepath.Join(dir, file.Name()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func FormatFile(file string) {
|
|
||||||
if !strings.HasSuffix(file, ".go") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("formatting file: %s\n", file)
|
|
||||||
|
|
||||||
source, err := os.ReadFile(file)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("error reading file: %s\n", err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
str := string(source)
|
|
||||||
|
|
||||||
if !strings.Contains(str, "github.com/maddalax/htmgo/framework/h") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
parsed := htmltogo.Indent(str)
|
|
||||||
|
|
||||||
os.WriteFile(file, []byte(parsed), 0644)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
@ -12,10 +12,7 @@ func KillProcess(process CmdWithFlags) error {
|
||||||
if process.Cmd == nil || process.Cmd.Process == nil {
|
if process.Cmd == nil || process.Cmd.Process == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
err := exec.Command("taskkill", "/F", "/T", "/PID", strconv.Itoa(process.Cmd.Process.Pid)).Run()
|
Run(NewRawCommand("killprocess", fmt.Sprintf("taskkill /F /T /PID %s", strconv.Itoa(process.Cmd.Process.Pid))))
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
time.Sleep(time.Millisecond * 50)
|
time.Sleep(time.Millisecond * 50)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -115,7 +115,7 @@ func OnShutdown() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// give it a second
|
// give it a second
|
||||||
time.Sleep(time.Second * 1)
|
time.Sleep(time.Second * 2)
|
||||||
// force kill
|
// force kill
|
||||||
KillAll()
|
KillAll()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,23 +9,12 @@ import (
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
func MakeBuildable() {
|
|
||||||
copyassets.CopyAssets()
|
|
||||||
css.GenerateCss(process.ExitOnError)
|
|
||||||
astgen.GenAst(process.ExitOnError)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Build() {
|
func Build() {
|
||||||
MakeBuildable()
|
copyassets.CopyAssets()
|
||||||
|
astgen.GenAst(process.ExitOnError)
|
||||||
|
css.GenerateCss(process.ExitOnError)
|
||||||
|
|
||||||
_ = os.RemoveAll("./dist")
|
process.RunOrExit(process.NewRawCommand("", "mkdir -p ./dist"))
|
||||||
|
|
||||||
err := os.Mkdir("./dist", 0755)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Error creating dist directory", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if os.Getenv("SKIP_GO_BUILD") != "1" {
|
if os.Getenv("SKIP_GO_BUILD") != "1" {
|
||||||
process.RunOrExit(process.NewRawCommand("", fmt.Sprintf("go build -tags prod -o ./dist")))
|
process.RunOrExit(process.NewRawCommand("", fmt.Sprintf("go build -tags prod -o ./dist")))
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,7 @@
|
||||||
package run
|
package run
|
||||||
|
|
||||||
import (
|
import "github.com/maddalax/htmgo/cli/htmgo/tasks/process"
|
||||||
"fmt"
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/tasks/process"
|
|
||||||
"io/fs"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Server(flags ...process.RunFlag) error {
|
func Server(flags ...process.RunFlag) error {
|
||||||
buildDir := "./__htmgo/temp-build"
|
return process.Run(process.NewRawCommand("run-server", "go run .", flags...))
|
||||||
_ = os.RemoveAll(buildDir)
|
|
||||||
err := os.Mkdir(buildDir, 0755)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
process.RunOrExit(process.NewRawCommand("", fmt.Sprintf("go build -o %s", buildDir)))
|
|
||||||
|
|
||||||
binaryPath := ""
|
|
||||||
|
|
||||||
// find the binary that was built
|
|
||||||
err = filepath.WalkDir(buildDir, func(path string, d fs.DirEntry, err error) error {
|
|
||||||
if d.IsDir() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
binaryPath = path
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if binaryPath == "" {
|
|
||||||
return fmt.Errorf("could not find the binary")
|
|
||||||
}
|
|
||||||
|
|
||||||
return process.Run(process.NewRawCommand("run-server", fmt.Sprintf("./%s", binaryPath), flags...))
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,19 @@
|
||||||
package run
|
package run
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/maddalax/htmgo/cli/htmgo/tasks/astgen"
|
||||||
|
"github.com/maddalax/htmgo/cli/htmgo/tasks/copyassets"
|
||||||
|
"github.com/maddalax/htmgo/cli/htmgo/tasks/css"
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/tasks/process"
|
"github.com/maddalax/htmgo/cli/htmgo/tasks/process"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Setup() {
|
func Setup() {
|
||||||
process.RunOrExit(process.NewRawCommand("", "go mod download"))
|
process.RunOrExit(process.NewRawCommand("", "go mod download"))
|
||||||
process.RunOrExit(process.NewRawCommand("", "go mod tidy"))
|
process.RunOrExit(process.NewRawCommand("", "go mod tidy"))
|
||||||
MakeBuildable()
|
|
||||||
|
copyassets.CopyAssets()
|
||||||
|
astgen.GenAst(process.ExitOnError)
|
||||||
|
css.GenerateCss(process.ExitOnError)
|
||||||
|
|
||||||
EntGenerate()
|
EntGenerate()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ func ReplaceTextInFile(file string, text string, replacement string) error {
|
||||||
|
|
||||||
func ReplaceTextInDirRecursive(dir string, text string, replacement string, filter func(file string) bool) error {
|
func ReplaceTextInDirRecursive(dir string, text string, replacement string, filter func(file string) bool) error {
|
||||||
return filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
|
return filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
|
||||||
if filter(filepath.Base(path)) {
|
if filter(path) {
|
||||||
_ = ReplaceTextInFile(path, text, replacement)
|
_ = ReplaceTextInFile(path, text, replacement)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,7 @@ func startWatcher(cb func(version string, file []*fsnotify.Event)) {
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
slog.Error("error:", slog.String("error", err.Error()))
|
slog.Error("error:", err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
@ -118,7 +118,7 @@ func startWatcher(cb func(version string, file []*fsnotify.Event)) {
|
||||||
if info.IsDir() {
|
if info.IsDir() {
|
||||||
err = watcher.Add(path)
|
err = watcher.Add(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Error adding directory to watcher:", slog.String("error", err.Error()))
|
slog.Error("Error adding directory to watcher:", err)
|
||||||
} else {
|
} else {
|
||||||
slog.Debug("Watching directory:", slog.String("path", path))
|
slog.Debug("Watching directory:", slog.String("path", path))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,25 +11,18 @@ import (
|
||||||
func MessageRow(message *Message) *h.Element {
|
func MessageRow(message *Message) *h.Element {
|
||||||
return h.Div(
|
return h.Div(
|
||||||
h.Attribute("hx-swap-oob", "beforeend"),
|
h.Attribute("hx-swap-oob", "beforeend"),
|
||||||
h.Class("flex flex-col gap-4 w-full break-words whitespace-normal"),
|
h.Class("flex flex-col gap-4 w-full break-words whitespace-normal"), // Ensure container breaks long words
|
||||||
// Ensure container breaks long words
|
|
||||||
h.Id("messages"),
|
h.Id("messages"),
|
||||||
h.Div(
|
h.Div(
|
||||||
h.Class("flex flex-col gap-1"),
|
h.Class("flex flex-col gap-1"),
|
||||||
h.Div(
|
h.Div(
|
||||||
h.Class("flex gap-2 items-center"),
|
h.Class("flex gap-2 items-center"),
|
||||||
h.Pf(
|
h.Pf(message.UserName, h.Class("font-bold")),
|
||||||
message.UserName,
|
|
||||||
h.Class("font-bold"),
|
|
||||||
),
|
|
||||||
h.Pf(message.CreatedAt.In(time.Local).Format("01/02 03:04 PM")),
|
h.Pf(message.CreatedAt.In(time.Local).Format("01/02 03:04 PM")),
|
||||||
),
|
),
|
||||||
h.Article(
|
h.Article(
|
||||||
h.Class("break-words whitespace-normal"),
|
h.Class("break-words whitespace-normal"), // Ensure message text wraps correctly
|
||||||
// Ensure message text wraps correctly
|
h.P(h.Text(message.Message)),
|
||||||
h.P(
|
|
||||||
h.Text(message.Message),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -28,28 +28,12 @@ func Button(props ButtonProps) h.Ren {
|
||||||
text := h.Text(props.Text)
|
text := h.Text(props.Text)
|
||||||
|
|
||||||
button := h.Button(
|
button := h.Button(
|
||||||
h.If(
|
h.If(props.Id != "", h.Id(props.Id)),
|
||||||
props.Id != "",
|
h.If(props.Children != nil, h.Children(props.Children...)),
|
||||||
h.Id(props.Id),
|
|
||||||
),
|
|
||||||
h.If(
|
|
||||||
props.Children != nil,
|
|
||||||
h.Children(props.Children...),
|
|
||||||
),
|
|
||||||
h.Class("flex gap-1 items-center justify-center border p-4 rounded cursor-hover", props.Class),
|
h.Class("flex gap-1 items-center justify-center border p-4 rounded cursor-hover", props.Class),
|
||||||
h.If(
|
h.If(props.Get != "", h.Get(props.Get)),
|
||||||
props.Get != "",
|
h.If(props.Target != "", h.HxTarget(props.Target)),
|
||||||
h.Get(props.Get),
|
h.IfElse(props.Type != "", h.Type(props.Type), h.Type("button")),
|
||||||
),
|
|
||||||
h.If(
|
|
||||||
props.Target != "",
|
|
||||||
h.HxTarget(props.Target),
|
|
||||||
),
|
|
||||||
h.IfElse(
|
|
||||||
props.Type != "",
|
|
||||||
h.Type(props.Type),
|
|
||||||
h.Type("button"),
|
|
||||||
),
|
|
||||||
text,
|
text,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,6 @@ func FormError(error string) *h.Element {
|
||||||
return h.Div(
|
return h.Div(
|
||||||
h.Id("form-error"),
|
h.Id("form-error"),
|
||||||
h.Text(error),
|
h.Text(error),
|
||||||
h.If(
|
h.If(error != "", h.Class("p-4 bg-rose-400 text-white rounded")),
|
||||||
error != "",
|
|
||||||
h.Class("p-4 bg-rose-400 text-white rounded"),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,14 +19,11 @@ type InputProps struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func Input(props InputProps) *h.Element {
|
func Input(props InputProps) *h.Element {
|
||||||
validation := h.If(
|
validation := h.If(props.ValidationPath != "", h.Children(
|
||||||
props.ValidationPath != "",
|
|
||||||
h.Children(
|
|
||||||
h.Post(props.ValidationPath, hx.BlurEvent),
|
h.Post(props.ValidationPath, hx.BlurEvent),
|
||||||
h.Attribute("hx-swap", "innerHTML transition:true"),
|
h.Attribute("hx-swap", "innerHTML transition:true"),
|
||||||
h.Attribute("hx-target", "next div"),
|
h.Attribute("hx-target", "next div"),
|
||||||
),
|
))
|
||||||
)
|
|
||||||
|
|
||||||
if props.Type == "" {
|
if props.Type == "" {
|
||||||
props.Type = "text"
|
props.Type = "text"
|
||||||
|
|
@ -35,41 +32,18 @@ func Input(props InputProps) *h.Element {
|
||||||
input := h.Input(
|
input := h.Input(
|
||||||
props.Type,
|
props.Type,
|
||||||
h.Class("border p-2 rounded focus:outline-none focus:ring focus:ring-slate-800"),
|
h.Class("border p-2 rounded focus:outline-none focus:ring focus:ring-slate-800"),
|
||||||
h.If(
|
h.If(props.Name != "", h.Name(props.Name)),
|
||||||
props.Name != "",
|
h.If(props.Children != nil, h.Children(props.Children...)),
|
||||||
h.Name(props.Name),
|
h.If(props.Required, h.Required()),
|
||||||
),
|
h.If(props.Placeholder != "", h.Placeholder(props.Placeholder)),
|
||||||
h.If(
|
h.If(props.DefaultValue != "", h.Attribute("value", props.DefaultValue)),
|
||||||
props.Children != nil,
|
|
||||||
h.Children(props.Children...),
|
|
||||||
),
|
|
||||||
h.If(
|
|
||||||
props.Required,
|
|
||||||
h.Required(),
|
|
||||||
),
|
|
||||||
h.If(
|
|
||||||
props.Placeholder != "",
|
|
||||||
h.Placeholder(props.Placeholder),
|
|
||||||
),
|
|
||||||
h.If(
|
|
||||||
props.DefaultValue != "",
|
|
||||||
h.Attribute("value", props.DefaultValue),
|
|
||||||
),
|
|
||||||
validation,
|
validation,
|
||||||
)
|
)
|
||||||
|
|
||||||
wrapped := h.Div(
|
wrapped := h.Div(
|
||||||
h.If(
|
h.If(props.Id != "", h.Id(props.Id)),
|
||||||
props.Id != "",
|
|
||||||
h.Id(props.Id),
|
|
||||||
),
|
|
||||||
h.Class("flex flex-col gap-1"),
|
h.Class("flex flex-col gap-1"),
|
||||||
h.If(
|
h.If(props.Label != "", h.Label(h.Text(props.Label))),
|
||||||
props.Label != "",
|
|
||||||
h.Label(
|
|
||||||
h.Text(props.Label),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
input,
|
input,
|
||||||
h.Div(
|
h.Div(
|
||||||
h.Id(props.Id+"-error"),
|
h.Id(props.Id+"-error"),
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ go 1.23.0
|
||||||
require (
|
require (
|
||||||
github.com/go-chi/chi/v5 v5.1.0
|
github.com/go-chi/chi/v5 v5.1.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/maddalax/htmgo/framework v1.0.7-0.20250703190716-06f01b3d7c1b
|
github.com/maddalax/htmgo/framework v0.0.0-20241021150618-635b17dd7f9d
|
||||||
github.com/mattn/go-sqlite3 v1.14.23
|
github.com/mattn/go-sqlite3 v1.14.23
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.4.0
|
github.com/puzpuzpuz/xsync/v3 v3.4.0
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
|
||||||
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/maddalax/htmgo/framework v1.0.7-0.20250703190716-06f01b3d7c1b h1:m+xI+HBEQdie/Rs+mYI0HTFTMlYQSCv0l/siPDoywA4=
|
github.com/maddalax/htmgo/framework v0.0.0-20241021150618-635b17dd7f9d h1:++qKuof1JEd+r2sbckUx63CsBcwbFNY7iD/2An53JWc=
|
||||||
github.com/maddalax/htmgo/framework v1.0.7-0.20250703190716-06f01b3d7c1b/go.mod h1:NGGzWVXWksrQJ9kV9SGa/A1F1Bjsgc08cN7ZVb98RqY=
|
github.com/maddalax/htmgo/framework v0.0.0-20241021150618-635b17dd7f9d/go.mod h1:NGGzWVXWksrQJ9kV9SGa/A1F1Bjsgc08cN7ZVb98RqY=
|
||||||
github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0=
|
github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0=
|
||||||
github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,13 @@ func ChatRoom(ctx *h.RequestContext) *h.Page {
|
||||||
RootPage(
|
RootPage(
|
||||||
h.Div(
|
h.Div(
|
||||||
h.TriggerChildren(),
|
h.TriggerChildren(),
|
||||||
|
|
||||||
h.Attribute("sse-connect", fmt.Sprintf("/sse/chat/%s", roomId)),
|
h.Attribute("sse-connect", fmt.Sprintf("/sse/chat/%s", roomId)),
|
||||||
|
|
||||||
h.HxOnSseOpen(
|
h.HxOnSseOpen(
|
||||||
js.ConsoleLog("Connected to chat room"),
|
js.ConsoleLog("Connected to chat room"),
|
||||||
),
|
),
|
||||||
|
|
||||||
h.HxOnSseError(
|
h.HxOnSseError(
|
||||||
js.EvalJs(fmt.Sprintf(`
|
js.EvalJs(fmt.Sprintf(`
|
||||||
const reason = e.detail.event.data
|
const reason = e.detail.event.data
|
||||||
|
|
@ -35,27 +38,35 @@ func ChatRoom(ctx *h.RequestContext) *h.Page {
|
||||||
}
|
}
|
||||||
`, roomId, roomId)),
|
`, roomId, roomId)),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Adjusted flex properties for responsive layout
|
// Adjusted flex properties for responsive layout
|
||||||
h.Class("flex flex-row h-screen bg-neutral-100 overflow-x-hidden"),
|
h.Class("flex flex-row h-screen bg-neutral-100 overflow-x-hidden"),
|
||||||
|
|
||||||
// Collapse Button for mobile
|
// Collapse Button for mobile
|
||||||
CollapseButton(),
|
CollapseButton(),
|
||||||
|
|
||||||
// Sidebar for connected users
|
// Sidebar for connected users
|
||||||
UserSidebar(),
|
UserSidebar(),
|
||||||
|
|
||||||
h.Div(
|
h.Div(
|
||||||
// Adjusted to fill height and width
|
// Adjusted to fill height and width
|
||||||
h.Class("flex flex-col h-full w-full bg-white p-4 overflow-hidden"),
|
h.Class("flex flex-col h-full w-full bg-white p-4 overflow-hidden"),
|
||||||
|
|
||||||
// Room name at the top, fixed
|
// Room name at the top, fixed
|
||||||
CachedRoomHeader(ctx),
|
CachedRoomHeader(ctx),
|
||||||
|
|
||||||
h.HxAfterSseMessage(
|
h.HxAfterSseMessage(
|
||||||
js.EvalJsOnSibling("#messages",
|
js.EvalJsOnSibling("#messages",
|
||||||
`element.scrollTop = element.scrollHeight;`),
|
`element.scrollTop = element.scrollHeight;`),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Chat Messages
|
// Chat Messages
|
||||||
h.Div(
|
h.Div(
|
||||||
h.Id("messages"),
|
h.Id("messages"),
|
||||||
// Adjusted flex properties and removed max-width
|
// Adjusted flex properties and removed max-width
|
||||||
h.Class("flex flex-col gap-4 mb-4 overflow-auto flex-grow w-full pt-[50px]"),
|
h.Class("flex flex-col gap-4 mb-4 overflow-auto flex-grow w-full pt-[50px]"),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Chat Input at the bottom
|
// Chat Input at the bottom
|
||||||
Form(),
|
Form(),
|
||||||
),
|
),
|
||||||
|
|
@ -80,10 +91,7 @@ func roomNameHeader(ctx *h.RequestContext) *h.Element {
|
||||||
}
|
}
|
||||||
return h.Div(
|
return h.Div(
|
||||||
h.Class("bg-neutral-700 text-white p-3 shadow-sm w-full fixed top-0 left-0 flex justify-center z-10"),
|
h.Class("bg-neutral-700 text-white p-3 shadow-sm w-full fixed top-0 left-0 flex justify-center z-10"),
|
||||||
h.H2F(
|
h.H2F(room.Name, h.Class("text-lg font-bold")),
|
||||||
room.Name,
|
|
||||||
h.Class("text-lg font-bold"),
|
|
||||||
),
|
|
||||||
h.Div(
|
h.Div(
|
||||||
h.Class("absolute right-5 top-3 cursor-pointer"),
|
h.Class("absolute right-5 top-3 cursor-pointer"),
|
||||||
h.Text("Share"),
|
h.Text("Share"),
|
||||||
|
|
@ -100,10 +108,7 @@ func UserSidebar() *h.Element {
|
||||||
return h.Div(
|
return h.Div(
|
||||||
h.Class("sidebar h-full pt-[67px] min-w-48 w-48 bg-neutral-200 p-4 flex-col justify-between gap-3 rounded-l-lg hidden md:flex"),
|
h.Class("sidebar h-full pt-[67px] min-w-48 w-48 bg-neutral-200 p-4 flex-col justify-between gap-3 rounded-l-lg hidden md:flex"),
|
||||||
h.Div(
|
h.Div(
|
||||||
h.H3F(
|
h.H3F("Connected Users", h.Class("text-lg font-bold")),
|
||||||
"Connected Users",
|
|
||||||
h.Class("text-lg font-bold"),
|
|
||||||
),
|
|
||||||
chat.ConnectedUsers(make([]db.User, 0), ""),
|
chat.ConnectedUsers(make([]db.User, 0), ""),
|
||||||
),
|
),
|
||||||
h.A(
|
h.A(
|
||||||
|
|
@ -116,11 +121,9 @@ func UserSidebar() *h.Element {
|
||||||
|
|
||||||
func CollapseButton() *h.Element {
|
func CollapseButton() *h.Element {
|
||||||
return h.Div(
|
return h.Div(
|
||||||
h.Class("fixed top-0 left-4 md:hidden z-50"),
|
h.Class("fixed top-0 left-4 md:hidden z-50"), // Always visible on mobile
|
||||||
// Always visible on mobile
|
|
||||||
h.Button(
|
h.Button(
|
||||||
h.Class("p-2 text-2xl bg-neutral-700 text-white rounded-md"),
|
h.Class("p-2 text-2xl bg-neutral-700 text-white rounded-md"), // Styling the button
|
||||||
// Styling the button
|
|
||||||
h.OnClick(
|
h.OnClick(
|
||||||
js.EvalJs(`
|
js.EvalJs(`
|
||||||
const sidebar = document.querySelector('.sidebar');
|
const sidebar = document.querySelector('.sidebar');
|
||||||
|
|
@ -128,16 +131,13 @@ func CollapseButton() *h.Element {
|
||||||
sidebar.classList.toggle('flex');
|
sidebar.classList.toggle('flex');
|
||||||
`),
|
`),
|
||||||
),
|
),
|
||||||
h.UnsafeRaw("☰"),
|
h.UnsafeRaw("☰"), // The icon for collapsing the sidebar
|
||||||
|
|
||||||
// The icon for collapsing the sidebar
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func MessageInput() *h.Element {
|
func MessageInput() *h.Element {
|
||||||
return h.Input(
|
return h.Input("text",
|
||||||
"text",
|
|
||||||
h.Id("message-input"),
|
h.Id("message-input"),
|
||||||
h.Required(),
|
h.Required(),
|
||||||
h.Class("p-4 rounded-md border border-slate-200 w-full focus:outline-none focus:ring focus:ring-slate-200"),
|
h.Class("p-4 rounded-md border border-slate-200 w-full focus:outline-none focus:ring focus:ring-slate-200"),
|
||||||
|
|
|
||||||
|
|
@ -13,14 +13,12 @@ func ChatAppFirstScreen(ctx *h.RequestContext) *h.Page {
|
||||||
h.Class("flex flex-col items-center justify-center min-h-screen bg-neutral-100"),
|
h.Class("flex flex-col items-center justify-center min-h-screen bg-neutral-100"),
|
||||||
h.Div(
|
h.Div(
|
||||||
h.Class("bg-white p-8 rounded-lg shadow-lg w-full max-w-md"),
|
h.Class("bg-white p-8 rounded-lg shadow-lg w-full max-w-md"),
|
||||||
h.H2F(
|
h.H2F("htmgo chat", h.Class("text-3xl font-bold text-center mb-6")),
|
||||||
"htmgo chat",
|
|
||||||
h.Class("text-3xl font-bold text-center mb-6"),
|
|
||||||
),
|
|
||||||
h.Form(
|
h.Form(
|
||||||
h.Attribute("hx-swap", "none"),
|
h.Attribute("hx-swap", "none"),
|
||||||
h.PostPartial(partials.CreateOrJoinRoom),
|
h.PostPartial(partials.CreateOrJoinRoom),
|
||||||
h.Class("flex flex-col gap-6"),
|
h.Class("flex flex-col gap-6"),
|
||||||
|
|
||||||
// Username input at the top
|
// Username input at the top
|
||||||
components.Input(components.InputProps{
|
components.Input(components.InputProps{
|
||||||
Id: "username",
|
Id: "username",
|
||||||
|
|
@ -32,9 +30,11 @@ func ChatAppFirstScreen(ctx *h.RequestContext) *h.Page {
|
||||||
h.MaxLength(15),
|
h.MaxLength(15),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Single box for Create or Join a Chat Room
|
// Single box for Create or Join a Chat Room
|
||||||
h.Div(
|
h.Div(
|
||||||
h.Class("p-4 border border-gray-300 rounded-md flex flex-col gap-6"),
|
h.Class("p-4 border border-gray-300 rounded-md flex flex-col gap-6"),
|
||||||
|
|
||||||
// Create New Chat Room input
|
// Create New Chat Room input
|
||||||
components.Input(components.InputProps{
|
components.Input(components.InputProps{
|
||||||
Name: "new-chat-room",
|
Name: "new-chat-room",
|
||||||
|
|
@ -45,20 +45,15 @@ func ChatAppFirstScreen(ctx *h.RequestContext) *h.Page {
|
||||||
h.MaxLength(20),
|
h.MaxLength(20),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// OR divider
|
// OR divider
|
||||||
h.Div(
|
h.Div(
|
||||||
h.Class("flex items-center justify-center gap-4"),
|
h.Class("flex items-center justify-center gap-4"),
|
||||||
h.Div(
|
h.Div(h.Class("border-t border-gray-300 flex-grow")),
|
||||||
h.Class("border-t border-gray-300 flex-grow"),
|
h.P(h.Text("OR"), h.Class("text-gray-500")),
|
||||||
),
|
h.Div(h.Class("border-t border-gray-300 flex-grow")),
|
||||||
h.P(
|
|
||||||
h.Text("OR"),
|
|
||||||
h.Class("text-gray-500"),
|
|
||||||
),
|
|
||||||
h.Div(
|
|
||||||
h.Class("border-t border-gray-300 flex-grow"),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
|
||||||
// Join Chat Room input
|
// Join Chat Room input
|
||||||
components.Input(components.InputProps{
|
components.Input(components.InputProps{
|
||||||
Id: "join-chat-room",
|
Id: "join-chat-room",
|
||||||
|
|
@ -72,8 +67,10 @@ func ChatAppFirstScreen(ctx *h.RequestContext) *h.Page {
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Error message
|
// Error message
|
||||||
components.FormError(""),
|
components.FormError(""),
|
||||||
|
|
||||||
// Submit button at the bottom
|
// Submit button at the bottom
|
||||||
components.PrimaryButton(components.ButtonProps{
|
components.PrimaryButton(components.ButtonProps{
|
||||||
Type: "submit",
|
Type: "submit",
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ func Handle() http.HandlerFunc {
|
||||||
defer manager.Disconnect(sessionId)
|
defer manager.Disconnect(sessionId)
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
|
fmt.Printf("empting channels\n")
|
||||||
for len(writer) > 0 {
|
for len(writer) > 0 {
|
||||||
<-writer
|
<-writer
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -70,14 +70,16 @@ func (manager *SocketManager) Listen(listener chan SocketEvent) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (manager *SocketManager) dispatch(event SocketEvent) {
|
func (manager *SocketManager) dispatch(event SocketEvent) {
|
||||||
|
fmt.Printf("dispatching event: %s\n", event.Type)
|
||||||
done := make(chan struct{}, 1)
|
done := make(chan struct{}, 1)
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-done:
|
case <-done:
|
||||||
|
fmt.Printf("dispatched event: %s\n", event.Type)
|
||||||
return
|
return
|
||||||
case <-time.After(5 * time.Second):
|
case <-time.After(5 * time.Second):
|
||||||
fmt.Printf("havent dispatched listener event after 5s, chan blocked: %s\n", event.Type)
|
fmt.Printf("havent dispatched event after 5s, chan blocked: %s\n", event.Type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,9 @@ module hackernews
|
||||||
|
|
||||||
go 1.23.0
|
go 1.23.0
|
||||||
|
|
||||||
require (
|
require github.com/maddalax/htmgo/framework v0.0.0-20241021150618-635b17dd7f9d
|
||||||
github.com/maddalax/htmgo/framework v1.0.7-0.20250703190716-06f01b3d7c1b
|
|
||||||
github.com/microcosm-cc/bluemonday v1.0.27
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/aymerick/douceur v0.2.0 // indirect
|
|
||||||
github.com/go-chi/chi/v5 v5.1.0 // indirect
|
github.com/go-chi/chi/v5 v5.1.0 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/gorilla/css v1.0.1 // indirect
|
|
||||||
golang.org/x/net v0.29.0 // indirect
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,11 @@
|
||||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
|
||||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
|
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
|
||||||
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
github.com/maddalax/htmgo/framework v0.0.0-20241021150618-635b17dd7f9d h1:++qKuof1JEd+r2sbckUx63CsBcwbFNY7iD/2An53JWc=
|
||||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
github.com/maddalax/htmgo/framework v0.0.0-20241021150618-635b17dd7f9d/go.mod h1:NGGzWVXWksrQJ9kV9SGa/A1F1Bjsgc08cN7ZVb98RqY=
|
||||||
github.com/maddalax/htmgo/framework v1.0.7-0.20250703190716-06f01b3d7c1b h1:m+xI+HBEQdie/Rs+mYI0HTFTMlYQSCv0l/siPDoywA4=
|
|
||||||
github.com/maddalax/htmgo/framework v1.0.7-0.20250703190716-06f01b3d7c1b/go.mod h1:NGGzWVXWksrQJ9kV9SGa/A1F1Bjsgc08cN7ZVb98RqY=
|
|
||||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
|
||||||
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"github.com/maddalax/htmgo/framework/h"
|
"github.com/maddalax/htmgo/framework/h"
|
||||||
"hackernews/internal/batch"
|
"hackernews/internal/batch"
|
||||||
"hackernews/internal/httpjson"
|
"hackernews/internal/httpjson"
|
||||||
"hackernews/internal/sanitize"
|
|
||||||
"hackernews/internal/timeformat"
|
"hackernews/internal/timeformat"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
@ -133,8 +132,6 @@ func GetComment(id int) (*Comment, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
c.Text = sanitize.Sanitize(c.Text)
|
|
||||||
c.By = sanitize.Sanitize(c.By)
|
|
||||||
c.Time = timeformat.ParseUnix(c.TimeRaw)
|
c.Time = timeformat.ParseUnix(c.TimeRaw)
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
@ -144,9 +141,6 @@ func GetStory(id int) (*Story, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
s.Title = sanitize.Sanitize(s.Title)
|
|
||||||
s.Text = sanitize.Sanitize(s.Text)
|
|
||||||
s.By = sanitize.Sanitize(s.By)
|
|
||||||
s.Time = timeformat.ParseUnix(s.TimeRaw)
|
s.Time = timeformat.ParseUnix(s.TimeRaw)
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
package sanitize
|
|
||||||
|
|
||||||
import "github.com/microcosm-cc/bluemonday"
|
|
||||||
|
|
||||||
var p = bluemonday.UGCPolicy()
|
|
||||||
|
|
||||||
func Sanitize(text string) string {
|
|
||||||
return p.Sanitize(text)
|
|
||||||
}
|
|
||||||
|
|
@ -5,17 +5,14 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func RootPage(children ...h.Ren) h.Ren {
|
func RootPage(children ...h.Ren) h.Ren {
|
||||||
banner := h.A(
|
banner := h.A(h.Class("bg-neutral-200 text-neutral-600 text-center p-2 flex items-center justify-center"),
|
||||||
h.Class("bg-neutral-200 text-neutral-600 text-center p-2 flex items-center justify-center"),
|
|
||||||
h.Href("https://github.com/maddalax/htmgo"),
|
h.Href("https://github.com/maddalax/htmgo"),
|
||||||
h.Attribute("target", "_blank"),
|
h.Attribute("target", "_blank"),
|
||||||
h.Text("Built with htmgo.dev"),
|
h.Text("Built with htmgo.dev"),
|
||||||
)
|
)
|
||||||
|
|
||||||
return h.Html(
|
return h.Html(
|
||||||
h.HxExtensions(
|
h.HxExtensions(h.BaseExtensions()),
|
||||||
h.BaseExtensions(),
|
|
||||||
),
|
|
||||||
h.Head(
|
h.Head(
|
||||||
h.Meta("viewport", "width=device-width, initial-scale=1"),
|
h.Meta("viewport", "width=device-width, initial-scale=1"),
|
||||||
h.Link("/public/favicon.ico", "icon"),
|
h.Link("/public/favicon.ico", "icon"),
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"github.com/maddalax/htmgo/framework/h"
|
"github.com/maddalax/htmgo/framework/h"
|
||||||
"hackernews/internal/batch"
|
"hackernews/internal/batch"
|
||||||
"hackernews/internal/news"
|
"hackernews/internal/news"
|
||||||
"hackernews/internal/sanitize"
|
|
||||||
"hackernews/internal/timeformat"
|
"hackernews/internal/timeformat"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -14,12 +13,7 @@ import (
|
||||||
func StoryComments(ctx *h.RequestContext) *h.Partial {
|
func StoryComments(ctx *h.RequestContext) *h.Partial {
|
||||||
return h.NewPartial(
|
return h.NewPartial(
|
||||||
h.Fragment(
|
h.Fragment(
|
||||||
h.OobSwap(
|
h.OobSwap(ctx, h.Div(h.Id("comments-loader"))),
|
||||||
ctx,
|
|
||||||
h.Div(
|
|
||||||
h.Id("comments-loader"),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
h.Div(
|
h.Div(
|
||||||
h.Class("flex flex-col gap-3 prose max-w-none"),
|
h.Class("flex flex-col gap-3 prose max-w-none"),
|
||||||
CachedStoryComments(news.MustItemId(ctx)),
|
CachedStoryComments(news.MustItemId(ctx)),
|
||||||
|
|
@ -63,20 +57,14 @@ func Comment(item news.Comment, nesting int) *h.Element {
|
||||||
"border-b border-gray-200": nesting == 0,
|
"border-b border-gray-200": nesting == 0,
|
||||||
"border-l border-gray-200": nesting > 0,
|
"border-l border-gray-200": nesting > 0,
|
||||||
}),
|
}),
|
||||||
h.If(
|
h.If(nesting > 0, h.Attribute("style", fmt.Sprintf("margin-left: %dpx", (nesting-1)*15))),
|
||||||
nesting > 0,
|
|
||||||
h.Attribute("style", fmt.Sprintf("margin-left: %dpx", (nesting-1)*15)),
|
|
||||||
),
|
|
||||||
h.Div(
|
h.Div(
|
||||||
h.If(
|
h.If(nesting > 0, h.Class("pl-4")),
|
||||||
nesting > 0,
|
|
||||||
h.Class("pl-4"),
|
|
||||||
),
|
|
||||||
h.Div(
|
h.Div(
|
||||||
h.Class("flex gap-1 items-center"),
|
h.Class("flex gap-1 items-center"),
|
||||||
h.Div(
|
h.Div(
|
||||||
h.Class("font-bold text-rose-500"),
|
h.Class("font-bold text-rose-500"),
|
||||||
h.UnsafeRaw(sanitize.Sanitize(item.By)),
|
h.UnsafeRaw(item.By),
|
||||||
),
|
),
|
||||||
h.Div(
|
h.Div(
|
||||||
h.Class("text-sm text-gray-600"),
|
h.Class("text-sm text-gray-600"),
|
||||||
|
|
@ -86,18 +74,15 @@ func Comment(item news.Comment, nesting int) *h.Element {
|
||||||
),
|
),
|
||||||
h.Div(
|
h.Div(
|
||||||
h.Class("text-sm text-gray-600"),
|
h.Class("text-sm text-gray-600"),
|
||||||
h.UnsafeRaw(sanitize.Sanitize(strings.TrimSpace(item.Text))),
|
h.UnsafeRaw(strings.TrimSpace(item.Text)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
h.If(
|
h.If(len(children) > 0, h.List(
|
||||||
len(children) > 0,
|
|
||||||
h.List(
|
|
||||||
children, func(child news.Comment, index int) *h.Element {
|
children, func(child news.Comment, index int) *h.Element {
|
||||||
return h.Div(
|
return h.Div(
|
||||||
Comment(child, nesting+1),
|
Comment(child, nesting+1),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
),
|
)),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import (
|
||||||
"hackernews/components"
|
"hackernews/components"
|
||||||
"hackernews/internal/news"
|
"hackernews/internal/news"
|
||||||
"hackernews/internal/parse"
|
"hackernews/internal/parse"
|
||||||
"hackernews/internal/sanitize"
|
|
||||||
"hackernews/internal/timeformat"
|
"hackernews/internal/timeformat"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
@ -58,18 +57,13 @@ func StorySidebar(ctx *h.RequestContext) *h.Partial {
|
||||||
|
|
||||||
page := parse.MustParseInt(pageRaw, 0)
|
page := parse.MustParseInt(pageRaw, 0)
|
||||||
|
|
||||||
fetchMorePath := h.GetPartialPathWithQs(
|
fetchMorePath := h.GetPartialPathWithQs(StorySidebar, h.NewQs("mode", "infinite", "page", fmt.Sprintf("%d", page+1), "category", category))
|
||||||
StorySidebar,
|
|
||||||
h.NewQs("mode", "infinite", "page", fmt.Sprintf("%d", page+1), "category", category),
|
|
||||||
)
|
|
||||||
|
|
||||||
list := CachedStoryList(category, page, 50, fetchMorePath)
|
list := CachedStoryList(category, page, 50, fetchMorePath)
|
||||||
|
|
||||||
body := h.Aside(
|
body := h.Aside(
|
||||||
h.Id("story-sidebar"),
|
h.Id("story-sidebar"),
|
||||||
h.JoinExtensions(
|
h.JoinExtensions(h.TriggerChildren()),
|
||||||
h.TriggerChildren(),
|
|
||||||
),
|
|
||||||
h.Class("sticky top-0 h-screen p-1 bg-gray-100 overflow-y-auto max-w-80 min-w-80"),
|
h.Class("sticky top-0 h-screen p-1 bg-gray-100 overflow-y-auto max-w-80 min-w-80"),
|
||||||
h.Div(
|
h.Div(
|
||||||
h.Class("flex flex-col gap-1"),
|
h.Class("flex flex-col gap-1"),
|
||||||
|
|
@ -105,9 +99,7 @@ func SidebarTitle(defaultCategory string) *h.Element {
|
||||||
h.Text("Hacker News"),
|
h.Text("Hacker News"),
|
||||||
),
|
),
|
||||||
h.Div(
|
h.Div(
|
||||||
h.OnLoad(
|
h.OnLoad(h.EvalJs(ScrollJs)),
|
||||||
h.EvalJs(ScrollJs),
|
|
||||||
),
|
|
||||||
h.Class("scroll-container mt-2 flex gap-1 no-scrollbar overflow-y-hidden whitespace-nowrap overflow-x-auto"),
|
h.Class("scroll-container mt-2 flex gap-1 no-scrollbar overflow-y-hidden whitespace-nowrap overflow-x-auto"),
|
||||||
h.List(news.Categories, func(item news.Category, index int) *h.Element {
|
h.List(news.Categories, func(item news.Category, index int) *h.Element {
|
||||||
return CategoryBadge(defaultCategory, item)
|
return CategoryBadge(defaultCategory, item)
|
||||||
|
|
@ -122,13 +114,7 @@ func CategoryBadge(defaultCategory string, category news.Category) *h.Element {
|
||||||
category.Name,
|
category.Name,
|
||||||
selected,
|
selected,
|
||||||
h.Attribute("hx-swap", "none"),
|
h.Attribute("hx-swap", "none"),
|
||||||
h.If(
|
h.If(!selected, h.PostPartialOnClickQs(StorySidebar, h.NewQs("category", category.Path))),
|
||||||
!selected,
|
|
||||||
h.PostPartialOnClickQs(
|
|
||||||
StorySidebar,
|
|
||||||
h.NewQs("category", category.Path),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -143,7 +129,7 @@ var CachedStoryList = h.CachedPerKeyT4(time.Minute*5, func(category string, page
|
||||||
h.Class("block p-2 bg-white rounded-md shadow cursor-pointer"),
|
h.Class("block p-2 bg-white rounded-md shadow cursor-pointer"),
|
||||||
h.Div(
|
h.Div(
|
||||||
h.Class("font-bold"),
|
h.Class("font-bold"),
|
||||||
h.UnsafeRaw(sanitize.Sanitize(item.Title)),
|
h.UnsafeRaw(item.Title),
|
||||||
),
|
),
|
||||||
h.Div(
|
h.Div(
|
||||||
h.Class("text-sm text-gray-600"),
|
h.Class("text-sm text-gray-600"),
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/maddalax/htmgo/framework/h"
|
"github.com/maddalax/htmgo/framework/h"
|
||||||
"hackernews/internal/news"
|
"hackernews/internal/news"
|
||||||
"hackernews/internal/sanitize"
|
|
||||||
"hackernews/internal/timeformat"
|
"hackernews/internal/timeformat"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
@ -58,7 +57,7 @@ func StoryBody(story *news.Story) *h.Element {
|
||||||
h.Class("prose prose-2xl border-b border-gray-200 pb-3 max-w-none w-full"),
|
h.Class("prose prose-2xl border-b border-gray-200 pb-3 max-w-none w-full"),
|
||||||
h.H5(
|
h.H5(
|
||||||
h.Class("flex gap-2 items-left font-bold"),
|
h.Class("flex gap-2 items-left font-bold"),
|
||||||
h.UnsafeRaw(sanitize.Sanitize(story.Title)),
|
h.UnsafeRaw(story.Title),
|
||||||
),
|
),
|
||||||
h.A(
|
h.A(
|
||||||
h.Href(story.Url),
|
h.Href(story.Url),
|
||||||
|
|
@ -67,7 +66,7 @@ func StoryBody(story *news.Story) *h.Element {
|
||||||
),
|
),
|
||||||
h.Div(
|
h.Div(
|
||||||
h.Class("text-sm text-gray-600"),
|
h.Class("text-sm text-gray-600"),
|
||||||
h.UnsafeRaw(sanitize.Sanitize(story.Text)),
|
h.UnsafeRaw(story.Text),
|
||||||
),
|
),
|
||||||
h.Div(
|
h.Div(
|
||||||
h.Class("text-sm text-gray-600 mt-2"),
|
h.Class("text-sm text-gray-600 mt-2"),
|
||||||
|
|
|
||||||
6
examples/minimal-htmgo/.gitignore
vendored
6
examples/minimal-htmgo/.gitignore
vendored
|
|
@ -1,6 +0,0 @@
|
||||||
/assets/dist
|
|
||||||
tmp
|
|
||||||
node_modules
|
|
||||||
.idea
|
|
||||||
__htmgo
|
|
||||||
dist
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
Minimal example that just uses htmgo for html rendering / js support and nothing else.
|
|
||||||
|
|
||||||
Removes automatic support for:
|
|
||||||
|
|
||||||
1. live reloading
|
|
||||||
2. tailwind recompilation
|
|
||||||
3. page/partial route registration
|
|
||||||
4. Single binary (since /public/ assets is required to be there), normally htmgo uses the embedded file system in other examples such as https://github.com/maddalax/htmgo/blob/master/templates/starter/assets_prod.go
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
module minimal-htmgo
|
|
||||||
|
|
||||||
go 1.23.0
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/go-chi/chi/v5 v5.1.0
|
|
||||||
github.com/maddalax/htmgo/framework v1.0.7-0.20250703190716-06f01b3d7c1b
|
|
||||||
)
|
|
||||||
|
|
||||||
require github.com/google/uuid v1.6.0 // indirect
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
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/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
|
|
||||||
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/maddalax/htmgo/framework v1.0.7-0.20250703190716-06f01b3d7c1b h1:m+xI+HBEQdie/Rs+mYI0HTFTMlYQSCv0l/siPDoywA4=
|
|
||||||
github.com/maddalax/htmgo/framework v1.0.7-0.20250703190716-06f01b3d7c1b/go.mod h1:NGGzWVXWksrQJ9kV9SGa/A1F1Bjsgc08cN7ZVb98RqY=
|
|
||||||
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/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
|
||||||
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
|
|
||||||
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/maddalax/htmgo/framework/h"
|
|
||||||
"github.com/maddalax/htmgo/framework/js"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Index(ctx *h.RequestContext) *h.Page {
|
|
||||||
return h.NewPage(
|
|
||||||
h.Html(
|
|
||||||
h.HxExtensions(
|
|
||||||
h.BaseExtensions(),
|
|
||||||
),
|
|
||||||
h.Head(
|
|
||||||
h.Meta("viewport", "width=device-width, initial-scale=1"),
|
|
||||||
h.Script("/public/htmgo.js"),
|
|
||||||
),
|
|
||||||
h.Body(
|
|
||||||
h.Pf("hello htmgo"),
|
|
||||||
h.Div(
|
|
||||||
h.Get("/current-time", "load, every 1s"),
|
|
||||||
),
|
|
||||||
h.Div(
|
|
||||||
h.Button(
|
|
||||||
h.Text("Click me"),
|
|
||||||
h.OnClick(
|
|
||||||
js.EvalJs(`
|
|
||||||
console.log("you evalulated javascript");
|
|
||||||
alert("you clicked me");
|
|
||||||
`),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func CurrentTime(ctx *h.RequestContext) *h.Partial {
|
|
||||||
return h.NewPartial(
|
|
||||||
h.Pf("It is %s", time.Now().String()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/go-chi/chi/v5"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
router := chi.NewRouter()
|
|
||||||
|
|
||||||
fileServer := http.StripPrefix("/public", http.FileServer(http.Dir("./public")))
|
|
||||||
router.Handle("/public/*", fileServer)
|
|
||||||
|
|
||||||
router.Get("/", func(writer http.ResponseWriter, request *http.Request) {
|
|
||||||
RenderPage(request, writer, Index)
|
|
||||||
})
|
|
||||||
|
|
||||||
router.Get("/current-time", func(writer http.ResponseWriter, request *http.Request) {
|
|
||||||
RenderPartial(request, writer, CurrentTime)
|
|
||||||
})
|
|
||||||
|
|
||||||
http.ListenAndServe(":3000", router)
|
|
||||||
}
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1,26 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/maddalax/htmgo/framework/h"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
func RenderToString(element *h.Element) string {
|
|
||||||
return h.Render(element)
|
|
||||||
}
|
|
||||||
|
|
||||||
func RenderPage(req *http.Request, w http.ResponseWriter, page func(ctx *h.RequestContext) *h.Page) {
|
|
||||||
ctx := h.RequestContext{
|
|
||||||
Request: req,
|
|
||||||
Response: w,
|
|
||||||
}
|
|
||||||
h.HtmlView(w, page(&ctx))
|
|
||||||
}
|
|
||||||
|
|
||||||
func RenderPartial(req *http.Request, w http.ResponseWriter, partial func(ctx *h.RequestContext) *h.Partial) {
|
|
||||||
ctx := h.RequestContext{
|
|
||||||
Request: req,
|
|
||||||
Response: w,
|
|
||||||
}
|
|
||||||
h.PartialView(w, partial(&ctx))
|
|
||||||
}
|
|
||||||
|
|
@ -3,7 +3,7 @@ module simpleauth
|
||||||
go 1.23.0
|
go 1.23.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/maddalax/htmgo/framework v1.0.7-0.20250703190716-06f01b3d7c1b
|
github.com/maddalax/htmgo/framework v0.0.0-20241021150618-635b17dd7f9d
|
||||||
github.com/mattn/go-sqlite3 v1.14.24
|
github.com/mattn/go-sqlite3 v1.14.24
|
||||||
golang.org/x/crypto v0.28.0
|
golang.org/x/crypto v0.28.0
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
|
||||||
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/maddalax/htmgo/framework v1.0.7-0.20250703190716-06f01b3d7c1b h1:m+xI+HBEQdie/Rs+mYI0HTFTMlYQSCv0l/siPDoywA4=
|
github.com/maddalax/htmgo/framework v0.0.0-20241021150618-635b17dd7f9d h1:++qKuof1JEd+r2sbckUx63CsBcwbFNY7iD/2An53JWc=
|
||||||
github.com/maddalax/htmgo/framework v1.0.7-0.20250703190716-06f01b3d7c1b/go.mod h1:NGGzWVXWksrQJ9kV9SGa/A1F1Bjsgc08cN7ZVb98RqY=
|
github.com/maddalax/htmgo/framework v0.0.0-20241021150618-635b17dd7f9d/go.mod h1:NGGzWVXWksrQJ9kV9SGa/A1F1Bjsgc08cN7ZVb98RqY=
|
||||||
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||||
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
|
|
||||||
|
|
@ -24,16 +24,14 @@ func UserProfilePage(u db.User) *h.Element {
|
||||||
|
|
||||||
return h.Div(
|
return h.Div(
|
||||||
h.Class("flex flex-col gap-6 items-center pt-10 min-h-screen bg-neutral-100"),
|
h.Class("flex flex-col gap-6 items-center pt-10 min-h-screen bg-neutral-100"),
|
||||||
h.H3F(
|
h.H3F("User Profile", h.Class("text-2xl font-bold")),
|
||||||
"User Profile",
|
|
||||||
h.Class("text-2xl font-bold"),
|
|
||||||
),
|
|
||||||
h.Pf("Welcome, %s!", u.Email),
|
h.Pf("Welcome, %s!", u.Email),
|
||||||
h.Form(
|
h.Form(
|
||||||
h.Attribute("hx-swap", "none"),
|
h.Attribute("hx-swap", "none"),
|
||||||
h.PostPartial(partials.UpdateProfile),
|
h.PostPartial(partials.UpdateProfile),
|
||||||
h.TriggerChildren(),
|
h.TriggerChildren(),
|
||||||
h.Class("flex flex-col gap-4 w-full max-w-md p-6 bg-white rounded-md shadow-md"),
|
h.Class("flex flex-col gap-4 w-full max-w-md p-6 bg-white rounded-md shadow-md"),
|
||||||
|
|
||||||
ui.Input(ui.InputProps{
|
ui.Input(ui.InputProps{
|
||||||
Id: "email",
|
Id: "email",
|
||||||
Name: "email",
|
Name: "email",
|
||||||
|
|
@ -44,22 +42,26 @@ func UserProfilePage(u db.User) *h.Element {
|
||||||
h.Disabled(),
|
h.Disabled(),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
ui.Input(ui.InputProps{
|
ui.Input(ui.InputProps{
|
||||||
Name: "birth-date",
|
Name: "birth-date",
|
||||||
Label: "Birth Date",
|
Label: "Birth Date",
|
||||||
DefaultValue: user.GetMetaKey(meta, "birthDate"),
|
DefaultValue: user.GetMetaKey(meta, "birthDate"),
|
||||||
Type: "date",
|
Type: "date",
|
||||||
}),
|
}),
|
||||||
|
|
||||||
ui.Input(ui.InputProps{
|
ui.Input(ui.InputProps{
|
||||||
Name: "favorite-color",
|
Name: "favorite-color",
|
||||||
Label: "Favorite Color",
|
Label: "Favorite Color",
|
||||||
DefaultValue: user.GetMetaKey(meta, "favoriteColor"),
|
DefaultValue: user.GetMetaKey(meta, "favoriteColor"),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
ui.Input(ui.InputProps{
|
ui.Input(ui.InputProps{
|
||||||
Name: "occupation",
|
Name: "occupation",
|
||||||
Label: "Occupation",
|
Label: "Occupation",
|
||||||
DefaultValue: user.GetMetaKey(meta, "occupation"),
|
DefaultValue: user.GetMetaKey(meta, "occupation"),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
ui.FormError(""),
|
ui.FormError(""),
|
||||||
ui.SubmitButton("Save Changes"),
|
ui.SubmitButton("Save Changes"),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,7 @@ import (
|
||||||
|
|
||||||
func RootPage(children ...h.Ren) h.Ren {
|
func RootPage(children ...h.Ren) h.Ren {
|
||||||
return h.Html(
|
return h.Html(
|
||||||
h.HxExtensions(
|
h.HxExtensions(h.BaseExtensions()),
|
||||||
h.BaseExtensions(),
|
|
||||||
),
|
|
||||||
h.Head(
|
h.Head(
|
||||||
h.Meta("viewport", "width=device-width, initial-scale=1"),
|
h.Meta("viewport", "width=device-width, initial-scale=1"),
|
||||||
h.Link("/public/favicon.ico", "icon"),
|
h.Link("/public/favicon.ico", "icon"),
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,7 @@ func FormError(error string) *h.Element {
|
||||||
return h.Div(
|
return h.Div(
|
||||||
h.Id("form-error"),
|
h.Id("form-error"),
|
||||||
h.Text(error),
|
h.Text(error),
|
||||||
h.If(
|
h.If(error != "", h.Class("p-4 bg-rose-400 text-white rounded")),
|
||||||
error != "",
|
|
||||||
h.Class("p-4 bg-rose-400 text-white rounded"),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,14 +19,11 @@ type InputProps struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func Input(props InputProps) *h.Element {
|
func Input(props InputProps) *h.Element {
|
||||||
validation := h.If(
|
validation := h.If(props.ValidationPath != "", h.Children(
|
||||||
props.ValidationPath != "",
|
|
||||||
h.Children(
|
|
||||||
h.Post(props.ValidationPath, hx.BlurEvent),
|
h.Post(props.ValidationPath, hx.BlurEvent),
|
||||||
h.Attribute("hx-swap", "innerHTML transition:true"),
|
h.Attribute("hx-swap", "innerHTML transition:true"),
|
||||||
h.Attribute("hx-target", "next div"),
|
h.Attribute("hx-target", "next div"),
|
||||||
),
|
))
|
||||||
)
|
|
||||||
|
|
||||||
if props.Type == "" {
|
if props.Type == "" {
|
||||||
props.Type = "text"
|
props.Type = "text"
|
||||||
|
|
@ -35,41 +32,18 @@ func Input(props InputProps) *h.Element {
|
||||||
input := h.Input(
|
input := h.Input(
|
||||||
props.Type,
|
props.Type,
|
||||||
h.Class("border p-2 rounded focus:outline-none focus:ring focus:ring-slate-800"),
|
h.Class("border p-2 rounded focus:outline-none focus:ring focus:ring-slate-800"),
|
||||||
h.If(
|
h.If(props.Name != "", h.Name(props.Name)),
|
||||||
props.Name != "",
|
h.If(props.Children != nil, h.Children(props.Children...)),
|
||||||
h.Name(props.Name),
|
h.If(props.Required, h.Required()),
|
||||||
),
|
h.If(props.Placeholder != "", h.Placeholder(props.Placeholder)),
|
||||||
h.If(
|
h.If(props.DefaultValue != "", h.Attribute("value", props.DefaultValue)),
|
||||||
props.Children != nil,
|
|
||||||
h.Children(props.Children...),
|
|
||||||
),
|
|
||||||
h.If(
|
|
||||||
props.Required,
|
|
||||||
h.Required(),
|
|
||||||
),
|
|
||||||
h.If(
|
|
||||||
props.Placeholder != "",
|
|
||||||
h.Placeholder(props.Placeholder),
|
|
||||||
),
|
|
||||||
h.If(
|
|
||||||
props.DefaultValue != "",
|
|
||||||
h.Attribute("value", props.DefaultValue),
|
|
||||||
),
|
|
||||||
validation,
|
validation,
|
||||||
)
|
)
|
||||||
|
|
||||||
wrapped := h.Div(
|
wrapped := h.Div(
|
||||||
h.If(
|
h.If(props.Id != "", h.Id(props.Id)),
|
||||||
props.Id != "",
|
|
||||||
h.Id(props.Id),
|
|
||||||
),
|
|
||||||
h.Class("flex flex-col gap-1"),
|
h.Class("flex flex-col gap-1"),
|
||||||
h.If(
|
h.If(props.Label != "", h.Label(h.Text(props.Label))),
|
||||||
props.Label != "",
|
|
||||||
h.Label(
|
|
||||||
h.Text(props.Label),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
input,
|
input,
|
||||||
h.Div(
|
h.Div(
|
||||||
h.Id(props.Id+"-error"),
|
h.Id(props.Id+"-error"),
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,7 @@ func CenteredForm(props CenteredFormProps) *h.Element {
|
||||||
h.Class("flex flex-col items-center justify-center min-h-screen bg-neutral-100"),
|
h.Class("flex flex-col items-center justify-center min-h-screen bg-neutral-100"),
|
||||||
h.Div(
|
h.Div(
|
||||||
h.Class("bg-white p-8 rounded-lg shadow-lg w-full max-w-md"),
|
h.Class("bg-white p-8 rounded-lg shadow-lg w-full max-w-md"),
|
||||||
h.H2F(
|
h.H2F(props.Title, h.Class("text-3xl font-bold text-center mb-6")),
|
||||||
props.Title,
|
|
||||||
h.Class("text-3xl font-bold text-center mb-6"),
|
|
||||||
),
|
|
||||||
h.Form(
|
h.Form(
|
||||||
h.TriggerChildren(),
|
h.TriggerChildren(),
|
||||||
h.Post(props.PostUrl),
|
h.Post(props.PostUrl),
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ go 1.23.0
|
||||||
require (
|
require (
|
||||||
entgo.io/ent v0.14.1
|
entgo.io/ent v0.14.1
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/maddalax/htmgo/framework v1.0.7-0.20250703190716-06f01b3d7c1b
|
github.com/maddalax/htmgo/framework v0.0.0-20241021150618-635b17dd7f9d
|
||||||
github.com/mattn/go-sqlite3 v1.14.23
|
github.com/mattn/go-sqlite3 v1.14.23
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,8 +33,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
github.com/maddalax/htmgo/framework v1.0.7-0.20250703190716-06f01b3d7c1b h1:m+xI+HBEQdie/Rs+mYI0HTFTMlYQSCv0l/siPDoywA4=
|
github.com/maddalax/htmgo/framework v0.0.0-20241021150618-635b17dd7f9d h1:++qKuof1JEd+r2sbckUx63CsBcwbFNY7iD/2An53JWc=
|
||||||
github.com/maddalax/htmgo/framework v1.0.7-0.20250703190716-06f01b3d7c1b/go.mod h1:NGGzWVXWksrQJ9kV9SGa/A1F1Bjsgc08cN7ZVb98RqY=
|
github.com/maddalax/htmgo/framework v0.0.0-20241021150618-635b17dd7f9d/go.mod h1:NGGzWVXWksrQJ9kV9SGa/A1F1Bjsgc08cN7ZVb98RqY=
|
||||||
github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0=
|
github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0=
|
||||||
github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
|
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,7 @@ import (
|
||||||
|
|
||||||
func RootPage(children ...h.Ren) h.Ren {
|
func RootPage(children ...h.Ren) h.Ren {
|
||||||
return h.Html(
|
return h.Html(
|
||||||
h.HxExtension(
|
h.HxExtension(h.BaseExtensions()),
|
||||||
h.BaseExtensions(),
|
|
||||||
),
|
|
||||||
h.Head(
|
h.Head(
|
||||||
h.Meta("viewport", "width=device-width, initial-scale=1"),
|
h.Meta("viewport", "width=device-width, initial-scale=1"),
|
||||||
h.Meta("title", "htmgo todo mvc"),
|
h.Meta("title", "htmgo todo mvc"),
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,7 @@ import (
|
||||||
func TaskListPage(ctx *h.RequestContext) *h.Page {
|
func TaskListPage(ctx *h.RequestContext) *h.Page {
|
||||||
|
|
||||||
title := h.Div(
|
title := h.Div(
|
||||||
h.H1(
|
h.H1(h.Class("text-7xl font-extralight text-rose-500 tracking-wide"), h.Text("todos")),
|
||||||
h.Class("text-7xl font-extralight text-rose-500 tracking-wide"),
|
|
||||||
h.Text("todos"),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return h.NewPage(base.RootPage(
|
return h.NewPage(base.RootPage(
|
||||||
|
|
@ -24,9 +21,7 @@ func TaskListPage(ctx *h.RequestContext) *h.Page {
|
||||||
title,
|
title,
|
||||||
task.Card(ctx),
|
task.Card(ctx),
|
||||||
h.Children(
|
h.Children(
|
||||||
h.Div(
|
h.Div(h.Text("Double-click to edit a todo")),
|
||||||
h.Text("Double-click to edit a todo"),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -58,9 +58,7 @@ func Input(list []*ent.Task) *h.Element {
|
||||||
h.Name("name"),
|
h.Name("name"),
|
||||||
h.Class("pl-12 text-xl p-4 w-full outline-none focus:outline-2 focus:outline-rose-400"),
|
h.Class("pl-12 text-xl p-4 w-full outline-none focus:outline-2 focus:outline-rose-400"),
|
||||||
h.Placeholder("What needs to be done?"),
|
h.Placeholder("What needs to be done?"),
|
||||||
h.Post(
|
h.Post(h.GetPartialPath(Create)),
|
||||||
h.GetPartialPath(Create),
|
|
||||||
),
|
|
||||||
h.HxTrigger(hx.OnEvent(hx.TriggerKeyUpEnter)),
|
h.HxTrigger(hx.OnEvent(hx.TriggerKeyUpEnter)),
|
||||||
),
|
),
|
||||||
CompleteAllIcon(list),
|
CompleteAllIcon(list),
|
||||||
|
|
@ -68,34 +66,23 @@ func Input(list []*ent.Task) *h.Element {
|
||||||
}
|
}
|
||||||
|
|
||||||
func CompleteAllIcon(list []*ent.Task) *h.Element {
|
func CompleteAllIcon(list []*ent.Task) *h.Element {
|
||||||
notCompletedCount := len(
|
notCompletedCount := len(h.Filter(list, func(item *ent.Task) bool {
|
||||||
h.Filter(list, func(item *ent.Task) bool {
|
|
||||||
return item.CompletedAt == nil
|
return item.CompletedAt == nil
|
||||||
}),
|
}))
|
||||||
)
|
|
||||||
|
|
||||||
return h.Div(
|
return h.Div(
|
||||||
h.ClassX("absolute top-1 left-5 p-2 rotate-90 text-3xl cursor-pointer", map[string]bool{
|
h.ClassX("absolute top-1 left-5 p-2 rotate-90 text-3xl cursor-pointer", map[string]bool{
|
||||||
"text-slate-400": notCompletedCount > 0,
|
"text-slate-400": notCompletedCount > 0,
|
||||||
}),
|
}), h.UnsafeRaw("›"),
|
||||||
h.UnsafeRaw("›"),
|
h.PostPartialWithQs(CompleteAll, h.NewQs("complete", h.Ternary(notCompletedCount > 0, "true", "false"))),
|
||||||
h.PostPartialWithQs(
|
|
||||||
CompleteAll,
|
|
||||||
h.NewQs(
|
|
||||||
"complete",
|
|
||||||
h.Ternary(notCompletedCount > 0, "true", "false"),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Footer(list []*ent.Task, activeTab Tab) *h.Element {
|
func Footer(list []*ent.Task, activeTab Tab) *h.Element {
|
||||||
|
|
||||||
notCompletedCount := len(
|
notCompletedCount := len(h.Filter(list, func(item *ent.Task) bool {
|
||||||
h.Filter(list, func(item *ent.Task) bool {
|
|
||||||
return item.CompletedAt == nil
|
return item.CompletedAt == nil
|
||||||
}),
|
}))
|
||||||
)
|
|
||||||
|
|
||||||
tabs := []Tab{TabAll, TabActive, TabComplete}
|
tabs := []Tab{TabAll, TabActive, TabComplete}
|
||||||
|
|
||||||
|
|
@ -109,12 +96,7 @@ func Footer(list []*ent.Task, activeTab Tab) *h.Element {
|
||||||
h.Class("flex items-center gap-4"),
|
h.Class("flex items-center gap-4"),
|
||||||
h.List(tabs, func(tab Tab, index int) *h.Element {
|
h.List(tabs, func(tab Tab, index int) *h.Element {
|
||||||
return h.P(
|
return h.P(
|
||||||
h.PostOnClick(
|
h.PostOnClick(h.GetPartialPathWithQs(ChangeTab, h.NewQs("tab", tab))),
|
||||||
h.GetPartialPathWithQs(
|
|
||||||
ChangeTab,
|
|
||||||
h.NewQs("tab", tab),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
h.ClassX("cursor-pointer px-2 py-1 rounded", map[string]bool{
|
h.ClassX("cursor-pointer px-2 py-1 rounded", map[string]bool{
|
||||||
"border border-rose-600": activeTab == tab,
|
"border border-rose-600": activeTab == tab,
|
||||||
}),
|
}),
|
||||||
|
|
@ -157,14 +139,12 @@ func Task(task *ent.Task, editing bool) *h.Element {
|
||||||
"border border-b-slate-100": !editing,
|
"border border-b-slate-100": !editing,
|
||||||
}),
|
}),
|
||||||
CompleteIcon(task),
|
CompleteIcon(task),
|
||||||
h.IfElse(
|
h.IfElse(editing,
|
||||||
editing,
|
|
||||||
h.Div(
|
h.Div(
|
||||||
h.Class("flex-1 h-full"),
|
h.Class("flex-1 h-full"),
|
||||||
h.Form(
|
h.Form(
|
||||||
h.Class("h-full"),
|
h.Class("h-full"),
|
||||||
h.Input(
|
h.Input("text",
|
||||||
"text",
|
|
||||||
h.Name("task"),
|
h.Name("task"),
|
||||||
h.Value(task.ID.String()),
|
h.Value(task.ID.String()),
|
||||||
h.Class("hidden"),
|
h.Class("hidden"),
|
||||||
|
|
@ -188,43 +168,30 @@ func Task(task *ent.Task, editing bool) *h.Element {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
h.P(
|
h.P(
|
||||||
h.GetPartialWithQs(
|
h.GetPartialWithQs(EditNameForm, h.NewQs("id", task.ID.String()), hx.TriggerDblClick),
|
||||||
EditNameForm,
|
|
||||||
h.NewQs("id", task.ID.String()),
|
|
||||||
hx.TriggerDblClick,
|
|
||||||
),
|
|
||||||
h.ClassX("text-xl break-all text-wrap truncate", map[string]bool{
|
h.ClassX("text-xl break-all text-wrap truncate", map[string]bool{
|
||||||
"line-through text-slate-400": task.CompletedAt != nil,
|
"line-through text-slate-400": task.CompletedAt != nil,
|
||||||
}),
|
}),
|
||||||
h.Text(task.Name),
|
h.Text(task.Name),
|
||||||
),
|
)),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func CompleteIcon(task *ent.Task) *h.Element {
|
func CompleteIcon(task *ent.Task) *h.Element {
|
||||||
return h.Div(
|
return h.Div(
|
||||||
h.HxTrigger(hx.OnClick()),
|
h.HxTrigger(hx.OnClick()),
|
||||||
h.Post(
|
h.Post(h.GetPartialPathWithQs(ToggleCompleted, h.NewQs("id", task.ID.String()))),
|
||||||
h.GetPartialPathWithQs(
|
|
||||||
ToggleCompleted,
|
|
||||||
h.NewQs("id", task.ID.String()),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
h.Class("flex items-center justify-center cursor-pointer"),
|
h.Class("flex items-center justify-center cursor-pointer"),
|
||||||
h.Div(
|
h.Div(
|
||||||
h.ClassX("w-10 h-10 border rounded-full flex items-center justify-center", map[string]bool{
|
h.ClassX("w-10 h-10 border rounded-full flex items-center justify-center", map[string]bool{
|
||||||
"border-green-500": task.CompletedAt != nil,
|
"border-green-500": task.CompletedAt != nil,
|
||||||
"border-slate-400": task.CompletedAt == nil,
|
"border-slate-400": task.CompletedAt == nil,
|
||||||
}),
|
}),
|
||||||
h.If(
|
h.If(task.CompletedAt != nil, h.UnsafeRaw(`
|
||||||
task.CompletedAt != nil,
|
|
||||||
h.UnsafeRaw(`
|
|
||||||
<svg class="w-6 h-6 text-green-500" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
<svg class="w-6 h-6 text-green-500" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"></path>
|
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"></path>
|
||||||
</svg>
|
</svg>
|
||||||
`),
|
`)),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -232,75 +199,46 @@ func CompleteIcon(task *ent.Task) *h.Element {
|
||||||
func UpdateName(ctx *h.RequestContext) *h.Partial {
|
func UpdateName(ctx *h.RequestContext) *h.Partial {
|
||||||
id, err := uuid.Parse(ctx.FormValue("task"))
|
id, err := uuid.Parse(ctx.FormValue("task"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return h.NewPartial(
|
return h.NewPartial(h.Div(h.Text("invalid id")))
|
||||||
h.Div(
|
|
||||||
h.Text("invalid id"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
name := ctx.FormValue("name")
|
name := ctx.FormValue("name")
|
||||||
if name == "" {
|
if name == "" {
|
||||||
return h.NewPartial(
|
return h.NewPartial(h.Div(h.Text("name is required")))
|
||||||
h.Div(
|
|
||||||
h.Text("name is required"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(name) > 150 {
|
if len(name) > 150 {
|
||||||
return h.NewPartial(
|
return h.NewPartial(h.Div(h.Text("task must be less than 150 characters")))
|
||||||
h.Div(
|
|
||||||
h.Text("task must be less than 150 characters"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
service := tasks.NewService(ctx)
|
service := tasks.NewService(ctx)
|
||||||
task, err := service.Get(id)
|
task, err := service.Get(id)
|
||||||
|
|
||||||
if task == nil {
|
if task == nil {
|
||||||
return h.NewPartial(
|
return h.NewPartial(h.Div(h.Text("task not found")))
|
||||||
h.Div(
|
|
||||||
h.Text("task not found"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
task, err = service.SetName(task.ID, name)
|
task, err = service.SetName(task.ID, name)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return h.NewPartial(
|
return h.NewPartial(h.Div(h.Text("failed to update")))
|
||||||
h.Div(
|
|
||||||
h.Text("failed to update"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return h.NewPartial(
|
return h.NewPartial(
|
||||||
h.OobSwap(ctx, Task(task, false)),
|
h.OobSwap(ctx, Task(task, false)))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func EditNameForm(ctx *h.RequestContext) *h.Partial {
|
func EditNameForm(ctx *h.RequestContext) *h.Partial {
|
||||||
id, err := uuid.Parse(ctx.QueryParam("id"))
|
id, err := uuid.Parse(ctx.QueryParam("id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return h.NewPartial(
|
return h.NewPartial(h.Div(h.Text("invalid id")))
|
||||||
h.Div(
|
|
||||||
h.Text("invalid id"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
service := tasks.NewService(ctx)
|
service := tasks.NewService(ctx)
|
||||||
task, err := service.Get(id)
|
task, err := service.Get(id)
|
||||||
|
|
||||||
if task == nil {
|
if task == nil {
|
||||||
return h.NewPartial(
|
return h.NewPartial(h.Div(h.Text("task not found")))
|
||||||
h.Div(
|
|
||||||
h.Text("task not found"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return h.NewPartial(
|
return h.NewPartial(
|
||||||
|
|
@ -311,36 +249,21 @@ func EditNameForm(ctx *h.RequestContext) *h.Partial {
|
||||||
func ToggleCompleted(ctx *h.RequestContext) *h.Partial {
|
func ToggleCompleted(ctx *h.RequestContext) *h.Partial {
|
||||||
id, err := uuid.Parse(ctx.QueryParam("id"))
|
id, err := uuid.Parse(ctx.QueryParam("id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return h.NewPartial(
|
return h.NewPartial(h.Div(h.Text("invalid id")))
|
||||||
h.Div(
|
|
||||||
h.Text("invalid id"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
service := tasks.NewService(ctx)
|
service := tasks.NewService(ctx)
|
||||||
task, err := service.Get(id)
|
task, err := service.Get(id)
|
||||||
|
|
||||||
if task == nil {
|
if task == nil {
|
||||||
return h.NewPartial(
|
return h.NewPartial(h.Div(h.Text("task not found")))
|
||||||
h.Div(
|
|
||||||
h.Text("task not found"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
task, err = service.SetCompleted(
|
task, err = service.SetCompleted(task.ID, h.
|
||||||
task.ID,
|
Ternary(task.CompletedAt == nil, true, false))
|
||||||
h.
|
|
||||||
Ternary(task.CompletedAt == nil, true, false),
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return h.NewPartial(
|
return h.NewPartial(h.Div(h.Text("failed to update")))
|
||||||
h.Div(
|
|
||||||
h.Text("failed to update"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
list, _ := service.List()
|
list, _ := service.List()
|
||||||
|
|
@ -359,9 +282,7 @@ func CompleteAll(ctx *h.RequestContext) *h.Partial {
|
||||||
|
|
||||||
list, _ := service.List()
|
list, _ := service.List()
|
||||||
|
|
||||||
return h.NewPartial(
|
return h.NewPartial(h.OobSwap(ctx, CardBody(list, getActiveTab(ctx))))
|
||||||
h.OobSwap(ctx, CardBody(list, getActiveTab(ctx))),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ClearCompleted(ctx *h.RequestContext) *h.Partial {
|
func ClearCompleted(ctx *h.RequestContext) *h.Partial {
|
||||||
|
|
@ -370,9 +291,7 @@ func ClearCompleted(ctx *h.RequestContext) *h.Partial {
|
||||||
|
|
||||||
list, _ := service.List()
|
list, _ := service.List()
|
||||||
|
|
||||||
return h.NewPartial(
|
return h.NewPartial(h.OobSwap(ctx, CardBody(list, getActiveTab(ctx))))
|
||||||
h.OobSwap(ctx, CardBody(list, getActiveTab(ctx))),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Create(ctx *h.RequestContext) *h.Partial {
|
func Create(ctx *h.RequestContext) *h.Partial {
|
||||||
|
|
@ -381,9 +300,7 @@ func Create(ctx *h.RequestContext) *h.Partial {
|
||||||
if len(name) > 150 {
|
if len(name) > 150 {
|
||||||
return h.NewPartial(
|
return h.NewPartial(
|
||||||
h.Div(
|
h.Div(
|
||||||
h.HxOnLoad(
|
h.HxOnLoad(js.Alert("Task must be less than 150 characters")),
|
||||||
js.Alert("Task must be less than 150 characters"),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -395,9 +312,7 @@ func Create(ctx *h.RequestContext) *h.Partial {
|
||||||
if list != nil && len(list) >= 100 {
|
if list != nil && len(list) >= 100 {
|
||||||
return h.NewPartial(
|
return h.NewPartial(
|
||||||
h.Div(
|
h.Div(
|
||||||
h.HxOnLoad(
|
h.HxOnLoad(js.Alert("There are too many tasks, please complete and clear some.")),
|
||||||
js.Alert("There are too many tasks, please complete and clear some."),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -407,11 +322,7 @@ func Create(ctx *h.RequestContext) *h.Partial {
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return h.NewPartial(
|
return h.NewPartial(h.Div(h.Text("failed to create")))
|
||||||
h.Div(
|
|
||||||
h.Text("failed to create"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
list, err = service.List()
|
list, err = service.List()
|
||||||
|
|
@ -427,12 +338,8 @@ func ChangeTab(ctx *h.RequestContext) *h.Partial {
|
||||||
|
|
||||||
tab := ctx.QueryParam("tab")
|
tab := ctx.QueryParam("tab")
|
||||||
|
|
||||||
return h.SwapManyPartialWithHeaders(
|
return h.SwapManyPartialWithHeaders(ctx,
|
||||||
ctx,
|
h.PushQsHeader(ctx, h.NewQs("tab", tab)),
|
||||||
h.PushQsHeader(
|
|
||||||
ctx,
|
|
||||||
h.NewQs("tab", tab),
|
|
||||||
),
|
|
||||||
List(list, tab),
|
List(list, tab),
|
||||||
Footer(list, tab),
|
Footer(list, tab),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
# Project exclude paths
|
|
||||||
/tmp/
|
|
||||||
node_modules/
|
|
||||||
dist/
|
|
||||||
js/dist
|
|
||||||
js/node_modules
|
|
||||||
go.work
|
|
||||||
go.work.sum
|
|
||||||
.idea
|
|
||||||
!framework/assets/dist
|
|
||||||
__htmgo
|
|
||||||
6
examples/ws-example/.gitignore
vendored
6
examples/ws-example/.gitignore
vendored
|
|
@ -1,6 +0,0 @@
|
||||||
/assets/dist
|
|
||||||
tmp
|
|
||||||
node_modules
|
|
||||||
.idea
|
|
||||||
__htmgo
|
|
||||||
dist
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
# Stage 1: Build the Go binary
|
|
||||||
FROM golang:1.23-alpine AS builder
|
|
||||||
|
|
||||||
RUN apk update
|
|
||||||
RUN apk add git
|
|
||||||
RUN apk add curl
|
|
||||||
|
|
||||||
# Set the working directory inside the container
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy go.mod and go.sum files
|
|
||||||
COPY go.mod go.sum ./
|
|
||||||
|
|
||||||
# Download and cache the Go modules
|
|
||||||
RUN go mod download
|
|
||||||
|
|
||||||
# Copy the source code into the container
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Build the Go binary for Linux
|
|
||||||
RUN GOPRIVATE=github.com/maddalax GOPROXY=direct go run github.com/maddalax/htmgo/cli/htmgo@latest build
|
|
||||||
|
|
||||||
|
|
||||||
# Stage 2: Create the smallest possible image
|
|
||||||
FROM gcr.io/distroless/base-debian11
|
|
||||||
|
|
||||||
# Set the working directory inside the container
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy the Go binary from the builder stage
|
|
||||||
COPY --from=builder /app/dist .
|
|
||||||
|
|
||||||
# Expose the necessary port (replace with your server port)
|
|
||||||
EXPOSE 3000
|
|
||||||
|
|
||||||
|
|
||||||
# Command to run the binary
|
|
||||||
CMD ["./ws-example"]
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
version: '3'
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
run:
|
|
||||||
cmds:
|
|
||||||
- go run github.com/maddalax/htmgo/cli/htmgo@latest run
|
|
||||||
silent: true
|
|
||||||
|
|
||||||
build:
|
|
||||||
cmds:
|
|
||||||
- go run github.com/maddalax/htmgo/cli/htmgo@latest build
|
|
||||||
|
|
||||||
docker:
|
|
||||||
cmds:
|
|
||||||
- docker build .
|
|
||||||
|
|
||||||
watch:
|
|
||||||
cmds:
|
|
||||||
- go run github.com/maddalax/htmgo/cli/htmgo@latest watch
|
|
||||||
silent: true
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
//go:build !prod
|
|
||||||
// +build !prod
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io/fs"
|
|
||||||
"ws-example/internal/embedded"
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetStaticAssets() fs.FS {
|
|
||||||
return embedded.NewOsFs()
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
//go:build prod
|
|
||||||
// +build prod
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"embed"
|
|
||||||
"io/fs"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed assets/dist/*
|
|
||||||
var staticAssets embed.FS
|
|
||||||
|
|
||||||
func GetStaticAssets() fs.FS {
|
|
||||||
return staticAssets
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
module ws-example
|
|
||||||
|
|
||||||
go 1.23.0
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/maddalax/htmgo/extensions/websocket v0.0.0-20241109180553-34e816ff7c8a
|
|
||||||
github.com/maddalax/htmgo/framework v1.0.7-0.20250703190716-06f01b3d7c1b
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/go-chi/chi/v5 v5.1.0 // indirect
|
|
||||||
github.com/gobwas/httphead v0.1.0 // indirect
|
|
||||||
github.com/gobwas/pool v0.2.1 // indirect
|
|
||||||
github.com/gobwas/ws v1.4.0 // indirect
|
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect
|
|
||||||
golang.org/x/sys v0.6.0 // indirect
|
|
||||||
)
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
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/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
|
|
||||||
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
|
||||||
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
|
||||||
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
|
||||||
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
|
||||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
|
||||||
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
|
|
||||||
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
|
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/maddalax/htmgo/extensions/websocket v0.0.0-20241109180553-34e816ff7c8a h1:BYVo9NCLHgXvf5pCGUnVg8UE7d9mWOyLgWXYTgVTkyA=
|
|
||||||
github.com/maddalax/htmgo/extensions/websocket v0.0.0-20241109180553-34e816ff7c8a/go.mod h1:r6/VqntLp7VlAUpIXy3MWZMHs2EkPKJP5rJdDL8lFP4=
|
|
||||||
github.com/maddalax/htmgo/framework v1.0.7-0.20250703190716-06f01b3d7c1b h1:m+xI+HBEQdie/Rs+mYI0HTFTMlYQSCv0l/siPDoywA4=
|
|
||||||
github.com/maddalax/htmgo/framework v1.0.7-0.20250703190716-06f01b3d7c1b/go.mod h1:NGGzWVXWksrQJ9kV9SGa/A1F1Bjsgc08cN7ZVb98RqY=
|
|
||||||
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/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4=
|
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
|
||||||
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
|
|
||||||
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
|
|
||||||
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
package embedded
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io/fs"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
type OsFs struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (receiver OsFs) Open(name string) (fs.File, error) {
|
|
||||||
return os.Open(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewOsFs() OsFs {
|
|
||||||
return OsFs{}
|
|
||||||
}
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/maddalax/htmgo/extensions/websocket"
|
|
||||||
ws2 "github.com/maddalax/htmgo/extensions/websocket/opts"
|
|
||||||
"github.com/maddalax/htmgo/extensions/websocket/session"
|
|
||||||
"github.com/maddalax/htmgo/framework/h"
|
|
||||||
"github.com/maddalax/htmgo/framework/service"
|
|
||||||
"io/fs"
|
|
||||||
"net/http"
|
|
||||||
"ws-example/__htmgo"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
locator := service.NewLocator()
|
|
||||||
|
|
||||||
h.Start(h.AppOpts{
|
|
||||||
ServiceLocator: locator,
|
|
||||||
LiveReload: true,
|
|
||||||
Register: func(app *h.App) {
|
|
||||||
|
|
||||||
app.Use(func(ctx *h.RequestContext) {
|
|
||||||
session.CreateSession(ctx)
|
|
||||||
})
|
|
||||||
|
|
||||||
websocket.EnableExtension(app, ws2.ExtensionOpts{
|
|
||||||
WsPath: "/ws",
|
|
||||||
RoomName: func(ctx *h.RequestContext) string {
|
|
||||||
return "all"
|
|
||||||
},
|
|
||||||
SessionId: func(ctx *h.RequestContext) string {
|
|
||||||
return ctx.QueryParam("sessionId")
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
sub, err := fs.Sub(GetStaticAssets(), "assets/dist")
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
http.FileServerFS(sub)
|
|
||||||
|
|
||||||
app.Router.Handle("/public/*", http.StripPrefix("/public", http.FileServerFS(sub)))
|
|
||||||
__htmgo.Register(app.Router)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
package pages
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/maddalax/htmgo/extensions/websocket/session"
|
|
||||||
"github.com/maddalax/htmgo/extensions/websocket/ws"
|
|
||||||
"github.com/maddalax/htmgo/framework/h"
|
|
||||||
"ws-example/partials"
|
|
||||||
)
|
|
||||||
|
|
||||||
func IndexPage(ctx *h.RequestContext) *h.Page {
|
|
||||||
sessionId := session.GetSessionId(ctx)
|
|
||||||
|
|
||||||
return h.NewPage(
|
|
||||||
RootPage(
|
|
||||||
ctx,
|
|
||||||
h.Div(
|
|
||||||
h.Attribute("ws-connect", fmt.Sprintf("/ws?sessionId=%s", sessionId)),
|
|
||||||
h.Class("flex flex-col gap-4 items-center pt-24 min-h-screen bg-neutral-100"),
|
|
||||||
h.H3(
|
|
||||||
h.Id("intro-text"),
|
|
||||||
h.Text("Repeater Example"),
|
|
||||||
h.Class("text-2xl"),
|
|
||||||
),
|
|
||||||
h.Div(
|
|
||||||
h.Id("ws-metrics"),
|
|
||||||
),
|
|
||||||
partials.CounterForm(ctx, partials.CounterProps{Id: "counter-1"}),
|
|
||||||
partials.Repeater(ctx, partials.RepeaterProps{
|
|
||||||
Id: "repeater-1",
|
|
||||||
OnAdd: func(data ws.HandlerData) {
|
|
||||||
//ws.BroadcastServerSideEvent("increment", map[string]any{})
|
|
||||||
},
|
|
||||||
OnRemove: func(data ws.HandlerData, index int) {
|
|
||||||
//ws.BroadcastServerSideEvent("decrement", map[string]any{})
|
|
||||||
},
|
|
||||||
AddButton: h.Button(
|
|
||||||
h.Text("+ Add Item"),
|
|
||||||
),
|
|
||||||
RemoveButton: func(index int, children ...h.Ren) *h.Element {
|
|
||||||
return h.Button(
|
|
||||||
h.Text("Remove"),
|
|
||||||
h.Children(children...),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
Item: func(index int) *h.Element {
|
|
||||||
return h.Input(
|
|
||||||
"text",
|
|
||||||
h.Class("border border-gray-300 rounded p-2"),
|
|
||||||
h.Value(fmt.Sprintf("item %d", index)),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
package pages
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/maddalax/htmgo/framework/h"
|
|
||||||
)
|
|
||||||
|
|
||||||
func RootPage(ctx *h.RequestContext, children ...h.Ren) h.Ren {
|
|
||||||
return h.Html(
|
|
||||||
h.JoinExtensions(
|
|
||||||
h.HxExtension(
|
|
||||||
h.BaseExtensions(),
|
|
||||||
),
|
|
||||||
h.HxExtension("ws"),
|
|
||||||
),
|
|
||||||
h.Head(
|
|
||||||
h.Link("/public/main.css", "stylesheet"),
|
|
||||||
h.Script("/public/htmgo.js"),
|
|
||||||
),
|
|
||||||
h.Body(
|
|
||||||
h.Div(
|
|
||||||
h.Class("flex flex-col gap-2 bg-white h-full"),
|
|
||||||
h.Fragment(children...),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,129 +0,0 @@
|
||||||
package ws
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/maddalax/htmgo/extensions/websocket/session"
|
|
||||||
"github.com/maddalax/htmgo/extensions/websocket/ws"
|
|
||||||
"github.com/maddalax/htmgo/framework/h"
|
|
||||||
"runtime"
|
|
||||||
"time"
|
|
||||||
"ws-example/pages"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Metrics(ctx *h.RequestContext) *h.Page {
|
|
||||||
|
|
||||||
ws.RunOnConnected(ctx, func() {
|
|
||||||
ws.PushElementCtx(
|
|
||||||
ctx,
|
|
||||||
metricsView(ctx),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
ws.Every(ctx, time.Second, func() bool {
|
|
||||||
return ws.PushElementCtx(
|
|
||||||
ctx,
|
|
||||||
metricsView(ctx),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
return h.NewPage(
|
|
||||||
pages.RootPage(
|
|
||||||
ctx,
|
|
||||||
h.Div(
|
|
||||||
h.Attribute("ws-connect", fmt.Sprintf("/ws?sessionId=%s", session.GetSessionId(ctx))),
|
|
||||||
h.Class("flex flex-col gap-4 items-center min-h-screen max-w-2xl mx-auto mt-8"),
|
|
||||||
h.H3(
|
|
||||||
h.Id("intro-text"),
|
|
||||||
h.Text("Websocket Metrics"),
|
|
||||||
h.Class("text-2xl"),
|
|
||||||
),
|
|
||||||
h.Div(
|
|
||||||
h.Id("ws-metrics"),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func metricsView(ctx *h.RequestContext) *h.Element {
|
|
||||||
metrics := ws.MetricsFromCtx(ctx)
|
|
||||||
|
|
||||||
return h.Div(
|
|
||||||
h.Id("ws-metrics"),
|
|
||||||
List(metrics),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func List(metrics ws.Metrics) *h.Element {
|
|
||||||
return h.Body(
|
|
||||||
h.Div(
|
|
||||||
h.Class("flow-root rounded-lg border border-gray-100 py-3 shadow-sm"),
|
|
||||||
h.Dl(
|
|
||||||
h.Class("-my-3 divide-y divide-gray-100 text-sm"),
|
|
||||||
ListItem("Current Time", time.Now().Format("15:04:05")),
|
|
||||||
ListItem("Seconds Elapsed", fmt.Sprintf("%d", metrics.Manager.SecondsElapsed)),
|
|
||||||
ListItem("Total Messages", fmt.Sprintf("%d", metrics.Manager.TotalMessages)),
|
|
||||||
ListItem("Messages Per Second", fmt.Sprintf("%d", metrics.Manager.MessagesPerSecond)),
|
|
||||||
ListItem("Total Goroutines For ws.Every", fmt.Sprintf("%d", metrics.Manager.RunningGoroutines)),
|
|
||||||
ListItem("Total Goroutines In System", fmt.Sprintf("%d", runtime.NumGoroutine())),
|
|
||||||
ListItem("Sockets", fmt.Sprintf("%d", metrics.Manager.TotalSockets)),
|
|
||||||
ListItem("Rooms", fmt.Sprintf("%d", metrics.Manager.TotalRooms)),
|
|
||||||
ListItem("Session Id To Hashes", fmt.Sprintf("%d", metrics.Handler.SessionIdToHashesCount)),
|
|
||||||
ListItem("Total Handlers", fmt.Sprintf("%d", metrics.Handler.TotalHandlers)),
|
|
||||||
ListItem("Server Event Names To Hash", fmt.Sprintf("%d", metrics.Handler.ServerEventNamesToHashCount)),
|
|
||||||
ListItem("Total Listeners", fmt.Sprintf("%d", metrics.Manager.TotalListeners)),
|
|
||||||
h.IterMap(metrics.Manager.SocketsPerRoom, func(key string, value []string) *h.Element {
|
|
||||||
return ListBlock(
|
|
||||||
fmt.Sprintf("Sockets In Room - %s", key),
|
|
||||||
h.IfElse(
|
|
||||||
len(value) > 100,
|
|
||||||
h.Div(
|
|
||||||
h.Pf("%d total sockets", len(value)),
|
|
||||||
),
|
|
||||||
h.Div(
|
|
||||||
h.List(value, func(item string, index int) *h.Element {
|
|
||||||
return h.Div(
|
|
||||||
h.Pf("%s", item),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ListItem(term, description string) *h.Element {
|
|
||||||
return h.Div(
|
|
||||||
h.Class("grid grid-cols-1 gap-1 p-3 even:bg-gray-50 sm:grid-cols-3 sm:gap-4"),
|
|
||||||
DescriptionTerm(term),
|
|
||||||
DescriptionDetail(description),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ListBlock(title string, children *h.Element) *h.Element {
|
|
||||||
return h.Div(
|
|
||||||
h.Class("grid grid-cols-1 gap-1 p-3 even:bg-gray-50 sm:grid-cols-3 sm:gap-4"),
|
|
||||||
DescriptionTerm(title),
|
|
||||||
h.Dd(
|
|
||||||
h.Class("text-gray-700 sm:col-span-2"),
|
|
||||||
children,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func DescriptionTerm(term string) *h.Element {
|
|
||||||
return h.Dt(
|
|
||||||
h.Class("font-medium text-gray-900"),
|
|
||||||
h.Text(term),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func DescriptionDetail(detail string) *h.Element {
|
|
||||||
return h.Dd(
|
|
||||||
h.Class("text-gray-700 sm:col-span-2"),
|
|
||||||
h.Text(detail),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,72 +0,0 @@
|
||||||
package partials
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/maddalax/htmgo/extensions/websocket/session"
|
|
||||||
"github.com/maddalax/htmgo/extensions/websocket/ws"
|
|
||||||
"github.com/maddalax/htmgo/framework/h"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Counter struct {
|
|
||||||
Count func() int
|
|
||||||
Increment func()
|
|
||||||
Decrement func()
|
|
||||||
}
|
|
||||||
|
|
||||||
func UseCounter(ctx *h.RequestContext, id string) Counter {
|
|
||||||
sessionId := session.GetSessionId(ctx)
|
|
||||||
get, set := session.UseState(sessionId, id, 0)
|
|
||||||
|
|
||||||
var increment = func() {
|
|
||||||
set(get() + 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
var decrement = func() {
|
|
||||||
set(get() - 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return Counter{
|
|
||||||
Count: get,
|
|
||||||
Increment: increment,
|
|
||||||
Decrement: decrement,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type CounterProps struct {
|
|
||||||
Id string
|
|
||||||
}
|
|
||||||
|
|
||||||
func CounterForm(ctx *h.RequestContext, props CounterProps) *h.Element {
|
|
||||||
if props.Id == "" {
|
|
||||||
props.Id = h.GenId(6)
|
|
||||||
}
|
|
||||||
counter := UseCounter(ctx, props.Id)
|
|
||||||
|
|
||||||
return h.Div(
|
|
||||||
h.Attribute("hx-swap", "none"),
|
|
||||||
h.Class("flex flex-col gap-3 items-center"),
|
|
||||||
h.Id(props.Id),
|
|
||||||
h.P(
|
|
||||||
h.Id("counter-text-"+props.Id),
|
|
||||||
h.AttributePairs(
|
|
||||||
"id", "counter",
|
|
||||||
"class", "text-xl",
|
|
||||||
"name", "count",
|
|
||||||
"text", "count",
|
|
||||||
),
|
|
||||||
h.TextF("Count: %d", counter.Count()),
|
|
||||||
),
|
|
||||||
h.Button(
|
|
||||||
h.Class("bg-rose-400 hover:bg-rose-500 text-white font-bold py-2 px-4 rounded"),
|
|
||||||
h.Type("submit"),
|
|
||||||
h.Text("Increment"),
|
|
||||||
ws.OnServerEvent(ctx, "increment", func(data ws.HandlerData) {
|
|
||||||
counter.Increment()
|
|
||||||
ws.PushElement(data, CounterForm(ctx, props))
|
|
||||||
}),
|
|
||||||
ws.OnServerEvent(ctx, "decrement", func(data ws.HandlerData) {
|
|
||||||
counter.Decrement()
|
|
||||||
ws.PushElement(data, CounterForm(ctx, props))
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
package partials
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/maddalax/htmgo/extensions/websocket/ws"
|
|
||||||
"github.com/maddalax/htmgo/framework/h"
|
|
||||||
)
|
|
||||||
|
|
||||||
type RepeaterProps struct {
|
|
||||||
Item func(index int) *h.Element
|
|
||||||
RemoveButton func(index int, children ...h.Ren) *h.Element
|
|
||||||
AddButton *h.Element
|
|
||||||
DefaultItems []*h.Element
|
|
||||||
Id string
|
|
||||||
currentIndex int
|
|
||||||
OnAdd func(data ws.HandlerData)
|
|
||||||
OnRemove func(data ws.HandlerData, index int)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (props *RepeaterProps) itemId(index int) string {
|
|
||||||
return fmt.Sprintf("%s-repeater-item-%d", props.Id, index)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (props *RepeaterProps) addButtonId() string {
|
|
||||||
return fmt.Sprintf("%s-repeater-add-button", props.Id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func repeaterItem(ctx *h.RequestContext, item *h.Element, index int, props *RepeaterProps) *h.Element {
|
|
||||||
id := props.itemId(index)
|
|
||||||
return h.Div(
|
|
||||||
h.Class("flex gap-2 items-center"),
|
|
||||||
h.Id(id),
|
|
||||||
item,
|
|
||||||
props.RemoveButton(
|
|
||||||
index,
|
|
||||||
h.ClassIf(index == 0, "opacity-0 disabled"),
|
|
||||||
h.If(
|
|
||||||
index == 0,
|
|
||||||
h.Disabled(),
|
|
||||||
),
|
|
||||||
ws.OnClick(ctx, func(data ws.HandlerData) {
|
|
||||||
props.OnRemove(data, index)
|
|
||||||
props.currentIndex--
|
|
||||||
ws.PushElement(
|
|
||||||
data,
|
|
||||||
h.Div(
|
|
||||||
h.Attribute("hx-swap-oob", fmt.Sprintf("delete:#%s", id)),
|
|
||||||
h.Div(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Repeater(ctx *h.RequestContext, props RepeaterProps) *h.Element {
|
|
||||||
if props.Id == "" {
|
|
||||||
props.Id = h.GenId(6)
|
|
||||||
}
|
|
||||||
return h.Div(
|
|
||||||
h.Class("flex flex-col gap-2"),
|
|
||||||
h.List(props.DefaultItems, func(item *h.Element, index int) *h.Element {
|
|
||||||
return repeaterItem(ctx, item, index, &props)
|
|
||||||
}),
|
|
||||||
h.Div(
|
|
||||||
h.Id(props.addButtonId()),
|
|
||||||
h.Class("flex justify-center"),
|
|
||||||
props.AddButton,
|
|
||||||
ws.OnClick(ctx, func(data ws.HandlerData) {
|
|
||||||
props.OnAdd(data)
|
|
||||||
ws.PushElement(
|
|
||||||
data,
|
|
||||||
h.Div(
|
|
||||||
h.Attribute("hx-swap-oob", "beforebegin:#"+props.addButtonId()),
|
|
||||||
repeaterItem(
|
|
||||||
ctx, props.Item(props.currentIndex), props.currentIndex, &props,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
props.currentIndex++
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
/** @type {import('tailwindcss').Config} */
|
|
||||||
module.exports = {
|
|
||||||
content: ["**/*.go"],
|
|
||||||
plugins: [],
|
|
||||||
};
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
module github.com/maddalax/htmgo/extensions/websocket
|
|
||||||
|
|
||||||
go 1.23.0
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/gobwas/ws v1.4.0
|
|
||||||
github.com/maddalax/htmgo/framework v1.0.7-0.20250703190716-06f01b3d7c1b
|
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.4.0
|
|
||||||
github.com/stretchr/testify v1.9.0
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
|
||||||
github.com/go-chi/chi/v5 v5.1.0 // indirect
|
|
||||||
github.com/gobwas/httphead v0.1.0 // indirect
|
|
||||||
github.com/gobwas/pool v0.2.1 // indirect
|
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
|
||||||
golang.org/x/sys v0.6.0 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
)
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
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/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
|
|
||||||
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
|
||||||
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
|
||||||
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
|
||||||
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
|
||||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
|
||||||
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
|
|
||||||
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
|
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/maddalax/htmgo/framework v1.0.7-0.20250703190716-06f01b3d7c1b h1:m+xI+HBEQdie/Rs+mYI0HTFTMlYQSCv0l/siPDoywA4=
|
|
||||||
github.com/maddalax/htmgo/framework v1.0.7-0.20250703190716-06f01b3d7c1b/go.mod h1:NGGzWVXWksrQJ9kV9SGa/A1F1Bjsgc08cN7ZVb98RqY=
|
|
||||||
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/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4=
|
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
|
||||||
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
|
|
||||||
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
|
|
||||||
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
package websocket
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/maddalax/htmgo/extensions/websocket/internal/wsutil"
|
|
||||||
"github.com/maddalax/htmgo/extensions/websocket/opts"
|
|
||||||
"github.com/maddalax/htmgo/extensions/websocket/ws"
|
|
||||||
"github.com/maddalax/htmgo/framework/h"
|
|
||||||
"github.com/maddalax/htmgo/framework/service"
|
|
||||||
)
|
|
||||||
|
|
||||||
func EnableExtension(app *h.App, opts opts.ExtensionOpts) {
|
|
||||||
if app.Opts.ServiceLocator == nil {
|
|
||||||
app.Opts.ServiceLocator = service.NewLocator()
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.WsPath == "" {
|
|
||||||
panic("websocket: WsPath is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.SessionId == nil {
|
|
||||||
panic("websocket: SessionId func is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
service.Set[wsutil.SocketManager](app.Opts.ServiceLocator, service.Singleton, func() *wsutil.SocketManager {
|
|
||||||
manager := wsutil.NewSocketManager(&opts)
|
|
||||||
manager.StartMetrics()
|
|
||||||
return manager
|
|
||||||
})
|
|
||||||
ws.StartListener(app.Opts.ServiceLocator)
|
|
||||||
app.Router.Handle(opts.WsPath, wsutil.WsHttpHandler(&opts))
|
|
||||||
}
|
|
||||||
|
|
@ -1,115 +0,0 @@
|
||||||
package wsutil
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"github.com/gobwas/ws"
|
|
||||||
"github.com/gobwas/ws/wsutil"
|
|
||||||
ws2 "github.com/maddalax/htmgo/extensions/websocket/opts"
|
|
||||||
"github.com/maddalax/htmgo/framework/h"
|
|
||||||
"github.com/maddalax/htmgo/framework/service"
|
|
||||||
"log/slog"
|
|
||||||
"net/http"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func WsHttpHandler(opts *ws2.ExtensionOpts) http.HandlerFunc {
|
|
||||||
|
|
||||||
if opts.RoomName == nil {
|
|
||||||
opts.RoomName = func(ctx *h.RequestContext) string {
|
|
||||||
return "all"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
cc := r.Context().Value(h.RequestContextKey).(*h.RequestContext)
|
|
||||||
locator := cc.ServiceLocator()
|
|
||||||
manager := service.Get[SocketManager](locator)
|
|
||||||
|
|
||||||
sessionId := opts.SessionId(cc)
|
|
||||||
|
|
||||||
if sessionId == "" {
|
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
conn, _, _, err := ws.UpgradeHTTP(r, w)
|
|
||||||
if err != nil {
|
|
||||||
slog.Info("failed to upgrade", slog.String("error", err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
roomId := opts.RoomName(cc)
|
|
||||||
/*
|
|
||||||
Large buffer in case the client disconnects while we are writing
|
|
||||||
we don't want to block the writer
|
|
||||||
*/
|
|
||||||
done := make(chan bool, 1000)
|
|
||||||
writer := make(WriterChan, 1000)
|
|
||||||
|
|
||||||
wg := sync.WaitGroup{}
|
|
||||||
|
|
||||||
manager.Add(roomId, sessionId, writer, done)
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This goroutine is responsible for writing messages to the client
|
|
||||||
*/
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer manager.Disconnect(sessionId)
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
for len(writer) > 0 {
|
|
||||||
<-writer
|
|
||||||
}
|
|
||||||
for len(done) > 0 {
|
|
||||||
<-done
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
ticker := time.NewTicker(5 * time.Second)
|
|
||||||
defer ticker.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-done:
|
|
||||||
fmt.Printf("closing connection: \n")
|
|
||||||
return
|
|
||||||
case <-ticker.C:
|
|
||||||
manager.Ping(sessionId)
|
|
||||||
case message := <-writer:
|
|
||||||
err = wsutil.WriteServerMessage(conn, ws.OpText, []byte(message))
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This goroutine is responsible for reading messages from the client
|
|
||||||
*/
|
|
||||||
go func() {
|
|
||||||
defer conn.Close()
|
|
||||||
for {
|
|
||||||
msg, op, err := wsutil.ReadClientData(conn)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if op != ws.OpText {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
m := make(map[string]any)
|
|
||||||
err = json.Unmarshal(msg, &m)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
manager.OnMessage(sessionId, m)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,365 +0,0 @@
|
||||||
package wsutil
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/maddalax/htmgo/extensions/websocket/opts"
|
|
||||||
"github.com/maddalax/htmgo/framework/h"
|
|
||||||
"github.com/maddalax/htmgo/framework/service"
|
|
||||||
"github.com/puzpuzpuz/xsync/v3"
|
|
||||||
"log/slog"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type EventType string
|
|
||||||
type WriterChan chan string
|
|
||||||
type DoneChan chan bool
|
|
||||||
|
|
||||||
const (
|
|
||||||
ConnectedEvent EventType = "connected"
|
|
||||||
DisconnectedEvent EventType = "disconnected"
|
|
||||||
MessageEvent EventType = "message"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SocketEvent struct {
|
|
||||||
SessionId string
|
|
||||||
RoomId string
|
|
||||||
Type EventType
|
|
||||||
Payload map[string]any
|
|
||||||
}
|
|
||||||
|
|
||||||
type CloseEvent struct {
|
|
||||||
Code int
|
|
||||||
Reason string
|
|
||||||
}
|
|
||||||
|
|
||||||
type SocketConnection struct {
|
|
||||||
Id string
|
|
||||||
RoomId string
|
|
||||||
Done DoneChan
|
|
||||||
Writer WriterChan
|
|
||||||
}
|
|
||||||
|
|
||||||
type ManagerMetrics struct {
|
|
||||||
RunningGoroutines int32
|
|
||||||
TotalSockets int
|
|
||||||
TotalRooms int
|
|
||||||
TotalListeners int
|
|
||||||
SocketsPerRoomCount map[string]int
|
|
||||||
SocketsPerRoom map[string][]string
|
|
||||||
TotalMessages int64
|
|
||||||
MessagesPerSecond int
|
|
||||||
SecondsElapsed int
|
|
||||||
}
|
|
||||||
|
|
||||||
type SocketManager struct {
|
|
||||||
sockets *xsync.MapOf[string, *xsync.MapOf[string, SocketConnection]]
|
|
||||||
idToRoom *xsync.MapOf[string, string]
|
|
||||||
listeners []chan SocketEvent
|
|
||||||
goroutinesRunning atomic.Int32
|
|
||||||
opts *opts.ExtensionOpts
|
|
||||||
lock sync.Mutex
|
|
||||||
totalMessages atomic.Int64
|
|
||||||
messagesPerSecond int
|
|
||||||
secondsElapsed int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (manager *SocketManager) StartMetrics() {
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
manager.lock.Lock()
|
|
||||||
manager.secondsElapsed++
|
|
||||||
totalMessages := manager.totalMessages.Load()
|
|
||||||
manager.messagesPerSecond = int(float64(totalMessages) / float64(manager.secondsElapsed))
|
|
||||||
manager.lock.Unlock()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (manager *SocketManager) Metrics() ManagerMetrics {
|
|
||||||
manager.lock.Lock()
|
|
||||||
defer manager.lock.Unlock()
|
|
||||||
count := manager.goroutinesRunning.Load()
|
|
||||||
metrics := ManagerMetrics{
|
|
||||||
RunningGoroutines: count,
|
|
||||||
TotalSockets: 0,
|
|
||||||
TotalRooms: 0,
|
|
||||||
TotalListeners: len(manager.listeners),
|
|
||||||
SocketsPerRoom: make(map[string][]string),
|
|
||||||
SocketsPerRoomCount: make(map[string]int),
|
|
||||||
TotalMessages: manager.totalMessages.Load(),
|
|
||||||
MessagesPerSecond: manager.messagesPerSecond,
|
|
||||||
SecondsElapsed: manager.secondsElapsed,
|
|
||||||
}
|
|
||||||
|
|
||||||
roomMap := make(map[string]int)
|
|
||||||
|
|
||||||
manager.idToRoom.Range(func(socketId string, roomId string) bool {
|
|
||||||
roomMap[roomId]++
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
metrics.TotalRooms = len(roomMap)
|
|
||||||
|
|
||||||
manager.sockets.Range(func(roomId string, sockets *xsync.MapOf[string, SocketConnection]) bool {
|
|
||||||
metrics.SocketsPerRoomCount[roomId] = sockets.Size()
|
|
||||||
sockets.Range(func(socketId string, conn SocketConnection) bool {
|
|
||||||
if metrics.SocketsPerRoom[roomId] == nil {
|
|
||||||
metrics.SocketsPerRoom[roomId] = []string{}
|
|
||||||
}
|
|
||||||
metrics.SocketsPerRoom[roomId] = append(metrics.SocketsPerRoom[roomId], socketId)
|
|
||||||
metrics.TotalSockets++
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
return metrics
|
|
||||||
}
|
|
||||||
|
|
||||||
func SocketManagerFromCtx(ctx *h.RequestContext) *SocketManager {
|
|
||||||
locator := ctx.ServiceLocator()
|
|
||||||
return service.Get[SocketManager](locator)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSocketManager(opts *opts.ExtensionOpts) *SocketManager {
|
|
||||||
return &SocketManager{
|
|
||||||
sockets: xsync.NewMapOf[string, *xsync.MapOf[string, SocketConnection]](),
|
|
||||||
idToRoom: xsync.NewMapOf[string, string](),
|
|
||||||
opts: opts,
|
|
||||||
goroutinesRunning: atomic.Int32{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (manager *SocketManager) ForEachSocket(roomId string, cb func(conn SocketConnection)) {
|
|
||||||
sockets, ok := manager.sockets.Load(roomId)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
sockets.Range(func(id string, conn SocketConnection) bool {
|
|
||||||
cb(conn)
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (manager *SocketManager) RunIntervalWithSocket(socketId string, interval time.Duration, cb func() bool) {
|
|
||||||
socketIdSlog := slog.String("socketId", socketId)
|
|
||||||
slog.Debug("ws-extension: starting every loop", socketIdSlog, slog.Duration("duration", interval))
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
manager.goroutinesRunning.Add(1)
|
|
||||||
defer manager.goroutinesRunning.Add(-1)
|
|
||||||
tries := 0
|
|
||||||
for {
|
|
||||||
socket := manager.Get(socketId)
|
|
||||||
// This can run before the socket is established, lets try a few times and kill it if socket isn't connected after a bit.
|
|
||||||
if socket == nil {
|
|
||||||
if tries > 200 {
|
|
||||||
slog.Debug("ws-extension: socket disconnected, killing goroutine", socketIdSlog)
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
time.Sleep(time.Millisecond * 15)
|
|
||||||
tries++
|
|
||||||
slog.Debug("ws-extension: socket not connected yet, trying again", socketIdSlog, slog.Int("attempt", tries))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
success := cb()
|
|
||||||
if !success {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
time.Sleep(interval)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (manager *SocketManager) Listen(listener chan SocketEvent) {
|
|
||||||
if manager.listeners == nil {
|
|
||||||
manager.listeners = make([]chan SocketEvent, 0)
|
|
||||||
}
|
|
||||||
if listener != nil {
|
|
||||||
manager.listeners = append(manager.listeners, listener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (manager *SocketManager) dispatch(event SocketEvent) {
|
|
||||||
done := make(chan struct{}, 1)
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-done:
|
|
||||||
return
|
|
||||||
case <-time.After(5 * time.Second):
|
|
||||||
fmt.Printf("havent dispatched event after 5s, chan blocked: %s\n", event.Type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
for _, listener := range manager.listeners {
|
|
||||||
listener <- event
|
|
||||||
}
|
|
||||||
done <- struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (manager *SocketManager) OnMessage(id string, message map[string]any) {
|
|
||||||
socket := manager.Get(id)
|
|
||||||
if socket == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
manager.totalMessages.Add(1)
|
|
||||||
manager.dispatch(SocketEvent{
|
|
||||||
SessionId: id,
|
|
||||||
Type: MessageEvent,
|
|
||||||
Payload: message,
|
|
||||||
RoomId: socket.RoomId,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (manager *SocketManager) Add(roomId string, id string, writer WriterChan, done DoneChan) {
|
|
||||||
manager.idToRoom.Store(id, roomId)
|
|
||||||
|
|
||||||
sockets, ok := manager.sockets.LoadOrCompute(roomId, func() *xsync.MapOf[string, SocketConnection] {
|
|
||||||
return xsync.NewMapOf[string, SocketConnection]()
|
|
||||||
})
|
|
||||||
|
|
||||||
sockets.Store(id, SocketConnection{
|
|
||||||
Id: id,
|
|
||||||
Writer: writer,
|
|
||||||
RoomId: roomId,
|
|
||||||
Done: done,
|
|
||||||
})
|
|
||||||
|
|
||||||
s, ok := sockets.Load(id)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
manager.dispatch(SocketEvent{
|
|
||||||
SessionId: s.Id,
|
|
||||||
Type: ConnectedEvent,
|
|
||||||
RoomId: s.RoomId,
|
|
||||||
Payload: map[string]any{},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (manager *SocketManager) OnClose(id string) {
|
|
||||||
socket := manager.Get(id)
|
|
||||||
if socket == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
slog.Debug("ws-extension: removing socket from manager", slog.String("socketId", id))
|
|
||||||
manager.dispatch(SocketEvent{
|
|
||||||
SessionId: id,
|
|
||||||
Type: DisconnectedEvent,
|
|
||||||
RoomId: socket.RoomId,
|
|
||||||
Payload: map[string]any{},
|
|
||||||
})
|
|
||||||
roomId, ok := manager.idToRoom.Load(id)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
sockets, ok := manager.sockets.Load(roomId)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
sockets.Delete(id)
|
|
||||||
manager.idToRoom.Delete(id)
|
|
||||||
slog.Debug("ws-extension: removed socket from manager", slog.String("socketId", id))
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (manager *SocketManager) CloseWithMessage(id string, message string) {
|
|
||||||
conn := manager.Get(id)
|
|
||||||
if conn != nil {
|
|
||||||
defer manager.OnClose(id)
|
|
||||||
manager.writeText(*conn, message)
|
|
||||||
conn.Done <- true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (manager *SocketManager) Disconnect(id string) {
|
|
||||||
conn := manager.Get(id)
|
|
||||||
if conn != nil {
|
|
||||||
manager.OnClose(id)
|
|
||||||
conn.Done <- true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (manager *SocketManager) Get(id string) *SocketConnection {
|
|
||||||
roomId, ok := manager.idToRoom.Load(id)
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
sockets, ok := manager.sockets.Load(roomId)
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
conn, ok := sockets.Load(id)
|
|
||||||
return &conn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (manager *SocketManager) Ping(id string) bool {
|
|
||||||
conn := manager.Get(id)
|
|
||||||
if conn != nil {
|
|
||||||
return manager.writeText(*conn, "ping")
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (manager *SocketManager) writeCloseRaw(writer WriterChan, message string) {
|
|
||||||
manager.writeTextRaw(writer, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (manager *SocketManager) writeTextRaw(writer WriterChan, message string) {
|
|
||||||
timeout := 3 * time.Second
|
|
||||||
select {
|
|
||||||
case writer <- message:
|
|
||||||
case <-time.After(timeout):
|
|
||||||
fmt.Printf("could not send %s to channel after %s\n", message, timeout)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (manager *SocketManager) writeText(socket SocketConnection, message string) bool {
|
|
||||||
if socket.Writer == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
manager.writeTextRaw(socket.Writer, message)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (manager *SocketManager) BroadcastText(roomId string, message string, predicate func(conn SocketConnection) bool) {
|
|
||||||
sockets, ok := manager.sockets.Load(roomId)
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
sockets.Range(func(id string, conn SocketConnection) bool {
|
|
||||||
if predicate(conn) {
|
|
||||||
manager.writeText(conn, message)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (manager *SocketManager) SendHtml(id string, message string) bool {
|
|
||||||
conn := manager.Get(id)
|
|
||||||
minified := strings.ReplaceAll(message, "\n", "")
|
|
||||||
minified = strings.ReplaceAll(minified, "\t", "")
|
|
||||||
minified = strings.TrimSpace(minified)
|
|
||||||
if conn != nil {
|
|
||||||
return manager.writeText(*conn, minified)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (manager *SocketManager) SendText(id string, message string) bool {
|
|
||||||
conn := manager.Get(id)
|
|
||||||
if conn != nil {
|
|
||||||
return manager.writeText(*conn, message)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
@ -1,202 +0,0 @@
|
||||||
package wsutil
|
|
||||||
|
|
||||||
import (
|
|
||||||
ws2 "github.com/maddalax/htmgo/extensions/websocket/opts"
|
|
||||||
"github.com/maddalax/htmgo/framework/h"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func createManager() *SocketManager {
|
|
||||||
return NewSocketManager(&ws2.ExtensionOpts{
|
|
||||||
WsPath: "/ws",
|
|
||||||
SessionId: func(ctx *h.RequestContext) string {
|
|
||||||
return "test"
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func addSocket(manager *SocketManager, roomId string, id string) (socketId string, writer WriterChan, done DoneChan) {
|
|
||||||
writer = make(chan string, 10)
|
|
||||||
done = make(chan bool, 10)
|
|
||||||
manager.Add(roomId, id, writer, done)
|
|
||||||
return id, writer, done
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager(t *testing.T) {
|
|
||||||
manager := createManager()
|
|
||||||
socketId, _, _ := addSocket(manager, "123", "456")
|
|
||||||
socket := manager.Get(socketId)
|
|
||||||
assert.NotNil(t, socket)
|
|
||||||
assert.Equal(t, socketId, socket.Id)
|
|
||||||
|
|
||||||
manager.OnClose(socketId)
|
|
||||||
socket = manager.Get(socketId)
|
|
||||||
assert.Nil(t, socket)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManagerForEachSocket(t *testing.T) {
|
|
||||||
manager := createManager()
|
|
||||||
addSocket(manager, "all", "456")
|
|
||||||
addSocket(manager, "all", "789")
|
|
||||||
var count int
|
|
||||||
manager.ForEachSocket("all", func(conn SocketConnection) {
|
|
||||||
count++
|
|
||||||
})
|
|
||||||
assert.Equal(t, 2, count)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSendText(t *testing.T) {
|
|
||||||
manager := createManager()
|
|
||||||
socketId, writer, done := addSocket(manager, "all", "456")
|
|
||||||
manager.SendText(socketId, "hello")
|
|
||||||
assert.Equal(t, "hello", <-writer)
|
|
||||||
manager.SendText(socketId, "hello2")
|
|
||||||
assert.Equal(t, "hello2", <-writer)
|
|
||||||
done <- true
|
|
||||||
assert.Equal(t, true, <-done)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBroadcastText(t *testing.T) {
|
|
||||||
manager := createManager()
|
|
||||||
_, w1, d1 := addSocket(manager, "all", "456")
|
|
||||||
_, w2, d2 := addSocket(manager, "all", "789")
|
|
||||||
manager.BroadcastText("all", "hello", func(conn SocketConnection) bool {
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
assert.Equal(t, "hello", <-w1)
|
|
||||||
assert.Equal(t, "hello", <-w2)
|
|
||||||
d1 <- true
|
|
||||||
d2 <- true
|
|
||||||
assert.Equal(t, true, <-d1)
|
|
||||||
assert.Equal(t, true, <-d2)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBroadcastTextWithPredicate(t *testing.T) {
|
|
||||||
manager := createManager()
|
|
||||||
_, w1, _ := addSocket(manager, "all", "456")
|
|
||||||
_, w2, _ := addSocket(manager, "all", "789")
|
|
||||||
manager.BroadcastText("all", "hello", func(conn SocketConnection) bool {
|
|
||||||
return conn.Id != "456"
|
|
||||||
})
|
|
||||||
|
|
||||||
assert.Equal(t, 0, len(w1))
|
|
||||||
assert.Equal(t, 1, len(w2))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSendHtml(t *testing.T) {
|
|
||||||
manager := createManager()
|
|
||||||
socketId, writer, _ := addSocket(manager, "all", "456")
|
|
||||||
rendered := h.Render(
|
|
||||||
h.Div(
|
|
||||||
h.P(
|
|
||||||
h.Text("hello"),
|
|
||||||
),
|
|
||||||
))
|
|
||||||
manager.SendHtml(socketId, rendered)
|
|
||||||
assert.Equal(t, "<div><p>hello</p></div>", <-writer)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOnMessage(t *testing.T) {
|
|
||||||
manager := createManager()
|
|
||||||
socketId, _, _ := addSocket(manager, "all", "456")
|
|
||||||
|
|
||||||
listener := make(chan SocketEvent, 10)
|
|
||||||
|
|
||||||
manager.Listen(listener)
|
|
||||||
|
|
||||||
manager.OnMessage(socketId, map[string]any{
|
|
||||||
"message": "hello",
|
|
||||||
})
|
|
||||||
|
|
||||||
event := <-listener
|
|
||||||
assert.Equal(t, "hello", event.Payload["message"])
|
|
||||||
assert.Equal(t, "456", event.SessionId)
|
|
||||||
assert.Equal(t, MessageEvent, event.Type)
|
|
||||||
assert.Equal(t, "all", event.RoomId)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOnClose(t *testing.T) {
|
|
||||||
manager := createManager()
|
|
||||||
socketId, _, _ := addSocket(manager, "all", "456")
|
|
||||||
listener := make(chan SocketEvent, 10)
|
|
||||||
manager.Listen(listener)
|
|
||||||
manager.OnClose(socketId)
|
|
||||||
event := <-listener
|
|
||||||
assert.Equal(t, "456", event.SessionId)
|
|
||||||
assert.Equal(t, DisconnectedEvent, event.Type)
|
|
||||||
assert.Equal(t, "all", event.RoomId)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOnAdd(t *testing.T) {
|
|
||||||
manager := createManager()
|
|
||||||
|
|
||||||
listener := make(chan SocketEvent, 10)
|
|
||||||
manager.Listen(listener)
|
|
||||||
|
|
||||||
socketId, _, _ := addSocket(manager, "all", "456")
|
|
||||||
event := <-listener
|
|
||||||
|
|
||||||
assert.Equal(t, socketId, event.SessionId)
|
|
||||||
assert.Equal(t, ConnectedEvent, event.Type)
|
|
||||||
assert.Equal(t, "all", event.RoomId)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCloseWithMessage(t *testing.T) {
|
|
||||||
manager := createManager()
|
|
||||||
socketId, w, _ := addSocket(manager, "all", "456")
|
|
||||||
manager.CloseWithMessage(socketId, "internal error")
|
|
||||||
assert.Equal(t, "internal error", <-w)
|
|
||||||
assert.Nil(t, manager.Get(socketId))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDisconnect(t *testing.T) {
|
|
||||||
manager := createManager()
|
|
||||||
socketId, _, _ := addSocket(manager, "all", "456")
|
|
||||||
manager.Disconnect(socketId)
|
|
||||||
assert.Nil(t, manager.Get(socketId))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPing(t *testing.T) {
|
|
||||||
manager := createManager()
|
|
||||||
socketId, w, _ := addSocket(manager, "all", "456")
|
|
||||||
manager.Ping(socketId)
|
|
||||||
assert.Equal(t, "ping", <-w)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMultipleRooms(t *testing.T) {
|
|
||||||
manager := createManager()
|
|
||||||
socketId1, _, _ := addSocket(manager, "room1", "456")
|
|
||||||
socketId2, _, _ := addSocket(manager, "room2", "789")
|
|
||||||
|
|
||||||
room1Count := 0
|
|
||||||
room2Count := 0
|
|
||||||
|
|
||||||
manager.ForEachSocket("room1", func(conn SocketConnection) {
|
|
||||||
room1Count++
|
|
||||||
})
|
|
||||||
|
|
||||||
manager.ForEachSocket("room2", func(conn SocketConnection) {
|
|
||||||
room2Count++
|
|
||||||
})
|
|
||||||
|
|
||||||
assert.Equal(t, 1, room1Count)
|
|
||||||
assert.Equal(t, 1, room2Count)
|
|
||||||
|
|
||||||
room1Count = 0
|
|
||||||
room2Count = 0
|
|
||||||
|
|
||||||
manager.OnClose(socketId1)
|
|
||||||
manager.OnClose(socketId2)
|
|
||||||
|
|
||||||
manager.ForEachSocket("room1", func(conn SocketConnection) {
|
|
||||||
room1Count++
|
|
||||||
})
|
|
||||||
|
|
||||||
manager.ForEachSocket("room2", func(conn SocketConnection) {
|
|
||||||
room2Count++
|
|
||||||
})
|
|
||||||
|
|
||||||
assert.Equal(t, 0, room1Count)
|
|
||||||
assert.Equal(t, 0, room2Count)
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
package opts
|
|
||||||
|
|
||||||
import "github.com/maddalax/htmgo/framework/h"
|
|
||||||
|
|
||||||
type ExtensionOpts struct {
|
|
||||||
WsPath string
|
|
||||||
RoomName func(ctx *h.RequestContext) string
|
|
||||||
SessionId func(ctx *h.RequestContext) string
|
|
||||||
}
|
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
package session
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/maddalax/htmgo/framework/h"
|
|
||||||
"github.com/puzpuzpuz/xsync/v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Id string
|
|
||||||
|
|
||||||
var cache = xsync.NewMapOf[Id, *xsync.MapOf[string, any]]()
|
|
||||||
|
|
||||||
type State struct {
|
|
||||||
SessionId Id
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewState(ctx *h.RequestContext) *State {
|
|
||||||
id := GetSessionId(ctx)
|
|
||||||
cache.Store(id, xsync.NewMapOf[string, any]())
|
|
||||||
return &State{
|
|
||||||
SessionId: id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func CreateSession(ctx *h.RequestContext) Id {
|
|
||||||
sessionId := fmt.Sprintf("session-id-%s", h.GenId(30))
|
|
||||||
ctx.Set("session-id", sessionId)
|
|
||||||
return Id(sessionId)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetSessionId(ctx *h.RequestContext) Id {
|
|
||||||
sessionIdRaw := ctx.Get("session-id")
|
|
||||||
sessionId := ""
|
|
||||||
|
|
||||||
if sessionIdRaw == "" || sessionIdRaw == nil {
|
|
||||||
panic("session id is not set, please use session.CreateSession(ctx) in middleware to create a session id")
|
|
||||||
} else {
|
|
||||||
sessionId = sessionIdRaw.(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
return Id(sessionId)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Update[T any](sessionId Id, key string, compute func(prev T) T) T {
|
|
||||||
actual := Get[T](sessionId, key, *new(T))
|
|
||||||
next := compute(actual)
|
|
||||||
Set(sessionId, key, next)
|
|
||||||
return next
|
|
||||||
}
|
|
||||||
|
|
||||||
func Get[T any](sessionId Id, key string, fallback T) T {
|
|
||||||
actual, _ := cache.LoadOrCompute(sessionId, func() *xsync.MapOf[string, any] {
|
|
||||||
return xsync.NewMapOf[string, any]()
|
|
||||||
})
|
|
||||||
value, exists := actual.Load(key)
|
|
||||||
if exists {
|
|
||||||
return value.(T)
|
|
||||||
}
|
|
||||||
return fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
func Set(sessionId Id, key string, value any) {
|
|
||||||
actual, _ := cache.LoadOrCompute(sessionId, func() *xsync.MapOf[string, any] {
|
|
||||||
return xsync.NewMapOf[string, any]()
|
|
||||||
})
|
|
||||||
actual.Store(key, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func UseState[T any](sessionId Id, key string, initial T) (func() T, func(T)) {
|
|
||||||
var get = func() T {
|
|
||||||
return Get[T](sessionId, key, initial)
|
|
||||||
}
|
|
||||||
var set = func(value T) {
|
|
||||||
Set(sessionId, key, value)
|
|
||||||
}
|
|
||||||
return get, set
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
package ws
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/maddalax/htmgo/extensions/websocket/internal/wsutil"
|
|
||||||
"github.com/maddalax/htmgo/framework/h"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ManagerFromCtx(ctx *h.RequestContext) *wsutil.SocketManager {
|
|
||||||
return wsutil.SocketManagerFromCtx(ctx)
|
|
||||||
}
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
package ws
|
|
||||||
|
|
||||||
import "github.com/maddalax/htmgo/framework/h"
|
|
||||||
|
|
||||||
func OnClick(ctx *h.RequestContext, handler Handler) *h.AttributeMapOrdered {
|
|
||||||
return AddClientSideHandler(ctx, "click", handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func OnClientEvent(ctx *h.RequestContext, eventName string, handler Handler) *h.AttributeMapOrdered {
|
|
||||||
return AddClientSideHandler(ctx, eventName, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func OnServerEvent(ctx *h.RequestContext, eventName string, handler Handler) h.Ren {
|
|
||||||
AddServerSideHandler(ctx, eventName, handler)
|
|
||||||
return h.Attribute("data-handler-id", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
func OnMouseOver(ctx *h.RequestContext, handler Handler) *h.AttributeMapOrdered {
|
|
||||||
return AddClientSideHandler(ctx, "mouseover", handler)
|
|
||||||
}
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
package ws
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/maddalax/htmgo/extensions/websocket/internal/wsutil"
|
|
||||||
"github.com/maddalax/htmgo/extensions/websocket/session"
|
|
||||||
"github.com/maddalax/htmgo/framework/h"
|
|
||||||
"github.com/maddalax/htmgo/framework/service"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PushServerSideEvent sends a server side event this specific session
|
|
||||||
func PushServerSideEvent(data HandlerData, event string, value map[string]any) {
|
|
||||||
serverSideMessageListener <- ServerSideEvent{
|
|
||||||
Event: event,
|
|
||||||
Payload: value,
|
|
||||||
SessionId: data.SessionId,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BroadcastServerSideEvent sends a server side event to all clients that have a handler for the event, not just the current session
|
|
||||||
func BroadcastServerSideEvent(event string, value map[string]any) {
|
|
||||||
serverSideMessageListener <- ServerSideEvent{
|
|
||||||
Event: event,
|
|
||||||
Payload: value,
|
|
||||||
SessionId: "*",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PushElement sends an element to the current session and swaps it into the page
|
|
||||||
func PushElement(data HandlerData, el *h.Element) bool {
|
|
||||||
return data.Manager.SendHtml(data.Socket.Id, h.Render(el))
|
|
||||||
}
|
|
||||||
|
|
||||||
// PushElementCtx sends an element to the current session and swaps it into the page
|
|
||||||
func PushElementCtx(ctx *h.RequestContext, el *h.Element) bool {
|
|
||||||
locator := ctx.ServiceLocator()
|
|
||||||
socketManager := service.Get[wsutil.SocketManager](locator)
|
|
||||||
socketId := session.GetSessionId(ctx)
|
|
||||||
socket := socketManager.Get(string(socketId))
|
|
||||||
if socket == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return PushElement(HandlerData{
|
|
||||||
Socket: socket,
|
|
||||||
Manager: socketManager,
|
|
||||||
SessionId: socketId,
|
|
||||||
}, el)
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue