diff --git a/.gitignore b/.gitignore index 806bf06..629516a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .pm *.db *_templ.go +*.ext \ No newline at end of file diff --git a/Makefile b/Makefile index d9ada4b..31a1718 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ tidy: clean: go clean - + cli: go run ./cmd/cli @@ -14,4 +14,4 @@ web: go run ./cmd/web dev: - templ generate -watch -cmd "go run ./cmd/web" \ No newline at end of file + go tool templ generate -watch -cmd "go run ./cmd/web" \ No newline at end of file diff --git a/cmd/web/main.go b/cmd/web/main.go index 771da85..eea75e1 100644 --- a/cmd/web/main.go +++ b/cmd/web/main.go @@ -3,13 +3,16 @@ package main import ( "beadstest/cmd/web/routes" "beadstest/cmd/web/server" + "beadstest/internal/models" "beadstest/internal/service" "beadstest/internal/storage" "context" "embed" "fmt" "os" + "time" + "github.com/google/uuid" "github.com/steveyegge/beads" ) @@ -27,15 +30,19 @@ func main() { handleError(err, "Error initializing Beads service") defer beadSvc.Close() - statStore := storage.NewStatisticsStorage("./stats.json") - statSvc := service.NewStatisticsService(statStore) + statStore := storage.NewJsonStorage("./.pm/stats.json", &models.Statistics{ + ID: uuid.New(), + StartTime: time.Now(), + }) - _ = statSvc // To avoid unused variable error; remove if statSvc is used + statSvc, err := service.NewStatisticsService(statStore) + handleError(err, "Error initializing Statistics service") server := server.NewServer(server.Server{ - Port: 8080, - Assets: assets, - Service: beadSvc, + Port: 8080, + Assets: assets, + Service: beadSvc, + StatisticsService: statSvc, Routes: []server.Route{ {Pattern: "/", Component: routes.Index()}, }, diff --git a/cmd/web/server/server.go b/cmd/web/server/server.go index 6717d46..5a79d60 100644 --- a/cmd/web/server/server.go +++ b/cmd/web/server/server.go @@ -1,6 +1,7 @@ package server import ( + "beadstest/internal/service" "embed" "fmt" "net/http" @@ -10,10 +11,11 @@ import ( ) type Server struct { - Port int - Assets embed.FS - Routes []Route - Service beads.Storage + Port int + Assets embed.FS + Routes []Route + Service beads.Storage + StatisticsService *service.StatisticsService } // NewServer creates and configures a new HTTP server instance. diff --git a/go.mod b/go.mod index 66dc209..c5acea7 100644 --- a/go.mod +++ b/go.mod @@ -12,9 +12,17 @@ require ( ) require ( + github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect + github.com/andybalholm/brotli v1.1.0 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cli/browser v1.3.0 // indirect + github.com/fatih/color v1.16.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/natefinch/atomic v1.0.1 // indirect github.com/ncruces/go-sqlite3 v0.30.4 // indirect github.com/ncruces/julianday v1.0.0 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect @@ -27,7 +35,13 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect github.com/tetratelabs/wazero v1.11.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/mod v0.32.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.32.0 // indirect + golang.org/x/tools v0.40.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) + +tool github.com/a-h/templ/cmd/templ diff --git a/go.sum b/go.sum index 903a2f7..1326dde 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,21 @@ github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= +github.com/a-h/parse v0.0.0-20250122154542-74294addb73e h1:HjVbSQHy+dnlS6C3XajZ69NYAb5jbGNfHanvm1+iYlo= +github.com/a-h/parse v0.0.0-20250122154542-74294addb73e/go.mod h1:3mnrkvGpurZ4ZrTDbYU84xhwXW2TjTKShSwjRi2ihfQ= github.com/a-h/templ v0.3.977 h1:kiKAPXTZE2Iaf8JbtM21r54A8bCNsncrfnokZZSrSDg= github.com/a-h/templ v0.3.977/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo= +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo= +github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= @@ -22,6 +32,13 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= +github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= github.com/ncruces/go-sqlite3 v0.30.4 h1:j9hEoOL7f9ZoXl8uqXVniaq1VNwlWAXihZbTvhqPPjA= github.com/ncruces/go-sqlite3 v0.30.4/go.mod h1:7WR20VSC5IZusKhUdiR9y1NsUqnZgqIYCmKKoMEYg68= github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M= @@ -62,10 +79,20 @@ github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbw github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/models/statistics.go b/internal/models/statistics.go index 614b6ed..c9e127a 100644 --- a/internal/models/statistics.go +++ b/internal/models/statistics.go @@ -21,4 +21,9 @@ type Statistics struct { InterfaceType InterfaceType `json:"interface_type"` TasksCompleted int `json:"tasks_completed"` + ButtonClicks ButtonClicks `json:"button_clicks"` +} + +type ButtonClicks struct { + Clicks int `json:"clicks"` } diff --git a/internal/service/statistics.go b/internal/service/statistics.go index f53a9f6..1ac87be 100644 --- a/internal/service/statistics.go +++ b/internal/service/statistics.go @@ -1,13 +1,27 @@ package service -import "beadstest/internal/storage" +import ( + "beadstest/internal/models" + "beadstest/internal/storage" + "errors" +) type StatisticsService struct { - storage *storage.StatisticsStorage + storage *storage.Storage[models.Statistics] } -func NewStatisticsService(storage *storage.StatisticsStorage) *StatisticsService { +func NewStatisticsService(storage *storage.Storage[models.Statistics]) (*StatisticsService, error) { + if err := storage.Init(); err != nil { + return nil, err + } return &StatisticsService{ storage: storage, - } + }, nil +} + +func (s *StatisticsService) GetStatistics() (models.Statistics, error) { + if s.storage.Data == nil { + return models.Statistics{}, errors.New("statistics data not initialized") + } + return *s.storage.Data, nil } diff --git a/internal/storage/storage.go b/internal/storage/storage.go index 99dc195..1099d7d 100644 --- a/internal/storage/storage.go +++ b/internal/storage/storage.go @@ -1,17 +1,54 @@ package storage -type StatisticsStorage struct { - Path string +import ( + "encoding/json" + "os" + "sync" +) + +type Storage[T any] struct { + path string + mu sync.RWMutex + Data *T } -func NewStatisticsStorage(path string) *StatisticsStorage { - return &StatisticsStorage{Path: path} +func NewJsonStorage[T any](path string, defaultData *T) *Storage[T] { + return &Storage[T]{ + path: path, + Data: defaultData, + } } -func (s *StatisticsStorage) Save(stats any) error { - return nil +func (s *Storage[T]) Save() error { + s.mu.Lock() + defer s.mu.Unlock() + + data, err := json.MarshalIndent(s.Data, "", " ") + if err != nil { + return err + } + + return os.WriteFile(s.path, data, 0644) } -func (s *StatisticsStorage) Load() (any, error) { - return nil, nil +func (s *Storage[T]) Load() error { + s.mu.Lock() + defer s.mu.Unlock() + + bytes, err := os.ReadFile(s.path) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + + return json.Unmarshal(bytes, s.Data) } + +func (s *Storage[T]) Init() error { + if _, err := os.Stat(s.path); os.IsNotExist(err) { + return s.Save() + } + return s.Load() +} \ No newline at end of file diff --git a/main.go b/main.go new file mode 100644 index 0000000..5bbfbad --- /dev/null +++ b/main.go @@ -0,0 +1,23 @@ +package main + +import ( + "beadstest/internal/models" + "beadstest/internal/storage" + "fmt" + "os" + "time" + + "github.com/google/uuid" +) + +func main() { + statStore := storage.NewJsonStorage("./.pm/test.json", &models.Statistics{ + ID: uuid.New(), + StartTime: time.Now(), + }) + + if err := statStore.Init(); err != nil { + fmt.Println("Error:", err) + os.Exit(1) + } +}