commit 251470852267cdead820d801511bd2ca59854b4a Author: maddalax Date: Mon Jan 22 09:22:16 2024 -0600 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4af982c --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# Project exclude paths +/tmp/ \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..b58b603 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,5 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/dataSources.local.xml b/.idea/dataSources.local.xml new file mode 100644 index 0000000..61a332e --- /dev/null +++ b/.idea/dataSources.local.xml @@ -0,0 +1,18 @@ + + + + + + " + + + master_key + no-auth + + + + + + + + \ No newline at end of file diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000..046eb30 --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,12 @@ + + + + + sqlite.xerial + true + org.sqlite.JDBC + jdbc:sqlite:$PROJECT_DIR$/site.db + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/.idea/dataSources/6b63d5bd-e451-4904-b659-21db5c54c16d.xml b/.idea/dataSources/6b63d5bd-e451-4904-b659-21db5c54c16d.xml new file mode 100644 index 0000000..74f2361 --- /dev/null +++ b/.idea/dataSources/6b63d5bd-e451-4904-b659-21db5c54c16d.xml @@ -0,0 +1,1475 @@ + + + + + 3.40.1 + + + + + + + + + + + + + + + + + 1 + + + window + + + 1 + + + 1 + 1 + + + window + + + 1 + + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + 1 + + + + 1 + + + + 1 + 1 + + + 1 + 1 + + + 1 + 1 + + + 1 + + + 1 + + + window + + + 1 + + + 1 + 1 + + + 1 + + + 1 + 1 + + + 1 + 1 + + + window + + + window + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + window + + + 1 + + + + 1 + + + 1 + 1 + + + 1 + + + 1 + + + window + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + + 1 + window + + + window + + + 1 + + + 1 + window + + + 1 + + + 1 + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + + 1 + + + window + + + window + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + 1 + + + 1 + 1 + + + 1 + + + window + + + 1 + 1 + + + 1 + + + + + + 1 + + + 1 + + + 1 + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + + 1 + 1 + + + 1 + 1 + + + window + + + 1 + + + 1 + + + 1 + + + window + + + 1 + + + 1 + + + + + window + + + window + + + 1 + + + 1 + + + 1 + + + 1 + 1 + + + window + + + 1 + 1 + + + window + + + window + + + window + + + 1 + + + 1 + + + 1 + + + window + + + 1 + + + 1 + + + 1 + + + 1 + 1 + + + aggregate + + + aggregate + + + aggregate + + + + + aggregate + + + + + + 1 + + + + + aggregate + + + + + + + 1 + + + + + + + + aggregate + + + + + + + 1 + + + + 1 + + + 1 + + + + + + + + + 1 + + + R + + + 1 + + + 2 + + + R + + + 1 + + + 2 + + + R + + + 1 + + + 2 + + + R + + + R + + + 1 + + + R + + + 1 + + + 2 + + + R + + + 1 + + + R + + + 1 + + + R + + + 1 + + + R + + + 1 + + + 2 + + + R + + + 1 + + + 2 + + + R + + + R + + + R + + + 1 + + + 2 + + + R + + + 1 + + + R + + + R + + + R + + + R + + + 1 + + + 2 + + + R + + + 1 + + + R + + + 1 + + + R + + + 1 + + + R + + + R + + + 1 + + + 2 + + + R + + + R + + + R + + + 1 + + + R + + + R + + + 1 + + + R + + + 1 + + + R + + + 1 + + + R + + + 1 + + + R + + + 1 + + + 2 + + + R + + + 1 + + + 2 + + + R + + + 1 + + + 2 + + + R + + + 1 + + + R + + + R + + + 1 + + + 2 + + + R + + + R + + + 1 + + + R + + + 1 + + + R + + + 1 + + + R + + + 1 + + + 2 + + + 3 + + + R + + + 1 + + + 2 + + + 3 + + + R + + + 1 + + + R + + + 1 + + + R + + + 1 + + + R + + + 1 + + + 2 + + + R + + + 1 + + + R + + + 1 + + + R + + + 1 + + + R + + + 1 + + + 2 + + + R + + + 1 + + + 2 + + + R + + + R + + + 1 + + + R + + + 1 + + + R + + + 1 + + + R + + + 1 + + + 2 + + + 3 + + + R + + + 1 + + + R + + + 1 + + + R + + + R + + + R + + + 1 + + + R + + + 1 + + + R + + + 1 + + + R + + + 1 + + + R + + + 1 + + + 2 + + + R + + + R + + + 1 + + + 2 + + + R + + + 1 + + + R + + + 1 + + + R + + + 1 + + + R + + + R + + + R + + + 1 + + + R + + + 1 + + + R + + + R + + + 1 + + + R + + + R + + + R + + + R + + + 1 + + + R + + + 1 + + + R + + + R + + + 1 + + + 2 + + + R + + + 1 + + + R + + + 1 + + + R + + + 1 + + + 2 + + + 3 + + + R + + + R + + + R + + + R + + + R + + + 1 + + + R + + + 1 + + + 2 + + + R + + + 1 + + + R + + + R + + + 1 + + + 2 + + + R + + + 1 + + + 2 + + + R + + + R + + + R + + + 1 + + + 2 + + + R + + + 1 + + + 2 + + + 3 + + + R + + + 1 + + + 2 + + + R + + + 1 + + + R + + + 1 + + + 2 + + + 3 + + + R + + + R + + + 1 + + + R + + + R + + + 1 + + + R + + + 1 + + + 2 + + + R + + + 1 + + + 2 + + + 3 + + + R + + + 1 + + + R + + + 1 + + + R + + + 1 + + + 2 + + + R + + + 1 + + + R + + + R + + + 1 + + + R + + + 1 + + + R + + + R + + + 1 + + + R + + + 1 + + + R + + + 1 + + + R + + + 1 + + + 2 + + + R + + + 1 + + + 2 + + + R + + + 1 + + + R + + + 1 + + + 2 + + + R + + + 1 + + + 2 + + + R + + + 1 + + + R + + + R + + + 1 + + + 2 + + + R + + + 1 + + + 2 + + + R + + + 1 + + + R + + + 1 + + + 2 + + + R + + + 1 + + + R + + + 1 + + + 2 + + + R + + + 1 + + + 2 + + + R + + + R + + + 1 + + + R + + + R + + + 1 + + + R + + + 1 + + + 2 + + + R + + + 1 + + + R + + + 1 + + + R + + + 1 + + + R + + + 1 + + + 2 + + + R + + + 1 + + + R + + + 1 + + + R + + + R + + + 1 + + + 2 + + + R + + + R + + + R + + + 1 + + + 2 + + + 3 + + + R + + + 1 + + + 2 + + + R + + + 1 + + + R + + + 1 + + + 2 + + + R + + + 1 + + + R + + + 1 + + + 1 +
+ + + TEXT|0s + 1 + + + TEXT|0s + 2 + + + TEXT|0s + 3 + + + INT|0s + 4 + + + TEXT|0s + 5 + + + text|0s + 1 + 1 + + + text|0s + 1 + 2 + + + id + 1 + 1 + + + id + 1 + sqlite_autoindex_users_1 + + + id + sqlite_autoindex_users_1 + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..0e32f00 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/.idea/mhtml.iml b/.idea/mhtml.iml new file mode 100644 index 0000000..cf2da3e --- /dev/null +++ b/.idea/mhtml.iml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..eaf7aed --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/watcherTasks.xml b/.idea/watcherTasks.xml new file mode 100644 index 0000000..afc2f4e --- /dev/null +++ b/.idea/watcherTasks.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/database/database.go b/database/database.go new file mode 100644 index 0000000..11750a0 --- /dev/null +++ b/database/database.go @@ -0,0 +1,91 @@ +package database + +import ( + "context" + "encoding/json" + "github.com/redis/go-redis/v9" + "sync" + "time" +) + +var ( + once sync.Once + rdb *redis.Client +) + +func Connect() *redis.Client { + once.Do(func() { + var ctx = context.Background() + var err error + rdb = redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password set + DB: 0, // use default DB + }) + if err != nil { + panic(err) + } + + cmd := rdb.Ping(ctx) + + if cmd.Err() != nil { + panic(err) + } + }) + return rdb +} + +func Set[T any](key string, value T) error { + db := Connect() + serialized, err := json.Marshal(value) + if err != nil { + return err + } + result := db.Set(context.Background(), key, serialized, time.Duration(0)) + return result.Err() +} + +func HSet[T any](set string, key string, value T) error { + db := Connect() + serialized, err := json.Marshal(value) + if err != nil { + return err + } + result := db.HSet(context.Background(), set, key, serialized) + return result.Err() +} + +func Get[T any](key string) (*T, error) { + db := Connect() + val, err := db.Get(context.Background(), key).Result() + if err != nil { + return nil, err + } + result := new(T) + err = json.Unmarshal([]byte(val), result) + if err != nil { + return nil, err + } + return result, nil +} + +func HList[T any](key string) ([]*T, error) { + db := Connect() + val, err := db.HGetAll(context.Background(), key).Result() + if err != nil { + return nil, err + } + result := make([]*T, len(val)) + + count := 0 + for _, t := range val { + item := new(T) + err = json.Unmarshal([]byte(t), item) + if err != nil { + return nil, err + } + result[count] = item + count++ + } + return result, nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c715135 --- /dev/null +++ b/go.mod @@ -0,0 +1,28 @@ +module mhtml + +go 1.20 + +require ( + github.com/andybalholm/brotli v1.0.5 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/gofiber/fiber/v2 v2.47.0 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/klauspost/compress v1.16.3 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/mattn/go-sqlite3 v1.14.17 // indirect + github.com/philhofer/fwd v1.1.2 // indirect + github.com/redis/go-redis/v9 v9.0.5 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/samber/lo v1.38.1 // indirect + github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 // indirect + github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect + github.com/tinylib/msgp v1.1.8 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.47.0 // indirect + github.com/valyala/tcplisten v1.0.0 // indirect + golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect + golang.org/x/sys v0.9.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..ec143fa --- /dev/null +++ b/go.sum @@ -0,0 +1,103 @@ +github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= +github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao= +github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/gofiber/fiber/v2 v2.47.0 h1:EN5lHVCc+Pyqh5OEsk8fzRiifgwpbrP0rulQ4iNf3fs= +github.com/gofiber/fiber/v2 v2.47.0/go.mod h1:mbFMVN1lQuzziTkkakgtKKdjfsXSw9BKR5lmcNksUoU= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/klauspost/compress v1.16.3 h1:XuJt9zzcnaz6a16/OU53ZjWp/v7/42WcR5t2a0PcNQY= +github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= +github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= +github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= +github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= +github.com/redis/go-redis/v9 v9.0.5 h1:CuQcn5HIEeK7BgElubPP8CGtE0KakrnbBSTLjathl5o= +github.com/redis/go-redis/v9 v9.0.5/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= +github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= +github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 h1:rmMl4fXJhKMNWl+K+r/fq4FbbKI+Ia2m9hYBLm2h4G4= +github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94/go.mod h1:90zrgN3D/WJsDd1iXHT96alCoN2KJo6/4x1DZC3wZs8= +github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d/go.mod h1:Gy+0tqhJvgGlqnTF8CVGP0AaGRjwBtXs/a5PA0Y3+A4= +github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk= +github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g= +github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw= +github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= +github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.47.0 h1:y7moDoxYzMooFpT5aHgNgVOQDrS3qlkfiP9mDtGGK9c= +github.com/valyala/fasthttp v1.47.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA= +github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= +golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= +golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= +golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.4.0 h1:7mTAgkunk3fr4GAloyyCasadO6h9zSsQZbwvcaIciV4= +golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/h/render.go b/h/render.go new file mode 100644 index 0000000..26e2e3a --- /dev/null +++ b/h/render.go @@ -0,0 +1,89 @@ +package h + +import ( + "fmt" + "strings" +) + +const FlagSkip = "skip" + +type Builder struct { + builder *strings.Builder + root *Node +} + +func (page Builder) render() { + page.renderNode(page.root) +} + +func insertAttribute(node *Node, name string, value string) { + existing := node.attributes[name] + if existing != "" { + node.attributes[name] = existing + " " + value + } else { + node.attributes[name] = value + } +} + +func (page Builder) renderNode(node *Node) { + if node.tag != "" { + page.builder.WriteString(fmt.Sprintf("<%s", node.tag)) + index := 0 + + if node.attributes == nil { + node.attributes = map[string]string{} + } + + for _, child := range node.children { + if child.tag == "class" { + insertAttribute(node, "class", child.value) + child.tag = FlagSkip + } + + if child.tag == "attribute" { + for key, value := range child.attributes { + insertAttribute(node, key, value) + } + child.tag = FlagSkip + } + } + + for key, value := range node.attributes { + if index == 0 { + page.builder.WriteString(" ") + } + page.builder.WriteString(key) + page.builder.WriteString("=") + page.builder.WriteRune('"') + page.builder.WriteString(value) + page.builder.WriteRune('"') + if index < len(node.attributes) { + page.builder.WriteRune(' ') + } + index += 1 + } + page.builder.WriteString(">") + if node.text != "" { + page.builder.WriteString(node.text) + } + } + for _, child := range node.children { + if child.tag != FlagSkip { + page.renderNode(child) + } + } + if node.tag != "" { + page.builder.WriteString(fmt.Sprintf("", node.tag)) + } +} + +func Render(node *Node) string { + builder := strings.Builder{} + page := Builder{ + builder: &builder, + root: node, + } + page.render() + d := page.builder.String() + return d +} diff --git a/h/tag.go b/h/tag.go new file mode 100644 index 0000000..dd3a62f --- /dev/null +++ b/h/tag.go @@ -0,0 +1,204 @@ +package h + +type Node struct { + tag string + attributes map[string]string + children []*Node + text string + value string +} + +func Class(value string) *Node { + return &Node{ + tag: "class", + value: value, + } +} + +func Id(value string) *Node { + return Attribute("id", value) +} + +func Attribute(key string, value string) *Node { + return &Node{ + tag: "attribute", + attributes: map[string]string{ + key: value, + }, + } +} + +func Get(url string) *Node { + return Attribute("hx-get", url) +} + +func Post(url string) *Node { + return Attribute("hx-post", url) +} + +func Trigger(trigger string) *Node { + return Attribute("hx-trigger", trigger) +} + +func Target(target string) *Node { + return Attribute("hx-target", target) +} + +func Name(name string) *Node { + return Attribute("name", name) +} + +func Confirm(message string) *Node { + return Attribute("hx-confirm", message) +} + +func Href(path string) *Node { + return Attribute("href", path) +} + +func Type(name string) *Node { + return Attribute("type", name) +} + +func Placeholder(placeholder string) *Node { + return Attribute("placeholder", placeholder) +} + +func Swap(swap string) *Node { + return Attribute("hx-swap", swap) +} + +func Click(value string) *Node { + return Attribute("onclick", value) +} + +func Page(children ...*Node) *Node { + return &Node{ + tag: "html", + children: children, + } +} + +func Head(children ...*Node) *Node { + return &Node{ + tag: "head", + children: children, + } +} + +func Body(children ...*Node) *Node { + return &Node{ + tag: "body", + children: children, + } +} + +func Script(url string) *Node { + return &Node{ + tag: "script", + attributes: map[string]string{ + "src": url, + }, + children: make([]*Node, 0), + } +} + +func Div(children ...*Node) *Node { + return &Node{ + tag: "div", + children: children, + } +} + +func Input(inputType string, children ...*Node) *Node { + return &Node{ + tag: "input", + attributes: map[string]string{ + "type": inputType, + }, + children: children, + } +} + +func HStack(children ...*Node) *Node { + return &Node{ + tag: "div", + attributes: map[string]string{ + "class": "flex gap-2", + }, + children: children, + } +} + +func VStack(children ...*Node) *Node { + return &Node{ + tag: "div", + attributes: map[string]string{ + "class": "flex flex-col gap-2", + }, + children: children, + } +} + +func List[T any](items []T, mapper func(item T) *Node) *Node { + node := &Node{ + tag: "", + children: make([]*Node, len(items)), + } + for index, value := range items { + node.children[index] = mapper(value) + } + return node +} + +func Fragment(children ...*Node) *Node { + return &Node{ + tag: "", + children: children, + } +} + +func Button(children ...*Node) *Node { + return &Node{ + tag: "button", + children: children, + } +} + +func P(text string, children ...*Node) *Node { + return &Node{ + tag: "p", + children: children, + text: text, + } +} + +func A(text string, children ...*Node) *Node { + return &Node{ + tag: "a", + children: children, + text: text, + } +} + +func Empty() *Node { + return &Node{ + tag: "", + } +} + +func If(condition bool, node *Node) *Node { + if condition { + return node + } else { + return Empty() + } +} + +func IfElse(condition bool, node *Node, node2 *Node) *Node { + if condition { + return node + } else { + return node2 + } +} diff --git a/httpjson/http.go b/httpjson/http.go new file mode 100644 index 0000000..f45457b --- /dev/null +++ b/httpjson/http.go @@ -0,0 +1,56 @@ +package httpjson + +import ( + "encoding/json" + "io" + "net/http" + "sync" + "time" +) + +var client *http.Client +var once sync.Once + +func getClient() *http.Client { + once.Do(func() { + tr := &http.Transport{ + MaxIdleConns: 10, + IdleConnTimeout: 15 * time.Second, + ResponseHeaderTimeout: 15 * time.Second, + DisableKeepAlives: false, + } + httpClient := &http.Client{ + Transport: tr, + // do not follow redirects + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } + client = httpClient + }) + + return client +} + +func Get[T any](url string) (T, error) { + resp, err := getClient().Get(url) + if err != nil { + return *new(T), err + } + + defer func() { + io.Copy(io.Discard, resp.Body) + resp.Body.Close() + }() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return *new(T), err + } + d := new(T) + err = json.Unmarshal(body, &d) + if err != nil { + return *new(T), err + } + return *d, nil +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..24e48f8 --- /dev/null +++ b/index.html @@ -0,0 +1,24 @@ + + + + + + +
+
+

Log in + +

+

Sydne Anschutz

+

sanschutz0808@gmail.com

+ +
+

Maddy

+

jm@madev.me

+ +
+
+

Enter name

+
+ + \ No newline at end of file diff --git a/main.go b/main.go new file mode 100644 index 0000000..c87b2cc --- /dev/null +++ b/main.go @@ -0,0 +1,86 @@ +package main + +import ( + "github.com/gofiber/fiber/v2" + "mhtml/database" + "mhtml/h" + "mhtml/news" + "strconv" + "time" +) + +type User struct { + Name string + Email string +} + +var Version = time.Now().Nanosecond() + +func LiveReloadHandler(c *fiber.Ctx) error { + v := strconv.FormatInt(int64(Version), 10) + current := c.Cookies("version", v) + + if current != v { + c.Set("HX-Refresh", "true") + } + + c.Cookie(&fiber.Cookie{ + Name: "version", + Value: v, + }) + return c.SendString("") +} + +func Page(children ...*h.Node) *h.Node { + return h.Page( + h.Head( + h.Script("https://cdn.tailwindcss.com"), + h.Script("https://unpkg.com/htmx.org@1.9.2"), + ), + h.Body( + h.VStack( + h.Class("flex flex-col gap-2 bg-gray-100 h-full w-full items-center p-12"), + h.Fragment(children...), + ), + ), + ) +} + +func IndexPage(c *fiber.Ctx) error { + page := Page( + h.Div( + h.P("Hacker News - Top Stories"), + ), + news.StoryList(), + ) + return HtmlView(c, page) +} + +func HtmlView(c *fiber.Ctx, child *h.Node) error { + c.Set(fiber.HeaderContentType, fiber.MIMETextHTML) + return c.SendString( + h.Render( + child, + ), + ) +} + +func main() { + + database.HSet("users", "sydne", User{ + Name: "Sydne Anschutz", + Email: "sanschutz0808@gmail.com", + }) + + app := fiber.New() + + app.Get("/", IndexPage) + app.Get("/news/:id", func(c *fiber.Ctx) error { + return HtmlView(c, Page( + news.StoryFull(c.Params("id")), + )) + }) + app.Get("/livereload", LiveReloadHandler) + + app.Listen(":3000") +} diff --git a/news/posts.go b/news/posts.go new file mode 100644 index 0000000..f44b757 --- /dev/null +++ b/news/posts.go @@ -0,0 +1,58 @@ +package news + +import ( + "fmt" + "mhtml/httpjson" + "sync" +) + +type Post struct { + By string `json:"by"` + Descendants int `json:"descendants"` + Id int `json:"id"` + Kids []int `json:"kids"` + Score int `json:"score"` + Time int `json:"time"` + Title string `json:"title"` + Type string `json:"type"` + Url string `json:"url"` +} + +func List() ([]Post, error) { + responseIds, err := httpjson.Get[[]int64]("https://hacker-news.firebaseio.com/v0/topstories.json") + responseIds = responseIds[0:50] + if err != nil { + return []Post{}, err + } + + var wg sync.WaitGroup + posts := make([]Post, len(responseIds)) + + for index, id := range responseIds { + wg.Add(1) + id := id + index := index + go func() { + defer wg.Done() + url := fmt.Sprintf("https://hacker-news.firebaseio.com/v0/item/%d.json", id) + post, err := httpjson.Get[Post](url) + if err != nil { + println(err.Error()) + } + posts[index] = post + }() + } + + wg.Wait() + + return posts, nil +} + +func Get(id string) (Post, error) { + url := fmt.Sprintf("https://hacker-news.firebaseio.com/v0/item/%s.json", id) + post, err := httpjson.Get[Post](url) + if err != nil { + return Post{}, err + } + return post, nil +} diff --git a/news/views.go b/news/views.go new file mode 100644 index 0000000..46a56d1 --- /dev/null +++ b/news/views.go @@ -0,0 +1,40 @@ +package news + +import ( + "fmt" + "mhtml/h" +) + +func StoryList() *h.Node { + posts, err := List() + + if err != nil { + return h.P(err.Error()) + } + + if len(posts) == 0 { + return h.P("No results found") + } + + return h.Fragment( + h.VStack(h.List(posts, func(item Post) *h.Node { + return StoryCard(item) + })), + ) +} + +func StoryCard(post Post) *h.Node { + url := fmt.Sprintf("/news/%d", post.Id) + return h.VStack( + h.Class("items-center bg-red-200 p-4 rounded"), + h.A(post.Title, h.Href(url)), + ) +} + +func StoryFull(id string) *h.Node { + post, err := Get(id) + if err != nil { + return h.P(err.Error()) + } + return StoryCard(post) +}