first commit
This commit is contained in:
+99
@@ -0,0 +1,99 @@
|
|||||||
|
name: lektura-red
|
||||||
|
|
||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: backend-build
|
||||||
|
image: golang:1.15
|
||||||
|
depends_on: []
|
||||||
|
commands:
|
||||||
|
- go mod download
|
||||||
|
- CGO_ENABLED=0 go build -ldflags "-w -s" -o build/api/handler cmd/stufflog2-lambda/main.go
|
||||||
|
|
||||||
|
- name: backend-test
|
||||||
|
image: golang:1.15
|
||||||
|
depends_on: []
|
||||||
|
commands:
|
||||||
|
- go test -v ./...
|
||||||
|
|
||||||
|
- name: backend-migrate
|
||||||
|
image: golang:1.15
|
||||||
|
depends_on: []
|
||||||
|
environment:
|
||||||
|
DB_CONNECT:
|
||||||
|
from:secret: db_connect
|
||||||
|
commands:
|
||||||
|
- go get -u github.com/pressly/goose/...
|
||||||
|
- cd migrations/postgres
|
||||||
|
- goose postgres "$DB_CONNECT" up
|
||||||
|
|
||||||
|
- name: backend-deploy
|
||||||
|
image: node:14.14.0
|
||||||
|
depends_on:
|
||||||
|
- backend-build
|
||||||
|
- backend-test
|
||||||
|
- backend-migrate
|
||||||
|
environment:
|
||||||
|
AWS_ACCESS_KEY_ID:
|
||||||
|
from_secret: aws_access_key_id
|
||||||
|
AWS_SECRET_ACCESS_KEY:
|
||||||
|
from_secret: aws_secret_access_key
|
||||||
|
AWS_DEFAULT_REGION:
|
||||||
|
from_secret: aws_region
|
||||||
|
AMI_ROLE:
|
||||||
|
from_secret: ami_role
|
||||||
|
S3_WEBUI_BUCKET:
|
||||||
|
from_secret: s3_webui_bucket
|
||||||
|
DOMAIN_NAME:
|
||||||
|
from_secret: domain_name
|
||||||
|
CERTIFICATE_NAME:
|
||||||
|
from_secret: certificate_name
|
||||||
|
CERTIFICATE_ARN:
|
||||||
|
from_secret: certificate_arn
|
||||||
|
HOSTED_ZONE_ID:
|
||||||
|
from_secret: hosted_zone_id
|
||||||
|
DB_DRIVER:
|
||||||
|
from_secret: db_driver
|
||||||
|
DB_CONNECT:
|
||||||
|
from:secret: db_connect
|
||||||
|
commands:
|
||||||
|
- apt-get update > /dev/null 2>&1
|
||||||
|
- apt-get -y install awscli zip > /dev/null 2>&1
|
||||||
|
- npm install -g serverless > /dev/null 2>&1
|
||||||
|
- npm install -g serverless-domain-manager > /dev/null 2>&1
|
||||||
|
- npm install -g serverless-apigateway-service-proxy > /dev/null 2>&1
|
||||||
|
- serverless deploy
|
||||||
|
|
||||||
|
- name: frontend-build
|
||||||
|
image: node:14.14.0
|
||||||
|
depends_on: []
|
||||||
|
environment:
|
||||||
|
AWS_AMPLIFY_REGION:
|
||||||
|
from_secret: aws_region
|
||||||
|
AWS_AMPLIFY_USER_POOL_ID:
|
||||||
|
from_secret: aws_amplify_user_pool_id
|
||||||
|
AWS_AMPLIFY_USER_POOL_WEB_CLIENT_ID:
|
||||||
|
from_secret: aws_amplify_user_pool_web_client_id
|
||||||
|
|
||||||
|
commands:
|
||||||
|
- cd svelte-ui
|
||||||
|
- npm install
|
||||||
|
- npm run build
|
||||||
|
|
||||||
|
- name: frontend-deploy
|
||||||
|
image: amazon/aws-cli:latest
|
||||||
|
depends_on:
|
||||||
|
- frontend-build
|
||||||
|
environment:
|
||||||
|
AWS_ACCESS_KEY_ID:
|
||||||
|
from_secret: aws_access_key_id
|
||||||
|
AWS_SECRET_ACCESS_KEY:
|
||||||
|
from_secret: aws_secret_access_key
|
||||||
|
AWS_DEFAULT_REGION:
|
||||||
|
from_secret: aws_region
|
||||||
|
S3_WEBUI_BUCKET:
|
||||||
|
from_secret: s3_webui_bucket
|
||||||
|
commands:
|
||||||
|
- cd webui/build
|
||||||
|
- aws s3 sync . s3://$S3_WEBUI_BUCKET
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
/stufflog2-*
|
||||||
|
/.idea
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gissleh/stufflog/internal/slerrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func handler(key string, callback func(c *gin.Context) (interface{}, error)) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
res, err := callback(c)
|
||||||
|
if err != nil {
|
||||||
|
slerrors.Respond(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resJson := make(map[string]interface{}, 1)
|
||||||
|
resJson[key] = res
|
||||||
|
|
||||||
|
c.JSON(200, resJson)
|
||||||
|
}
|
||||||
|
}
|
||||||
+141
@@ -0,0 +1,141 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gissleh/stufflog/database"
|
||||||
|
"github.com/gissleh/stufflog/internal/auth"
|
||||||
|
"github.com/gissleh/stufflog/internal/generate"
|
||||||
|
"github.com/gissleh/stufflog/internal/slerrors"
|
||||||
|
"github.com/gissleh/stufflog/models"
|
||||||
|
"github.com/gissleh/stufflog/services"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Goal(g *gin.RouterGroup, db database.Database) {
|
||||||
|
l := services.Loader{DB: db}
|
||||||
|
|
||||||
|
g.GET("/", handler("goals", func(c *gin.Context) (interface{}, error) {
|
||||||
|
filter := models.GoalFilter{}
|
||||||
|
if value := c.Query("minTime"); value != "" {
|
||||||
|
minTime, err := time.Parse(time.RFC3339Nano, value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, slerrors.BadRequest("Invalid minTime")
|
||||||
|
}
|
||||||
|
minTime = minTime.UTC()
|
||||||
|
|
||||||
|
filter.MinTime = &minTime
|
||||||
|
} else {
|
||||||
|
lastMonth := time.Now().Add(-30 * 24 * time.Hour).UTC()
|
||||||
|
filter.MinTime = &lastMonth
|
||||||
|
}
|
||||||
|
if value := c.Query("maxTime"); value != "" {
|
||||||
|
maxTime, err := time.Parse(time.RFC3339Nano, value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, slerrors.BadRequest("Invalid maxTime")
|
||||||
|
}
|
||||||
|
maxTime = maxTime.UTC()
|
||||||
|
|
||||||
|
filter.MaxTime = &maxTime
|
||||||
|
}
|
||||||
|
if value := c.Query("includesTime"); value != "" {
|
||||||
|
includesTime, err := time.Parse(time.RFC3339Nano, value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, slerrors.BadRequest("Invalid includesTime")
|
||||||
|
}
|
||||||
|
includesTime = includesTime.UTC()
|
||||||
|
|
||||||
|
filter.IncludesTime = &includesTime
|
||||||
|
}
|
||||||
|
|
||||||
|
return l.ListGoals(c.Request.Context(), filter)
|
||||||
|
}))
|
||||||
|
|
||||||
|
g.GET("/:id", handler("goal", func(c *gin.Context) (interface{}, error) {
|
||||||
|
return l.FindGoal(c.Request.Context(), c.Param("id"))
|
||||||
|
}))
|
||||||
|
|
||||||
|
g.POST("/", handler("goal", func(c *gin.Context) (interface{}, error) {
|
||||||
|
goal := models.Goal{}
|
||||||
|
err := c.BindJSON(&goal)
|
||||||
|
if err != nil {
|
||||||
|
return nil, slerrors.BadRequest("Invalid JSON")
|
||||||
|
}
|
||||||
|
goal.ID = generate.GoalID()
|
||||||
|
goal.UserID = auth.UserID(c)
|
||||||
|
|
||||||
|
if goal.Amount <= 0 {
|
||||||
|
return nil, slerrors.BadRequest("Goal amount must be more than 0.")
|
||||||
|
}
|
||||||
|
if goal.StartTime.IsZero() {
|
||||||
|
return nil, slerrors.BadRequest("Start time must be set.")
|
||||||
|
}
|
||||||
|
if goal.EndTime.IsZero() {
|
||||||
|
return nil, slerrors.BadRequest("End time must be set.")
|
||||||
|
}
|
||||||
|
if !goal.EndTime.After(goal.StartTime) {
|
||||||
|
return nil, slerrors.BadRequest("Start time must be before end time.")
|
||||||
|
}
|
||||||
|
|
||||||
|
goal.StartTime = goal.StartTime.UTC()
|
||||||
|
goal.EndTime = goal.EndTime.UTC()
|
||||||
|
|
||||||
|
group, err := db.Groups().Find(c.Request.Context(), goal.GroupID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
goal.GroupID = group.ID
|
||||||
|
|
||||||
|
err = db.Goals().Insert(c.Request.Context(), goal)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return l.FindGoal(c.Request.Context(), goal.ID)
|
||||||
|
}))
|
||||||
|
|
||||||
|
g.PUT("/:id", handler("goal", func(c *gin.Context) (interface{}, error) {
|
||||||
|
update := models.GoalUpdate{}
|
||||||
|
err := c.BindJSON(&update)
|
||||||
|
if err != nil {
|
||||||
|
return nil, slerrors.BadRequest("Invalid JSON")
|
||||||
|
}
|
||||||
|
|
||||||
|
goal, err := l.FindGoal(c.Request.Context(), c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
goal.StartTime = goal.StartTime.UTC()
|
||||||
|
goal.EndTime = goal.EndTime.UTC()
|
||||||
|
|
||||||
|
goal.Update(update)
|
||||||
|
|
||||||
|
if goal.Amount <= 0 {
|
||||||
|
return nil, slerrors.BadRequest("Goal amount must be more than 0.")
|
||||||
|
}
|
||||||
|
if !goal.EndTime.After(goal.StartTime) {
|
||||||
|
return nil, slerrors.BadRequest("Start time must be before end time.")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Goals().Update(c.Request.Context(), goal.Goal)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return goal, nil
|
||||||
|
}))
|
||||||
|
|
||||||
|
g.DELETE("/:id", handler("goal", func(c *gin.Context) (interface{}, error) {
|
||||||
|
goal, err := l.FindGoal(c.Request.Context(), c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Goals().Delete(c.Request.Context(), goal.Goal)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return goal, nil
|
||||||
|
}))
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gissleh/stufflog/database"
|
||||||
|
"github.com/gissleh/stufflog/internal/auth"
|
||||||
|
"github.com/gissleh/stufflog/internal/generate"
|
||||||
|
"github.com/gissleh/stufflog/internal/slerrors"
|
||||||
|
"github.com/gissleh/stufflog/models"
|
||||||
|
"github.com/gissleh/stufflog/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Group(g *gin.RouterGroup, db database.Database) {
|
||||||
|
l := services.Loader{DB: db}
|
||||||
|
|
||||||
|
g.GET("/", handler("groups", func(c *gin.Context) (interface{}, error) {
|
||||||
|
return l.ListGroups(c, models.GroupFilter{})
|
||||||
|
}))
|
||||||
|
|
||||||
|
g.GET("/:id", handler("group", func(c *gin.Context) (interface{}, error) {
|
||||||
|
return l.FindGroup(c, c.Param("id"))
|
||||||
|
}))
|
||||||
|
|
||||||
|
g.POST("/", handler("group", func(c *gin.Context) (interface{}, error) {
|
||||||
|
group := models.Group{}
|
||||||
|
err := c.BindJSON(&group)
|
||||||
|
if err != nil {
|
||||||
|
return nil, slerrors.BadRequest("Invalid JSON")
|
||||||
|
}
|
||||||
|
group.ID = generate.GroupID()
|
||||||
|
group.UserID = auth.UserID(c)
|
||||||
|
|
||||||
|
err = db.Groups().Insert(c.Request.Context(), group)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &models.GroupResult{
|
||||||
|
Group: group,
|
||||||
|
Items: []*models.Item{},
|
||||||
|
}, nil
|
||||||
|
}))
|
||||||
|
|
||||||
|
g.PUT("/:id", handler("group", func(c *gin.Context) (interface{}, error) {
|
||||||
|
update := models.GroupUpdate{}
|
||||||
|
err := c.BindJSON(&update)
|
||||||
|
if err != nil {
|
||||||
|
return nil, slerrors.BadRequest("Invalid JSON")
|
||||||
|
}
|
||||||
|
|
||||||
|
group, err := db.Groups().Find(c.Request.Context(), c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if auth.UserID(c) != group.UserID {
|
||||||
|
return nil, slerrors.NotFound("Item")
|
||||||
|
}
|
||||||
|
|
||||||
|
group.Update(update)
|
||||||
|
err = db.Groups().Update(c.Request.Context(), *group)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return l.FindGroup(c, group.ID)
|
||||||
|
}))
|
||||||
|
|
||||||
|
g.DELETE("/:id", handler("group", func(c *gin.Context) (interface{}, error) {
|
||||||
|
group, err := l.FindGroup(c, c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(group.Items) > 0 {
|
||||||
|
return nil, slerrors.Forbidden("cannot delete non-empty group.")
|
||||||
|
}
|
||||||
|
goals, err := db.Goals().List(c.Request.Context(), models.GoalFilter{GroupIDs: []string{group.ID}})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(goals) > 0 {
|
||||||
|
return nil, slerrors.Forbidden("cannot delete group with goals.")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Groups().Delete(c.Request.Context(), group.Group)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return group, nil
|
||||||
|
}))
|
||||||
|
}
|
||||||
+98
@@ -0,0 +1,98 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gissleh/stufflog/database"
|
||||||
|
"github.com/gissleh/stufflog/internal/auth"
|
||||||
|
"github.com/gissleh/stufflog/internal/generate"
|
||||||
|
"github.com/gissleh/stufflog/internal/slerrors"
|
||||||
|
"github.com/gissleh/stufflog/models"
|
||||||
|
"github.com/gissleh/stufflog/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Item(g *gin.RouterGroup, db database.Database) {
|
||||||
|
l := services.Loader{DB: db}
|
||||||
|
|
||||||
|
g.GET("/", handler("items", func(c *gin.Context) (interface{}, error) {
|
||||||
|
return l.ListItems(c, models.ItemFilter{})
|
||||||
|
}))
|
||||||
|
|
||||||
|
g.GET("/:id", handler("item", func(c *gin.Context) (interface{}, error) {
|
||||||
|
return l.FindItem(c, c.Param("id"))
|
||||||
|
}))
|
||||||
|
|
||||||
|
g.POST("/", handler("item", func(c *gin.Context) (interface{}, error) {
|
||||||
|
item := models.Item{}
|
||||||
|
err := c.BindJSON(&item)
|
||||||
|
if err != nil {
|
||||||
|
return nil, slerrors.BadRequest("Invalid JSON")
|
||||||
|
}
|
||||||
|
|
||||||
|
group, err := db.Groups().Find(c.Request.Context(), item.GroupID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if auth.UserID(c) != group.UserID {
|
||||||
|
return nil, slerrors.NotFound("Group")
|
||||||
|
}
|
||||||
|
|
||||||
|
item.ID = generate.ItemID()
|
||||||
|
item.UserID = auth.UserID(c)
|
||||||
|
item.GroupID = group.ID
|
||||||
|
|
||||||
|
err = db.Items().Insert(c.Request.Context(), item)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &models.ItemResult{
|
||||||
|
Item: item,
|
||||||
|
Group: group,
|
||||||
|
}, nil
|
||||||
|
}))
|
||||||
|
|
||||||
|
g.PUT("/:id", handler("item", func(c *gin.Context) (interface{}, error) {
|
||||||
|
update := models.ItemUpdate{}
|
||||||
|
err := c.BindJSON(&update)
|
||||||
|
if err != nil {
|
||||||
|
return nil, slerrors.BadRequest("Invalid JSON")
|
||||||
|
}
|
||||||
|
|
||||||
|
item, err := l.FindItem(c.Request.Context(), c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
item.Update(update)
|
||||||
|
err = db.Items().Update(c.Request.Context(), item.Item)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return item, nil
|
||||||
|
}))
|
||||||
|
|
||||||
|
g.DELETE("/:id", handler("item", func(c *gin.Context) (interface{}, error) {
|
||||||
|
item, err := l.FindItem(c.Request.Context(), c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks, err := db.Tasks().List(c.Request.Context(), models.TaskFilter{
|
||||||
|
UserID: auth.UserID(c),
|
||||||
|
ItemIDs: []string{item.ID},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(tasks) > 0 {
|
||||||
|
return nil, slerrors.Forbidden("cannot delete item referenced in tasks.")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Items().Delete(c.Request.Context(), item.Item)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return item, nil
|
||||||
|
}))
|
||||||
|
}
|
||||||
+115
@@ -0,0 +1,115 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gissleh/stufflog/database"
|
||||||
|
"github.com/gissleh/stufflog/internal/auth"
|
||||||
|
"github.com/gissleh/stufflog/internal/generate"
|
||||||
|
"github.com/gissleh/stufflog/internal/slerrors"
|
||||||
|
"github.com/gissleh/stufflog/models"
|
||||||
|
"github.com/gissleh/stufflog/services"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Log(g *gin.RouterGroup, db database.Database) {
|
||||||
|
l := services.Loader{DB: db}
|
||||||
|
|
||||||
|
g.GET("/", handler("logs", func(c *gin.Context) (interface{}, error) {
|
||||||
|
filter := models.LogFilter{}
|
||||||
|
if value := c.Query("minTime"); value != "" {
|
||||||
|
minTime, err := time.Parse(time.RFC3339Nano, value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, slerrors.BadRequest("Invalid minTime")
|
||||||
|
}
|
||||||
|
minTime = minTime.UTC()
|
||||||
|
|
||||||
|
filter.MinTime = &minTime
|
||||||
|
} else {
|
||||||
|
lastMonth := time.Now().Add(-30 * 24 * time.Hour).UTC()
|
||||||
|
filter.MinTime = &lastMonth
|
||||||
|
}
|
||||||
|
if value := c.Query("maxTime"); value != "" {
|
||||||
|
maxTime, err := time.Parse(time.RFC3339Nano, value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, slerrors.BadRequest("Invalid maxTime")
|
||||||
|
}
|
||||||
|
maxTime = maxTime.UTC()
|
||||||
|
|
||||||
|
filter.MaxTime = &maxTime
|
||||||
|
}
|
||||||
|
|
||||||
|
return l.ListLogs(c, filter)
|
||||||
|
}))
|
||||||
|
|
||||||
|
g.GET("/:id", handler("log", func(c *gin.Context) (interface{}, error) {
|
||||||
|
return l.FindLog(c, c.Param("id"))
|
||||||
|
}))
|
||||||
|
|
||||||
|
g.POST("/", handler("log", func(c *gin.Context) (interface{}, error) {
|
||||||
|
log := models.Log{}
|
||||||
|
err := c.BindJSON(&log)
|
||||||
|
if err != nil {
|
||||||
|
return nil, slerrors.BadRequest("Invalid JSON")
|
||||||
|
}
|
||||||
|
|
||||||
|
task, err := l.FindTask(c.Request.Context(), log.TaskID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.ID = generate.LogID()
|
||||||
|
log.UserID = auth.UserID(c)
|
||||||
|
log.TaskID = task.ID
|
||||||
|
log.ItemID = task.ItemID
|
||||||
|
if log.LoggedTime.IsZero() {
|
||||||
|
log.LoggedTime = time.Now().UTC()
|
||||||
|
} else {
|
||||||
|
log.LoggedTime = log.LoggedTime.UTC()
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Logs().Insert(c.Request.Context(), log)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &models.LogResult{
|
||||||
|
Log: log,
|
||||||
|
Task: &task.Task,
|
||||||
|
}, nil
|
||||||
|
}))
|
||||||
|
|
||||||
|
g.PUT("/:id", handler("log", func(c *gin.Context) (interface{}, error) {
|
||||||
|
update := models.LogUpdate{}
|
||||||
|
err := c.BindJSON(&update)
|
||||||
|
if err != nil {
|
||||||
|
return nil, slerrors.BadRequest("Invalid JSON")
|
||||||
|
}
|
||||||
|
|
||||||
|
log, err := l.FindLog(c.Request.Context(), c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Update(update)
|
||||||
|
err = db.Logs().Update(c.Request.Context(), log.Log)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return log, nil
|
||||||
|
}))
|
||||||
|
|
||||||
|
g.DELETE("/:id", handler("log", func(c *gin.Context) (interface{}, error) {
|
||||||
|
log, err := l.FindLog(c.Request.Context(), c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Logs().Delete(c.Request.Context(), log.Log)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return log, nil
|
||||||
|
}))
|
||||||
|
}
|
||||||
+101
@@ -0,0 +1,101 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gissleh/stufflog/database"
|
||||||
|
"github.com/gissleh/stufflog/internal/auth"
|
||||||
|
"github.com/gissleh/stufflog/internal/generate"
|
||||||
|
"github.com/gissleh/stufflog/internal/slerrors"
|
||||||
|
"github.com/gissleh/stufflog/models"
|
||||||
|
"github.com/gissleh/stufflog/services"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Project(g *gin.RouterGroup, db database.Database) {
|
||||||
|
l := services.Loader{DB: db}
|
||||||
|
|
||||||
|
defaultActive := true
|
||||||
|
|
||||||
|
g.GET("/", handler("projects", func(c *gin.Context) (interface{}, error) {
|
||||||
|
filter := models.ProjectFilter{}
|
||||||
|
if setting := c.Query("active"); setting != "" {
|
||||||
|
active := setting == "true"
|
||||||
|
filter.Active = &active
|
||||||
|
} else {
|
||||||
|
filter.Active = &defaultActive
|
||||||
|
}
|
||||||
|
if setting := c.Query("expiring"); setting != "" {
|
||||||
|
filter.Expiring = setting == "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
return l.ListProjects(c, filter)
|
||||||
|
}))
|
||||||
|
|
||||||
|
g.GET("/:id", handler("project", func(c *gin.Context) (interface{}, error) {
|
||||||
|
return l.FindProject(c, c.Param("id"))
|
||||||
|
}))
|
||||||
|
|
||||||
|
g.POST("/", handler("project", func(c *gin.Context) (interface{}, error) {
|
||||||
|
project := models.Project{}
|
||||||
|
err := c.BindJSON(&project)
|
||||||
|
if err != nil {
|
||||||
|
return nil, slerrors.BadRequest("Invalid JSON")
|
||||||
|
}
|
||||||
|
if project.EndTime != nil && project.EndTime.Before(time.Now()) {
|
||||||
|
return nil, slerrors.BadRequest("Project end time must be later than current time.")
|
||||||
|
}
|
||||||
|
|
||||||
|
project.ID = generate.ProjectID()
|
||||||
|
project.UserID = auth.UserID(c)
|
||||||
|
project.CreatedTime = time.Now().UTC()
|
||||||
|
|
||||||
|
err = db.Projects().Insert(c.Request.Context(), project)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &models.ProjectResult{
|
||||||
|
Project: project,
|
||||||
|
Tasks: []*models.TaskResult{},
|
||||||
|
}, nil
|
||||||
|
}))
|
||||||
|
|
||||||
|
g.PUT("/:id", handler("project", func(c *gin.Context) (interface{}, error) {
|
||||||
|
update := models.ProjectUpdate{}
|
||||||
|
err := c.BindJSON(&update)
|
||||||
|
if err != nil {
|
||||||
|
return nil, slerrors.BadRequest("Invalid JSON")
|
||||||
|
}
|
||||||
|
|
||||||
|
project, err := l.FindProject(c.Request.Context(), c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
project.Update(update)
|
||||||
|
err = db.Projects().Update(c.Request.Context(), project.Project)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return project, nil
|
||||||
|
}))
|
||||||
|
|
||||||
|
g.DELETE("/:id", handler("project", func(c *gin.Context) (interface{}, error) {
|
||||||
|
project, err := l.FindProject(c.Request.Context(), c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(project.Tasks) > 0 {
|
||||||
|
return nil, slerrors.Forbidden("cannot delete non-empty projects.")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Projects().Delete(c.Request.Context(), project.Project)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return project, nil
|
||||||
|
}))
|
||||||
|
}
|
||||||
+111
@@ -0,0 +1,111 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gissleh/stufflog/database"
|
||||||
|
"github.com/gissleh/stufflog/internal/auth"
|
||||||
|
"github.com/gissleh/stufflog/internal/generate"
|
||||||
|
"github.com/gissleh/stufflog/internal/slerrors"
|
||||||
|
"github.com/gissleh/stufflog/models"
|
||||||
|
"github.com/gissleh/stufflog/services"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Task(g *gin.RouterGroup, db database.Database) {
|
||||||
|
l := services.Loader{DB: db}
|
||||||
|
|
||||||
|
defaultActive := true
|
||||||
|
|
||||||
|
g.GET("/", handler("tasks", func(c *gin.Context) (interface{}, error) {
|
||||||
|
filter := models.TaskFilter{}
|
||||||
|
if setting := c.Query("active"); setting != "" {
|
||||||
|
active := setting == "true"
|
||||||
|
filter.Active = &active
|
||||||
|
} else {
|
||||||
|
filter.Active = &defaultActive
|
||||||
|
}
|
||||||
|
|
||||||
|
return l.ListTasks(c, filter)
|
||||||
|
}))
|
||||||
|
|
||||||
|
g.GET("/:id", handler("task", func(c *gin.Context) (interface{}, error) {
|
||||||
|
return l.FindTask(c, c.Param("id"))
|
||||||
|
}))
|
||||||
|
|
||||||
|
g.POST("/", handler("task", func(c *gin.Context) (interface{}, error) {
|
||||||
|
task := models.Task{}
|
||||||
|
err := c.BindJSON(&task)
|
||||||
|
if err != nil {
|
||||||
|
return nil, slerrors.BadRequest("Invalid JSON")
|
||||||
|
}
|
||||||
|
if task.EndTime != nil && task.EndTime.Before(time.Now()) {
|
||||||
|
return nil, slerrors.BadRequest("Project end time must be later than current time.")
|
||||||
|
}
|
||||||
|
|
||||||
|
project, err := l.FindProject(c.Request.Context(), task.ProjectID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
item, err := l.FindItem(c.Request.Context(), task.ItemID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
task.ID = generate.TaskID()
|
||||||
|
task.UserID = auth.UserID(c)
|
||||||
|
task.CreatedTime = time.Now().UTC()
|
||||||
|
task.ItemID = item.ID
|
||||||
|
task.ProjectID = project.ID
|
||||||
|
|
||||||
|
err = db.Tasks().Insert(c.Request.Context(), task)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &models.TaskResult{
|
||||||
|
Task: task,
|
||||||
|
Logs: []*models.Log{},
|
||||||
|
Item: &item.Item,
|
||||||
|
CompletedAmount: 0,
|
||||||
|
}, nil
|
||||||
|
}))
|
||||||
|
|
||||||
|
g.PUT("/:id", handler("task", func(c *gin.Context) (interface{}, error) {
|
||||||
|
update := models.TaskUpdate{}
|
||||||
|
err := c.BindJSON(&update)
|
||||||
|
if err != nil {
|
||||||
|
return nil, slerrors.BadRequest("Invalid JSON")
|
||||||
|
}
|
||||||
|
|
||||||
|
task, err := l.FindTask(c.Request.Context(), c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
task.Update(update)
|
||||||
|
err = db.Tasks().Update(c.Request.Context(), task.Task)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return task, nil
|
||||||
|
}))
|
||||||
|
|
||||||
|
g.DELETE("/:id", handler("task", func(c *gin.Context) (interface{}, error) {
|
||||||
|
task, err := l.FindTask(c.Request.Context(), c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(task.Logs) > 0 {
|
||||||
|
return nil, slerrors.Forbidden("cannot delete tasks with logs.")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Tasks().Delete(c.Request.Context(), task.Task)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return task, nil
|
||||||
|
}))
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/aws/aws-lambda-go/events"
|
||||||
|
"github.com/aws/aws-lambda-go/lambda"
|
||||||
|
ginadapter "github.com/awslabs/aws-lambda-go-api-proxy/gin"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gissleh/stufflog/api"
|
||||||
|
"github.com/gissleh/stufflog/database"
|
||||||
|
"github.com/gissleh/stufflog/internal/auth"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func env(key string, defaultValue string) string {
|
||||||
|
e := os.Getenv(key)
|
||||||
|
if e == "" {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
dbDriver := env("DB_DRIVER", "postgres")
|
||||||
|
dbConnect := env("DB_CONNECT", "")
|
||||||
|
|
||||||
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
|
||||||
|
db, err := database.Open(context.Background(), dbDriver, dbConnect)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to open database:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
server := gin.New()
|
||||||
|
server.Use(auth.TrustingJwtParserMiddleware())
|
||||||
|
|
||||||
|
api.Group(server.Group("/api/group"), db)
|
||||||
|
api.Item(server.Group("/api/item"), db)
|
||||||
|
api.Project(server.Group("/api/project"), db)
|
||||||
|
api.Task(server.Group("/api/task"), db)
|
||||||
|
api.Log(server.Group("/api/log"), db)
|
||||||
|
api.Goal(server.Group("/api/goal"), db)
|
||||||
|
|
||||||
|
ginLambda := ginadapter.New(server)
|
||||||
|
lambda.Start(func(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
|
||||||
|
return ginLambda.ProxyWithContext(ctx, request)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gissleh/stufflog/api"
|
||||||
|
"github.com/gissleh/stufflog/database"
|
||||||
|
"github.com/gissleh/stufflog/internal/auth"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func env(key string, defaultValue string) string {
|
||||||
|
e := os.Getenv(key)
|
||||||
|
if e == "" {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
dbDriver := env("DB_DRIVER", "postgres")
|
||||||
|
dbConnect := env("DB_CONNECT", "host=localhost user=stufflog2 password=stufflog2_dev_password dbname=stufflog2 sslmode=disable")
|
||||||
|
serverListen := env("SERVER_LISTEN", ":8000")
|
||||||
|
useDummyUuid := env("USE_DUMMY_UUID", "no")
|
||||||
|
dummyUuid := env("DUMMY_UUID", "9d3214f1-6321-403e-ab87-764ad1a1252d")
|
||||||
|
|
||||||
|
db, err := database.Open(context.Background(), dbDriver, dbConnect)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to open database:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
server := gin.New()
|
||||||
|
if useDummyUuid == "yes" {
|
||||||
|
server.Use(auth.DummyMiddleware(dummyUuid))
|
||||||
|
} else {
|
||||||
|
server.Use(auth.TrustingJwtParserMiddleware())
|
||||||
|
}
|
||||||
|
|
||||||
|
api.Group(server.Group("/api/group"), db)
|
||||||
|
api.Item(server.Group("/api/item"), db)
|
||||||
|
api.Project(server.Group("/api/project"), db)
|
||||||
|
api.Task(server.Group("/api/task"), db)
|
||||||
|
api.Log(server.Group("/api/log"), db)
|
||||||
|
api.Goal(server.Group("/api/goal"), db)
|
||||||
|
|
||||||
|
exitSignal := make(chan os.Signal)
|
||||||
|
signal.Notify(exitSignal, os.Interrupt, os.Kill, syscall.SIGTERM)
|
||||||
|
|
||||||
|
errCh := make(chan error)
|
||||||
|
go func() {
|
||||||
|
err := server.Run(serverListen)
|
||||||
|
if err != nil {
|
||||||
|
errCh <- err
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case sig := <-exitSignal:
|
||||||
|
{
|
||||||
|
log.Println("Received signal", sig)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
case err := <-errCh:
|
||||||
|
{
|
||||||
|
log.Println("Server run failed:", err)
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"github.com/gissleh/stufflog/database/postgres"
|
||||||
|
"github.com/gissleh/stufflog/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrUnsupportedDriver = errors.New("usupported driver")
|
||||||
|
|
||||||
|
type Database interface {
|
||||||
|
Goals() models.GoalRepository
|
||||||
|
Groups() models.GroupRepository
|
||||||
|
Items() models.ItemRepository
|
||||||
|
Logs() models.LogRepository
|
||||||
|
Projects() models.ProjectRepository
|
||||||
|
Tasks() models.TaskRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func Open(ctx context.Context, driver string, connect string) (Database, error) {
|
||||||
|
switch driver {
|
||||||
|
case "postgres":
|
||||||
|
return postgres.Setup(ctx, connect)
|
||||||
|
default:
|
||||||
|
return nil, ErrUnsupportedDriver
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/gissleh/stufflog/models"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
|
||||||
|
_ "github.com/lib/pq"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Database struct {
|
||||||
|
db *sqlx.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) Goals() models.GoalRepository {
|
||||||
|
return &goalRepository{db: d.db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) Groups() models.GroupRepository {
|
||||||
|
return &groupRepository{db: d.db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) Items() models.ItemRepository {
|
||||||
|
return &itemRepository{db: d.db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) Logs() models.LogRepository {
|
||||||
|
return &logRepository{db: d.db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) Projects() models.ProjectRepository {
|
||||||
|
return &projectRepository{db: d.db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) Tasks() models.TaskRepository {
|
||||||
|
return &taskRepository{db: d.db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Setup(ctx context.Context, connect string) (*Database, error) {
|
||||||
|
db, err := sqlx.ConnectContext(ctx, "postgres", connect)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.PingContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Database{db: db}, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
package postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"github.com/Masterminds/squirrel"
|
||||||
|
"github.com/gissleh/stufflog/internal/slerrors"
|
||||||
|
"github.com/gissleh/stufflog/models"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type goalRepository struct {
|
||||||
|
db *sqlx.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *goalRepository) Find(ctx context.Context, id string) (*models.Goal, error) {
|
||||||
|
res := models.Goal{}
|
||||||
|
err := r.db.GetContext(ctx, &res, "SELECT * FROM goal WHERE goal_id=$1", id)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, slerrors.NotFound("Goal")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *goalRepository) List(ctx context.Context, filter models.GoalFilter) ([]*models.Goal, error) {
|
||||||
|
sq := squirrel.Select("*").From("goal").PlaceholderFormat(squirrel.Dollar)
|
||||||
|
sq = sq.Where(squirrel.Eq{"user_id": filter.UserID})
|
||||||
|
if len(filter.IDs) > 0 {
|
||||||
|
sq = sq.Where(squirrel.Eq{"goal_id": filter.IDs})
|
||||||
|
}
|
||||||
|
if filter.MinTime != nil {
|
||||||
|
sq = sq.Where(squirrel.GtOrEq{
|
||||||
|
"end_time": *filter.MinTime,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if filter.MaxTime != nil {
|
||||||
|
sq = sq.Where(squirrel.LtOrEq{
|
||||||
|
"start_time": *filter.MaxTime,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if filter.IncludesTime != nil {
|
||||||
|
sq = sq.Where(squirrel.LtOrEq{
|
||||||
|
"start_time": *filter.IncludesTime,
|
||||||
|
}).Where(squirrel.GtOrEq{
|
||||||
|
"end_time": *filter.IncludesTime,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
sq = sq.OrderBy("start_time", "end_time", "name")
|
||||||
|
|
||||||
|
query, args, err := sq.ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res := make([]*models.Goal, 0, 8)
|
||||||
|
err = r.db.SelectContext(ctx, &res, query, args...)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *goalRepository) Insert(ctx context.Context, goal models.Goal) error {
|
||||||
|
_, err := r.db.NamedExecContext(ctx, `
|
||||||
|
INSERT INTO goal (
|
||||||
|
goal_id, user_id, group_id, amount, start_time, end_time, name, description
|
||||||
|
) VALUES (
|
||||||
|
:goal_id, :user_id, :group_id, :amount, :start_time, :end_time, :name, :description
|
||||||
|
)
|
||||||
|
`, &goal)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *goalRepository) Update(ctx context.Context, goal models.Goal) error {
|
||||||
|
_, err := r.db.NamedExecContext(ctx, `
|
||||||
|
UPDATE goal SET
|
||||||
|
amount=:amount,
|
||||||
|
start_time=:start_time,
|
||||||
|
end_time=:end_time,
|
||||||
|
name=:name,
|
||||||
|
description=:description
|
||||||
|
WHERE goal_id=:goal_id
|
||||||
|
`, &goal)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *goalRepository) Delete(ctx context.Context, goal models.Goal) error {
|
||||||
|
_, err := r.db.ExecContext(ctx, `DELETE FROM goal WHERE goal_id=$1`, goal.ID)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return slerrors.NotFound("Goal")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"github.com/Masterminds/squirrel"
|
||||||
|
"github.com/gissleh/stufflog/internal/slerrors"
|
||||||
|
"github.com/gissleh/stufflog/models"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type groupRepository struct {
|
||||||
|
db *sqlx.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *groupRepository) Find(ctx context.Context, id string) (*models.Group, error) {
|
||||||
|
res := models.Group{}
|
||||||
|
err := r.db.GetContext(ctx, &res, "SELECT * FROM \"group\" WHERE group_id=$1", id)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, slerrors.NotFound("Group")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *groupRepository) List(ctx context.Context, filter models.GroupFilter) ([]*models.Group, error) {
|
||||||
|
sq := squirrel.Select("*").From("\"group\"").PlaceholderFormat(squirrel.Dollar)
|
||||||
|
sq = sq.Where(squirrel.Eq{"user_id": filter.UserID})
|
||||||
|
if len(filter.IDs) > 0 {
|
||||||
|
sq = sq.Where(squirrel.Eq{"group_id": filter.IDs})
|
||||||
|
}
|
||||||
|
|
||||||
|
sq = sq.OrderBy("name")
|
||||||
|
|
||||||
|
query, args, err := sq.ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res := make([]*models.Group, 0, 8)
|
||||||
|
err = r.db.SelectContext(ctx, &res, query, args...)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return []*models.Group{}, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *groupRepository) Insert(ctx context.Context, group models.Group) error {
|
||||||
|
_, err := r.db.NamedExecContext(ctx, `
|
||||||
|
INSERT INTO "group" (
|
||||||
|
group_id, user_id, name, icon, description
|
||||||
|
) VALUES (
|
||||||
|
:group_id, :user_id, :name, :icon, :description
|
||||||
|
)
|
||||||
|
`, &group)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *groupRepository) Update(ctx context.Context, group models.Group) error {
|
||||||
|
_, err := r.db.NamedExecContext(ctx, `
|
||||||
|
UPDATE "group" SET
|
||||||
|
name=:name,
|
||||||
|
icon=:icon,
|
||||||
|
description=:description
|
||||||
|
WHERE group_id=:group_id
|
||||||
|
`, &group)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *groupRepository) Delete(ctx context.Context, group models.Group) error {
|
||||||
|
_, err := r.db.ExecContext(ctx, `DELETE FROM "group" WHERE group_id=$1`, group.ID)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return slerrors.NotFound("Group")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
package postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"github.com/Masterminds/squirrel"
|
||||||
|
"github.com/gissleh/stufflog/internal/slerrors"
|
||||||
|
"github.com/gissleh/stufflog/models"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type itemRepository struct {
|
||||||
|
db *sqlx.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *itemRepository) Find(ctx context.Context, id string) (*models.Item, error) {
|
||||||
|
res := models.Item{}
|
||||||
|
err := r.db.GetContext(ctx, &res, "SELECT item.*, g.icon FROM item INNER JOIN \"group\" AS g ON item.group_id = g.group_id WHERE item_id=$1", id)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, slerrors.NotFound("Item")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *itemRepository) List(ctx context.Context, filter models.ItemFilter) ([]*models.Item, error) {
|
||||||
|
sq := squirrel.Select("item.*", "g.icon").From("item").PlaceholderFormat(squirrel.Dollar)
|
||||||
|
sq = sq.Where(squirrel.Eq{"item.user_id": filter.UserID})
|
||||||
|
if len(filter.IDs) > 0 {
|
||||||
|
sq = sq.Where(squirrel.Eq{"item.item_id": filter.IDs})
|
||||||
|
}
|
||||||
|
if len(filter.GroupIDs) > 0 {
|
||||||
|
sq = sq.Where(squirrel.Eq{"item.group_id": filter.GroupIDs})
|
||||||
|
}
|
||||||
|
|
||||||
|
sq = sq.InnerJoin("\"group\" AS g ON item.group_id = g.group_id")
|
||||||
|
sq = sq.OrderBy("item.group_weight", "item.name")
|
||||||
|
|
||||||
|
query, args, err := sq.ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res := make([]*models.Item, 0, 8)
|
||||||
|
err = r.db.SelectContext(ctx, &res, query, args...)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *itemRepository) Insert(ctx context.Context, item models.Item) error {
|
||||||
|
_, err := r.db.NamedExecContext(ctx, `
|
||||||
|
INSERT INTO item (
|
||||||
|
item_id, user_id, group_id, group_weight, name, description
|
||||||
|
) VALUES (
|
||||||
|
:item_id, :user_id, :group_id, :group_weight, :name, :description
|
||||||
|
)
|
||||||
|
`, &item)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *itemRepository) Update(ctx context.Context, item models.Item) error {
|
||||||
|
_, err := r.db.NamedExecContext(ctx, `
|
||||||
|
UPDATE item SET
|
||||||
|
group_weight = :group_weight,
|
||||||
|
name = :name,
|
||||||
|
description = :description
|
||||||
|
WHERE item_id=:item_id
|
||||||
|
`, &item)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *itemRepository) Delete(ctx context.Context, item models.Item) error {
|
||||||
|
_, err := r.db.ExecContext(ctx, `DELETE FROM item WHERE item_id=$1`, item.ID)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return slerrors.NotFound("Item")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"github.com/Masterminds/squirrel"
|
||||||
|
"github.com/gissleh/stufflog/internal/slerrors"
|
||||||
|
"github.com/gissleh/stufflog/models"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type logRepository struct {
|
||||||
|
db *sqlx.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *logRepository) Find(ctx context.Context, id string) (*models.Log, error) {
|
||||||
|
res := models.Log{}
|
||||||
|
err := r.db.GetContext(ctx, &res, "SELECT * FROM log WHERE log_id=$1", id)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, slerrors.NotFound("Log")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *logRepository) List(ctx context.Context, filter models.LogFilter) ([]*models.Log, error) {
|
||||||
|
sq := squirrel.Select("log.*").From("log").PlaceholderFormat(squirrel.Dollar)
|
||||||
|
sq = sq.Where(squirrel.Eq{"user_id": filter.UserID})
|
||||||
|
if len(filter.IDs) > 0 {
|
||||||
|
sq = sq.Where(squirrel.Eq{"task_id": filter.IDs})
|
||||||
|
}
|
||||||
|
if len(filter.ItemIDs) > 0 {
|
||||||
|
sq = sq.Where(squirrel.Eq{"item_id": filter.ItemIDs})
|
||||||
|
}
|
||||||
|
if filter.MinTime != nil {
|
||||||
|
sq = sq.Where(squirrel.GtOrEq{
|
||||||
|
"logged_time": *filter.MinTime,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if filter.MaxTime != nil {
|
||||||
|
sq = sq.Where(squirrel.LtOrEq{
|
||||||
|
"logged_time": *filter.MaxTime,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
sq = sq.OrderBy("logged_time")
|
||||||
|
|
||||||
|
query, args, err := sq.ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res := make([]*models.Log, 0, 8)
|
||||||
|
err = r.db.SelectContext(ctx, &res, query, args...)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *logRepository) Insert(ctx context.Context, log models.Log) error {
|
||||||
|
_, err := r.db.NamedExecContext(ctx, `
|
||||||
|
INSERT INTO log (
|
||||||
|
log_id, user_id, task_id, item_id, logged_time, description
|
||||||
|
) VALUES (
|
||||||
|
:log_id, :user_id, :task_id, :item_id, :logged_time, :description
|
||||||
|
)
|
||||||
|
`, &log)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *logRepository) Update(ctx context.Context, log models.Log) error {
|
||||||
|
_, err := r.db.NamedExecContext(ctx, `
|
||||||
|
UPDATE log SET
|
||||||
|
logged_time=:logged_time,
|
||||||
|
description=:description
|
||||||
|
WHERE log_id=:log_id
|
||||||
|
`, &log)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *logRepository) Delete(ctx context.Context, log models.Log) error {
|
||||||
|
_, err := r.db.ExecContext(ctx, `DELETE FROM log WHERE log_id=$1`, log.ID)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return slerrors.NotFound("Log")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
package postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"github.com/Masterminds/squirrel"
|
||||||
|
"github.com/gissleh/stufflog/internal/slerrors"
|
||||||
|
"github.com/gissleh/stufflog/models"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type projectRepository struct {
|
||||||
|
db *sqlx.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *projectRepository) Find(ctx context.Context, id string) (*models.Project, error) {
|
||||||
|
res := models.Project{}
|
||||||
|
err := r.db.GetContext(ctx, &res, "SELECT * FROM project WHERE project_id=$1", id)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, slerrors.NotFound("Log")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *projectRepository) List(ctx context.Context, filter models.ProjectFilter) ([]*models.Project, error) {
|
||||||
|
sq := squirrel.Select("*").From("project").PlaceholderFormat(squirrel.Dollar)
|
||||||
|
sq = sq.Where(squirrel.Eq{"user_id": filter.UserID})
|
||||||
|
if filter.IDs != nil {
|
||||||
|
sq = sq.Where(squirrel.Eq{"project_id": filter.IDs})
|
||||||
|
}
|
||||||
|
if filter.Active != nil {
|
||||||
|
sq = sq.Where(squirrel.Eq{"active": *filter.Active})
|
||||||
|
}
|
||||||
|
if filter.Expiring {
|
||||||
|
sq = sq.Where("end_time IS NOT NULL")
|
||||||
|
}
|
||||||
|
|
||||||
|
sq = sq.OrderBy("created_time DESC")
|
||||||
|
|
||||||
|
query, args, err := sq.ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res := make([]*models.Project, 0, 8)
|
||||||
|
err = r.db.SelectContext(ctx, &res, query, args...)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *projectRepository) Insert(ctx context.Context, project models.Project) error {
|
||||||
|
_, err := r.db.NamedExecContext(ctx, `
|
||||||
|
INSERT INTO project(
|
||||||
|
project_id, user_id, name, description, icon, active, created_time, end_time
|
||||||
|
) VALUES (
|
||||||
|
:project_id, :user_id, :name, :description, :icon, :active, :created_time, :end_time
|
||||||
|
)
|
||||||
|
`, &project)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *projectRepository) Update(ctx context.Context, project models.Project) error {
|
||||||
|
_, err := r.db.NamedExecContext(ctx, `
|
||||||
|
UPDATE project SET
|
||||||
|
name = :name,
|
||||||
|
description = :description,
|
||||||
|
icon = :icon,
|
||||||
|
active = :active,
|
||||||
|
end_time = :end_time
|
||||||
|
WHERE project_id=:project_id
|
||||||
|
`, &project)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *projectRepository) Delete(ctx context.Context, project models.Project) error {
|
||||||
|
_, err := r.db.ExecContext(ctx, `DELETE FROM project WHERE project_id=$1`, project.ID)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return slerrors.NotFound("Project")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"github.com/Masterminds/squirrel"
|
||||||
|
"github.com/gissleh/stufflog/internal/slerrors"
|
||||||
|
"github.com/gissleh/stufflog/models"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type taskRepository struct {
|
||||||
|
db *sqlx.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *taskRepository) Find(ctx context.Context, id string) (*models.Task, error) {
|
||||||
|
res := models.Task{}
|
||||||
|
err := r.db.GetContext(ctx, &res, "SELECT task.*, p.icon FROM task INNER JOIN project AS p ON task.project_id = p.project_id WHERE task_id=$1", id)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, slerrors.NotFound("Task")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *taskRepository) List(ctx context.Context, filter models.TaskFilter) ([]*models.Task, error) {
|
||||||
|
sq := squirrel.Select("task.*", "p.icon").From("task").PlaceholderFormat(squirrel.Dollar)
|
||||||
|
sq = sq.Where(squirrel.Eq{"task.user_id": filter.UserID})
|
||||||
|
if filter.Active != nil {
|
||||||
|
sq = sq.Where(squirrel.Eq{"task.active": *filter.Active})
|
||||||
|
}
|
||||||
|
if filter.IDs != nil {
|
||||||
|
sq = sq.Where(squirrel.Eq{"task.task_id": filter.IDs})
|
||||||
|
}
|
||||||
|
if filter.ItemIDs != nil {
|
||||||
|
sq = sq.Where(squirrel.Eq{"task.item_id": filter.ItemIDs})
|
||||||
|
}
|
||||||
|
if filter.ProjectIDs != nil {
|
||||||
|
sq = sq.Where(squirrel.Eq{"task.project_id": filter.ProjectIDs})
|
||||||
|
}
|
||||||
|
|
||||||
|
sq = sq.InnerJoin("project AS p ON task.project_id = p.project_id")
|
||||||
|
sq = sq.OrderBy("created_time")
|
||||||
|
|
||||||
|
query, args, err := sq.ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res := make([]*models.Task, 0, 8)
|
||||||
|
err = r.db.SelectContext(ctx, &res, query, args...)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *taskRepository) Insert(ctx context.Context, task models.Task) error {
|
||||||
|
_, err := r.db.NamedExecContext(ctx, `
|
||||||
|
INSERT INTO task (
|
||||||
|
task_id, user_id, item_id, project_id, item_amount, name, description, active, created_time, end_time
|
||||||
|
) VALUES (
|
||||||
|
:task_id, :user_id, :item_id, :project_id, :item_amount, :name, :description, :active, :created_time, :end_time
|
||||||
|
)
|
||||||
|
`, &task)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *taskRepository) Update(ctx context.Context, task models.Task) error {
|
||||||
|
_, err := r.db.NamedExecContext(ctx, `
|
||||||
|
UPDATE task SET
|
||||||
|
item_id = :item_id,
|
||||||
|
item_amount = :item_amount,
|
||||||
|
name = :name,
|
||||||
|
description = :description,
|
||||||
|
active = :active,
|
||||||
|
end_time = :end_time
|
||||||
|
WHERE task_id=:task_id
|
||||||
|
`, &task)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *taskRepository) Delete(ctx context.Context, task models.Task) error {
|
||||||
|
_, err := r.db.ExecContext(ctx, `DELETE FROM task WHERE task_id=$1`, task.ID)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return slerrors.NotFound("Task")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
module github.com/gissleh/stufflog
|
||||||
|
|
||||||
|
go 1.14
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/AchievementNetwork/stringset v1.1.0
|
||||||
|
github.com/Masterminds/squirrel v1.5.0
|
||||||
|
github.com/aws/aws-lambda-go v1.22.0
|
||||||
|
github.com/awslabs/aws-lambda-go-api-proxy v0.9.0
|
||||||
|
github.com/gin-gonic/gin v1.6.3
|
||||||
|
github.com/go-sql-driver/mysql v1.5.0 // indirect
|
||||||
|
github.com/jmoiron/sqlx v1.2.0
|
||||||
|
github.com/lib/pq v1.9.0
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.6 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/pressly/goose v2.6.0+incompatible // indirect
|
||||||
|
github.com/ziutek/mymysql v1.5.4 // indirect
|
||||||
|
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a
|
||||||
|
google.golang.org/appengine v1.6.7 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +1,333 @@
|
|||||||
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
|
github.com/AchievementNetwork/stringset v1.1.0 h1:6iGjAlend+mCls1tvEAxOBAUTb6nO6QG1/gfBzmPn+s=
|
||||||
|
github.com/AchievementNetwork/stringset v1.1.0/go.mod h1:FhOLOVB2mo9zbOR/N+hekGXEnh1VRbJprLlBk/HY71M=
|
||||||
|
github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
|
||||||
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno=
|
||||||
|
github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMdUywE7VMo=
|
||||||
|
github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY=
|
||||||
|
github.com/Masterminds/squirrel v1.5.0 h1:JukIZisrUXadA9pl3rMkjhiamxiB0cXiu+HGp/Y8cY8=
|
||||||
|
github.com/Masterminds/squirrel v1.5.0/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
|
||||||
|
github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0=
|
||||||
|
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||||
|
github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
||||||
|
github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
||||||
|
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||||
|
github.com/aws/aws-lambda-go v1.19.1/go.mod h1:jJmlefzPfGnckuHdXX7/80O3BvUUi12XOkbv4w9SGLU=
|
||||||
|
github.com/aws/aws-lambda-go v1.22.0 h1:X7BKqIdfoJcbsEIi+Lrt5YjX1HnZexIbNWOQgkYKgfE=
|
||||||
|
github.com/aws/aws-lambda-go v1.22.0/go.mod h1:jJmlefzPfGnckuHdXX7/80O3BvUUi12XOkbv4w9SGLU=
|
||||||
|
github.com/awslabs/aws-lambda-go-api-proxy v0.9.0 h1:oawiEVOu1ER3ROpDg8CaQ+V7A52frLGD3taPQjTywng=
|
||||||
|
github.com/awslabs/aws-lambda-go-api-proxy v0.9.0/go.mod h1:O8jHVv+ga5Kpg8+6i8qSZFp9rnxC1KB/R2yNFNgtFis=
|
||||||
|
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||||
|
github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g=
|
||||||
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
|
github.com/chris-ramon/douceur v0.2.0/go.mod h1:wDW5xjJdeoMm1mRt4sD4c/LbF/mWdEpRXQKjTR8nIBE=
|
||||||
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
|
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||||
|
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||||
|
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||||
|
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||||
|
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/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4=
|
||||||
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||||
|
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||||
|
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||||
|
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
|
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
|
github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw=
|
||||||
|
github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8=
|
||||||
|
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||||
|
github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
|
||||||
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
|
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
|
||||||
|
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||||
|
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
|
||||||
|
github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
||||||
|
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||||
|
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||||
|
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||||
|
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
||||||
|
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||||
|
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
|
||||||
|
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||||
|
github.com/go-playground/validator/v10 v10.3.0 h1:nZU+7q+yJoFmwvNgv/LnPUkwPal62+b2xXj0AU1Es7o=
|
||||||
|
github.com/go-playground/validator/v10 v10.3.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||||
|
github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk=
|
||||||
|
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||||
|
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
||||||
|
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||||
|
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||||
|
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||||
|
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||||
|
github.com/gofiber/fiber/v2 v2.1.0/go.mod h1:aG+lMkwy3LyVit4CnmYUbUdgjpc3UYOltvlJZ78rgQ0=
|
||||||
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
|
||||||
|
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||||
|
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||||
|
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||||
|
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
|
||||||
|
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
|
github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
||||||
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
|
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
||||||
|
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||||
|
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||||
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
|
github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
|
||||||
|
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||||
|
github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI=
|
||||||
|
github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0=
|
||||||
|
github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk=
|
||||||
|
github.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0GqwkjqxNd0u65g=
|
||||||
|
github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw=
|
||||||
|
github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA=
|
||||||
|
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
|
||||||
|
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
|
||||||
|
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
|
github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
|
||||||
|
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
|
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
|
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
|
||||||
|
github.com/kataras/golog v0.0.10/go.mod h1:yJ8YKCmyL+nWjERB90Qwn+bdyBZsaQwU3bTVFgkFIp8=
|
||||||
|
github.com/kataras/golog v0.0.18/go.mod h1:jRYl7dFYqP8aQj9VkwdBUXYZSfUktm+YYg1arJILfyw=
|
||||||
|
github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYbq3UhfoFmE=
|
||||||
|
github.com/kataras/neffos v0.0.14/go.mod h1:8lqADm8PnbeFfL7CLXh1WHw53dG27MC3pgi2R1rmoTE=
|
||||||
|
github.com/kataras/pio v0.0.2/go.mod h1:hAoW0t9UmXi4R5Oyq5Z4irTbaTsOemSrDGUtaTl7Dro=
|
||||||
|
github.com/kataras/pio v0.0.8/go.mod h1:NFfMp2kVP1rmV4N6gH6qgWpuoDKlrOeYi3VrAIWCGsE=
|
||||||
|
github.com/kataras/sitemap v0.0.5/go.mod h1:KY2eugMKiPwsJgx7+U103YZehfvNGOXURubcGyk0Bz8=
|
||||||
|
github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||||
|
github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||||
|
github.com/klauspost/compress v1.11.1/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/labstack/echo/v4 v4.1.17/go.mod h1:Tn2yRQL/UclUalpb5rPdXDevbkJ+lp/2svdyFBg6CHQ=
|
||||||
|
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
|
||||||
|
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
|
||||||
|
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
|
||||||
|
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
|
||||||
|
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
|
||||||
|
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||||
|
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||||
|
github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
|
||||||
|
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
|
github.com/lib/pq v1.9.0 h1:L8nSXQQzAYByakOFMTwpjRoHsMJklur4Gi59b6VivR8=
|
||||||
|
github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
|
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
|
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
|
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||||
|
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||||
|
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
|
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||||
|
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||||
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
|
github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/4=
|
||||||
|
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||||
|
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
|
||||||
|
github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8=
|
||||||
|
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
|
||||||
|
github.com/microcosm-cc/bluemonday v1.0.3/go.mod h1:8iwZnFn2CDDNZ0r6UXhF4xawGvzaqzCRa1n3/lO3W2w=
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
|
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
|
||||||
|
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
|
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||||
|
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
|
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
|
||||||
|
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
|
||||||
|
github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
|
||||||
|
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
|
||||||
|
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||||
|
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||||
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||||
|
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
|
||||||
|
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||||
|
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||||
|
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||||
|
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
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/pressly/goose v2.6.0+incompatible h1:3f8zIQ8rfgP9tyI0Hmcs2YNAqUCL1c+diLe3iU8Qd/k=
|
||||||
|
github.com/pressly/goose v2.6.0+incompatible/go.mod h1:m+QHWCqxR3k8D9l7qfzuC/djtlfzxr34mozWDYEu1z8=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||||
|
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||||
|
github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g=
|
||||||
|
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||||
|
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||||
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||||
|
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||||
|
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||||
|
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||||
|
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||||
|
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||||
|
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
|
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||||
|
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||||
|
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||||
|
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||||
|
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||||
|
github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
|
||||||
|
github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
|
github.com/valyala/fasthttp v1.16.0/go.mod h1:YOKImeEosDdBPnxc0gy7INqi3m1zK6A+xl6TwOBhHCA=
|
||||||
|
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
||||||
|
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||||
|
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
|
||||||
|
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||||
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||||
|
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||||
|
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||||
|
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
|
||||||
|
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
|
||||||
|
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
|
||||||
|
github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
|
||||||
|
github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs=
|
||||||
|
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
|
||||||
|
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
|
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs=
|
||||||
|
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/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-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
|
||||||
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201016160150-f659759dc4ca h1:mLWBs1i4Qi5cHWGEtn2jieJQ2qtwV/gT0A2zLrmzaoE=
|
||||||
|
golang.org/x/sys v0.0.0-20201016160150-f659759dc4ca/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||||
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
|
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
|
||||||
|
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
|
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
|
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||||
|
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||||
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
|
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||||
|
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||||
|
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||||
|
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||||
|
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
|
||||||
|
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||||
|
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/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
|
gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
|
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||||
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||||
|
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gissleh/stufflog/internal/slerrors"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var contextKey = struct{}{}
|
||||||
|
|
||||||
|
func UserID(ctx context.Context) string {
|
||||||
|
if c, ok := ctx.(*gin.Context); ok {
|
||||||
|
return UserID(c.Request.Context())
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Value(&contextKey).(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DummyMiddleware(uuid string) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
c.Request = c.Request.WithContext(
|
||||||
|
context.WithValue(c.Request.Context(), &contextKey, uuid),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func abortRequest(c *gin.Context) {
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized, slerrors.ErrorResponse{
|
||||||
|
Code: http.StatusUnauthorized,
|
||||||
|
Message: "You're not supposed to be here!",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrustingJwtParserMiddleware is meant to be put behind an AWS API gateway that has already
|
||||||
|
// verified this token.
|
||||||
|
func TrustingJwtParserMiddleware() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
auth := c.GetHeader("Authorization")
|
||||||
|
split := strings.Split(auth, ".")
|
||||||
|
|
||||||
|
if len(split) >= 3 {
|
||||||
|
data, err := base64.RawStdEncoding.DecodeString(split[1])
|
||||||
|
if err != nil {
|
||||||
|
abortRequest(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fields := make(map[string]interface{})
|
||||||
|
err = json.Unmarshal(data, &fields)
|
||||||
|
if err != nil {
|
||||||
|
abortRequest(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if sub, ok := fields["sub"].(string); ok {
|
||||||
|
c.Request = c.Request.WithContext(
|
||||||
|
context.WithValue(c.Request.Context(), &contextKey, sub),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
abortRequest(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
abortRequest(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package generate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func id(prefix string, length int) string {
|
||||||
|
var id [16]byte
|
||||||
|
var buffer [32]byte
|
||||||
|
builder := strings.Builder{}
|
||||||
|
builder.Grow(length + 31)
|
||||||
|
builder.WriteString(prefix)
|
||||||
|
|
||||||
|
for builder.Len() < length {
|
||||||
|
_, err := rand.Read(id[:])
|
||||||
|
if err != nil {
|
||||||
|
log.Panicln("generate.id: failed to use OS random:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hex.Encode(buffer[:], id[:])
|
||||||
|
builder.Write(buffer[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.String()[:length]
|
||||||
|
}
|
||||||
|
|
||||||
|
func GroupID() string {
|
||||||
|
return id("G", 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ItemID() string {
|
||||||
|
return id("I", 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ProjectID() string {
|
||||||
|
return id("P", 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TaskID() string {
|
||||||
|
return id("T", 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
func LogID() string {
|
||||||
|
return id("L", 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GoalID() string {
|
||||||
|
return id("A", 16)
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package slerrors
|
||||||
|
|
||||||
|
type badRequestError struct {
|
||||||
|
Text string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err *badRequestError) Error() string {
|
||||||
|
return "validation failed: " + err.Text
|
||||||
|
}
|
||||||
|
|
||||||
|
func BadRequest(text string) error {
|
||||||
|
return &badRequestError{Text: text}
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsBadRequest(err error) bool {
|
||||||
|
_, ok := err.(*badRequestError)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package slerrors
|
||||||
|
|
||||||
|
type forbiddenError struct {
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *forbiddenError) Error() string {
|
||||||
|
return e.Message
|
||||||
|
}
|
||||||
|
|
||||||
|
func Forbidden(message string) error {
|
||||||
|
return &forbiddenError{Message: message}
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsForbidden(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
_, ok := err.(*forbiddenError)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package slerrors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ErrorResponse struct {
|
||||||
|
Code int `json:"errorCode"`
|
||||||
|
Message string `json:"errorMessage"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func Respond(c *gin.Context, err error) {
|
||||||
|
if IsNotFound(err) {
|
||||||
|
c.JSON(http.StatusNotFound, ErrorResponse{
|
||||||
|
Code: http.StatusNotFound,
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
} else if IsForbidden(err) {
|
||||||
|
c.JSON(http.StatusForbidden, ErrorResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
} else if IsBadRequest(err) {
|
||||||
|
c.JSON(http.StatusBadRequest, ErrorResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusInternalServerError, ErrorResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package slerrors
|
||||||
|
|
||||||
|
type notFoundError struct {
|
||||||
|
Subject string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err *notFoundError) Error() string {
|
||||||
|
return err.Subject + " not found"
|
||||||
|
}
|
||||||
|
|
||||||
|
func NotFound(subject string) error {
|
||||||
|
return ¬FoundError{Subject: subject}
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsNotFound(err error) bool {
|
||||||
|
_, ok := err.(*notFoundError)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE TABLE "group" (
|
||||||
|
group_id CHAR(16) PRIMARY KEY,
|
||||||
|
user_id CHAR(36) NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
icon TEXT NOT NULL,
|
||||||
|
description TEXT NOT NULL
|
||||||
|
);
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
DROP TABLE "group";
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE TABLE item (
|
||||||
|
item_id CHAR(16) PRIMARY KEY,
|
||||||
|
user_id CHAR(36) NOT NULL,
|
||||||
|
group_id CHAR(16) NOT NULL,
|
||||||
|
group_weight INT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
description TEXT NOT NULL
|
||||||
|
);
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
DROP TABLE item;
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE TABLE project (
|
||||||
|
project_id CHAR(16) PRIMARY KEY,
|
||||||
|
user_id CHAR(36) NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
description TEXT NOT NULL,
|
||||||
|
icon TEXT NOT NULL,
|
||||||
|
active BOOLEAN NOT NULL,
|
||||||
|
created_time TIMESTAMP NOT NULL,
|
||||||
|
end_time TIMESTAMP
|
||||||
|
);
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
DROP TABLE project;
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE TABLE task (
|
||||||
|
task_id CHAR(16) PRIMARY KEY,
|
||||||
|
user_id CHAR(36) NOT NULL,
|
||||||
|
item_id CHAR(16) NOT NULL,
|
||||||
|
project_id CHAR(16) NOT NULL,
|
||||||
|
item_amount INT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
description TEXT NOT NULL,
|
||||||
|
created_time TIMESTAMP NOT NULL,
|
||||||
|
active BOOLEAN NOT NULL,
|
||||||
|
end_time TIMESTAMP
|
||||||
|
);
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
DROP TABLE task;
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE TABLE log (
|
||||||
|
log_id CHAR(16) PRIMARY KEY,
|
||||||
|
user_id CHAR(36) NOT NULL,
|
||||||
|
task_id CHAR(16) NOT NULL,
|
||||||
|
item_id CHAR(16) NOT NULL,
|
||||||
|
logged_time TIMESTAMP NOT NULL,
|
||||||
|
description TEXT NOT NULL
|
||||||
|
);
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
DROP TABLE log;
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE TABLE goal (
|
||||||
|
goal_id CHAR(16) PRIMARY KEY,
|
||||||
|
user_id CHAR(36) NOT NULL,
|
||||||
|
group_id CHAR(16) NOT NULL,
|
||||||
|
start_time TIMESTAMP NOT NULL,
|
||||||
|
end_time TIMESTAMP NOT NULL,
|
||||||
|
amount INTEGER NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
description TEXT NOT NULL
|
||||||
|
);
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
DROP TABLE goal;
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE INDEX item_group_id on item(group_id);
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
DROP INDEX item_group_id;
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE INDEX log_task_id on log(task_id);
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
DROP INDEX log_task_id;
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE INDEX goal_start_time on goal(start_time);
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
DROP INDEX goal_start_time;
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE INDEX goal_end_time on goal(end_time);
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
DROP INDEX goal_end_time;
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE INDEX group_user_id on "group"(user_id);
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
DROP INDEX group_user_id;
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE INDEX item_user_id on item(user_id);
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
DROP INDEX item_user_id;
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE INDEX project_user_id on project(user_id);
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
DROP INDEX project_user_id;
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE INDEX task_user_id on task(user_id);
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
DROP INDEX task_user_id;
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE INDEX log_user_id on log(user_id);
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
DROP INDEX log_user_id;
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE INDEX goal_user_id on goal(user_id);
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
DROP INDEX goal_user_id;
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE INDEX project_created_time on project(created_time);
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
DROP INDEX project_created_time;
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE INDEX task_item_id on task(item_id);
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
DROP INDEX task_item_id;
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE INDEX log_logged_time on log(logged_time);
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
DROP INDEX log_logged_time;
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE INDEX log_item_id on log(item_id);
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
DROP INDEX log_item_id;
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Goal struct {
|
||||||
|
ID string `json:"id" db:"goal_id"`
|
||||||
|
UserID string `json:"-" db:"user_id"`
|
||||||
|
GroupID string `json:"groupId" db:"group_id"`
|
||||||
|
StartTime time.Time `json:"startTime" db:"start_time"`
|
||||||
|
EndTime time.Time `json:"endTime" db:"end_time"`
|
||||||
|
Amount int `json:"amount" db:"amount"`
|
||||||
|
Name string `json:"name" db:"name"`
|
||||||
|
Description string `json:"description" db:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (goal *Goal) Update(update GoalUpdate) {
|
||||||
|
if update.Amount != nil {
|
||||||
|
goal.Amount = *update.Amount
|
||||||
|
}
|
||||||
|
if update.StartTime != nil {
|
||||||
|
goal.StartTime = *update.StartTime
|
||||||
|
}
|
||||||
|
if update.EndTime != nil {
|
||||||
|
goal.EndTime = *update.EndTime
|
||||||
|
}
|
||||||
|
if update.Name != nil {
|
||||||
|
goal.Name = *update.Name
|
||||||
|
}
|
||||||
|
if update.Description != nil {
|
||||||
|
goal.Description = *update.Description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type GoalUpdate struct {
|
||||||
|
StartTime *time.Time `json:"startTime"`
|
||||||
|
EndTime *time.Time `json:"endTime"`
|
||||||
|
Amount *int `json:"amount"`
|
||||||
|
Name *string `json:"name"`
|
||||||
|
Description *string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GoalResult struct {
|
||||||
|
Goal
|
||||||
|
Group *Group `json:"group"`
|
||||||
|
Items []*GoalResultItem `json:"items"`
|
||||||
|
Logs []*LogResult `json:"logs"`
|
||||||
|
CompletedAmount int `json:"completedAmount"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GoalResultItem struct {
|
||||||
|
Item
|
||||||
|
CompletedAmount int `json:"completedAmount"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GoalFilter struct {
|
||||||
|
UserID string
|
||||||
|
GroupIDs []string
|
||||||
|
IncludesTime *time.Time
|
||||||
|
MinTime *time.Time
|
||||||
|
MaxTime *time.Time
|
||||||
|
IDs []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type GoalRepository interface {
|
||||||
|
Find(ctx context.Context, id string) (*Goal, error)
|
||||||
|
List(ctx context.Context, filter GoalFilter) ([]*Goal, error)
|
||||||
|
Insert(ctx context.Context, goal Goal) error
|
||||||
|
Update(ctx context.Context, goal Goal) error
|
||||||
|
Delete(ctx context.Context, goal Goal) error
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
type Group struct {
|
||||||
|
ID string `json:"id" db:"group_id"`
|
||||||
|
UserID string `json:"-" db:"user_id"`
|
||||||
|
Name string `json:"name" db:"name"`
|
||||||
|
Icon string `json:"icon" db:"icon"`
|
||||||
|
Description string `json:"description" db:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) Update(update GroupUpdate) {
|
||||||
|
if update.Name != nil {
|
||||||
|
g.Name = *update.Name
|
||||||
|
}
|
||||||
|
if update.Icon != nil {
|
||||||
|
g.Icon = *update.Icon
|
||||||
|
}
|
||||||
|
if update.Description != nil {
|
||||||
|
g.Description = *update.Description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type GroupUpdate struct {
|
||||||
|
Name *string `json:"name"`
|
||||||
|
Icon *string `jsoN:"icon"`
|
||||||
|
Description *string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GroupResult struct {
|
||||||
|
Group
|
||||||
|
Items []*Item `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GroupFilter struct {
|
||||||
|
UserID string
|
||||||
|
IDs []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type GroupRepository interface {
|
||||||
|
Find(ctx context.Context, id string) (*Group, error)
|
||||||
|
List(ctx context.Context, filter GroupFilter) ([]*Group, error)
|
||||||
|
Insert(ctx context.Context, group Group) error
|
||||||
|
Update(ctx context.Context, group Group) error
|
||||||
|
Delete(ctx context.Context, group Group) error
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
type Item struct {
|
||||||
|
ID string `json:"id" db:"item_id"`
|
||||||
|
UserID string `json:"-" db:"user_id"`
|
||||||
|
GroupID string `json:"groupId" db:"group_id"`
|
||||||
|
GroupWeight int `json:"groupWeight" db:"group_weight"`
|
||||||
|
Icon string `json:"icon" db:"icon"`
|
||||||
|
Name string `json:"name" db:"name"`
|
||||||
|
Description string `json:"description" db:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (item *Item) Update(update ItemUpdate) {
|
||||||
|
if update.GroupWeight != nil {
|
||||||
|
item.GroupWeight = *update.GroupWeight
|
||||||
|
}
|
||||||
|
if update.Name != nil {
|
||||||
|
item.Name = *update.Name
|
||||||
|
}
|
||||||
|
if update.Description != nil {
|
||||||
|
item.Description = *update.Description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ItemUpdate struct {
|
||||||
|
GroupWeight *int `json:"groupWeight"`
|
||||||
|
Name *string `json:"name"`
|
||||||
|
Description *string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ItemResult struct {
|
||||||
|
Item
|
||||||
|
Group *Group `json:"group"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ItemFilter struct {
|
||||||
|
UserID string
|
||||||
|
IDs []string
|
||||||
|
GroupIDs []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ItemRepository interface {
|
||||||
|
Find(ctx context.Context, id string) (*Item, error)
|
||||||
|
List(ctx context.Context, filter ItemFilter) ([]*Item, error)
|
||||||
|
Insert(ctx context.Context, item Item) error
|
||||||
|
Update(ctx context.Context, item Item) error
|
||||||
|
Delete(ctx context.Context, item Item) error
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Log struct {
|
||||||
|
ID string `json:"id" db:"log_id"`
|
||||||
|
UserID string `json:"-" db:"user_id"`
|
||||||
|
TaskID string `json:"taskId" db:"task_id"`
|
||||||
|
ItemID string `json:"itemId" db:"item_id"`
|
||||||
|
LoggedTime time.Time `json:"loggedTime" db:"logged_time"`
|
||||||
|
Description string `json:"description" db:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Log) Update(update LogUpdate) {
|
||||||
|
if update.LoggedTime != nil {
|
||||||
|
log.LoggedTime = update.LoggedTime.UTC()
|
||||||
|
}
|
||||||
|
if update.Description != nil {
|
||||||
|
log.Description = *update.Description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogUpdate struct {
|
||||||
|
LoggedTime *time.Time `json:"loggedTime"`
|
||||||
|
Description *string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogResult struct {
|
||||||
|
Log
|
||||||
|
Task *Task `json:"task"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogFilter struct {
|
||||||
|
UserID string
|
||||||
|
IDs []string
|
||||||
|
ItemIDs []string
|
||||||
|
MinTime *time.Time
|
||||||
|
MaxTime *time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogRepository interface {
|
||||||
|
Find(ctx context.Context, id string) (*Log, error)
|
||||||
|
List(ctx context.Context, filter LogFilter) ([]*Log, error)
|
||||||
|
Insert(ctx context.Context, log Log) error
|
||||||
|
Update(ctx context.Context, log Log) error
|
||||||
|
Delete(ctx context.Context, log Log) error
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Project struct {
|
||||||
|
ID string `json:"id" db:"project_id"`
|
||||||
|
UserID string `json:"-" db:"user_id"`
|
||||||
|
Name string `json:"name" db:"name"`
|
||||||
|
Description string `json:"description" db:"description"`
|
||||||
|
Icon string `json:"icon" db:"icon"`
|
||||||
|
Active bool `json:"active" db:"active"`
|
||||||
|
CreatedTime time.Time `json:"createdTime" db:"created_time"`
|
||||||
|
EndTime *time.Time `json:"endTime" db:"end_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (project *Project) Update(update ProjectUpdate) {
|
||||||
|
if update.Name != nil {
|
||||||
|
project.Name = *update.Name
|
||||||
|
}
|
||||||
|
if update.Description != nil {
|
||||||
|
project.Description = *update.Description
|
||||||
|
}
|
||||||
|
if update.Icon != nil {
|
||||||
|
project.Icon = *update.Icon
|
||||||
|
}
|
||||||
|
if update.Active != nil {
|
||||||
|
project.Active = *update.Active
|
||||||
|
}
|
||||||
|
if update.EndTime != nil {
|
||||||
|
endTimeCopy := update.EndTime.UTC()
|
||||||
|
project.EndTime = &endTimeCopy
|
||||||
|
}
|
||||||
|
if update.ClearEndTime {
|
||||||
|
project.EndTime = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProjectUpdate struct {
|
||||||
|
Name *string `json:"name"`
|
||||||
|
Description *string `json:"description"`
|
||||||
|
Icon *string `json:"icon"`
|
||||||
|
Active *bool `json:"active"`
|
||||||
|
EndTime *time.Time `json:"endTime"`
|
||||||
|
ClearEndTime bool `json:"clearEndTime"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProjectResult struct {
|
||||||
|
Project
|
||||||
|
Tasks []*TaskResult `json:"tasks"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProjectFilter struct {
|
||||||
|
UserID string
|
||||||
|
Active *bool
|
||||||
|
Expiring bool
|
||||||
|
IDs []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProjectRepository interface {
|
||||||
|
Find(ctx context.Context, id string) (*Project, error)
|
||||||
|
List(ctx context.Context, filter ProjectFilter) ([]*Project, error)
|
||||||
|
Insert(ctx context.Context, project Project) error
|
||||||
|
Update(ctx context.Context, project Project) error
|
||||||
|
Delete(ctx context.Context, project Project) error
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Task struct {
|
||||||
|
ID string `json:"id" db:"task_id"`
|
||||||
|
UserID string `json:"-" db:"user_id"`
|
||||||
|
ItemID string `json:"itemId" db:"item_id"`
|
||||||
|
ProjectID string `json:"projectId" db:"project_id"`
|
||||||
|
ItemAmount int `json:"itemAmount" db:"item_amount"`
|
||||||
|
Name string `json:"name" db:"name"`
|
||||||
|
Description string `json:"description" db:"description"`
|
||||||
|
Icon string `json:"icon" db:"icon"`
|
||||||
|
Active bool `json:"active" db:"active"`
|
||||||
|
CreatedTime time.Time `json:"createdTime" db:"created_time"`
|
||||||
|
EndTime *time.Time `json:"endTime" db:"end_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (task *Task) Update(update TaskUpdate) {
|
||||||
|
if update.ItemAmount != nil {
|
||||||
|
task.ItemAmount = *update.ItemAmount
|
||||||
|
}
|
||||||
|
if update.Name != nil {
|
||||||
|
task.Name = *update.Name
|
||||||
|
}
|
||||||
|
if update.Description != nil {
|
||||||
|
task.Description = *update.Description
|
||||||
|
}
|
||||||
|
if update.Active != nil {
|
||||||
|
task.Active = *update.Active
|
||||||
|
}
|
||||||
|
if update.EndTime != nil {
|
||||||
|
endTimeCopy := update.EndTime.UTC()
|
||||||
|
task.EndTime = &endTimeCopy
|
||||||
|
}
|
||||||
|
if update.ClearEndTime {
|
||||||
|
task.EndTime = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type TaskUpdate struct {
|
||||||
|
ItemAmount *int `json:"itemAmount"`
|
||||||
|
Name *string `json:"name"`
|
||||||
|
Description *string `json:"description"`
|
||||||
|
Active *bool `json:"active"`
|
||||||
|
EndTime *time.Time `json:"endTime"`
|
||||||
|
ClearEndTime bool `json:"clearEndTime"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TaskResult struct {
|
||||||
|
Task
|
||||||
|
Item *Item `json:"item"`
|
||||||
|
Logs []*Log `json:"logs"`
|
||||||
|
CompletedAmount int `json:"completedAmount"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TaskFilter struct {
|
||||||
|
UserID string
|
||||||
|
Active *bool
|
||||||
|
IDs []string
|
||||||
|
ItemIDs []string
|
||||||
|
ProjectIDs []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type TaskRepository interface {
|
||||||
|
Find(ctx context.Context, id string) (*Task, error)
|
||||||
|
List(ctx context.Context, filter TaskFilter) ([]*Task, error)
|
||||||
|
Insert(ctx context.Context, task Task) error
|
||||||
|
Update(ctx context.Context, task Task) error
|
||||||
|
Delete(ctx context.Context, task Task) error
|
||||||
|
}
|
||||||
+115
@@ -0,0 +1,115 @@
|
|||||||
|
service: stufflog2
|
||||||
|
|
||||||
|
frameworkVersion: '2'
|
||||||
|
|
||||||
|
provider:
|
||||||
|
name: aws
|
||||||
|
runtime: go1.x
|
||||||
|
memorySize: 512
|
||||||
|
stage: prod
|
||||||
|
region: ${env:AWS_DEFAULT_REGION}
|
||||||
|
role: ${env:AMI_ROLE}
|
||||||
|
tags:
|
||||||
|
Name: Stufflog2
|
||||||
|
Type: Application
|
||||||
|
apiGateway:
|
||||||
|
shouldStartNameWithService: true
|
||||||
|
minimumCompressionSize: 2048
|
||||||
|
versionFunctions: false
|
||||||
|
environment:
|
||||||
|
DB_DRIVER: ${env:DB_DRIVER}
|
||||||
|
DB_CONNECT: ${env:DB_CONNECT}
|
||||||
|
|
||||||
|
functions:
|
||||||
|
handleApiCalls:
|
||||||
|
handler: ./build/api/handler
|
||||||
|
package:
|
||||||
|
include:
|
||||||
|
- ./build/api/handler
|
||||||
|
events:
|
||||||
|
- http: ANY /api/{proxy+}
|
||||||
|
timeout: 30
|
||||||
|
|
||||||
|
package:
|
||||||
|
individually: true
|
||||||
|
exclude:
|
||||||
|
- ./**
|
||||||
|
|
||||||
|
plugins:
|
||||||
|
- serverless-domain-manager
|
||||||
|
- serverless-apigateway-service-proxy
|
||||||
|
|
||||||
|
custom:
|
||||||
|
webuiBucket: ${env:S3_WEBUI_BUCKET}
|
||||||
|
|
||||||
|
apiGatewayServiceProxies:
|
||||||
|
- s3:
|
||||||
|
path: /
|
||||||
|
method: get
|
||||||
|
action: GetObject
|
||||||
|
bucket: ${self:custom.webuiBucket}
|
||||||
|
roleArn: ${self:provider.role}
|
||||||
|
key: index.html
|
||||||
|
requestParameters:
|
||||||
|
'integration.request.header.cache-control': "'public, max-age=86400, immutable'"
|
||||||
|
|
||||||
|
- s3:
|
||||||
|
path: /goals
|
||||||
|
method: get
|
||||||
|
action: GetObject
|
||||||
|
bucket: ${self:custom.webuiBucket}
|
||||||
|
roleArn: ${self:provider.role}
|
||||||
|
key: index.html
|
||||||
|
requestParameters:
|
||||||
|
'integration.request.header.cache-control': "'public, max-age=86400, immutable'"
|
||||||
|
|
||||||
|
- s3:
|
||||||
|
path: /projects
|
||||||
|
method: get
|
||||||
|
action: GetObject
|
||||||
|
bucket: ${self:custom.webuiBucket}
|
||||||
|
roleArn: ${self:provider.role}
|
||||||
|
key: index.html
|
||||||
|
requestParameters:
|
||||||
|
'integration.request.header.cache-control': "'public, max-age=86400, immutable'"
|
||||||
|
|
||||||
|
- s3:
|
||||||
|
path: /items
|
||||||
|
method: get
|
||||||
|
action: GetObject
|
||||||
|
bucket: ${self:custom.webuiBucket}
|
||||||
|
roleArn: ${self:provider.role}
|
||||||
|
key: index.html
|
||||||
|
requestParameters:
|
||||||
|
'integration.request.header.cache-control': "'public, max-age=86400, immutable'"
|
||||||
|
|
||||||
|
- s3:
|
||||||
|
path: /logs
|
||||||
|
method: get
|
||||||
|
action: GetObject
|
||||||
|
bucket: ${self:custom.webuiBucket}
|
||||||
|
roleArn: ${self:provider.role}
|
||||||
|
key: index.html
|
||||||
|
requestParameters:
|
||||||
|
'integration.request.header.cache-control': "'public, max-age=86400, immutable'"
|
||||||
|
|
||||||
|
- s3:
|
||||||
|
path: /{myPath+}
|
||||||
|
method: get
|
||||||
|
action: GetObject
|
||||||
|
bucket: ${self:custom.webuiBucket}
|
||||||
|
roleArn: ${self:provider.role}
|
||||||
|
requestParameters:
|
||||||
|
'integration.request.path.myPath': 'method.request.path.myPath'
|
||||||
|
'integration.request.path.object': 'method.request.path.myPath'
|
||||||
|
'integration.request.header.cache-control': "'public, max-age=86400, immutable'"
|
||||||
|
|
||||||
|
customDomain:
|
||||||
|
domainName: ${env:DOMAIN_NAME}
|
||||||
|
basePath: ''
|
||||||
|
certificateName: ${env:CERTIFICATE_NAME}
|
||||||
|
certificateArn: ${env:CERTIFICATE_ARN}
|
||||||
|
createRoute53Record: true
|
||||||
|
endpointType: 'regional'
|
||||||
|
hostedZoneId: ${env:HOSTED_ZONE_ID}
|
||||||
|
autoDomain: true
|
||||||
@@ -0,0 +1,508 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/AchievementNetwork/stringset"
|
||||||
|
"github.com/gissleh/stufflog/database"
|
||||||
|
"github.com/gissleh/stufflog/internal/auth"
|
||||||
|
"github.com/gissleh/stufflog/internal/slerrors"
|
||||||
|
"github.com/gissleh/stufflog/models"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Loader loads the stuff.
|
||||||
|
type Loader struct {
|
||||||
|
DB database.Database
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Loader) FindGroup(ctx context.Context, id string) (*models.GroupResult, error) {
|
||||||
|
group, err := l.DB.Groups().Find(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if group.UserID != auth.UserID(ctx) {
|
||||||
|
return nil, slerrors.NotFound("Goal")
|
||||||
|
}
|
||||||
|
|
||||||
|
result := &models.GroupResult{Group: *group}
|
||||||
|
result.Items, err = l.DB.Items().List(ctx, models.ItemFilter{
|
||||||
|
UserID: auth.UserID(ctx),
|
||||||
|
GroupIDs: []string{group.ID},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Loader) ListGroups(ctx context.Context, filter models.GroupFilter) ([]*models.GroupResult, error) {
|
||||||
|
filter.UserID = auth.UserID(ctx)
|
||||||
|
groups, err := l.DB.Groups().List(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
groupIDs := make([]string, 0, len(groups))
|
||||||
|
for _, group := range groups {
|
||||||
|
groupIDs = append(groupIDs, group.ID)
|
||||||
|
}
|
||||||
|
items, err := l.DB.Items().List(ctx, models.ItemFilter{
|
||||||
|
UserID: auth.UserID(ctx),
|
||||||
|
GroupIDs: groupIDs,
|
||||||
|
})
|
||||||
|
|
||||||
|
results := make([]*models.GroupResult, len(groups))
|
||||||
|
for i, group := range groups {
|
||||||
|
results[i] = &models.GroupResult{Group: *group, Items: []*models.Item{}}
|
||||||
|
for _, item := range items {
|
||||||
|
if item.GroupID == group.ID {
|
||||||
|
results[i].Items = append(results[i].Items, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Loader) FindItem(ctx context.Context, id string) (*models.ItemResult, error) {
|
||||||
|
item, err := l.DB.Items().Find(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if item.UserID != auth.UserID(ctx) {
|
||||||
|
return nil, slerrors.NotFound("Item")
|
||||||
|
}
|
||||||
|
|
||||||
|
result := &models.ItemResult{Item: *item}
|
||||||
|
result.Group, err = l.DB.Groups().Find(ctx, item.GroupID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Loader) ListItems(ctx context.Context, filter models.ItemFilter) ([]*models.ItemResult, error) {
|
||||||
|
filter.UserID = auth.UserID(ctx)
|
||||||
|
items, err := l.DB.Items().List(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
groupIDs := make([]string, 0, len(items))
|
||||||
|
for _, item := range items {
|
||||||
|
groupIDs = append(groupIDs, item.GroupID)
|
||||||
|
}
|
||||||
|
groups, err := l.DB.Groups().List(ctx, models.GroupFilter{
|
||||||
|
UserID: auth.UserID(ctx),
|
||||||
|
IDs: groupIDs,
|
||||||
|
})
|
||||||
|
|
||||||
|
results := make([]*models.ItemResult, len(items))
|
||||||
|
for i, item := range items {
|
||||||
|
results[i] = &models.ItemResult{Item: *item}
|
||||||
|
for _, group := range groups {
|
||||||
|
if item.GroupID == group.ID {
|
||||||
|
results[i].Group = group
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Loader) FindLog(ctx context.Context, id string) (*models.LogResult, error) {
|
||||||
|
log, err := l.DB.Logs().Find(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if log.UserID != auth.UserID(ctx) {
|
||||||
|
return nil, slerrors.NotFound("Goal")
|
||||||
|
}
|
||||||
|
result := &models.LogResult{
|
||||||
|
Log: *log,
|
||||||
|
Task: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Task, _ = l.DB.Tasks().Find(ctx, id)
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Loader) ListLogs(ctx context.Context, filter models.LogFilter) ([]*models.LogResult, error) {
|
||||||
|
filter.UserID = auth.UserID(ctx)
|
||||||
|
logs, err := l.DB.Logs().List(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
taskIDs := stringset.New()
|
||||||
|
for _, log := range logs {
|
||||||
|
taskIDs.Add(log.TaskID)
|
||||||
|
}
|
||||||
|
tasks, err := l.DB.Tasks().List(ctx, models.TaskFilter{
|
||||||
|
UserID: auth.UserID(ctx),
|
||||||
|
IDs: taskIDs.Strings(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make([]*models.LogResult, len(logs))
|
||||||
|
for i, log := range logs {
|
||||||
|
results[i] = &models.LogResult{
|
||||||
|
Log: *log,
|
||||||
|
Task: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, task := range tasks {
|
||||||
|
if task.ID == log.TaskID {
|
||||||
|
results[i].Task = task
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Loader) FindProject(ctx context.Context, id string) (*models.ProjectResult, error) {
|
||||||
|
project, err := l.DB.Projects().Find(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if project.UserID != auth.UserID(ctx) {
|
||||||
|
return nil, slerrors.NotFound("Goal")
|
||||||
|
}
|
||||||
|
result := &models.ProjectResult{Project: *project}
|
||||||
|
|
||||||
|
tasks, err := l.DB.Tasks().List(ctx, models.TaskFilter{
|
||||||
|
UserID: auth.UserID(ctx),
|
||||||
|
ProjectIDs: []string{project.ID},
|
||||||
|
})
|
||||||
|
taskIDs := make([]string, 0, len(tasks))
|
||||||
|
itemIDs := stringset.New()
|
||||||
|
for _, task := range tasks {
|
||||||
|
taskIDs = append(taskIDs, task.ID)
|
||||||
|
itemIDs.Add(task.ItemID)
|
||||||
|
}
|
||||||
|
|
||||||
|
logs, err := l.DB.Logs().List(ctx, models.LogFilter{
|
||||||
|
UserID: auth.UserID(ctx),
|
||||||
|
IDs: taskIDs,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items, err := l.DB.Items().List(ctx, models.ItemFilter{
|
||||||
|
UserID: auth.UserID(ctx),
|
||||||
|
IDs: itemIDs.Strings(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Tasks = make([]*models.TaskResult, len(tasks))
|
||||||
|
for i, task := range tasks {
|
||||||
|
result.Tasks[i] = &models.TaskResult{
|
||||||
|
Logs: []*models.Log{},
|
||||||
|
}
|
||||||
|
result.Tasks[i].Task = *task
|
||||||
|
for _, log := range logs {
|
||||||
|
if log.TaskID == task.ID {
|
||||||
|
result.Tasks[i].Logs = append(result.Tasks[i].Logs, log)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, item := range items {
|
||||||
|
if item.ID == task.ItemID {
|
||||||
|
result.Tasks[i].Item = item
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.Tasks[i].CompletedAmount = len(result.Tasks[i].Logs)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Loader) ListProjects(ctx context.Context, filter models.ProjectFilter) ([]*models.ProjectResult, error) {
|
||||||
|
filter.UserID = auth.UserID(ctx)
|
||||||
|
projects, err := l.DB.Projects().List(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
projectIDs := make([]string, 0, len(projects))
|
||||||
|
for _, project := range projects {
|
||||||
|
projectIDs = append(projectIDs, project.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks, err := l.DB.Tasks().List(ctx, models.TaskFilter{
|
||||||
|
UserID: auth.UserID(ctx),
|
||||||
|
ProjectIDs: projectIDs,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
taskIDs := make([]string, 0, len(tasks))
|
||||||
|
itemIDs := stringset.New()
|
||||||
|
for _, task := range tasks {
|
||||||
|
taskIDs = append(taskIDs, task.ID)
|
||||||
|
itemIDs.Add(task.ItemID)
|
||||||
|
}
|
||||||
|
|
||||||
|
logs, err := l.DB.Logs().List(ctx, models.LogFilter{
|
||||||
|
UserID: auth.UserID(ctx),
|
||||||
|
IDs: taskIDs,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
items, err := l.DB.Items().List(ctx, models.ItemFilter{
|
||||||
|
UserID: auth.UserID(ctx),
|
||||||
|
IDs: itemIDs.Strings(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make([]*models.ProjectResult, len(projects))
|
||||||
|
for i, project := range projects {
|
||||||
|
results[i] = &models.ProjectResult{Project: *project}
|
||||||
|
results[i].Tasks = make([]*models.TaskResult, 0, 16)
|
||||||
|
for _, task := range tasks {
|
||||||
|
if task.ProjectID != project.ID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
taskResult := &models.TaskResult{
|
||||||
|
Task: *task,
|
||||||
|
Logs: []*models.Log{},
|
||||||
|
}
|
||||||
|
for _, log := range logs {
|
||||||
|
if log.TaskID == task.ID {
|
||||||
|
taskResult.Logs = append(taskResult.Logs, log)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, item := range items {
|
||||||
|
if item.ID == task.ItemID {
|
||||||
|
taskResult.Item = item
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
taskResult.CompletedAmount = len(taskResult.Logs)
|
||||||
|
|
||||||
|
results[i].Tasks = append(results[i].Tasks, taskResult)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Loader) FindTask(ctx context.Context, id string) (*models.TaskResult, error) {
|
||||||
|
task, err := l.DB.Tasks().Find(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if task.UserID != auth.UserID(ctx) {
|
||||||
|
return nil, slerrors.NotFound("Goal")
|
||||||
|
}
|
||||||
|
result := &models.TaskResult{Task: *task}
|
||||||
|
|
||||||
|
result.Item, _ = l.DB.Items().Find(ctx, task.ItemID)
|
||||||
|
result.Logs, err = l.DB.Logs().List(ctx, models.LogFilter{
|
||||||
|
UserID: task.UserID,
|
||||||
|
IDs: []string{task.ID},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result.CompletedAmount = len(result.Logs)
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Loader) ListTasks(ctx context.Context, filter models.TaskFilter) ([]*models.TaskResult, error) {
|
||||||
|
filter.UserID = auth.UserID(ctx)
|
||||||
|
tasks, err := l.DB.Tasks().List(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(tasks) == 0 {
|
||||||
|
return []*models.TaskResult{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
taskIDs := make([]string, 0, len(tasks))
|
||||||
|
itemIDs := stringset.New()
|
||||||
|
for _, task := range tasks {
|
||||||
|
taskIDs = append(taskIDs, task.ID)
|
||||||
|
itemIDs.Add(task.ItemID)
|
||||||
|
}
|
||||||
|
|
||||||
|
logs, err := l.DB.Logs().List(ctx, models.LogFilter{
|
||||||
|
UserID: auth.UserID(ctx),
|
||||||
|
IDs: taskIDs,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
items, err := l.DB.Items().List(ctx, models.ItemFilter{
|
||||||
|
UserID: auth.UserID(ctx),
|
||||||
|
IDs: itemIDs.Strings(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make([]*models.TaskResult, 0, len(tasks))
|
||||||
|
for _, task := range tasks {
|
||||||
|
result := &models.TaskResult{
|
||||||
|
Task: *task,
|
||||||
|
Logs: []*models.Log{},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, log := range logs {
|
||||||
|
if log.TaskID != task.ID {
|
||||||
|
result.Logs = append(result.Logs, log)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range items {
|
||||||
|
if item.ID == task.ItemID {
|
||||||
|
result.Item = item
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.CompletedAmount = len(result.Logs)
|
||||||
|
|
||||||
|
results = append(results, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Loader) FindGoal(ctx context.Context, id string) (*models.GoalResult, error) {
|
||||||
|
goal, err := l.DB.Goals().Find(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if goal.UserID != auth.UserID(ctx) {
|
||||||
|
return nil, slerrors.NotFound("Goal")
|
||||||
|
}
|
||||||
|
|
||||||
|
return l.populateGoals(ctx, goal)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Loader) ListGoals(ctx context.Context, filter models.GoalFilter) ([]*models.GoalResult, error) {
|
||||||
|
filter.UserID = auth.UserID(ctx)
|
||||||
|
|
||||||
|
goals, err := l.DB.Goals().List(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make([]*models.GoalResult, len(goals))
|
||||||
|
eg := errgroup.Group{}
|
||||||
|
for i := range results {
|
||||||
|
index := i // Required to avoid race condition.
|
||||||
|
|
||||||
|
eg.Go(func() error {
|
||||||
|
res, err := l.populateGoals(ctx, goals[index])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
results[index] = res
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
err = eg.Wait()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Loader) populateGoals(ctx context.Context, goal *models.Goal) (*models.GoalResult, error) {
|
||||||
|
userID := auth.UserID(ctx)
|
||||||
|
result := &models.GoalResult{
|
||||||
|
Goal: *goal,
|
||||||
|
Group: nil,
|
||||||
|
Items: nil,
|
||||||
|
Logs: nil,
|
||||||
|
CompletedAmount: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Group, _ = l.DB.Groups().Find(ctx, goal.GroupID)
|
||||||
|
if result.Group != nil {
|
||||||
|
// Get items
|
||||||
|
items, err := l.DB.Items().List(ctx, models.ItemFilter{
|
||||||
|
UserID: userID,
|
||||||
|
GroupIDs: []string{goal.GroupID},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
itemIDs := make([]string, 0, len(items))
|
||||||
|
for _, item := range items {
|
||||||
|
result.Items = append(result.Items, &models.GoalResultItem{
|
||||||
|
Item: *item,
|
||||||
|
CompletedAmount: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
itemIDs = append(itemIDs, item.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get logs
|
||||||
|
logs, err := l.DB.Logs().List(ctx, models.LogFilter{
|
||||||
|
UserID: userID,
|
||||||
|
ItemIDs: itemIDs,
|
||||||
|
MinTime: &goal.StartTime,
|
||||||
|
MaxTime: &goal.EndTime,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get tasks
|
||||||
|
taskIDs := make([]string, 0, len(result.Logs))
|
||||||
|
for _, log := range logs {
|
||||||
|
taskIDs = append(taskIDs, log.TaskID)
|
||||||
|
}
|
||||||
|
tasks, err := l.DB.Tasks().List(ctx, models.TaskFilter{
|
||||||
|
UserID: userID,
|
||||||
|
IDs: taskIDs,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Apply logs
|
||||||
|
result.Logs = make([]*models.LogResult, 0, len(logs))
|
||||||
|
for _, log := range logs {
|
||||||
|
resultLog := &models.LogResult{
|
||||||
|
Log: *log,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, task := range tasks {
|
||||||
|
if task.ID == log.TaskID {
|
||||||
|
resultLog.Task = task
|
||||||
|
|
||||||
|
for _, item := range result.Items {
|
||||||
|
if task.ItemID == item.ID {
|
||||||
|
item.CompletedAmount += 1
|
||||||
|
result.CompletedAmount += item.GroupWeight
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.Logs = append(result.Logs, resultLog)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
/node_modules/
|
||||||
|
/public/build/
|
||||||
|
/build.env
|
||||||
|
/.idea
|
||||||
|
/.vscode
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
Generated
+4368
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"name": "@gisle/stufflog2-svelte-ui",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "rollup -c",
|
||||||
|
"dev": "rollup -c -w",
|
||||||
|
"start": "sirv public",
|
||||||
|
"validate": "svelte-check"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@fortawesome/free-solid-svg-icons": "^5.15.1",
|
||||||
|
"@rollup/plugin-alias": "^3.1.1",
|
||||||
|
"@rollup/plugin-commonjs": "^16.0.0",
|
||||||
|
"@rollup/plugin-json": "^4.1.0",
|
||||||
|
"@rollup/plugin-node-resolve": "^10.0.0",
|
||||||
|
"@rollup/plugin-replace": "^2.3.4",
|
||||||
|
"@rollup/plugin-typescript": "^6.0.0",
|
||||||
|
"@tsconfig/svelte": "^1.0.0",
|
||||||
|
"@types/node": "^14.14.17",
|
||||||
|
"amazon-cognito-identity-js": "^4.5.6",
|
||||||
|
"aws-amplify": "^3.3.13",
|
||||||
|
"fa-svelte": "^3.1.0",
|
||||||
|
"lodash-es": "^4.17.20",
|
||||||
|
"rollup": "^2.3.4",
|
||||||
|
"rollup-plugin-css-only": "^3.1.0",
|
||||||
|
"rollup-plugin-dev": "^1.1.3",
|
||||||
|
"rollup-plugin-livereload": "^2.0.0",
|
||||||
|
"rollup-plugin-svelte": "^7.0.0",
|
||||||
|
"rollup-plugin-terser": "^7.0.0",
|
||||||
|
"svelte": "^3.0.0",
|
||||||
|
"svelte-calendar": "^2.0.4",
|
||||||
|
"svelte-check": "^1.0.0",
|
||||||
|
"svelte-preprocess": "^4.0.0",
|
||||||
|
"svelte-routing": "^1.4.2",
|
||||||
|
"svelte-time-picker": "^1.0.6",
|
||||||
|
"tslib": "^2.0.0",
|
||||||
|
"typescript": "^3.9.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"sirv-cli": "^1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
@@ -0,0 +1,62 @@
|
|||||||
|
html, body {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #111;
|
||||||
|
color: #CCC;
|
||||||
|
margin: 0;
|
||||||
|
padding: 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #FC1;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
input, button, select, textarea {
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
-webkit-padding: 0.4em 0;
|
||||||
|
padding: 0.4em;
|
||||||
|
margin: 0 0 0.5em 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:disabled {
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
color: #333;
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:not(:disabled):active {
|
||||||
|
background-color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:focus {
|
||||||
|
border-color: #666;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset='utf-8'>
|
||||||
|
<meta name='viewport' content='width=device-width,initial-scale=1'>
|
||||||
|
|
||||||
|
<title>Svelte app</title>
|
||||||
|
|
||||||
|
<link rel='icon' type='image/png' href='/favicon.png'>
|
||||||
|
<link rel='stylesheet' href='/global.css'>
|
||||||
|
<link rel='stylesheet' href='/build/bundle.css'>
|
||||||
|
|
||||||
|
<script defer src='/build/bundle.js'></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body></body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
|
||||||
|
import svelte from "rollup-plugin-svelte";
|
||||||
|
import commonjs from "@rollup/plugin-commonjs";
|
||||||
|
import resolve from "@rollup/plugin-node-resolve";
|
||||||
|
import livereload from "rollup-plugin-livereload";
|
||||||
|
import { terser } from "rollup-plugin-terser";
|
||||||
|
import sveltePreprocess from "svelte-preprocess";
|
||||||
|
import typescript from "@rollup/plugin-typescript";
|
||||||
|
import css from "rollup-plugin-css-only";
|
||||||
|
import dev from "rollup-plugin-dev";
|
||||||
|
import replace from "@rollup/plugin-replace";
|
||||||
|
import json from "@rollup/plugin-json";
|
||||||
|
|
||||||
|
const production = !process.env.ROLLUP_WATCH;
|
||||||
|
const envVariables = fs.readFileSync("build.env", "utf-8")
|
||||||
|
.split("\n")
|
||||||
|
.filter(l => l.length > 0)
|
||||||
|
.map(l => l.trim().split("="))
|
||||||
|
.reduce((p, [key, value]) => ({...p, [key]: value}), {});
|
||||||
|
|
||||||
|
export default {
|
||||||
|
input: "src/main.ts",
|
||||||
|
output: {
|
||||||
|
sourcemap: true,
|
||||||
|
format: "iife",
|
||||||
|
name: "app",
|
||||||
|
file: "public/build/bundle.js"
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
svelte({
|
||||||
|
preprocess: sveltePreprocess(),
|
||||||
|
compilerOptions: {
|
||||||
|
// enable run-time checks when not in production
|
||||||
|
dev: !production
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
// we"ll extract any component CSS out into
|
||||||
|
// a separate file - better for performance
|
||||||
|
css({ output: "bundle.css" }),
|
||||||
|
|
||||||
|
// If you have external dependencies installed from
|
||||||
|
// npm, you'll most likely need these plugins. In
|
||||||
|
// some cases you'll need additional configuration -
|
||||||
|
// consult the documentation for details:
|
||||||
|
// https://github.com/rollup/plugins/tree/master/packages/commonjs
|
||||||
|
resolve({
|
||||||
|
browser: true,
|
||||||
|
preferBuiltins: false,
|
||||||
|
dedupe: ["svelte"],
|
||||||
|
}),
|
||||||
|
json(),
|
||||||
|
commonjs({
|
||||||
|
include: 'node_modules/**',
|
||||||
|
}),
|
||||||
|
typescript({
|
||||||
|
sourceMap: !production,
|
||||||
|
inlineSources: !production
|
||||||
|
}),
|
||||||
|
|
||||||
|
replace({
|
||||||
|
// 2 level deep object should be stringify
|
||||||
|
"process.env": JSON.stringify({
|
||||||
|
NODE_ENV: production ? "production" : "development",
|
||||||
|
...envVariables,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Watch the `public` directory and refresh the
|
||||||
|
// browser on changes when not in production
|
||||||
|
!production && livereload("public"),
|
||||||
|
|
||||||
|
// Add dev server in development.
|
||||||
|
!production && dev({
|
||||||
|
dirs: ["public"],
|
||||||
|
spa: "public/index.html",
|
||||||
|
port: 5000,
|
||||||
|
proxy: {
|
||||||
|
"/api/*": "localhost:8000",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
// If we're building for production (npm run build
|
||||||
|
// instead of npm run dev), minify
|
||||||
|
production && terser({
|
||||||
|
output: { comments: false },
|
||||||
|
})
|
||||||
|
],
|
||||||
|
watch: {
|
||||||
|
clearScreen: false
|
||||||
|
},
|
||||||
|
onwarn: function(warning) {
|
||||||
|
// Skip certain warnings
|
||||||
|
if ( warning.code === "THIS_IS_UNDEFINED" ) { return; }
|
||||||
|
|
||||||
|
// console.warn everything else
|
||||||
|
console.warn( warning.message );
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Router, Link, Route } from "svelte-routing";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import Menu from "./components/Menu.svelte";
|
||||||
|
import FrontPage from "./pages/FrontPage.svelte";
|
||||||
|
import ProjectPage from "./pages/ProjectPage.svelte";
|
||||||
|
import ModalRoute from "./components/ModalRoute.svelte";
|
||||||
|
import LogAddForm from "./forms/LogAddForm.svelte";
|
||||||
|
import LogsPage from "./pages/LogsPage.svelte";
|
||||||
|
import LogEditForm from "./forms/LogEditForm.svelte";
|
||||||
|
import LogDeleteForm from "./forms/LogDeleteForm.svelte";
|
||||||
|
import TaskAddForm from "./forms/TaskAddForm.svelte";
|
||||||
|
import TaskEditForm from "./forms/TaskEditForm.svelte";
|
||||||
|
import TaskDeleteForm from "./forms/TaskDeleteForm.svelte";
|
||||||
|
import ProjectAddForm from "./forms/ProjectAddForm.svelte";
|
||||||
|
import ProjectEditForm from "./forms/ProjectEditForm.svelte";
|
||||||
|
import ProjectDeleteForm from "./forms/ProjectDeleteForm.svelte";
|
||||||
|
import GroupPage from "./pages/GroupPage.svelte";
|
||||||
|
import ItemAddForm from "./forms/ItemAddForm.svelte";
|
||||||
|
import ItemEditForm from "./forms/ItemEditForm.svelte";
|
||||||
|
import ItemDeleteForm from "./forms/ItemDeleteForm.svelte";
|
||||||
|
import GroupForm from "./forms/GroupForm.svelte";
|
||||||
|
import GoalPage from "./pages/GoalPage.svelte";
|
||||||
|
import GoalForm from "./forms/GoalForm.svelte";
|
||||||
|
import LoginForm from "./forms/LoginForm.svelte";
|
||||||
|
import authStore from "./stores/auth";
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
authStore.check()
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $authStore.checked}
|
||||||
|
{#if $authStore.loggedIn}
|
||||||
|
<Router>
|
||||||
|
<Menu />
|
||||||
|
<main>
|
||||||
|
<Route path="/" component={FrontPage} />
|
||||||
|
<Route path="/goals/" component={GoalPage} />
|
||||||
|
<Route path="/projects/" component={ProjectPage} />
|
||||||
|
<Route path="/logs/" component={LogsPage} />
|
||||||
|
<Route path="/items/" component={GroupPage} />
|
||||||
|
</main>
|
||||||
|
</Router>
|
||||||
|
<ModalRoute name="log.add"> <LogAddForm/> </ModalRoute>
|
||||||
|
<ModalRoute name="log.edit"> <LogEditForm/> </ModalRoute>
|
||||||
|
<ModalRoute name="log.delete"> <LogDeleteForm/> </ModalRoute>
|
||||||
|
<ModalRoute name="task.add"> <TaskAddForm/> </ModalRoute>
|
||||||
|
<ModalRoute name="task.edit"> <TaskEditForm/> </ModalRoute>
|
||||||
|
<ModalRoute name="task.delete"> <TaskDeleteForm/> </ModalRoute>
|
||||||
|
<ModalRoute name="project.add"> <ProjectAddForm/> </ModalRoute>
|
||||||
|
<ModalRoute name="project.edit"> <ProjectEditForm/> </ModalRoute>
|
||||||
|
<ModalRoute name="project.delete"> <ProjectDeleteForm/> </ModalRoute>
|
||||||
|
<ModalRoute name="item.add"> <ItemAddForm/> </ModalRoute>
|
||||||
|
<ModalRoute name="item.edit"> <ItemEditForm/> </ModalRoute>
|
||||||
|
<ModalRoute name="item.delete"> <ItemDeleteForm/> </ModalRoute>
|
||||||
|
<ModalRoute name="group.add"> <GroupForm creation/> </ModalRoute>
|
||||||
|
<ModalRoute name="group.edit"> <GroupForm/> </ModalRoute>
|
||||||
|
<ModalRoute name="group.delete"> <GroupForm deletion/> </ModalRoute>
|
||||||
|
<ModalRoute name="goal.add"> <GoalForm creation/> </ModalRoute>
|
||||||
|
<ModalRoute name="goal.edit"> <GoalForm/> </ModalRoute>
|
||||||
|
<ModalRoute name="goal.delete"> <GoalForm deletion/> </ModalRoute>
|
||||||
|
{:else}
|
||||||
|
<LoginForm />
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
main {
|
||||||
|
text-align: left;
|
||||||
|
max-width: 99.5%;
|
||||||
|
width: 920px;
|
||||||
|
margin: 1em auto;
|
||||||
|
padding-bottom: 4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import Amplify from "@aws-amplify/core";
|
||||||
|
import Auth from "@aws-amplify/auth";
|
||||||
|
import type {CognitoAccessToken, CognitoUser} from "amazon-cognito-identity-js";
|
||||||
|
|
||||||
|
Amplify.configure({
|
||||||
|
Auth: {
|
||||||
|
region: process.env.AWS_AMPLIFY_REGION,
|
||||||
|
userPoolId: process.env.AWS_AMPLIFY_USER_POOL_ID,
|
||||||
|
userPoolWebClientId: process.env.AWS_AMPLIFY_USER_POOL_WEB_CLIENT_ID,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function signIn(username: string, password: string): Promise<CognitoUser | null> {
|
||||||
|
const u = await Auth.signIn(username, password);
|
||||||
|
return u || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function signOut(): Promise<void> {
|
||||||
|
await Auth.signOut();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAccessToken(): Promise<CognitoAccessToken | null> {
|
||||||
|
try {
|
||||||
|
const u = await Auth.currentSession();
|
||||||
|
if (!u || !u.isValid()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.getAccessToken();
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getJwt(): Promise<string> {
|
||||||
|
const token = await getAccessToken();
|
||||||
|
if (!token) {
|
||||||
|
throw new Error("unauthorized");
|
||||||
|
}
|
||||||
|
|
||||||
|
return token.getJwtToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkSession(): Promise<boolean> {
|
||||||
|
return !!(await getAccessToken());
|
||||||
|
}
|
||||||
@@ -0,0 +1,260 @@
|
|||||||
|
import { getJwt } from "./amplify";
|
||||||
|
import type { GoalFilter, GoalInput, GoalResult, GoalUpdate } from "../models/goal";
|
||||||
|
import type { ProjectFilter, ProjectInput, ProjectResult, ProjectUpdate } from "../models/project";
|
||||||
|
import type { TaskInput, TaskResult, TaskUpdate } from "../models/task";
|
||||||
|
import type { LogFilter, LogInput, LogResult, LogUpdate } from "../models/log";
|
||||||
|
import type { GroupInput, GroupResult, GroupUpdate } from "../models/group";
|
||||||
|
import type { ItemInput, ItemResult, ItemUpdate } from "../models/item";
|
||||||
|
|
||||||
|
export class StufflogClient {
|
||||||
|
private root: string;
|
||||||
|
|
||||||
|
constructor(root: string) {
|
||||||
|
this.root = root;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async findGoal(id: string): Promise<GoalResult> {
|
||||||
|
const data = await this.fetch("GET", `/api/goal/${id}`);
|
||||||
|
return data.goal;
|
||||||
|
}
|
||||||
|
|
||||||
|
async listGoals({minTime, maxTime, includesTime}: GoalFilter): Promise<GoalResult[]> {
|
||||||
|
let queries = [];
|
||||||
|
if (minTime != null) {
|
||||||
|
queries.push(`minTime=${minTime.toISOString()}`);
|
||||||
|
}
|
||||||
|
if (maxTime != null) {
|
||||||
|
queries.push(`maxTime=${maxTime.toISOString()}`);
|
||||||
|
}
|
||||||
|
if (includesTime != null) {
|
||||||
|
queries.push(`includesTime=${includesTime.toISOString()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = queries.length > 0 ? `?${queries.join("&")}` : "";
|
||||||
|
|
||||||
|
const data = await this.fetch("GET", `/api/goal/${query}`);
|
||||||
|
return data.goals;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createGoal(input: GoalInput): Promise<GoalResult> {
|
||||||
|
const data = await this.fetch("POST", "/api/goal/", input);
|
||||||
|
return data.goal;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateGoal(id: string, update: GoalUpdate): Promise<GoalResult> {
|
||||||
|
const data = await this.fetch("PUT", `/api/goal/${id}`, update);
|
||||||
|
return data.goal;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteGoal(id: string): Promise<GoalResult> {
|
||||||
|
const data = await this.fetch("DELETE", `/api/goal/${id}`);
|
||||||
|
return data.goal;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async findProject(id: string): Promise<ProjectResult> {
|
||||||
|
const data = await this.fetch("GET", `/api/project/${id}`);
|
||||||
|
return data.project;
|
||||||
|
}
|
||||||
|
|
||||||
|
async listProjects({active, expiring}: ProjectFilter): Promise<ProjectResult[]> {
|
||||||
|
let queries = [];
|
||||||
|
if (active != null) {
|
||||||
|
queries.push(`active=${active}`);
|
||||||
|
}
|
||||||
|
if (expiring != null) {
|
||||||
|
queries.push(`expiring=${expiring}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = queries.length > 0 ? `?${queries.join("&")}` : "";
|
||||||
|
|
||||||
|
const data = await this.fetch("GET", `/api/project/${query}`);
|
||||||
|
return data.projects;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createProject(input: ProjectInput): Promise<ProjectResult> {
|
||||||
|
const data = await this.fetch("POST", "/api/project/", input);
|
||||||
|
return data.project;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateProject(id: string, update: ProjectUpdate): Promise<ProjectResult> {
|
||||||
|
const data = await this.fetch("PUT", `/api/project/${id}`, update);
|
||||||
|
return data.project;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteProject(id: string): Promise<ProjectResult> {
|
||||||
|
const data = await this.fetch("DELETE", `/api/project/${id}`);
|
||||||
|
return data.project;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async findLog(id: string): Promise<LogResult> {
|
||||||
|
const data = await this.fetch("GET", `/api/log/${id}`);
|
||||||
|
return data.log;
|
||||||
|
}
|
||||||
|
|
||||||
|
async listLogs({minTime, maxTime}: LogFilter): Promise<LogResult[]> {
|
||||||
|
let queries = [];
|
||||||
|
if (minTime != null) {
|
||||||
|
queries.push(`minTime=${minTime.toISOString()}`);
|
||||||
|
}
|
||||||
|
if (maxTime != null) {
|
||||||
|
queries.push(`maxTime=${maxTime.toISOString()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = queries.length > 0 ? `?${queries.join("&")}` : "";
|
||||||
|
|
||||||
|
const data = await this.fetch("GET", `/api/log/${query}`);
|
||||||
|
return data.logs;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createLog(input: LogInput): Promise<LogResult> {
|
||||||
|
const data = await this.fetch("POST", "/api/log/", input);
|
||||||
|
return data.log;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateLog(id: string, update: LogUpdate): Promise<LogResult> {
|
||||||
|
const data = await this.fetch("PUT", `/api/log/${id}`, update);
|
||||||
|
return data.log;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteLog(id: string): Promise<LogResult> {
|
||||||
|
const data = await this.fetch("DELETE", `/api/log/${id}`);
|
||||||
|
return data.log;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async findTask(id: string): Promise<TaskResult> {
|
||||||
|
const data = await this.fetch("GET", `/api/task/${id}`);
|
||||||
|
return data.task;
|
||||||
|
}
|
||||||
|
|
||||||
|
async listTasks(active?: boolean): Promise<TaskResult[]> {
|
||||||
|
let query = (active != null) ? `?active=${active}` : "";
|
||||||
|
|
||||||
|
const data = await this.fetch("GET", `/api/task/${query}`);
|
||||||
|
return data.tasks;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createTask(input: TaskInput): Promise<TaskResult> {
|
||||||
|
const data = await this.fetch("POST", "/api/task/", input);
|
||||||
|
return data.task;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateTask(id: string, update: TaskUpdate): Promise<TaskResult> {
|
||||||
|
const data = await this.fetch("PUT", `/api/task/${id}`, update);
|
||||||
|
return data.task;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteTask(id: string): Promise<TaskResult> {
|
||||||
|
const data = await this.fetch("DELETE", `/api/task/${id}`);
|
||||||
|
return data.task;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async findGroup(id: string): Promise<GroupResult> {
|
||||||
|
const data = await this.fetch("GET", `/api/group/${id}`);
|
||||||
|
return data.group;
|
||||||
|
}
|
||||||
|
|
||||||
|
async listGroups(): Promise<GroupResult[]> {
|
||||||
|
const data = await this.fetch("GET", "/api/group/");
|
||||||
|
return data.groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createGroup(input: GroupInput): Promise<GroupResult> {
|
||||||
|
const data = await this.fetch("POST", "/api/group/", input);
|
||||||
|
return data.group;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateGroup(id: string, update: GroupUpdate): Promise<GroupResult> {
|
||||||
|
const data = await this.fetch("PUT", `/api/group/${id}`, update);
|
||||||
|
return data.group;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteGroup(id: string): Promise<GroupResult> {
|
||||||
|
const data = await this.fetch("DELETE", `/api/group/${id}`);
|
||||||
|
return data.group;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async findItem(id: string): Promise<ItemResult> {
|
||||||
|
const data = await this.fetch("GET", `/api/item/${id}`);
|
||||||
|
return data.item;
|
||||||
|
}
|
||||||
|
|
||||||
|
async listItems(): Promise<ItemResult[]> {
|
||||||
|
const data = await this.fetch("GET", "/api/item/");
|
||||||
|
return data.projects;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createItem(input: ItemInput): Promise<ItemResult> {
|
||||||
|
const data = await this.fetch("POST", "/api/item/", input);
|
||||||
|
return data.item;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateItem(id: string, update: ItemUpdate): Promise<ItemResult> {
|
||||||
|
const data = await this.fetch("PUT", `/api/item/${id}`, update);
|
||||||
|
return data.item;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteItem(id: string): Promise<ItemResult> {
|
||||||
|
const data = await this.fetch("DELETE", `/api/item/${id}`);
|
||||||
|
return data.item;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async fetch(method: string, path: string, body?: object) {
|
||||||
|
const fullPath = this.root + path;
|
||||||
|
const req: RequestInit = {method, headers: {}}
|
||||||
|
|
||||||
|
if (body != null) {
|
||||||
|
const data = new Blob([JSON.stringify(body)])
|
||||||
|
|
||||||
|
req.headers["Content-Type"] = req;
|
||||||
|
req.headers["Content-Length"] = data.size;
|
||||||
|
req.body = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn("AUTH SKIPPED, remember to change back in prod!")
|
||||||
|
req.headers["Authorization"] = `Bearer ${await getJwt()}`;
|
||||||
|
|
||||||
|
const res = await fetch(fullPath, req);
|
||||||
|
if (!res.ok) {
|
||||||
|
if ((res.headers.get("Content-Type") || "").includes("application/json")) {
|
||||||
|
const data = await res.json();
|
||||||
|
throw new StuffLogError(data.errorCode, data.errorMessage)
|
||||||
|
} else {
|
||||||
|
const text = await res.text();
|
||||||
|
throw new StuffLogError(res.status, text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class StuffLogError {
|
||||||
|
public code: number
|
||||||
|
public message: string
|
||||||
|
|
||||||
|
constructor(code: number, message: string) {
|
||||||
|
this.code = code;
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
return `Error ${this.code}: ${this.message}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stuffLogClient = new StufflogClient("");
|
||||||
|
export default stuffLogClient
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
import type { ModalData } from "../stores/modal";
|
||||||
|
import modalStore from "../stores/modal";
|
||||||
|
|
||||||
|
export let open: ModalData = {name: "none"};
|
||||||
|
export let disabled: boolean = false;
|
||||||
|
export let compact: boolean = false;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
dispatch("click", {open});
|
||||||
|
|
||||||
|
if (open.name !== "none") {
|
||||||
|
modalStore.set(open);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="boi" class:disabled class:compact on:click={handleClick}><slot></slot></div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div.boi {
|
||||||
|
border: 6px dashed;
|
||||||
|
padding: 0.5em;
|
||||||
|
margin: 1em 0.5ch;
|
||||||
|
text-align: center;
|
||||||
|
color: #777;
|
||||||
|
border-color: #333;
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom-right-radius: 0.25em;
|
||||||
|
font-size: 2em;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
}
|
||||||
|
div.boi:hover {
|
||||||
|
color: #AAA;
|
||||||
|
border-color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.boi.disabled {
|
||||||
|
color: #333;
|
||||||
|
border-color: #222;
|
||||||
|
cursor: wait;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.boi.compact {
|
||||||
|
margin: 0;
|
||||||
|
border-width: 4px;
|
||||||
|
padding: 0.25em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let time: Date | string = new Date();
|
||||||
|
|
||||||
|
let timeStr = "";
|
||||||
|
|
||||||
|
function formatTime(time: Date): string {
|
||||||
|
const pad = (n:number) => n < 10 ? '0'+n : n.toString();
|
||||||
|
|
||||||
|
return `${time.getFullYear()}-${pad(time.getMonth()+1)}-${pad(time.getDate())} ${pad(time.getHours())}:${pad(time.getMinutes())}`
|
||||||
|
}
|
||||||
|
|
||||||
|
$: timeStr = formatTime(new Date(time));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span>{timeStr}</span>
|
||||||
|
|
||||||
|
<style></style>
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let startTime: Date | string = new Date();
|
||||||
|
export let endTime: Date | string = new Date();
|
||||||
|
|
||||||
|
let started = false;
|
||||||
|
let overdue = false;
|
||||||
|
let danger = false;
|
||||||
|
let amount = 0;
|
||||||
|
let amountStr = "0";
|
||||||
|
let unit = "days";
|
||||||
|
let titleTimeStr = "";
|
||||||
|
|
||||||
|
function formatTime(time: Date): string {
|
||||||
|
const pad = (n:number) => n < 9 ? '0'+n : n.toString();
|
||||||
|
|
||||||
|
return `${time.getFullYear()}-${pad(time.getMonth()+1)}-${pad(time.getDate())}`
|
||||||
|
}
|
||||||
|
|
||||||
|
$: {
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
overdue = false;
|
||||||
|
unit = "days";
|
||||||
|
|
||||||
|
const st = (startTime instanceof Date) ? startTime : new Date(startTime);
|
||||||
|
const et = (endTime instanceof Date) ? endTime : new Date(endTime);
|
||||||
|
|
||||||
|
if (now < st) {
|
||||||
|
started = false;
|
||||||
|
amount = (st.getTime() - now.getTime()) / 86400000
|
||||||
|
} else {
|
||||||
|
started = true;
|
||||||
|
amount = (et.getTime() - now.getTime()) / 86400000
|
||||||
|
}
|
||||||
|
|
||||||
|
if (amount < 0) {
|
||||||
|
overdue = true;
|
||||||
|
amount = -amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
danger = (!overdue && started && amount <= 3);
|
||||||
|
|
||||||
|
if (amount < 2) {
|
||||||
|
amount *= 24;
|
||||||
|
unit = "hours"
|
||||||
|
}
|
||||||
|
if (amount < 2) {
|
||||||
|
amount *= 60;
|
||||||
|
unit = "minutes";
|
||||||
|
}
|
||||||
|
|
||||||
|
amount = Math.floor(amount);
|
||||||
|
if (amount <= 1) {
|
||||||
|
unit = unit.slice(0, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (amount < 1) {
|
||||||
|
amountStr = "< 1"
|
||||||
|
} else {
|
||||||
|
amountStr = amount.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
titleTimeStr = `${formatTime(new Date(startTime))} – ${formatTime(new Date(endTime))}`
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span title={titleTimeStr}>
|
||||||
|
{#if (overdue)}
|
||||||
|
<span class="overdue">{amountStr} {unit} ago</span>
|
||||||
|
{:else if (started)}
|
||||||
|
<span class:danger class="started">{amountStr} {unit} left</span>
|
||||||
|
{:else}
|
||||||
|
<span class="pending">In {amountStr} {unit}</span>
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
span.pending {
|
||||||
|
color: #2797e2;
|
||||||
|
}
|
||||||
|
span.danger {
|
||||||
|
color: #e28127;
|
||||||
|
}
|
||||||
|
span.overdue {
|
||||||
|
color: #666666;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { IconName } from "../external/icons";
|
||||||
|
import type { GoalResult } from "../models/goal";
|
||||||
|
import type { ModalData } from "../stores/modal";
|
||||||
|
import DaysLeft from "./DaysLeft.svelte";
|
||||||
|
import Icon from "./Icon.svelte";
|
||||||
|
import Option from "./Option.svelte";
|
||||||
|
import OptionRow from "./OptionRow.svelte";
|
||||||
|
import Progress from "./Progress.svelte";
|
||||||
|
|
||||||
|
export let goal: GoalResult = null;
|
||||||
|
export let showAllOptions = false;
|
||||||
|
|
||||||
|
let iconName: IconName = "question";
|
||||||
|
let mdGoalEdit: ModalData;
|
||||||
|
let mdGoalDelete: ModalData;
|
||||||
|
|
||||||
|
$: iconName = goal.group.icon as IconName;
|
||||||
|
$: mdGoalEdit = {name:"goal.edit", goal};
|
||||||
|
$: mdGoalDelete = {name:"goal.delete", goal};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="goal" class:full={showAllOptions}>
|
||||||
|
<div class="icon"><Icon block name={iconName} /></div>
|
||||||
|
<div class="body">
|
||||||
|
<div class="header">
|
||||||
|
<div class="name">{goal.name}</div>
|
||||||
|
<div class="times">
|
||||||
|
<DaysLeft startTime={goal.startTime} endTime={goal.endTime} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if showAllOptions}
|
||||||
|
<div class="description">
|
||||||
|
<p>{goal.description}</p>
|
||||||
|
</div>
|
||||||
|
<OptionRow>
|
||||||
|
<Option open={mdGoalEdit}>Edit</Option>
|
||||||
|
<Option open={mdGoalDelete}>Delete</Option>
|
||||||
|
</OptionRow>
|
||||||
|
{/if}
|
||||||
|
<div class="progress">
|
||||||
|
<Progress count={goal.completedAmount} target={goal.amount} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div.goal {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
padding-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
div.goal.full {
|
||||||
|
padding-bottom: 1em;
|
||||||
|
}
|
||||||
|
div.icon {
|
||||||
|
font-size: 2em;
|
||||||
|
padding: 0 0.5ch;
|
||||||
|
width: 2ch;
|
||||||
|
padding-top: 0.125em;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
div.body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
div.header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.name {
|
||||||
|
font-size: 1em;
|
||||||
|
margin: auto 0;
|
||||||
|
vertical-align: middle;
|
||||||
|
font-weight: 100;
|
||||||
|
}
|
||||||
|
div.times {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: 0.25ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.progress {
|
||||||
|
padding-top: 0.125em;
|
||||||
|
font-size: 1.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.description > p {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0.25em 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { IconName } from "../external/icons";
|
||||||
|
import type { GroupResult } from "../models/group";
|
||||||
|
import type { ModalData } from "../stores/modal";
|
||||||
|
import DaysLeft from "./DaysLeft.svelte";
|
||||||
|
import Icon from "./Icon.svelte";
|
||||||
|
import ItemEntry from "./ItemEntry.svelte";
|
||||||
|
import Option from "./Option.svelte";
|
||||||
|
import OptionRow from "./OptionRow.svelte";
|
||||||
|
import TaskEntry from "./TaskEntry.svelte";
|
||||||
|
|
||||||
|
export let group: GroupResult = null;
|
||||||
|
export let showAllOptions: boolean = false;
|
||||||
|
|
||||||
|
let iconName: IconName = "question";
|
||||||
|
let mdItemAdd: ModalData;
|
||||||
|
let mdGroupEdit: ModalData;
|
||||||
|
let mdGroupDelete: ModalData;
|
||||||
|
|
||||||
|
$: iconName = group.icon as IconName;
|
||||||
|
$: mdItemAdd = {name:"item.add", group};
|
||||||
|
$: mdGroupEdit = {name:"group.edit", group};
|
||||||
|
$: mdGroupDelete = {name:"group.delete", group};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="group">
|
||||||
|
<div class="icon"><Icon block name={iconName} /></div>
|
||||||
|
<div class="body">
|
||||||
|
<div class="header">
|
||||||
|
<div class="name">{group.name}</div>
|
||||||
|
</div>
|
||||||
|
{#if showAllOptions}
|
||||||
|
<div class="description">
|
||||||
|
<p>{group.description}</p>
|
||||||
|
</div>
|
||||||
|
<OptionRow>
|
||||||
|
<Option open={mdItemAdd}>Add Item</Option>
|
||||||
|
<Option open={mdGroupEdit}>Edit</Option>
|
||||||
|
<Option open={mdGroupDelete}>Delete</Option>
|
||||||
|
</OptionRow>
|
||||||
|
{/if}
|
||||||
|
<div class="list" class:full={showAllOptions}>
|
||||||
|
{#each group.items as item (item.id)}
|
||||||
|
<ItemEntry item={item} group={group} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div.group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
padding-bottom: 1em;
|
||||||
|
}
|
||||||
|
div.icon {
|
||||||
|
font-size: 2em;
|
||||||
|
padding: 0 0.5ch;
|
||||||
|
width: 2ch;
|
||||||
|
padding-top: 0.125em;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
div.body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
div.header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.name {
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 100;
|
||||||
|
margin: auto 0;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.description > p {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0.25em 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import groupStore from "../stores/group";
|
||||||
|
|
||||||
|
export let value = "";
|
||||||
|
export let name = "";
|
||||||
|
export let disabled = false;
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if ($groupStore.stale && !$groupStore.loading) {
|
||||||
|
groupStore.load();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if (!disabled && $groupStore.groups.length > 0 && value === "") {
|
||||||
|
const nonEmpty = $groupStore.groups.find(g => g.items.length > 0);
|
||||||
|
if (nonEmpty != null) {
|
||||||
|
value = nonEmpty.id;
|
||||||
|
} else {
|
||||||
|
value = $groupStore.groups[0].id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<select disabled={disabled || $groupStore.loading} name={name} bind:value={value}>
|
||||||
|
{#each $groupStore.groups as group (group.id)}
|
||||||
|
<option value={group.id} selected={group.id === value}>{group.name} ({group.items.length} items)</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Icon from "fa-svelte"
|
||||||
|
import icons from "../external/icons";
|
||||||
|
import type { IconName } from "../external/icons";
|
||||||
|
|
||||||
|
export let name: IconName = "question";
|
||||||
|
export let block: boolean = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if block}
|
||||||
|
<div>
|
||||||
|
<Icon class="activity-icon" icon={icons[name] || icons.question} />
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<Icon class="activity-icon" icon={icons[name] || icons.question} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { IconName } from "../external/icons";
|
||||||
|
import { iconNames } from "../external/icons";
|
||||||
|
import Icon from "./Icon.svelte";
|
||||||
|
|
||||||
|
export let value: IconName;
|
||||||
|
export let disabled: boolean;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class:disabled class="icon-select">
|
||||||
|
{#each iconNames as iconName (iconName)}
|
||||||
|
<div class="icon-item" class:selected={value===iconName} on:click={() => {if (!disabled) { value = iconName }}}>
|
||||||
|
<Icon name={iconName} />
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div.icon-select {
|
||||||
|
background: #222;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 0.05em;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.icon-select.disabled {
|
||||||
|
background: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.icon-item {
|
||||||
|
display: inline-block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: calc(100% / 8);
|
||||||
|
padding: 0.35em 0 0.25em 0;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
div.icon-item:hover {
|
||||||
|
background-color: #292929;
|
||||||
|
}
|
||||||
|
div.icon-item.selected {
|
||||||
|
background-color: rgb(18, 63, 75);
|
||||||
|
}
|
||||||
|
div.icon-item.selected:hover {
|
||||||
|
background-color: rgb(24, 83, 99);
|
||||||
|
}
|
||||||
|
div.icon-select.disabled > div.icon-item {
|
||||||
|
background-color: #444;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
div.icon-select.disabled > div.icon-item.selected {
|
||||||
|
background-color: #555;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { IconName } from "../external/icons";
|
||||||
|
import type { GroupResult } from "../models/group";
|
||||||
|
import type { default as Item } from "../models/item";
|
||||||
|
import type { ModalData } from "../stores/modal";
|
||||||
|
import Option from "./Option.svelte";
|
||||||
|
import OptionRow from "./OptionRow.svelte";
|
||||||
|
|
||||||
|
export let item: Item = null;
|
||||||
|
export let group: GroupResult = null;
|
||||||
|
|
||||||
|
let mdItemEdit: ModalData;
|
||||||
|
let mdItemDelete: ModalData;
|
||||||
|
|
||||||
|
$: mdItemEdit = {name: "item.edit", item, group};
|
||||||
|
$: mdItemDelete = {name: "item.delete", item, group};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="item">
|
||||||
|
<div class="body">
|
||||||
|
<div class="header">
|
||||||
|
<div class="icon">
|
||||||
|
{item.groupWeight}
|
||||||
|
</div>
|
||||||
|
<div class="name">{item.name}</div>
|
||||||
|
</div>
|
||||||
|
<div class="description">
|
||||||
|
<p>{item.description}</p>
|
||||||
|
|
||||||
|
<OptionRow>
|
||||||
|
<Option open={mdItemEdit}>Edit</Option>
|
||||||
|
<Option open={mdItemDelete}>Delete</Option>
|
||||||
|
</OptionRow>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div.item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin: 0.25em 0 0.75em 0;
|
||||||
|
}
|
||||||
|
div.body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
div.header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
background: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.icon {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-size: 1em;
|
||||||
|
padding: 0.125em .5ch;
|
||||||
|
min-width: 2ch;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
margin-right: 0.5em;
|
||||||
|
background: #444;
|
||||||
|
color: #CCC;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.name {
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 100;
|
||||||
|
margin: auto 0;
|
||||||
|
vertical-align: middle;
|
||||||
|
padding: 0.125em .5ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.description {
|
||||||
|
padding: 0.25em 1ch;
|
||||||
|
background: #222;
|
||||||
|
color: #aaa;
|
||||||
|
border-bottom-right-radius: 0.5em;
|
||||||
|
}
|
||||||
|
div.description p {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0.25em 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import groupStore from "../stores/group";
|
||||||
|
|
||||||
|
export let value = "";
|
||||||
|
export let name = "";
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if ($groupStore.stale && !$groupStore.loading) {
|
||||||
|
groupStore.load();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if ($groupStore.groups.length > 0 && value === "") {
|
||||||
|
const nonEmpty = $groupStore.groups.find(g => g.items.length > 0);
|
||||||
|
if (nonEmpty != null) {
|
||||||
|
value = nonEmpty.items[0].id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<select name={name} bind:value={value} disabled={$groupStore.loading}>
|
||||||
|
{#each $groupStore.groups as group (group.id)}
|
||||||
|
<optgroup label={group.name}>
|
||||||
|
{#each group.items as item (item.id)}
|
||||||
|
<option value={item.id} selected={item.id === value}>{item.name} ({item.groupWeight})</option>
|
||||||
|
{/each}
|
||||||
|
</optgroup>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { IconName } from "../external/icons";
|
||||||
|
import type { LogResult } from "../models/log";
|
||||||
|
import type { ModalData } from "../stores/modal";
|
||||||
|
import { formatTime } from "../utils/time";
|
||||||
|
import Icon from "./Icon.svelte";
|
||||||
|
import Option from "./Option.svelte";
|
||||||
|
import OptionRow from "./OptionRow.svelte";
|
||||||
|
|
||||||
|
export let log: LogResult = null;
|
||||||
|
|
||||||
|
let taskIconName: IconName = "question";
|
||||||
|
let mdLogEdit: ModalData;
|
||||||
|
let mdLogDelete: ModalData;
|
||||||
|
|
||||||
|
$: taskIconName = log.task.icon as IconName;
|
||||||
|
$: mdLogEdit = {name: "log.edit", log};
|
||||||
|
$: mdLogDelete = {name: "log.delete", log};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="log">
|
||||||
|
<div class="body">
|
||||||
|
<div class="header">
|
||||||
|
<div class="icon">
|
||||||
|
<Icon name={taskIconName} />
|
||||||
|
</div>
|
||||||
|
<div class="name">{log.task.name}</div>
|
||||||
|
<div class="times">{formatTime(log.loggedTime)}</div>
|
||||||
|
</div>
|
||||||
|
<div class="description">
|
||||||
|
<p>{log.description}</p>
|
||||||
|
|
||||||
|
<OptionRow>
|
||||||
|
<Option open={mdLogEdit}>Edit Log</Option>
|
||||||
|
<Option open={mdLogDelete}>Delete Log</Option>
|
||||||
|
</OptionRow>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div.log {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin: 0.25em 0;
|
||||||
|
}
|
||||||
|
div.icon {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-size: 1em;
|
||||||
|
padding: 0.125em .5ch;
|
||||||
|
padding-top: 0.2em;
|
||||||
|
|
||||||
|
margin-right: 0.5em;
|
||||||
|
background: #444;
|
||||||
|
color: #CCC;
|
||||||
|
}
|
||||||
|
div.body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
div.header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
background: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.name {
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 100;
|
||||||
|
margin: auto 0;
|
||||||
|
vertical-align: middle;
|
||||||
|
padding: 0.125em .5ch;
|
||||||
|
}
|
||||||
|
div.times {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: 0.25ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.description {
|
||||||
|
padding: 0.25em 1ch;
|
||||||
|
background: #222;
|
||||||
|
color: #aaa;
|
||||||
|
border-bottom-right-radius: 0.5em;
|
||||||
|
}
|
||||||
|
div.description p {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0.25em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.log {
|
||||||
|
padding: 0.25em 1ch;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { link } from "svelte-routing";
|
||||||
|
|
||||||
|
export let location: string = window.location.pathname.split("?")[0];
|
||||||
|
|
||||||
|
function updateLocation() {
|
||||||
|
setTimeout(() => {
|
||||||
|
location = window.location.pathname.split("?")[0];
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
$: selected = {
|
||||||
|
home: location == "/",
|
||||||
|
goals: location.startsWith("/goals"),
|
||||||
|
projects: location.startsWith("/projects"),
|
||||||
|
items: location.startsWith("/items"),
|
||||||
|
logs: location.startsWith("/logs"),
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<nav>
|
||||||
|
<a on:click={updateLocation} class:selected={selected.home} use:link href="/">Stufflog</a>
|
||||||
|
<a on:click={updateLocation} class:selected={selected.goals} use:link href="/goals">Goals</a>
|
||||||
|
<a on:click={updateLocation} class:selected={selected.projects} use:link href="/projects">Projects</a>
|
||||||
|
<a on:click={updateLocation} class:selected={selected.items} use:link href="/items">Items</a>
|
||||||
|
<a on:click={updateLocation} class:selected={selected.logs} use:link href="/logs">Logs</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
nav {
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.25em;
|
||||||
|
color: #555;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
a.selected {
|
||||||
|
color: #AAA;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher, onMount } from 'svelte';
|
||||||
|
import Icon from './Icon.svelte';
|
||||||
|
|
||||||
|
export let title: string = "";
|
||||||
|
export let wide: boolean = false;
|
||||||
|
export let error: string | null = null;
|
||||||
|
export let closable: boolean = false;
|
||||||
|
export let show: boolean = false;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const listener = (ev: KeyboardEvent) => {
|
||||||
|
console.log(ev.key);
|
||||||
|
|
||||||
|
if ((ev.ctrlKey || ev.altKey) && (ev.key === "Escape" || ev.key.toLowerCase() === "q")) {
|
||||||
|
dispatch("close");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("keyup", listener);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("keyup", listener);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if show}
|
||||||
|
<div class="modal-background">
|
||||||
|
<div class="modal" class:wide>
|
||||||
|
<div class="header">
|
||||||
|
<div class="title" class:noclose={!closable}>{title}</div>
|
||||||
|
{#if (closable)}
|
||||||
|
<div class="x">
|
||||||
|
<div class="button" on:click={() => dispatch("close")}>
|
||||||
|
<Icon name="times" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
{#if (error != null)}
|
||||||
|
<div class="error">{error}</div>
|
||||||
|
{/if}
|
||||||
|
<div class="body">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div.modal-background {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.modal {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
width: calc(100vw - 4em);
|
||||||
|
max-width: 40ch;
|
||||||
|
max-height: calc(100vh - 4em);
|
||||||
|
overflow: auto;
|
||||||
|
transform: translate(-50%,-50%);
|
||||||
|
padding: 1em;
|
||||||
|
border-radius: 0.2em;
|
||||||
|
|
||||||
|
background: #333;
|
||||||
|
}
|
||||||
|
div.modal.wide {
|
||||||
|
max-width: 60ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.modal :global(hr) {
|
||||||
|
border: 0.5px solid gray;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.error {
|
||||||
|
margin: 0.5em;
|
||||||
|
padding: 0.5em;
|
||||||
|
|
||||||
|
border: 1px solid rgb(204, 65, 65);
|
||||||
|
border-radius: 0.2em;
|
||||||
|
background-color: rgb(133, 39, 39);
|
||||||
|
color: rgb(211, 141, 141);
|
||||||
|
|
||||||
|
animation: fadein 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.body {
|
||||||
|
margin: 1em 0.25ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.title {
|
||||||
|
color: #CCC;
|
||||||
|
line-height: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.title.noclose {
|
||||||
|
margin-bottom: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.x {
|
||||||
|
position: relative;
|
||||||
|
line-height: 1em;
|
||||||
|
top: -1em;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
div.x div.button {
|
||||||
|
color: #CCC;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0em 0.5ch 0.1em 0.5ch;
|
||||||
|
line-height: 1em;
|
||||||
|
user-select: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
div.x div.button:hover {
|
||||||
|
color: #FFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.modal :global(button) {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.25em 0.75ch 0.26em 0.75ch;
|
||||||
|
margin: 0.75em 0.25ch 0.25em 0.25ch;
|
||||||
|
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.2em;
|
||||||
|
color: #CCC;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.modal :global(button:hover), div.modal :global(button:focus) {
|
||||||
|
background: #222;
|
||||||
|
color: #FFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.modal :global(label) {
|
||||||
|
padding: 0 0 0.125em 0.25ch;
|
||||||
|
font-size: 0.75em;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.modal :global(input), div.modal :global(select), div.modal :global(textarea) {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
|
||||||
|
background: #222;
|
||||||
|
color: #777;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
div.modal :global(select) {
|
||||||
|
padding-left: 0.5ch;
|
||||||
|
}
|
||||||
|
div.modal :global(input:disabled) {
|
||||||
|
background: #444;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
div.modal :global(textarea) {
|
||||||
|
height: 6em;
|
||||||
|
}
|
||||||
|
div.modal :global(textarea:disabled) {
|
||||||
|
background: #444;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.modal :global(input:last-of-type) {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
div.modal :global(input.nolast) {
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.modal :global(input[type="checkbox"]) {
|
||||||
|
width: initial;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
div.modal :global(input[type="checkbox"] + label) {
|
||||||
|
width: initial;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.modal :global(input:focus), div.modal :global(select:focus), div.modal :global(textarea:focus) {
|
||||||
|
background: #111;
|
||||||
|
color: #CCC;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.modal :global(p) {
|
||||||
|
margin: 0.25em 1ch 1em 1ch;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadein {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { ModalData } from "../stores/modal";
|
||||||
|
import modalStore from "../stores/modal";
|
||||||
|
|
||||||
|
export let name: ModalData["name"] = "none";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $modalStore.name === name}
|
||||||
|
<slot></slot>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
import type { ModalData } from "../stores/modal";
|
||||||
|
import modalStore from "../stores/modal";
|
||||||
|
|
||||||
|
export let open: ModalData = {name: "none"};
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
dispatch("click", {open});
|
||||||
|
|
||||||
|
if (open.name !== "none") {
|
||||||
|
modalStore.set(open);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div on:click={handleClick} class="option"><slot></slot></div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div.option {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0.9em;
|
||||||
|
padding: 0.125em 0.75ch;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #aa8822;
|
||||||
|
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.option:hover {
|
||||||
|
color: #FC1;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<div class="option-row">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div.option-row {
|
||||||
|
margin-left: -0.5ch;
|
||||||
|
margin-right: -0.5ch;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
<script lang="ts" context="module">
|
||||||
|
const COLORS = [
|
||||||
|
"none",
|
||||||
|
"bronze",
|
||||||
|
"silver",
|
||||||
|
"gold",
|
||||||
|
"diamond"
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export let target = 1;
|
||||||
|
export let count = 0;
|
||||||
|
|
||||||
|
let offClass = COLORS[0];
|
||||||
|
let onClass = COLORS[1];
|
||||||
|
let ons = 0;
|
||||||
|
let offs = 1;
|
||||||
|
|
||||||
|
$: {
|
||||||
|
let level = Math.floor(count / target);
|
||||||
|
if (level >= COLORS.length - 1) {
|
||||||
|
offs = 0;
|
||||||
|
ons = target;
|
||||||
|
offClass = "gold";
|
||||||
|
onClass = "diamond";
|
||||||
|
} else {
|
||||||
|
if (count > 0 && count == (level * target)) {
|
||||||
|
level -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ons = count - (level * target);
|
||||||
|
offs = target - ons;
|
||||||
|
offClass = COLORS[level];
|
||||||
|
onClass = COLORS[level + 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="bar">
|
||||||
|
{#each {length: ons} as _}
|
||||||
|
<div class={"on " + onClass}></div>
|
||||||
|
{/each}
|
||||||
|
{#each {length: offs} as _}
|
||||||
|
<div class={"off " + offClass}></div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div.bar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.bar > div {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-basis: 0;
|
||||||
|
display: inline-block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 0.1px solid #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.none { background-color: #555555; }
|
||||||
|
div.bronze { background-color: #f4b083; }
|
||||||
|
div.silver { background-color: #d8dce4; }
|
||||||
|
div.gold { background-color: #ffd966; }
|
||||||
|
div.diamond { background-color: #84f5ff; }
|
||||||
|
|
||||||
|
div.on {
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.off {
|
||||||
|
opacity: 0.33;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { IconName } from "../external/icons";
|
||||||
|
import type { ProjectResult } from "../models/project";
|
||||||
|
import type { ModalData } from "../stores/modal";
|
||||||
|
import DaysLeft from "./DaysLeft.svelte";
|
||||||
|
import Icon from "./Icon.svelte";
|
||||||
|
import Option from "./Option.svelte";
|
||||||
|
import OptionRow from "./OptionRow.svelte";
|
||||||
|
import TaskEntry from "./TaskEntry.svelte";
|
||||||
|
|
||||||
|
export let project: ProjectResult = null;
|
||||||
|
export let showAllOptions: boolean = false;
|
||||||
|
|
||||||
|
let iconName: IconName = "question";
|
||||||
|
let mdAddTask: ModalData;
|
||||||
|
let mdProjectAdd: ModalData;
|
||||||
|
let mdProjectEdit: ModalData;
|
||||||
|
let mdProjectDelete: ModalData;
|
||||||
|
|
||||||
|
$: iconName = project.icon as IconName;
|
||||||
|
$: mdAddTask = {name:"task.add", project};
|
||||||
|
$: mdProjectAdd = {name:"project.add"};
|
||||||
|
$: mdProjectEdit = {name:"project.edit", project};
|
||||||
|
$: mdProjectDelete = {name:"project.delete", project};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="project">
|
||||||
|
<div class="icon"><Icon block name={iconName} /></div>
|
||||||
|
<div class="body">
|
||||||
|
<div class="header">
|
||||||
|
<div class="name">{project.name}</div>
|
||||||
|
{#if (project.endTime != null)}
|
||||||
|
<div class="times">
|
||||||
|
<DaysLeft startTime={project.createdTime} endTime={project.endTime} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if showAllOptions}
|
||||||
|
<div class="description">
|
||||||
|
<p>{project.description}</p>
|
||||||
|
</div>
|
||||||
|
<OptionRow>
|
||||||
|
<Option open={mdAddTask}>Add Task</Option>
|
||||||
|
<Option open={mdProjectEdit}>Edit</Option>
|
||||||
|
<Option open={mdProjectDelete}>Delete</Option>
|
||||||
|
</OptionRow>
|
||||||
|
{/if}
|
||||||
|
<div class="list" class:full={showAllOptions}>
|
||||||
|
{#each project.tasks as task (task.id)}
|
||||||
|
<TaskEntry showAllOptions={showAllOptions} task={task} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div.project {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
padding-bottom: 1em;
|
||||||
|
}
|
||||||
|
div.icon {
|
||||||
|
font-size: 2em;
|
||||||
|
padding: 0 0.5ch;
|
||||||
|
width: 2ch;
|
||||||
|
padding-top: 0.125em;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
div.body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
div.header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.name {
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 100;
|
||||||
|
margin: auto 0;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
div.times {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: 0.25ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.description > p {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0.25em 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { IconName } from "../external/icons";
|
||||||
|
import type { TaskResult } from "../models/task";
|
||||||
|
import type { ModalData } from "../stores/modal";
|
||||||
|
import DateSpan from "./DateSpan.svelte";
|
||||||
|
import DaysLeft from "./DaysLeft.svelte";
|
||||||
|
import Icon from "./Icon.svelte";
|
||||||
|
import Option from "./Option.svelte";
|
||||||
|
import OptionRow from "./OptionRow.svelte";
|
||||||
|
|
||||||
|
export let task: TaskResult = null;
|
||||||
|
export let showAllOptions: boolean = false;
|
||||||
|
|
||||||
|
let itomIconName: IconName = "question";
|
||||||
|
let showLogs = false;
|
||||||
|
let mdLogAdd: ModalData;
|
||||||
|
let mdTaskEdit: ModalData;
|
||||||
|
let mdTaskDelete: ModalData;
|
||||||
|
|
||||||
|
function toggleShowLogs() {
|
||||||
|
showLogs = !showLogs;
|
||||||
|
}
|
||||||
|
|
||||||
|
$: itomIconName = task.item.icon as IconName;
|
||||||
|
$: mdLogAdd = {name: "log.add", task};
|
||||||
|
$: mdTaskEdit = {name: "task.edit", task};
|
||||||
|
$: mdTaskDelete = {name: "task.delete", task};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="task">
|
||||||
|
<div class="body">
|
||||||
|
<div class="header">
|
||||||
|
{#if !task.active}
|
||||||
|
<div class="icon done">
|
||||||
|
<Icon name="check" />
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="icon">
|
||||||
|
{task.completedAmount} / {task.itemAmount}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="name">{task.name}</div>
|
||||||
|
{#if (task.endTime != null)}
|
||||||
|
<div class="times">
|
||||||
|
<DaysLeft startTime={task.createdTime} endTime={task.endTime} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="description">
|
||||||
|
<p>{task.description}</p>
|
||||||
|
<div class="item">
|
||||||
|
<div class="item-icon">
|
||||||
|
<Icon name={itomIconName} />
|
||||||
|
</div>
|
||||||
|
<div class="item-name">{task.item.name} ({task.item.groupWeight})</div>
|
||||||
|
</div>
|
||||||
|
<OptionRow>
|
||||||
|
{#if task.logs.length > 0}
|
||||||
|
<Option on:click={toggleShowLogs}>{showLogs ? "Hide Logs" : "Show Logs"}</Option>
|
||||||
|
{/if}
|
||||||
|
<Option open={mdLogAdd}>Add Log</Option>
|
||||||
|
{#if showAllOptions}
|
||||||
|
<Option open={mdTaskEdit}>Edit</Option>
|
||||||
|
<Option open={mdTaskDelete}>Delete</Option>
|
||||||
|
{/if}
|
||||||
|
</OptionRow>
|
||||||
|
{#if showLogs && task.logs.length > 0}
|
||||||
|
<div class="log-list">
|
||||||
|
{#each task.logs as log (log.id)}
|
||||||
|
<div class="log">
|
||||||
|
<div class="log-time"><DateSpan time={log.loggedTime} /></div>
|
||||||
|
<div class="log-description">{log.description}</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div.task {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin: 0.25em 0 0.75em 0;
|
||||||
|
}
|
||||||
|
div.icon {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-size: 1em;
|
||||||
|
padding: 0.125em .5ch;
|
||||||
|
|
||||||
|
margin-right: 0.5em;
|
||||||
|
background: #444;
|
||||||
|
color: #CCC;
|
||||||
|
}
|
||||||
|
div.icon.done {
|
||||||
|
padding-top: 0.2em;
|
||||||
|
background: #484;
|
||||||
|
color: #78ff78;
|
||||||
|
}
|
||||||
|
div.body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
div.header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
background: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.name {
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 100;
|
||||||
|
margin: auto 0;
|
||||||
|
vertical-align: middle;
|
||||||
|
padding: 0.125em .5ch;
|
||||||
|
}
|
||||||
|
div.times {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: 0.25ch;
|
||||||
|
padding: 0.125em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.description {
|
||||||
|
padding: 0.25em 1ch;
|
||||||
|
background: #222;
|
||||||
|
color: #aaa;
|
||||||
|
border-bottom-right-radius: 0.5em;
|
||||||
|
}
|
||||||
|
div.description p {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0.25em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.log-list {
|
||||||
|
padding: 0.5em 0;
|
||||||
|
}
|
||||||
|
div.log {
|
||||||
|
padding: 0.25em 1ch;
|
||||||
|
}
|
||||||
|
div.log-time {
|
||||||
|
font-size: 0.75em;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin-top: 0.25em;
|
||||||
|
margin-bottom: 0em;
|
||||||
|
font-size: 0.75em;
|
||||||
|
}
|
||||||
|
div.item div.item-icon {
|
||||||
|
padding: 0.25em 0.5ch 0.25em 0;
|
||||||
|
}
|
||||||
|
div.item div.item-name {
|
||||||
|
padding: 0.125em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Vendored
+138
@@ -0,0 +1,138 @@
|
|||||||
|
import { faQuestion } from "@fortawesome/free-solid-svg-icons/faQuestion";
|
||||||
|
import { faPlus } from "@fortawesome/free-solid-svg-icons/faPlus";
|
||||||
|
import { faCube } from "@fortawesome/free-solid-svg-icons/faCube";
|
||||||
|
import { faCubes } from "@fortawesome/free-solid-svg-icons/faCubes";
|
||||||
|
import { faBook } from "@fortawesome/free-solid-svg-icons/faBook";
|
||||||
|
import { faBookOpen } from "@fortawesome/free-solid-svg-icons/faBookOpen";
|
||||||
|
import { faBookDead } from "@fortawesome/free-solid-svg-icons/faBookDead";
|
||||||
|
import { faPen } from "@fortawesome/free-solid-svg-icons/faPen";
|
||||||
|
import { faPencilAlt } from "@fortawesome/free-solid-svg-icons/faPencilAlt";
|
||||||
|
import { faDiceD20 } from "@fortawesome/free-solid-svg-icons/faDiceD20";
|
||||||
|
import { faDiceD6 } from "@fortawesome/free-solid-svg-icons/faDiceD6";
|
||||||
|
import { faDungeon } from "@fortawesome/free-solid-svg-icons/faDungeon";
|
||||||
|
import { faGamepad } from "@fortawesome/free-solid-svg-icons/faGamepad";
|
||||||
|
import { faHeadphones } from "@fortawesome/free-solid-svg-icons/faHeadphones";
|
||||||
|
import { faLanguage } from "@fortawesome/free-solid-svg-icons/faLanguage";
|
||||||
|
import { faCode } from "@fortawesome/free-solid-svg-icons/faCode";
|
||||||
|
import { faCodeBranch } from "@fortawesome/free-solid-svg-icons/faCodeBranch";
|
||||||
|
import { faGuitar } from "@fortawesome/free-solid-svg-icons/faGuitar";
|
||||||
|
import { faMusic } from "@fortawesome/free-solid-svg-icons/faMusic";
|
||||||
|
import { faArchive } from "@fortawesome/free-solid-svg-icons/faArchive";
|
||||||
|
import { faCheck } from "@fortawesome/free-solid-svg-icons/faCheck";
|
||||||
|
import { faDrawPolygon } from "@fortawesome/free-solid-svg-icons/faDrawPolygon";
|
||||||
|
import { faComment } from "@fortawesome/free-solid-svg-icons/faComment";
|
||||||
|
import { faDatabase } from "@fortawesome/free-solid-svg-icons/faDatabase";
|
||||||
|
import { faCog } from "@fortawesome/free-solid-svg-icons/faCog";
|
||||||
|
import { faLink } from "@fortawesome/free-solid-svg-icons/faLink";
|
||||||
|
import { faStar } from "@fortawesome/free-solid-svg-icons/faStar";
|
||||||
|
import { faStarOfLife } from "@fortawesome/free-solid-svg-icons/faStarOfLife";
|
||||||
|
import { faSun } from "@fortawesome/free-solid-svg-icons/faSun";
|
||||||
|
import { faHdd } from "@fortawesome/free-solid-svg-icons/faHdd";
|
||||||
|
import { faServer } from "@fortawesome/free-solid-svg-icons/faServer";
|
||||||
|
import { faBlender } from "@fortawesome/free-solid-svg-icons/faBlender";
|
||||||
|
import { faCross } from "@fortawesome/free-solid-svg-icons/faCross";
|
||||||
|
import { faTimes } from "@fortawesome/free-solid-svg-icons/faTimes";
|
||||||
|
import { faSkullCrossbones } from "@fortawesome/free-solid-svg-icons/faSkullCrossbones";
|
||||||
|
import { faCrosshairs } from "@fortawesome/free-solid-svg-icons/faCrosshairs";
|
||||||
|
import { faLaptop } from "@fortawesome/free-solid-svg-icons/faLaptop";
|
||||||
|
import { faMemory } from "@fortawesome/free-solid-svg-icons/faMemory";
|
||||||
|
import { faKeyboard } from "@fortawesome/free-solid-svg-icons/faKeyboard";
|
||||||
|
import { faCookie } from "@fortawesome/free-solid-svg-icons/faCookie";
|
||||||
|
import { faMicrochip } from "@fortawesome/free-solid-svg-icons/faMicrochip";
|
||||||
|
import { faClipboard } from "@fortawesome/free-solid-svg-icons/faClipboard";
|
||||||
|
import { faPizzaSlice } from "@fortawesome/free-solid-svg-icons/faPizzaSlice";
|
||||||
|
import { faPaperclip } from "@fortawesome/free-solid-svg-icons/faPaperclip";
|
||||||
|
import { faReceipt } from "@fortawesome/free-solid-svg-icons/faReceipt";
|
||||||
|
import { faSuperscript } from "@fortawesome/free-solid-svg-icons/faSuperscript";
|
||||||
|
import { faCouch } from "@fortawesome/free-solid-svg-icons/faCouch";
|
||||||
|
import { faTerminal } from "@fortawesome/free-solid-svg-icons/faTerminal";
|
||||||
|
import { faGift } from "@fortawesome/free-solid-svg-icons/faGift";
|
||||||
|
import { faGifts } from "@fortawesome/free-solid-svg-icons/faGifts";
|
||||||
|
import { faImage } from "@fortawesome/free-solid-svg-icons/faImage";
|
||||||
|
import { faImages } from "@fortawesome/free-solid-svg-icons/faImages";
|
||||||
|
import { faDragon } from "@fortawesome/free-solid-svg-icons/faDragon";
|
||||||
|
import { faLightbulb } from "@fortawesome/free-solid-svg-icons/faLightbulb";
|
||||||
|
import { faTools } from "@fortawesome/free-solid-svg-icons/faTools";
|
||||||
|
import { faHammer } from "@fortawesome/free-solid-svg-icons/faHammer";
|
||||||
|
import { faScrewdriver } from "@fortawesome/free-solid-svg-icons/faScrewdriver";
|
||||||
|
import { faWrench } from "@fortawesome/free-solid-svg-icons/faWrench";
|
||||||
|
import { faBug } from "@fortawesome/free-solid-svg-icons/faBug";
|
||||||
|
import { faUtensils } from "@fortawesome/free-solid-svg-icons/faUtensils";
|
||||||
|
import { faHome } from "@fortawesome/free-solid-svg-icons/faHome";
|
||||||
|
import { faIgloo } from "@fortawesome/free-solid-svg-icons/faIgloo";
|
||||||
|
import { faWarehouse } from "@fortawesome/free-solid-svg-icons/faWarehouse";
|
||||||
|
import { faToiletPaperSlash } from "@fortawesome/free-solid-svg-icons/faToiletPaperSlash";
|
||||||
|
|
||||||
|
const icons = {
|
||||||
|
"question": faQuestion,
|
||||||
|
"plus": faPlus,
|
||||||
|
"cube": faCube,
|
||||||
|
"cubes": faCubes,
|
||||||
|
"book": faBook,
|
||||||
|
"book_open": faBookOpen,
|
||||||
|
"book_dead": faBookDead,
|
||||||
|
"pen": faPen,
|
||||||
|
"pencil_alt": faPencilAlt,
|
||||||
|
"draw_poligon": faDrawPolygon,
|
||||||
|
"dice_d20": faDiceD20,
|
||||||
|
"dice_d6": faDiceD6,
|
||||||
|
"dungeon": faDungeon,
|
||||||
|
"gamepad": faGamepad,
|
||||||
|
"headphones": faHeadphones,
|
||||||
|
"language": faLanguage,
|
||||||
|
"code": faCode,
|
||||||
|
"code_branch": faCodeBranch,
|
||||||
|
"guitar": faGuitar,
|
||||||
|
"archive": faArchive,
|
||||||
|
"check": faCheck,
|
||||||
|
"music": faMusic,
|
||||||
|
"comment": faComment,
|
||||||
|
"database": faDatabase,
|
||||||
|
"cog": faCog,
|
||||||
|
"link": faLink,
|
||||||
|
"star": faStar,
|
||||||
|
"star_of_life": faStarOfLife,
|
||||||
|
"sun": faSun,
|
||||||
|
"hdd": faHdd,
|
||||||
|
"server": faServer,
|
||||||
|
"blender": faBlender,
|
||||||
|
"cross": faCross,
|
||||||
|
"times": faTimes,
|
||||||
|
"crosshairs": faCrosshairs,
|
||||||
|
"skull_crossbones": faSkullCrossbones,
|
||||||
|
"laptop": faLaptop,
|
||||||
|
"memory": faMemory,
|
||||||
|
"keyboard": faKeyboard,
|
||||||
|
"cookie": faCookie,
|
||||||
|
"microchip": faMicrochip,
|
||||||
|
"clipboard": faClipboard,
|
||||||
|
"pizza_slice": faPizzaSlice,
|
||||||
|
"paperclip": faPaperclip,
|
||||||
|
"receipt": faReceipt,
|
||||||
|
"superscript": faSuperscript,
|
||||||
|
"couch": faCouch,
|
||||||
|
"terminal": faTerminal,
|
||||||
|
"gift": faGift,
|
||||||
|
"gifts": faGifts,
|
||||||
|
"image": faImage,
|
||||||
|
"images": faImages,
|
||||||
|
"dragon": faDragon,
|
||||||
|
"lightbulb": faLightbulb,
|
||||||
|
"tools": faTools,
|
||||||
|
"hammer": faHammer,
|
||||||
|
"screwdriver": faScrewdriver,
|
||||||
|
"wrench": faWrench,
|
||||||
|
"bug": faBug,
|
||||||
|
"utensils": faUtensils,
|
||||||
|
"home": faHome,
|
||||||
|
"igloo": faIgloo,
|
||||||
|
"warehouse": faWarehouse,
|
||||||
|
"toilet_paper_slash": faToiletPaperSlash,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type IconName = keyof typeof icons;
|
||||||
|
|
||||||
|
export const iconNames = Object.keys(icons).sort() as IconName[];
|
||||||
|
export const DEFAULT_ICON = iconNames[0] as IconName;
|
||||||
|
|
||||||
|
export default icons;
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import stuffLogClient from "../clients/stufflog";
|
||||||
|
import Modal from "../components/Modal.svelte";
|
||||||
|
import modalStore from "../stores/modal";
|
||||||
|
import goalStore, { fpGoalStore } from "../stores/goal";
|
||||||
|
import groupStore from "../stores/goal";
|
||||||
|
import IconSelect from "../components/IconSelect.svelte";
|
||||||
|
import { DEFAULT_ICON } from "../external/icons";
|
||||||
|
import type { IconName } from "../external/icons";
|
||||||
|
import type { GoalResult } from "../models/goal";
|
||||||
|
import projectStore, { fpProjectStore } from "../stores/project";
|
||||||
|
import { formatFormTime, nextMonth } from "../utils/time";
|
||||||
|
import GroupSelect from "../components/GroupSelect.svelte";
|
||||||
|
|
||||||
|
export let deletion = false;
|
||||||
|
export let creation = false;
|
||||||
|
|
||||||
|
const md = $modalStore;
|
||||||
|
|
||||||
|
let goal: GoalResult = {
|
||||||
|
id: "",
|
||||||
|
groupId: "",
|
||||||
|
startTime: nextMonth(new Date()).toISOString(),
|
||||||
|
endTime: new Date(nextMonth(nextMonth(new Date())).getTime() - 1).toISOString(),
|
||||||
|
amount: 1,
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
completedAmount: 0,
|
||||||
|
group: {id: "", name: "", icon: "question", description: ""},
|
||||||
|
items: [],
|
||||||
|
logs: [],
|
||||||
|
};
|
||||||
|
let verb = "Add";
|
||||||
|
if (md.name === "goal.edit" || md.name === "goal.delete") {
|
||||||
|
goal = md.goal;
|
||||||
|
verb = (md.name === "goal.edit") ? "Edit" : "Delete";
|
||||||
|
} else if (md.name !== "goal.add") {
|
||||||
|
throw new Error("Wrong form")
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = goal.name;
|
||||||
|
let description = goal.description;
|
||||||
|
let groupId = goal.groupId;
|
||||||
|
let amount = goal.amount;
|
||||||
|
let startTime = formatFormTime(goal.startTime);
|
||||||
|
let endTime = formatFormTime(goal.endTime);
|
||||||
|
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
function onSubmit() {
|
||||||
|
if (creation) {
|
||||||
|
stuffLogClient.createGoal({
|
||||||
|
startTime: new Date(startTime),
|
||||||
|
endTime: new Date(endTime),
|
||||||
|
groupId, name, description, amount,
|
||||||
|
}).then(() => {
|
||||||
|
goalStore.markStale();
|
||||||
|
fpGoalStore.markStale();
|
||||||
|
modalStore.close();
|
||||||
|
}).catch(err => {
|
||||||
|
error = err.message ? err.message : err.toString();
|
||||||
|
})
|
||||||
|
} else if (deletion) {
|
||||||
|
stuffLogClient.deleteGoal(goal.id).then(() => {
|
||||||
|
goalStore.markStale();
|
||||||
|
fpGoalStore.markStale();
|
||||||
|
modalStore.close();
|
||||||
|
}).catch(err => {
|
||||||
|
error = err.message ? err.message : err.toString();
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
stuffLogClient.updateGoal(goal.id, {
|
||||||
|
startTime: new Date(startTime),
|
||||||
|
endTime: new Date(endTime),
|
||||||
|
name, description, amount,
|
||||||
|
}).then(() => {
|
||||||
|
goalStore.markStale();
|
||||||
|
fpGoalStore.markStale();
|
||||||
|
modalStore.close();
|
||||||
|
}).catch(err => {
|
||||||
|
error = err.message ? err.message : err.toString();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
error = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClose() {
|
||||||
|
modalStore.close();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal show title="{verb} Goal" error={error} closable on:close={onClose}>
|
||||||
|
<form on:submit|preventDefault={onSubmit}>
|
||||||
|
<label for="name">Name</label>
|
||||||
|
<input disabled={deletion} name="name" type="text" bind:value={name} />
|
||||||
|
<label for="description">Description</label>
|
||||||
|
<textarea disabled={deletion} name="description" bind:value={description} />
|
||||||
|
<label for="groupId">Group</label>
|
||||||
|
<GroupSelect disabled={!creation} name="groupId" bind:value={groupId}/>
|
||||||
|
<label for="amount">Amount</label>
|
||||||
|
<input disabled={deletion} name="amount" type="number" bind:value={amount} />
|
||||||
|
<label for="startTime">Start Time</label>
|
||||||
|
<input disabled={deletion} name="startTime" type="datetime-local" bind:value={startTime} />
|
||||||
|
<label for="endTime">End Time</label>
|
||||||
|
<input disabled={deletion} name="endTime" type="datetime-local" bind:value={endTime} />
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<button type="submit">{verb} Goal</button>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import stuffLogClient from "../clients/stufflog";
|
||||||
|
import Modal from "../components/Modal.svelte";
|
||||||
|
import modalStore from "../stores/modal";
|
||||||
|
import goalStore, { fpGoalStore } from "../stores/goal";
|
||||||
|
import groupStore from "../stores/group";
|
||||||
|
import IconSelect from "../components/IconSelect.svelte";
|
||||||
|
import { DEFAULT_ICON } from "../external/icons";
|
||||||
|
import type { IconName } from "../external/icons";
|
||||||
|
import type { GroupResult } from "../models/group";
|
||||||
|
import projectStore, { fpProjectStore } from "../stores/project";
|
||||||
|
|
||||||
|
export let deletion = false;
|
||||||
|
export let creation = false;
|
||||||
|
|
||||||
|
const md = $modalStore;
|
||||||
|
let group: GroupResult = {
|
||||||
|
id: "",
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
icon: DEFAULT_ICON,
|
||||||
|
items: [],
|
||||||
|
};
|
||||||
|
let verb = "Add";
|
||||||
|
if (md.name === "group.edit" || md.name === "group.delete") {
|
||||||
|
group = md.group;
|
||||||
|
verb = (md.name === "group.edit") ? "Edit" : "Delete";
|
||||||
|
} else if (md.name !== "group.add") {
|
||||||
|
throw new Error("Wrong form")
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = group.name;
|
||||||
|
let description = group.description;
|
||||||
|
let icon = group.icon as IconName;
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
function onSubmit() {
|
||||||
|
if (creation) {
|
||||||
|
stuffLogClient.createGroup({
|
||||||
|
name, description, icon,
|
||||||
|
}).then(() => {
|
||||||
|
groupStore.markStale();
|
||||||
|
modalStore.close();
|
||||||
|
}).catch(err => {
|
||||||
|
error = err.message ? err.message : err.toString();
|
||||||
|
})
|
||||||
|
} else if (deletion) {
|
||||||
|
stuffLogClient.deleteGroup(group.id).then(() => {
|
||||||
|
groupStore.markStale();
|
||||||
|
modalStore.close();
|
||||||
|
}).catch(err => {
|
||||||
|
error = err.message ? err.message : err.toString();
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
stuffLogClient.updateGroup(group.id, {
|
||||||
|
name, description, icon,
|
||||||
|
}).then(() => {
|
||||||
|
groupStore.markStale();
|
||||||
|
goalStore.markStale();
|
||||||
|
fpGoalStore.markStale();
|
||||||
|
projectStore.markStale();
|
||||||
|
fpProjectStore.markStale();
|
||||||
|
modalStore.close();
|
||||||
|
}).catch(err => {
|
||||||
|
error = err.message ? err.message : err.toString();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
error = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClose() {
|
||||||
|
modalStore.close();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal show title="{verb} Group" error={error} closable on:close={onClose}>
|
||||||
|
<form on:submit|preventDefault={onSubmit}>
|
||||||
|
<label for="name">Name</label>
|
||||||
|
<input disabled={deletion} name="name" type="text" bind:value={name} />
|
||||||
|
<label for="description">Description</label>
|
||||||
|
<textarea disabled={deletion} name="description" bind:value={description} />
|
||||||
|
<label for="icon">Icon</label>
|
||||||
|
<IconSelect disabled={deletion} bind:value={icon} />
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<button type="submit">{verb} Group</button>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import stuffLogClient from "../clients/stufflog";
|
||||||
|
import Modal from "../components/Modal.svelte";
|
||||||
|
import modalStore from "../stores/modal";
|
||||||
|
import goalStore, { fpGoalStore } from "../stores/goal";
|
||||||
|
import groupStore from "../stores/group";
|
||||||
|
|
||||||
|
const md = $modalStore;
|
||||||
|
if (md.name !== "item.add") {
|
||||||
|
throw new Error("Wrong form");
|
||||||
|
}
|
||||||
|
|
||||||
|
let group = md.group;
|
||||||
|
let name = "";
|
||||||
|
let description = "";
|
||||||
|
let groupWeight = 1;
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
function onSubmit() {
|
||||||
|
stuffLogClient.createItem({
|
||||||
|
groupId: group.id,
|
||||||
|
|
||||||
|
name, description, groupWeight,
|
||||||
|
}).then(() => {
|
||||||
|
groupStore.markStale();
|
||||||
|
goalStore.markStale();
|
||||||
|
fpGoalStore.markStale();
|
||||||
|
modalStore.close();
|
||||||
|
})
|
||||||
|
|
||||||
|
error = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClose() {
|
||||||
|
modalStore.close();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal show title="Add Item" error={error} closable on:close={onClose}>
|
||||||
|
<form on:submit|preventDefault={onSubmit}>
|
||||||
|
<label for="groupName">Group</label>
|
||||||
|
<input disabled name="groupName" type="text" value={group.name} />
|
||||||
|
<label for="name">Name</label>
|
||||||
|
<input name="name" type="text" bind:value={name} />
|
||||||
|
<label for="description">Description</label>
|
||||||
|
<textarea name="description" bind:value={description} />
|
||||||
|
<label for="groupWeight">Group Weight</label>
|
||||||
|
<input name="groupWeight" type="number" bind:value={groupWeight} />
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<button type="submit">Add Item</button>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import stuffLogClient from "../clients/stufflog";
|
||||||
|
import Modal from "../components/Modal.svelte";
|
||||||
|
import modalStore from "../stores/modal";
|
||||||
|
import groupStore from "../stores/group";
|
||||||
|
|
||||||
|
const md = $modalStore;
|
||||||
|
if (md.name !== "item.delete") {
|
||||||
|
throw new Error("Wrong form");
|
||||||
|
}
|
||||||
|
|
||||||
|
let item = md.item;
|
||||||
|
let group = md.group;
|
||||||
|
let name = item.name;
|
||||||
|
let description = item.description;
|
||||||
|
let groupWeight = item.groupWeight;
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
function onSubmit() {
|
||||||
|
stuffLogClient.deleteItem(item.id).then(() => {
|
||||||
|
groupStore.markStale();
|
||||||
|
modalStore.close();
|
||||||
|
}).catch(err => {
|
||||||
|
error = err.message ? err.message : err.toString();
|
||||||
|
})
|
||||||
|
|
||||||
|
error = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClose() {
|
||||||
|
modalStore.close();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal show title="Edit Item" error={error} closable on:close={onClose}>
|
||||||
|
<form on:submit|preventDefault={onSubmit}>
|
||||||
|
<label for="groupName">Group</label>
|
||||||
|
<input disabled name="groupName" type="text" value={group.name} />
|
||||||
|
<label for="name">Name</label>
|
||||||
|
<input disabled name="name" type="text" value={name} />
|
||||||
|
<label for="description">Description</label>
|
||||||
|
<textarea disabled name="description" value={description} />
|
||||||
|
<label for="groupWeight">Group Weight</label>
|
||||||
|
<input disabled name="groupWeight" type="number" value={groupWeight} />
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<button type="submit">Delete Item</button>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import stuffLogClient from "../clients/stufflog";
|
||||||
|
import Modal from "../components/Modal.svelte";
|
||||||
|
import modalStore from "../stores/modal";
|
||||||
|
import goalStore, { fpGoalStore } from "../stores/goal";
|
||||||
|
import groupStore from "../stores/group";
|
||||||
|
import projectStore, { fpProjectStore } from "../stores/project";
|
||||||
|
|
||||||
|
const md = $modalStore;
|
||||||
|
if (md.name !== "item.edit") {
|
||||||
|
throw new Error("Wrong form");
|
||||||
|
}
|
||||||
|
|
||||||
|
let item = md.item;
|
||||||
|
let group = md.group;
|
||||||
|
let name = item.name;
|
||||||
|
let description = item.description;
|
||||||
|
let groupWeight = item.groupWeight;
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
function onSubmit() {
|
||||||
|
stuffLogClient.updateItem(item.id, {
|
||||||
|
name, description, groupWeight,
|
||||||
|
}).then(() => {
|
||||||
|
groupStore.markStale();
|
||||||
|
goalStore.markStale();
|
||||||
|
fpGoalStore.markStale();
|
||||||
|
projectStore.markStale();
|
||||||
|
fpProjectStore.markStale();
|
||||||
|
modalStore.close();
|
||||||
|
})
|
||||||
|
|
||||||
|
error = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClose() {
|
||||||
|
modalStore.close();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal show title="Edit Item" error={error} closable on:close={onClose}>
|
||||||
|
<form on:submit|preventDefault={onSubmit}>
|
||||||
|
<label for="groupName">Group</label>
|
||||||
|
<input disabled name="groupName" type="text" value={group.name} />
|
||||||
|
<label for="name">Name</label>
|
||||||
|
<input name="name" type="text" bind:value={name} />
|
||||||
|
<label for="description">Description</label>
|
||||||
|
<textarea name="description" bind:value={description} />
|
||||||
|
<label for="groupWeight">Group Weight</label>
|
||||||
|
<input name="groupWeight" type="number" bind:value={groupWeight} />
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<button type="submit">Edit Item</button>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import stuffLogClient from "../clients/stufflog";
|
||||||
|
import Modal from "../components/Modal.svelte";
|
||||||
|
import goalStore, { fpGoalStore } from "../stores/goal";
|
||||||
|
import logStore from "../stores/logs";
|
||||||
|
import modalStore from "../stores/modal";
|
||||||
|
import projectStore, { fpProjectStore } from "../stores/project";
|
||||||
|
import { formatFormTime } from "../utils/time";
|
||||||
|
|
||||||
|
let loggedTime = formatFormTime(new Date);
|
||||||
|
let taskName = "";
|
||||||
|
let description = "";
|
||||||
|
let markInactive = false;
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
function onSubmit() {
|
||||||
|
const md = $modalStore;
|
||||||
|
if (md.name !== "log.add") {
|
||||||
|
throw new Error("Wrong form");
|
||||||
|
}
|
||||||
|
|
||||||
|
stuffLogClient.createLog({
|
||||||
|
taskId: md.task.id,
|
||||||
|
loggedTime: new Date(loggedTime).toISOString(),
|
||||||
|
description,
|
||||||
|
}).then(() => {
|
||||||
|
if (markInactive) {
|
||||||
|
return stuffLogClient.updateTask(md.task.id, {active: false})
|
||||||
|
}
|
||||||
|
}).then(() => {
|
||||||
|
modalStore.close();
|
||||||
|
}).finally(() => {
|
||||||
|
projectStore.markStale();
|
||||||
|
fpProjectStore.markStale();
|
||||||
|
goalStore.markStale();
|
||||||
|
fpGoalStore.markStale();
|
||||||
|
logStore.markStale();
|
||||||
|
})
|
||||||
|
|
||||||
|
error = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClose() {
|
||||||
|
modalStore.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
$: {
|
||||||
|
const md = $modalStore;
|
||||||
|
if (md.name === "log.add") {
|
||||||
|
taskName = md.task.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal show title="Add Log" error={error} closable on:close={onClose}>
|
||||||
|
<form on:submit|preventDefault={onSubmit}>
|
||||||
|
<label for="taskName">Task</label>
|
||||||
|
<input disabled name="taskName" type="text" bind:value={taskName} />
|
||||||
|
|
||||||
|
<label for="loggedTime">Logged Time</label>
|
||||||
|
<input name="loggedTime" type="datetime-local" bind:value={loggedTime} />
|
||||||
|
<label for="description">Description</label>
|
||||||
|
<textarea name="description" bind:value={description} />
|
||||||
|
<input id="markInactive" type="checkbox" bind:checked={markInactive} />
|
||||||
|
<label for="markInactive">Complete Task</label>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<button type="submit">Add Log</button>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import stuffLogClient from "../clients/stufflog";
|
||||||
|
import Modal from "../components/Modal.svelte";
|
||||||
|
import modalStore from "../stores/modal";
|
||||||
|
import goalStore, { fpGoalStore } from "../stores/goal";
|
||||||
|
import projectStore, { fpProjectStore } from "../stores/project";
|
||||||
|
import { formatFormTime } from "../utils/time";
|
||||||
|
import logStore from "../stores/logs";
|
||||||
|
|
||||||
|
const md = $modalStore;
|
||||||
|
if (md.name !== "log.delete") {
|
||||||
|
throw new Error("Wrong form");
|
||||||
|
}
|
||||||
|
|
||||||
|
let loggedTime = formatFormTime(new Date(md.log.loggedTime));
|
||||||
|
let description = md.log.description;
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
function onSubmit() {
|
||||||
|
const md = $modalStore;
|
||||||
|
if (md.name !== "log.delete") {
|
||||||
|
throw new Error("Wrong form");
|
||||||
|
}
|
||||||
|
|
||||||
|
stuffLogClient.deleteLog(md.log.id).then(() => {
|
||||||
|
projectStore.markStale();
|
||||||
|
fpProjectStore.markStale();
|
||||||
|
goalStore.markStale();
|
||||||
|
fpGoalStore.markStale();
|
||||||
|
logStore.markStale();
|
||||||
|
|
||||||
|
modalStore.close();
|
||||||
|
})
|
||||||
|
|
||||||
|
error = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClose() {
|
||||||
|
modalStore.close();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal show title="Delete Log" error={error} closable on:close={onClose}>
|
||||||
|
<form on:submit|preventDefault={onSubmit}>
|
||||||
|
<label for="taskName">Task</label>
|
||||||
|
<input disabled name="taskName" type="text" bind:value={md.log.task.name} />
|
||||||
|
<label for="loggedTime">Logged Time</label>
|
||||||
|
<input disabled name="loggedTime" type="datetime-local" bind:value={loggedTime} />
|
||||||
|
<label for="description">Description</label>
|
||||||
|
<textarea disabled name="description" bind:value={description} />
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<button type="submit">Delete Log</button>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import stuffLogClient from "../clients/stufflog";
|
||||||
|
import Modal from "../components/Modal.svelte";
|
||||||
|
import modalStore from "../stores/modal";
|
||||||
|
import goalStore, { fpGoalStore } from "../stores/goal";
|
||||||
|
import projectStore, { fpProjectStore } from "../stores/project";
|
||||||
|
import { formatFormTime } from "../utils/time";
|
||||||
|
import logStore from "../stores/logs";
|
||||||
|
|
||||||
|
const md = $modalStore;
|
||||||
|
if (md.name !== "log.edit") {
|
||||||
|
throw new Error("Wrong form");
|
||||||
|
}
|
||||||
|
|
||||||
|
let loggedTime = formatFormTime(new Date(md.log.loggedTime));
|
||||||
|
let description = md.log.description;
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
function onSubmit() {
|
||||||
|
const md = $modalStore;
|
||||||
|
if (md.name !== "log.edit") {
|
||||||
|
throw new Error("Wrong form");
|
||||||
|
}
|
||||||
|
|
||||||
|
stuffLogClient.updateLog(md.log.id, {
|
||||||
|
loggedTime: new Date(loggedTime).toISOString(),
|
||||||
|
description,
|
||||||
|
}).then(() => {
|
||||||
|
projectStore.markStale();
|
||||||
|
fpProjectStore.markStale();
|
||||||
|
goalStore.markStale();
|
||||||
|
fpGoalStore.markStale();
|
||||||
|
logStore.markStale();
|
||||||
|
|
||||||
|
modalStore.close();
|
||||||
|
})
|
||||||
|
|
||||||
|
error = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClose() {
|
||||||
|
modalStore.close();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal show title="Edit Log" error={error} closable on:close={onClose}>
|
||||||
|
<form on:submit|preventDefault={onSubmit}>
|
||||||
|
<label for="taskName">Task</label>
|
||||||
|
<input disabled name="taskName" type="text" bind:value={md.log.task.name} />
|
||||||
|
<label for="loggedTime">Logged Time</label>
|
||||||
|
<input name="loggedTime" type="datetime-local" bind:value={loggedTime} />
|
||||||
|
<label for="description">Description</label>
|
||||||
|
<textarea name="description" bind:value={description} />
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<button type="submit">Edit Log</button>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type {CognitoUser} from "amazon-cognito-identity-js";
|
||||||
|
|
||||||
|
import { signIn } from "../clients/amplify";
|
||||||
|
import authStore from "../stores/auth";
|
||||||
|
import Modal from "../components/Modal.svelte";
|
||||||
|
|
||||||
|
let user: CognitoUser | null = null;
|
||||||
|
let username = "";
|
||||||
|
let password = "";
|
||||||
|
let newPassword = "";
|
||||||
|
let newPasswordRepeat = "";
|
||||||
|
let settingNewPassword = false;
|
||||||
|
let error = null;
|
||||||
|
let done = false;
|
||||||
|
|
||||||
|
function login() {
|
||||||
|
error = null;
|
||||||
|
|
||||||
|
if (settingNewPassword) {
|
||||||
|
if (newPasswordRepeat !== newPassword) {
|
||||||
|
error = "New passwords do not match.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
user.completeNewPasswordChallenge(newPassword, null, {
|
||||||
|
onSuccess: () => {
|
||||||
|
done = true;
|
||||||
|
authStore.check();
|
||||||
|
},
|
||||||
|
onFailure: err => {
|
||||||
|
error = err
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
signIn(username, password).then(newUser => {
|
||||||
|
if (!newUser) {
|
||||||
|
error = "Incorrect username or password."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((newUser as any).challengeName === "NEW_PASSWORD_REQUIRED") {
|
||||||
|
error = "Password is expired, please update it."
|
||||||
|
settingNewPassword = true;
|
||||||
|
} else {
|
||||||
|
authStore.check();
|
||||||
|
done = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
user = newUser;
|
||||||
|
}).catch(err => {
|
||||||
|
error = err
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal show={!done} title="Login" error={error}>
|
||||||
|
<form on:submit|preventDefault={login}>
|
||||||
|
<label for="username">Username</label>
|
||||||
|
<input name="username" type="text" bind:value={username} />
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input name="password" type="password" bind:value={password} />
|
||||||
|
{#if settingNewPassword}
|
||||||
|
<label for="newPassword">New Password</label>
|
||||||
|
<input name="newPassword" type="password" bind:value={newPassword} />
|
||||||
|
<label for="newPasswordRepeat">New Password</label>
|
||||||
|
<input name="newPasswordRepeat" type="password" bind:value={newPasswordRepeat} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
<button type="submit">Login</button>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import stuffLogClient from "../clients/stufflog";
|
||||||
|
import IconSelect from "../components/IconSelect.svelte";
|
||||||
|
import Modal from "../components/Modal.svelte";
|
||||||
|
import { iconNames } from "../external/icons";
|
||||||
|
import modalStore from "../stores/modal";
|
||||||
|
import projectStore, { fpProjectStore } from "../stores/project";
|
||||||
|
|
||||||
|
let endTime = "";
|
||||||
|
let name = "";
|
||||||
|
let description = "";
|
||||||
|
let icon = iconNames[0];
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
function onSubmit() {
|
||||||
|
stuffLogClient.createProject({
|
||||||
|
active: true,
|
||||||
|
endTime: ( endTime == "" ) ? null : new Date(endTime),
|
||||||
|
|
||||||
|
name, description, icon,
|
||||||
|
}).then(() => {
|
||||||
|
projectStore.markStale();
|
||||||
|
if (endTime !== "") {
|
||||||
|
fpProjectStore.markStale();
|
||||||
|
}
|
||||||
|
modalStore.close();
|
||||||
|
}).catch(err => {
|
||||||
|
error = err.message ? err.message : err.toString();
|
||||||
|
})
|
||||||
|
|
||||||
|
error = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClose() {
|
||||||
|
modalStore.close();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal show title="Add Project" error={error} closable on:close={onClose}>
|
||||||
|
<form on:submit|preventDefault={onSubmit}>
|
||||||
|
<label for="name">Name</label>
|
||||||
|
<input name="name" type="text" bind:value={name} />
|
||||||
|
<label for="description">Description</label>
|
||||||
|
<textarea name="description" bind:value={description} />
|
||||||
|
<label for="itemId">Icon</label>
|
||||||
|
<IconSelect bind:value={icon} />
|
||||||
|
<label for="endTime">Deadline (Optional)</label>
|
||||||
|
<input name="endTime" type="datetime-local" bind:value={endTime} />
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<button type="submit">Add Project</button>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import stuffLogClient from "../clients/stufflog";
|
||||||
|
import IconSelect from "../components/IconSelect.svelte";
|
||||||
|
import Modal from "../components/Modal.svelte";
|
||||||
|
import type { IconName } from "../external/icons";
|
||||||
|
import modalStore from "../stores/modal";
|
||||||
|
import projectStore, { fpProjectStore } from "../stores/project";
|
||||||
|
import { formatFormTime } from "../utils/time";
|
||||||
|
|
||||||
|
const md = $modalStore;
|
||||||
|
if (md.name !== "project.delete") {
|
||||||
|
throw new Error("Wrong form");
|
||||||
|
}
|
||||||
|
const project = md.project;
|
||||||
|
|
||||||
|
let endTime = project.endTime ? formatFormTime(project.endTime) : "";
|
||||||
|
let name = project.name;
|
||||||
|
let description = project.description;
|
||||||
|
let icon = project.icon as IconName;
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
function onSubmit() {
|
||||||
|
stuffLogClient.deleteProject(project.id).then(() => {
|
||||||
|
projectStore.markStale();
|
||||||
|
if (endTime !== "") {
|
||||||
|
fpProjectStore.markStale();
|
||||||
|
}
|
||||||
|
modalStore.close();
|
||||||
|
}).catch(err => {
|
||||||
|
error = err.message ? err.message : err.toString();
|
||||||
|
})
|
||||||
|
|
||||||
|
error = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClose() {
|
||||||
|
modalStore.close();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal show title="Delete Project" error={error} closable on:close={onClose}>
|
||||||
|
<form on:submit|preventDefault={onSubmit}>
|
||||||
|
<label for="name">Name</label>
|
||||||
|
<input disabled name="name" type="text" value={name} />
|
||||||
|
<label for="description">Description</label>
|
||||||
|
<textarea disabled name="description" value={description} />
|
||||||
|
<label for="itemId">Icon</label>
|
||||||
|
<IconSelect disabled value={icon} />
|
||||||
|
<label for="endTime">Deadline (Optional)</label>
|
||||||
|
<input disabled name="endTime" type="datetime-local" value={endTime} />
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<button type="submit">Delete Project</button>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import stuffLogClient from "../clients/stufflog";
|
||||||
|
import IconSelect from "../components/IconSelect.svelte";
|
||||||
|
import Modal from "../components/Modal.svelte";
|
||||||
|
import type { IconName } from "../external/icons";
|
||||||
|
import modalStore from "../stores/modal";
|
||||||
|
import projectStore, { fpProjectStore } from "../stores/project";
|
||||||
|
import { formatFormTime } from "../utils/time";
|
||||||
|
|
||||||
|
const md = $modalStore;
|
||||||
|
if (md.name !== "project.edit") {
|
||||||
|
throw new Error("Wrong form");
|
||||||
|
}
|
||||||
|
const project = md.project;
|
||||||
|
|
||||||
|
let endTime = project.endTime ? formatFormTime(project.endTime) : "";
|
||||||
|
let name = project.name;
|
||||||
|
let description = project.description;
|
||||||
|
let icon = project.icon as IconName;
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
function onSubmit() {
|
||||||
|
stuffLogClient.updateProject(project.id, {
|
||||||
|
active: true,
|
||||||
|
endTime: ( endTime == "" ) ? null : new Date(endTime),
|
||||||
|
clearEndTime: ( endTime == "" ),
|
||||||
|
|
||||||
|
name, description, icon,
|
||||||
|
}).then(() => {
|
||||||
|
projectStore.markStale();
|
||||||
|
fpProjectStore.markStale();
|
||||||
|
modalStore.close();
|
||||||
|
}).catch(err => {
|
||||||
|
error = err.message ? err.message : err.toString();
|
||||||
|
})
|
||||||
|
|
||||||
|
error = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClose() {
|
||||||
|
modalStore.close();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal show title="Edit Project" error={error} closable on:close={onClose}>
|
||||||
|
<form on:submit|preventDefault={onSubmit}>
|
||||||
|
<label for="name">Name</label>
|
||||||
|
<input name="name" type="text" bind:value={name} />
|
||||||
|
<label for="description">Description</label>
|
||||||
|
<textarea name="description" bind:value={description} />
|
||||||
|
<label for="itemId">Icon</label>
|
||||||
|
<IconSelect bind:value={icon} />
|
||||||
|
<label for="endTime">Deadline (Optional)</label>
|
||||||
|
<input name="endTime" type="datetime-local" bind:value={endTime} />
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<button type="submit">Edit Project</button>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import stuffLogClient from "../clients/stufflog";
|
||||||
|
import ItemSelect from "../components/ItemSelect.svelte";
|
||||||
|
import Modal from "../components/Modal.svelte";
|
||||||
|
import type { ProjectResult } from "../models/project";
|
||||||
|
import modalStore from "../stores/modal";
|
||||||
|
import projectStore, { fpProjectStore } from "../stores/project";
|
||||||
|
|
||||||
|
let project: ProjectResult
|
||||||
|
let endTime = "";
|
||||||
|
let itemId = "";
|
||||||
|
let name = "";
|
||||||
|
let description = "";
|
||||||
|
let itemAmount = 1;
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
function onSubmit() {
|
||||||
|
stuffLogClient.createTask({
|
||||||
|
projectId: project.id,
|
||||||
|
itemId: itemId,
|
||||||
|
active: true,
|
||||||
|
endTime: ( endTime == "") ? null : new Date(endTime),
|
||||||
|
|
||||||
|
name, description, itemAmount,
|
||||||
|
}).then(() => {
|
||||||
|
projectStore.markStale();
|
||||||
|
fpProjectStore.markStale();
|
||||||
|
modalStore.close();
|
||||||
|
})
|
||||||
|
|
||||||
|
error = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClose() {
|
||||||
|
modalStore.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
$: {
|
||||||
|
const md = $modalStore;
|
||||||
|
if (md.name !== "task.add") {
|
||||||
|
throw new Error("Wrong form");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itemId === "") {
|
||||||
|
project = md.project;
|
||||||
|
if (project.tasks.length > 0) {
|
||||||
|
itemId = project.tasks[0].itemId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal show title="Add Task" error={error} closable on:close={onClose}>
|
||||||
|
<form on:submit|preventDefault={onSubmit}>
|
||||||
|
<label for="projectName">Project</label>
|
||||||
|
<input disabled name="projectName" type="text" value={project.name} />
|
||||||
|
<label for="name">Name</label>
|
||||||
|
<input name="name" type="text" bind:value={name} />
|
||||||
|
<label for="description">Description</label>
|
||||||
|
<textarea name="description" bind:value={description} />
|
||||||
|
<label for="itemId">Item {itemId}</label>
|
||||||
|
<ItemSelect name="itemId" bind:value={itemId} />
|
||||||
|
<label for="itemAmount">Amount</label>
|
||||||
|
<input name="itemAmount" type="number" bind:value={itemAmount} />
|
||||||
|
<label for="endTime">Deadline (Optional)</label>
|
||||||
|
<input name="endTime" type="datetime-local" bind:value={endTime} />
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<button type="submit">Add Task</button>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import stuffLogClient from "../clients/stufflog";
|
||||||
|
import Modal from "../components/Modal.svelte";
|
||||||
|
import goalStore, { fpGoalStore } from "../stores/goal";
|
||||||
|
import modalStore from "../stores/modal";
|
||||||
|
import projectStore, { fpProjectStore } from "../stores/project";
|
||||||
|
import { formatFormTime } from "../utils/time";
|
||||||
|
|
||||||
|
const md = $modalStore;
|
||||||
|
if (md.name !== "task.delete") {
|
||||||
|
throw new Error("Wrong form");
|
||||||
|
}
|
||||||
|
|
||||||
|
let task = md.task
|
||||||
|
let name = task.name;
|
||||||
|
let description = task.description;
|
||||||
|
let itemAmount = task.itemAmount;
|
||||||
|
let active = task.active;
|
||||||
|
let endTime = task.endTime ? formatFormTime(task.endTime) : "";
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
function onSubmit() {
|
||||||
|
stuffLogClient.deleteTask(task.id).then(() => {
|
||||||
|
projectStore.markStale();
|
||||||
|
fpProjectStore.markStale();
|
||||||
|
goalStore.markStale();
|
||||||
|
fpGoalStore.markStale();
|
||||||
|
modalStore.close();
|
||||||
|
}).catch(err => {
|
||||||
|
error = err.message ? err.message : err.toString();
|
||||||
|
})
|
||||||
|
|
||||||
|
error = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClose() {
|
||||||
|
modalStore.close();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal show title="Delete Task" error={error} closable on:close={onClose}>
|
||||||
|
<form on:submit|preventDefault={onSubmit}>
|
||||||
|
<label for="name">Name</label>
|
||||||
|
<input disabled name="name" type="text" value={name} />
|
||||||
|
<label for="description">Description</label>
|
||||||
|
<textarea disabled name="description" value={description} />
|
||||||
|
<label for="name">Item</label>
|
||||||
|
<input disabled name="name" type="text" value={task.item.name} />
|
||||||
|
<label for="itemAmount">Amount</label>
|
||||||
|
<input disabled name="itemAmount" type="number" value={itemAmount} />
|
||||||
|
<label for="endTime">Deadline (Optional)</label>
|
||||||
|
<input disabled name="endTime" type="datetime-local" value={endTime} />
|
||||||
|
<input id="active" type="checkbox" checked={active} />
|
||||||
|
<label for="active">Task is active/incomplete</label>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<button type="submit">Delete1 Task</button>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import stuffLogClient from "../clients/stufflog";
|
||||||
|
import Modal from "../components/Modal.svelte";
|
||||||
|
import goalStore, { fpGoalStore } from "../stores/goal";
|
||||||
|
import modalStore from "../stores/modal";
|
||||||
|
import projectStore, { fpProjectStore } from "../stores/project";
|
||||||
|
import { formatFormTime } from "../utils/time";
|
||||||
|
|
||||||
|
const md = $modalStore;
|
||||||
|
if (md.name !== "task.edit") {
|
||||||
|
throw new Error("Wrong form");
|
||||||
|
}
|
||||||
|
|
||||||
|
let task = md.task
|
||||||
|
let name = task.name;
|
||||||
|
let description = task.description;
|
||||||
|
let itemAmount = task.itemAmount;
|
||||||
|
let active = task.active;
|
||||||
|
let endTime = task.endTime ? formatFormTime(task.endTime) : "";
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
function onSubmit() {
|
||||||
|
stuffLogClient.updateTask(task.id, {
|
||||||
|
endTime: (endTime == "") ? null : new Date(endTime),
|
||||||
|
clearEndTime: endTime == "",
|
||||||
|
|
||||||
|
name, description, itemAmount, active,
|
||||||
|
}).then(() => {
|
||||||
|
projectStore.markStale();
|
||||||
|
fpProjectStore.markStale();
|
||||||
|
goalStore.markStale();
|
||||||
|
fpGoalStore.markStale();
|
||||||
|
modalStore.close();
|
||||||
|
})
|
||||||
|
|
||||||
|
error = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClose() {
|
||||||
|
modalStore.close();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal show title="Add Task" error={error} closable on:close={onClose}>
|
||||||
|
<form on:submit|preventDefault={onSubmit}>
|
||||||
|
<label for="name">Name</label>
|
||||||
|
<input name="name" type="text" bind:value={name} />
|
||||||
|
<label for="description">Description</label>
|
||||||
|
<textarea name="description" bind:value={description} />
|
||||||
|
<label for="name">Item</label>
|
||||||
|
<input disabled name="name" type="text" value={task.item.name} />
|
||||||
|
<label for="itemAmount">Amount</label>
|
||||||
|
<input name="itemAmount" type="number" bind:value={itemAmount} />
|
||||||
|
<label for="endTime">Deadline (Optional)</label>
|
||||||
|
<input name="endTime" type="datetime-local" bind:value={endTime} />
|
||||||
|
<input id="active" type="checkbox" bind:checked={active} />
|
||||||
|
<label for="active">Task is active/incomplete</label>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<button type="submit">Add Task</button>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user