Initial commit
This commit is contained in:
commit
2514708522
19 changed files with 2332 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# Project exclude paths
|
||||
/tmp/
|
||||
5
.idea/.gitignore
vendored
Normal file
5
.idea/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
18
.idea/dataSources.local.xml
Normal file
18
.idea/dataSources.local.xml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="dataSourceStorageLocal" created-in="GO-233.13135.104">
|
||||
<data-source name="site.db" uuid="6b63d5bd-e451-4904-b659-21db5c54c16d">
|
||||
<database-info product="SQLite" version="3.40.1" jdbc-version="4.2" driver-name="SQLite JDBC" driver-version="3.40.1.0" dbms="SQLITE" exact-version="3.40.1" exact-driver-version="3.40">
|
||||
<identifier-quote-string>"</identifier-quote-string>
|
||||
</database-info>
|
||||
<case-sensitivity plain-identifiers="mixed" quoted-identifiers="mixed" />
|
||||
<secret-storage>master_key</secret-storage>
|
||||
<auth-provider>no-auth</auth-provider>
|
||||
<schema-mapping>
|
||||
<introspection-scope>
|
||||
<node kind="schema" qname="@" />
|
||||
</introspection-scope>
|
||||
</schema-mapping>
|
||||
</data-source>
|
||||
</component>
|
||||
</project>
|
||||
12
.idea/dataSources.xml
Normal file
12
.idea/dataSources.xml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||
<data-source source="LOCAL" name="site.db" uuid="6b63d5bd-e451-4904-b659-21db5c54c16d">
|
||||
<driver-ref>sqlite.xerial</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
||||
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/site.db</jdbc-url>
|
||||
<working-dir>$ProjectFileDir$</working-dir>
|
||||
</data-source>
|
||||
</component>
|
||||
</project>
|
||||
1475
.idea/dataSources/6b63d5bd-e451-4904-b659-21db5c54c16d.xml
Normal file
1475
.idea/dataSources/6b63d5bd-e451-4904-b659-21db5c54c16d.xml
Normal file
File diff suppressed because it is too large
Load diff
16
.idea/inspectionProfiles/Project_Default.xml
Normal file
16
.idea/inspectionProfiles/Project_Default.xml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="HtmlUnknownAttribute" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="myValues">
|
||||
<value>
|
||||
<list size="1">
|
||||
<item index="0" class="java.lang.String" itemvalue="x:onclick" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
<option name="myCustomValuesEnabled" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="XmlUnboundNsPrefix" enabled="true" level="INFORMATION" enabled_by_default="true" />
|
||||
</profile>
|
||||
</component>
|
||||
13
.idea/mhtml.iml
Normal file
13
.idea/mhtml.iml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="Go" enabled="true" />
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
8
.idea/modules.xml
Normal file
8
.idea/modules.xml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/mhtml.iml" filepath="$PROJECT_DIR$/.idea/mhtml.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
4
.idea/watcherTasks.xml
Normal file
4
.idea/watcherTasks.xml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectTasksOptions" suppressed-tasks="Pug/Jade" />
|
||||
</project>
|
||||
91
database/database.go
Normal file
91
database/database.go
Normal file
|
|
@ -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
|
||||
}
|
||||
28
go.mod
Normal file
28
go.mod
Normal file
|
|
@ -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
|
||||
)
|
||||
103
go.sum
Normal file
103
go.sum
Normal file
|
|
@ -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=
|
||||
89
h/render.go
Normal file
89
h/render.go
Normal file
|
|
@ -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("</%s>", node.tag))
|
||||
}
|
||||
}
|
||||
|
||||
func Render(node *Node) string {
|
||||
builder := strings.Builder{}
|
||||
page := Builder{
|
||||
builder: &builder,
|
||||
root: node,
|
||||
}
|
||||
page.render()
|
||||
d := page.builder.String()
|
||||
return d
|
||||
}
|
||||
204
h/tag.go
Normal file
204
h/tag.go
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
56
httpjson/http.go
Normal file
56
httpjson/http.go
Normal file
|
|
@ -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
|
||||
}
|
||||
24
index.html
Normal file
24
index.html
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<html>
|
||||
<head>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/htmx.org@1.9.2"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="flex flex-col gap-2 flex flex-col gap-2 bg-gray-100 h-full w-full items-center p-12">
|
||||
<div>
|
||||
<p>Log in
|
||||
<button></button>
|
||||
</p>
|
||||
<div class="flex flex-col gap-2 items-center"><p>Sydne Anschutz</p>
|
||||
<p>sanschutz0808@gmail.com</p>
|
||||
<button class="bg-red-200 rounded p-2" onclick="alert(1)"><p>Delete</p></button>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 items-center"><p>Maddy</p>
|
||||
<p>jm@madev.me</p>
|
||||
<button class="bg-red-200 rounded p-2" onclick="alert(1)"><p>Delete</p></button>
|
||||
</div>
|
||||
</div>
|
||||
<div><p>Enter name</p><input type="text" class="rounded p-4 border-gray-100"></input></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
86
main.go
Normal file
86
main.go
Normal file
|
|
@ -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")
|
||||
}
|
||||
58
news/posts.go
Normal file
58
news/posts.go
Normal file
|
|
@ -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
|
||||
}
|
||||
40
news/views.go
Normal file
40
news/views.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
Loading…
Reference in a new issue