Browse Source

first commit

main
Gisle Aune 4 years ago
commit
2240985aa1
  1. 99
      .drone.yml
  2. 2
      .gitignore
  3. 21
      api/common.go
  4. 141
      api/goal.go
  5. 90
      api/group.go
  6. 98
      api/item.go
  7. 115
      api/log.go
  8. 101
      api/project.go
  9. 111
      api/task.go
  10. 51
      cmd/stufflog2-lambda/main.go
  11. 74
      cmd/stufflog2-local/main.go
  12. 28
      database/database.go
  13. 51
      database/postgres/db.go
  14. 115
      database/postgres/goals.go
  15. 96
      database/postgres/group.go
  16. 100
      database/postgres/item.go
  17. 108
      database/postgres/logs.go
  18. 104
      database/postgres/project.go
  19. 109
      database/postgres/tasks.go
  20. 20
      go.mod
  21. 333
      go.sum
  22. 71
      internal/auth/auth.go
  23. 52
      internal/generate/ids.go
  24. 18
      internal/slerrors/badrequest.go
  25. 22
      internal/slerrors/forbidden.go
  26. 35
      internal/slerrors/gin.go
  27. 18
      internal/slerrors/notfound.go
  28. 15
      migrations/postgres/20201218170259_create_table_group.sql
  29. 16
      migrations/postgres/20201218170301_create_table_item.sql
  30. 18
      migrations/postgres/20201218170319_create_table_project.sql
  31. 20
      migrations/postgres/20201218170338_create_table_task.sql
  32. 16
      migrations/postgres/20201218170348_create_table_log.sql
  33. 18
      migrations/postgres/20201218170417_create_table_goal.sql
  34. 9
      migrations/postgres/20201223121327_create_index_item_group_id.sql
  35. 9
      migrations/postgres/20201223125438_create_index_log_task_id.sql
  36. 9
      migrations/postgres/20201223125556_create_index_goal_start_time.sql
  37. 9
      migrations/postgres/20201223125559_create_index_goal_end_time.sql
  38. 9
      migrations/postgres/20201223125934_create_index_group_user_id.sql
  39. 9
      migrations/postgres/20201223125938_create_index_item_user_id.sql
  40. 9
      migrations/postgres/20201223125947_create_index_project_user_id.sql
  41. 9
      migrations/postgres/20201223125957_create_index_task_user_id.sql
  42. 9
      migrations/postgres/20201223130003_create_index_log_user_id.sql
  43. 9
      migrations/postgres/20201223130007_create_index_goal_user_id.sql
  44. 9
      migrations/postgres/20201223135724_create_index_project_created_time.sql
  45. 9
      migrations/postgres/20201223135812_create_index_task_item_id.sql
  46. 9
      migrations/postgres/20201223140113_create_index_log_logged_time.sql
  47. 9
      migrations/postgres/20201225175922_create_index_log_item_id.sql
  48. 73
      models/goal.go
  49. 47
      models/group.go
  50. 50
      models/item.go
  51. 50
      models/log.go
  52. 68
      models/project.go
  53. 74
      models/task.go
  54. 115
      serverless.yml
  55. 508
      services/loader.go
  56. 7
      svelte-ui/.gitignore
  57. 4368
      svelte-ui/package-lock.json
  58. 43
      svelte-ui/package.json
  59. BIN
      svelte-ui/public/favicon.png
  60. 62
      svelte-ui/public/global.css
  61. 17
      svelte-ui/public/index.html
  62. 99
      svelte-ui/rollup.config.js
  63. 77
      svelte-ui/src/App.svelte
  64. 46
      svelte-ui/src/clients/amplify.ts
  65. 260
      svelte-ui/src/clients/stufflog.ts
  66. 53
      svelte-ui/src/components/Boi.svelte
  67. 17
      svelte-ui/src/components/DateSpan.svelte
  68. 87
      svelte-ui/src/components/DaysLeft.svelte
  69. 93
      svelte-ui/src/components/GoalEntry.svelte
  70. 84
      svelte-ui/src/components/GroupEntry.svelte
  71. 30
      svelte-ui/src/components/GroupSelect.svelte
  72. 22
      svelte-ui/src/components/Icon.svelte
  73. 56
      svelte-ui/src/components/IconSelect.svelte
  74. 86
      svelte-ui/src/components/ItemEntry.svelte
  75. 31
      svelte-ui/src/components/ItemSelect.svelte
  76. 95
      svelte-ui/src/components/LogEntry.svelte
  77. 44
      svelte-ui/src/components/Menu.svelte
  78. 215
      svelte-ui/src/components/Modal.svelte
  79. 10
      svelte-ui/src/components/ModalRoute.svelte
  80. 38
      svelte-ui/src/components/Option.svelte
  81. 10
      svelte-ui/src/components/OptionRow.svelte
  82. 80
      svelte-ui/src/components/Progress.svelte
  83. 94
      svelte-ui/src/components/ProjectEntry.svelte
  84. 161
      svelte-ui/src/components/TaskEntry.svelte
  85. 138
      svelte-ui/src/external/icons.ts
  86. 112
      svelte-ui/src/forms/GoalForm.svelte
  87. 90
      svelte-ui/src/forms/GroupForm.svelte
  88. 54
      svelte-ui/src/forms/ItemAddForm.svelte
  89. 50
      svelte-ui/src/forms/ItemDeleteForm.svelte
  90. 56
      svelte-ui/src/forms/ItemEditForm.svelte
  91. 71
      svelte-ui/src/forms/LogAddForm.svelte
  92. 56
      svelte-ui/src/forms/LogDeleteForm.svelte
  93. 59
      svelte-ui/src/forms/LogEditForm.svelte
  94. 74
      svelte-ui/src/forms/LoginForm.svelte
  95. 54
      svelte-ui/src/forms/ProjectAddForm.svelte
  96. 56
      svelte-ui/src/forms/ProjectDeleteForm.svelte
  97. 60
      svelte-ui/src/forms/ProjectEditForm.svelte
  98. 72
      svelte-ui/src/forms/TaskAddForm.svelte
  99. 60
      svelte-ui/src/forms/TaskDeleteForm.svelte
  100. 63
      svelte-ui/src/forms/TaskEditForm.svelte

99
.drone.yml

@ -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

2
.gitignore

@ -0,0 +1,2 @@
/stufflog2-*
/.idea

21
api/common.go

@ -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
api/goal.go

@ -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
}))
}

90
api/group.go

@ -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
api/item.go

@ -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
api/log.go

@ -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
api/project.go

@ -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
api/task.go

@ -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
}))
}

51
cmd/stufflog2-lambda/main.go

@ -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)
})
}

74
cmd/stufflog2-local/main.go

@ -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)
}
}
}

28
database/database.go

@ -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
}
}

51
database/postgres/db.go

@ -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
}

115
database/postgres/goals.go

@ -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
}

96
database/postgres/group.go

@ -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
}

100
database/postgres/item.go

@ -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
}

108
database/postgres/logs.go

@ -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
}

104
database/postgres/project.go

@ -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
}

109
database/postgres/tasks.go

@ -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
}

20
go.mod

@ -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
)

333
go.sum

@ -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=

71
internal/auth/auth.go

@ -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)
}
}
}

52
internal/generate/ids.go

@ -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)
}

18
internal/slerrors/badrequest.go

@ -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
}

22
internal/slerrors/forbidden.go

@ -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
}

35
internal/slerrors/gin.go

@ -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(),
})
}
}

18
internal/slerrors/notfound.go

@ -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 &notFoundError{Subject: subject}
}
func IsNotFound(err error) bool {
_, ok := err.(*notFoundError)
return ok
}

15
migrations/postgres/20201218170259_create_table_group.sql

@ -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

16
migrations/postgres/20201218170301_create_table_item.sql

@ -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

18
migrations/postgres/20201218170319_create_table_project.sql

@ -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

20
migrations/postgres/20201218170338_create_table_task.sql

@ -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

16
migrations/postgres/20201218170348_create_table_log.sql

@ -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

18
migrations/postgres/20201218170417_create_table_goal.sql

@ -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

9
migrations/postgres/20201223121327_create_index_item_group_id.sql

@ -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

9
migrations/postgres/20201223125438_create_index_log_task_id.sql

@ -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

9
migrations/postgres/20201223125556_create_index_goal_start_time.sql

@ -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

9
migrations/postgres/20201223125559_create_index_goal_end_time.sql

@ -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

9
migrations/postgres/20201223125934_create_index_group_user_id.sql

@ -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

9
migrations/postgres/20201223125938_create_index_item_user_id.sql

@ -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

9
migrations/postgres/20201223125947_create_index_project_user_id.sql

@ -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

9
migrations/postgres/20201223125957_create_index_task_user_id.sql

@ -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

9
migrations/postgres/20201223130003_create_index_log_user_id.sql

@ -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

9
migrations/postgres/20201223130007_create_index_goal_user_id.sql

@ -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

9
migrations/postgres/20201223135724_create_index_project_created_time.sql

@ -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

9
migrations/postgres/20201223135812_create_index_task_item_id.sql

@ -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

9
migrations/postgres/20201223140113_create_index_log_logged_time.sql

@ -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

9
migrations/postgres/20201225175922_create_index_log_item_id.sql

@ -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

73
models/goal.go

@ -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
}

47
models/group.go

@ -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
}

50
models/item.go

@ -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
}

50
models/log.go

@ -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
}

68
models/project.go

@ -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
}

74
models/task.go

@ -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
serverless.yml

@ -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

508
services/loader.go

@ -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
}

7
svelte-ui/.gitignore

@ -0,0 +1,7 @@
/node_modules/
/public/build/
/build.env
/.idea
/.vscode
.DS_Store

4368
svelte-ui/package-lock.json
File diff suppressed because it is too large
View File

43
svelte-ui/package.json

@ -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"
}
}

BIN
svelte-ui/public/favicon.png

After

Width: 128  |  Height: 128  |  Size: 3.1 KiB

62
svelte-ui/public/global.css

@ -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;
}

17
svelte-ui/public/index.html

@ -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>

99
svelte-ui/rollup.config.js

@ -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 );
}
};

77
svelte-ui/src/App.svelte

@ -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>

46
svelte-ui/src/clients/amplify.ts

@ -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());
}

260
svelte-ui/src/clients/stufflog.ts

@ -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

53
svelte-ui/src/components/Boi.svelte

@ -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>

17
svelte-ui/src/components/DateSpan.svelte

@ -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>

87
svelte-ui/src/components/DaysLeft.svelte

@ -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>

93
svelte-ui/src/components/GoalEntry.svelte

@ -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>

84
svelte-ui/src/components/GroupEntry.svelte

@ -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>

30
svelte-ui/src/components/GroupSelect.svelte

@ -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>

22
svelte-ui/src/components/Icon.svelte

@ -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>

56
svelte-ui/src/components/IconSelect.svelte

@ -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>

86
svelte-ui/src/components/ItemEntry.svelte

@ -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>

31
svelte-ui/src/components/ItemSelect.svelte

@ -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>

95
svelte-ui/src/components/LogEntry.svelte

@ -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>

44
svelte-ui/src/components/Menu.svelte

@ -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>

215
svelte-ui/src/components/Modal.svelte

@ -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>

10
svelte-ui/src/components/ModalRoute.svelte

@ -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}

38
svelte-ui/src/components/Option.svelte

@ -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>

10
svelte-ui/src/components/OptionRow.svelte

@ -0,0 +1,10 @@
<div class="option-row">
<slot></slot>
</div>
<style>
div.option-row {
margin-left: -0.5ch;
margin-right: -0.5ch;
}
</style>

80
svelte-ui/src/components/Progress.svelte

@ -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>

94
svelte-ui/src/components/ProjectEntry.svelte

@ -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>

161
svelte-ui/src/components/TaskEntry.svelte

@ -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>

138
svelte-ui/src/external/icons.ts

@ -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;

112
svelte-ui/src/forms/GoalForm.svelte

@ -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>

90
svelte-ui/src/forms/GroupForm.svelte

@ -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>

54
svelte-ui/src/forms/ItemAddForm.svelte

@ -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>

50
svelte-ui/src/forms/ItemDeleteForm.svelte

@ -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>

56
svelte-ui/src/forms/ItemEditForm.svelte

@ -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>

71
svelte-ui/src/forms/LogAddForm.svelte

@ -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>

56
svelte-ui/src/forms/LogDeleteForm.svelte

@ -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>

59
svelte-ui/src/forms/LogEditForm.svelte

@ -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>

74
svelte-ui/src/forms/LoginForm.svelte

@ -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>

54
svelte-ui/src/forms/ProjectAddForm.svelte

@ -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>

56
svelte-ui/src/forms/ProjectDeleteForm.svelte

@ -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>

60
svelte-ui/src/forms/ProjectEditForm.svelte

@ -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>

72
svelte-ui/src/forms/TaskAddForm.svelte

@ -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>

60
svelte-ui/src/forms/TaskDeleteForm.svelte

@ -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>

63
svelte-ui/src/forms/TaskEditForm.svelte

@ -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 changed in this diff

Loading…
Cancel
Save